Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions banzuke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package sumoapi

// Banzuke represents the ranking list of rikishi in a basho division.
type Banzuke struct {
BashoID BashoID `json:"bashoId" jsonschema:"The unique identifier for the basho (sumo tournament)."`
Division string `json:"division" jsonschema:"The division of the basho (sumo tournament). One of Makuuchi, Juryo, Makushita, Sandanme, Jonidan, Jonokuchi."`
East []RikishiBanzuke `json:"east" jsonschema:"The banzuke (ranking list) for the east side of the division."`
West []RikishiBanzuke `json:"west" jsonschema:"The banzuke (ranking list) for the west side of the division."`
}

// RikishiBanzuke represents a rikishi's ranking information in a basho division.
type RikishiBanzuke struct {
Side string `json:"side" jsonschema:"The side of the rikishi in the banzuke (ranking list). Either East or West."`
RikishiID int `json:"rikishiID" jsonschema:"The unique identifier for the rikishi (sumo wrestler)."`
ShikonaEnglish string `json:"shikonaEn" jsonschema:"The shikona (ring name) in English of the rikishi."`
ShikonaJapanese string `json:"shikonaJp" jsonschema:"The shikona (ring name) in Japanese of the rikishi."`
HumanReadableRankName string `json:"rank" jsonschema:"The human-readable name of the rank (e.g., Maegashira 1 East, Ozeki 2 West) of the rikishi (sumo wrestler) in the specific basho (sumo tournament)."`
NumericRankName int `json:"rankValue" jsonschema:"The numeric name of the rank of the rikishi (sumo wrestler) in the specific basho (sumo tournament)."`
Wins int `json:"wins" jsonschema:"The number of wins the rikishi (sumo wrestler) achieved in the specific basho (sumo tournament)."`
Losses int `json:"losses" jsonschema:"The number of losses the rikishi (sumo wrestler) had in the specific basho (sumo tournament)."`
Absences int `json:"absences" jsonschema:"The number of absences the rikishi (sumo wrestler) had in the specific basho (sumo tournament)."`
Matches []RikishiBanzukeMatch `json:"record" jsonschema:"The list of matches the rikishi (sumo wrestler) had or will have in the specific basho (sumo tournament)."`
}

// RikishiBanzukeMatch represents a match against an opponent in the banzuke.
type RikishiBanzukeMatch struct {
OpponentShikonaEnglish string `json:"opponentShikonaEn" jsonschema:"The shikona (ring name) in English of the opponent rikishi (sumo wrestler)."`
OpponentShikonaJapanese string `json:"opponentShikonaJp" jsonschema:"The shikona (ring name) in Japanese of the opponent rikishi (sumo wrestler)."`
OpponentID int `json:"opponentID" jsonschema:"The unique identifier for the opponent rikishi (sumo wrestler)."`
Result string `json:"result,omitempty" jsonschema:"The result of the match for the rikishi (sumo wrestler). One of win, loss, absent, fusen win (forfeit win), fusen loss (forfeit loss). This field may be omitted if the match has not yet occurred."`
Kimarite string `json:"kimarite,omitempty" jsonschema:"The kimarite (technique) used in the match, if the match has already occurred."`
}
4 changes: 4 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Client interface {
ListRikishiMatchesAPI
ListRikishiMatchesAgainstOpponentAPI
GetBashoAPI
GetBanzukeAPI
GetBashoWithTorikumiAPI
ListKimariteAPI
ListKimariteMatchesAPI
Expand Down Expand Up @@ -70,6 +71,9 @@ func (c *client) doRequest(ctx context.Context, method, path string, query url.V
if err != nil {
return nil, fmt.Errorf("error creating http request: %w", err)
}
if obj != nil {
req.Header.Set("Content-Type", "application/json")
}

resp, err := c.httpClient.Do(req)
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions get_banzuke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package sumoapi

import (
"context"
"fmt"
)

// GetBanzukeAPI defines the methods available for retrieving a banzuke.
type GetBanzukeAPI interface {
// GetBanzuke calls the GET /api/basho/{bashoID}/banzuke/{division} endpoint.
GetBanzuke(ctx context.Context, req GetBanzukeRequest) (*Banzuke, error)
}

// GetBanzukeRequest represents the request parameters for the GetBanzuke method.
type GetBanzukeRequest struct {
BashoID BashoID `json:"bashoId" jsonschema:"The unique identifier of the basho (sumo tournament) to retrieve the banzuke (ranking list) for. Format: YYYYMM, e.g., 202401 for the January 2024 basho."`
Division string `json:"division" jsonschema:"The division of the basho (sumo tournament) to retrieve the banzuke (ranking list) for. One of Makuuchi, Juryo, Makushita, Sandanme, Jonidan, Jonokuchi."`
}

func (c *client) GetBanzuke(ctx context.Context, req GetBanzukeRequest) (*Banzuke, error) {
path := fmt.Sprintf("/basho/%s/banzuke/%s", req.BashoID.String(), req.Division)
return getObject[Banzuke](ctx, c, path, nil)
}
272 changes: 272 additions & 0 deletions get_banzuke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package sumoapi_test

import (
"context"
"io"
"net/http"
"strings"
"testing"

. "github.com/onsi/gomega"

"github.com/sumo-mcp/sumoapi-go"
)

func TestClient_GetBanzuke(t *testing.T) {
t.Run("get banzuke for division", func(t *testing.T) {
g := NewWithT(t)

mockResp := `{
"bashoId": "202511",
"division": "Makuuchi",
"east": [
{
"side": "East",
"rikishiID": 8850,
"shikonaEn": "Onosato",
"shikonaJp": "大の里",
"rankValue": 101,
"rank": "Yokozuna 1 East",
"record": [
{
"result": "win",
"opponentShikonaEn": "Takayasu",
"opponentShikonaJp": "高安",
"opponentID": 44,
"kimarite": "yorikiri"
}
],
"wins": 11,
"losses": 4,
"absences": 0
}
],
"west": [
{
"side": "West",
"rikishiID": 19,
"shikonaEn": "Hoshoryu",
"shikonaJp": "豊昇龍",
"rankValue": 201,
"rank": "Ozeki 1 West",
"record": [],
"wins": 12,
"losses": 3,
"absences": 0
}
]
}`

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/basho/202511/banzuke/Makuuchi"))
return nil
},
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(mockResp)),
},
}

client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport}))
bashoID := sumoapi.BashoID{Year: 2025, Month: 11}
resp, err := client.GetBanzuke(context.Background(), sumoapi.GetBanzukeRequest{
BashoID: bashoID,
Division: "Makuuchi",
})

g.Expect(err).ToNot(HaveOccurred())
g.Expect(resp).ToNot(BeNil())
g.Expect(resp.BashoID).To(Equal(bashoID))
g.Expect(resp.Division).To(Equal("Makuuchi"))

// Check east side
g.Expect(resp.East).To(HaveLen(1))
g.Expect(resp.East[0].Side).To(Equal("East"))
g.Expect(resp.East[0].RikishiID).To(Equal(8850))
g.Expect(resp.East[0].ShikonaEnglish).To(Equal("Onosato"))
g.Expect(resp.East[0].ShikonaJapanese).To(Equal("大の里"))
g.Expect(resp.East[0].HumanReadableRankName).To(Equal("Yokozuna 1 East"))
g.Expect(resp.East[0].NumericRankName).To(Equal(101))
g.Expect(resp.East[0].Wins).To(Equal(11))
g.Expect(resp.East[0].Losses).To(Equal(4))
g.Expect(resp.East[0].Absences).To(Equal(0))

// Check match record
g.Expect(resp.East[0].Matches).To(HaveLen(1))
g.Expect(resp.East[0].Matches[0].Result).To(Equal("win"))
g.Expect(resp.East[0].Matches[0].OpponentShikonaEnglish).To(Equal("Takayasu"))
g.Expect(resp.East[0].Matches[0].OpponentShikonaJapanese).To(Equal("高安"))
g.Expect(resp.East[0].Matches[0].OpponentID).To(Equal(44))
g.Expect(resp.East[0].Matches[0].Kimarite).To(Equal("yorikiri"))

// Check west side
g.Expect(resp.West).To(HaveLen(1))
g.Expect(resp.West[0].Side).To(Equal("West"))
g.Expect(resp.West[0].RikishiID).To(Equal(19))
g.Expect(resp.West[0].ShikonaEnglish).To(Equal("Hoshoryu"))
})

t.Run("get banzuke for different division", func(t *testing.T) {
g := NewWithT(t)

mockResp := `{
"bashoId": "202501",
"division": "Juryo",
"east": [],
"west": []
}`

transport := &mockTransport{
validateRequest: func(req *http.Request) error {
g.Expect(req.URL.Path).To(Equal("/api/basho/202501/banzuke/Juryo"))
return nil
},
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(mockResp)),
},
}

client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport}))
bashoID := sumoapi.BashoID{Year: 2025, Month: 1}
resp, err := client.GetBanzuke(context.Background(), sumoapi.GetBanzukeRequest{
BashoID: bashoID,
Division: "Juryo",
})

g.Expect(err).ToNot(HaveOccurred())
g.Expect(resp).ToNot(BeNil())
g.Expect(resp.BashoID).To(Equal(bashoID))
g.Expect(resp.Division).To(Equal("Juryo"))
})

t.Run("banzuke with all result types", func(t *testing.T) {
g := NewWithT(t)

mockResp := `{
"bashoId": "202511",
"division": "Makuuchi",
"east": [
{
"side": "East",
"rikishiID": 1,
"shikonaEn": "TestRikishi",
"shikonaJp": "テスト力士",
"rankValue": 101,
"rank": "Yokozuna 1 East",
"record": [
{"result": "win", "opponentShikonaEn": "A", "opponentShikonaJp": "あ", "opponentID": 2, "kimarite": "yorikiri"},
{"result": "loss", "opponentShikonaEn": "B", "opponentShikonaJp": "い", "opponentID": 3, "kimarite": "oshidashi"},
{"result": "absent", "opponentShikonaEn": "C", "opponentShikonaJp": "う", "opponentID": 4},
{"result": "fusen win", "opponentShikonaEn": "D", "opponentShikonaJp": "え", "opponentID": 5, "kimarite": "fusen"},
{"result": "fusen loss", "opponentShikonaEn": "E", "opponentShikonaJp": "お", "opponentID": 6, "kimarite": "fusen"}
],
"wins": 2,
"losses": 2,
"absences": 1
}
],
"west": []
}`

transport := &mockTransport{
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(mockResp)),
},
}

client := sumoapi.New(sumoapi.WithHTTPClient(&http.Client{Transport: transport}))
resp, err := client.GetBanzuke(context.Background(), sumoapi.GetBanzukeRequest{
BashoID: sumoapi.BashoID{Year: 2025, Month: 11},
Division: "Makuuchi",
})

g.Expect(err).ToNot(HaveOccurred())
g.Expect(resp).ToNot(BeNil())
g.Expect(resp.East[0].Matches).To(HaveLen(5))
g.Expect(resp.East[0].Matches[0].Result).To(Equal("win"))
g.Expect(resp.East[0].Matches[1].Result).To(Equal("loss"))
g.Expect(resp.East[0].Matches[2].Result).To(Equal("absent"))
g.Expect(resp.East[0].Matches[3].Result).To(Equal("fusen win"))
g.Expect(resp.East[0].Matches[4].Result).To(Equal("fusen loss"))
})

t.Run("context is propagated", func(t *testing.T) {
g := NewWithT(t)

mockResp := `{
"bashoId": "202511",
"division": "Makuuchi",
"east": [],
"west": []
}`

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.GetBanzuke(ctx, sumoapi.GetBanzukeRequest{
BashoID: sumoapi.BashoID{Year: 2025, Month: 11},
Division: "Makuuchi",
})

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.GetBanzuke(context.Background(), sumoapi.GetBanzukeRequest{
BashoID: sumoapi.BashoID{Year: 2025, Month: 11},
Division: "Makuuchi",
})

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.GetBanzuke(context.Background(), sumoapi.GetBanzukeRequest{
BashoID: sumoapi.BashoID{Year: 2025, Month: 11},
Division: "Makuuchi",
})

g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("error unmarshaling response body"))
g.Expect(resp).To(BeNil())
})
}
4 changes: 2 additions & 2 deletions rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ 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)."`
HumanReadableName string `json:"rank,omitempty" jsonschema:"The human-readable name of the rank (e.g., Maegashira 1 East, Ozeki 2 West) of the rikishi (sumo wrestler) in the specific basho (sumo tournament)."`
NumericName int `json:"rankValue,omitempty" jsonschema:"The numeric name of the rank of the rikishi (sumo wrestler) in the specific basho (sumo tournament)."`
}
Loading