diff --git a/igdb.go b/igdb.go index 16a7d6a..f163981 100644 --- a/igdb.go +++ b/igdb.go @@ -73,6 +73,7 @@ type Client struct { Pulses *PulseService PulseGroups *PulseGroupService PulseSources *PulseSourceService + PulseURLs *PulseURLService ReleaseDates *ReleaseDateService Screenshots *ScreenshotService Themes *ThemeService @@ -158,6 +159,7 @@ func NewClient(apiKey string, custom *http.Client) *Client { c.Pulses = &PulseService{client: c, end: EndpointPulse} c.PulseGroups = &PulseGroupService{client: c, end: EndpointPulseGroup} c.PulseSources = &PulseSourceService{client: c, end: EndpointPulseSource} + c.PulseURLs = &PulseURLService{client: c, end: EndpointPulseURL} c.ReleaseDates = &ReleaseDateService{client: c, end: EndpointReleaseDate} c.Screenshots = &ScreenshotService{client: c, end: EndpointScreenshot} c.Themes = &ThemeService{client: c, end: EndpointTheme} diff --git a/pulseurl.go b/pulseurl.go new file mode 100644 index 0000000..c46e384 --- /dev/null +++ b/pulseurl.go @@ -0,0 +1,118 @@ +package igdb + +import ( + "github.com/Henry-Sarabia/sliceconv" + "github.com/pkg/errors" + "strconv" +) + +//go:generate gomodifytags -file $GOFILE -struct PulseURL -add-tags json -w + +// PulseURL represents a URL linking to an article. +// For more information visit: https://api-docs.igdb.com/#pulse-url +type PulseURL struct { + ID int `json:"id"` + Trusted bool `json:"trusted"` + URL string `json:"url"` +} + +// PulseURLService handles all the API +// calls for the IGDB PulseURL endpoint. +type PulseURLService service + +// Get returns a single PulseURL identified by the provided IGDB ID. Provide +// the SetFields functional option if you need to specify which fields to +// retrieve. If the ID does not match any PulseURLs, an error is returned. +func (ps *PulseURLService) Get(id int, opts ...Option) (*PulseURL, error) { + if id < 0 { + return nil, ErrNegativeID + } + + var URL []*PulseURL + + opts = append(opts, SetFilter("id", OpEquals, strconv.Itoa(id))) + err := ps.client.get(ps.end, &URL, opts...) + if err != nil { + return nil, errors.Wrapf(err, "cannot get PulseURL with ID %v", id) + } + + return URL[0], nil +} + +// List returns a list of PulseURLs identified by the provided list of IGDB IDs. +// Provide functional options to sort, filter, and paginate the results. +// Any ID that does not match a PulseURL is ignored. If none of the IDs +// match a PulseURL, an error is returned. +func (ps *PulseURLService) List(ids []int, opts ...Option) ([]*PulseURL, error) { + for len(ids) < 1 { + return nil, ErrEmptyIDs + } + + for _, id := range ids { + if id < 0 { + return nil, ErrNegativeID + } + } + + var URL []*PulseURL + + opts = append(opts, SetFilter("id", OpContainsAtLeast, sliceconv.Itoa(ids)...)) + err := ps.client.get(ps.end, &URL, opts...) + if err != nil { + return nil, errors.Wrapf(err, "cannot get PulseURLs with IDs %v", ids) + } + + return URL, nil +} + +// Index returns an index of PulseURLs based solely on the provided functional +// options used to sort, filter, and paginate the results. If no PulseURLs can +// be found using the provided options, an error is returned. +func (ps *PulseURLService) Index(opts ...Option) ([]*PulseURL, error) { + var URL []*PulseURL + + err := ps.client.get(ps.end, &URL, opts...) + if err != nil { + return nil, errors.Wrap(err, "cannot get index of PulseURLs") + } + + return URL, nil +} + +// Search returns a list of PulseURLs found by searching the IGDB using the provided +// query. Provide functional options to sort, filter, and paginate the results. If +// no PulseURLs are found using the provided query, an error is returned. +func (ps *PulseURLService) Search(qry string, opts ...Option) ([]*PulseURL, error) { + var URL []*PulseURL + + opts = append(opts, setSearch(qry)) + err := ps.client.get(ps.end, &URL, opts...) + if err != nil { + return nil, errors.Wrapf(err, "cannot get PulseURL with query %s", qry) + } + + return URL, nil +} + +// Count returns the number of PulseURLs available in the IGDB. +// Provide the SetFilter functional option if you need to filter +// which PulseURLs to count. +func (ps *PulseURLService) Count(opts ...Option) (int, error) { + ct, err := ps.client.getCount(ps.end, opts...) + if err != nil { + return 0, errors.Wrap(err, "cannot count PulseURLs") + } + + return ct, nil +} + +// Fields returns the up-to-date list of fields in an +// IGDB PulseURL object. +func (ps *PulseURLService) Fields() ([]string, error) { + f, err := ps.client.getFields(ps.end) + if err != nil { + return nil, errors.Wrap(err, "cannot get PulseURL fields") + } + + return f, nil +} diff --git a/pulseurl_test.go b/pulseurl_test.go new file mode 100644 index 0000000..a86a67c --- /dev/null +++ b/pulseurl_test.go @@ -0,0 +1,205 @@ +package igdb + +import ( + "encoding/json" + "github.com/pkg/errors" + "io/ioutil" + "net/http" + "reflect" + "testing" +) + +const ( + testPulseURLGet string = "test_data/pulseurl_get.json" + testPulseURLList string = "test_data/pulseurl_list.json" +) + +func TestPulseURLService_Get(t *testing.T) { + f, err := ioutil.ReadFile(testPulseURLGet) + if err != nil { + t.Fatal(err) + } + + init := make([]*PulseURL, 1) + json.Unmarshal(f, &init) + + var tests = []struct { + name string + file string + id int + opts []Option + wantPulseURL *PulseURL + wantErr error + }{ + {"Valid response", testPulseURLGet, 105759, []Option{SetFields("name")}, init[0], nil}, + {"Invalid ID", testFileEmpty, -1, nil, nil, ErrNegativeID}, + {"Empty response", testFileEmpty, 105759, nil, nil, errInvalidJSON}, + {"Invalid option", testFileEmpty, 105759, []Option{SetOffset(-99999)}, nil, ErrOutOfRange}, + {"No results", testFileEmptyArray, 0, nil, nil, ErrNoResults}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts, c, err := testServerFile(http.StatusOK, test.file) + if err != nil { + t.Fatal(err) + } + defer ts.Close() + + URL, err := c.PulseURLs.Get(test.id, test.opts...) + if errors.Cause(err) != test.wantErr { + t.Errorf("got: <%v>, want: <%v>", errors.Cause(err), test.wantErr) + } + + if !reflect.DeepEqual(URL, test.wantPulseURL) { + t.Errorf("got: <%v>, \nwant: <%v>", URL, test.wantPulseURL) + } + }) + } +} + +func TestPulseURLService_List(t *testing.T) { + f, err := ioutil.ReadFile(testPulseURLList) + if err != nil { + t.Fatal(err) + } + + init := make([]*PulseURL, 0) + json.Unmarshal(f, &init) + + var tests = []struct { + name string + file string + ids []int + opts []Option + wantPulseURLs []*PulseURL + wantErr error + }{ + {"Valid response", testPulseURLList, []int{105784, 105904, 105984, 92066, 70576}, []Option{SetLimit(5)}, init, nil}, + {"Zero IDs", testFileEmpty, nil, nil, nil, ErrEmptyIDs}, + {"Invalid ID", testFileEmpty, []int{-500}, nil, nil, ErrNegativeID}, + {"Empty response", testFileEmpty, []int{105784, 105904, 105984, 92066, 70576}, nil, nil, errInvalidJSON}, + {"Invalid option", testFileEmpty, []int{105784, 105904, 105984, 92066, 70576}, []Option{SetOffset(-99999)}, nil, ErrOutOfRange}, + {"No results", testFileEmptyArray, []int{0, 9999999}, nil, nil, ErrNoResults}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts, c, err := testServerFile(http.StatusOK, test.file) + if err != nil { + t.Fatal(err) + } + defer ts.Close() + + URL, err := c.PulseURLs.List(test.ids, test.opts...) + if errors.Cause(err) != test.wantErr { + t.Errorf("got: <%v>, want: <%v>", errors.Cause(err), test.wantErr) + } + + if !reflect.DeepEqual(URL, test.wantPulseURLs) { + t.Errorf("got: <%v>, \nwant: <%v>", URL, test.wantPulseURLs) + } + }) + } +} + +func TestPulseURLService_Index(t *testing.T) { + f, err := ioutil.ReadFile(testPulseURLList) + if err != nil { + t.Fatal(err) + } + + init := make([]*PulseURL, 0) + json.Unmarshal(f, &init) + + tests := []struct { + name string + file string + opts []Option + wantPulseURLs []*PulseURL + wantErr error + }{ + {"Valid response", testPulseURLList, []Option{SetLimit(5)}, init, nil}, + {"Empty response", testFileEmpty, nil, nil, errInvalidJSON}, + {"Invalid option", testFileEmpty, []Option{SetOffset(-99999)}, nil, ErrOutOfRange}, + {"No results", testFileEmptyArray, nil, nil, ErrNoResults}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts, c, err := testServerFile(http.StatusOK, test.file) + if err != nil { + t.Fatal(err) + } + defer ts.Close() + + URL, err := c.PulseURLs.Index(test.opts...) + if errors.Cause(err) != test.wantErr { + t.Errorf("got: <%v>, want: <%v>", errors.Cause(err), test.wantErr) + } + + if !reflect.DeepEqual(URL, test.wantPulseURLs) { + t.Errorf("got: <%v>, \nwant: <%v>", URL, test.wantPulseURLs) + } + }) + } +} + +func TestPulseURLService_Count(t *testing.T) { + var tests = []struct { + name string + resp string + opts []Option + wantCount int + wantErr error + }{ + {"Happy path", `{"count": 100}`, []Option{SetFilter("popularity", OpGreaterThan, "75")}, 100, nil}, + {"Empty response", "", nil, 0, errInvalidJSON}, + {"Invalid option", "", []Option{SetLimit(-100)}, 0, ErrOutOfRange}, + {"No results", "[]", nil, 0, ErrNoResults}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts, c := testServerString(http.StatusOK, test.resp) + defer ts.Close() + + count, err := c.PulseURLs.Count(test.opts...) + if errors.Cause(err) != test.wantErr { + t.Errorf("got: <%v>, want: <%v>", errors.Cause(err), test.wantErr) + } + + if count != test.wantCount { + t.Fatalf("got: <%v>, want: <%v>", count, test.wantCount) + } + }) + } +} + +func TestPulseURLService_Fields(t *testing.T) { + var tests = []struct { + name string + resp string + wantFields []string + wantErr error + }{ + {"Happy path", `["name", "slug", "url"]`, []string{"url", "slug", "name"}, nil}, + {"Dot operator", `["logo.url", "background.id"]`, []string{"background.id", "logo.url"}, nil}, + {"Asterisk", `["*"]`, []string{"*"}, nil}, + {"Empty response", "", nil, errInvalidJSON}, + {"No results", "[]", nil, nil}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts, c := testServerString(http.StatusOK, test.resp) + defer ts.Close() + + fields, err := c.PulseURLs.Fields() + if errors.Cause(err) != test.wantErr { + t.Errorf("got: <%v>, want: <%v>", errors.Cause(err), test.wantErr) + } + + if !equalSlice(fields, test.wantFields) { + t.Fatalf("Expected fields '%v', got '%v'", test.wantFields, fields) + } + }) + } +} diff --git a/test_data/pulseurl_get.json b/test_data/pulseurl_get.json new file mode 100644 index 0000000..f4cf7db --- /dev/null +++ b/test_data/pulseurl_get.json @@ -0,0 +1,7 @@ +[ + { + "id": 105759, + "trusted": false, + "url": "http://shoryuken.com/2017/10/30/the-latest-beta-version-of-fightcade-goes-live-for-accounts-from-2015-and-older/" + } +] \ No newline at end of file diff --git a/test_data/pulseurl_list.json b/test_data/pulseurl_list.json new file mode 100644 index 0000000..929136b --- /dev/null +++ b/test_data/pulseurl_list.json @@ -0,0 +1,27 @@ +[ + { + "id": 105784, + "trusted": false, + "url": "http://www.criticalhit.net/gaming/you-can-play-the-whole-of-far-cry-5-in-co-op/" + }, + { + "id": 105904, + "trusted": false, + "url": "http://www.pcgamer.com/the-elder-scrolls-legends-is-getting-a-clockwork-city-expansion" + }, + { + "id": 105984, + "trusted": false, + "url": "http://shoryuken.com/2017/10/31/dante-shows-some-bold-moves-in-this-new-marvel-vs-capcom-infinite-combo-video/" + }, + { + "id": 92066, + "trusted": false, + "url": "http://gamingbolt.com/saints-row-the-third-is-now-playable-on-xbox-one-via-backward-compatibility" + }, + { + "id": 70576, + "trusted": false, + "url": "http://www.gamepur.com/news/27082-zelda-master-trials.html" + } +] \ No newline at end of file