Skip to content

Commit

Permalink
add GetGameLeaderboards endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
joshraphael committed Nov 21, 2024
1 parent 70e2bf4 commit dda99b7
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 4 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,10 @@ For convenience, the API docs and examples can be found in the tables below
|`GetGameHashes()`|Get the hashes linked to a game.|[docs](https://api-docs.retroachievements.org/v1/get-game-hashes.html) \| [example](examples/game/getgamehashes/getgamehashes.go)|
|`GetAchievementCount()`|Get the list of achievement IDs for a game.|[docs](https://api-docs.retroachievements.org/v1/get-achievement-count.html) \| [example](examples/game/getachievementcount/getachievementcount.go)|
|`GetAchievementDistribution()`|Gets how many players have unlocked how many achievements for a game.|[docs](https://api-docs.retroachievements.org/v1/get-achievement-distribution.html) \| [example](examples/game/getachievementdistribution/getachievementdistribution.go)|
|`GetGameRankAndScore()`|Gets metadata about either the latest masters for a game, or the highest points earners for a game.|[docs](https://api-docs.retroachievements.org/v1/get-game-rank-and-score.html) \| [example](examples/game/getgamerankandscore/getgamerankandscore.go)|
|`GetGameRankAndScore()`|Gets metadata about either the latest masters for a game, or the highest points earners for a game.|[docs](https://api-docs.retroachievements.org/v1/get-game-rank-and-score.html) \| [example](examples/game/getgamerankandscore/getgamerankandscore.go)|

<h3>Leaderboards</h3>

|Function|Description|Links|
|-|-|-|
|`GetGameLeaderboards()`|Gets a given games's list of leaderboards.|[docs](https://api-docs.retroachievements.org/v1/get-game-leaderboards.html) \| [example](examples/leaderboards/getgameleaderboards/getgameleaderboards.go)|
28 changes: 28 additions & 0 deletions examples/leaderboards/getgameleaderboards/getgameleaderboards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Package getgameleaderboards provides an example for getting a given games's list of leaderboards.
package main

import (
"fmt"
"os"

"github.com/joshraphael/go-retroachievements"
"github.com/joshraphael/go-retroachievements/models"
)

/*
Test script, add RA_API_KEY to your env and use `go run getgameleaderboards.go`
*/
func main() {
secret := os.Getenv("RA_API_KEY")

client := retroachievements.NewClient(secret)

resp, err := client.GetGameLeaderboards(models.GetGameLeaderboardsParameters{
GameID: 1,
})
if err != nil {
panic(err)
}

fmt.Printf("%+v\n", resp)
}
6 changes: 3 additions & 3 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ func (c *Client) GetGameExtended(params models.GetGameExtentedParameters) (*mode
if params.Unofficial {
details = append(details, raHttp.From(int64(5)))
}
resp, err := c.do(details...)
r, err := c.do(details...)
if err != nil {
return nil, fmt.Errorf("calling endpoint: %w", err)
}
game, err := raHttp.ResponseObject[models.GetGameExtented](resp)
resp, err := raHttp.ResponseObject[models.GetGameExtented](r)
if err != nil {
return nil, fmt.Errorf("parsing response object: %w", err)
}
return game, nil
return resp, nil
}

// GetGameHashes get the hashes linked to a game.
Expand Down
34 changes: 34 additions & 0 deletions leaderboards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package retroachievements

import (
"fmt"
"net/http"

raHttp "github.com/joshraphael/go-retroachievements/http"
"github.com/joshraphael/go-retroachievements/models"
)

// GetGameLeaderboards gets a given games's list of leaderboards.
func (c *Client) GetGameLeaderboards(params models.GetGameLeaderboardsParameters) (*models.GetGameLeaderboards, error) {
details := []raHttp.RequestDetail{
raHttp.Method(http.MethodGet),
raHttp.Path("/API/API_GetGameLeaderboards.php"),
raHttp.APIToken(c.Secret),
raHttp.IDs([]int{params.GameID}),
}
if params.Count != nil {
details = append(details, raHttp.Count(*params.Count))
}
if params.Offset != nil {
details = append(details, raHttp.Offset(*params.Offset))
}
r, err := c.do(details...)
if err != nil {
return nil, fmt.Errorf("calling endpoint: %w", err)
}
resp, err := raHttp.ResponseObject[models.GetGameLeaderboards](r)
if err != nil {
return nil, fmt.Errorf("parsing response object: %w", err)
}
return resp, nil
}
149 changes: 149 additions & 0 deletions leaderboards_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package retroachievements_test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/joshraphael/go-retroachievements"
"github.com/joshraphael/go-retroachievements/models"
"github.com/stretchr/testify/require"
)

func TestGetGameLeaderboards(tt *testing.T) {
count := 10
offset := 10
tests := []struct {
name string
params models.GetGameLeaderboardsParameters
modifyURL func(url string) string
responseCode int
responseMessage models.GetGameLeaderboards
responseError models.ErrorResponse
response func(messageBytes []byte, errorBytes []byte) []byte
assert func(t *testing.T, resp *models.GetGameLeaderboards, err error)
}{
{
name: "fail to call endpoint",
params: models.GetGameLeaderboardsParameters{
GameID: 14402,
Count: &count,
Offset: &offset,
},
modifyURL: func(url string) string {
return ""
},
responseCode: http.StatusOK,
response: func(messageBytes []byte, errorBytes []byte) []byte {
return messageBytes
},
assert: func(t *testing.T, resp *models.GetGameLeaderboards, err error) {
require.Nil(t, resp)
require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetGameLeaderboards.php?c=10&i=14402&o=10&y=some_secret\": unsupported protocol scheme \"\"")
},
},
{
name: "error response",
params: models.GetGameLeaderboardsParameters{
GameID: 14402,
Count: &count,
Offset: &offset,
},
modifyURL: func(url string) string {
return url
},
responseCode: http.StatusUnauthorized,
responseError: models.ErrorResponse{
Message: "test",
Errors: []models.ErrorDetail{
{
Status: http.StatusUnauthorized,
Code: "unauthorized",
Title: "Not Authorized",
},
},
},
response: func(messageBytes []byte, errorBytes []byte) []byte {
return errorBytes
},
assert: func(t *testing.T, resp *models.GetGameLeaderboards, err error) {
require.Nil(t, resp)
require.EqualError(t, err, "parsing response object: error responses: [401] Not Authorized")
},
},
{
name: "success",
params: models.GetGameLeaderboardsParameters{
GameID: 14402,
Count: &count,
Offset: &offset,
},
modifyURL: func(url string) string {
return url
},
responseCode: http.StatusOK,
responseMessage: models.GetGameLeaderboards{
Count: 1,
Total: 2,
Results: []models.GetGameLeaderboardsResult{
{
ID: 114798,
RankAsc: true,
Title: "Speedrun Monster Max",
Description: "Complete the game from start to finish as fast as possible without using passwords",
Format: "TIME",
TopEntry: &models.GetGameLeaderboardsTopEntry{
User: "joshraphael",
Score: 2267,
FormattedScore: "0:37.78",
},
},
},
},
response: func(messageBytes []byte, errorBytes []byte) []byte {
return messageBytes
},
assert: func(t *testing.T, resp *models.GetGameLeaderboards, err error) {
require.NotNil(t, resp)
require.Equal(t, 1, resp.Count)
require.Equal(t, 2, resp.Total)
require.Len(t, resp.Results, 1)
require.Equal(t, 114798, resp.Results[0].ID)
require.True(t, resp.Results[0].RankAsc)
require.Equal(t, "Speedrun Monster Max", resp.Results[0].Title)
require.Equal(t, "Complete the game from start to finish as fast as possible without using passwords", resp.Results[0].Description)
require.Equal(t, "TIME", resp.Results[0].Format)
require.NotNil(t, resp.Results[0].TopEntry)
require.Equal(t, "joshraphael", resp.Results[0].TopEntry.User)
require.Equal(t, 2267, resp.Results[0].TopEntry.Score)
require.Equal(t, "0:37.78", resp.Results[0].TopEntry.FormattedScore)
require.NoError(t, err)
},
},
}
for _, test := range tests {
tt.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expectedPath := "/API/API_GetGameLeaderboards.php"
if r.URL.Path != expectedPath {
t.Errorf("Expected to request '%s', got: %s", expectedPath, r.URL.Path)
}
w.WriteHeader(test.responseCode)
messageBytes, err := json.Marshal(test.responseMessage)
require.NoError(t, err)
errBytes, err := json.Marshal(test.responseError)
require.NoError(t, err)
resp := test.response(messageBytes, errBytes)
num, err := w.Write(resp)
require.NoError(t, err)
require.Equal(t, num, len(resp))
}))
defer server.Close()

client := retroachievements.New(test.modifyURL(server.URL), "some_secret")
resp, err := client.GetGameLeaderboards(test.params)
test.assert(t, resp, err)
})
}
}
33 changes: 33 additions & 0 deletions models/leaderboards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package models

type GetGameLeaderboardsParameters struct {
// The target game ID
GameID int

// [Optional] The number of records to return (default: 100, max: 500).
Count *int

// [Optional] The number of entries to skip (default: 0).
Offset *int
}

type GetGameLeaderboards struct {
Count int `json:"Count"`
Total int `json:"Total"`
Results []GetGameLeaderboardsResult `json:"Results"`
}

type GetGameLeaderboardsResult struct {
ID int `json:"ID"`
RankAsc bool `json:"RankAsc"`
Title string `json:"Title"`
Description string `json:"Description"`
Format string `json:"Format"`
TopEntry *GetGameLeaderboardsTopEntry `json:"TopEntry"`
}

type GetGameLeaderboardsTopEntry struct {
User string `json:"User"`
Score int `json:"Score"`
FormattedScore string `json:"FormattedScore"`
}

0 comments on commit dda99b7

Please sign in to comment.