From d7ea58bb985d6da158cd4eea6180e696189db8f8 Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Sun, 23 Nov 2025 14:11:37 +0000 Subject: [PATCH] Add match APIs Signed-off-by: Matheus Pimenta --- client.go | 7 +- get_rikishi.go | 10 +- get_rikishi_stats.go | 30 +- list_kimarite_matches.go | 47 +++ list_kimarite_matches_test.go | 329 ++++++++++++++++++ list_measurement_changes.go | 5 +- list_rank_changes.go | 2 +- list_rikishi_changes.go | 6 +- list_rikishi_matches.go | 49 +++ list_rikishi_matches_against_opponent.go | 54 +++ list_rikishi_matches_against_opponent_test.go | 323 +++++++++++++++++ list_rikishi_matches_test.go | 306 ++++++++++++++++ list_shikona_changes.go | 2 +- match.go | 75 ++++ match_test.go | 201 +++++++++++ measurement.go | 10 +- rank.go | 10 +- rikishi.go | 36 +- search_rikishis.go => search_rikishi.go | 45 +-- ...rikishis_test.go => search_rikishi_test.go | 60 ++-- shikona.go | 10 +- tests/integration/get_rikishi_stats_test.go | 8 - tests/integration/get_rikishi_test.go | 2 +- .../integration/list_kimarite_matches_test.go | 53 +++ .../list_measurement_changes_test.go | 4 +- tests/integration/list_rank_changes_test.go | 2 +- ...t_rikishi_matches_against_opponent_test.go | 55 +++ .../integration/list_rikishi_matches_test.go | 49 +++ .../integration/list_shikona_changes_test.go | 2 +- ...ikishis_test.go => search_rikishi_test.go} | 13 +- 30 files changed, 1676 insertions(+), 129 deletions(-) create mode 100644 list_kimarite_matches.go create mode 100644 list_kimarite_matches_test.go create mode 100644 list_rikishi_matches.go create mode 100644 list_rikishi_matches_against_opponent.go create mode 100644 list_rikishi_matches_against_opponent_test.go create mode 100644 list_rikishi_matches_test.go create mode 100644 match.go create mode 100644 match_test.go rename search_rikishis.go => search_rikishi.go (51%) rename search_rikishis_test.go => search_rikishi_test.go (86%) create mode 100644 tests/integration/list_kimarite_matches_test.go create mode 100644 tests/integration/list_rikishi_matches_against_opponent_test.go create mode 100644 tests/integration/list_rikishi_matches_test.go rename tests/integration/{search_rikishis_test.go => search_rikishi_test.go} (66%) diff --git a/client.go b/client.go index 4de0873..5ab442c 100644 --- a/client.go +++ b/client.go @@ -12,12 +12,15 @@ import ( // Client is a client for the Sumo API. type Client interface { - SearchRikishisAPI + SearchRikishiAPI GetRikishiAPI GetRikishiStatsAPI + ListRikishiMatchesAPI + ListRikishiMatchesAgainstOpponentAPI + ListKimariteMatchesAPI + ListMeasurementChangesAPI ListRankChangesAPI ListShikonaChangesAPI - ListMeasurementChangesAPI } type client struct { diff --git a/get_rikishi.go b/get_rikishi.go index 254477c..f4de3b5 100644 --- a/get_rikishi.go +++ b/get_rikishi.go @@ -6,7 +6,7 @@ import ( "net/url" ) -// GetRikishiAPI defines the methods available for retrieving a single Rikishi. +// GetRikishiAPI defines the methods available for retrieving a single rikishi. type GetRikishiAPI interface { // GetRikishi calls the GET /api/rikishi/{rikishiID} endpoint. GetRikishi(ctx context.Context, req GetRikishiRequest) (*Rikishi, error) @@ -14,10 +14,10 @@ type GetRikishiAPI interface { // GetRikishiRequest represents the request parameters for the GetRikishi method. type GetRikishiRequest struct { - RikishiID int `json:"rikishiId" jsonschema:"The unique identifier of the Rikishi to retrieve. Example: 45 = Terunofuji"` - IncludeRanks bool `json:"includeRanks,omitempty" jsonschema:"Whether to include rank records over time in the Rikishi data."` - IncludeShikonas bool `json:"includeShikonas,omitempty" jsonschema:"Whether to include shikona (ring name) records over time in the Rikishi data."` - IncludeMeasurements bool `json:"includeMeasurements,omitempty" jsonschema:"Whether to include measurement records over time in the Rikishi data."` + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier of the rikishi (sumo wrestler) to retrieve. Example: 45 = Terunofuji"` + IncludeRanks bool `json:"includeRanks,omitempty" jsonschema:"Whether to include rank records over time in the rikishi (sumo wrestler) data."` + IncludeShikonas bool `json:"includeShikonas,omitempty" jsonschema:"Whether to include shikona (ring name) records over time in the rikishi (sumo wrestler) data."` + IncludeMeasurements bool `json:"includeMeasurements,omitempty" jsonschema:"Whether to include measurement records over time in the rikishi (sumo wrestler) data."` } func (c *client) GetRikishi(ctx context.Context, req GetRikishiRequest) (*Rikishi, error) { diff --git a/get_rikishi_stats.go b/get_rikishi_stats.go index 0bd5206..32f6888 100644 --- a/get_rikishi_stats.go +++ b/get_rikishi_stats.go @@ -5,7 +5,7 @@ import ( "fmt" ) -// GetRikishiStatsAPI defines the methods available for retrieving statistics for a single Rikishi. +// GetRikishiStatsAPI defines the methods available for retrieving statistics for a single rikishi. type GetRikishiStatsAPI interface { // GetRikishiStats calls the GET /api/rikishi/{rikishiID}/stats endpoint. GetRikishiStats(ctx context.Context, req GetRikishiStatsRequest) (*GetRikishiStatsResponse, error) @@ -13,24 +13,24 @@ type GetRikishiStatsAPI interface { // GetRikishiStatsRequest represents the request parameters for the GetRikishiStats method. type GetRikishiStatsRequest struct { - RikishiID int `json:"rikishiId" jsonschema:"The unique identifier of the Rikishi to retrieve. Example: 45 = Terunofuji"` + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier of the rikishi (sumo wrestler) to retrieve. Example: 45 = Terunofuji"` } // GetRikishiStatsResponse represents the response from the GetRikishiStats method. type GetRikishiStatsResponse struct { - Basho int `json:"basho,omitempty" jsonschema:"The number of official tournaments (basho) the Rikishi has participated in."` - Yusho int `json:"yusho,omitempty" jsonschema:"The number of tournament championships (yusho) the Rikishi has won."` - TotalMatches int `json:"totalMatches,omitempty" jsonschema:"The total number of matches the Rikishi has had."` - TotalWins int `json:"totalWins,omitempty" jsonschema:"The total number of wins the Rikishi has achieved."` - TotalLosses int `json:"totalLosses,omitempty" jsonschema:"The total number of losses the Rikishi has suffered."` - TotalAbsences int `json:"totalAbsences,omitempty" jsonschema:"The total number of absences the Rikishi has had."` - Sansho map[string]int `json:"sansho,omitempty" jsonschema:"A mapping of special prize names to the number of times the Rikishi has won each prize."` - BashoByDivision map[string]int `json:"bashoByDivision,omitempty" jsonschema:"A mapping of division names to the number of basho the Rikishi has participated in each division."` - YushoByDivision map[string]int `json:"yushoByDivision,omitempty" jsonschema:"A mapping of division names to the number of yusho the Rikishi has won in each division."` - WinsByDivision map[string]int `json:"winsByDivision,omitempty" jsonschema:"A mapping of division names to the number of wins the Rikishi has had in each division."` - LossByDivision map[string]int `json:"lossByDivision,omitempty" jsonschema:"A mapping of division names to the number of losses the Rikishi has had in each division."` - AbsenceByDivision map[string]int `json:"absenceByDivision,omitempty" jsonschema:"A mapping of division names to the number of absences the Rikishi has had in each division."` - TotalMatchesByDivision map[string]int `json:"totalByDivision,omitempty" jsonschema:"A mapping of division names to the total number of matches the Rikishi has had in each division."` + Basho int `json:"basho,omitempty" jsonschema:"The number of official tournaments (basho) the rikishi (sumo wrestler) has participated in."` + Yusho int `json:"yusho,omitempty" jsonschema:"The number of tournament championships (yusho) the rikishi (sumo wrestler) has won."` + TotalMatches int `json:"totalMatches,omitempty" jsonschema:"The total number of matches the rikishi (sumo wrestler) has had."` + TotalWins int `json:"totalWins,omitempty" jsonschema:"The total number of wins the rikishi (sumo wrestler) has achieved."` + TotalLosses int `json:"totalLosses,omitempty" jsonschema:"The total number of losses the rikishi (sumo wrestler) has suffered."` + TotalAbsences int `json:"totalAbsences,omitempty" jsonschema:"The total number of absences the rikishi (sumo wrestler) has had."` + Sansho map[string]int `json:"sansho,omitempty" jsonschema:"A mapping of sansho (special prize) names to the number of times the rikishi (sumo wrestler) has won each prize."` + BashoByDivision map[string]int `json:"bashoByDivision,omitempty" jsonschema:"A mapping of division names to the number of basho (sumo tournaments) the rikishi (sumo wrestler) has participated in each division."` + YushoByDivision map[string]int `json:"yushoByDivision,omitempty" jsonschema:"A mapping of division names to the number of yusho (tournament championships) the rikishi (sumo wrestler) has won in each division."` + WinsByDivision map[string]int `json:"winsByDivision,omitempty" jsonschema:"A mapping of division names to the number of wins the rikishi (sumo wrestler) has had in each division."` + LossByDivision map[string]int `json:"lossByDivision,omitempty" jsonschema:"A mapping of division names to the number of losses the rikishi (sumo wrestler) has had in each division."` + AbsenceByDivision map[string]int `json:"absenceByDivision,omitempty" jsonschema:"A mapping of division names to the number of absences the rikishi (sumo wrestler) has had in each division."` + TotalMatchesByDivision map[string]int `json:"totalByDivision,omitempty" jsonschema:"A mapping of division names to the total number of matches the rikishi (sumo wrestler) has had in each division."` } func (c *client) GetRikishiStats(ctx context.Context, req GetRikishiStatsRequest) (*GetRikishiStatsResponse, error) { diff --git a/list_kimarite_matches.go b/list_kimarite_matches.go new file mode 100644 index 0000000..c02ded1 --- /dev/null +++ b/list_kimarite_matches.go @@ -0,0 +1,47 @@ +package sumoapi + +import ( + "context" + "fmt" + "net/url" +) + +// ListKimariteMatchesAPI defines the methods available for listing matches for a single kimarite. +type ListKimariteMatchesAPI interface { + // ListKimariteMatches calls the GET /api/kimarite/{kimarite} endpoint. + // + // Potential improvements: + // - Support filtering by basho ID like the other match listing endpoints. + ListKimariteMatches(ctx context.Context, req ListKimariteMatchesRequest) (*ListKimariteMatchesResponse, error) +} + +// ListKimariteMatchesRequest represents the request parameters for the ListKimariteMatches method. +type ListKimariteMatchesRequest struct { + Kimarite string `json:"kimarite" jsonschema:"The kimarite (winning technique) to list matches for."` + SortOrder string `json:"sortOrder,omitempty" jsonschema:"The order in which to sort the results by basho (sumo tournament), then day (and is not guaranteed to be the actual use order on that day). Valid values are 'asc' for ascending and 'desc' for descending. Default is 'asc'."` + Limit int `json:"limit,omitempty" jsonschema:"The maximum number of results to return."` + Skip int `json:"skip,omitempty" jsonschema:"The number of results to skip over for pagination."` +} + +// ListKimariteMatchesResponse represents the response from the ListKimariteMatches method. +type ListKimariteMatchesResponse struct { + Limit int `json:"limit" jsonschema:"The maximum number of results that were returned."` + Skip int `json:"skip" jsonschema:"The number of results that were skipped over."` + Total int `json:"total" jsonschema:"The total number of matching results."` + Matches []Match `json:"records" jsonschema:"The list of matches matching the filters."` +} + +func (c *client) ListKimariteMatches(ctx context.Context, req ListKimariteMatchesRequest) (*ListKimariteMatchesResponse, error) { + query := make(url.Values) + if order := getSortOrder(req.SortOrder); order != "" { + query.Set("sortOrder", order) + } + if req.Limit > 0 { + query.Set("limit", fmt.Sprint(req.Limit)) + } + if req.Skip > 0 { + query.Set("skip", fmt.Sprint(req.Skip)) + } + path := fmt.Sprintf("/kimarite/%s", req.Kimarite) + return getObject[ListKimariteMatchesResponse](ctx, c, path, query) +} diff --git a/list_kimarite_matches_test.go b/list_kimarite_matches_test.go new file mode 100644 index 0000000..c9fdfc8 --- /dev/null +++ b/list_kimarite_matches_test.go @@ -0,0 +1,329 @@ +package sumoapi_test + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestClient_ListKimariteMatches(t *testing.T) { + t.Run("list matches by kimarite", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 2, + "records": [ + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 1, + "matchNo": 5, + "eastId": 45, + "eastShikona": "Terunofuji", + "eastRank": "Yokozuna 1 East", + "westId": 123, + "westShikona": "Takakeisho", + "westRank": "Ozeki 1 West", + "winnerId": 45, + "winnerEn": "Terunofuji", + "kimarite": "Yorikiri" + }, + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 3, + "matchNo": 8, + "eastId": 100, + "eastShikona": "Wrestler A", + "eastRank": "Maegashira 1 East", + "westId": 200, + "westShikona": "Wrestler B", + "westRank": "Maegashira 1 West", + "winnerId": 100, + "winnerEn": "Wrestler A", + "kimarite": "Yorikiri" + } + ] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Method).To(Equal(http.MethodGet)) + g.Expect(req.URL.Scheme).To(Equal("https")) + g.Expect(req.URL.Host).To(Equal("sumo-api.com")) + g.Expect(req.URL.Path).To(Equal("/api/kimarite/Yorikiri")) + g.Expect(req.URL.Query().Has("sortOrder")).To(BeFalse()) + g.Expect(req.URL.Query().Has("limit")).To(BeFalse()) + g.Expect(req.URL.Query().Has("skip")).To(BeFalse()) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(2)) + g.Expect(resp.Matches).To(HaveLen(2)) + g.Expect(resp.Matches[0].Kimarite).To(Equal("Yorikiri")) + g.Expect(resp.Matches[1].Kimarite).To(Equal("Yorikiri")) + }) + + t.Run("list matches with sort order asc", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 1, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Path).To(Equal("/api/kimarite/Oshidashi")) + g.Expect(req.URL.Query().Get("sortOrder")).To(Equal("asc")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Oshidashi", + SortOrder: "asc", + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + }) + + t.Run("list matches with sort order desc", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 1, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Path).To(Equal("/api/kimarite/Hatakikomi")) + g.Expect(req.URL.Query().Get("sortOrder")).To(Equal("desc")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Hatakikomi", + SortOrder: "desc", + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + }) + + t.Run("list matches with limit", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 5, + "skip": 0, + "total": 100, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("limit")).To(Equal("5")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + Limit: 5, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(100)) + g.Expect(resp.Limit).To(Equal(5)) + }) + + t.Run("list matches with skip", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 10, + "total": 100, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("skip")).To(Equal("10")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + Skip: 10, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(100)) + g.Expect(resp.Skip).To(Equal(10)) + }) + + t.Run("list matches with all parameters", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 20, + "skip": 5, + "total": 50, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + query := req.URL.Query() + g.Expect(req.URL.Path).To(Equal("/api/kimarite/Tsukiotoshi")) + g.Expect(query.Get("sortOrder")).To(Equal("desc")) + g.Expect(query.Get("limit")).To(Equal("20")) + g.Expect(query.Get("skip")).To(Equal("5")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Tsukiotoshi", + SortOrder: "desc", + Limit: 20, + Skip: 5, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(50)) + g.Expect(resp.Limit).To(Equal(20)) + g.Expect(resp.Skip).To(Equal(5)) + }) + + t.Run("context is propagated", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 0, + "records": [] + }` + + type testKey struct{} + ctx := context.WithValue(context.Background(), testKey{}, "test-value") + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Context().Value(testKey{})).To(Equal("test-value")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + _, err := client.ListKimariteMatches(ctx, sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + }) + + g.Expect(err).ToNot(HaveOccurred()) + }) + + t.Run("http request error", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + return http.ErrAbortHandler + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error making http request")) + g.Expect(resp).To(BeNil()) + }) + + t.Run("invalid JSON response", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("not valid json")), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "Yorikiri", + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body")) + g.Expect(resp).To(BeNil()) + }) +} diff --git a/list_measurement_changes.go b/list_measurement_changes.go index 5c232ca..28e37b3 100644 --- a/list_measurement_changes.go +++ b/list_measurement_changes.go @@ -2,9 +2,12 @@ package sumoapi import "context" -// ListMeasurementChangesAPI defines the methods available for listing Rikishi measurement changes across bashos (sumo tournaments). +// ListMeasurementChangesAPI defines the methods available for listing rikishi measurement changes across bashos. type ListMeasurementChangesAPI interface { // ListMeasurementChanges calls the GET /api/measurements endpoint. + // + // Documented bugs: + // - The API is ignoring the bashoId input. Measurement changes are returned for all bashos instead of the specified one. ListMeasurementChanges(ctx context.Context, req ListRikishiChangesRequest) ([]Measurement, error) } diff --git a/list_rank_changes.go b/list_rank_changes.go index e70ffda..9aeafcc 100644 --- a/list_rank_changes.go +++ b/list_rank_changes.go @@ -2,7 +2,7 @@ package sumoapi import "context" -// ListRankChangesAPI defines the methods available for listing Rikishi rank changes across bashos (sumo tournaments). +// ListRankChangesAPI defines the methods available for listing rikishi rank changes across bashos. type ListRankChangesAPI interface { // ListRankChanges calls the GET /api/ranks endpoint. ListRankChanges(ctx context.Context, req ListRikishiChangesRequest) ([]Rank, error) diff --git a/list_rikishi_changes.go b/list_rikishi_changes.go index 3ab5dc1..7c997af 100644 --- a/list_rikishi_changes.go +++ b/list_rikishi_changes.go @@ -8,9 +8,9 @@ import ( // ListRikishiChangesRequest represents a request to list Rikishi changes with optional filters. type ListRikishiChangesRequest struct { - RikishiID int `json:"rikishiId,omitempty" jsonschema:"The ID of the rikishi whose changes are to be listed."` - BashoID *BashoID `json:"bashoId,omitempty" jsonschema:"The ID of the basho for which rikishi changes are to be listed."` - SortOrder string `json:"sortOrder,omitempty" jsonschema:"The order in which to sort the results by Basho (sumo tournament). Valid values are 'asc' for ascending and 'desc' for descending. Default is 'desc'."` + RikishiID int `json:"rikishiId,omitempty" jsonschema:"The ID of the rikishi (sumo wrestler) whose changes are to be listed."` + BashoID *BashoID `json:"bashoId,omitempty" jsonschema:"The ID of the basho (sumo tournament) for which rikishi (sumo wrestler) changes are to be listed."` + SortOrder string `json:"sortOrder,omitempty" jsonschema:"The order in which to sort the results by basho (sumo tournament). Valid values are 'asc' for ascending and 'desc' for descending. Default is 'desc'."` } func listRikishiChanges[obj any](ctx context.Context, c *client, path string, req ListRikishiChangesRequest) ([]obj, error) { diff --git a/list_rikishi_matches.go b/list_rikishi_matches.go new file mode 100644 index 0000000..018ab60 --- /dev/null +++ b/list_rikishi_matches.go @@ -0,0 +1,49 @@ +package sumoapi + +import ( + "context" + "fmt" + "net/url" +) + +// ListRikishiMatchesAPI defines the methods available for listing matches for a single rikishi. +type ListRikishiMatchesAPI interface { + // ListRikishiMatches calls the GET /api/rikishi/{rikishiID}/matches endpoint. + // + // Documented bugs: + // - The API accepts and takes into account the limit and skip inputs, but they are not documented in the API guide. + // - The API response always returns 0 for the limit and skip outputs. + // - The API response does not return the match ID like in the GET /api/kimarite/{kimariteID} endpoint. + ListRikishiMatches(ctx context.Context, req ListRikishiMatchesRequest) (*ListRikishiMatchesResponse, error) +} + +// ListRikishiMatchesRequest represents the request parameters for the ListRikishiMatches method. +type ListRikishiMatchesRequest struct { + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the rikishi (sumo wrestler)."` + BashoID *BashoID `json:"bashoId,omitempty" jsonschema:"The ID of the basho (sumo tournament) to filter matches by, in the format YYYYMM."` + Limit int `json:"limit,omitempty" jsonschema:"The maximum number of results to return."` + Skip int `json:"skip,omitempty" jsonschema:"The number of results to skip over for pagination."` +} + +// ListRikishiMatchesResponse represents the response from the ListRikishiMatches method. +type ListRikishiMatchesResponse struct { + Limit int `json:"limit" jsonschema:"The maximum number of results that were returned."` + Skip int `json:"skip" jsonschema:"The number of results that were skipped over."` + Total int `json:"total" jsonschema:"The total number of matching results."` + Matches []Match `json:"records" jsonschema:"The list of matches matching the filters."` +} + +func (c *client) ListRikishiMatches(ctx context.Context, req ListRikishiMatchesRequest) (*ListRikishiMatchesResponse, error) { + query := make(url.Values) + if req.BashoID != nil { + query.Set("bashoId", req.BashoID.String()) + } + if req.Limit > 0 { + query.Set("limit", fmt.Sprint(req.Limit)) + } + if req.Skip > 0 { + query.Set("skip", fmt.Sprint(req.Skip)) + } + path := fmt.Sprintf("/rikishi/%d/matches", req.RikishiID) + return getObject[ListRikishiMatchesResponse](ctx, c, path, query) +} diff --git a/list_rikishi_matches_against_opponent.go b/list_rikishi_matches_against_opponent.go new file mode 100644 index 0000000..550ba25 --- /dev/null +++ b/list_rikishi_matches_against_opponent.go @@ -0,0 +1,54 @@ +package sumoapi + +import ( + "context" + "fmt" + "net/url" +) + +// ListRikishiMatchesAgainstOpponentAPI defines the methods available for listing matches for a single rikishi and opponent pair. +type ListRikishiMatchesAgainstOpponentAPI interface { + // ListRikishiMatchesAgainstOpponent calls the GET /api/rikishi/{rikishiID}/matches/{opponentID} endpoint. + // + // Documented bugs: + // - The API accepts and takes into account the limit and skip inputs, but they are not documented in the API guide. + // - The API response does not return the limit and skip outputs like in other endpoints. + // - The API response does not return the match ID like in the GET /api/kimarite/{kimariteID} endpoint. + ListRikishiMatchesAgainstOpponent(ctx context.Context, req ListRikishiMatchesAgainstOpponentRequest) (*ListRikishiMatchesAgainstOpponentResponse, error) +} + +// ListRikishiMatchesAgainstOpponentRequest represents the request parameters for the ListRikishiMatchesAgainstOpponent method. +type ListRikishiMatchesAgainstOpponentRequest struct { + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the rikishi (sumo wrestler)."` + OpponentID int `json:"opponentId" jsonschema:"The unique identifier for the opponent rikishi (sumo wrestler)."` + BashoID *BashoID `json:"bashoId,omitempty" jsonschema:"The ID of the basho (sumo tournament) to filter matches by, in the format YYYYMM."` + Limit int `json:"limit,omitempty" jsonschema:"The maximum number of results to return."` + Skip int `json:"skip,omitempty" jsonschema:"The number of results to skip over for pagination."` +} + +// ListRikishiMatchesAgainstOpponentResponse represents the response from the ListRikishiMatchesAgainstOpponent method. +type ListRikishiMatchesAgainstOpponentResponse struct { + RikishiWins int `json:"rikishiWins" jsonschema:"The total number of wins for the rikishi against the opponent in the matching results."` + OpponentWins int `json:"opponentWins" jsonschema:"The total number of wins for the opponent against the rikishi in the matching results."` + KimariteWins map[string]int `json:"kimariteWins" jsonschema:"A breakdown of wins by kimarite (winning technique) for the rikishi against the opponent in the matching results."` + KimariteLosses map[string]int `json:"kimariteLosses" jsonschema:"A breakdown of losses by kimarite (winning technique) for the rikishi against the opponent in the matching results."` + Limit int `json:"limit" jsonschema:"The maximum number of results that were returned."` + Skip int `json:"skip" jsonschema:"The number of results that were skipped over."` + Total int `json:"total" jsonschema:"The total number of matching results."` + Matches []Match `json:"matches" jsonschema:"The list of matches matching the filters."` +} + +func (c *client) ListRikishiMatchesAgainstOpponent(ctx context.Context, req ListRikishiMatchesAgainstOpponentRequest) (*ListRikishiMatchesAgainstOpponentResponse, error) { + query := make(url.Values) + if req.BashoID != nil { + query.Set("bashoId", req.BashoID.String()) + } + if req.Limit > 0 { + query.Set("limit", fmt.Sprint(req.Limit)) + } + if req.Skip > 0 { + query.Set("skip", fmt.Sprint(req.Skip)) + } + path := fmt.Sprintf("/rikishi/%d/matches/%d", req.RikishiID, req.OpponentID) + return getObject[ListRikishiMatchesAgainstOpponentResponse](ctx, c, path, query) +} diff --git a/list_rikishi_matches_against_opponent_test.go b/list_rikishi_matches_against_opponent_test.go new file mode 100644 index 0000000..43aa4b1 --- /dev/null +++ b/list_rikishi_matches_against_opponent_test.go @@ -0,0 +1,323 @@ +package sumoapi_test + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestClient_ListRikishiMatchesAgainstOpponent(t *testing.T) { + t.Run("list matches against opponent", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 5, + "opponentWins": 3, + "kimariteWins": { + "Yorikiri": 3, + "Oshidashi": 2 + }, + "kimariteLosses": { + "Yorikiri": 2, + "Hatakikomi": 1 + }, + "limit": 0, + "skip": 0, + "total": 8, + "matches": [ + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 1, + "matchNo": 5, + "eastId": 45, + "eastShikona": "Terunofuji", + "eastRank": "Yokozuna 1 East", + "westId": 123, + "westShikona": "Takakeisho", + "westRank": "Ozeki 1 West", + "winnerId": 45, + "winnerEn": "Terunofuji", + "kimarite": "Yorikiri" + } + ] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Method).To(Equal(http.MethodGet)) + g.Expect(req.URL.Scheme).To(Equal("https")) + g.Expect(req.URL.Host).To(Equal("sumo-api.com")) + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches/123")) + g.Expect(req.URL.Query().Has("bashoId")).To(BeFalse()) + g.Expect(req.URL.Query().Has("limit")).To(BeFalse()) + g.Expect(req.URL.Query().Has("skip")).To(BeFalse()) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.RikishiWins).To(Equal(5)) + g.Expect(resp.OpponentWins).To(Equal(3)) + g.Expect(resp.Total).To(Equal(8)) + g.Expect(resp.KimariteWins).To(HaveLen(2)) + g.Expect(resp.KimariteWins["Yorikiri"]).To(Equal(3)) + g.Expect(resp.KimariteWins["Oshidashi"]).To(Equal(2)) + g.Expect(resp.KimariteLosses).To(HaveLen(2)) + g.Expect(resp.KimariteLosses["Yorikiri"]).To(Equal(2)) + g.Expect(resp.KimariteLosses["Hatakikomi"]).To(Equal(1)) + g.Expect(resp.Matches).To(HaveLen(1)) + }) + + t.Run("list matches with basho ID", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 1, + "opponentWins": 0, + "kimariteWins": {}, + "kimariteLosses": {}, + "limit": 0, + "skip": 0, + "total": 1, + "matches": [] + }` + + bashoID := sumoapi.BashoID{Year: 2025, Month: 1} + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches/123")) + g.Expect(req.URL.Query().Get("bashoId")).To(Equal("202501")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + BashoID: &bashoID, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + }) + + t.Run("list matches with limit", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 0, + "opponentWins": 0, + "kimariteWins": {}, + "kimariteLosses": {}, + "limit": 0, + "skip": 0, + "total": 10, + "matches": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("limit")).To(Equal("5")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + Limit: 5, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(10)) + }) + + t.Run("list matches with skip", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 0, + "opponentWins": 0, + "kimariteWins": {}, + "kimariteLosses": {}, + "limit": 0, + "skip": 0, + "total": 10, + "matches": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("skip")).To(Equal("3")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + Skip: 3, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(10)) + }) + + t.Run("list matches with all parameters", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 0, + "opponentWins": 0, + "kimariteWins": {}, + "kimariteLosses": {}, + "limit": 0, + "skip": 0, + "total": 5, + "matches": [] + }` + + bashoID := sumoapi.BashoID{Year: 2025, Month: 1} + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + query := req.URL.Query() + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches/123")) + g.Expect(query.Get("bashoId")).To(Equal("202501")) + g.Expect(query.Get("limit")).To(Equal("10")) + g.Expect(query.Get("skip")).To(Equal("5")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + BashoID: &bashoID, + Limit: 10, + Skip: 5, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(5)) + }) + + t.Run("context is propagated", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "rikishiWins": 0, + "opponentWins": 0, + "kimariteWins": {}, + "kimariteLosses": {}, + "limit": 0, + "skip": 0, + "total": 0, + "matches": [] + }` + + type testKey struct{} + ctx := context.WithValue(context.Background(), testKey{}, "test-value") + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Context().Value(testKey{})).To(Equal("test-value")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + _, err := client.ListRikishiMatchesAgainstOpponent(ctx, sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + }) + + g.Expect(err).ToNot(HaveOccurred()) + }) + + t.Run("http request error", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + return http.ErrAbortHandler + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error making http request")) + g.Expect(resp).To(BeNil()) + }) + + t.Run("invalid JSON response", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("not valid json")), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, + OpponentID: 123, + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body")) + g.Expect(resp).To(BeNil()) + }) +} diff --git a/list_rikishi_matches_test.go b/list_rikishi_matches_test.go new file mode 100644 index 0000000..6daf5b1 --- /dev/null +++ b/list_rikishi_matches_test.go @@ -0,0 +1,306 @@ +package sumoapi_test + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestClient_ListRikishiMatches(t *testing.T) { + t.Run("list matches by rikishi ID", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 2, + "records": [ + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 1, + "matchNo": 5, + "eastId": 45, + "eastShikona": "Terunofuji", + "eastRank": "Yokozuna 1 East", + "westId": 123, + "westShikona": "Takakeisho", + "westRank": "Ozeki 1 West", + "winnerId": 45, + "winnerEn": "Terunofuji", + "kimarite": "Yorikiri" + }, + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 2, + "matchNo": 6, + "eastId": 45, + "eastShikona": "Terunofuji", + "eastRank": "Yokozuna 1 East", + "westId": 456, + "westShikona": "Hoshoryu", + "westRank": "Sekiwake 1 West", + "winnerId": 45, + "winnerEn": "Terunofuji", + "kimarite": "Oshidashi" + } + ] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Method).To(Equal(http.MethodGet)) + g.Expect(req.URL.Scheme).To(Equal("https")) + g.Expect(req.URL.Host).To(Equal("sumo-api.com")) + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches")) + g.Expect(req.URL.Query().Has("bashoId")).To(BeFalse()) + g.Expect(req.URL.Query().Has("limit")).To(BeFalse()) + g.Expect(req.URL.Query().Has("skip")).To(BeFalse()) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(2)) + g.Expect(resp.Matches).To(HaveLen(2)) + g.Expect(resp.Matches[0].EastID).To(Equal(45)) + g.Expect(resp.Matches[0].EastShikona).To(Equal("Terunofuji")) + g.Expect(resp.Matches[1].WinnerID).To(Equal(45)) + }) + + t.Run("list matches with basho ID", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 1, + "records": [ + { + "bashoId": "202501", + "division": "Makuuchi", + "day": 1, + "eastId": 45, + "winnerId": 45 + } + ] + }` + + bashoID := sumoapi.BashoID{Year: 2025, Month: 1} + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches")) + g.Expect(req.URL.Query().Get("bashoId")).To(Equal("202501")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + BashoID: &bashoID, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + g.Expect(resp.Matches).To(HaveLen(1)) + }) + + t.Run("list matches with limit", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 100, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("limit")).To(Equal("10")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + Limit: 10, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(100)) + g.Expect(resp.Matches).To(BeEmpty()) + }) + + t.Run("list matches with skip", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 100, + "records": [] + }` + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.URL.Query().Get("skip")).To(Equal("20")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + Skip: 20, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(100)) + g.Expect(resp.Matches).To(BeEmpty()) + }) + + t.Run("list matches with all parameters", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 5, + "records": [] + }` + + bashoID := sumoapi.BashoID{Year: 2025, Month: 1} + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + query := req.URL.Query() + g.Expect(req.URL.Path).To(Equal("/api/rikishi/45/matches")) + g.Expect(query.Get("bashoId")).To(Equal("202501")) + g.Expect(query.Get("limit")).To(Equal("5")) + g.Expect(query.Get("skip")).To(Equal("10")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + BashoID: &bashoID, + Limit: 5, + Skip: 10, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(5)) + }) + + t.Run("context is propagated", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "limit": 0, + "skip": 0, + "total": 0, + "records": [] + }` + + type testKey struct{} + ctx := context.WithValue(context.Background(), testKey{}, "test-value") + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + g.Expect(req.Context().Value(testKey{})).To(Equal("test-value")) + return nil + }, + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResp)), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + _, err := client.ListRikishiMatches(ctx, sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + }) + + g.Expect(err).ToNot(HaveOccurred()) + }) + + t.Run("http request error", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + validateRequest: func(req *http.Request) error { + return http.ErrAbortHandler + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error making http request")) + g.Expect(resp).To(BeNil()) + }) + + t.Run("invalid JSON response", func(t *testing.T) { + g := NewWithT(t) + + transport := &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("not valid json")), + }, + } + + client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body")) + g.Expect(resp).To(BeNil()) + }) +} diff --git a/list_shikona_changes.go b/list_shikona_changes.go index 2750080..84e574e 100644 --- a/list_shikona_changes.go +++ b/list_shikona_changes.go @@ -2,7 +2,7 @@ package sumoapi import "context" -// ListShikonaChangesAPI defines the methods available for listing Rikishi shikona (ring name) changes across bashos (sumo tournaments). +// ListShikonaChangesAPI defines the methods available for listing rikishi shikona changes across bashos. type ListShikonaChangesAPI interface { // ListShikonaChanges calls the GET /api/shikonas endpoint. ListShikonaChanges(ctx context.Context, req ListRikishiChangesRequest) ([]Shikona, error) diff --git a/match.go b/match.go new file mode 100644 index 0000000..fb3a6fc --- /dev/null +++ b/match.go @@ -0,0 +1,75 @@ +package sumoapi + +import ( + "encoding/json" + "fmt" +) + +// Match represents a sumo match. +type Match struct { + // ID is optional because it's not returned by the APIs for listing rikishi matches. + ID *MatchID `json:"id,omitempty" jsonschema:"The unique identifier for the match in the format YYYYMM-D-NUM-EASTID-WESTID."` + BashoID BashoID `json:"bashoId" jsonschema:"The ID of the basho (sumo tournament) in which the match took place, in the format YYYYMM."` + Division string `json:"division" jsonschema:"The division in which the match took place."` + Day int `json:"day" jsonschema:"The day of the basho (sumo tournament) on which the match took place."` + MatchNumber int `json:"matchNo,omitempty" jsonschema:"The number of the match on the given day."` + EastID int `json:"eastId,omitempty" jsonschema:"The unique identifier for the rikishi (sumo wrestler) on the east side."` + EastShikona string `json:"eastShikona,omitempty" jsonschema:"The shikona (ring name) in English of the rikishi (sumo wrestler) on the east side."` + EastRank string `json:"eastRank,omitempty" jsonschema:"The rank of the rikishi (sumo wrestler) on the east side."` + WestID int `json:"westId,omitempty" jsonschema:"The unique identifier for the rikishi (sumo wrestler) on the west side."` + WestShikona string `json:"westShikona,omitempty" jsonschema:"The shikona (ring name) in English of the rikishi (sumo wrestler) on the west side."` + WestRank string `json:"westRank,omitempty" jsonschema:"The rank of the rikishi (sumo wrestler) on the west side."` + WinnerID int `json:"winnerId,omitempty" jsonschema:"The unique identifier for the winning rikishi (sumo wrestler)."` + WinnerEnglish string `json:"winnerEn,omitempty" jsonschema:"The shikona (ring name) in English of the winning rikishi (sumo wrestler)."` + WinnerJapanese string `json:"winnerJp,omitempty" jsonschema:"The shikona (ring name) in Japanese of the winning rikishi (sumo wrestler)."` + Kimarite string `json:"kimarite,omitempty" jsonschema:"The kimarite (winning technique) used in the match."` +} + +// MatchID represents the unique identifier for a sumo match. +type MatchID struct { + BashoID + Day int + MatchNumber int + EastID int + WestID int +} + +func (m MatchID) String() string { + return fmt.Sprintf("%s-%d-%d-%d-%d", m.BashoID.String(), m.Day, m.MatchNumber, m.EastID, m.WestID) +} + +func (m MatchID) MarshalJSON() ([]byte, error) { + return []byte(`"` + m.String() + `"`), nil +} + +func (m *MatchID) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("error unmarshaling MatchID: %w", err) + } + if s == "" { // Match ID is optional in some APIs. + *m = MatchID{} + return nil + } + var bashoID BashoID + if len(s) < 11 { + return fmt.Errorf("invalid MatchID format: %s", s) + } + if err := bashoID.UnmarshalJSON([]byte(`"` + s[0:6] + `"`)); err != nil { + return fmt.Errorf("error parsing BashoID from MatchID: %w", err) + } + var day, matchNumber, eastID, westID int + n, err := fmt.Sscanf(s[7:], "%d-%d-%d-%d", &day, &matchNumber, &eastID, &westID) + if err != nil { + return fmt.Errorf("error parsing day, match number, east ID, and west ID from MatchID: %w", err) + } + if n != 4 { + return fmt.Errorf("invalid MatchID format: %s", s) + } + m.BashoID = bashoID + m.Day = day + m.MatchNumber = matchNumber + m.EastID = eastID + m.WestID = westID + return nil +} diff --git a/match_test.go b/match_test.go new file mode 100644 index 0000000..f71323d --- /dev/null +++ b/match_test.go @@ -0,0 +1,201 @@ +package sumoapi_test + +import ( + "encoding/json" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestMarshalMatchIDToJSON(t *testing.T) { + for _, tt := range []struct { + name string + matchID sumoapi.MatchID + expectedJSON string + }{ + { + name: "double digit month", + matchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2023, Month: 11}, + Day: 1, + MatchNumber: 5, + EastID: 45, + WestID: 123, + }, + expectedJSON: `"202311-1-5-45-123"`, + }, + { + name: "single digit month", + matchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 1999, Month: 3}, + Day: 15, + MatchNumber: 42, + EastID: 100, + WestID: 200, + }, + expectedJSON: `"199903-15-42-100-200"`, + }, + { + name: "large match number", + matchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2025, Month: 1}, + Day: 10, + MatchNumber: 999, + EastID: 1, + WestID: 2, + }, + expectedJSON: `"202501-10-999-1-2"`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + b, err := json.Marshal(tt.matchID) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(string(b)).To(Equal(tt.expectedJSON)) + }) + } + + t.Run("marshal inside struct", func(t *testing.T) { + g := NewWithT(t) + type wrapper struct { + Match sumoapi.MatchID `json:"match"` + } + w := wrapper{ + Match: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2023, Month: 11}, + Day: 1, + MatchNumber: 5, + EastID: 45, + WestID: 123, + }, + } + b, err := json.Marshal(w) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(string(b)).To(Equal(`{"match":"202311-1-5-45-123"}`)) + }) +} + +func TestUnmarshalMatchIDFromJSON(t *testing.T) { + for _, tt := range []struct { + name string + jsonData string + expectedMatchID sumoapi.MatchID + expectError bool + }{ + { + name: "double digit month", + jsonData: `"202311-1-5-45-123"`, + expectedMatchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2023, Month: 11}, + Day: 1, + MatchNumber: 5, + EastID: 45, + WestID: 123, + }, + expectError: false, + }, + { + name: "single digit month", + jsonData: `"199903-15-42-100-200"`, + expectedMatchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 1999, Month: 3}, + Day: 15, + MatchNumber: 42, + EastID: 100, + WestID: 200, + }, + expectError: false, + }, + { + name: "large match number", + jsonData: `"202501-10-999-1-2"`, + expectedMatchID: sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2025, Month: 1}, + Day: 10, + MatchNumber: 999, + EastID: 1, + WestID: 2, + }, + expectError: false, + }, + { + name: "empty string", + jsonData: `""`, + expectedMatchID: sumoapi.MatchID{}, + }, + { + name: "invalid basho format", + jsonData: `"20A311-1-5-45-123"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "wrong length - too short", + jsonData: `"202311-1"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "missing day", + jsonData: `"202311--5-45-123"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "missing match number", + jsonData: `"202311-1--45-123"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "missing east ID", + jsonData: `"202311-1-5--123"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "missing west ID", + jsonData: `"202311-1-5-45-"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + { + name: "invalid day - not a number", + jsonData: `"202311-X-5-45-123"`, + expectedMatchID: sumoapi.MatchID{}, + expectError: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + var m sumoapi.MatchID + err := json.Unmarshal([]byte(tt.jsonData), &m) + if tt.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(m).To(Equal(tt.expectedMatchID)) + } + }) + } + + t.Run("unmarshal inside struct", func(t *testing.T) { + g := NewWithT(t) + type wrapper struct { + Match sumoapi.MatchID `json:"match"` + } + jsonData := `{"match":"202311-1-5-45-123"}` + var w wrapper + err := json.Unmarshal([]byte(jsonData), &w) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(w.Match).To(Equal(sumoapi.MatchID{ + BashoID: sumoapi.BashoID{Year: 2023, Month: 11}, + Day: 1, + MatchNumber: 5, + EastID: 45, + WestID: 123, + })) + }) +} diff --git a/measurement.go b/measurement.go index 917de19..234364d 100644 --- a/measurement.go +++ b/measurement.go @@ -2,9 +2,9 @@ package sumoapi // Measurement represents a sumo wrestler's measurement in a specific Basho (sumo tournament). type Measurement struct { - ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the Measurement in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the Rikishi in the API."` - BashoID BashoID `json:"bashoId" jsonschema:"The ID of the Basho (sumo tournament) in the format YYYYMM."` - RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the Rikishi (sumo wrestler) in the API."` - Height float64 `json:"height,omitempty" jsonschema:"The height of the Rikishi in centimeters measured on the beginning of the specified Basho (sumo tournament)."` - Weight float64 `json:"weight,omitempty" jsonschema:"The weight of the Rikishi in kilograms measured on the beginning of the specified Basho (sumo tournament)."` + ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the measurement in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the rikishi in the API."` + BashoID BashoID `json:"bashoId" jsonschema:"The ID of the basho (sumo tournament) in the format YYYYMM."` + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the rikishi (sumo wrestler) in the API."` + Height float64 `json:"height,omitempty" jsonschema:"The height of the rikishi in centimeters measured on the beginning of the specified basho (sumo tournament)."` + Weight float64 `json:"weight,omitempty" jsonschema:"The weight of the rikishi in kilograms measured on the beginning of the specified basho (sumo tournament)."` } diff --git a/rank.go b/rank.go index fa08242..d0348cb 100644 --- a/rank.go +++ b/rank.go @@ -2,9 +2,9 @@ package sumoapi // Rank represents a sumo wrestler's rank in a specific Basho (sumo tournament). type Rank struct { - ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the Rank in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the Rikishi in the API."` - BashoID BashoID `json:"bashoId" jsonschema:"The ID of the Basho (sumo tournament) in the format YYYYMM."` - RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the Rikishi (sumo wrestler) in the API."` - HumanReadableName string `json:"rank,omitempty" jsonschema:"The human-readable name of the rank (e.g., Maegashira 1 East, Ozeki 2 West) of the Rikishi in the specific Basho (sumo tournament)."` - NumericName int `json:"rankValue,omitempty" jsonschema:"The numeric name of the rank of the Rikishi in the specific Basho (sumo tournament)."` + ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the rank in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the rikishi in the API."` + BashoID BashoID `json:"bashoId" jsonschema:"The ID of the basho (sumo tournament) in the format YYYYMM."` + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the rikishi (sumo wrestler) in the API."` + HumanReadableName string `json:"rank,omitempty" jsonschema:"The human-readable name of the rank (e.g., Maegashira 1 East, Ozeki 2 West) of the rikishi in the specific basho (sumo tournament)."` + NumericName int `json:"rankValue,omitempty" jsonschema:"The numeric name of the rank of the rikishi in the specific basho (sumo tournament)."` } diff --git a/rikishi.go b/rikishi.go index 18c7c90..73b6e21 100644 --- a/rikishi.go +++ b/rikishi.go @@ -4,22 +4,22 @@ import "time" // Rikishi represents a sumo wrestler. type Rikishi struct { - ID int `json:"id" jsonschema:"The unique identifier for the Rikishi in the API."` - SumoDBID int `json:"sumodbId,omitempty" jsonschema:"The SumoDB ID of the Rikishi."` - OfficialID int `json:"nskId,omitempty" jsonschema:"The official Nihon Sumo Kyokai (Japan Sumo Association) ID of the Rikishi."` - ShikonaEnglish string `json:"shikonaEn,omitempty" jsonschema:"The shikona (ring name) of the Rikishi in English."` - ShikonaJapanese string `json:"shikonaJp,omitempty" jsonschema:"The shikona (ring name) of the Rikishi in Japanese."` - CurrentRank string `json:"currentRank,omitempty" jsonschema:"The current rank of the Rikishi."` - Heya string `json:"heya,omitempty" jsonschema:"The heya (stable) of the Rikishi."` - BirthDate *time.Time `json:"birthDate,omitempty" jsonschema:"The birth date of the Rikishi."` - Shusshin string `json:"shusshin,omitempty" jsonschema:"The place of birth of the Rikishi."` - Height float64 `json:"height,omitempty" jsonschema:"The height of the Rikishi in centimeters."` - Weight float64 `json:"weight,omitempty" jsonschema:"The weight of the Rikishi in kilograms."` - Debut *BashoID `json:"debut,omitempty" jsonschema:"The ID of the Basho when the Rikishi made their debut in the format YYYYMM."` - Intai *time.Time `json:"intai,omitempty" jsonschema:"The retirement date of the Rikishi, if retired."` - RankHistory []Rank `json:"rankHistory,omitempty" jsonschema:"The historical rank records of the Rikishi over time. Each record is the rank of the Rikishi in a specific Basho (sumo tournament)."` - ShikonaHistory []Shikona `json:"shikonaHistory,omitempty" jsonschema:"The historical shikona (ring name) records of the Rikishi over time. Each record is the shikona of the Rikishi in a specific Basho (sumo tournament)."` - MeasurementHistory []Measurement `json:"measurementHistory,omitempty" jsonschema:"The historical measurement records of the Rikishi over time. Each record includes height and weight of the Rikishi in a specific Basho (sumo tournament)."` - CreatedAt *time.Time `json:"createdAt,omitempty" jsonschema:"The timestamp when the Rikishi record was created in the API."` - UpdatedAt *time.Time `json:"updatedAt,omitempty" jsonschema:"The timestamp when the Rikishi record was last updated in the API."` + ID int `json:"id" jsonschema:"The unique identifier for the rikishi (sumo wrestler) in the API."` + SumoDBID int `json:"sumodbId,omitempty" jsonschema:"The SumoDB ID of the rikishi (sumo wrestler)."` + OfficialID int `json:"nskId,omitempty" jsonschema:"The official Nihon Sumo Kyokai (Japan Sumo Association) ID of the rikishi (sumo wrestler)."` + ShikonaEnglish string `json:"shikonaEn,omitempty" jsonschema:"The shikona (ring name) of the rikishi (sumo wrestler) in English."` + ShikonaJapanese string `json:"shikonaJp,omitempty" jsonschema:"The shikona (ring name) of the rikishi (sumo wrestler) in Japanese."` + CurrentRank string `json:"currentRank,omitempty" jsonschema:"The current rank of the rikishi (sumo wrestler)."` + Heya string `json:"heya,omitempty" jsonschema:"The heya (stable) of the rikishi (sumo wrestler)."` + BirthDate *time.Time `json:"birthDate,omitempty" jsonschema:"The birth date of the rikishi (sumo wrestler)."` + Shusshin string `json:"shusshin,omitempty" jsonschema:"The place of birth of the rikishi (sumo wrestler)."` + Height float64 `json:"height,omitempty" jsonschema:"The height of the rikishi (sumo wrestler) in centimeters."` + Weight float64 `json:"weight,omitempty" jsonschema:"The weight of the rikishi (sumo wrestler) in kilograms."` + Debut *BashoID `json:"debut,omitempty" jsonschema:"The ID of the basho (sumo tournament) when the rikishi (sumo wrestler) made their debut in the format YYYYMM."` + Intai *time.Time `json:"intai,omitempty" jsonschema:"The retirement date of the rikishi (sumo wrestler), if retired."` + RankHistory []Rank `json:"rankHistory,omitempty" jsonschema:"The historical rank records of the rikishi (sumo wrestler) over time. Each record is the rank of the rikishi (sumo wrestler) in a specific basho (sumo tournament)."` + ShikonaHistory []Shikona `json:"shikonaHistory,omitempty" jsonschema:"The historical shikona (ring name) records of the rikishi (sumo wrestler) over time. Each record is the shikona (ring name) of the rikishi (sumo wrestler) in a specific basho (sumo tournament)."` + MeasurementHistory []Measurement `json:"measurementHistory,omitempty" jsonschema:"The historical measurement records of the rikishi (sumo wrestler) over time. Each record includes height and weight of the rikishi (sumo wrestler) in a specific basho (sumo tournament)."` + CreatedAt *time.Time `json:"createdAt,omitempty" jsonschema:"The timestamp when the rikishi (sumo wrestler) record was created in the API."` + UpdatedAt *time.Time `json:"updatedAt,omitempty" jsonschema:"The timestamp when the rikishi (sumo wrestler) record was last updated in the API."` } diff --git a/search_rikishis.go b/search_rikishi.go similarity index 51% rename from search_rikishis.go rename to search_rikishi.go index 2c9e7c4..4b87455 100644 --- a/search_rikishis.go +++ b/search_rikishi.go @@ -6,35 +6,38 @@ import ( "net/url" ) -// SearchRikishisAPI defines the methods available for searching Rikishis. -type SearchRikishisAPI interface { - // SearchRikishis calls the GET /api/rikishis endpoint. - SearchRikishis(ctx context.Context, req SearchRikishisRequest) (*SearchRikishisResponse, error) +// SearchRikishiAPI defines the methods available for searching rikishi. +type SearchRikishiAPI interface { + // SearchRikishi calls the GET /api/rikishis endpoint. + // + // Documented bugs: + // - The API always returns the overall total number of rikishi in the database, instead of the total number of matching results like in the other endpoints. + SearchRikishi(ctx context.Context, req SearchRikishiRequest) (*SearchRikishiResponse, error) } -// SearchRikishisRequest represents the request parameters for the SearchRikishis method. -type SearchRikishisRequest struct { - Shikona string `json:"shikona,omitempty" jsonschema:"The English shikona (ring name) to search for. Example: Terunofuji"` +// SearchRikishiRequest represents the request parameters for the SearchRikishi method. +type SearchRikishiRequest struct { + Shikona string `json:"shikona,omitempty" jsonschema:"The shikona (ring name) in English to search for. Example: Terunofuji"` Heya string `json:"heya,omitempty" jsonschema:"The full name in English of the heya (stable) to search for. Example: Isegahama"` SumoDBID int `json:"sumoDBID,omitempty" jsonschema:"The SumoDB ID to search for. Example: 11927 = Terunofuji"` OfficialID int `json:"officialID,omitempty" jsonschema:"The official Nihon Sumo Kyokai (Japan Sumo Association) ID to search for. Example: 3321 = Terunofuji"` - IncludeRetired bool `json:"includeRetired,omitempty" jsonschema:"Whether to include retired rikishis in the results."` - IncludeRanks bool `json:"includeRanks,omitempty" jsonschema:"Whether to include rank records over time in the Rikishi data."` - IncludeShikonas bool `json:"includeShikonas,omitempty" jsonschema:"Whether to include shikona (ring name) records over time in the Rikishi data."` - IncludeMeasurements bool `json:"includeMeasurements,omitempty" jsonschema:"Whether to include measurement records over time in the Rikishi data."` + IncludeRetired bool `json:"includeRetired,omitempty" jsonschema:"Whether to include retired rikishi (sumo wrestlers) in the results."` + IncludeRanks bool `json:"includeRanks,omitempty" jsonschema:"Whether to include rank records over time in the rikishi (sumo wrestler) data."` + IncludeShikonas bool `json:"includeShikonas,omitempty" jsonschema:"Whether to include shikona (ring name) records over time in the rikishi (sumo wrestler) data."` + IncludeMeasurements bool `json:"includeMeasurements,omitempty" jsonschema:"Whether to include measurement records over time in the rikishi (sumo wrestler) data."` Limit int `json:"limit,omitempty" jsonschema:"The maximum number of results to return."` Skip int `json:"skip,omitempty" jsonschema:"The number of results to skip over for pagination."` } -// SearchRikishisResponse represents the response from the SearchRikishis method. -type SearchRikishisResponse struct { - Limit int `json:"limit" jsonschema:"The maximum number of results as specified in the request."` - Skip int `json:"skip" jsonschema:"The number of results to skip over as specified in the request."` - Total int `json:"total" jsonschema:"The overall total number of Rikishis available in the API."` - Rikishis []Rikishi `json:"records" jsonschema:"The list of Rikishis matching the search criteria."` +// SearchRikishiResponse represents the response from the SearchRikishi method. +type SearchRikishiResponse struct { + Limit int `json:"limit" jsonschema:"The maximum number of results that were returned."` + Skip int `json:"skip" jsonschema:"The number of results that were skipped over."` + Total int `json:"total" jsonschema:"The total number of matching results."` + Rikishi []Rikishi `json:"records" jsonschema:"The list of rikishi (sumo wrestlers) matching the search criteria."` } -func (c *client) SearchRikishis(ctx context.Context, req SearchRikishisRequest) (*SearchRikishisResponse, error) { +func (c *client) SearchRikishi(ctx context.Context, req SearchRikishiRequest) (*SearchRikishiResponse, error) { query := make(url.Values) if req.Shikona != "" { query.Set("shikonaEn", req.Shikona) @@ -61,10 +64,10 @@ func (c *client) SearchRikishis(ctx context.Context, req SearchRikishisRequest) query.Set("shikonas", "true") } if req.Limit > 0 { - query.Set("limit", fmt.Sprintf("%d", req.Limit)) + query.Set("limit", fmt.Sprint(req.Limit)) } if req.Skip > 0 { - query.Set("skip", fmt.Sprintf("%d", req.Skip)) + query.Set("skip", fmt.Sprint(req.Skip)) } - return getObject[SearchRikishisResponse](ctx, c, "/rikishis", query) + return getObject[SearchRikishiResponse](ctx, c, "/rikishis", query) } diff --git a/search_rikishis_test.go b/search_rikishi_test.go similarity index 86% rename from search_rikishis_test.go rename to search_rikishi_test.go index c1e6fe1..22c4c01 100644 --- a/search_rikishis_test.go +++ b/search_rikishi_test.go @@ -12,7 +12,7 @@ import ( "github.com/sumo-mcp/sumoapi-go" ) -func TestClient_SearchRikishis(t *testing.T) { +func TestClient_SearchRikishi(t *testing.T) { t.Run("search by shikona", func(t *testing.T) { g := NewWithT(t) @@ -44,16 +44,16 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Shikona: "Terunofuji", }) g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) g.Expect(resp.Total).To(Equal(100)) - g.Expect(resp.Rikishis).To(HaveLen(1)) - g.Expect(resp.Rikishis[0].ID).To(Equal(1)) - g.Expect(resp.Rikishis[0].ShikonaEnglish).To(Equal("Terunofuji")) + g.Expect(resp.Rikishi).To(HaveLen(1)) + g.Expect(resp.Rikishi[0].ID).To(Equal(1)) + g.Expect(resp.Rikishi[0].ShikonaEnglish).To(Equal("Terunofuji")) }) t.Run("search by heya", func(t *testing.T) { @@ -80,14 +80,14 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Heya: "Isegahama", }) g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) g.Expect(resp.Total).To(Equal(50)) - g.Expect(resp.Rikishis).To(BeEmpty()) + g.Expect(resp.Rikishi).To(BeEmpty()) }) t.Run("search by SumoDB ID", func(t *testing.T) { @@ -117,14 +117,14 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ SumoDBID: 11927, }) g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) - g.Expect(resp.Rikishis).To(HaveLen(1)) - g.Expect(resp.Rikishis[0].SumoDBID).To(Equal(11927)) + g.Expect(resp.Rikishi).To(HaveLen(1)) + g.Expect(resp.Rikishi[0].SumoDBID).To(Equal(11927)) }) t.Run("search by official ID", func(t *testing.T) { @@ -154,14 +154,14 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ OfficialID: 3321, }) g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) - g.Expect(resp.Rikishis).To(HaveLen(1)) - g.Expect(resp.Rikishis[0].OfficialID).To(Equal(3321)) + g.Expect(resp.Rikishi).To(HaveLen(1)) + g.Expect(resp.Rikishi[0].OfficialID).To(Equal(3321)) }) t.Run("search with include retired flag", func(t *testing.T) { @@ -186,7 +186,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ IncludeRetired: true, }) @@ -216,7 +216,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ IncludeMeasurements: true, }) @@ -246,7 +246,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ IncludeRanks: true, }) @@ -276,7 +276,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ IncludeShikonas: true, }) @@ -306,7 +306,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Limit: 10, }) @@ -337,7 +337,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Skip: 20, }) @@ -376,7 +376,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Shikona: "Hakuho", Heya: "Miyagino", IncludeRetired: true, @@ -426,7 +426,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) @@ -457,7 +457,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - _, err := client.SearchRikishis(ctx, sumoapi.SearchRikishisRequest{}) + _, err := client.SearchRikishi(ctx, sumoapi.SearchRikishiRequest{}) g.Expect(err).ToNot(HaveOccurred()) }) @@ -472,7 +472,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("error making http request")) @@ -490,7 +490,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("received HTTP 404 response")) @@ -509,7 +509,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("received HTTP 500 response with empty body")) @@ -527,7 +527,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("received HTTP 400 response")) @@ -546,7 +546,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body")) @@ -564,7 +564,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body")) @@ -582,7 +582,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("received HTTP 403 response")) @@ -601,7 +601,7 @@ func TestClient_SearchRikishis(t *testing.T) { } client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport})) - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{}) + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{}) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("received HTTP 503 response")) diff --git a/shikona.go b/shikona.go index ec22347..d900e91 100644 --- a/shikona.go +++ b/shikona.go @@ -2,9 +2,9 @@ package sumoapi // Shikona represents a sumo wrestler's shikona (ring name) in a specific Basho (sumo tournament). type Shikona struct { - ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the Shikona in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the Rikishi in the API."` - BashoID BashoID `json:"bashoId" jsonschema:"The ID of the Basho (sumo tournament) in the format YYYYMM."` - RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the Rikishi (sumo wrestler) in the API."` - ShikonaEnglish string `json:"shikonaEn,omitempty" jsonschema:"The shikona (ring name) in English of the Rikishi in the specific Basho (sumo tournament)."` - ShikonaJapanese string `json:"shikonaJp,omitempty" jsonschema:"The shikona (ring name) in Japanese of the Rikishi in the specific Basho (sumo tournament)."` + ID RikishiChangeID `json:"id" jsonschema:"The unique identifier for the shikona in the format {bashoID}-{rikishiID} where {bashoID} is in the format YYYYMM and {rikishiID} is the unique identifier for the rikishi in the API."` + BashoID BashoID `json:"bashoId" jsonschema:"The ID of the basho (sumo tournament) in the format YYYYMM."` + RikishiID int `json:"rikishiId" jsonschema:"The unique identifier for the rikishi (sumo wrestler) in the API."` + ShikonaEnglish string `json:"shikonaEn,omitempty" jsonschema:"The shikona (ring name) in English of the rikishi in the specific basho (sumo tournament)."` + ShikonaJapanese string `json:"shikonaJp,omitempty" jsonschema:"The shikona (ring name) in Japanese of the rikishi in the specific basho (sumo tournament)."` } diff --git a/tests/integration/get_rikishi_stats_test.go b/tests/integration/get_rikishi_stats_test.go index fae5fd6..443973c 100644 --- a/tests/integration/get_rikishi_stats_test.go +++ b/tests/integration/get_rikishi_stats_test.go @@ -21,7 +21,6 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) - // Verify overall stats g.Expect(resp.Basho).To(Equal(81)) g.Expect(resp.Yusho).To(Equal(13)) g.Expect(resp.TotalMatches).To(Equal(798)) @@ -29,13 +28,11 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(resp.TotalLosses).To(Equal(275)) g.Expect(resp.TotalAbsences).To(Equal(231)) - // Verify sansho (special prizes) g.Expect(resp.Sansho).To(HaveLen(3)) g.Expect(resp.Sansho["Gino-sho"]).To(Equal(3)) g.Expect(resp.Sansho["Kanto-sho"]).To(Equal(3)) g.Expect(resp.Sansho["Shukun-sho"]).To(Equal(3)) - // Verify basho by division g.Expect(resp.BashoByDivision["Jonokuchi"]).To(Equal(1)) g.Expect(resp.BashoByDivision["Jonidan"]).To(Equal(2)) g.Expect(resp.BashoByDivision["Sandanme"]).To(Equal(4)) @@ -43,12 +40,10 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(resp.BashoByDivision["Juryo"]).To(Equal(7)) g.Expect(resp.BashoByDivision["Makuuchi"]).To(Equal(52)) - // Verify yusho by division g.Expect(resp.YushoByDivision["Makushita"]).To(Equal(1)) g.Expect(resp.YushoByDivision["Juryo"]).To(Equal(2)) g.Expect(resp.YushoByDivision["Makuuchi"]).To(Equal(10)) - // Verify wins by division g.Expect(resp.WinsByDivision["Jonokuchi"]).To(Equal(5)) g.Expect(resp.WinsByDivision["Jonidan"]).To(Equal(13)) g.Expect(resp.WinsByDivision["Sandanme"]).To(Equal(13)) @@ -56,7 +51,6 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(resp.WinsByDivision["Juryo"]).To(Equal(61)) g.Expect(resp.WinsByDivision["Makuuchi"]).To(Equal(366)) - // Verify losses by division g.Expect(resp.LossByDivision["Jonokuchi"]).To(Equal(2)) g.Expect(resp.LossByDivision["Jonidan"]).To(Equal(1)) g.Expect(resp.LossByDivision["Sandanme"]).To(Equal(1)) @@ -64,7 +58,6 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(resp.LossByDivision["Juryo"]).To(Equal(38)) g.Expect(resp.LossByDivision["Makuuchi"]).To(Equal(207)) - // Verify absences by division g.Expect(resp.AbsenceByDivision["Jonokuchi"]).To(Equal(0)) g.Expect(resp.AbsenceByDivision["Jonidan"]).To(Equal(0)) g.Expect(resp.AbsenceByDivision["Sandanme"]).To(Equal(14)) @@ -72,7 +65,6 @@ func TestIntegration_GetRikishiStats(t *testing.T) { g.Expect(resp.AbsenceByDivision["Juryo"]).To(Equal(6)) g.Expect(resp.AbsenceByDivision["Makuuchi"]).To(Equal(197)) - // Verify total matches by division g.Expect(resp.TotalMatchesByDivision["Jonokuchi"]).To(Equal(7)) g.Expect(resp.TotalMatchesByDivision["Jonidan"]).To(Equal(14)) g.Expect(resp.TotalMatchesByDivision["Sandanme"]).To(Equal(14)) diff --git a/tests/integration/get_rikishi_test.go b/tests/integration/get_rikishi_test.go index 3dacd59..6cee7c8 100644 --- a/tests/integration/get_rikishi_test.go +++ b/tests/integration/get_rikishi_test.go @@ -15,7 +15,7 @@ func TestIntegration_GetRikishi(t *testing.T) { client := sumoapi.New() resp, err := client.GetRikishi(context.Background(), sumoapi.GetRikishiRequest{ - RikishiID: 45, + RikishiID: 45, // Terunofuji IncludeRanks: true, IncludeShikonas: true, IncludeMeasurements: true, diff --git a/tests/integration/list_kimarite_matches_test.go b/tests/integration/list_kimarite_matches_test.go new file mode 100644 index 0000000..b895581 --- /dev/null +++ b/tests/integration/list_kimarite_matches_test.go @@ -0,0 +1,53 @@ +package integration_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestIntegration_ListKimariteMatches(t *testing.T) { + g := NewWithT(t) + + client := sumoapi.New() + + resp, err := client.ListKimariteMatches(context.Background(), sumoapi.ListKimariteMatchesRequest{ + Kimarite: "tsumatori", + Limit: 1, + Skip: 1, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Limit).To(Equal(1)) + g.Expect(resp.Skip).To(Equal(1)) + g.Expect(resp.Total).To(BeNumerically(">", 1)) + g.Expect(resp.Matches).To(HaveLen(1)) + + bashoID := sumoapi.BashoID{Year: 1958, Month: 11} + + match := resp.Matches[0] + g.Expect(match.ID).NotTo(BeNil()) + g.Expect(match.ID.BashoID).To(Equal(bashoID)) + g.Expect(match.ID.Day).To(Equal(7)) + g.Expect(match.ID.MatchNumber).To(Equal(15)) + g.Expect(match.ID.EastID).To(Equal(1339)) + g.Expect(match.ID.WestID).To(Equal(1351)) + g.Expect(match.BashoID).To(Equal(bashoID)) + g.Expect(match.Division).To(Equal("Juryo")) + g.Expect(match.Day).To(Equal(7)) + g.Expect(match.MatchNumber).To(Equal(15)) + g.Expect(match.EastID).To(Equal(1339)) + g.Expect(match.EastShikona).To(Equal("Nanatsuumi")) + g.Expect(match.EastRank).To(Equal("Juryo 9 East")) + g.Expect(match.WestID).To(Equal(1351)) + g.Expect(match.WestShikona).To(Equal("Maegashio")) + g.Expect(match.WestRank).To(Equal("Juryo 13 West")) + g.Expect(match.Kimarite).To(Equal("tsumatori")) + g.Expect(match.WinnerID).To(Equal(1339)) + g.Expect(match.WinnerEnglish).To(Equal("Nanatsuumi")) + g.Expect(match.WinnerJapanese).To(Equal("")) +} diff --git a/tests/integration/list_measurement_changes_test.go b/tests/integration/list_measurement_changes_test.go index d0aace1..77766fe 100644 --- a/tests/integration/list_measurement_changes_test.go +++ b/tests/integration/list_measurement_changes_test.go @@ -15,7 +15,7 @@ func TestIntegration_ListMeasurementChanges(t *testing.T) { client := sumoapi.New() resp, err := client.ListMeasurementChanges(context.Background(), sumoapi.ListRikishiChangesRequest{ - RikishiID: 3081, + RikishiID: 3081, // Hakuho BashoID: &sumoapi.BashoID{ Year: 2021, Month: 3, @@ -24,7 +24,7 @@ func TestIntegration_ListMeasurementChanges(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) - g.Expect(len(resp)).To(BeNumerically(">", 1)) // There's a bug in the API, the bashoId filter is not working. + g.Expect(len(resp)).To(BeNumerically(">", 1)) // Bug: The bashoId filter is not working. expectedBashoID := sumoapi.BashoID{Year: 2021, Month: 3} diff --git a/tests/integration/list_rank_changes_test.go b/tests/integration/list_rank_changes_test.go index 58defd6..33708dd 100644 --- a/tests/integration/list_rank_changes_test.go +++ b/tests/integration/list_rank_changes_test.go @@ -17,7 +17,7 @@ func TestIntegration_ListRankChanges(t *testing.T) { // Here we test a specific Rikishi that had rank changes to make sure the API // is returning exactly one change when filtering by RikishiID and BashoID. resp, err := client.ListRankChanges(context.Background(), sumoapi.ListRikishiChangesRequest{ - RikishiID: 3081, + RikishiID: 3081, // Hakuho BashoID: &sumoapi.BashoID{ Year: 2021, Month: 9, diff --git a/tests/integration/list_rikishi_matches_against_opponent_test.go b/tests/integration/list_rikishi_matches_against_opponent_test.go new file mode 100644 index 0000000..ee5bcbf --- /dev/null +++ b/tests/integration/list_rikishi_matches_against_opponent_test.go @@ -0,0 +1,55 @@ +package integration_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestIntegration_ListRikishiMatchesAgainstOpponent(t *testing.T) { + g := NewWithT(t) + + client := sumoapi.New() + + resp, err := client.ListRikishiMatchesAgainstOpponent(context.Background(), sumoapi.ListRikishiMatchesAgainstOpponentRequest{ + RikishiID: 45, // Terunofuji + OpponentID: 3081, // Hakuho + Limit: 1, + Skip: 1, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + g.Expect(resp.Matches).To(HaveLen(1)) + g.Expect(resp.RikishiWins).To(Equal(0)) + g.Expect(resp.OpponentWins).To(Equal(1)) + g.Expect(resp.KimariteWins).To(Equal(map[string]int{})) + g.Expect(resp.KimariteLosses).To(Equal(map[string]int{"yorikiri": 1})) + + // Bug: The limit and skip outputs are always 0. + g.Expect(resp.Limit).To(Equal(0)) + g.Expect(resp.Skip).To(Equal(0)) + + match := resp.Matches[0] + g.Expect(match.BashoID).To(Equal(sumoapi.BashoID{Year: 2017, Month: 5})) + g.Expect(match.Division).To(Equal("Makuuchi")) + g.Expect(match.Day).To(Equal(14)) + g.Expect(match.MatchNumber).To(Equal(19)) + g.Expect(match.EastID).To(Equal(45)) + g.Expect(match.EastShikona).To(Equal("Terunofuji")) + g.Expect(match.EastRank).To(Equal("Ozeki 1 East")) + g.Expect(match.WestID).To(Equal(3081)) + g.Expect(match.WestShikona).To(Equal("Hakuho")) + g.Expect(match.WestRank).To(Equal("Yokozuna 2 West")) + g.Expect(match.Kimarite).To(Equal("yorikiri")) + g.Expect(match.WinnerID).To(Equal(3081)) + g.Expect(match.WinnerEnglish).To(Equal("Hakuho")) + g.Expect(match.WinnerJapanese).To(Equal("")) + + // Bug: The match ID is not returned. + g.Expect(match.ID).To(BeNil()) +} diff --git a/tests/integration/list_rikishi_matches_test.go b/tests/integration/list_rikishi_matches_test.go new file mode 100644 index 0000000..01b108d --- /dev/null +++ b/tests/integration/list_rikishi_matches_test.go @@ -0,0 +1,49 @@ +package integration_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestIntegration_ListRikishiMatches(t *testing.T) { + g := NewWithT(t) + + client := sumoapi.New() + + resp, err := client.ListRikishiMatches(context.Background(), sumoapi.ListRikishiMatchesRequest{ + RikishiID: 45, // Terunofuji + Limit: 1, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Total).To(Equal(1)) + g.Expect(resp.Matches).To(HaveLen(1)) + + // Bug: The limit and skip outputs are always 0. + g.Expect(resp.Limit).To(Equal(0)) + g.Expect(resp.Skip).To(Equal(0)) + + match := resp.Matches[0] + g.Expect(match.BashoID).To(Equal(sumoapi.BashoID{Year: 2025, Month: 1})) + g.Expect(match.Division).To(Equal("Makuuchi")) + g.Expect(match.Day).To(Equal(5)) + g.Expect(match.MatchNumber).To(Equal(21)) + g.Expect(match.EastID).To(Equal(45)) + g.Expect(match.EastShikona).To(Equal("Terunofuji")) + g.Expect(match.EastRank).To(Equal("Yokozuna 1 East")) + g.Expect(match.WestID).To(Equal(56)) + g.Expect(match.WestShikona).To(Equal("Gonoyama")) + g.Expect(match.WestRank).To(Equal("Maegashira 3 East")) + g.Expect(match.Kimarite).To(Equal("fusen")) + g.Expect(match.WinnerID).To(Equal(56)) + g.Expect(match.WinnerEnglish).To(Equal("Gonoyama")) + g.Expect(match.WinnerJapanese).To(Equal("")) + + // Bug: The match ID is not returned. + g.Expect(match.ID).To(BeNil()) +} diff --git a/tests/integration/list_shikona_changes_test.go b/tests/integration/list_shikona_changes_test.go index dbb302c..57a4fe1 100644 --- a/tests/integration/list_shikona_changes_test.go +++ b/tests/integration/list_shikona_changes_test.go @@ -17,7 +17,7 @@ func TestIntegration_ListShikonaChanges(t *testing.T) { // Here we test a specific Rikishi that had shikona changes to make sure the API // is returning exactly one change when filtering by RikishiID and BashoID. resp, err := client.ListShikonaChanges(context.Background(), sumoapi.ListRikishiChangesRequest{ - RikishiID: 8857, + RikishiID: 8857, // Yoshinofuji BashoID: &sumoapi.BashoID{ Year: 2025, Month: 11, diff --git a/tests/integration/search_rikishis_test.go b/tests/integration/search_rikishi_test.go similarity index 66% rename from tests/integration/search_rikishis_test.go rename to tests/integration/search_rikishi_test.go index b0cf674..83e8e2c 100644 --- a/tests/integration/search_rikishis_test.go +++ b/tests/integration/search_rikishi_test.go @@ -9,12 +9,12 @@ import ( "github.com/sumo-mcp/sumoapi-go" ) -func TestIntegration_SearchRikishis(t *testing.T) { +func TestIntegration_SearchRikishi(t *testing.T) { g := NewWithT(t) client := sumoapi.New() - resp, err := client.SearchRikishis(context.Background(), sumoapi.SearchRikishisRequest{ + resp, err := client.SearchRikishi(context.Background(), sumoapi.SearchRikishiRequest{ Limit: 1, Shikona: "Hakuho Sho", IncludeRetired: true, @@ -25,9 +25,14 @@ func TestIntegration_SearchRikishis(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(resp).ToNot(BeNil()) - g.Expect(resp.Rikishis).To(HaveLen(1)) + g.Expect(resp.Limit).To(Equal(1)) + g.Expect(resp.Skip).To(Equal(0)) + g.Expect(resp.Rikishi).To(HaveLen(1)) - hakuho := resp.Rikishis[0] + // Bug: The total is always the overall total amount of rikishi in the database. + g.Expect(resp.Total).To(BeNumerically(">", 9000)) + + hakuho := resp.Rikishi[0] g.Expect(hakuho.ShikonaEnglish).To(Equal("Hakuho Sho")) g.Expect(hakuho.Heya).To(Equal("Miyagino")) g.Expect(hakuho.Debut).To(Equal(&sumoapi.BashoID{Year: 2001, Month: 3}))