diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1f665e1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: 'v1.55.2' \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4e142e8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,116 @@ +linters: + disable-all: true + enable: + - asciicheck + - bidichk +# - bodyclose -> streams? + - containedctx + - contextcheck + - cyclop + - decorder +# - depguard -> reports testify/require and go-querystring/query +# - dogsled -> why not? + - dupl + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint +# - execinquery -> no sql +# - exhaustive -> why? +# - exhaustruct -> why? + - exportloopref + - forbidigo + - forcetypeassert + - funlen + - gci +# - ginkgolinter -> not used + - gocheckcompilerdirectives + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox +# - goerr113 -> annoying, revisit in the future + - gofmt + - gofumpt + - goheader + - goimports + - gomnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - gosmopolitan + - govet + - grouper + - importas + - ineffassign + - interfacebloat +# - ireturn -> why not? sometimes (generic) + - lll + - loggercheck + - maintidx + - makezero + - mirror + - misspell + - musttag + - nakedret + - nestif + - nilerr + - nilnil +# - nlreturn -> annoying + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + - paralleltest + - prealloc + - predeclared +# - promlinter -> no prom +# - protogetter -> no protos + - reassign + - revive +# - rowserrcheck -> no sql/rows + - sloglint +# - sqlclosecheck -> no sql + - staticcheck +# - stylecheck -> annoying (var-naming) + - tagalign + - tagliatelle + - tenv + - testableexamples + - testifylint + - testpackage + - thelper + - tparallel + - unconvert + - unparam + - unused + - usestdlibvars +# - varnamelen -> annoying + - wastedassign + - whitespace +# - wrapcheck -> why? always? +# - wsl -> annoying +# - zerologlint -> no zerolog + +linters-settings: + revive: + rules: + - name: var-naming + disabled: true + + gci: + sections: + - standard + - default + - prefix(github.com/joanlopez/go-lichess) + + # Make the section order the same as the order of `sections`. + custom-order: true diff --git a/lichess/games_export.go b/lichess/games_export.go index 7d29e86..3bf4b45 100644 --- a/lichess/games_export.go +++ b/lichess/games_export.go @@ -63,7 +63,11 @@ func (s *GamesService) ExportById(ctx context.Context, id string, opts *ExportOp // ExportCurrent exports the current [Game] being played by the given username. // Find more details at https://lichess.org/api#tag/Games/operation/apiUserCurrentGame. -func (s *GamesService) ExportCurrent(ctx context.Context, username string, opts *ExportOptions) (*Game, *Response, error) { +func (s *GamesService) ExportCurrent( + ctx context.Context, + username string, + opts *ExportOptions, +) (*Game, *Response, error) { u := fmt.Sprintf("api/user/%v/current-game", username) u, err := addOptions(u, opts) if err != nil { @@ -86,7 +90,11 @@ func (s *GamesService) ExportCurrent(ctx context.Context, username string, opts // ExportByUsername exports a list of [Game] played by the given username. // Find more details at https://lichess.org/api#tag/Games/operation/apiGamesUser. -func (s *GamesService) ExportByUsername(ctx context.Context, username string, opts *ExportByUsernameOptions) ([]*Game, *Response, error) { +func (s *GamesService) ExportByUsername( + ctx context.Context, + username string, + opts *ExportByUsernameOptions, +) ([]*Game, *Response, error) { u := fmt.Sprintf("api/games/user/%v", username) u, err := addOptions(u, opts) if err != nil { diff --git a/lichess/games_stream.go b/lichess/games_stream.go index e0b3099..a716824 100644 --- a/lichess/games_stream.go +++ b/lichess/games_stream.go @@ -93,41 +93,41 @@ func (s *GamesService) StreamGameMoves(ctx context.Context, id string) (chan Gam ch := make(chan GameStreamEvent) - finalize := func() { + go s.streamGameMoves(ctx, ch, resp) + + return ch, resp, nil +} + +func (s *GamesService) streamGameMoves(ctx context.Context, ch chan GameStreamEvent, resp *Response) { + defer func() { // Explicit ignore error. // We might want to revisit this later. _ = resp.Body.Close() close(ch) - } + }() - go func() { - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - select { - case <-ctx.Done(): - finalize() - return - default: - } - - select { - case <-ctx.Done(): - finalize() - return - case ch <- s.parseGameStreamEvent(scanner.Text()): - } + scanner := bufio.NewScanner(resp.Body) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: } - if scanner.Err() != nil { - select { - case <-ctx.Done(): - case ch <- GameStreamEventError{error: scanner.Err()}: - } + select { + case <-ctx.Done(): + return + case ch <- s.parseGameStreamEvent(scanner.Text()): } - finalize() - }() + } - return ch, resp, nil + if scanner.Err() != nil { + select { + case <-ctx.Done(): + case ch <- GameStreamEventError{error: scanner.Err()}: + } + } } func (s *GamesService) parseGameStreamEvent(event string) GameStreamEvent { diff --git a/lichess/lichess.go b/lichess/lichess.go index 10240f2..c9357ee 100644 --- a/lichess/lichess.go +++ b/lichess/lichess.go @@ -151,9 +151,9 @@ func (c *Client) BareDo(req *http.Request) (*Response, error) { // If rate limit is exceeded and reset time is in the future, Do returns // *RateLimitError immediately without making a network API call. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { - resp, err := c.BareDo(req) + res, err := c.BareDo(req) if err != nil { - return resp, err + return res, err } // We only close the response body, when the @@ -164,43 +164,56 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { if v != nil { // Explicit ignore error. // We might want to revisit this later. - _ = resp.Body.Close() + _ = res.Body.Close() } }() + err = c.decodeResponse(req, res, v) + + return res, err +} + +func (c *Client) decodeResponse(req *http.Request, res *Response, v interface{}) error { + var err error + switch v := v.(type) { case nil: case io.Writer: - _, err = io.Copy(v, resp.Body) + _, err = io.Copy(v, res.Body) default: switch typeOfResponse(req.Method, req.URL.Path) { case jsonResponseType: - decErr := json.NewDecoder(resp.Body).Decode(v) - if decErr == io.EOF { - decErr = nil // ignore EOF errors caused by empty response body - } - if decErr != nil { + decErr := json.NewDecoder(res.Body).Decode(v) + // Ignore EOF errors caused by empty response body + if decErr != nil && !errors.Is(decErr, io.EOF) { err = decErr } case ndJsonResponseType: - if reflect.ValueOf(v).Elem().Kind() != reflect.Slice { - err = errors.New("v is not a pointer to a slice") - } + err = c.decodeNdJson(res, v) + } + } - itemType := reflect.ValueOf(v).Elem().Type().Elem() + return err +} - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - item := reflect.New(itemType).Interface() - if err = json.Unmarshal(scanner.Bytes(), item); err != nil { - break - } +func (c *Client) decodeNdJson(res *Response, v interface{}) error { + if reflect.ValueOf(v).Elem().Kind() != reflect.Slice { + return errors.New("v is not a pointer to a slice") + } - reflect.ValueOf(v).Elem().Set(reflect.Append(reflect.ValueOf(v).Elem(), reflect.ValueOf(item).Elem())) - } + itemType := reflect.ValueOf(v).Elem().Type().Elem() + + scanner := bufio.NewScanner(res.Body) + for scanner.Scan() { + item := reflect.New(itemType).Interface() + if err := json.Unmarshal(scanner.Bytes(), item); err != nil { + return err } + + reflect.ValueOf(v).Elem().Set(reflect.Append(reflect.ValueOf(v).Elem(), reflect.ValueOf(item).Elem())) } - return resp, err + + return nil } type service struct { @@ -252,7 +265,7 @@ const ( ) // typeOfResponse returns the response type of the endpoint, determined by HTTP method and Request.URL.Path. -func typeOfResponse(method, path string) responseType { +func typeOfResponse(_, path string) responseType { switch { default: return jsonResponseType @@ -262,7 +275,7 @@ func typeOfResponse(method, path string) responseType { } } -// roundTripperFunc creates a RoundTripper (transport) +// roundTripperFunc creates a RoundTripper (transport). type roundTripperFunc func(*http.Request) (*http.Response, error) func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { diff --git a/lichess/puzzles_activity.go b/lichess/puzzles_activity.go index ba3e4c0..849ef86 100644 --- a/lichess/puzzles_activity.go +++ b/lichess/puzzles_activity.go @@ -16,7 +16,10 @@ type GetPuzzleActivityOptions struct { Before *int `url:"before,omitempty"` // >= 1356998400070 } -func (s *PuzzlesService) GetPuzzleActivity(ctx context.Context, opts *GetPuzzleActivityOptions) ([]*PuzzleRound, *Response, error) { +func (s *PuzzlesService) GetPuzzleActivity( + ctx context.Context, + opts *GetPuzzleActivityOptions, +) ([]*PuzzleRound, *Response, error) { u := "api/puzzle/activity" u, err := addOptions(u, opts) if err != nil {