Repository: nicklaw5/helix
Pull request #182 (2023-02-16 09:39:55)

Add SearchCategories to find game categories
Add ability to search categories by query string to return like categories from twitch https://dev.twitch.tv/docs/api/reference/#search-categories


Patch:
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index d6ec56f..37c0ab5 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -102,7 +102,7 @@
 - [ ] Create Channel Stream Schedule Segment
 - [ ] Update Channel Stream Schedule Segment
 - [ ] Delete Channel Stream Schedule Segment
-- [ ] Search Categories
+- [x] Search Categories
 - [ ] Search Channels
 - [x] Get Stream Key
 - [x] Get Streams
diff --git a/categories.go b/categories.go
new file mode 100644
index 0000000..f4df2ba
--- /dev/null
+++ b/categories.go
@@ -0,0 +1,42 @@
+package helix
+
+// SearchCategoriesParams is parameters for SearchCategories
+type SearchCategoriesParams struct {
+	Query string `query:"query"`
+	After string `query:"after"`
+	First int    `query:"first,20"` // Limit 100
+}
+
+// ManySearchCategories is the response data from SearchCategories
+type ManySearchCategories struct {
+	Categories []Category `json:"data"`
+	Pagination Pagination `json:"pagination"`
+}
+
+// Category describes a category from SearchCategory
+type Category struct {
+	ID        string `json:"id"`
+	Name      string `json:"name"`
+	BoxArtURL string `json:"box_art_url"`
+}
+
+// SearchCategoriesResponse is the response from SearchCategories
+type SearchCategoriesResponse struct {
+	ResponseCommon
+	Data ManySearchCategories
+}
+
+// SearchCategories searches for Twitch categories based on the given search query
+func (c *Client) SearchCategories(params *SearchCategoriesParams) (*SearchCategoriesResponse, error) {
+	resp, err := c.get("/search/categories", &ManySearchCategories{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	categories := &SearchCategoriesResponse{}
+	resp.HydrateResponseCommon(&categories.ResponseCommon)
+	categories.Data.Categories = resp.Data.(*ManySearchCategories).Categories
+	categories.Data.Pagination = resp.Data.(*ManySearchCategories).Pagination
+
+	return categories, nil
+}
diff --git a/docs/README.md b/docs/README.md
index f4b4e49..d18c197 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,7 @@ Follow the links below to their respective API usage examples:
 - [Analytics](analytics_docs.md)
 - [Authentication](authentication_docs.md)
 - [Bits](bits_docs.md)
+- [Categories](categories_docs.md)
 - [Channels](channels_docs.md)
 - [Channels Points](channels_points_docs.md)
 - [Chat](chat_docs.md)
diff --git a/docs/categories_docs.md b/docs/categories_docs.md
new file mode 100644
index 0000000..0f05ed9
--- /dev/null
+++ b/docs/categories_docs.md
@@ -0,0 +1,22 @@
+# Categories Documentation
+
+## Search Categories
+
+This is an example of how to search categories. Here we are requesting the first two categories that match the query string `pokemon`. 
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+resp, err := client.SearchCategories(&helix.SearchCategoriesParams{
+    First: 2,
+    Query: "pokemon",
+})
+if err != nil {
+    // handle error
+}
+fmt.Printf("%+v\n", resp)
+```
\ No newline at end of file


Test patch:
diff --git a/categories_test.go b/categories_test.go
new file mode 100644
index 0000000..f32ea9b
--- /dev/null
+++ b/categories_test.go
@@ -0,0 +1,105 @@
+package helix
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestSearchCategories(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		First      int
+		respBody   string
+		parsed     []Category
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			2,
+			`{"data":[{"name":"Pocket Monster","id":"390264144","box_art_url":"https://static-cdn.jtvnw.net/ttv-boxart/390264144_IGDB-52x72.jpg"},{"name":"Pokémon Black/White","id":"27602","box_art_url":"https://static-cdn.jtvnw.net/ttv-boxart/27602-52x72.jpg"}],"pagination":{"cursor":"eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6Mn19"}}`,
+			[]Category{
+				{
+					Name:      "Pocket Monster",
+					ID:        "390264144",
+					BoxArtURL: "https://static-cdn.jtvnw.net/ttv-boxart/390264144_IGDB-52x72.jpg",
+				},
+				{
+					Name:      "Pokémon Black/White",
+					ID:        "27602",
+					BoxArtURL: "https://static-cdn.jtvnw.net/ttv-boxart/27602-52x72.jpg",
+				},
+			},
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			101,
+			`{"error":"Bad Request","status":400,"message":"The parameter \"first\" was malformed: the value must be less than or equal to 100"}`,
+			[]Category{},
+		},
+	}
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.SearchCategories(&SearchCategoriesParams{
+			First: testCase.First,
+		})
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test Bad Request Responses
+		if resp.StatusCode == http.StatusBadRequest {
+			firstErrStr := "The parameter \"first\" was malformed: the value must be less than or equal to 100"
+			if resp.ErrorMessage != firstErrStr {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", firstErrStr, resp.ErrorMessage)
+			}
+			continue
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
+		}
+
+		if len(resp.Data.Categories) != testCase.First {
+			t.Errorf("expected \"%d\" streams, got \"%d\"", testCase.First, len(resp.Data.Categories))
+		}
+
+		for i, category := range resp.Data.Categories {
+			if category.ID != testCase.parsed[i].ID {
+				t.Errorf("Expected struct field ID = %s, was %s", testCase.parsed[i].ID, category.ID)
+			}
+			if category.Name != testCase.parsed[i].Name {
+				t.Errorf("Expected struct field ID = %s, was %s", testCase.parsed[i].Name, category.Name)
+			}
+			if category.BoxArtURL != testCase.parsed[i].BoxArtURL {
+				t.Errorf("Expected struct field ID = %s, was %s", testCase.parsed[i].BoxArtURL, category.BoxArtURL)
+			}
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.SearchCategories(&SearchCategoriesParams{})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+		t.Error("expected error does match return error")
+	}
+}