Upgrade to go 1.19 and update dependencies (#3069)

* Update to go 1.19
* Update dependencies
* Update cross-compile script
* Add missing targets to cross-compile-all
* Update cache action to remove warning
This commit is contained in:
WithoutPants 2022-11-04 13:41:26 +11:00 committed by GitHub
parent f25881a3bf
commit bba7c23957
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
939 changed files with 101336 additions and 43819 deletions

View file

@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
build:
@ -27,7 +27,7 @@ jobs:
run: docker pull $COMPILER_IMAGE
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node_modules
with:
@ -35,7 +35,7 @@ jobs:
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }}
- name: Cache UI build
uses: actions/cache@v2
uses: actions/cache@v3
id: cache-ui
env:
cache-name: cache-ui
@ -44,7 +44,7 @@ jobs:
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }}
- name: Cache go build
uses: actions/cache@v2
uses: actions/cache@v3
env:
# increment the number suffix to bump the cache
cache-name: cache-go-cache-1

View file

@ -9,7 +9,7 @@ on:
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
golangci:

View file

@ -135,7 +135,8 @@ cross-compile-linux-arm32v6: build-release-static
cross-compile-all:
make cross-compile-windows
make cross-compile-macos
make cross-compile-macos-intel
make cross-compile-macos-applesilicon
make cross-compile-linux
make cross-compile-linux-arm64v8
make cross-compile-linux-arm32v7

View file

@ -16,7 +16,7 @@ ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
# Build Backend
FROM golang:1.17-alpine as backend
FROM golang:1.19-alpine as backend
RUN apk add --no-cache make alpine-sdk
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/

View file

@ -1,4 +1,4 @@
FROM golang:1.17
FROM golang:1.19
LABEL maintainer="https://discord.gg/2TsNFKt"

View file

@ -1,6 +1,6 @@
user=stashapp
repo=compiler
version=6
version=7
latest:
docker build -t ${user}/${repo}:latest .

114
go.mod
View file

@ -1,111 +1,115 @@
module github.com/stashapp/stash
require (
github.com/99designs/gqlgen v0.17.2
github.com/Yamashou/gqlgenc v0.0.6
github.com/anacrolix/dms v1.2.2
github.com/antchfx/htmlquery v1.2.5-0.20211125074323-810ee8082758
github.com/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84
github.com/chromedp/chromedp v0.7.3
github.com/corona10/goimagehash v1.0.3
github.com/disintegration/imaging v1.6.0
github.com/99designs/gqlgen v0.17.20
github.com/Yamashou/gqlgenc v0.11.2
github.com/anacrolix/dms v1.5.0
github.com/antchfx/htmlquery v1.2.5
github.com/chromedp/cdproto v0.0.0-20221029224954-108014bf7279
github.com/chromedp/chromedp v0.8.6
github.com/corona10/goimagehash v1.1.0
github.com/disintegration/imaging v1.6.2
github.com/fvbommel/sortorder v1.0.2
github.com/go-chi/chi v4.0.2+incompatible
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
github.com/go-chi/chi v4.1.2+incompatible
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.3.5
github.com/jmoiron/sqlx v1.3.5
github.com/json-iterator/go v1.1.12
github.com/mattn/go-sqlite3 v1.14.7
github.com/mattn/go-sqlite3 v1.14.16
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/rs/cors v1.6.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.8.2 // indirect
github.com/robertkrimen/otto v0.0.0-20221025135307-511d75fba9f8
github.com/rs/cors v1.8.2
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29
github.com/sirupsen/logrus v1.9.0
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.9.3
github.com/tidwall/pretty v1.2.0 // indirect
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.1
github.com/tidwall/gjson v1.14.3
github.com/tidwall/pretty v1.2.1 // indirect
github.com/vektra/mockery/v2 v2.10.0
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
golang.org/x/crypto v0.1.0
golang.org/x/image v0.1.0
golang.org/x/net v0.1.0
golang.org/x/sys v0.1.0
golang.org/x/term v0.1.0
golang.org/x/text v0.4.0
golang.org/x/tools v0.1.12 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/asticode/go-astisub v0.20.0
github.com/asticode/go-astisub v0.21.0
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/httplog v0.2.1
github.com/go-chi/httplog v0.2.5
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/hashicorp/golang-lru v0.5.4
github.com/kermieisinthehouse/gosx-notifier v0.1.1
github.com/kermieisinthehouse/systray v1.2.4
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/spf13/cast v1.4.1
github.com/vearutop/statigz v1.1.6
github.com/spf13/cast v1.5.0
github.com/vearutop/statigz v1.1.8
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser/v2 v2.4.1
github.com/vektah/gqlparser/v2 v2.5.1
gopkg.in/guregu/null.v4 v4.0.0
)
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/antchfx/xpath v1.2.0 // indirect
github.com/asticode/go-astikit v0.20.0 // indirect
github.com/asticode/go-astits v1.8.0 // indirect
github.com/anacrolix/log v0.13.1 // indirect
github.com/antchfx/xpath v1.2.1 // indirect
github.com/asticode/go-astikit v0.35.0 // indirect
github.com/asticode/go-astits v1.11.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-chi/chi/v5 v5.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-chi/chi/v5 v5.0.7 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0-rc.5 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect
github.com/rs/zerolog v1.28.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/urfave/cli/v2 v2.4.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.17
go 1.19

1102
go.sum

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
package api
import (
"compress/flate"
"context"
"crypto/tls"
"errors"
@ -61,8 +62,7 @@ func Start() error {
r.Use(httplog.RequestLogger(httpLogger))
}
r.Use(SecurityHeadersMiddleware)
r.Use(middleware.DefaultCompress)
r.Use(middleware.StripSlashes)
r.Use(middleware.Compress(flate.DefaultCompression))
r.Use(cors.AllowAll().Handler)
r.Use(BaseURLMiddleware)

View file

@ -14,15 +14,15 @@ const APIKeySubject = "APIKey"
type APIKeyClaims struct {
UserID string `json:"uid"`
jwt.StandardClaims
jwt.RegisteredClaims
}
func GenerateAPIKey(userID string) (string, error) {
claims := &APIKeyClaims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: APIKeySubject,
IssuedAt: time.Now().Unix(),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}

View file

@ -1,6 +1,6 @@
#!/bin/bash
COMPILER_CONTAINER="stashapp/compiler:6"
COMPILER_CONTAINER="stashapp/compiler:7"
BUILD_DATE=`go run -mod=vendor scripts/getDate.go`
GITHASH=`git rev-parse --short HEAD`

View file

@ -14,3 +14,5 @@
.idea/
*.test
*.out
gqlgen
*.exe

File diff suppressed because it is too large Load diff

View file

@ -142,9 +142,20 @@ first model in this list is used as the default type and it will always be used
There isn't any way around this, gqlgen has no way to know what you want in a given context.
### Why do my interfaces have getters? Can I disable these?
These were added in v0.17.14 to allow accessing common interface fields without casting to a concrete type.
However, certain fields, like Relay-style Connections, cannot be implemented with simple getters.
If you'd prefer to not have getters generated in your interfaces, you can add the following in your `gqlgen.yml`:
```yaml
# gqlgen.yml
omit_getters: true
```
## Other Resources
- [Christopher Biscardi @ Gophercon UK 2018](https://youtu.be/FdURVezcdcw)
- [Introducing gqlgen: a GraphQL Server Generator for Go](https://99designs.com.au/blog/engineering/gqlgen-a-graphql-server-generator-for-go/)
- [Dive into GraphQL by Iván Corrales Solera](https://medium.com/@ivan.corrales.solera/dive-into-graphql-9bfedf22e1a)
- [Sample Project built on gqlgen with Postgres by Oleg Shalygin](https://github.com/oshalygin/gqlgen-pg-todo-example)
- [Hackernews GraphQL Server with gqlgen by Shayegan Hooshyari](https://www.howtographql.com/graphql-go/0-introduction/)

View file

@ -2,6 +2,7 @@ package api
import (
"fmt"
"regexp"
"syscall"
"github.com/99designs/gqlgen/codegen"
@ -24,7 +25,20 @@ func Generate(cfg *config.Config, option ...Option) error {
}
plugins = append(plugins, resolvergen.New())
if cfg.Federation.IsDefined() {
plugins = append([]plugin.Plugin{federation.New()}, plugins...)
if cfg.Federation.Version == 0 { // default to using the user's choice of version, but if unset, try to sort out which federation version to use
urlRegex := regexp.MustCompile(`(?s)@link.*\(.*url:.*?"(.*?)"[^)]+\)`) // regex to grab the url of a link directive, should it exist
// check the sources, and if one is marked as federation v2, we mark the entirety to be generated using that format
for _, v := range cfg.Sources {
cfg.Federation.Version = 1
urlString := urlRegex.FindStringSubmatch(v.Input)
if urlString != nil && urlString[1] == "https://specs.apollo.dev/federation/v2.0" {
cfg.Federation.Version = 2
break
}
}
}
plugins = append([]plugin.Plugin{federation.New(cfg.Federation.Version)}, plugins...)
}
for _, o := range option {

View file

@ -73,11 +73,15 @@ func (b *builder) buildArg(obj *Object, arg *ast.ArgumentDefinition) (*FieldArgu
return &newArg, nil
}
func (b *builder) bindArgs(field *Field, params *types.Tuple) ([]*FieldArgument, error) {
var newArgs []*FieldArgument
func (b *builder) bindArgs(field *Field, sig *types.Signature, params *types.Tuple) ([]*FieldArgument, error) {
n := params.Len()
newArgs := make([]*FieldArgument, 0, len(field.Args))
// Accept variadic methods (i.e. have optional parameters).
if params.Len() > len(field.Args) && sig.Variadic() {
n = len(field.Args)
}
nextArg:
for j := 0; j < params.Len(); j++ {
for j := 0; j < n; j++ {
param := params.At(j)
for _, oldArg := range field.Args {
if strings.EqualFold(oldArg.Name, param.Name()) {

View file

@ -216,7 +216,6 @@ func (t *TypeReference) IsPtr() bool {
}
// fix for https://github.com/golang/go/issues/31103 may make it possible to remove this (may still be useful)
//
func (t *TypeReference) IsPtrToPtr() bool {
if p, isPtr := t.GO.(*types.Pointer); isPtr {
_, isPtr := p.Elem().(*types.Pointer)

View file

@ -1,8 +1,8 @@
package config
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -12,25 +12,28 @@ import (
"github.com/99designs/gqlgen/internal/code"
"github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
type Config struct {
SchemaFilename StringList `yaml:"schema,omitempty"`
Exec ExecConfig `yaml:"exec"`
Model PackageConfig `yaml:"model,omitempty"`
Federation PackageConfig `yaml:"federation,omitempty"`
Resolver ResolverConfig `yaml:"resolver,omitempty"`
AutoBind []string `yaml:"autobind"`
Models TypeMap `yaml:"models,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"`
Directives map[string]DirectiveConfig `yaml:"directives,omitempty"`
OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"`
SkipValidation bool `yaml:"skip_validation,omitempty"`
SkipModTidy bool `yaml:"skip_mod_tidy,omitempty"`
Sources []*ast.Source `yaml:"-"`
Packages *code.Packages `yaml:"-"`
Schema *ast.Schema `yaml:"-"`
SchemaFilename StringList `yaml:"schema,omitempty"`
Exec ExecConfig `yaml:"exec"`
Model PackageConfig `yaml:"model,omitempty"`
Federation PackageConfig `yaml:"federation,omitempty"`
Resolver ResolverConfig `yaml:"resolver,omitempty"`
AutoBind []string `yaml:"autobind"`
Models TypeMap `yaml:"models,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"`
Directives map[string]DirectiveConfig `yaml:"directives,omitempty"`
OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"`
OmitGetters bool `yaml:"omit_getters,omitempty"`
StructFieldsAlwaysPointers bool `yaml:"struct_fields_always_pointers,omitempty"`
ResolversAlwaysReturnPointers bool `yaml:"resolvers_always_return_pointers,omitempty"`
SkipValidation bool `yaml:"skip_validation,omitempty"`
SkipModTidy bool `yaml:"skip_mod_tidy,omitempty"`
Sources []*ast.Source `yaml:"-"`
Packages *code.Packages `yaml:"-"`
Schema *ast.Schema `yaml:"-"`
// Deprecated: use Federation instead. Will be removed next release
Federated bool `yaml:"federated,omitempty"`
@ -41,11 +44,13 @@ var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"}
// DefaultConfig creates a copy of the default config
func DefaultConfig() *Config {
return &Config{
SchemaFilename: StringList{"schema.graphql"},
Model: PackageConfig{Filename: "models_gen.go"},
Exec: ExecConfig{Filename: "generated.go"},
Directives: map[string]DirectiveConfig{},
Models: TypeMap{},
SchemaFilename: StringList{"schema.graphql"},
Model: PackageConfig{Filename: "models_gen.go"},
Exec: ExecConfig{Filename: "generated.go"},
Directives: map[string]DirectiveConfig{},
Models: TypeMap{},
StructFieldsAlwaysPointers: true,
ResolversAlwaysReturnPointers: true,
}
}
@ -57,7 +62,7 @@ func LoadDefaultConfig() (*Config, error) {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
schemaRaw, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to open schema: %w", err)
}
@ -94,12 +99,15 @@ var path2regex = strings.NewReplacer(
func LoadConfig(filename string) (*Config, error) {
config := DefaultConfig()
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to read config: %w", err)
}
if err := yaml.UnmarshalStrict(b, config); err != nil {
dec := yaml.NewDecoder(bytes.NewReader(b))
dec.KnownFields(true)
if err := dec.Decode(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
@ -173,7 +181,7 @@ func CompleteConfig(config *Config) error {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
schemaRaw, err = os.ReadFile(filename)
if err != nil {
return fmt.Errorf("unable to open schema: %w", err)
}

View file

@ -12,6 +12,7 @@ import (
type PackageConfig struct {
Filename string `yaml:"filename,omitempty"`
Package string `yaml:"package,omitempty"`
Version int `yaml:"version,omitempty"`
}
func (c *PackageConfig) ImportPath() string {

View file

@ -2,7 +2,10 @@ package codegen
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/vektah/gqlparser/v2/ast"
@ -30,6 +33,26 @@ type Data struct {
QueryRoot *Object
MutationRoot *Object
SubscriptionRoot *Object
AugmentedSources []AugmentedSource
}
func (d *Data) HasEmbeddableSources() bool {
hasEmbeddableSources := false
for _, s := range d.AugmentedSources {
if s.Embeddable {
hasEmbeddableSources = true
}
}
return hasEmbeddableSources
}
// AugmentedSource contains extra information about graphql schema files which is not known directly from the Config.Sources data
type AugmentedSource struct {
// path relative to Config.Exec.Filename
RelativePath string
Embeddable bool
BuiltIn bool
Source string
}
type builder struct {
@ -147,6 +170,31 @@ func BuildData(cfg *config.Config) (*Data, error) {
// otherwise show a generic error message
return nil, fmt.Errorf("invalid types were encountered while traversing the go source code, this probably means the invalid code generated isnt correct. add try adding -v to debug")
}
aSources := []AugmentedSource{}
for _, s := range cfg.Sources {
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err)
}
outputDir := cfg.Exec.Dir()
sourcePath := filepath.Join(wd, s.Name)
relative, err := filepath.Rel(outputDir, sourcePath)
if err != nil {
return nil, fmt.Errorf("failed to compute path of %s relative to %s: %w", sourcePath, outputDir, err)
}
relative = filepath.ToSlash(relative)
embeddable := true
if strings.HasPrefix(relative, "..") || s.BuiltIn {
embeddable = false
}
aSources = append(aSources, AugmentedSource{
RelativePath: relative,
Embeddable: embeddable,
BuiltIn: s.BuiltIn,
Source: s.Input,
})
}
s.AugmentedSources = aSources
return &s, nil
}

View file

@ -70,7 +70,7 @@ func (ec *executionContext) _mutationMiddleware(ctx context.Context, obj *ast.Op
{{ end }}
{{ if .Directives.LocationDirectives "SUBSCRIPTION" }}
func (ec *executionContext) _subscriptionMiddleware(ctx context.Context, obj *ast.OperationDefinition, next func(ctx context.Context) (interface{}, error)) func() graphql.Marshaler {
func (ec *executionContext) _subscriptionMiddleware(ctx context.Context, obj *ast.OperationDefinition, next func(ctx context.Context) (interface{}, error)) func(ctx context.Context) graphql.Marshaler {
for _, d := range obj.Directives {
switch d.Name {
{{- range $directive := .Directives.LocationDirectives "SUBSCRIPTION" }}
@ -80,7 +80,7 @@ func (ec *executionContext) _subscriptionMiddleware(ctx context.Context, obj *as
args, err := ec.{{ $directive.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return func() graphql.Marshaler {
return func(ctx context.Context) graphql.Marshaler {
return graphql.Null
}
}
@ -98,15 +98,15 @@ func (ec *executionContext) _subscriptionMiddleware(ctx context.Context, obj *as
tmp, err := next(ctx)
if err != nil {
ec.Error(ctx, err)
return func() graphql.Marshaler {
return func(ctx context.Context) graphql.Marshaler {
return graphql.Null
}
}
if data, ok := tmp.(func() graphql.Marshaler); ok {
if data, ok := tmp.(func(ctx context.Context) graphql.Marshaler); ok {
return data
}
ec.Errorf(ctx, `unexpected type %T from directive, should be graphql.Marshaler`, tmp)
return func() graphql.Marshaler {
return func(ctx context.Context) graphql.Marshaler {
return graphql.Null
}
}

View file

@ -12,6 +12,8 @@ import (
"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates"
"github.com/vektah/gqlparser/v2/ast"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Field struct {
@ -71,7 +73,7 @@ func (b *builder) buildField(obj *Object, field *ast.FieldDefinition) (*Field, e
log.Println(err.Error())
}
if f.IsResolver && !f.TypeReference.IsPtr() && f.TypeReference.IsStruct() {
if f.IsResolver && b.Config.ResolversAlwaysReturnPointers && !f.TypeReference.IsPtr() && f.TypeReference.IsStruct() {
f.TypeReference = b.Binder.PointerTo(f.TypeReference)
}
@ -179,8 +181,8 @@ func (b *builder) bindField(obj *Object, f *Field) (errret error) {
params = types.NewTuple(vars...)
}
// Try to match target function's arguments with GraphQL field arguments
newArgs, err := b.bindArgs(f, params)
// Try to match target function's arguments with GraphQL field arguments.
newArgs, err := b.bindArgs(f, sig, params)
if err != nil {
return fmt.Errorf("%s:%d: %w", pos.Filename, pos.Line, err)
}
@ -469,10 +471,11 @@ func (f *Field) GoNameUnexported() string {
}
func (f *Field) ShortInvocation() string {
caser := cases.Title(language.English, cases.NoLower)
if f.Object.Kind == ast.InputObject {
return fmt.Sprintf("%s().%s(ctx, &it, data)", strings.Title(f.Object.Definition.Name), f.GoFieldName)
return fmt.Sprintf("%s().%s(ctx, &it, data)", caser.String(f.Object.Definition.Name), f.GoFieldName)
}
return fmt.Sprintf("%s().%s(%s)", strings.Title(f.Object.Definition.Name), f.GoFieldName, f.CallArgs())
return fmt.Sprintf("%s().%s(%s)", caser.String(f.Object.Definition.Name), f.GoFieldName, f.CallArgs())
}
func (f *Field) ArgsFunc() string {
@ -483,6 +486,14 @@ func (f *Field) ArgsFunc() string {
return "field_" + f.Object.Definition.Name + "_" + f.Name + "_args"
}
func (f *Field) FieldContextFunc() string {
return "fieldContext_" + f.Object.Definition.Name + "_" + f.Name
}
func (f *Field) ChildFieldContextFunc(name string) string {
return "fieldContext_" + f.TypeReference.Definition.Name + "_" + name
}
func (f *Field) ResolverType() string {
if !f.IsResolver {
return ""
@ -549,7 +560,20 @@ func (f *Field) CallArgs() string {
}
for _, arg := range f.Args {
args = append(args, "args["+strconv.Quote(arg.Name)+"].("+templates.CurrentImports.LookupType(arg.TypeReference.GO)+")")
tmp := "fc.Args[" + strconv.Quote(arg.Name) + "].(" + templates.CurrentImports.LookupType(arg.TypeReference.GO) + ")"
if iface, ok := arg.TypeReference.GO.(*types.Interface); ok && iface.Empty() {
tmp = fmt.Sprintf(`
func () interface{} {
if fc.Args["%s"] == nil {
return nil
}
return fc.Args["%s"].(interface{})
}()`, arg.Name, arg.Name,
)
}
args = append(args, tmp)
}
return strings.Join(args, ", ")

View file

@ -1,34 +1,21 @@
{{- range $object := .Objects }}{{- range $field := $object.Fields }}
func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj {{$object.Reference | ref}}{{end}}) (ret {{ if $object.Stream }}func(){{ end }}graphql.Marshaler) {
func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj {{$object.Reference | ref}}{{end}}) (ret {{ if $object.Stream }}func(ctx context.Context){{ end }}graphql.Marshaler) {
{{- $null := "graphql.Null" }}
{{- if $object.Stream }}
{{- $null = "nil" }}
{{- end }}
fc, err := ec.{{ $field.FieldContextFunc }}(ctx, field)
if err != nil {
return {{ $null }}
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func () {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = {{ $null }}
}
}()
fc := &graphql.FieldContext{
Object: {{$object.Name|quote}},
Field: field,
Args: nil,
IsMethod: {{or $field.IsMethod $field.IsResolver}},
IsResolver: {{ $field.IsResolver }},
}
ctx = graphql.WithFieldContext(ctx, fc)
{{- if $field.Args }}
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.{{ $field.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return {{ $null }}
}
fc.Args = args
{{- end }}
{{- if $.AllDirectives.LocationDirectives "FIELD" }}
resTmp := ec._fieldMiddleware(ctx, {{if $object.Root}}nil{{else}}obj{{end}}, func(rctx context.Context) (interface{}, error) {
{{ template "field" $field }}
@ -51,18 +38,22 @@ func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Contex
return {{ $null }}
}
{{- if $object.Stream }}
return func() graphql.Marshaler {
res, ok := <-resTmp.(<-chan {{$field.TypeReference.GO | ref}})
if !ok {
return func(ctx context.Context) graphql.Marshaler {
select {
case res, ok := <-resTmp.(<-chan {{$field.TypeReference.GO | ref}}):
if !ok {
return nil
}
return graphql.WriterFunc(func(w io.Writer) {
w.Write([]byte{'{'})
graphql.MarshalString(field.Alias).MarshalGQL(w)
w.Write([]byte{':'})
ec.{{ $field.TypeReference.MarshalFunc }}(ctx, field.Selections, res).MarshalGQL(w)
w.Write([]byte{'}'})
})
case <-ctx.Done():
return nil
}
return graphql.WriterFunc(func(w io.Writer) {
w.Write([]byte{'{'})
graphql.MarshalString(field.Alias).MarshalGQL(w)
w.Write([]byte{':'})
ec.{{ $field.TypeReference.MarshalFunc }}(ctx, field.Selections, res).MarshalGQL(w)
w.Write([]byte{'}'})
})
}
{{- else }}
res := resTmp.({{$field.TypeReference.GO | ref}})
@ -71,6 +62,44 @@ func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Contex
{{- end }}
}
func (ec *executionContext) {{ $field.FieldContextFunc }}(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: {{quote $field.Object.Name}},
Field: field,
IsMethod: {{or $field.IsMethod $field.IsResolver}},
IsResolver: {{ $field.IsResolver }},
Child: func (ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
{{- if not $field.TypeReference.Definition.Fields }}
return nil, errors.New("field of type {{ $field.TypeReference.Definition.Name }} does not have child fields")
{{- else if ne $field.TypeReference.Definition.Kind "OBJECT" }}
return nil, errors.New("FieldContext.Child cannot be called on type {{ $field.TypeReference.Definition.Kind }}")
{{- else }}
switch field.Name {
{{- range $f := $field.TypeReference.Definition.Fields }}
case "{{ $f.Name }}":
return ec.{{ $field.ChildFieldContextFunc $f.Name }}(ctx, field)
{{- end }}
}
return nil, fmt.Errorf("no field named %q was found under type {{ $field.TypeReference.Definition.Name }}", field.Name)
{{- end }}
},
}
{{- if $field.Args }}
defer func () {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.{{ $field.ArgsFunc }}(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
{{- end }}
return fc, nil
}
{{- end }}{{- end}}
{{ define "field" }}

View file

@ -1,9 +1,10 @@
package codegen
import (
"embed"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
@ -13,6 +14,9 @@ import (
"github.com/vektah/gqlparser/v2/ast"
)
//go:embed *.gotpl
var codegenTemplates embed.FS
func GenerateCode(data *Data) error {
if !data.Config.Exec.IsDefined() {
return fmt.Errorf("missing exec config")
@ -36,6 +40,7 @@ func generateSingleFile(data *Data) error {
RegionTags: true,
GeneratedHeader: true,
Packages: data.Config.Packages,
TemplateFS: codegenTemplates,
})
}
@ -82,6 +87,7 @@ func generatePerSchema(data *Data) error {
RegionTags: true,
GeneratedHeader: true,
Packages: data.Config.Packages,
TemplateFS: codegenTemplates,
})
if err != nil {
return err
@ -131,7 +137,7 @@ func generateRootFile(data *Data) error {
_, thisFile, _, _ := runtime.Caller(0)
rootDir := filepath.Dir(thisFile)
templatePath := filepath.Join(rootDir, "root_.gotpl")
templateBytes, err := ioutil.ReadFile(templatePath)
templateBytes, err := os.ReadFile(templatePath)
if err != nil {
return err
}
@ -145,6 +151,7 @@ func generateRootFile(data *Data) error {
RegionTags: false,
GeneratedHeader: true,
Packages: data.Config.Packages,
TemplateFS: codegenTemplates,
})
}

View file

@ -7,6 +7,7 @@
{{ reserveImport "sync/atomic" }}
{{ reserveImport "errors" }}
{{ reserveImport "bytes" }}
{{ reserveImport "embed" }}
{{ reserveImport "github.com/vektah/gqlparser/v2" "gqlparser" }}
{{ reserveImport "github.com/vektah/gqlparser/v2/ast" }}
@ -135,6 +136,13 @@
func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
rc := graphql.GetOperationContext(ctx)
ec := executionContext{rc, e}
inputUnmarshalMap := graphql.BuildUnmarshalerMap(
{{- range $input := .Inputs -}}
{{ if not $input.HasUnmarshal }}
ec.unmarshalInput{{ $input.Name }},
{{- end }}
{{- end }}
)
first := true
switch rc.Operation.Operation {
@ -142,6 +150,7 @@
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
{{ if .Directives.LocationDirectives "QUERY" -}}
data := ec._queryMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.QueryRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
@ -162,6 +171,7 @@
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
{{ if .Directives.LocationDirectives "MUTATION" -}}
data := ec._mutationMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.MutationRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
@ -190,7 +200,7 @@
var buf bytes.Buffer
return func(ctx context.Context) *graphql.Response {
buf.Reset()
data := next()
data := next(ctx)
if data == nil {
return nil
@ -226,9 +236,22 @@
return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil
}
{{if .HasEmbeddableSources }}
//go:embed{{- range $source := .AugmentedSources }}{{if $source.Embeddable}} {{$source.RelativePath|quote}}{{end}}{{- end }}
var sourcesFS embed.FS
func sourceData(filename string) string {
data, err := sourcesFS.ReadFile(filename)
if err != nil {
panic(fmt.Sprintf("codegen problem: %s not available", filename))
}
return string(data)
}
{{- end }}
var sources = []*ast.Source{
{{- range $source := .Config.Sources }}
{Name: {{$source.Name|quote}}, Input: {{$source.Input|rawQuote}}, BuiltIn: {{$source.BuiltIn}}},
{{- range $source := .AugmentedSources }}
{Name: {{$source.RelativePath|quote}}, Input: {{if (not $source.Embeddable)}}{{$source.Source|rawQuote}}{{else}}sourceData({{$source.RelativePath|quote}}){{end}}, BuiltIn: {{$source.BuiltIn}}},
{{- end }}
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)

View file

@ -14,7 +14,12 @@
{{- end}}
{{- end }}
for k, v := range asMap {
fieldsInOrder := [...]string{ {{ range .Fields }}{{ quote .Name }},{{ end }} }
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
{{- range $field := .Fields }}
case {{$field.Name|quote}}:

View file

@ -9,6 +9,8 @@ import (
"github.com/99designs/gqlgen/codegen/config"
"github.com/vektah/gqlparser/v2/ast"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type GoFieldType int
@ -38,7 +40,7 @@ func (b *builder) buildObject(typ *ast.Definition) (*Object, error) {
if err != nil {
return nil, fmt.Errorf("%s: %w", typ.Name, err)
}
caser := cases.Title(language.English, cases.NoLower)
obj := &Object{
Definition: typ,
Root: b.Schema.Query == typ || b.Schema.Mutation == typ || b.Schema.Subscription == typ,
@ -46,7 +48,7 @@ func (b *builder) buildObject(typ *ast.Definition) (*Object, error) {
Stream: typ == b.Schema.Subscription,
Directives: dirs,
ResolverInterface: types.NewNamed(
types.NewTypeName(0, b.Config.Exec.Pkg(), strings.Title(typ.Name)+"Resolver", nil),
types.NewTypeName(0, b.Config.Exec.Pkg(), caser.String(typ.Name)+"Resolver", nil),
nil,
nil,
),

View file

@ -3,7 +3,7 @@
var {{ $object.Name|lcFirst}}Implementors = {{$object.Implementors}}
{{- if .Stream }}
func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {
func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet) func(ctx context.Context) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, {{$object.Name|lcFirst}}Implementors)
ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
Object: {{$object.Name|quote}},
@ -80,13 +80,12 @@ func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.Selec
{{end}}
})
{{- else }}
innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
return ec._{{$object.Name}}_{{$field.Name}}(ctx, field{{if not $object.Root}}, obj{{end}})
}
{{if $object.Root}}
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, innerFunc)
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._{{$object.Name}}_{{$field.Name}}(ctx, field)
})
{{else}}
out.Values[i] = innerFunc(ctx)
out.Values[i] = ec._{{$object.Name}}_{{$field.Name}}(ctx, field, obj)
{{end}}
{{- if $field.TypeReference.GQL.NonNull }}

View file

@ -7,6 +7,7 @@
{{ reserveImport "sync/atomic" }}
{{ reserveImport "errors" }}
{{ reserveImport "bytes" }}
{{ reserveImport "embed" }}
{{ reserveImport "github.com/vektah/gqlparser/v2" "gqlparser" }}
{{ reserveImport "github.com/vektah/gqlparser/v2/ast" }}
@ -34,6 +35,11 @@ type ResolverRoot interface {
{{ucFirst $object.Name}}() {{ucFirst $object.Name}}Resolver
{{ end }}
{{- end }}
{{- range $object := .Inputs -}}
{{ if $object.HasResolvers -}}
{{ucFirst $object.Name}}() {{ucFirst $object.Name}}Resolver
{{ end }}
{{- end }}
}
type DirectiveRoot struct {
@ -102,6 +108,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
rc := graphql.GetOperationContext(ctx)
ec := executionContext{rc, e}
inputUnmarshalMap := graphql.BuildUnmarshalerMap(
{{- range $input := .Inputs -}}
{{ if not $input.HasUnmarshal }}
ec.unmarshalInput{{ $input.Name }},
{{- end }}
{{- end }}
)
first := true
switch rc.Operation.Operation {
@ -109,6 +122,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
{{ if .Directives.LocationDirectives "QUERY" -}}
data := ec._queryMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.QueryRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
@ -129,6 +143,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
{{ if .Directives.LocationDirectives "MUTATION" -}}
data := ec._mutationMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.MutationRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
@ -157,7 +172,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
var buf bytes.Buffer
return func(ctx context.Context) *graphql.Response {
buf.Reset()
data := next()
data := next(ctx)
if data == nil {
return nil
@ -193,9 +208,23 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil
}
{{if .HasEmbeddableSources }}
//go:embed{{- range $source := .AugmentedSources }}{{if $source.Embeddable}} {{$source.RelativePath|quote}}{{end}}{{- end }}
var sourcesFS embed.FS
func sourceData(filename string) string {
data, err := sourcesFS.ReadFile(filename)
if err != nil {
panic(fmt.Sprintf("codegen problem: %s not available", filename))
}
return string(data)
}
{{- end}}
var sources = []*ast.Source{
{{- range $source := .Config.Sources }}
{Name: {{$source.Name|quote}}, Input: {{$source.Input|rawQuote}}, BuiltIn: {{$source.BuiltIn}}},
{{- range $source := .AugmentedSources }}
{Name: {{$source.RelativePath|quote}}, Input: {{if (not $source.Embeddable)}}{{$source.Source|rawQuote}}{{else}}sourceData({{$source.RelativePath|quote}}){{end}}, BuiltIn: {{$source.BuiltIn}}},
{{- end }}
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)

View file

@ -4,14 +4,16 @@ import (
"bytes"
"fmt"
"go/types"
"io/ioutil"
"io/fs"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"text/template"
"unicode"
@ -36,6 +38,11 @@ type Options struct {
// the plugin processor will look for .gotpl files
// in the same directory of where you wrote the plugin.
Template string
// Use the go:embed API to collect all the template files you want to pass into Render
// this is an alternative to passing the Template option
TemplateFS fs.FS
// Filename is the name of the file that will be
// written to the system disk once the template is rendered.
Filename string
@ -53,6 +60,12 @@ type Options struct {
Packages *code.Packages
}
var (
modelNamesMu sync.Mutex
modelNames = make(map[string]string, 0)
goNameRe = regexp.MustCompile("[^a-zA-Z0-9_]")
)
// Render renders a gql plugin template from the given Options. Render is an
// abstraction of the text/template package that makes it easier to write gqlgen
// plugins. If Options.Template is empty, the Render function will look for `.gotpl`
@ -63,55 +76,27 @@ func Render(cfg Options) error {
}
CurrentImports = &Imports{packages: cfg.Packages, destDir: filepath.Dir(cfg.Filename)}
// load path relative to calling source file
_, callerFile, _, _ := runtime.Caller(1)
rootDir := filepath.Dir(callerFile)
funcs := Funcs()
for n, f := range cfg.Funcs {
funcs[n] = f
}
t := template.New("").Funcs(funcs)
t, err := parseTemplates(cfg, t)
if err != nil {
return err
}
var roots []string
if cfg.Template != "" {
var err error
t, err = t.New("template.gotpl").Parse(cfg.Template)
if err != nil {
return fmt.Errorf("error with provided template: %w", err)
roots := make([]string, 0, len(t.Templates()))
for _, template := range t.Templates() {
// templates that end with _.gotpl are special files we don't want to include
if strings.HasSuffix(template.Name(), "_.gotpl") ||
// filter out templates added with {{ template xxx }} syntax inside the template file
!strings.HasSuffix(template.Name(), ".gotpl") {
continue
}
roots = append(roots, "template.gotpl")
} else {
// load all the templates in the directory
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
name := filepath.ToSlash(strings.TrimPrefix(path, rootDir+string(os.PathSeparator)))
if !strings.HasSuffix(info.Name(), ".gotpl") {
return nil
}
// omit any templates with "_" at the end of their name, which are meant for specific contexts only
if strings.HasSuffix(info.Name(), "_.gotpl") {
return nil
}
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
t, err = t.New(name).Parse(string(b))
if err != nil {
return fmt.Errorf("%s: %w", cfg.Filename, err)
}
roots = append(roots, name)
return nil
})
if err != nil {
return fmt.Errorf("locating templates: %w", err)
}
roots = append(roots, template.Name())
}
// then execute all the important looking ones in order, adding them to the same file
@ -125,6 +110,7 @@ func Render(cfg Options) error {
}
return roots[i] < roots[j]
})
var buf bytes.Buffer
for _, root := range roots {
if cfg.RegionTags {
@ -156,7 +142,7 @@ func Render(cfg Options) error {
result.WriteString("import (\n")
result.WriteString(CurrentImports.String())
result.WriteString(")\n")
_, err := buf.WriteTo(&result)
_, err = buf.WriteTo(&result)
if err != nil {
return err
}
@ -171,6 +157,34 @@ func Render(cfg Options) error {
return nil
}
func parseTemplates(cfg Options, t *template.Template) (*template.Template, error) {
if cfg.Template != "" {
var err error
t, err = t.New("template.gotpl").Parse(cfg.Template)
if err != nil {
return nil, fmt.Errorf("error with provided template: %w", err)
}
return t, nil
}
var fileSystem fs.FS
if cfg.TemplateFS != nil {
fileSystem = cfg.TemplateFS
} else {
// load path relative to calling source file
_, callerFile, _, _ := runtime.Caller(1)
rootDir := filepath.Dir(callerFile)
fileSystem = os.DirFS(rootDir)
}
t, err := t.ParseFS(fileSystem, "*.gotpl")
if err != nil {
return nil, fmt.Errorf("locating templates: %w", err)
}
return t, nil
}
func center(width int, pad string, s string) string {
if len(s)+2 > width {
return s
@ -182,20 +196,22 @@ func center(width int, pad string, s string) string {
func Funcs() template.FuncMap {
return template.FuncMap{
"ucFirst": UcFirst,
"lcFirst": LcFirst,
"quote": strconv.Quote,
"rawQuote": rawQuote,
"dump": Dump,
"ref": ref,
"ts": TypeIdentifier,
"call": Call,
"prefixLines": prefixLines,
"notNil": notNil,
"reserveImport": CurrentImports.Reserve,
"lookupImport": CurrentImports.Lookup,
"go": ToGo,
"goPrivate": ToGoPrivate,
"ucFirst": UcFirst,
"lcFirst": LcFirst,
"quote": strconv.Quote,
"rawQuote": rawQuote,
"dump": Dump,
"ref": ref,
"ts": TypeIdentifier,
"call": Call,
"prefixLines": prefixLines,
"notNil": notNil,
"reserveImport": CurrentImports.Reserve,
"lookupImport": CurrentImports.Lookup,
"go": ToGo,
"goPrivate": ToGoPrivate,
"goModelName": ToGoModelName,
"goPrivateModelName": ToGoPrivateModelName,
"add": func(a, b int) int {
return a + b
},
@ -285,25 +301,154 @@ func Call(p *types.Func) string {
return pkg + p.Name()
}
func resetModelNames() {
modelNamesMu.Lock()
defer modelNamesMu.Unlock()
modelNames = make(map[string]string, 0)
}
func buildGoModelNameKey(parts []string) string {
const sep = ":"
return strings.Join(parts, sep)
}
func goModelName(primaryToGoFunc func(string) string, parts []string) string {
modelNamesMu.Lock()
defer modelNamesMu.Unlock()
var (
goNameKey string
partLen int
nameExists = func(n string) bool {
for _, v := range modelNames {
if n == v {
return true
}
}
return false
}
applyToGoFunc = func(parts []string) string {
var out string
switch len(parts) {
case 0:
return ""
case 1:
return primaryToGoFunc(parts[0])
default:
out = primaryToGoFunc(parts[0])
}
for _, p := range parts[1:] {
out = fmt.Sprintf("%s%s", out, ToGo(p))
}
return out
}
applyValidGoName = func(parts []string) string {
var out string
for _, p := range parts {
out = fmt.Sprintf("%s%s", out, replaceInvalidCharacters(p))
}
return out
}
)
// build key for this entity
goNameKey = buildGoModelNameKey(parts)
// determine if we've seen this entity before, and reuse if so
if goName, ok := modelNames[goNameKey]; ok {
return goName
}
// attempt first pass
if goName := applyToGoFunc(parts); !nameExists(goName) {
modelNames[goNameKey] = goName
return goName
}
// determine number of parts
partLen = len(parts)
// if there is only 1 part, append incrementing number until no conflict
if partLen == 1 {
base := applyToGoFunc(parts)
for i := 0; ; i++ {
tmp := fmt.Sprintf("%s%d", base, i)
if !nameExists(tmp) {
modelNames[goNameKey] = tmp
return tmp
}
}
}
// best effort "pretty" name
for i := partLen - 1; i >= 1; i-- {
tmp := fmt.Sprintf("%s%s", applyToGoFunc(parts[0:i]), applyValidGoName(parts[i:]))
if !nameExists(tmp) {
modelNames[goNameKey] = tmp
return tmp
}
}
// finally, fallback to just adding an incrementing number
base := applyToGoFunc(parts)
for i := 0; ; i++ {
tmp := fmt.Sprintf("%s%d", base, i)
if !nameExists(tmp) {
modelNames[goNameKey] = tmp
return tmp
}
}
}
func ToGoModelName(parts ...string) string {
return goModelName(ToGo, parts)
}
func ToGoPrivateModelName(parts ...string) string {
return goModelName(ToGoPrivate, parts)
}
func replaceInvalidCharacters(in string) string {
return goNameRe.ReplaceAllLiteralString(in, "_")
}
func wordWalkerFunc(private bool, nameRunes *[]rune) func(*wordInfo) {
return func(info *wordInfo) {
word := info.Word
switch {
case private && info.WordOffset == 0:
if strings.ToUpper(word) == word || strings.ToLower(word) == word {
// ID → id, CAMEL → camel
word = strings.ToLower(info.Word)
} else {
// ITicket → iTicket
word = LcFirst(info.Word)
}
case info.MatchCommonInitial:
word = strings.ToUpper(word)
case !info.HasCommonInitial && (strings.ToUpper(word) == word || strings.ToLower(word) == word):
// FOO or foo → Foo
// FOo → FOo
word = UcFirst(strings.ToLower(word))
}
*nameRunes = append(*nameRunes, []rune(word)...)
}
}
func ToGo(name string) string {
if name == "_" {
return "_"
}
runes := make([]rune, 0, len(name))
wordWalker(name, func(info *wordInfo) {
word := info.Word
if info.MatchCommonInitial {
word = strings.ToUpper(word)
} else if !info.HasCommonInitial {
if strings.ToUpper(word) == word || strings.ToLower(word) == word {
// FOO or foo → Foo
// FOo → FOo
word = UcFirst(strings.ToLower(word))
}
}
runes = append(runes, []rune(word)...)
})
wordWalker(name, wordWalkerFunc(false, &runes))
return string(runes)
}
@ -314,31 +459,13 @@ func ToGoPrivate(name string) string {
}
runes := make([]rune, 0, len(name))
first := true
wordWalker(name, func(info *wordInfo) {
word := info.Word
switch {
case first:
if strings.ToUpper(word) == word || strings.ToLower(word) == word {
// ID → id, CAMEL → camel
word = strings.ToLower(info.Word)
} else {
// ITicket → iTicket
word = LcFirst(info.Word)
}
first = false
case info.MatchCommonInitial:
word = strings.ToUpper(word)
case !info.HasCommonInitial:
word = UcFirst(strings.ToLower(word))
}
runes = append(runes, []rune(word)...)
})
wordWalker(name, wordWalkerFunc(true, &runes))
return sanitizeKeywords(string(runes))
}
type wordInfo struct {
WordOffset int
Word string
MatchCommonInitial bool
HasCommonInitial bool
@ -348,7 +475,7 @@ type wordInfo struct {
// https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679
func wordWalker(str string, f func(*wordInfo)) {
runes := []rune(strings.TrimFunc(str, isDelimiter))
w, i := 0, 0 // index of start of word, scan
w, i, wo := 0, 0, 0 // index of start of word, scan, word offset
hasCommonInitial := false
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
@ -396,12 +523,14 @@ func wordWalker(str string, f func(*wordInfo)) {
}
f(&wordInfo{
WordOffset: wo,
Word: word,
MatchCommonInitial: matchCommonInitial,
HasCommonInitial: hasCommonInitial,
})
hasCommonInitial = false
w = i
wo++
}
}
@ -576,7 +705,7 @@ func resolveName(name string, skip int) string {
func render(filename string, tpldata interface{}) (*bytes.Buffer, error) {
t := template.New("").Funcs(Funcs())
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
@ -602,7 +731,7 @@ func write(filename string, b []byte, packages *code.Packages) error {
formatted = b
}
err = ioutil.WriteFile(filename, formatted, 0o644)
err = os.WriteFile(filename, formatted, 0o644)
if err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
}

View file

@ -0,0 +1 @@
this is my test package

View file

@ -0,0 +1 @@
this will not be included

View file

@ -151,7 +151,7 @@
if v == nil {
{{- if $type.GQL.NonNull }}
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
{{- end }}
return graphql.Null
@ -174,7 +174,7 @@
{{- if $type.GQL.NonNull }}
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
}
{{- end }}

View file

@ -30,6 +30,25 @@ type FieldContext struct {
IsMethod bool
// IsResolver indicates if the field has a user-specified resolver
IsResolver bool
// Child allows getting a child FieldContext by its field collection description.
// Note that, the returned child FieldContext represents the context as it was
// before the execution of the field resolver. For example:
//
// srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (interface{}, error) {
// fc := graphql.GetFieldContext(ctx)
// op := graphql.GetOperationContext(ctx)
// collected := graphql.CollectFields(opCtx, fc.Field.Selections, []string{"User"})
//
// child, err := fc.Child(ctx, collected[0])
// if err != nil {
// return nil, err
// }
// fmt.Println("child context %q with args: %v", child.Field.Name, child.Args)
//
// return next(ctx)
// })
//
Child func(context.Context, CollectedField) (*FieldContext, error)
}
type FieldStats struct {

View file

@ -3,6 +3,7 @@ package graphql
import (
"context"
"errors"
"net/http"
"github.com/vektah/gqlparser/v2/ast"
)
@ -15,6 +16,7 @@ type OperationContext struct {
Variables map[string]interface{}
OperationName string
Doc *ast.QueryDocument
Headers http.Header
Operation *ast.OperationDefinition
DisableIntrospection bool

View file

@ -23,18 +23,27 @@ var codeType = map[string]ErrorKind{
ParseFailed: KindProtocol,
}
// RegisterErrorType should be called by extensions that want to customize the http status codes for errors they return
// RegisterErrorType should be called by extensions that want to customize the http status codes for
// errors they return
func RegisterErrorType(code string, kind ErrorKind) {
codeType[code] = kind
}
// Set the error code on a given graphql error extension
func Set(err *gqlerror.Error, value string) {
if err.Extensions == nil {
err.Extensions = map[string]interface{}{}
func Set(err error, value string) {
if err == nil {
return
}
gqlErr, ok := err.(*gqlerror.Error)
if !ok {
return
}
err.Extensions["code"] = value
if gqlErr.Extensions == nil {
gqlErr.Extensions = map[string]interface{}{}
}
gqlErr.Extensions["code"] = value
}
// get the kind of the first non User error, defaults to User if no errors have a custom extension

View file

@ -26,7 +26,8 @@ func ErrorOnPath(ctx context.Context, err error) error {
if gqlErr.Path == nil {
gqlErr.Path = GetPath(ctx)
}
return gqlErr
// Return the original error to avoid losing any attached annotation
return err
}
return gqlerror.WrapPath(GetPath(ctx), err)
}

View file

@ -118,6 +118,11 @@ func getOrCreateAndAppendField(c *[]CollectedField, name string, alias string, o
return &(*c)[i]
}
}
for _, ifc := range cf.ObjectDefinition.Interfaces {
if ifc == objectDefinition.Name {
return &(*c)[i]
}
}
}
}

View file

@ -37,7 +37,10 @@ func New(es graphql.ExecutableSchema) *Executor {
return e
}
func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.RawParams) (*graphql.OperationContext, gqlerror.List) {
func (e *Executor) CreateOperationContext(
ctx context.Context,
params *graphql.RawParams,
) (*graphql.OperationContext, gqlerror.List) {
rc := &graphql.OperationContext{
DisableIntrospection: true,
RecoverFunc: e.recoverFunc,
@ -58,6 +61,7 @@ func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.R
rc.RawQuery = params.Query
rc.OperationName = params.OperationName
rc.Headers = params.Headers
var listErr gqlerror.List
rc.Doc, listErr = e.parseQuery(ctx, &rc.Stats, params.Query)
@ -67,15 +71,21 @@ func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.R
rc.Operation = rc.Doc.Operations.ForName(params.OperationName)
if rc.Operation == nil {
return rc, gqlerror.List{gqlerror.Errorf("operation %s not found", params.OperationName)}
}
var err *gqlerror.Error
rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables)
if err != nil {
err := gqlerror.Errorf("operation %s not found", params.OperationName)
errcode.Set(err, errcode.ValidationFailed)
return rc, gqlerror.List{err}
}
var err error
rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables)
if err != nil {
gqlErr, ok := err.(*gqlerror.Error)
if ok {
errcode.Set(gqlErr, errcode.ValidationFailed)
return rc, gqlerror.List{gqlErr}
}
}
rc.Stats.Validation.End = graphql.Now()
for _, p := range e.ext.operationContextMutators {
@ -87,7 +97,10 @@ func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.R
return rc, nil
}
func (e *Executor) DispatchOperation(ctx context.Context, rc *graphql.OperationContext) (graphql.ResponseHandler, context.Context) {
func (e *Executor) DispatchOperation(
ctx context.Context,
rc *graphql.OperationContext,
) (graphql.ResponseHandler, context.Context) {
ctx = graphql.WithOperationContext(ctx, rc)
var innerCtx context.Context
@ -130,7 +143,7 @@ func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graph
resp := e.ext.responseMiddleware(ctx, func(ctx context.Context) *graphql.Response {
resp := &graphql.Response{
Errors: list,
Errors: graphql.GetErrors(ctx),
}
resp.Extensions = graphql.GetExtensions(ctx)
return resp
@ -139,7 +152,7 @@ func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graph
return resp
}
func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) *gqlerror.Error {
func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) error {
return e.errorPresenter(ctx, e.recoverFunc(ctx, err))
}
@ -157,9 +170,14 @@ func (e *Executor) SetRecoverFunc(f graphql.RecoverFunc) {
// parseQuery decodes the incoming query and validates it, pulling from cache if present.
//
// NOTE: This should NOT look at variables, they will change per request. It should only parse and validate
// NOTE: This should NOT look at variables, they will change per request. It should only parse and
// validate
// the raw query string.
func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query string) (*ast.QueryDocument, gqlerror.List) {
func (e *Executor) parseQuery(
ctx context.Context,
stats *graphql.Stats,
query string,
) (*ast.QueryDocument, gqlerror.List) {
stats.Parsing.Start = graphql.Now()
if doc, ok := e.queryCache.Get(ctx, query); ok {
@ -172,12 +190,23 @@ func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query s
doc, err := parser.ParseQuery(&ast.Source{Input: query})
if err != nil {
errcode.Set(err, errcode.ParseFailed)
return nil, gqlerror.List{err}
gqlErr, ok := err.(*gqlerror.Error)
if ok {
errcode.Set(gqlErr, errcode.ParseFailed)
return nil, gqlerror.List{gqlErr}
}
}
stats.Parsing.End = graphql.Now()
stats.Validation.Start = graphql.Now()
if len(doc.Operations) == 0 {
err = gqlerror.Errorf("no operation provided")
gqlErr, _ := err.(*gqlerror.Error)
errcode.Set(err, errcode.ValidationFailed)
return nil, gqlerror.List{gqlErr}
}
listErr := validator.Validate(e.es.Schema(), doc)
if len(listErr) != 0 {
for _, e := range listErr {

View file

@ -27,6 +27,7 @@ type (
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
Extensions map[string]interface{} `json:"extensions"`
Headers http.Header `json:"headers"`
ReadTime TraceTiming `json:"-"`
}
@ -41,14 +42,14 @@ type (
// Its important to understand the lifecycle of a graphql request and the terminology we use in gqlgen
// before working with these
//
// +--- REQUEST POST /graphql --------------------------------------------+
// | +- OPERATION query OpName { viewer { name } } -----------------------+ |
// | | RESPONSE { "data": { "viewer": { "name": "bob" } } } | |
// | +- OPERATION subscription OpName2 { chat { message } } --------------+ |
// | | RESPONSE { "data": { "chat": { "message": "hello" } } } | |
// | | RESPONSE { "data": { "chat": { "message": "byee" } } } | |
// | +--------------------------------------------------------------------+ |
// +------------------------------------------------------------------------+
// +--- REQUEST POST /graphql --------------------------------------------+
// | +- OPERATION query OpName { viewer { name } } -----------------------+ |
// | | RESPONSE { "data": { "viewer": { "name": "bob" } } } | |
// | +- OPERATION subscription OpName2 { chat { message } } --------------+ |
// | | RESPONSE { "data": { "chat": { "message": "hello" } } } | |
// | | RESPONSE { "data": { "chat": { "message": "byee" } } } | |
// | +--------------------------------------------------------------------+ |
// +------------------------------------------------------------------------+
HandlerExtension interface {
// ExtensionName should be a CamelCase string version of the extension which may be shown in stats and logging.
ExtensionName() string

View file

@ -102,7 +102,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
err := s.exec.PresentRecoveredError(r.Context(), err)
resp := &graphql.Response{Errors: []*gqlerror.Error{err}}
gqlErr, _ := err.(*gqlerror.Error)
resp := &graphql.Response{Errors: []*gqlerror.Error{gqlErr}}
b, _ := json.Marshal(resp)
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(b)

View file

@ -3,11 +3,9 @@ package transport
import (
"encoding/json"
"io"
"io/ioutil"
"mime"
"net/http"
"os"
"strings"
"github.com/99designs/gqlgen/graphql"
)
@ -64,133 +62,145 @@ func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.G
return
}
r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize())
if err = r.ParseMultipartForm(f.maxMemory()); err != nil {
defer r.Body.Close()
mr, err := r.MultipartReader()
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if strings.Contains(err.Error(), "request body too large") {
writeJsonError(w, "failed to parse multipart form, request body too large")
return
}
writeJsonError(w, "failed to parse multipart form")
return
}
defer r.Body.Close()
part, err := mr.NextPart()
if err != nil || part.FormName() != "operations" {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "first part must be operations")
return
}
var params graphql.RawParams
if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &params); err != nil {
if err = jsonDecode(part, &params); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "operations form field could not be decoded")
return
}
part, err = mr.NextPart()
if err != nil || part.FormName() != "map" {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "second part must be map")
return
}
uploadsMap := map[string][]string{}
if err = json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil {
if err = json.NewDecoder(part).Decode(&uploadsMap); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "map form field could not be decoded")
return
}
var upload graphql.Upload
for key, paths := range uploadsMap {
for {
part, err = mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to parse part")
return
}
key := part.FormName()
filename := part.FileName()
contentType := part.Header.Get("Content-Type")
paths := uploadsMap[key]
if len(paths) == 0 {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "invalid empty operations paths list for key %s", key)
return
}
file, header, err := r.FormFile(key)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to get key %s from form", key)
return
}
defer file.Close()
delete(uploadsMap, key)
if len(paths) == 1 {
upload = graphql.Upload{
File: file,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, paths[0]); err != nil {
var upload graphql.Upload
if r.ContentLength < f.maxMemory() {
fileBytes, err := io.ReadAll(part)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
writeJsonErrorf(w, "failed to read file for key %s", key)
return
}
for _, path := range paths {
upload = graphql.Upload{
File: &bytesReader{s: &fileBytes, i: 0},
Size: int64(len(fileBytes)),
Filename: filename,
ContentType: contentType,
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
} else {
if r.ContentLength < f.maxMemory() {
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to read file for key %s", key)
return
}
for _, path := range paths {
upload = graphql.Upload{
File: &bytesReader{s: &fileBytes, i: 0, prevRune: -1},
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
} else {
tmpFile, err := ioutil.TempFile(os.TempDir(), "gqlgen-")
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to create temp file for key %s", key)
return
}
tmpName := tmpFile.Name()
defer func() {
_ = os.Remove(tmpName)
}()
_, err = io.Copy(tmpFile, file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if err := tmpFile.Close(); err != nil {
writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key)
return
}
writeJsonErrorf(w, "failed to copy to temp file for key %s", key)
return
}
tmpFile, err := os.CreateTemp(os.TempDir(), "gqlgen-")
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to create temp file for key %s", key)
return
}
tmpName := tmpFile.Name()
defer func() {
_ = os.Remove(tmpName)
}()
fileSize, err := io.Copy(tmpFile, part)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if err := tmpFile.Close(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to close temp file for key %s", key)
writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key)
return
}
for _, path := range paths {
pathTmpFile, err := os.Open(tmpName)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to open temp file for key %s", key)
return
}
defer pathTmpFile.Close()
upload = graphql.Upload{
File: pathTmpFile,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
writeJsonErrorf(w, "failed to copy to temp file for key %s", key)
return
}
if err := tmpFile.Close(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to close temp file for key %s", key)
return
}
for _, path := range paths {
pathTmpFile, err := os.Open(tmpName)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to open temp file for key %s", key)
return
}
defer pathTmpFile.Close()
upload = graphql.Upload{
File: pathTmpFile,
Size: fileSize,
Filename: filename,
ContentType: contentType,
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
}
}
for key := range uploadsMap {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to get key %s from form", key)
return
}
params.Headers = r.Header
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"github.com/99designs/gqlgen/graphql"
@ -27,15 +28,22 @@ func (h GET) Supports(r *http.Request) bool {
}
func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
query, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
raw := &graphql.RawParams{
Query: r.URL.Query().Get("query"),
OperationName: r.URL.Query().Get("operationName"),
Query: query.Get("query"),
OperationName: query.Get("operationName"),
Headers: r.Header,
}
raw.ReadTime.Start = graphql.Now()
if variables := r.URL.Query().Get("variables"); variables != "" {
if variables := query.Get("variables"); variables != "" {
if err := jsonDecode(strings.NewReader(variables), &raw.Variables); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "variables could not be decoded")
@ -43,7 +51,7 @@ func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut
}
}
if extensions := r.URL.Query().Get("extensions"); extensions != "" {
if extensions := query.Get("extensions"); extensions != "" {
if err := jsonDecode(strings.NewReader(extensions), &raw.Extensions); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "extensions could not be decoded")
@ -53,10 +61,10 @@ func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut
raw.ReadTime.End = graphql.Now()
rc, err := exec.CreateOperationContext(r.Context(), raw)
if err != nil {
w.WriteHeader(statusFor(err))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), err)
rc, gqlError := exec.CreateOperationContext(r.Context(), raw)
if gqlError != nil {
w.WriteHeader(statusFor(gqlError))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gqlError)
writeJson(w, resp)
return
}

View file

@ -36,6 +36,9 @@ func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecu
writeJsonErrorf(w, "json body could not be decoded: "+err.Error())
return
}
params.Headers = r.Header
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),

View file

@ -2,12 +2,16 @@ package transport
import (
"net/http"
"strings"
"github.com/99designs/gqlgen/graphql"
)
// Options responds to http OPTIONS and HEAD requests
type Options struct{}
type Options struct {
// AllowedMethods is a list of allowed HTTP methods.
AllowedMethods []string
}
var _ graphql.Transport = Options{}
@ -18,9 +22,16 @@ func (o Options) Supports(r *http.Request) bool {
func (o Options) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
switch r.Method {
case http.MethodOptions:
w.Header().Set("Allow", "OPTIONS, GET, POST")
w.Header().Set("Allow", o.allowedMethods())
w.WriteHeader(http.StatusOK)
case http.MethodHead:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (o Options) allowedMethods() string {
if len(o.AllowedMethods) == 0 {
return "OPTIONS, GET, POST"
}
return strings.Join(o.AllowedMethods, ", ")
}

View file

@ -6,9 +6,8 @@ import (
)
type bytesReader struct {
s *[]byte
i int64 // current reading index
prevRune int // index of previous rune; or < 0
s *[]byte
i int64 // current reading index
}
func (r *bytesReader) Read(b []byte) (n int, err error) {
@ -18,8 +17,29 @@ func (r *bytesReader) Read(b []byte) (n int, err error) {
if r.i >= int64(len(*r.s)) {
return 0, io.EOF
}
r.prevRune = -1
n = copy(b, (*r.s)[r.i:])
r.i += int64(n)
return
}
func (r *bytesReader) Seek(offset int64, whence int) (int64, error) {
if r.s == nil {
return 0, errors.New("byte slice pointer is nil")
}
var abs int64
switch whence {
case io.SeekStart:
abs = offset
case io.SeekCurrent:
abs = r.i + offset
case io.SeekEnd:
abs = int64(len(*r.s)) + offset
default:
return 0, errors.New("invalid whence")
}
if abs < 0 {
return 0, errors.New("negative position")
}
r.i = abs
return abs, nil
}

View file

@ -49,7 +49,24 @@ type (
var errReadTimeout = errors.New("read timeout")
var _ graphql.Transport = Websocket{}
type WebsocketError struct {
Err error
// IsReadError flags whether the error occurred on read or write to the websocket
IsReadError bool
}
func (e WebsocketError) Error() string {
if e.IsReadError {
return fmt.Sprintf("websocket read: %v", e.Err)
}
return fmt.Sprintf("websocket write: %v", e.Err)
}
var (
_ graphql.Transport = Websocket{}
_ error = WebsocketError{}
)
func (t Websocket) Supports(r *http.Request) bool {
return r.Header.Get("Upgrade") != ""
@ -94,9 +111,12 @@ func (t Websocket) Do(w http.ResponseWriter, r *http.Request, exec graphql.Graph
conn.run()
}
func (c *wsConnection) handlePossibleError(err error) {
func (c *wsConnection) handlePossibleError(err error, isReadError bool) {
if c.ErrorFunc != nil && err != nil {
c.ErrorFunc(c.ctx, err)
c.ErrorFunc(c.ctx, WebsocketError{
Err: err,
IsReadError: isReadError,
})
}
}
@ -181,7 +201,7 @@ func (c *wsConnection) init() bool {
func (c *wsConnection) write(msg *message) {
c.mu.Lock()
c.handlePossibleError(c.me.Send(msg))
c.handlePossibleError(c.me.Send(msg), false)
c.mu.Unlock()
}
@ -227,7 +247,7 @@ func (c *wsConnection) run() {
if err != nil {
// If the connection got closed by us, don't report the error
if !errors.Is(err, net.ErrClosed) {
c.handlePossibleError(err)
c.handlePossibleError(err, true)
}
return
}
@ -358,12 +378,8 @@ func (c *wsConnection) subscribe(start time.Time, msg *message) {
c.sendResponse(msg.id, response)
}
c.complete(msg.id)
c.mu.Lock()
delete(c.active, msg.id)
c.mu.Unlock()
cancel()
// complete and context cancel comes from the defer
}()
}

View file

@ -7,7 +7,7 @@ import (
"github.com/gorilla/websocket"
)
// https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
const (
graphqltransportwsSubprotocol = "graphql-transport-ws"

View file

@ -7,7 +7,7 @@ import (
"github.com/gorilla/websocket"
)
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
// https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
const (
graphqlwsSubprotocol = "graphql-ws"

55
vendor/github.com/99designs/gqlgen/graphql/input.go generated vendored Normal file
View file

@ -0,0 +1,55 @@
package graphql
import (
"context"
"errors"
"reflect"
)
const unmarshalInputCtx key = "unmarshal_input_context"
// BuildUnmarshalerMap returns a map of unmarshal functions of the ExecutableContext
// to use with the WithUnmarshalerMap function.
func BuildUnmarshalerMap(unmarshaler ...interface{}) map[reflect.Type]reflect.Value {
maps := make(map[reflect.Type]reflect.Value)
for _, v := range unmarshaler {
ft := reflect.TypeOf(v)
if ft.Kind() == reflect.Func {
maps[ft.Out(0)] = reflect.ValueOf(v)
}
}
return maps
}
// WithUnmarshalerMap returns a new context with a map from input types to their unmarshaler functions.
func WithUnmarshalerMap(ctx context.Context, maps map[reflect.Type]reflect.Value) context.Context {
return context.WithValue(ctx, unmarshalInputCtx, maps)
}
// UnmarshalInputFromContext allows unmarshaling input object from a context.
func UnmarshalInputFromContext(ctx context.Context, raw, v interface{}) error {
m, ok := ctx.Value(unmarshalInputCtx).(map[reflect.Type]reflect.Value)
if m == nil || !ok {
return errors.New("graphql: the input context is empty")
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return errors.New("graphql: input must be a non-nil pointer")
}
if fn, ok := m[rv.Elem().Type()]; ok {
res := fn.Call([]reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(raw),
})
if err := res[1].Interface(); err != nil {
return err.(error)
}
rv.Elem().Set(res[0])
return nil
}
return errors.New("graphql: no unmarshal function found")
}

View file

@ -3,22 +3,26 @@ package playground
import (
"html/template"
"net/http"
"net/url"
)
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
integrity="{{.cssSRI}}"
crossorigin="anonymous"
/>
</head>
<body style="margin: 0;">
<div id="graphiql" style="height: 100vh;"></div>
<meta charset="utf-8">
<title>{{.title}}</title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<script
src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"
integrity="{{.reactSRI}}"
@ -29,6 +33,16 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
integrity="{{.reactDOMSRI}}"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
integrity="{{.cssSRI}}"
crossorigin="anonymous"
/>
</head>
<body>
<div id="graphiql">Loading...</div>
<script
src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js"
integrity="{{.jsSRI}}"
@ -36,15 +50,20 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
></script>
<script>
const url = location.protocol + '//' + location.host + '{{.endpoint}}';
{{- if .endpointIsAbsolute}}
const url = {{.endpoint}};
const subscriptionUrl = {{.subscriptionEndpoint}};
{{- else}}
const url = location.protocol + '//' + location.host + {{.endpoint}};
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:';
const subscriptionUrl = wsProto + '//' + location.host + '{{.endpoint}}';
const subscriptionUrl = wsProto + '//' + location.host + {{.endpoint}};
{{- end}}
const fetcher = GraphiQL.createFetcher({ url, subscriptionUrl });
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: fetcher,
headerEditorEnabled: true,
isHeadersEditorEnabled: true,
shouldPersistHeaders: true
}),
document.getElementById('graphiql'),
@ -54,20 +73,47 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
</html>
`))
// Handler responsible for setting up the playground
func Handler(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{
"title": title,
"endpoint": endpoint,
"version": "1.5.16",
"cssSRI": "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=",
"jsSRI": "sha256-uHp12yvpXC4PC9+6JmITxKuLYwjlW9crq9ywPE5Rxco=",
"reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
"reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
w.Header().Add("Content-Type", "text/html; charset=UTF-8")
err := page.Execute(w, map[string]interface{}{
"title": title,
"endpoint": endpoint,
"endpointIsAbsolute": endpointHasScheme(endpoint),
"subscriptionEndpoint": getSubscriptionEndpoint(endpoint),
"version": "2.0.7",
"cssSRI": "sha256-gQryfbGYeYFxnJYnfPStPYFt0+uv8RP8Dm++eh00G9c=",
"jsSRI": "sha256-qQ6pw7LwTLC+GfzN+cJsYXfVWRKH9O5o7+5H96gTJhQ=",
"reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
"reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
})
if err != nil {
panic(err)
}
}
}
// endpointHasScheme checks if the endpoint has a scheme.
func endpointHasScheme(endpoint string) bool {
u, err := url.Parse(endpoint)
return err == nil && u.Scheme != ""
}
// getSubscriptionEndpoint returns the subscription endpoint for the given
// endpoint if it is parsable as a URL, or an empty string.
func getSubscriptionEndpoint(endpoint string) string {
u, err := url.Parse(endpoint)
if err != nil {
return ""
}
switch u.Scheme {
case "https":
u.Scheme = "wss"
default:
u.Scheme = "ws"
}
return u.String()
}

View file

@ -6,7 +6,7 @@ import (
)
type Upload struct {
File io.Reader
File io.ReadSeeker
Filename string
Size int64
ContentType string

View file

@ -1,3 +1,3 @@
package graphql
const Version = "v0.17.2"
const Version = "v0.17.20"

View file

@ -29,6 +29,13 @@ resolver:
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: turn off to make struct-type struct fields not use pointers
# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing }
# struct_fields_always_pointers: true
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true

View file

@ -1,10 +1,12 @@
package code
import (
"bufio"
"fmt"
"go/build"
"go/parser"
"go/token"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
@ -26,7 +28,7 @@ func NameForDir(dir string) string {
if err != nil {
return SanitizePackageName(filepath.Base(dir))
}
files, err := ioutil.ReadDir(dir)
files, err := os.ReadDir(dir)
if err != nil {
return SanitizePackageName(filepath.Base(dir))
}
@ -73,8 +75,8 @@ func goModuleRoot(dir string) (string, bool) {
break
}
if content, err := ioutil.ReadFile(filepath.Join(modDir, "go.mod")); err == nil {
moduleName := string(modregex.FindSubmatch(content)[1])
if content, err := os.ReadFile(filepath.Join(modDir, "go.mod")); err == nil {
moduleName := extractModuleName(content)
result = goModuleSearchResult{
path: moduleName,
goModPath: modDir,
@ -126,6 +128,27 @@ func goModuleRoot(dir string) (string, bool) {
return res.path, true
}
func extractModuleName(content []byte) string {
for {
advance, tkn, err := bufio.ScanLines(content, false)
if err != nil {
panic(fmt.Errorf("error parsing mod file: %w", err))
}
if advance == 0 {
break
}
s := strings.Trim(string(tkn), " \t")
if len(s) != 0 && !strings.HasPrefix(s, "//") {
break
}
if advance <= len(content) {
content = content[advance:]
}
}
moduleName := string(modregex.FindSubmatch(content)[1])
return moduleName
}
// ImportPathForDir takes a path and returns a golang import path for the package
func ImportPathForDir(dir string) (res string) {
dir, err := filepath.Abs(dir)

View file

@ -5,7 +5,7 @@ import (
"fmt"
"go/ast"
"go/token"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
@ -56,7 +56,7 @@ func (r *Rewriter) getSource(start, end token.Pos) string {
func (r *Rewriter) getFile(filename string) string {
if _, ok := r.files[filename]; !ok {
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
panic(fmt.Errorf("unable to load file, already exists: %w", err))
}
@ -68,6 +68,37 @@ func (r *Rewriter) getFile(filename string) string {
return r.files[filename]
}
func (r *Rewriter) GetMethodComment(structname string, methodname string) string {
for _, f := range r.pkg.Syntax {
for _, d := range f.Decls {
d, isFunc := d.(*ast.FuncDecl)
if !isFunc {
continue
}
if d.Name.Name != methodname {
continue
}
if d.Recv == nil || len(d.Recv.List) == 0 {
continue
}
recv := d.Recv.List[0].Type
if star, isStar := recv.(*ast.StarExpr); isStar {
recv = star.X
}
ident, ok := recv.(*ast.Ident)
if !ok {
continue
}
if ident.Name != structname {
continue
}
return d.Doc.Text()
}
}
return ""
}
func (r *Rewriter) GetMethodBody(structname string, methodname string) string {
for _, f := range r.pkg.Syntax {
for _, d := range f.Decls {

View file

@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"io/ioutil"
"log"
"os"
"path/filepath"
@ -43,7 +43,7 @@ func initFile(filename, contents string) error {
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
return fmt.Errorf("unable to create directory for file '%s': %w\n", filename, err)
}
if err := ioutil.WriteFile(filename, []byte(contents), 0o644); err != nil {
if err := os.WriteFile(filename, []byte(contents), 0o644); err != nil {
return fmt.Errorf("unable to write file '%s': %w\n", filename, err)
}
@ -171,7 +171,7 @@ func main() {
if context.Bool("verbose") {
log.SetFlags(0)
} else {
log.SetOutput(ioutil.Discard)
log.SetOutput(io.Discard)
}
return nil
}

View file

@ -1,6 +1,7 @@
package federation
import (
_ "embed"
"fmt"
"sort"
"strings"
@ -14,14 +15,22 @@ import (
"github.com/99designs/gqlgen/plugin/federation/fieldset"
)
//go:embed federation.gotpl
var federationTemplate string
type federation struct {
Entities []*Entity
Version int
}
// New returns a federation plugin that injects
// federated directives and types into the schema
func New() plugin.Plugin {
return &federation{}
func New(version int) plugin.Plugin {
if version == 0 {
version = 1
}
return &federation{Version: version}
}
// Name returns the plugin name
@ -51,6 +60,7 @@ func (f *federation) MutateConfig(cfg *config.Config) error {
Model: config.StringList{"github.com/99designs/gqlgen/graphql.Map"},
},
}
for typeName, entry := range builtins {
if cfg.Models.Exists(typeName) {
return fmt.Errorf("%v already exists which must be reserved when Federation is enabled", typeName)
@ -63,22 +73,46 @@ func (f *federation) MutateConfig(cfg *config.Config) error {
cfg.Directives["key"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["extends"] = config.DirectiveConfig{SkipRuntime: true}
// Federation 2 specific directives
if f.Version == 2 {
cfg.Directives["shareable"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["link"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["tag"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["override"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["inaccessible"] = config.DirectiveConfig{SkipRuntime: true}
}
return nil
}
func (f *federation) InjectSourceEarly() *ast.Source {
return &ast.Source{
Name: "federation/directives.graphql",
Input: `
scalar _Any
scalar _FieldSet
input := `
scalar _Any
scalar _FieldSet
directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
directive @extends on OBJECT | INTERFACE
`,
directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @extends on OBJECT | INTERFACE
`
// add version-specific changes on key directive, as well as adding the new directives for federation 2
if f.Version == 1 {
input += `
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
`
} else if f.Version == 2 {
input += `
directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @link(import: [String!], url: String!) repeatable on SCHEMA
directive @shareable on OBJECT | FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @override(from: String!) on FIELD_DEFINITION
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
`
}
return &ast.Source{
Name: "federation/directives.graphql",
Input: input,
BuiltIn: true,
}
}
@ -244,6 +278,7 @@ func (f *federation) GenerateCode(data *codegen.Data) error {
Data: f,
GeneratedHeader: true,
Packages: data.Config.Packages,
Template: federationTemplate,
})
}
@ -290,10 +325,21 @@ func (f *federation) setEntities(schema *ast.Schema) {
// }
if !e.allFieldsAreExternal() {
for _, dir := range keys {
if len(dir.Arguments) != 1 || dir.Arguments[0].Name != "fields" {
panic("Exactly one `fields` argument needed for @key declaration.")
if len(dir.Arguments) > 2 {
panic("More than two arguments provided for @key declaration.")
}
arg := dir.Arguments[0]
var arg *ast.Argument
// since keys are able to now have multiple arguments, we need to check both possible for a possible @key(fields="" fields="")
for _, a := range dir.Arguments {
if a.Name == "fields" {
if arg != nil {
panic("More than one `fields` provided for @key declaration.")
}
arg = a
}
}
keyFieldSet := fieldset.New(arg.Value.Raw, nil)
keyFields := make([]*KeyField, len(keyFieldSet))

View file

@ -11,15 +11,12 @@ import (
// Set represents a FieldSet that is used in federation directives @key and @requires.
// Would be happier to reuse FieldSet parsing from gqlparser, but this suits for now.
//
type Set []Field
// Field represents a single field in a FieldSet
//
type Field []string
// New parses a FieldSet string into a TinyFieldSet.
//
func New(raw string, prefix []string) Set {
if !strings.Contains(raw, "{") {
return parseUnnestedKeyFieldSet(raw, prefix)
@ -48,7 +45,6 @@ func New(raw string, prefix []string) Set {
}
// FieldDefinition looks up a field in the type.
//
func (f Field) FieldDefinition(schemaType *ast.Definition, schema *ast.Schema) *ast.FieldDefinition {
objType := schemaType
def := objType.Fields.ForName(f[0])
@ -74,7 +70,6 @@ func (f Field) FieldDefinition(schemaType *ast.Definition, schema *ast.Schema) *
}
// TypeReference looks up the type of a field.
//
func (f Field) TypeReference(obj *codegen.Object, objects codegen.Objects) *codegen.Field {
var def *codegen.Field
@ -89,7 +84,6 @@ func (f Field) TypeReference(obj *codegen.Object, objects codegen.Objects) *code
}
// ToGo converts a (possibly nested) field into a proper public Go name.
//
func (f Field) ToGo() string {
var ret string
@ -100,7 +94,6 @@ func (f Field) ToGo() string {
}
// ToGoPrivate converts a (possibly nested) field into a proper private Go name.
//
func (f Field) ToGoPrivate() string {
var ret string
@ -115,13 +108,11 @@ func (f Field) ToGoPrivate() string {
}
// Join concatenates the field parts with a string separator between. Useful in templates.
//
func (f Field) Join(str string) string {
return strings.Join(f, str)
}
// JoinGo concatenates the Go name of field parts with a string separator between. Useful in templates.
//
func (f Field) JoinGo(str string) string {
strs := []string{}
@ -138,7 +129,6 @@ func (f Field) LastIndex() int {
// local functions
// parseUnnestedKeyFieldSet // handles simple case where none of the fields are nested.
//
func parseUnnestedKeyFieldSet(raw string, prefix []string) Set {
ret := Set{}
@ -150,7 +140,6 @@ func parseUnnestedKeyFieldSet(raw string, prefix []string) Set {
}
// extractSubs splits out and trims sub-expressions from before, inside, and after "{}".
//
func extractSubs(str string) (string, string, string) {
start := strings.Index(str, "{")
end := matchingBracketIndex(str, start)
@ -162,7 +151,6 @@ func extractSubs(str string) (string, string, string) {
}
// matchingBracketIndex returns the index of the closing bracket, assuming an open bracket at start.
//
func matchingBracketIndex(str string, start int) int {
if start < 0 || len(str) <= start+1 {
return -1

View file

@ -1,10 +1,12 @@
package modelgen
import (
_ "embed"
"fmt"
"go/types"
"sort"
"strings"
"text/template"
"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates"
@ -12,6 +14,9 @@ import (
"github.com/vektah/gqlparser/v2/ast"
)
//go:embed models.gotpl
var modelTemplate string
type BuildMutateHook = func(b *ModelBuild) *ModelBuild
type FieldMutateHook = func(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error)
@ -36,6 +41,7 @@ type ModelBuild struct {
type Interface struct {
Description string
Name string
Fields []*Field
Implements []string
}
@ -48,9 +54,12 @@ type Object struct {
type Field struct {
Description string
Name string
Type types.Type
Tag string
// Name is the field's name as it appears in the schema
Name string
// GoName is the field's name as it appears in the generated Go code
GoName string
Type types.Type
Tag string
}
type Enum struct {
@ -83,7 +92,6 @@ func (m *Plugin) Name() string {
}
func (m *Plugin) MutateConfig(cfg *config.Config) error {
binder := cfg.NewBinder()
b := &ModelBuild{
PackageName: cfg.Model.Package,
@ -95,10 +103,20 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
}
switch schemaType.Kind {
case ast.Interface, ast.Union:
var fields []*Field
var err error
if !cfg.OmitGetters {
fields, err = m.generateFields(cfg, schemaType)
if err != nil {
return err
}
}
it := &Interface{
Description: schemaType.Description,
Name: schemaType.Name,
Implements: schemaType.Interfaces,
Fields: fields,
}
b.Interfaces = append(b.Interfaces, it)
@ -106,9 +124,16 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
if schemaType == cfg.Schema.Query || schemaType == cfg.Schema.Mutation || schemaType == cfg.Schema.Subscription {
continue
}
fields, err := m.generateFields(cfg, schemaType)
if err != nil {
return err
}
it := &Object{
Description: schemaType.Description,
Name: schemaType.Name,
Fields: fields,
}
// If Interface A implements interface B, and Interface C also implements interface B
@ -129,84 +154,6 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
}
}
for _, field := range schemaType.Fields {
var typ types.Type
fieldDef := cfg.Schema.Types[field.Type.Name()]
if cfg.Models.UserDefined(field.Type.Name()) {
var err error
typ, err = binder.FindTypeFromName(cfg.Models[field.Type.Name()].Model[0])
if err != nil {
return err
}
} else {
switch fieldDef.Kind {
case ast.Scalar:
// no user defined model, referencing a default scalar
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), "string", nil),
nil,
nil,
)
case ast.Interface, ast.Union:
// no user defined model, referencing a generated interface type
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewInterfaceType([]*types.Func{}, []types.Type{}),
nil,
)
case ast.Enum:
// no user defined model, must reference a generated enum
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
nil,
nil,
)
case ast.Object, ast.InputObject:
// no user defined model, must reference a generated struct
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewStruct(nil, nil),
nil,
)
default:
panic(fmt.Errorf("unknown ast type %s", fieldDef.Kind))
}
}
name := field.Name
if nameOveride := cfg.Models[schemaType.Name].Fields[field.Name].FieldName; nameOveride != "" {
name = nameOveride
}
typ = binder.CopyModifiersFromAst(field.Type, typ)
if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) {
typ = types.NewPointer(typ)
}
f := &Field{
Name: name,
Type: typ,
Description: field.Description,
Tag: `json:"` + field.Name + `"`,
}
if m.FieldHook != nil {
mf, err := m.FieldHook(schemaType, field, f)
if err != nil {
return fmt.Errorf("generror: field %v.%v: %w", it.Name, field.Name, err)
}
f = mf
}
it.Fields = append(it.Fields, f)
}
b.Models = append(b.Models, it)
case ast.Enum:
it := &Enum{
@ -230,6 +177,12 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
sort.Slice(b.Models, func(i, j int) bool { return b.Models[i].Name < b.Models[j].Name })
sort.Slice(b.Interfaces, func(i, j int) bool { return b.Interfaces[i].Name < b.Interfaces[j].Name })
// if we are not just turning all struct-type fields in generated structs into pointers, we need to at least
// check for cyclical relationships and recursive structs
if !cfg.StructFieldsAlwaysPointers {
findAndHandleCyclicalRelationships(b)
}
for _, it := range b.Enums {
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
}
@ -251,12 +204,84 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
b = m.MutateHook(b)
}
getInterfaceByName := func(name string) *Interface {
// Allow looking up interfaces, so template can generate getters for each field
for _, i := range b.Interfaces {
if i.Name == name {
return i
}
}
return nil
}
gettersGenerated := make(map[string]map[string]struct{})
generateGetter := func(model *Object, field *Field) string {
if model == nil || field == nil {
return ""
}
// Let templates check if a given getter has been generated already
typeGetters, exists := gettersGenerated[model.Name]
if !exists {
typeGetters = make(map[string]struct{})
gettersGenerated[model.Name] = typeGetters
}
_, exists = typeGetters[field.GoName]
typeGetters[field.GoName] = struct{}{}
if exists {
return ""
}
_, interfaceFieldTypeIsPointer := field.Type.(*types.Pointer)
var structFieldTypeIsPointer bool
for _, f := range model.Fields {
if f.GoName == field.GoName {
_, structFieldTypeIsPointer = f.Type.(*types.Pointer)
break
}
}
goType := templates.CurrentImports.LookupType(field.Type)
if strings.HasPrefix(goType, "[]") {
getter := fmt.Sprintf("func (this %s) Get%s() %s {\n", templates.ToGo(model.Name), field.GoName, goType)
getter += fmt.Sprintf("\tif this.%s == nil { return nil }\n", field.GoName)
getter += fmt.Sprintf("\tinterfaceSlice := make(%s, 0, len(this.%s))\n", goType, field.GoName)
getter += fmt.Sprintf("\tfor _, concrete := range this.%s { interfaceSlice = append(interfaceSlice, ", field.GoName)
if interfaceFieldTypeIsPointer && !structFieldTypeIsPointer {
getter += "&"
} else if !interfaceFieldTypeIsPointer && structFieldTypeIsPointer {
getter += "*"
}
getter += "concrete) }\n"
getter += "\treturn interfaceSlice\n"
getter += "}"
return getter
} else {
getter := fmt.Sprintf("func (this %s) Get%s() %s { return ", templates.ToGo(model.Name), field.GoName, goType)
if interfaceFieldTypeIsPointer && !structFieldTypeIsPointer {
getter += "&"
} else if !interfaceFieldTypeIsPointer && structFieldTypeIsPointer {
getter += "*"
}
getter += fmt.Sprintf("this.%s }", field.GoName)
return getter
}
}
funcMap := template.FuncMap{
"getInterfaceByName": getInterfaceByName,
"generateGetter": generateGetter,
}
err := templates.Render(templates.Options{
PackageName: cfg.Model.Package,
Filename: cfg.Model.Filename,
Data: b,
GeneratedHeader: true,
Packages: cfg.Packages,
Template: modelTemplate,
Funcs: funcMap,
})
if err != nil {
return err
@ -269,6 +294,94 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
return nil
}
func (m *Plugin) generateFields(cfg *config.Config, schemaType *ast.Definition) ([]*Field, error) {
binder := cfg.NewBinder()
fields := make([]*Field, 0)
for _, field := range schemaType.Fields {
var typ types.Type
fieldDef := cfg.Schema.Types[field.Type.Name()]
if cfg.Models.UserDefined(field.Type.Name()) {
var err error
typ, err = binder.FindTypeFromName(cfg.Models[field.Type.Name()].Model[0])
if err != nil {
return nil, err
}
} else {
switch fieldDef.Kind {
case ast.Scalar:
// no user defined model, referencing a default scalar
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), "string", nil),
nil,
nil,
)
case ast.Interface, ast.Union:
// no user defined model, referencing a generated interface type
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewInterfaceType([]*types.Func{}, []types.Type{}),
nil,
)
case ast.Enum:
// no user defined model, must reference a generated enum
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
nil,
nil,
)
case ast.Object, ast.InputObject:
// no user defined model, must reference a generated struct
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewStruct(nil, nil),
nil,
)
default:
panic(fmt.Errorf("unknown ast type %s", fieldDef.Kind))
}
}
name := templates.ToGo(field.Name)
if nameOveride := cfg.Models[schemaType.Name].Fields[field.Name].FieldName; nameOveride != "" {
name = nameOveride
}
typ = binder.CopyModifiersFromAst(field.Type, typ)
if cfg.StructFieldsAlwaysPointers {
if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) {
typ = types.NewPointer(typ)
}
}
f := &Field{
Name: field.Name,
GoName: name,
Type: typ,
Description: field.Description,
Tag: `json:"` + field.Name + `"`,
}
if m.FieldHook != nil {
mf, err := m.FieldHook(schemaType, field, f)
if err != nil {
return nil, fmt.Errorf("generror: field %v.%v: %w", schemaType.Name, field.Name, err)
}
f = mf
}
fields = append(fields, f)
}
return fields, nil
}
// GoTagFieldHook applies the goTag directive to the generated Field f. When applying the Tag to the field, the field
// name is used when no value argument is present.
func GoTagFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) {
@ -303,3 +416,55 @@ func isStruct(t types.Type) bool {
_, is := t.Underlying().(*types.Struct)
return is
}
// findAndHandleCyclicalRelationships checks for cyclical relationships between generated structs and replaces them
// with pointers. These relationships will produce compilation errors if they are not pointers.
// Also handles recursive structs.
func findAndHandleCyclicalRelationships(b *ModelBuild) {
for ii, structA := range b.Models {
for _, fieldA := range structA.Fields {
if strings.Contains(fieldA.Type.String(), "NotCyclicalA") {
fmt.Print()
}
if !isStruct(fieldA.Type) {
continue
}
// the field Type string will be in the form "github.com/99designs/gqlgen/codegen/testserver/followschema.LoopA"
// we only want the part after the last dot: "LoopA"
// this could lead to false positives, as we are only checking the name of the struct type, but these
// should be extremely rare, if it is even possible at all.
fieldAStructNameParts := strings.Split(fieldA.Type.String(), ".")
fieldAStructName := fieldAStructNameParts[len(fieldAStructNameParts)-1]
// find this struct type amongst the generated structs
for jj, structB := range b.Models {
if structB.Name != fieldAStructName {
continue
}
// check if structB contains a cyclical reference back to structA
var cyclicalReferenceFound bool
for _, fieldB := range structB.Fields {
if !isStruct(fieldB.Type) {
continue
}
fieldBStructNameParts := strings.Split(fieldB.Type.String(), ".")
fieldBStructName := fieldBStructNameParts[len(fieldBStructNameParts)-1]
if fieldBStructName == structA.Name {
cyclicalReferenceFound = true
fieldB.Type = types.NewPointer(fieldB.Type)
// keep looping in case this struct has additional fields of this type
}
}
// if this is a recursive struct (i.e. structA == structB), ensure that we only change this field to a pointer once
if cyclicalReferenceFound && ii != jj {
fieldA.Type = types.NewPointer(fieldA.Type)
break
}
}
}
}
}

View file

@ -14,74 +14,88 @@
{{- range $model := .Interfaces }}
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
type {{.Name|go }} interface {
type {{ goModelName .Name }} interface {
{{- range $impl := .Implements }}
{{ $impl|go }}
Is{{ goModelName $impl }}()
{{- end }}
Is{{ goModelName .Name }}()
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- end}}
Get{{ $field.GoName }}() {{ $field.Type | ref }}
{{- end }}
Is{{.Name|go }}()
}
{{- end }}
{{ range $model := .Models }}
{{with .Description }} {{.|prefixLines "// "}} {{end}}
type {{ .Name|go }} struct {
type {{ goModelName .Name }} struct {
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- end}}
{{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}}`
{{ $field.GoName }} {{$field.Type | ref}} `{{$field.Tag}}`
{{- end }}
}
{{- range $iface := .Implements }}
func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
{{- end }}
{{ range .Implements }}
func ({{ goModelName $model.Name }}) Is{{ goModelName . }}() {}
{{- with getInterfaceByName . }}
{{- range .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- end}}
{{ generateGetter $model . }}
{{- end }}
{{- end }}
{{ end }}
{{- end}}
{{ range $enum := .Enums }}
{{ with .Description }} {{.|prefixLines "// "}} {{end}}
type {{.Name|go }} string
type {{ goModelName .Name }} string
const (
{{- range $value := .Values}}
{{- with .Description}}
{{.|prefixLines "// "}}
{{- end}}
{{ $enum.Name|go }}{{ .Name|go }} {{$enum.Name|go }} = {{.Name|quote}}
{{ goModelName $enum.Name .Name }} {{ goModelName $enum.Name }} = {{ .Name|quote }}
{{- end }}
)
var All{{.Name|go }} = []{{ .Name|go }}{
var All{{ goModelName .Name }} = []{{ goModelName .Name }}{
{{- range $value := .Values}}
{{$enum.Name|go }}{{ .Name|go }},
{{ goModelName $enum.Name .Name }},
{{- end }}
}
func (e {{.Name|go }}) IsValid() bool {
func (e {{ goModelName .Name }}) IsValid() bool {
switch e {
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.Name|go }}{{ $element.Name|go }}{{end}}:
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ goModelName $enum.Name $element.Name }}{{end}}:
return true
}
return false
}
func (e {{.Name|go }}) String() string {
func (e {{ goModelName .Name }}) String() string {
return string(e)
}
func (e *{{.Name|go }}) UnmarshalGQL(v interface{}) error {
func (e *{{ goModelName .Name }}) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = {{ .Name|go }}(str)
*e = {{ goModelName .Name }}(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid {{ .Name }}", str)
}
return nil
}
func (e {{.Name|go }}) MarshalGQL(w io.Writer) {
func (e {{ goModelName .Name }}) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View file

@ -1,7 +1,9 @@
package resolvergen
import (
_ "embed"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
@ -12,8 +14,13 @@ import (
"github.com/99designs/gqlgen/codegen/templates"
"github.com/99designs/gqlgen/internal/rewrite"
"github.com/99designs/gqlgen/plugin"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
//go:embed resolver.gotpl
var resolverTemplate string
func New() plugin.Plugin {
return &Plugin{}
}
@ -58,7 +65,7 @@ func (m *Plugin) generateSingleFile(data *codegen.Data) error {
continue
}
resolver := Resolver{o, f, `panic("not implemented")`}
resolver := Resolver{o, f, "// foo", `panic("not implemented")`}
file.Resolvers = append(file.Resolvers, &resolver)
}
}
@ -76,6 +83,7 @@ func (m *Plugin) generateSingleFile(data *codegen.Data) error {
Filename: data.Config.Resolver.Filename,
Data: resolverBuild,
Packages: data.Config.Packages,
Template: resolverTemplate,
})
}
@ -98,8 +106,9 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error {
files[fn] = &File{}
}
caser := cases.Title(language.English, cases.NoLower)
rewriter.MarkStructCopied(templates.LcFirst(o.Name) + templates.UcFirst(data.Config.Resolver.Type))
rewriter.GetMethodBody(data.Config.Resolver.Type, strings.Title(o.Name))
rewriter.GetMethodBody(data.Config.Resolver.Type, caser.String(o.Name))
files[fn].Objects = append(files[fn].Objects, o)
}
for _, f := range o.Fields {
@ -109,11 +118,15 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error {
structName := templates.LcFirst(o.Name) + templates.UcFirst(data.Config.Resolver.Type)
implementation := strings.TrimSpace(rewriter.GetMethodBody(structName, f.GoFieldName))
comment := strings.TrimSpace(strings.TrimLeft(rewriter.GetMethodComment(structName, f.GoFieldName), `\`))
if implementation == "" {
implementation = `panic(fmt.Errorf("not implemented"))`
implementation = fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v - %v\"))", f.GoFieldName, f.Name)
}
if comment == "" {
comment = fmt.Sprintf("%v is the resolver for the %v field.", f.GoFieldName, f.Name)
}
resolver := Resolver{o, f, implementation}
resolver := Resolver{o, f, comment, implementation}
fn := gqlToResolverName(data.Config.Resolver.Dir(), f.Position.Src.Name, data.Config.Resolver.FilenameTemplate)
if files[fn] == nil {
files[fn] = &File{}
@ -143,6 +156,7 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error {
Filename: filename,
Data: resolverBuild,
Packages: data.Config.Packages,
Template: resolverTemplate,
})
if err != nil {
return err
@ -198,6 +212,7 @@ func (f *File) Imports() string {
type Resolver struct {
Object *codegen.Object
Field *codegen.Field
Comment string
Implementation string
}

View file

@ -19,6 +19,7 @@
{{ end }}
{{ range $resolver := .Resolvers -}}
// {{ $resolver.Comment }}
func (r *{{lcFirst $resolver.Object.Name}}{{ucFirst $.ResolverType}}) {{$resolver.Field.GoFieldName}}{{ $resolver.Field.ShortResolverDeclaration }} {
{{ $resolver.Implementation }}
}

View file

@ -1,6 +1,7 @@
package servergen
import (
_ "embed"
"errors"
"io/fs"
"log"
@ -11,6 +12,9 @@ import (
"github.com/99designs/gqlgen/plugin"
)
//go:embed server.gotpl
var serverTemplate string
func New(filename string) plugin.Plugin {
return &Plugin{filename}
}
@ -37,6 +41,7 @@ func (m *Plugin) GenerateCode(data *codegen.Data) error {
Filename: m.filename,
Data: serverBuild,
Packages: data.Config.Packages,
Template: serverTemplate,
})
}

View file

@ -1,8 +0,0 @@
//go:build tools
// +build tools
package main
import (
_ "github.com/matryer/moq"
)

View file

@ -44,9 +44,13 @@ linters:
- varnamelen
- nilnil
- structcheck
- exhaustruct
- nonamedreturns
fast: false
issues:
max-issues-per-linter: 0
max-same-issues: 0
exclude-rules:
# Test
- path: _test\.go
@ -57,6 +61,7 @@ issues:
linters:
- unused
- structcheck
- forcetypeassert
- path: introspection/type.go
linters:
- structcheck # These types fits IntrospectionQuery

View file

@ -48,6 +48,9 @@ endpoint:
Authorization: "Bearer ${ANNICT_KEY}" # support environment variables
query:
- "./query/*.graphql" # Where are all the query files located?
generate:
clientV2: true # Generate a Client that provides a new signature
clientInterfaceName: "GithubGraphQLClient" # Determine the name of the generated client interface
```
Load a schema from a local file:
@ -68,6 +71,9 @@ schema:
- "schema/**/*.graphql" # Where are all the schema files located?
query:
- "./query/*.graphql" # Where are all the query files located?
generate:
clientV2: true # Generate a Client that provides a new signature
clientInterfaceName: "GithubGraphQLClient" # Determine the name of the generated client interface
```
Execute the following command on same directory for .gqlgenc.yml

View file

@ -5,7 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"github.com/Yamashou/gqlgenc/graphqljson"
@ -55,6 +55,9 @@ func (c *Client) newRequest(ctx context.Context, operationName, query string, va
return nil, fmt.Errorf("create request struct failed: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json; charset=utf-8")
for _, httpRequestOption := range c.HTTPRequestOptions {
httpRequestOption(req)
}
@ -109,8 +112,6 @@ func (c *Client) Post(ctx context.Context, operationName, query string, respData
if err != nil {
return fmt.Errorf("don't create request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json; charset=utf-8")
resp, err := c.Client.Do(req)
if err != nil {
@ -118,7 +119,7 @@ func (c *Client) Post(ctx context.Context, operationName, query string, respData
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

View file

@ -24,7 +24,6 @@ package clientgen
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -94,7 +93,7 @@ func LoadQuerySources(queryFileNames []string) ([]*ast.Source, error) {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
schemaRaw, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to open schema: %w", err)
}

View file

@ -19,7 +19,7 @@ func RenderTemplate(cfg *config.Config, query *Query, mutation *Mutation, fragme
"Operation": operations,
"OperationResponse": operationResponses,
"GenerateClient": generateCfg.ShouldGenerateClient(),
"ClientInterfaceName": generateCfg.ClientInterfaceName,
"ClientInterfaceName": generateCfg.GetClientInterfaceName(),
},
Packages: cfg.Packages,
PackageDoc: "// Code generated by github.com/Yamashou/gqlgenc, DO NOT EDIT.\n",

View file

@ -4,7 +4,6 @@
{{ reserveImport "encoding/json" }}
{{ reserveImport "fmt" }}
{{ reserveImport "io" }}
{{ reserveImport "io/ioutil" }}
{{ reserveImport "net/http" }}
{{ reserveImport "net/url" }}
{{ reserveImport "path" }}

View file

@ -24,7 +24,6 @@ package clientgenv2
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -94,7 +93,7 @@ func LoadQuerySources(queryFileNames []string) ([]*ast.Source, error) {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
schemaRaw, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to open schema: %w", err)
}

View file

@ -4,7 +4,6 @@
{{ reserveImport "encoding/json" }}
{{ reserveImport "fmt" }}
{{ reserveImport "io" }}
{{ reserveImport "io/ioutil" }}
{{ reserveImport "net/http" }}
{{ reserveImport "net/url" }}
{{ reserveImport "path" }}

View file

@ -3,7 +3,6 @@ package config
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@ -116,7 +115,7 @@ var path2regex = strings.NewReplacer(
// LoadConfig loads and parses the config gqlgenc config
func LoadConfig(filename string) (*Config, error) {
var cfg Config
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to read config: %w", err)
}
@ -188,7 +187,7 @@ func LoadConfig(filename string) (*Config, error) {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
schemaRaw, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("unable to open schema: %w", err)
}
@ -269,7 +268,7 @@ func (c *Config) loadRemoteSchema(ctx context.Context) (*ast.Schema, error) {
func (c *Config) loadLocalSchema() (*ast.Schema, error) {
schema, err := gqlparser.LoadSchema(c.GQLConfig.Sources...)
if err != nil {
return nil, err
return nil, fmt.Errorf("loadLocalSchema: %w", err)
}
return schema, nil

View file

@ -5,33 +5,43 @@ import (
"fmt"
"github.com/99designs/gqlgen/api"
codegenconfig "github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/plugin"
"github.com/99designs/gqlgen/plugin/modelgen"
"github.com/Yamashou/gqlgenc/config"
)
// mutateHook adds the "omitempty" option to nilable fields.
// mutateHook adds the "omitempty" option to optional field from input type model as defined in graphql schema
// For more info see https://github.com/99designs/gqlgen/blob/master/docs/content/recipes/modelgen-hook.md
func mutateHook(build *modelgen.ModelBuild) *modelgen.ModelBuild {
for _, model := range build.Models {
for _, field := range model.Fields {
field.Tag = `json:"` + field.Name
if codegenconfig.IsNilable(field.Type) {
field.Tag += ",omitempty"
}
field.Tag += `"`
}
}
func mutateHook(cfg *config.Config) func(b *modelgen.ModelBuild) *modelgen.ModelBuild {
return func(build *modelgen.ModelBuild) *modelgen.ModelBuild {
for _, model := range build.Models {
// only handle input type model
if schemaModel, ok := cfg.GQLConfig.Schema.Types[model.Name]; ok && schemaModel.IsInputType() {
for _, field := range model.Fields {
// find field in graphql schema
for _, def := range schemaModel.Fields {
if def.Name == field.Name {
// only add 'omitempty' on optional field as defined in graphql schema
if !def.Type.NonNull {
field.Tag = `json:"` + field.Name + `,omitempty"`
}
return build
break
}
}
}
}
}
return build
}
}
func Generate(ctx context.Context, cfg *config.Config, option ...api.Option) error {
var plugins []plugin.Plugin
if cfg.Model.IsDefined() {
p := modelgen.Plugin{
MutateHook: mutateHook,
MutateHook: mutateHook(cfg),
}
plugins = append(plugins, &p)
}

View file

@ -146,12 +146,17 @@ func (d *Decoder) decode() error {
// We've just consumed the current token, which was the key.
// Read the next token, which should be the value.
// If it's of json.RawMessage type, decode the value.
if matchingFieldValue.Type() == reflect.TypeOf(json.RawMessage{}) {
// If it's of json.RawMessage or map type, decode the value.
switch matchingFieldValue.Type() {
case reflect.TypeOf(json.RawMessage{}):
var data json.RawMessage
err = d.jsonDecoder.Decode(&data)
tok = data
} else {
case reflect.TypeOf(map[string]interface{}{}):
var data map[string]interface{}
err = d.jsonDecoder.Decode(&data)
tok = data
default:
tok, err = d.jsonDecoder.Token()
}
@ -183,7 +188,7 @@ func (d *Decoder) decode() error {
}
switch tok := tok.(type) {
case string, json.Number, bool, nil, json.RawMessage:
case string, json.Number, bool, nil, json.RawMessage, map[string]interface{}:
// Value.
for i := range d.vs {

View file

@ -2,6 +2,7 @@ package introspection
import (
"fmt"
"strings"
"github.com/vektah/gqlparser/v2/ast"
)
@ -12,6 +13,7 @@ func ParseIntrospectionQuery(url string, query Query) *ast.SchemaDocument {
Name: "remote",
BuiltIn: false,
}},
typeMap: query.Schema.Types.NameMap(),
}
if url != "" {
@ -22,23 +24,27 @@ func ParseIntrospectionQuery(url string, query Query) *ast.SchemaDocument {
}
type parser struct {
sharedPosition *ast.Position
sharedPosition *ast.Position
typeMap map[string]*FullType
deprecatedDirectiveDefinition *ast.DirectiveDefinition
}
func (p parser) parseIntrospectionQuery(query Query) *ast.SchemaDocument {
var doc ast.SchemaDocument
typeMap := query.Schema.Types.NameMap()
doc.Schema = append(doc.Schema, p.parseSchemaDefinition(query, typeMap))
doc.Schema = append(doc.Schema, p.parseSchemaDefinition(query, p.typeMap))
doc.Position = p.sharedPosition
for _, typeVale := range typeMap {
doc.Definitions = append(doc.Definitions, p.parseTypeSystemDefinition(typeVale))
}
// parseDirectiveDefinition before parseTypeSystemDefinition
// Because SystemDefinition depends on DirectiveDefinition
for _, directiveValue := range query.Schema.Directives {
doc.Directives = append(doc.Directives, p.parseDirectiveDefinition(directiveValue))
}
p.deprecatedDirectiveDefinition = doc.Directives.ForName("deprecated")
for _, typeVale := range p.typeMap {
doc.Definitions = append(doc.Definitions, p.parseTypeSystemDefinition(typeVale))
}
return &doc
}
@ -116,6 +122,7 @@ func (p parser) parseObjectFields(typeVale *FullType) ast.FieldList {
Arguments: args,
Type: typ,
Position: p.sharedPosition,
Directives: p.buildDeprecatedDirective(field),
}
fieldList = append(fieldList, fieldDefinition)
}
@ -164,7 +171,7 @@ func (p parser) parseObjectTypeDefinition(typeVale *FullType) *ast.Definition {
Fields: fieldList,
EnumValues: enums,
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: builtInObject(typeVale),
}
}
@ -182,7 +189,7 @@ func (p parser) parseInterfaceTypeDefinition(typeVale *FullType) *ast.Definition
Interfaces: interfaces,
Fields: fieldList,
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: false,
}
}
@ -200,7 +207,7 @@ func (p parser) parseInputObjectTypeDefinition(typeVale *FullType) *ast.Definiti
Interfaces: interfaces,
Fields: fieldList,
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: false,
}
}
@ -216,7 +223,7 @@ func (p parser) parseUnionTypeDefinition(typeVale *FullType) *ast.Definition {
Name: pointerString(typeVale.Name),
Types: unions,
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: false,
}
}
@ -237,7 +244,7 @@ func (p parser) parseEnumTypeDefinition(typeVale *FullType) *ast.Definition {
Name: pointerString(typeVale.Name),
EnumValues: enums,
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: builtInEnum(typeVale),
}
}
@ -247,7 +254,7 @@ func (p parser) parseScalarTypeExtension(typeVale *FullType) *ast.Definition {
Description: pointerString(typeVale.Description),
Name: pointerString(typeVale.Name),
Position: p.sharedPosition,
BuiltIn: true,
BuiltIn: builtInScalar(typeVale),
}
}
@ -279,7 +286,7 @@ func (p parser) buildInputValue(input *InputValue) *ast.ArgumentDefinition {
if input.DefaultValue != nil {
defaultValue = &ast.Value{
Raw: pointerString(input.DefaultValue),
Kind: ast.Variable,
Kind: p.parseValueKind(typ),
Position: p.sharedPosition,
}
}
@ -317,6 +324,67 @@ func (p parser) getType(typeRef *TypeRef) *ast.Type {
return ast.NamedType(pointerString(typeRef.Name), p.sharedPosition)
}
func (p parser) buildDeprecatedDirective(field *FieldValue) ast.DirectiveList {
var directives ast.DirectiveList
if field.IsDeprecated {
var arguments ast.ArgumentList
if field.DeprecationReason != nil {
arguments = append(arguments, &ast.Argument{
Name: "reason",
Value: &ast.Value{
Raw: *field.DeprecationReason,
Kind: ast.StringValue,
Position: p.sharedPosition,
},
Position: p.sharedPosition,
})
}
deprecatedDirective := &ast.Directive{
Name: "deprecated",
Arguments: arguments,
Position: p.sharedPosition,
ParentDefinition: nil,
Definition: p.deprecatedDirectiveDefinition,
Location: ast.LocationVariableDefinition,
}
directives = append(directives, deprecatedDirective)
}
return directives
}
func (p parser) parseValueKind(typ *ast.Type) ast.ValueKind {
typName := typ.Name()
if fullType, ok := p.typeMap[typName]; ok {
switch fullType.Kind {
case TypeKindEnum:
return ast.EnumValue
case TypeKindInputObject, TypeKindObject, TypeKindUnion, TypeKindInterface:
return ast.ObjectValue
case TypeKindList:
return ast.ListValue
case TypeKindNonNull:
panic(fmt.Sprintf("parseValueKind not match Type Name: %s", typ.Name()))
case TypeKindScalar:
switch typName {
case "Int":
return ast.IntValue
case "Float":
return ast.FloatValue
case "Boolean":
return ast.BooleanValue
case "String", "ID":
return ast.StringValue
default:
return ast.StringValue
}
}
}
panic(fmt.Sprintf("parseValueKind not match Type Name: %s", typ.Name()))
}
func pointerString(s *string) string {
if s == nil {
return ""
@ -324,3 +392,28 @@ func pointerString(s *string) string {
return *s
}
func builtInScalar(fullType *FullType) bool {
name := pointerString(fullType.Name)
if strings.HasPrefix(name, "__") {
return true
}
switch name {
case "String", "Int", "Float", "Boolean", "ID":
return true
}
return false
}
func builtInEnum(fullType *FullType) bool {
name := pointerString(fullType.Name)
return strings.HasPrefix(name, "__")
}
func builtInObject(fullType *FullType) bool {
name := pointerString(fullType.Name)
return strings.HasPrefix(name, "__")
}

View file

@ -31,8 +31,8 @@ func BinaryInt(b bool) uint {
// flags are in hex. trailing 24 zeroes, 26 are after the space
// "DLNA.ORG_OP=" time-seek-range-supp bytes-range-header-supp
func (cf ContentFeatures) String() (ret string) {
//DLNA.ORG_PN=[a-zA-Z0-9_]*
params := make([]string, 0, 2)
// DLNA.ORG_PN=[a-zA-Z0-9_]*
params := make([]string, 0, 3)
if cf.ProfileName != "" {
params = append(params, "DLNA.ORG_PN="+cf.ProfileName)
}
@ -41,6 +41,7 @@ func (cf ContentFeatures) String() (ret string) {
BinaryInt(cf.SupportTimeSeek),
BinaryInt(cf.SupportRange),
BinaryInt(cf.Transcoded)))
params = append(params, "DLNA.ORG_FLAGS=01700000000000000000000000000000")
return strings.Join(params, ";")
}

View file

@ -5,7 +5,6 @@ import (
"bytes"
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
@ -14,6 +13,8 @@ import (
"strings"
"time"
"github.com/anacrolix/log"
"golang.org/x/net/ipv4"
)
@ -24,15 +25,13 @@ const (
byebyeNTS = "ssdp:byebye"
)
var (
NetAddr *net.UDPAddr
)
var NetAddr *net.UDPAddr
func init() {
var err error
NetAddr, err = net.ResolveUDPAddr("udp4", AddrString)
if err != nil {
log.Panicf("Could not resolve %s: %s", AddrString, err)
log.Printf("Could not resolve %s: %s", AddrString, err)
}
}
@ -85,10 +84,12 @@ type Server struct {
Server string
Services []string
Devices []string
IPFilter func(net.IP) bool
Location func(net.IP) string
UUID string
NotifyInterval time.Duration
closed chan struct{}
Logger log.Logger
}
func makeConn(ifi net.Interface) (ret *net.UDPConn, err error) {
@ -98,17 +99,23 @@ func makeConn(ifi net.Interface) (ret *net.UDPConn, err error) {
}
p := ipv4.NewPacketConn(ret)
if err := p.SetMulticastTTL(2); err != nil {
log.Println(err)
}
if err := p.SetMulticastLoopback(true); err != nil {
log.Println(err)
log.Print(err)
}
// if err := p.SetMulticastLoopback(true); err != nil {
// log.Println(err)
// }
return
}
func (me *Server) serve() {
for {
b := make([]byte, me.Interface.MTU)
size := me.Interface.MTU
if size > 65536 {
size = 65536
} else if size <= 0 { // fix for windows with mtu 4gb
size = 65536
}
b := make([]byte, size)
n, addr, err := me.conn.ReadFromUDP(b)
select {
case <-me.closed:
@ -116,7 +123,7 @@ func (me *Server) serve() {
default:
}
if err != nil {
log.Printf("error reading from UDP socket: %s", err)
me.Logger.Printf("error reading from UDP socket: %s", err)
break
}
go me.handle(b[:n], addr)
@ -126,6 +133,9 @@ func (me *Server) serve() {
func (me *Server) Init() (err error) {
me.closed = make(chan struct{})
me.conn, err = makeConn(me.Interface)
if me.IPFilter == nil {
me.IPFilter = func(net.IP) bool { return true }
}
return
}
@ -152,6 +162,14 @@ func (me *Server) Serve() (err error) {
}
panic(fmt.Sprint("unexpected addr type:", addr))
}()
if !me.IPFilter(ip) {
continue
}
if ip.IsLinkLocalUnicast() {
// These addresses seem to confuse VLC. Possibly there's supposed to be a zone
// included in the address, but I don't see one.
continue
}
extraHdrs := [][2]string{
{"CACHE-CONTROL", fmt.Sprintf("max-age=%d", 5*me.NotifyInterval/2/time.Second)},
{"LOCATION", me.Location(ip)},
@ -194,9 +212,9 @@ func (me *Server) makeNotifyMessage(target, nts string, extraHdrs [][2]string) [
func (me *Server) send(buf []byte, addr *net.UDPAddr) {
if n, err := me.conn.WriteToUDP(buf, addr); err != nil {
log.Printf("error writing to UDP socket: %s", err)
me.Logger.Printf("error writing to UDP socket: %s", err)
} else if n != len(buf) {
log.Printf("short write: %d/%d bytes", n, len(buf))
me.Logger.Printf("short write: %d/%d bytes", n, len(buf))
}
}
@ -212,7 +230,7 @@ func (me *Server) delayedSend(delay time.Duration, buf []byte, addr *net.UDPAddr
func (me *Server) log(args ...interface{}) {
args = append([]interface{}{me.Interface.Name + ":"}, args...)
log.Print(args...)
me.Logger.Print(args...)
}
func (me *Server) sendByeBye() {
@ -244,7 +262,7 @@ func (me *Server) allTypes() (ret []string) {
func (me *Server) handle(buf []byte, sender *net.UDPAddr) {
req, err := ReadRequest(bufio.NewReader(bytes.NewReader(buf)))
if err != nil {
log.Println(err)
me.Logger.Println(err)
return
}
if req.Method != "M-SEARCH" || req.Header.Get("man") != `"ssdp:discover"` {
@ -255,7 +273,7 @@ func (me *Server) handle(buf []byte, sender *net.UDPAddr) {
mxHeader := req.Header.Get("mx")
i, err := strconv.ParseUint(mxHeader, 0, 0)
if err != nil {
log.Printf("Invalid mx header %q: %s", mxHeader, err)
me.Logger.Printf("Invalid mx header %q: %s", mxHeader, err)
return
}
mx = uint(i)

View file

@ -5,10 +5,11 @@ import (
"encoding/xml"
"fmt"
"io"
"log"
"net/url"
"regexp"
"time"
"github.com/anacrolix/log"
)
// TODO: Why use namespace prefixes in PropertySet et al? Because the spec

View file

@ -4,21 +4,23 @@ import (
"encoding/xml"
"errors"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"github.com/anacrolix/log"
)
var serviceURNRegexp *regexp.Regexp = regexp.MustCompile(`^urn:schemas-upnp-org:service:(\w+):(\d+)$`)
var serviceURNRegexp *regexp.Regexp = regexp.MustCompile(`^urn:(.*):service:(\w+):(\d+)$`)
type ServiceURN struct {
Auth string
Type string
Version uint64
}
func (me ServiceURN) String() string {
return fmt.Sprintf("urn:schemas-upnp-org:service:%s:%d", me.Type, me.Version)
return fmt.Sprintf("urn:%s:service:%s:%d", me.Auth, me.Type, me.Version)
}
func ParseServiceType(s string) (ret ServiceURN, err error) {
@ -27,11 +29,12 @@ func ParseServiceType(s string) (ret ServiceURN, err error) {
err = errors.New(s)
return
}
if len(matches) != 3 {
log.Panicf("Invalid serviceURNRegexp ?")
if len(matches) != 4 {
log.Panicf("Invalid serviceURNRegexp?")
}
ret.Type = matches[1]
ret.Version, err = strconv.ParseUint(matches[2], 0, 0)
ret.Auth = matches[1]
ret.Type = matches[2]
ret.Version, err = strconv.ParseUint(matches[3], 0, 0)
return
}
@ -80,17 +83,21 @@ type Service struct {
}
type Device struct {
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
ModelName string `xml:"modelName"`
UDN string
IconList []Icon `xml:"iconList>icon"`
ServiceList []Service `xml:"serviceList>service"`
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
ModelName string `xml:"modelName"`
UDN string
VendorXML string `xml:",innerxml"`
IconList []Icon `xml:"iconList>icon"`
ServiceList []Service `xml:"serviceList>service"`
PresentationURL string `xml:"presentationURL,omitempty"`
}
type DeviceDesc struct {
XMLName xml.Name `xml:"urn:schemas-upnp-org:device-1-0 root"`
NSDLNA string `xml:"xmlns:dlna,attr"`
NSSEC string `xml:"xmlns:sec,attr"`
SpecVersion SpecVersion `xml:"specVersion"`
Device Device `xml:"device"`
}

View file

@ -2,12 +2,15 @@ package upnpav
import (
"encoding/xml"
"time"
)
const (
// NoSuchObjectErrorCode : The specified ObjectID is invalid.
NoSuchObjectErrorCode = 701
)
// Resource description
type Resource struct {
XMLName xml.Name `xml:"res"`
ProtocolInfo string `xml:"protocolInfo,attr"`
@ -18,28 +21,44 @@ type Resource struct {
Resolution string `xml:"resolution,attr,omitempty"`
}
// Container description
type Container struct {
Object
XMLName xml.Name `xml:"container"`
ChildCount int `xml:"childCount,attr"`
}
// Item description
type Item struct {
Object
XMLName xml.Name `xml:"item"`
Res []Resource
XMLName xml.Name `xml:"item"`
Res []Resource
InnerXML string `xml:",innerxml"`
}
// Object description
type Object struct {
ID string `xml:"id,attr"`
ParentID string `xml:"parentID,attr"`
Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable
Class string `xml:"upnp:class"`
Icon string `xml:"upnp:icon,omitempty"`
Title string `xml:"dc:title"`
Artist string `xml:"upnp:artist,omitempty"`
Album string `xml:"upnp:album,omitempty"`
Genre string `xml:"upnp:genre,omitempty"`
AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"`
Searchable int `xml:"searchable,attr"`
ID string `xml:"id,attr"`
ParentID string `xml:"parentID,attr"`
Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable
Title string `xml:"dc:title"`
Class string `xml:"upnp:class"`
Icon string `xml:"upnp:icon,omitempty"`
Date Timestamp `xml:"dc:date"`
Artist string `xml:"upnp:artist,omitempty"`
Album string `xml:"upnp:album,omitempty"`
Genre string `xml:"upnp:genre,omitempty"`
AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"`
Searchable int `xml:"searchable,attr"`
SearchXML string `xml:",innerxml"`
}
// Timestamp wraps time.Time for formatting purposes
type Timestamp struct {
time.Time
}
// MarshalXML formats the Timestamp per DIDL-Lite spec
func (t Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(t.Format("2006-01-02"), start)
}

373
vendor/github.com/anacrolix/log/LICENSE generated vendored Normal file
View file

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

15
vendor/github.com/anacrolix/log/context.go generated vendored Normal file
View file

@ -0,0 +1,15 @@
package log
import (
"context"
)
var loggerContextKey interface{} = (*Logger)(nil)
func ContextWithLogger(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, loggerContextKey, logger)
}
func ContextLogger(ctx context.Context) Logger {
return ctx.Value(loggerContextKey).(Logger)
}

8
vendor/github.com/anacrolix/log/doc.go generated vendored Normal file
View file

@ -0,0 +1,8 @@
// Package log implements a std log compatible logging system that draws some inspiration from the
// Python standard library [logging module](https://docs.python.org/3/library/logging.html). It
// supports multiple handlers, log levels, zero-allocation, scopes, custom formatting, and
// environment and runtime configuration.
//
// When not used to replace std log, the import should use the package name "analog" as in:
// import analog "github.com/anacrolix/log".
package log

41
vendor/github.com/anacrolix/log/global.go generated vendored Normal file
View file

@ -0,0 +1,41 @@
package log
import (
"fmt"
"io/ioutil"
"os"
)
var (
DefaultHandler = StreamHandler{
W: os.Stderr,
Fmt: LineFormatter,
}
Default Logger // Inited after GO_LOG is parsed.
DiscardHandler = StreamHandler{
W: ioutil.Discard,
Fmt: func(Record) []byte { return nil },
}
)
func Levelf(level Level, format string, a ...interface{}) {
Default.LazyLog(level, func() Msg {
return Fmsg(format, a...).Skip(1)
})
}
func Printf(format string, a ...interface{}) {
Default.Log(Fmsg(format, a...).Skip(1))
}
// Prints the arguments to the Default Logger.
func Print(a ...interface{}) {
// TODO: There's no "Print" equivalent constructor for a Msg, and I don't know what I'd call it.
Str(fmt.Sprint(a...)).Skip(1).Log(Default)
}
func Println(a ...interface{}) {
Default.LazyLogDefaultLevel(func() Msg {
return Str(fmt.Sprintln(a...)).Skip(1)
})
}

11
vendor/github.com/anacrolix/log/handler.go generated vendored Normal file
View file

@ -0,0 +1,11 @@
package log
type Handler interface {
Handle(Record)
}
type Record struct {
Msg
Level Level
Names []string
}

22
vendor/github.com/anacrolix/log/init.go generated vendored Normal file
View file

@ -0,0 +1,22 @@
package log
import (
"os"
)
func init() {
var err error
rules, err = parseEnvRules()
if err != nil {
panic(err)
}
Default = loggerCore{
nonZero: true,
filterLevel: Error,
Handlers: []Handler{DefaultHandler},
}.withFilterLevelFromRules()
Default.defaultLevel, _, err = levelFromString(os.Getenv("GO_LOG_DEFAULT_LEVEL"))
if err != nil {
panic(err)
}
}

75
vendor/github.com/anacrolix/log/level.go generated vendored Normal file
View file

@ -0,0 +1,75 @@
package log
import (
"encoding"
"fmt"
"strconv"
"strings"
)
type Level struct {
rank int
}
var (
Never = Level{-1} // A message at this level should never be logged.
NotSet = Level{0}
Debug = Level{1}
Info = Level{2}
Warning = Level{3}
Error = Level{4}
Critical = Level{5}
disabled = Level{6} // It shouldn't be possible to define a message at this level.
)
func (l Level) isNotSet() bool {
return l.rank == 0
}
func (l Level) LogString() string {
switch l.rank {
case NotSet.rank:
return "NIL"
case Debug.rank:
return "DBG"
case Info.rank:
return "INF"
case Warning.rank:
return "WRN"
case Error.rank:
return "ERR"
case Critical.rank:
return "CRT"
default:
return strconv.FormatInt(int64(l.rank), 10)
}
}
func (l Level) LessThan(r Level) bool {
if l.rank == NotSet.rank {
return false
}
return l.rank < r.rank
}
var _ encoding.TextUnmarshaler = (*Level)(nil)
func (l *Level) UnmarshalText(text []byte) error {
switch strings.ToLower(string(text)) {
case "nil", "notset", "unset", "all", "*":
*l = NotSet
case "dbg", "debug":
*l = Debug
case "inf", "info":
*l = Info
case "wrn", "warning", "warn":
*l = Warning
case "err", "error":
*l = Error
case "crt", "critical", "crit":
*l = Critical
default:
return fmt.Errorf("unknown log level: %q", text)
}
return nil
}

13
vendor/github.com/anacrolix/log/log.go generated vendored Normal file
View file

@ -0,0 +1,13 @@
package log
import (
"runtime"
)
func Call() Msg {
var pc [1]uintptr
n := runtime.Callers(4, pc[:])
fs := runtime.CallersFrames(pc[:n])
f, _ := fs.Next()
return Fmsg("called %q", f.Function)
}

102
vendor/github.com/anacrolix/log/logger-core.go generated vendored Normal file
View file

@ -0,0 +1,102 @@
package log
import (
"fmt"
)
// loggerCore is the essential part of Logger.
type loggerCore struct {
nonZero bool
names []string
values []interface{}
defaultLevel Level
filterLevel Level
msgMaps []func(Msg) Msg
Handlers []Handler
}
func (l loggerCore) asLogger() Logger {
return Logger{l}
}
// Returns a logger that adds the given values to logged messages.
func (l loggerCore) WithValues(v ...interface{}) Logger {
l.values = append(l.values, v...)
return l.asLogger()
}
// Returns a logger that for a given message propagates the result of `f` instead.
func (l loggerCore) WithMap(f func(m Msg) Msg) Logger {
l.msgMaps = append(l.msgMaps, f)
return l.asLogger()
}
func (l loggerCore) WithDefaultLevel(level Level) Logger {
l.defaultLevel = level
return l.asLogger()
}
func (l loggerCore) FilterLevel(minLevel Level) Logger {
if _, ok := levelFromRules(l.names); !ok {
l.filterLevel = minLevel
}
return l.asLogger()
}
func (l loggerCore) IsZero() bool {
return !l.nonZero
}
func (l loggerCore) IsEnabledFor(level Level) bool {
return !level.LessThan(l.filterLevel)
}
func (l loggerCore) LazyLog(level Level, f func() Msg) {
l.lazyLog(level, 1, f)
}
func (l loggerCore) LazyLogDefaultLevel(f func() Msg) {
l.lazyLog(l.defaultLevel, 1, f)
}
func (l loggerCore) lazyLog(level Level, skip int, f func() Msg) {
if !l.IsEnabledFor(level) {
// have a big sook
//internalLogger.Levelf(Debug, "skipped logging %v for %q", level, l.names)
return
}
r := f().Skip(skip + 1)
for i := len(l.msgMaps) - 1; i >= 0; i-- {
r = l.msgMaps[i](r)
}
l.handle(level, r)
}
func (l loggerCore) handle(level Level, m Msg) {
r := Record{
Msg: m.Skip(1),
Level: level,
Names: l.names,
}
if !l.nonZero {
panic(fmt.Sprintf("Logger uninitialized. names=%q", l.names))
}
for _, h := range l.Handlers {
h.Handle(r)
}
}
func (l loggerCore) WithNames(names ...string) Logger {
// Avoid sharing after appending. This might not be enough because some formatters might add
// more elements concurrently, or names could be empty.
l.names = append(l.names[:len(l.names):len(l.names)], names...)
return l.withFilterLevelFromRules()
}
func (l loggerCore) withFilterLevelFromRules() Logger {
level, ok := levelFromRules(l.names)
if ok {
l.filterLevel = level
}
return l.asLogger()
}

72
vendor/github.com/anacrolix/log/logger.go generated vendored Normal file
View file

@ -0,0 +1,72 @@
package log
import (
"fmt"
)
// Logger handles logging in a specific context. It includes a bunch of helpers and compatibility
// over the loggerCore.
type Logger struct {
loggerCore
}
func (l Logger) WithText(f func(Msg) string) Logger {
l.msgMaps = append(l.msgMaps, func(msg Msg) Msg {
return msg.WithText(f)
})
return l
}
// Helper for compatibility with "log".Logger.
func (l Logger) Printf(format string, a ...interface{}) {
l.LazyLog(l.defaultLevel, func() Msg {
return Fmsg(format, a...).Skip(1)
})
}
func (l Logger) Log(m Msg) {
l.LogLevel(l.defaultLevel, m.Skip(1))
}
func (l Logger) LogLevel(level Level, m Msg) {
l.LazyLog(level, func() Msg {
return m.Skip(1)
})
}
// Helper for compatibility with "log".Logger.
func (l Logger) Print(v ...interface{}) {
l.LazyLog(l.defaultLevel, func() Msg {
return Str(fmt.Sprint(v...)).Skip(1)
})
}
func (l Logger) WithContextValue(v interface{}) Logger {
return l.WithText(func(m Msg) string {
return fmt.Sprintf("%v: %v", v, m)
})
}
func (l Logger) WithContextText(s string) Logger {
return l.WithText(func(m Msg) string {
return s + ": " + m.Text()
})
}
func (l Logger) SkipCallers(skip int) Logger {
return l.WithMap(func(m Msg) Msg {
return m.Skip(skip)
})
}
func (l Logger) Levelf(level Level, format string, a ...interface{}) {
l.LazyLog(level, func() Msg {
return Fmsg(format, a...).Skip(1)
})
}
func (l Logger) Println(a ...interface{}) {
l.LazyLogDefaultLevel(func() Msg {
return Str(fmt.Sprintln(a...)).Skip(1)
})
}

153
vendor/github.com/anacrolix/log/msg.go generated vendored Normal file
View file

@ -0,0 +1,153 @@
package log
import (
"fmt"
)
type Msg struct {
MsgImpl
}
func (me Msg) String() string {
return me.Text()
}
func newMsg(text func() string) Msg {
return Msg{rootMsgImpl{text}}
}
func Fmsg(format string, a ...interface{}) Msg {
return newMsg(func() string { return fmt.Sprintf(format, a...) })
}
var Fstr = Fmsg
func Str(s string) (m Msg) {
return newMsg(func() string { return s })
}
type msgSkipCaller struct {
MsgImpl
skip int
}
func (me msgSkipCaller) Callers(skip int, pc []uintptr) int {
return me.MsgImpl.Callers(skip+1+me.skip, pc)
}
func (m Msg) Skip(skip int) Msg {
return Msg{msgSkipCaller{m.MsgImpl, skip}}
}
type item struct {
key, value interface{}
}
// rename sink
func (m Msg) Log(l Logger) Msg {
l.Log(m.Skip(1))
return m
}
func (m Msg) LogLevel(level Level, l Logger) Msg {
l.LogLevel(level, m.Skip(1))
return m
}
type msgWithValues struct {
MsgImpl
values []interface{}
}
func (me msgWithValues) Values(cb valueIterCallback) {
for _, v := range me.values {
if !cb(v) {
return
}
}
me.MsgImpl.Values(cb)
}
// TODO: What ordering should be applied to the values here, per MsgImpl.Values. For now they're
// traversed in order of the slice.
func (m Msg) WithValues(v ...interface{}) Msg {
return Msg{msgWithValues{m.MsgImpl, v}}
}
func (m Msg) AddValues(v ...interface{}) Msg {
return m.WithValues(v...)
}
func (m Msg) With(key, value interface{}) Msg {
return m.WithValues(item{key, value})
}
func (m Msg) Add(key, value interface{}) Msg {
return m.With(key, value)
}
//func (m Msg) SetLevel(level Level) Msg {
// return m.With(levelKey, level)
//}
//func (m Msg) GetByKey(key interface{}) (value interface{}, ok bool) {
// m.Values(func(i interface{}) bool {
// if keyValue, isKeyValue := i.(item); isKeyValue && keyValue.key == key {
// value = keyValue.value
// ok = true
// }
// return !ok
// })
// return
//}
//func (m Msg) GetLevel() (l Level, ok bool) {
// v, ok := m.GetByKey(levelKey)
// if ok {
// l = v.(Level)
// }
// return
//}
func (m Msg) HasValue(v interface{}) (has bool) {
m.Values(func(i interface{}) bool {
if i == v {
has = true
}
return !has
})
return
}
func (m Msg) AddValue(v interface{}) Msg {
return m.AddValues(v)
}
//func (m Msg) GetValueByType(p interface{}) bool {
// pve := reflect.ValueOf(p).Elem()
// t := pve.Type()
// return !iter.All(func(i interface{}) bool {
// iv := reflect.ValueOf(i)
// if iv.Type() == t {
// pve.Set(iv)
// return false
// }
// return true
// }, m.Values)
//}
func (m Msg) WithText(f func(Msg) string) Msg {
return Msg{msgWithText{
m,
func() string { return f(m) },
}}
}
type msgWithText struct {
MsgImpl
text func() string
}
func (me msgWithText) Text() string {
return me.text()
}

33
vendor/github.com/anacrolix/log/msgimpl.go generated vendored Normal file
View file

@ -0,0 +1,33 @@
package log
import (
"runtime"
)
type valueIterCallback func(value interface{}) (more bool)
// The minimal interface required for the Msg helper/wrapper to operate on.
type MsgImpl interface {
// Returns the message text. Allows for lazy evaluation/prefixing etc.
Text() string
// Sets the program counters in pc. Having it in the interface may allow us to cache/freeze them
// for serialization etc.
Callers(skip int, pc []uintptr) int
// Iterates over the values as added LIFO.
Values(callback valueIterCallback)
}
// maybe implement finalizer to ensure msgs are sunk
type rootMsgImpl struct {
text func() string
}
func (m rootMsgImpl) Text() string {
return m.text()
}
func (m rootMsgImpl) Callers(skip int, pc []uintptr) int {
return runtime.Callers(skip+2, pc)
}
func (m rootMsgImpl) Values(valueIterCallback) {}

100
vendor/github.com/anacrolix/log/rules.go generated vendored Normal file
View file

@ -0,0 +1,100 @@
package log
import (
"fmt"
"os"
"strings"
)
var rules []Rule
type Rule func(names []string) (level Level, matched bool)
func alwaysLevel(level Level) Rule {
return func(names []string) (Level, bool) {
return level, true
}
}
func stringSliceContains(s string, ss []string) bool {
for _, sss := range ss {
if s == sss {
return true
}
}
return false
}
func containsAllNames(all []string, level Level) Rule {
return func(names []string) (_ Level, matched bool) {
for _, s := range all {
//log.Println(s, all, names)
if !stringSliceContains(s, names) {
return
}
}
return level, true
}
}
func parseRuleString(s string) (_ Rule, ok bool, _ error) {
if s == "" {
return
}
ss := strings.SplitN(s, "=", 2)
level := NotSet
var names []string
if ss[0] != "*" {
names = []string{ss[0]}
}
if len(ss) > 1 {
var ok bool
var err error
level, ok, err = levelFromString(ss[1])
if !ok {
// blah= means disable the name, but just blah means to always include it
level = disabled
}
if err != nil {
return nil, false, fmt.Errorf("parsing level %q: %w", ss[1], err)
}
}
return containsAllNames(names, level), true, nil
}
func parseEnvRules() (rules []Rule, err error) {
rulesStr := os.Getenv("GO_LOG")
ruleStrs := strings.Split(rulesStr, ",")
for _, ruleStr := range ruleStrs {
rule, ok, err := parseRuleString(ruleStr)
if err != nil {
return nil, fmt.Errorf("parsing rule %q: %w", ruleStr, err)
}
if !ok {
continue
}
rules = append(rules, rule)
}
return
}
func levelFromString(s string) (level Level, ok bool, err error) {
if s == "" {
return
}
ok = true
err = level.UnmarshalText([]byte(s))
return
}
func levelFromRules(names []string) (_ Level, ok bool) {
// Later rules take precedence
for i := len(rules) - 1; i >= 0; i-- {
r := rules[i]
level, ok := r(names)
if ok {
return level, true
}
}
return
}

12
vendor/github.com/anacrolix/log/stdlog.go generated vendored Normal file
View file

@ -0,0 +1,12 @@
package log
import (
"log"
)
// Deprecated: Logging shouldn't include control flow.
var (
Panicf = log.Panicf
Fatalf = log.Fatalf
Fatal = log.Fatal
)

59
vendor/github.com/anacrolix/log/stream-handler.go generated vendored Normal file
View file

@ -0,0 +1,59 @@
package log
import (
"fmt"
"io"
"runtime"
"time"
)
type StreamHandler struct {
W io.Writer
Fmt ByteFormatter
}
func (me StreamHandler) Handle(r Record) {
r.Msg = r.Skip(1)
me.W.Write(me.Fmt(r))
}
type ByteFormatter func(Record) []byte
func LineFormatter(msg Record) []byte {
names := msg.Names
if true || len(names) == 0 {
var pc [1]uintptr
msg.Callers(1, pc[:])
names = pcNames(pc[0], names)
}
ret := []byte(fmt.Sprintf(
"%s %s %s: %s",
time.Now().Format("2006-01-02T15:04:05-0700"),
msg.Level.LogString(),
names,
msg.Text(),
))
if ret[len(ret)-1] != '\n' {
ret = append(ret, '\n')
}
return ret
}
func pcNames(pc uintptr, names []string) []string {
if pc == 0 {
panic(pc)
}
funcName, file, line := func() (string, string, int) {
if false {
// This seems to result in one less allocation, but doesn't handle inlining?
func_ := runtime.FuncForPC(pc)
file, line := func_.FileLine(pc)
return func_.Name(), file, line
} else {
f, _ := runtime.CallersFrames([]uintptr{pc}).Next()
return f.Function, f.File, f.Line
}
}()
_ = file
return append(names, fmt.Sprintf("%s:%v", funcName, line))
}

View file

@ -57,6 +57,7 @@ Supported Features
- `(a, b, c)` : Evaluates each of its operands and concatenates the resulting sequences, in order, into a single result sequence
- `(a/b)` : Selects all matches nodes as grouping set.
#### Node Axes
@ -158,16 +159,4 @@ Supported Features
`system-property()`| ✗ |
`translate()`| ✓ |
`true()`| ✓ |
`unparsed-entity-url()` | ✗ |
Changelogs
===
2019-03-19
- optimize XPath `|` operation performance. [#33](https://github.com/antchfx/xpath/issues/33). Tips: suggest split into multiple subquery if you have a lot of `|` operations.
2019-01-29
- improvement `normalize-space` function. [#32](https://github.com/antchfx/xpath/issues/32)
2018-12-07
- supports XPath 2.0 Sequence expressions. [#30](https://github.com/antchfx/xpath/pull/30) by [@minherz](https://github.com/minherz).
`unparsed-entity-url()` | ✗ |

View file

@ -42,7 +42,7 @@ func axisPredicate(root *axisNode) func(NodeNavigator) bool {
}
nametest := root.LocalName != "" || root.Prefix != ""
predicate := func(n NodeNavigator) bool {
if typ == n.NodeType() || typ == allNode || typ == TextNode {
if typ == n.NodeType() || typ == allNode {
if nametest {
if root.LocalName == n.LocalName() && root.Prefix == n.Prefix() {
return true

View file

@ -122,17 +122,19 @@ func asNumber(t iterator, o interface{}) float64 {
return typ
case string:
v, err := strconv.ParseFloat(typ, 64)
if err != nil {
panic(errors.New("ceiling() function argument type must be a node-set or number"))
if err == nil {
return v
}
return v
}
return 0
return math.NaN()
}
// ceilingFunc is a XPath Node Set functions ceiling(node-set).
func ceilingFunc(q query, t iterator) interface{} {
val := asNumber(t, functionArgs(q).Evaluate(t))
// if math.IsNaN(val) {
// panic(errors.New("ceiling() function argument type must be a valid number"))
// }
return math.Ceil(val)
}

Some files were not shown because too many files have changed in this diff Show more