diff --git a/client.go b/client.go index a6e4606..4de0873 100644 --- a/client.go +++ b/client.go @@ -14,6 +14,7 @@ import ( type Client interface { SearchRikishisAPI GetRikishiAPI + GetRikishiStatsAPI ListRankChangesAPI ListShikonaChangesAPI ListMeasurementChangesAPI diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..0b94ed5 --- /dev/null +++ b/client_test.go @@ -0,0 +1,17 @@ +package sumoapi_test + +import "net/http" + +type mockTransport struct { + validateRequest func(*http.Request) error + response *http.Response +} + +func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if m.validateRequest != nil { + if err := m.validateRequest(req); err != nil { + return nil, err + } + } + return m.response, nil +} diff --git a/get_rikishi_stats.go b/get_rikishi_stats.go new file mode 100644 index 0000000..0bd5206 --- /dev/null +++ b/get_rikishi_stats.go @@ -0,0 +1,39 @@ +package sumoapi + +import ( + "context" + "fmt" +) + +// 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) +} + +// 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"` +} + +// 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."` +} + +func (c *client) GetRikishiStats(ctx context.Context, req GetRikishiStatsRequest) (*GetRikishiStatsResponse, error) { + path := fmt.Sprintf("/rikishi/%d/stats", req.RikishiID) + return getObject[GetRikishiStatsResponse](ctx, c, path, nil) +} diff --git a/get_rikishi_stats_test.go b/get_rikishi_stats_test.go new file mode 100644 index 0000000..605f57f --- /dev/null +++ b/get_rikishi_stats_test.go @@ -0,0 +1,169 @@ +package sumoapi_test + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestClient_GetRikishiStats(t *testing.T) { + t.Run("get rikishi stats by ID", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "basho": 81, + "yusho": 13, + "totalMatches": 798, + "totalWins": 523, + "totalLosses": 275, + "totalAbsences": 231, + "sansho": { + "Gino-sho": 3, + "Kanto-sho": 3, + "Shukun-sho": 3 + }, + "bashoByDivision": { + "Makuuchi": 52, + "Juryo": 7 + }, + "yushoByDivision": { + "Makuuchi": 10, + "Juryo": 2 + }, + "winsByDivision": { + "Makuuchi": 366, + "Juryo": 61 + }, + "lossByDivision": { + "Makuuchi": 207, + "Juryo": 38 + }, + "absenceByDivision": { + "Makuuchi": 197, + "Juryo": 6 + }, + "totalByDivision": { + "Makuuchi": 573, + "Juryo": 99 + } + }` + + 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/stats")) + g.Expect(req.URL.RawQuery).To(BeEmpty()) + 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.GetRikishiStats(context.Background(), sumoapi.GetRikishiStatsRequest{ + RikishiID: 45, + }) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(resp).ToNot(BeNil()) + g.Expect(resp.Basho).To(Equal(81)) + g.Expect(resp.Yusho).To(Equal(13)) + g.Expect(resp.TotalMatches).To(Equal(798)) + g.Expect(resp.TotalWins).To(Equal(523)) + g.Expect(resp.TotalLosses).To(Equal(275)) + g.Expect(resp.TotalAbsences).To(Equal(231)) + + // Check sansho + 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)) + + // Check division stats + g.Expect(resp.BashoByDivision["Makuuchi"]).To(Equal(52)) + g.Expect(resp.YushoByDivision["Makuuchi"]).To(Equal(10)) + g.Expect(resp.WinsByDivision["Makuuchi"]).To(Equal(366)) + g.Expect(resp.LossByDivision["Makuuchi"]).To(Equal(207)) + g.Expect(resp.AbsenceByDivision["Makuuchi"]).To(Equal(197)) + g.Expect(resp.TotalMatchesByDivision["Makuuchi"]).To(Equal(573)) + }) + + t.Run("context is propagated", func(t *testing.T) { + g := NewWithT(t) + + mockResp := `{ + "basho": 10, + "yusho": 1 + }` + + 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.GetRikishiStats(ctx, sumoapi.GetRikishiStatsRequest{ + 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.GetRikishiStats(context.Background(), sumoapi.GetRikishiStatsRequest{ + 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.GetRikishiStats(context.Background(), sumoapi.GetRikishiStatsRequest{ + 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/search_rikishis_test.go b/search_rikishis_test.go index 613bcb7..c1e6fe1 100644 --- a/search_rikishis_test.go +++ b/search_rikishis_test.go @@ -12,20 +12,6 @@ import ( "github.com/sumo-mcp/sumoapi-go" ) -type mockTransport struct { - validateRequest func(*http.Request) error - response *http.Response -} - -func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if m.validateRequest != nil { - if err := m.validateRequest(req); err != nil { - return nil, err - } - } - return m.response, nil -} - func TestClient_SearchRikishis(t *testing.T) { t.Run("search by shikona", func(t *testing.T) { g := NewWithT(t) diff --git a/tests/integration/get_rikishi_stats_test.go b/tests/integration/get_rikishi_stats_test.go new file mode 100644 index 0000000..fae5fd6 --- /dev/null +++ b/tests/integration/get_rikishi_stats_test.go @@ -0,0 +1,82 @@ +package integration_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/sumo-mcp/sumoapi-go" +) + +func TestIntegration_GetRikishiStats(t *testing.T) { + g := NewWithT(t) + + client := sumoapi.New() + + resp, err := client.GetRikishiStats(context.Background(), sumoapi.GetRikishiStatsRequest{ + RikishiID: 45, // Terunofuji + }) + + 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)) + g.Expect(resp.TotalWins).To(Equal(523)) + 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)) + g.Expect(resp.BashoByDivision["Makushita"]).To(Equal(15)) + 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)) + g.Expect(resp.WinsByDivision["Makushita"]).To(Equal(65)) + 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)) + g.Expect(resp.LossByDivision["Makushita"]).To(Equal(26)) + 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)) + g.Expect(resp.AbsenceByDivision["Makushita"]).To(Equal(14)) + 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)) + g.Expect(resp.TotalMatchesByDivision["Makushita"]).To(Equal(91)) + g.Expect(resp.TotalMatchesByDivision["Juryo"]).To(Equal(99)) + g.Expect(resp.TotalMatchesByDivision["Makuuchi"]).To(Equal(573)) +}