diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..cfaa562 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ trunk ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ trunk ] + schedule: + - cron: '32 4 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3c84b5e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,14 @@ +name: Lint +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + version: latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..000f7da --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Test +on: [push, pull_request] +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go: [1.16] + runs-on: ${{ matrix.os }} + + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run tests + run: go test -v ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2d5cc27 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,11 @@ +linters: + enable: + - gofmt + - godot + +linters-settings: + godot: + # comments to be checked: `declarations`, `toplevel`, or `all` + scope: declarations + # check that each sentence starts with a capital letter + capital: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6452acb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: go -go: - - 1.x - - master -matrix: - allow_failures: - - go: master - fast_finish: true -install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). -script: - - go get -t -v ./... - - diff -n <(echo -n) <(gofmt -d -s .) - - go vet ./... - - go test -v -race ./... diff --git a/README.md b/README.md index 3353795..40e8de9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ graphql ======= -[![Build Status](https://travis-ci.org/shurcooL/graphql.svg?branch=master)](https://travis-ci.org/shurcooL/graphql) [![GoDoc](https://godoc.org/github.com/shurcooL/graphql?status.svg)](https://godoc.org/github.com/shurcooL/graphql) - -Package `graphql` provides a GraphQL client implementation. - -For more information, see package [`github.com/shurcooL/githubv4`](https://github.com/shurcooL/githubv4), which is a specialized version targeting GitHub GraphQL API v4. That package is driving the feature development. +Package `graphql` provides a GraphQL client implementation, and is forked from `https://github.com/shurcooL/graphql`. Installation ------------ -`graphql` requires Go version 1.8 or later. +`graphql` requires Go version 1.16 or later. ```bash -go get -u github.com/shurcooL/graphql +go get -u github.com/cli/shurcooL-graphql ``` Usage @@ -276,15 +272,6 @@ fmt.Printf("Created a %v star review: %v\n", m.CreateReview.Stars, m.CreateRevie // Created a 5 star review: This is a great movie! ``` -Directories ------------ - -| Path | Synopsis | -|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. | -| [ident](https://godoc.org/github.com/shurcooL/graphql/ident) | Package ident provides functions for parsing and converting identifier names between various naming convention. | -| [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. | - License ------- diff --git a/doc.go b/doc.go deleted file mode 100644 index 870e3d4..0000000 --- a/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package graphql provides a GraphQL client implementation. -// -// For more information, see package github.com/shurcooL/githubv4, -// which is a specialized version targeting GitHub GraphQL API v4. -// That package is driving the feature development. -// -// For now, see README for more details. -package graphql // import "github.com/shurcooL/graphql" diff --git a/example/graphqldev/main.go b/example/graphqldev/main.go deleted file mode 100644 index ffc8302..0000000 --- a/example/graphqldev/main.go +++ /dev/null @@ -1,105 +0,0 @@ -// graphqldev is a test program currently being used for developing graphql package. -// It performs queries against a local test GraphQL server instance. -// -// It's not meant to be a clean or readable example. But it's functional. -// Better, actual examples will be created in the future. -package main - -import ( - "context" - "encoding/json" - "flag" - "log" - "net/http" - "net/http/httptest" - "os" - - graphqlserver "github.com/graph-gophers/graphql-go" - "github.com/graph-gophers/graphql-go/example/starwars" - "github.com/graph-gophers/graphql-go/relay" - "github.com/shurcooL/graphql" -) - -func main() { - flag.Parse() - - err := run() - if err != nil { - log.Println(err) - } -} - -func run() error { - // Set up a GraphQL server. - schema, err := graphqlserver.ParseSchema(starwars.Schema, &starwars.Resolver{}) - if err != nil { - return err - } - mux := http.NewServeMux() - mux.Handle("/query", &relay.Handler{Schema: schema}) - - client := graphql.NewClient("/query", &http.Client{Transport: localRoundTripper{handler: mux}}) - - /* - query { - hero { - id - name - } - character(id: "1003") { - name - friends { - name - __typename - } - appearsIn - } - } - */ - var q struct { - Hero struct { - ID graphql.ID - Name graphql.String - } - Character struct { - Name graphql.String - Friends []struct { - Name graphql.String - Typename graphql.String `graphql:"__typename"` - } - AppearsIn []graphql.String - } `graphql:"character(id: $characterID)"` - } - variables := map[string]interface{}{ - "characterID": graphql.ID("1003"), - } - err = client.Query(context.Background(), &q, variables) - if err != nil { - return err - } - print(q) - - return nil -} - -// print pretty prints v to stdout. It panics on any error. -func print(v interface{}) { - w := json.NewEncoder(os.Stdout) - w.SetIndent("", "\t") - err := w.Encode(v) - if err != nil { - panic(err) - } -} - -// localRoundTripper is an http.RoundTripper that executes HTTP transactions -// by using handler directly, instead of going over an HTTP connection. -type localRoundTripper struct { - handler http.Handler -} - -func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - w := httptest.NewRecorder() - l.handler.ServeHTTP(w, req) - return w.Result(), nil -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1d5df24 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/cli/shurcooL-graphql + +go 1.16 + +require golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cccd595 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/graphql.go b/graphql.go index 8520956..06ca511 100644 --- a/graphql.go +++ b/graphql.go @@ -7,8 +7,9 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" - "github.com/shurcooL/graphql/internal/jsonutil" + "github.com/cli/shurcooL-graphql/internal/jsonutil" "golang.org/x/net/context/ctxhttp" ) @@ -32,26 +33,36 @@ func NewClient(url string, httpClient *http.Client) *Client { // Query executes a single GraphQL query request, // with a query derived from q, populating the response into it. -// q should be a pointer to struct that corresponds to the GraphQL schema. +// Argument q should be a pointer to struct that corresponds to the GraphQL schema. func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error { - return c.do(ctx, queryOperation, q, variables) + return c.do(ctx, queryOperation, q, variables, "") +} + +// QueryNamed is the same as Query but allows a name to be specified for the query. +func (c *Client) QueryNamed(ctx context.Context, queryName string, q interface{}, variables map[string]interface{}) error { + return c.do(ctx, queryOperation, q, variables, queryName) } // Mutate executes a single GraphQL mutation request, // with a mutation derived from m, populating the response into it. -// m should be a pointer to struct that corresponds to the GraphQL schema. +// Argument m should be a pointer to struct that corresponds to the GraphQL schema. func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}) error { - return c.do(ctx, mutationOperation, m, variables) + return c.do(ctx, mutationOperation, m, variables, "") +} + +// MutateNamed is the same as Mutate but allows a name to be specified for the mutation. +func (c *Client) MutateNamed(ctx context.Context, queryName string, m interface{}, variables map[string]interface{}) error { + return c.do(ctx, mutationOperation, m, variables, queryName) } // do executes a single GraphQL operation. -func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}) error { +func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, queryName string) error { var query string switch op { case queryOperation: - query = constructQuery(v, variables) + query = constructQuery(v, variables, queryName) case mutationOperation: - query = constructMutation(v, variables) + query = constructMutation(v, variables, queryName) } in := struct { Query string `json:"query"` @@ -111,7 +122,15 @@ type errors []struct { // Error implements error interface. func (e errors) Error() string { - return e[0].Message + b := strings.Builder{} + l := len(e) + for i, err := range e { + b.WriteString(fmt.Sprintf("Message: %s, Locations: %+v", err.Message, err.Locations)) + if i != l-1 { + b.WriteString("\n") + } + } + return b.String() } type operationType uint8 @@ -119,5 +138,4 @@ type operationType uint8 const ( queryOperation operationType = iota mutationOperation - //subscriptionOperation // Unused. ) diff --git a/graphql_test.go b/graphql_test.go index e09dcc9..2fb7722 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -8,7 +8,7 @@ import ( "net/http/httptest" "testing" - "github.com/shurcooL/graphql" + graphql "github.com/cli/shurcooL-graphql" ) func TestClient_Query_partialDataWithErrorResponse(t *testing.T) { @@ -53,7 +53,7 @@ func TestClient_Query_partialDataWithErrorResponse(t *testing.T) { if err == nil { t.Fatal("got error: nil, want: non-nil") } - if got, want := err.Error(), "Could not resolve to a node with the global id of 'NotExist'"; got != want { + if got, want := err.Error(), "Message: Could not resolve to a node with the global id of 'NotExist', Locations: [{Line:10 Column:4}]"; got != want { t.Errorf("got error: %v, want: %v", got, want) } if q.Node1 == nil || q.Node1.ID != "MDEyOklzc3VlQ29tbWVudDE2OTQwNzk0Ng==" { @@ -93,7 +93,7 @@ func TestClient_Query_noDataWithErrorResponse(t *testing.T) { if err == nil { t.Fatal("got error: nil, want: non-nil") } - if got, want := err.Error(), "Field 'user' is missing required arguments: login"; got != want { + if got, want := err.Error(), "Message: Field 'user' is missing required arguments: login, Locations: [{Line:7 Column:3}]"; got != want { t.Errorf("got error: %v, want: %v", got, want) } if q.User.Name != "" { diff --git a/ident/ident_test.go b/ident/ident_test.go index 9ee1b47..b6487a8 100644 --- a/ident/ident_test.go +++ b/ident/ident_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/shurcooL/graphql/ident" + "github.com/cli/shurcooL-graphql/ident" ) func Example_lowerCamelCaseToMixedCaps() { diff --git a/internal/jsonutil/benchmark_test.go b/internal/jsonutil/benchmark_test.go index f8788b1..c803530 100644 --- a/internal/jsonutil/benchmark_test.go +++ b/internal/jsonutil/benchmark_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/shurcooL/graphql" - "github.com/shurcooL/graphql/internal/jsonutil" + "github.com/cli/shurcooL-graphql" + "github.com/cli/shurcooL-graphql/internal/jsonutil" ) func TestUnmarshalGraphQL_benchmark(t *testing.T) { diff --git a/internal/jsonutil/graphql.go b/internal/jsonutil/graphql.go index 15bae24..1f165fd 100644 --- a/internal/jsonutil/graphql.go +++ b/internal/jsonutil/graphql.go @@ -300,7 +300,7 @@ func isGraphQLFragment(f reflect.StructField) bool { } // unmarshalValue unmarshals JSON value into v. -// v must be addressable and not obtained by the use of unexported +// Argument v must be addressable and not obtained by the use of unexported // struct fields, otherwise unmarshalValue will panic. func unmarshalValue(value json.Token, v reflect.Value) error { b, err := json.Marshal(value) // TODO: Short-circuit (if profiling says it's worth it). diff --git a/internal/jsonutil/graphql_test.go b/internal/jsonutil/graphql_test.go index 6329ed8..1c92965 100644 --- a/internal/jsonutil/graphql_test.go +++ b/internal/jsonutil/graphql_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/shurcooL/graphql" - "github.com/shurcooL/graphql/internal/jsonutil" + "github.com/cli/shurcooL-graphql" + "github.com/cli/shurcooL-graphql/internal/jsonutil" ) func TestUnmarshalGraphQL(t *testing.T) { @@ -243,7 +243,7 @@ func TestUnmarshalGraphQL_pointerWithInlineFragment(t *testing.T) { func TestUnmarshalGraphQL_unexportedField(t *testing.T) { type query struct { - foo graphql.String + foo graphql.String //nolint } err := jsonutil.UnmarshalGraphQL([]byte(`{"foo": "bar"}`), new(query)) if err == nil { diff --git a/query.go b/query.go index e10b771..0d04b04 100644 --- a/query.go +++ b/query.go @@ -7,23 +7,32 @@ import ( "reflect" "sort" - "github.com/shurcooL/graphql/ident" + "github.com/cli/shurcooL-graphql/ident" ) -func constructQuery(v interface{}, variables map[string]interface{}) string { +func constructQuery(v interface{}, variables map[string]interface{}, queryName string) string { query := query(v) if len(variables) > 0 { - return "query(" + queryArguments(variables) + ")" + query + return "query" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query + } else if queryName != "" { + return "query" + queryNameFormat(queryName) + query } return query } -func constructMutation(v interface{}, variables map[string]interface{}) string { +func constructMutation(v interface{}, variables map[string]interface{}, queryName string) string { query := query(v) if len(variables) > 0 { - return "mutation(" + queryArguments(variables) + ")" + query + return "mutation" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query } - return "mutation" + query + return "mutation" + queryNameFormat(queryName) + query +} + +func queryNameFormat(n string) string { + if n != "" { + return " " + n + } + return n } // queryArguments constructs a minified arguments string for variables. @@ -40,9 +49,9 @@ func queryArguments(variables map[string]interface{}) string { var buf bytes.Buffer for _, k := range keys { - io.WriteString(&buf, "$") - io.WriteString(&buf, k) - io.WriteString(&buf, ":") + _, _ = io.WriteString(&buf, "$") + _, _ = io.WriteString(&buf, k) + _, _ = io.WriteString(&buf, ":") writeArgumentType(&buf, reflect.TypeOf(variables[k]), true) // Don't insert a comma here. // Commas in GraphQL are insignificant, and we want minified output. @@ -52,7 +61,7 @@ func queryArguments(variables map[string]interface{}) string { } // writeArgumentType writes a minified GraphQL type for t to w. -// value indicates whether t is a value (required) type or pointer (optional) type. +// Argument value indicates whether t is a value (required) type or pointer (optional) type. // If value is true, then "!" is written at the end of t. func writeArgumentType(w io.Writer, t reflect.Type, value bool) { if t.Kind() == reflect.Ptr { @@ -64,21 +73,21 @@ func writeArgumentType(w io.Writer, t reflect.Type, value bool) { switch t.Kind() { case reflect.Slice, reflect.Array: // List. E.g., "[Int]". - io.WriteString(w, "[") + _, _ = io.WriteString(w, "[") writeArgumentType(w, t.Elem(), true) - io.WriteString(w, "]") + _, _ = io.WriteString(w, "]") default: // Named type. E.g., "Int". name := t.Name() if name == "string" { // HACK: Workaround for https://github.com/shurcooL/githubv4/issues/12. name = "ID" } - io.WriteString(w, name) + _, _ = io.WriteString(w, name) } if value { // Value is a required type, so add "!" to the end. - io.WriteString(w, "!") + _, _ = io.WriteString(w, "!") } } @@ -104,26 +113,26 @@ func writeQuery(w io.Writer, t reflect.Type, inline bool) { return } if !inline { - io.WriteString(w, "{") + _, _ = io.WriteString(w, "{") } for i := 0; i < t.NumField(); i++ { if i != 0 { - io.WriteString(w, ",") + _, _ = io.WriteString(w, ",") } f := t.Field(i) value, ok := f.Tag.Lookup("graphql") inlineField := f.Anonymous && !ok if !inlineField { if ok { - io.WriteString(w, value) + _, _ = io.WriteString(w, value) } else { - io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase()) + _, _ = io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase()) } } writeQuery(w, f.Type, inlineField) } if !inline { - io.WriteString(w, "}") + _, _ = io.WriteString(w, "}") } } } diff --git a/query_test.go b/query_test.go index 4de8cb5..c290d91 100644 --- a/query_test.go +++ b/query_test.go @@ -229,7 +229,7 @@ func TestConstructQuery(t *testing.T) { }, } for _, tc := range tests { - got := constructQuery(tc.inV, tc.inVariables) + got := constructQuery(tc.inV, tc.inVariables, "") if got != tc.want { t.Errorf("\ngot: %q\nwant: %q\n", got, tc.want) } @@ -264,7 +264,7 @@ func TestConstructMutation(t *testing.T) { }, } for _, tc := range tests { - got := constructMutation(tc.inV, tc.inVariables) + got := constructMutation(tc.inV, tc.inVariables, "") if got != tc.want { t.Errorf("\ngot: %q\nwant: %q\n", got, tc.want) } diff --git a/scalar_test.go b/scalar_test.go index 8334b96..b91fcf5 100644 --- a/scalar_test.go +++ b/scalar_test.go @@ -3,7 +3,7 @@ package graphql_test import ( "testing" - "github.com/shurcooL/graphql" + "github.com/cli/shurcooL-graphql" ) func TestNewScalars(t *testing.T) {