diff --git a/README.md b/README.md index 55345cc..36bd9e1 100644 --- a/README.md +++ b/README.md @@ -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)| \ No newline at end of file +|`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)| \ No newline at end of file diff --git a/examples/game/getachievementdistribution/getachievementdistribution.go b/examples/game/getachievementdistribution/getachievementdistribution.go new file mode 100644 index 0000000..7d89f31 --- /dev/null +++ b/examples/game/getachievementdistribution/getachievementdistribution.go @@ -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) +} diff --git a/game.go b/game.go index 03f026e..ced0571 100644 --- a/game.go +++ b/game.go @@ -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 +} diff --git a/game_test.go b/game_test.go index 9d3332c..2369ab0 100644 --- a/game_test.go +++ b/game_test.go @@ -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) + }) + } +} diff --git a/http/request.go b/http/request.go index 38c2ede..cf6140b 100644 --- a/http/request.go +++ b/http/request.go @@ -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) { diff --git a/http/request_test.go b/http/request_test.go index 350ca2a..0a55a06 100644 --- a/http/request_test.go +++ b/http/request_test.go @@ -29,6 +29,7 @@ func TestNewRequest(t *testing.T) { raHttp.Achievement(1), raHttp.Count(20), raHttp.Offset(34), + raHttp.Hardcore(true), ) expected := &raHttp.Request{ @@ -50,6 +51,7 @@ func TestNewRequest(t *testing.T) { "a": "1", "c": "20", "o": "34", + "h": "1", }, } diff --git a/models/game.go b/models/game.go index b7c68bf..ff1304f 100644 --- a/models/game.go +++ b/models/game.go @@ -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