Skip to content

Commit 70bc20d

Browse files
authored
Merge pull request #5 from joshraphael/issue/4
issue-4: Add get user game leaderboards endpoint
2 parents 8a17bf4 + c6a61ff commit 70bc20d

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ For convenience, the API docs and examples can be found in the tables below
8888
|-|-|-|
8989
|`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)|
9090
|`GetLeaderboardEntries()`|Gets a given leadboard's entries.|[docs](https://api-docs.retroachievements.org/v1/get-leaderboard-entries.html) \| [example](examples/leaderboards/getleaderboardentries/getleaderboardentries.go)|
91+
|`GetUserGameLeaderboards()`|Gets a user's list of leaderboards for a given game.|[docs](https://api-docs.retroachievements.org/v1/get-user-game-leaderboards.html) \| [example](examples/leaderboards/getusergameleaderboards/getusergameleaderboards.go)|
9192

9293
<h3>System</h3>
9394

assets/ra_gopher_big_banner.png

6.17 KB
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Package getusergameleaderboards provides an example for getting a user's list of leaderboards for a given game.
2+
package main
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/joshraphael/go-retroachievements"
9+
"github.com/joshraphael/go-retroachievements/models"
10+
)
11+
12+
/*
13+
Test script, add RA_API_KEY to your env and use `go run getusergameleaderboards.go`
14+
*/
15+
func main() {
16+
secret := os.Getenv("RA_API_KEY")
17+
18+
client := retroachievements.NewClient(secret)
19+
20+
resp, err := client.GetUserGameLeaderboards(models.GetUserGameLeaderboardsParameters{
21+
GameID: 583,
22+
Username: "joshraphael",
23+
})
24+
if err != nil {
25+
panic(err)
26+
}
27+
28+
fmt.Printf("%+v\n", resp)
29+
}

leaderboards.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,30 @@ func (c *Client) GetLeaderboardEntries(params models.GetLeaderboardEntriesParame
6060
}
6161
return resp, nil
6262
}
63+
64+
// GetUserGameLeaderboards gets a user's list of leaderboards for a given game.
65+
func (c *Client) GetUserGameLeaderboards(params models.GetUserGameLeaderboardsParameters) (*models.GetUserGameLeaderboards, error) {
66+
details := []raHttp.RequestDetail{
67+
raHttp.Method(http.MethodGet),
68+
raHttp.UserAgent(c.UserAgent),
69+
raHttp.Path("/API/API_GetUserGameLeaderboards.php"),
70+
raHttp.APIToken(c.Secret),
71+
raHttp.U(params.Username),
72+
raHttp.I([]string{strconv.Itoa(params.GameID)}),
73+
}
74+
if params.Count != nil {
75+
details = append(details, raHttp.C(*params.Count))
76+
}
77+
if params.Offset != nil {
78+
details = append(details, raHttp.O(*params.Offset))
79+
}
80+
r, err := c.do(details...)
81+
if err != nil {
82+
return nil, fmt.Errorf("calling endpoint: %w", err)
83+
}
84+
resp, err := raHttp.ResponseObject[models.GetUserGameLeaderboards](r)
85+
if err != nil {
86+
return nil, fmt.Errorf("parsing response object: %w", err)
87+
}
88+
return resp, nil
89+
}

leaderboards_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ func TestGetLeaderboardEntries(tt *testing.T) {
242242
},
243243
assert: func(t *testing.T, resp *models.GetLeaderboardEntries, err error) {
244244
require.NotNil(t, resp)
245+
require.Equal(t, 1, resp.Count)
246+
require.Equal(t, 2, resp.Total)
247+
require.Len(t, resp.Results, 1)
248+
require.Equal(t, "ramenoid", resp.Results[0].User)
249+
require.Equal(t, dateSubmitted, resp.Results[0].DateSubmitted.Time)
250+
require.Equal(t, 1908730, resp.Results[0].Score)
251+
require.Equal(t, "1,908,730", resp.Results[0].FormattedScore)
252+
require.Equal(t, 1, resp.Results[0].Rank)
245253
require.NoError(t, err)
246254
},
247255
},
@@ -270,3 +278,149 @@ func TestGetLeaderboardEntries(tt *testing.T) {
270278
})
271279
}
272280
}
281+
282+
func TestGetUserGameLeaderboards(tt *testing.T) {
283+
count := 10
284+
offset := 10
285+
dateUpdated, err := time.Parse(models.RFC3339NumColonTZFormat, "2024-10-05T18:30:59+00:00")
286+
require.NoError(tt, err)
287+
tests := []struct {
288+
name string
289+
params models.GetUserGameLeaderboardsParameters
290+
modifyURL func(url string) string
291+
responseCode int
292+
responseMessage models.GetUserGameLeaderboards
293+
responseError models.ErrorResponse
294+
response func(messageBytes []byte, errorBytes []byte) []byte
295+
assert func(t *testing.T, resp *models.GetUserGameLeaderboards, err error)
296+
}{
297+
{
298+
name: "fail to call endpoint",
299+
params: models.GetUserGameLeaderboardsParameters{
300+
GameID: 515,
301+
Username: "test",
302+
Count: &count,
303+
Offset: &offset,
304+
},
305+
modifyURL: func(url string) string {
306+
return ""
307+
},
308+
responseCode: http.StatusOK,
309+
response: func(messageBytes []byte, errorBytes []byte) []byte {
310+
return messageBytes
311+
},
312+
assert: func(t *testing.T, resp *models.GetUserGameLeaderboards, err error) {
313+
require.Nil(t, resp)
314+
require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetUserGameLeaderboards.php?c=10&i=515&o=10&u=test&y=some_secret\": unsupported protocol scheme \"\"")
315+
},
316+
},
317+
{
318+
name: "error response",
319+
params: models.GetUserGameLeaderboardsParameters{
320+
GameID: 515,
321+
Username: "test",
322+
Count: &count,
323+
Offset: &offset,
324+
},
325+
modifyURL: func(url string) string {
326+
return url
327+
},
328+
responseCode: http.StatusUnauthorized,
329+
responseError: models.ErrorResponse{
330+
Message: "test",
331+
Errors: []models.ErrorDetail{
332+
{
333+
Status: http.StatusUnauthorized,
334+
Code: "unauthorized",
335+
Title: "Not Authorized",
336+
},
337+
},
338+
},
339+
response: func(messageBytes []byte, errorBytes []byte) []byte {
340+
return errorBytes
341+
},
342+
assert: func(t *testing.T, resp *models.GetUserGameLeaderboards, err error) {
343+
require.Nil(t, resp)
344+
require.EqualError(t, err, "parsing response object: error code 401 returned: {\"message\":\"test\",\"errors\":[{\"status\":401,\"code\":\"unauthorized\",\"title\":\"Not Authorized\"}]}")
345+
},
346+
},
347+
{
348+
name: "success",
349+
params: models.GetUserGameLeaderboardsParameters{
350+
GameID: 515,
351+
Username: "test",
352+
Count: &count,
353+
Offset: &offset,
354+
},
355+
modifyURL: func(url string) string {
356+
return url
357+
},
358+
responseCode: http.StatusOK,
359+
responseMessage: models.GetUserGameLeaderboards{
360+
Count: 1,
361+
Total: 2,
362+
Results: []models.GetUserGameLeaderboardsResult{
363+
{
364+
ID: 114798,
365+
RankAsc: true,
366+
Title: "Speedrun Monster Max",
367+
Description: "Complete the game from start to finish as fast as possible without using passwords",
368+
Format: "TIME",
369+
UserEntry: models.GetUserGameLeaderboardsUserEntry{
370+
User: "ramenoid",
371+
DateUpdated: models.RFC3339NumColonTZ{
372+
Time: dateUpdated,
373+
},
374+
Score: 1908730,
375+
FormattedScore: "1,908,730",
376+
Rank: 1,
377+
},
378+
},
379+
},
380+
},
381+
response: func(messageBytes []byte, errorBytes []byte) []byte {
382+
return messageBytes
383+
},
384+
assert: func(t *testing.T, resp *models.GetUserGameLeaderboards, err error) {
385+
require.NotNil(t, resp)
386+
require.Equal(t, 1, resp.Count)
387+
require.Equal(t, 2, resp.Total)
388+
require.Len(t, resp.Results, 1)
389+
require.Equal(t, 114798, resp.Results[0].ID)
390+
require.True(t, resp.Results[0].RankAsc)
391+
require.Equal(t, "Speedrun Monster Max", resp.Results[0].Title)
392+
require.Equal(t, "Complete the game from start to finish as fast as possible without using passwords", resp.Results[0].Description)
393+
require.Equal(t, "TIME", resp.Results[0].Format)
394+
require.Equal(t, "ramenoid", resp.Results[0].UserEntry.User)
395+
require.Equal(t, 1908730, resp.Results[0].UserEntry.Score)
396+
require.Equal(t, "1,908,730", resp.Results[0].UserEntry.FormattedScore)
397+
require.Equal(t, 1, resp.Results[0].UserEntry.Rank)
398+
require.Equal(t, dateUpdated, resp.Results[0].UserEntry.DateUpdated.Time)
399+
require.NoError(t, err)
400+
},
401+
},
402+
}
403+
for _, test := range tests {
404+
tt.Run(test.name, func(t *testing.T) {
405+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
406+
expectedPath := "/API/API_GetUserGameLeaderboards.php"
407+
if r.URL.Path != expectedPath {
408+
t.Errorf("Expected to request '%s', got: %s", expectedPath, r.URL.Path)
409+
}
410+
w.WriteHeader(test.responseCode)
411+
messageBytes, err := json.Marshal(test.responseMessage)
412+
require.NoError(t, err)
413+
errBytes, err := json.Marshal(test.responseError)
414+
require.NoError(t, err)
415+
resp := test.response(messageBytes, errBytes)
416+
num, err := w.Write(resp)
417+
require.NoError(t, err)
418+
require.Equal(t, num, len(resp))
419+
}))
420+
defer server.Close()
421+
client := retroachievements.New(test.modifyURL(server.URL), "go-retroachievements/v0.0.0", "some_secret")
422+
resp, err := client.GetUserGameLeaderboards(test.params)
423+
test.assert(t, resp, err)
424+
})
425+
}
426+
}

models/leaderboards.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,40 @@ type GetLeaderboardEntriesResult struct {
5656
FormattedScore string `json:"FormattedScore"`
5757
DateSubmitted RFC3339NumColonTZ `json:"DateSubmitted"`
5858
}
59+
60+
type GetUserGameLeaderboardsParameters struct {
61+
// The target username
62+
Username string
63+
64+
// The target game ID
65+
GameID int
66+
67+
// [Optional] The number of records to return (default: 100, max: 500).
68+
Count *int
69+
70+
// [Optional] The number of entries to skip (default: 0).
71+
Offset *int
72+
}
73+
74+
type GetUserGameLeaderboards struct {
75+
Count int `json:"Count"`
76+
Total int `json:"Total"`
77+
Results []GetUserGameLeaderboardsResult `json:"Results"`
78+
}
79+
80+
type GetUserGameLeaderboardsResult struct {
81+
ID int `json:"ID"`
82+
RankAsc bool `json:"RankAsc"`
83+
Title string `json:"Title"`
84+
Description string `json:"Description"`
85+
Format string `json:"Format"`
86+
UserEntry GetUserGameLeaderboardsUserEntry `json:"UserEntry"`
87+
}
88+
89+
type GetUserGameLeaderboardsUserEntry struct {
90+
Rank int `json:"Rank"`
91+
User string `json:"User"`
92+
Score int `json:"Score"`
93+
FormattedScore string `json:"FormattedScore"`
94+
DateUpdated RFC3339NumColonTZ `json:"DateUpdated"`
95+
}

0 commit comments

Comments
 (0)