From 2d1b4f1da3e028258647ea868a29e93a4762e1d1 Mon Sep 17 00:00:00 2001 From: Gabriel Chaves Date: Tue, 17 Aug 2021 18:13:51 -0300 Subject: [PATCH] version v0.1.0 implemented --- .editorconfig | 9 +++ .github/workflows/release.yml | 34 +++++++++ .gitignore | 2 + .goreleaser.yml | 17 +++++ LICENSE | 21 +++++ cmd/lab01s01/main.go | 44 +++++++++++ cmd/lab01s01/structures.go | 13 ++++ go.mod | 3 + internal/pkg/configuration/configuration.go | 19 +++++ pkg/graphql/client.go | 85 +++++++++++++++++++++ pkg/graphql/errors.go | 26 +++++++ pkg/graphql/providers/github/client.go | 49 ++++++++++++ pkg/graphql/providers/github/errors.go | 25 ++++++ pkg/graphql/request.go | 64 ++++++++++++++++ pkg/graphql/response.go | 29 +++++++ pkg/logs/logs.go | 10 +++ 16 files changed, 450 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 LICENSE create mode 100644 cmd/lab01s01/main.go create mode 100644 cmd/lab01s01/structures.go create mode 100644 go.mod create mode 100644 internal/pkg/configuration/configuration.go create mode 100644 pkg/graphql/client.go create mode 100644 pkg/graphql/errors.go create mode 100644 pkg/graphql/providers/github/client.go create mode 100644 pkg/graphql/providers/github/errors.go create mode 100644 pkg/graphql/request.go create mode 100644 pkg/graphql/response.go create mode 100644 pkg/logs/logs.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..181aeeb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..12b8590 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release Project + +env: + GO_VERSION: 1.16 + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + build: + name: GoReleaser build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66f8fb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.vscode/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..64bc3a4 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,17 @@ +project_name: popular_repos + +archives: + - format_overrides: + - goos: windows + format: zip + +builds: + - id: lab01s01 + main: ./cmd/lab01s01 + binary: bin/lab01s01 + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cb39f5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Gabriel Moreira Chaves and Ian Bittencourt Andrade Jacinto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cmd/lab01s01/main.go b/cmd/lab01s01/main.go new file mode 100644 index 0000000..4b24e3d --- /dev/null +++ b/cmd/lab01s01/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "encoding/json" + "os" + + "github.com/gamoch/popular_repos/internal/pkg/configuration" + "github.com/gamoch/popular_repos/pkg/graphql" + "github.com/gamoch/popular_repos/pkg/graphql/providers/github" + "github.com/gamoch/popular_repos/pkg/logs" +) + +const query = `query PopularRepos { + search(query: "stars:>1", type: REPOSITORY, first: 100) { + nodes { + ... on Repository { + nameWithOwner + stargazerCount + createdAt + } + } + } +}` + +func main() { + config := configuration.Get() + ctx := context.Background() + + githubClient := github.NewClient(config.Token) + req := graphql.NewRequest(query) + + graphQLData := new(GraphQLData) + if err := githubClient.Run(ctx, req, graphQLData); err != nil { + logs.Error.Fatal(err) + } + + graphqlJSON, err := json.MarshalIndent(graphQLData, "", " ") + if err != nil { + logs.Error.Fatal(err) + } + + os.Stdout.Write(graphqlJSON) +} diff --git a/cmd/lab01s01/structures.go b/cmd/lab01s01/structures.go new file mode 100644 index 0000000..eb1b57b --- /dev/null +++ b/cmd/lab01s01/structures.go @@ -0,0 +1,13 @@ +package main + +import "time" + +type GraphQLData struct { + Search struct { + Nodes []struct { + NameWithOwner string `json:"nameWithOwner"` + StargazerCount int `json:"stargazerCount"` + CreatedAt time.Time `json:"createdAt"` + } `json:"nodes"` + } `json:"search"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29970a5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gamoch/popular_repos + +go 1.16 diff --git a/internal/pkg/configuration/configuration.go b/internal/pkg/configuration/configuration.go new file mode 100644 index 0000000..497df74 --- /dev/null +++ b/internal/pkg/configuration/configuration.go @@ -0,0 +1,19 @@ +package configuration + +import ( + "flag" + "os" +) + +type config struct { + Token string +} + +func Get() *config { + token := flag.String("token", os.Getenv("GITHUB_TOKEN"), "GitHub Token (env: GITHUB_TOKEN)") + flag.Parse() + + return &config{ + Token: *token, + } +} diff --git a/pkg/graphql/client.go b/pkg/graphql/client.go new file mode 100644 index 0000000..7ed3a17 --- /dev/null +++ b/pkg/graphql/client.go @@ -0,0 +1,85 @@ +package graphql + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +type Client struct { + endpoint string + httpClient *http.Client +} + +type ClientOption func(*Client) + +type ClientRunner interface { + Run(ctx context.Context, req *Request, res interface{}) error +} + +func NewClient(endpoint string, options ...ClientOption) *Client { + client := &Client{ + endpoint: endpoint, + httpClient: http.DefaultClient, + } + + for _, optionFunc := range options { + optionFunc(client) + } + + return client +} + +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(client *Client) { + client.httpClient = httpClient + } +} + +func (c *Client) Run(ctx context.Context, req *Request, res interface{}) error { + if req == nil { + return &Err{err: ErrNilRequest} + } + + if ctx == nil { + ctx = context.Background() + } else if err := ctx.Err(); err != nil { + return &Err{err: err} + } + + reqBody := new(bytes.Buffer) + if err := json.NewEncoder(reqBody).Encode(req.requestBody); err != nil { + return &Err{err: err} + } + + graphQLReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, reqBody) + if err != nil { + return &Err{err: err} + } + graphQLReq.Header = req.Header + + resp, err := c.httpClient.Do(graphQLReq) + if err != nil { + return &Err{err: err} + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return &Err{err: &ErrStatus{ + Status: resp.Status, + StatusCode: resp.StatusCode, + }} + } + + graphQLRes := &response{Data: res} + if err = json.NewDecoder(resp.Body).Decode(graphQLRes); err != nil { + return &Err{err: err} + } + + if len(graphQLRes.Errors) > 0 { + return &Err{err: joinResponseErrors(graphQLRes.Errors)} + } + + return nil +} diff --git a/pkg/graphql/errors.go b/pkg/graphql/errors.go new file mode 100644 index 0000000..546a5c5 --- /dev/null +++ b/pkg/graphql/errors.go @@ -0,0 +1,26 @@ +package graphql + +import "errors" + +type Err struct { + err error +} + +func (e *Err) Error() string { + return "graphql: " + e.err.Error() +} + +func (e *Err) Unwrap() error { + return e.err +} + +type ErrStatus struct { + StatusCode int + Status string +} + +func (e *ErrStatus) Error() string { + return "wrong HTTP status: " + e.Status +} + +var ErrNilRequest = errors.New("graphQL request not provided") diff --git a/pkg/graphql/providers/github/client.go b/pkg/graphql/providers/github/client.go new file mode 100644 index 0000000..054dffe --- /dev/null +++ b/pkg/graphql/providers/github/client.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "errors" + "net/http" + + "github.com/gamoch/popular_repos/pkg/graphql" + "github.com/gamoch/popular_repos/pkg/logs" +) + +const endpoint = "https://api.github.com/graphql" + +type client struct { + token string + client *graphql.Client +} + +func NewClient(token string, options ...graphql.ClientOption) *client { + if token == "" { + logs.Error.Fatalln("GITHUB_TOKEN is required") + } + + return &client{ + token: token, + client: graphql.NewClient(endpoint, options...), + } +} + +func (c *client) Run(ctx context.Context, req *graphql.Request, res interface{}) error { + if req != nil { + req.Header.Add("Authorization", "bearer "+c.token) + } + + err := c.client.Run(ctx, req, res) + if cause := errors.Unwrap(err); cause != nil { + if errStatus, ok := cause.(*graphql.ErrStatus); ok { + if errStatus.StatusCode == http.StatusUnauthorized { + return &Err{err: &ErrInvalidToken{err: err}} + } + } + } + + if err != nil { + return &Err{err: err} + } + + return nil +} diff --git a/pkg/graphql/providers/github/errors.go b/pkg/graphql/providers/github/errors.go new file mode 100644 index 0000000..f0a3b9a --- /dev/null +++ b/pkg/graphql/providers/github/errors.go @@ -0,0 +1,25 @@ +package github + +type Err struct { + err error +} + +func (e *Err) Error() string { + return "github: " + e.err.Error() +} + +func (e *Err) Unwrap() error { + return e.err +} + +type ErrInvalidToken struct { + err error +} + +func (e *ErrInvalidToken) Error() string { + return "invalid token; " + e.err.Error() +} + +func (e *ErrInvalidToken) Unwrap() error { + return e.err +} diff --git a/pkg/graphql/request.go b/pkg/graphql/request.go new file mode 100644 index 0000000..8fcdd23 --- /dev/null +++ b/pkg/graphql/request.go @@ -0,0 +1,64 @@ +package graphql + +import "net/http" + +type Request struct { + requestBody requestBody + Header http.Header +} + +type requestBody struct { + Query string `json:"query"` + Variables RequestVariables `json:"variables,omitempty"` +} + +type RequestVariables map[string]interface{} + +type RequestOption func(*Request) + +func NewRequest(query string, options ...RequestOption) *Request { + request := &Request{ + requestBody: requestBody{ + Query: query, + Variables: make(RequestVariables), + }, + Header: http.Header{ + "Content-Type": {"application/json; charset=utf-8"}, + "Accept": {"application/json; charset=utf-8"}, + }, + } + + for _, optionFunc := range options { + optionFunc(request) + } + + return request +} + +func WithHTTPHeader(httpHeader http.Header) RequestOption { + return func(request *Request) { + for key, values := range httpHeader { + for _, value := range values { + request.Header.Add(key, value) + } + } + } +} + +func WithVariables(variables RequestVariables) RequestOption { + return func(request *Request) { + request.requestBody.Variables = variables + } +} + +func (r *Request) GetQuery() string { + return r.requestBody.Query +} + +func (r *Request) GetVariables() RequestVariables { + return r.requestBody.Variables +} + +func (r *Request) SetVariable(key string, value interface{}) { + r.requestBody.Variables[key] = value +} diff --git a/pkg/graphql/response.go b/pkg/graphql/response.go new file mode 100644 index 0000000..c031aa9 --- /dev/null +++ b/pkg/graphql/response.go @@ -0,0 +1,29 @@ +package graphql + +import ( + "strings" +) + +type response struct { + Data interface{} `json:"data,omitempty"` + Errors []ErrResponse `json:"errors,omitempty"` +} + +type ErrResponse struct { + Message string +} + +func (e *ErrResponse) Error() string { + return "graphql query: " + e.Message +} + +func joinResponseErrors(errs []ErrResponse) error { + messages := make([]string, len(errs)) + for i, err := range errs { + messages[i] = err.Message + } + + return &ErrResponse{ + Message: strings.Join(messages, "; "), + } +} diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go new file mode 100644 index 0000000..bd0558c --- /dev/null +++ b/pkg/logs/logs.go @@ -0,0 +1,10 @@ +package logs + +import ( + "log" + "os" +) + +var ( + Error = log.New(os.Stderr, "[ERROR] ", 0) +)