Skip to content

Commit

Permalink
add GetAchievementDistribution endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
joshraphael committed Nov 20, 2024
1 parent d88f2bf commit 0be495f
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ For convenience, the API docs and examples can be found in the tables below
|`GetGame()`|Get basic metadata about a game.|[docs](https://api-docs.retroachievements.org/v1/get-game.html) \| [example](examples/game/getgame/getgame.go)|
|`GetGameExtended()`|Get extended metadata about a game.|[docs](https://api-docs.retroachievements.org/v1/get-game-extended.html) \| [example](examples/game/getgameextended/getgameextended.go)|
|`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)|
|`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)|
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package getachievementdistribution provides an example for getting how many players have unlocked how many achievements for a game
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 getachievementdistribution.go`
*/
func main() {
secret := os.Getenv("RA_API_KEY")

client := retroachievements.NewClient(secret)

hardcore := true
unofficial := false
resp, err := client.GetAchievementDistribution(models.GetAchievementDistributionParameters{
GameID: 1,
Hardcore: &hardcore,
Unofficial: &unofficial,
})
if err != nil {
panic(err)
}

fmt.Printf("%+v\n", resp)
}
29 changes: 29 additions & 0 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,32 @@ func (c *Client) GetAchievementCount(params models.GetAchievementCountParameters
}
return resp, nil
}

// GetAchievementDistribution gets how many players have unlocked how many achievements for a game.
func (c *Client) GetAchievementDistribution(params models.GetAchievementDistributionParameters) (*models.GetAchievementDistribution, error) {
details := []raHttp.RequestDetail{
raHttp.Method(http.MethodGet),
raHttp.Path("/API/API_GetAchievementDistribution.php"),
raHttp.APIToken(c.Secret),
raHttp.IDs([]int{params.GameID}),
}
if params.Unofficial != nil {
if *params.Unofficial {
details = append(details, raHttp.From(int64(5)))
} else {
details = append(details, raHttp.From(int64(3)))
}
}
if params.Hardcore != nil {
details = append(details, raHttp.Hardcore(*params.Hardcore))
}
r, err := c.do(details...)
if err != nil {
return nil, fmt.Errorf("calling endpoint: %w", err)
}
resp, err := raHttp.ResponseObject[models.GetAchievementDistribution](r)
if err != nil {
return nil, fmt.Errorf("parsing response object: %w", err)
}
return resp, nil
}
171 changes: 171 additions & 0 deletions game_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,174 @@ func TestGetAchievementCount(tt *testing.T) {
})
}
}

func TestGetAchievementDistribution(tt *testing.T) {
hardcore := true
unofficial := true
official := false
tests := []struct {
name string
params models.GetAchievementDistributionParameters
modifyURL func(url string) string
responseCode int
responseMessage models.GetAchievementDistribution
responseError models.ErrorResponse
response func(messageBytes []byte, errorBytes []byte) []byte
assert func(t *testing.T, game *models.GetAchievementDistribution, err error)
}{
{
name: "fail to call endpoint",
params: models.GetAchievementDistributionParameters{
GameID: 14402,
Hardcore: &hardcore,
Unofficial: &unofficial,
},
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.GetAchievementDistribution, err error) {
require.Nil(t, resp)
require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetAchievementDistribution.php?f=5&h=1&i=14402&y=some_secret\": unsupported protocol scheme \"\"")
},
},
{
name: "error response",
params: models.GetAchievementDistributionParameters{
GameID: 14402,
Hardcore: &hardcore,
Unofficial: &unofficial,
},
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.GetAchievementDistribution, err error) {
require.Nil(t, resp)
require.EqualError(t, err, "parsing response object: error responses: [401] Not Authorized")
},
},
{
name: "success",
params: models.GetAchievementDistributionParameters{
GameID: 14402,
Hardcore: &hardcore,
Unofficial: &official,
},
modifyURL: func(url string) string {
return url
},
responseCode: http.StatusOK,
responseMessage: models.GetAchievementDistribution{
"1": 105,
"2": 28,
"3": 33,
"4": 30,
"5": 20,
"6": 15,
"7": 4,
"8": 29,
"9": 8,
"10": 4,
"11": 1,
"12": 0,
"13": 0,
"14": 0,
"15": 3,
},
response: func(messageBytes []byte, errorBytes []byte) []byte {
return messageBytes
},
assert: func(t *testing.T, resp *models.GetAchievementDistribution, err error) {
require.NotNil(t, resp)
r := *resp
val, ok := r["1"]
require.True(t, ok)
require.Equal(t, 105, val)
val, ok = r["2"]
require.True(t, ok)
require.Equal(t, 28, val)
val, ok = r["3"]
require.True(t, ok)
require.Equal(t, 33, val)
val, ok = r["4"]
require.True(t, ok)
require.Equal(t, 30, val)
val, ok = r["5"]
require.True(t, ok)
require.Equal(t, 20, val)
val, ok = r["6"]
require.True(t, ok)
require.Equal(t, 15, val)
val, ok = r["7"]
require.True(t, ok)
require.Equal(t, 4, val)
val, ok = r["8"]
require.True(t, ok)
require.Equal(t, 29, val)
val, ok = r["9"]
require.True(t, ok)
require.Equal(t, 8, val)
val, ok = r["10"]
require.True(t, ok)
require.Equal(t, 4, val)
val, ok = r["11"]
require.True(t, ok)
require.Equal(t, 1, val)
val, ok = r["12"]
require.True(t, ok)
require.Equal(t, 0, val)
val, ok = r["13"]
require.True(t, ok)
require.Equal(t, 0, val)
val, ok = r["14"]
require.True(t, ok)
require.Equal(t, 0, val)
val, ok = r["15"]
require.True(t, ok)
require.Equal(t, 3, val)
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_GetAchievementDistribution.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.GetAchievementDistribution(test.params)
test.assert(t, resp, err)
})
}
}
11 changes: 11 additions & 0 deletions http/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ func Achievement(achievement int) RequestDetail {
})
}

// Hardcore adds a Hardcore boolean query parameter
func Hardcore(hardcore bool) RequestDetail {
numHardcore := 0
if hardcore {
numHardcore = 1
}
return requestDetailFn(func(r *Request) {
r.Params["h"] = strconv.Itoa(numHardcore)
})
}

// Path adds a URL path to the host
func Path(path string) RequestDetail {
return requestDetailFn(func(r *Request) {
Expand Down
2 changes: 2 additions & 0 deletions http/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestNewRequest(t *testing.T) {
raHttp.Achievement(1),
raHttp.Count(20),
raHttp.Offset(34),
raHttp.Hardcore(true),
)

expected := &raHttp.Request{
Expand All @@ -50,6 +51,7 @@ func TestNewRequest(t *testing.T) {
"a": "1",
"c": "20",
"o": "34",
"h": "1",
},
}

Expand Down
13 changes: 13 additions & 0 deletions models/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,16 @@ type GetAchievementCount struct {
GameID int `json:"GameID"`
AchievementIDs []int `json:"AchievementIDs"`
}

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

// [Optional] Only query hardcore unlocks (default: false)
Hardcore *bool

// [Optional] Get unofficial achievements (default: false)
Unofficial *bool
}

type GetAchievementDistribution map[string]int

0 comments on commit 0be495f

Please sign in to comment.