diff --git a/.goreleaser.yml b/.goreleaser.yml index fe421ac..f81c178 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -11,27 +11,19 @@ builds: gobinary: garble env: - CGO_ENABLED=0 - ldflags: - - -X 'github.com/graph-guard/ggproxy/lvs.PublicKey={{ .Env.PUB_KEY }}' goos: - linux - darwin goarch: - - 386 - - amd64 - - arm - - arm64 + - "386" + - "amd64" + - "arm" + - "arm64" archives: - id: ggproxy name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}" replacements: - 386: i386 - files: - - src: assets/etc/ - info: - owner: root - group: ggproxy - mode: 0664 + "386": "i386" checksum: name_template: "{{ .ProjectName }}-{{ .Tag }}-checksums.txt" snapshot: @@ -39,7 +31,7 @@ snapshot: release: draft: false replace_existing_draft: true - prerelease: true + prerelease: "true" # Don't remove the quotes, it's supposed to be a string, not a boolean. mode: append header: | **[CHANGELOG.md](https://github.com/graph-guard/ggproxy/blob/ci/CHANGELOG.md#{{ .Env.COMPACT_TAG }})** diff --git a/CHANGELOG.md b/CHANGELOG.md index 584da6e..11d66ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ ### Fix * Fix comparison of byte slices -* Fix lvs failure on empty license * Reset segmented array index counter * Accept enum values * Fix broken pipe handling @@ -77,8 +76,6 @@ * Add 'null' support * Add processing of enums * Prepare to Beta release -* Use LVS validation and rename 'licence' to 'license' -* Add licence key environment variable * Add support for basic auth in API server * Add service and template statistics * Add ggproxy GraphQL API @@ -107,7 +104,6 @@ ### Refactor -* Refactor LVS ([#4](https://github.com/graph-guard/ggproxy/issues/4)) * Get rid of the matcher interface * Simplify code diff --git a/assets/etc/ggproxy/all-services/a.yml b/assets/etc/ggproxy/all-services/a.yml deleted file mode 100644 index f7b4d32..0000000 --- a/assets/etc/ggproxy/all-services/a.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Source URL path -path: "/path" - -# Destination URL (where to proxy requests to) -forward-url: "http://localhost:8080/path" - -# false for forwarding the original request, -# true for the reduced version. -forward-reduced: true - -all-templates: ../all-templates/a -enabled-templates: ../enabled-templates/a diff --git a/assets/etc/ggproxy/all-templates/a/example_template.gqt b/assets/etc/ggproxy/all-templates/a/example_template.gqt deleted file mode 100644 index cd16428..0000000 --- a/assets/etc/ggproxy/all-templates/a/example_template.gqt +++ /dev/null @@ -1,20 +0,0 @@ ---- -# The template's display name -name: "Example Template" - -# Arbitrary tags -tags: - - query - - products - - related_products ---- -query { - products(limit: val <= 10, after: any) { - id - name - relatedProducts(type: val = "tea" || val = "juice") { - id - name - } - } -} diff --git a/assets/etc/ggproxy/config.yml b/assets/etc/ggproxy/config.yml deleted file mode 100644 index 60060dc..0000000 --- a/assets/etc/ggproxy/config.yml +++ /dev/null @@ -1,25 +0,0 @@ -proxy: - # Address and port of the proxy server. - host: localhost:8000 - # Optional, enables HTTPS. - #tls: - # Certificate file path. - #cert-file: proxy.cert - # Private key file path. - #key-file: proxy.key - # Optional, in bytes, default: 4MiB. - #max-request-body-size: 1024 - -# Optional, enables API server. -api: - # Address and port of the API server. - host: localhost:3000 - # Optional, enables HTTPS. - #tls: - # Certificate file path. - #cert-file: api.cert - # Private key file path. - #key-file: api.key - -all-services: ./all-services -enabled-services: ./enabled-services diff --git a/assets/etc/ggproxy/enabled-services/a.yml b/assets/etc/ggproxy/enabled-services/a.yml deleted file mode 120000 index 1fcb412..0000000 --- a/assets/etc/ggproxy/enabled-services/a.yml +++ /dev/null @@ -1 +0,0 @@ -../all-services/a.yml \ No newline at end of file diff --git a/assets/etc/ggproxy/enabled-templates/a/example_template.gqt b/assets/etc/ggproxy/enabled-templates/a/example_template.gqt deleted file mode 120000 index 007c8d3..0000000 --- a/assets/etc/ggproxy/enabled-templates/a/example_template.gqt +++ /dev/null @@ -1 +0,0 @@ -../../all-templates/a/example_template.gqt \ No newline at end of file diff --git a/cmd/ggproxy/main.go b/cmd/ggproxy/main.go index e013ff7..d5c7e55 100644 --- a/cmd/ggproxy/main.go +++ b/cmd/ggproxy/main.go @@ -4,20 +4,12 @@ import ( "fmt" "os" - "github.com/graph-guard/ggproxy/cli" - "github.com/graph-guard/ggproxy/lvs" + "github.com/graph-guard/ggproxy/pkg/cli" ) func main() { w := os.Stdout - switch c := cli.Parse( - w, - os.Args, - func(licenseToken string) error { - _, err := lvs.ValidateLicenseToken(licenseToken) - return err - }, - ).(type) { + switch c := cli.Parse(w, os.Args).(type) { case cli.CommandServe: serve(w, c) case cli.CommandReload: diff --git a/cmd/ggproxy/read_config.go b/cmd/ggproxy/read_config.go index 1aeffc5..daa1b7a 100644 --- a/cmd/ggproxy/read_config.go +++ b/cmd/ggproxy/read_config.go @@ -3,15 +3,18 @@ package main import ( "fmt" "io" + "os" + "path/filepath" - "github.com/graph-guard/ggproxy/config" + "github.com/graph-guard/ggproxy/pkg/config" ) func ReadConfig( w io.Writer, configPath string, ) *config.Config { - conf, err := config.New(configPath) + basePath, fileName := basePathAndFileName(configPath) + conf, err := config.Read(os.DirFS(basePath), basePath, fileName) if err != nil { fmt.Fprintf(w, "reading config: %s\n", err) return nil @@ -34,3 +37,7 @@ func ReadConfig( return conf } + +func basePathAndFileName(path string) (basePath, fileName string) { + return filepath.Base(path), path[:len(path)-len(filepath.Base(path))] +} diff --git a/cmd/ggproxy/reload_unix.go b/cmd/ggproxy/reload_unix.go index 63aa935..3bc0ae2 100644 --- a/cmd/ggproxy/reload_unix.go +++ b/cmd/ggproxy/reload_unix.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" ) func reload(w io.Writer, c cli.CommandReload) { diff --git a/cmd/ggproxy/reload_windows.go b/cmd/ggproxy/reload_windows.go index abd1fdf..e7385ad 100644 --- a/cmd/ggproxy/reload_windows.go +++ b/cmd/ggproxy/reload_windows.go @@ -3,7 +3,7 @@ package main import ( "io" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" ) func reload(w io.Writer, c cli.CommandReload) { diff --git a/cmd/ggproxy/serve_unix.go b/cmd/ggproxy/serve_unix.go index dfb93e1..9196dbd 100644 --- a/cmd/ggproxy/serve_unix.go +++ b/cmd/ggproxy/serve_unix.go @@ -5,8 +5,8 @@ import ( "sync" "time" - "github.com/graph-guard/ggproxy/cli" - "github.com/graph-guard/ggproxy/server" + "github.com/graph-guard/ggproxy/pkg/cli" + "github.com/graph-guard/ggproxy/pkg/server" "github.com/phuslu/log" ) diff --git a/cmd/ggproxy/serve_windows.go b/cmd/ggproxy/serve_windows.go index 63ee5a2..6a99db0 100644 --- a/cmd/ggproxy/serve_windows.go +++ b/cmd/ggproxy/serve_windows.go @@ -3,7 +3,7 @@ package main import ( "io" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" ) func serve(w io.Writer, c cli.CommandServe) { diff --git a/cmd/ggproxy/stop_unix.go b/cmd/ggproxy/stop_unix.go index f2ff351..8ba793d 100644 --- a/cmd/ggproxy/stop_unix.go +++ b/cmd/ggproxy/stop_unix.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" ) func stop(w io.Writer, c cli.CommandStop) { diff --git a/cmd/ggproxy/stop_windows.go b/cmd/ggproxy/stop_windows.go index 49d2a62..cc5d6df 100644 --- a/cmd/ggproxy/stop_windows.go +++ b/cmd/ggproxy/stop_windows.go @@ -3,7 +3,7 @@ package main import ( "io" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" ) func stop(w io.Writer, c cli.CommandStop) { diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 89ac979..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,913 +0,0 @@ -package config_test - -import ( - "crypto/md5" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "testing" - "testing/fstest" - - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/gqt" - "github.com/stretchr/testify/require" -) - -type TestOK struct { - Path string - Expect *config.Config -} - -type TestError struct { - Filesystem fstest.MapFS - Check func(*testing.T, error) -} - -var ServerConfigFileName = "config.yml" - -func TestReadConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - for _, td := range []TestOK{ - { - Path: filepath.Join(path, ServerConfigFileName), - Expect: conf, - }, - } { - t.Run("", func(t *testing.T) { - c, err := config.New(td.Path) - require.NoError(t, err) - require.True(t, td.Expect.Equal(c)) - }) - } - }) -} - -func TestReadConfigDefaultMaxReqBodySize(t *testing.T) { - validFS(func(path string, conf *config.Config) { - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:443`, - ` tls:`, - ` cert-file: proxy.cert`, - ` key-file: proxy.key`, - ` # max-request-body-size: 1234`, - `api:`, - ` host: localhost:3000`, - ` tls:`, - ` cert-file: api.cert`, - ` key-file: api.key`, - `all-services: all-services`, - `enabled-services: enabled-services`, - ), - }, nil, path) - require.NoError(t, err) - conf.Proxy.MaxReqBodySizeBytes = config.DefaultMaxReqBodySize - for _, td := range []TestOK{ - { - Path: filepath.Join(path, ServerConfigFileName), - Expect: conf, - }, - } { - t.Run("", func(t *testing.T) { - c, err := config.New(td.Path) - require.NoError(t, err) - require.True(t, td.Expect.Equal(c)) - }) - } - }) -} - -func TestReadConfigErrorMissingServerConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := os.Remove(p) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Contains(t, err.Error(), "no such file or directory") - }) -} - -func TestReadConfigErrorMalformedServerConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines("not a valid config"), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorIllegal{ - FilePath: p, - Feature: "syntax", - Message: "yaml: unmarshal errors:\n " + - "line 1: cannot unmarshal !!str `not a v...` " + - "into config.serverConfig", - }, err) - }) -} - -func TestReadConfigErrorMissingProxyHostConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: `, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "proxy.host", - }, err) - }) -} - -func TestReadConfigErrorMissingAPIHostConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - `api:`, - ` host: `, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "api.host", - }, err) - }) -} - -func TestReadConfigErrorMissingProxyTLSCert(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - ` tls:`, - ` key-file: proxy.key`, - `api:`, - ` host: localhost:9090`, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "proxy.tls.cert-file", - }, err) - }) -} - -func TestReadConfigErrorMissingProxyTLSKey(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - ` tls:`, - ` cert-file: proxy.cert`, - `api:`, - ` host: localhost:9090`, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "proxy.tls.key-file", - }, err) - }) -} - -func TestReadConfigErrorMissingAPITLSCert(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - `api:`, - ` host: localhost:9090`, - ` tls:`, - ` key-file: api.key`, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "api.tls.cert-file", - }, err) - }) -} - -func TestReadConfigErrorMissingAPITLSKey(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - `api:`, - ` host: localhost:9090`, - ` tls:`, - ` cert-file: api.cert`, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorMissing{ - FilePath: p, - Feature: "api.tls.key-file", - }, err) - }) -} - -func TestReadConfigErrorIllegalProxyMaxReqBodySize(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:8080`, - ` max-request-body-size: 255`, - `api:`, - ` host: localhost:9090`, - ), - }, nil, path) - require.NoError(t, err) - c, err := config.New(p) - require.Nil(t, c) - require.Equal(t, &config.ErrorIllegal{ - FilePath: p, - Feature: "proxy.max-request-body-size", - Message: fmt.Sprintf( - "maximum request body size should not be smaller than %d B", - config.MinReqBodySize, - ), - }, err) - }) -} - -func TestReadServiceConfigErrorMissingConfig(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, "all-services", "a.yml") - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "irrelevant_file.txt": []byte(`this file only keeps the directory`), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Contains(t, err.Error(), "no such file or directory") - }) -} - -func TestReadConfigErrorMalformedMetadata(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join("all-templates", "a", "a.gqt") - err := createFiles(map[string]any{ - p: lines( - "---", - "malformed metadata", - "---", - `query { foo }`, - ), - }, nil, path) - require.NoError(t, err) - _, err = config.New(filepath.Join(path, ServerConfigFileName)) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, p), - Feature: "metadata", - Message: "decoding yaml: yaml: " + - "unmarshal errors:\n " + - "line 1: cannot unmarshal !!str `malform...` " + - "into metadata.Metadata", - }, err) - }) -} - -func TestReadConfigErrorDuplicateTemplate(t *testing.T) { - validFS(func(path string, conf *config.Config) { - t1 := filepath.Join("all-templates", "a", "d1.gqt") - t2 := filepath.Join("all-templates", "a", "d2.gqt") - err := createFiles(map[string]any{ - t1: []byte( - `query { duplicate }`, - ), - t2: []byte( - `query { duplicate }`, - ), - }, nil, path) - require.NoError(t, err) - _, err = config.New(filepath.Join(path, ServerConfigFileName)) - require.Equal(t, &config.ErrorDuplicate{ - Original: filepath.Join(path, t1), - Duplicate: filepath.Join(path, t2), - }, err) - }) -} - -func TestReadConfigErrorDuplicateService(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `path: /`, - `forward-url: http://localhost:8080/`, - `all-templates: ../all-templates/a`, - `enabled-templates: ../enabled-templates/a`, - ), - "b.yml": lines( - `path: /`, - `forward-url: http://localhost:8080/`, - `all-templates: ../all-templates/a`, - `enabled-templates: ../enabled-templates/a`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorDuplicate{ - Original: filepath.Join(path, "all-services", "a.yml"), - Duplicate: filepath.Join(path, "all-services", "b.yml"), - }, err) - }) -} - -func TestReadConfigErrorMissingPath(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `forward-url: http://localhost:8080/`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorMissing{ - FilePath: filepath.Join( - path, "all-services", "a.yml", - ), - Feature: "path", - }, err) - }) - -} - -func TestReadConfigErrorMissingForwardURL(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `path: /`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorMissing{ - FilePath: filepath.Join( - path, "all-services", "a.yml", - ), - Feature: "forward-url", - }, err) - }) -} - -func TestReadConfigErrorInvalidPath(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `path: invalid_path`, - `forward-url: http://localhost:8080/`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, "all-services", "a.yml"), - Feature: "path", - Message: `path is not starting with /`, - }, err) - }) -} - -func TestReadConfigErrorInvalidForwardURLInvalidScheme(t *testing.T) { - minValidFS(func(path string) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `path: /`, - `forward-url: localhost:8080`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, "all-services", "a.yml"), - Feature: "forward-url", - Message: `protocol is not supported or undefined`, - }, err) - }) -} - -func TestReadConfigErrorInvalidTemplate(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join("all-templates", "a", "invalid_template.gqt") - err := createFiles(map[string]any{ - p: []byte( - `invalid { template }`, - ), - }, nil, path) - require.NoError(t, err) - _, err = config.New(filepath.Join(path, ServerConfigFileName)) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, p), - Feature: "template", - Message: `error at 0: unexpected definition`, - }, err) - }) -} - -func TestReadConfigErrorInvalidTemplateID(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join("all-templates", "a", "invalid_template#.gqt") - err := createFiles(map[string]any{ - p: []byte( - `invalid { template }`, - ), - }, nil, path) - require.NoError(t, err) - _, err = config.New(filepath.Join(path, ServerConfigFileName)) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, p), - Feature: "id", - Message: `contains illegal character at index 16`, - }, err) - }) -} - -func TestReadConfigErrorInvalidServiceID(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a#.yml": lines( - `path: /`, - `forward-url: http://localhost:8080/`, - `all-templates: ../all-templates/a`, - `enabled-templates: ../enabled-templates/a`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join( - path, - "all-services", - "a#.yml", - ), - Feature: "id", - Message: `contains illegal character at index 1`, - }, err) - }) -} - -func TestReadConfigErrorMalformedConfig(t *testing.T) { - validFS(func(path string, conf *config.Config) { - p := filepath.Join(path, ServerConfigFileName) - err := createFiles(map[string]any{ - "all-services": map[string]any{ - "a.yml": lines( - `malformed yaml`, - ), - }, - }, nil, path) - require.NoError(t, err) - _, err = config.New(p) - require.Equal(t, &config.ErrorIllegal{ - FilePath: filepath.Join(path, "all-services", "a.yml"), - Feature: "syntax", - Message: "yaml: unmarshal errors:\n " + - "line 1: cannot unmarshal !!str `malform...` " + - "into config.serviceConfig", - }, err) - }) -} - -func TestErrorString(t *testing.T) { - for _, td := range []struct { - input error - expect string - }{ - { - input: config.ErrorMissing{ - FilePath: "path/to/file.txt", - Feature: "some_feature", - }, - expect: "missing some_feature in path/to/file.txt", - }, - { - input: config.ErrorMissing{ - FilePath: "path/to/file.txt", - }, - expect: "missing path/to/file.txt", - }, - { - input: config.ErrorIllegal{ - FilePath: "path/to/file.txt", - Feature: "some_feature", - Message: "some message", - }, - expect: "illegal some_feature in path/to/file.txt: some message", - }, - { - input: config.ErrorDuplicate{ - Original: "path/to/file_a.txt", - Duplicate: "path/to/file_b.txt", - }, - expect: "path/to/file_b.txt is a duplicate of path/to/file_a.txt", - }, - } { - t.Run("", func(t *testing.T) { - require.Equal(t, td.expect, td.input.Error()) - }) - } -} - -func minValidFS(fn func(path string)) { - base, err := os.MkdirTemp("", "ggproxy-") - if err != nil { - panic(err) - } - defer os.RemoveAll(base) - - dirs := map[string]any{ - "all-services": nil, - "enabled-services": nil, - "all-templates": map[string]any{ - "a": nil, - "b": nil, - }, - "enabled-templates": map[string]any{ - "a": nil, - "b": nil, - }, - "irrelevant-dir": nil, - } - files := map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:443`, - `all-services: all-services`, - `enabled-services: enabled-services`, - ), - "irrelevant-file.txt": lines( - `this file is irrelevant and exists only for the purposes`, - `of testing function ReadConfig.`, - ), - "irrelevant-dir": map[string]any{ - "irrelevant_file.txt": lines( - `this file is irrelevant and exists only for the purposes`, - `of testing function ReadConfig.`, - ), - }, - } - - var hashes = make(map[string][]byte) - - if err := createDirs(dirs, base); err != nil { - panic(err) - } - if err := createFiles(files, hashes, base); err != nil { - panic(err) - } - - fn(base) -} - -func validFS(fn func(path string, conf *config.Config)) { - base, err := os.MkdirTemp("", "ggproxy-") - if err != nil { - panic(err) - } - defer os.RemoveAll(base) - - dirs := map[string]any{ - "all-services": nil, - "enabled-services": nil, - "all-templates": map[string]any{ - "a": nil, - "b": nil, - }, - "enabled-templates": map[string]any{ - "a": nil, - "b": nil, - }, - "irrelevant-dir": nil, - } - files := map[string]any{ - ServerConfigFileName: lines( - `proxy:`, - ` host: localhost:443`, - ` tls:`, - ` cert-file: proxy.cert`, - ` key-file: proxy.key`, - fmt.Sprintf( - ` max-request-body-size: %d`, - config.MinReqBodySize+256, - ), - `api:`, - ` host: localhost:3000`, - ` tls:`, - ` cert-file: api.cert`, - ` key-file: api.key`, - `all-services: all-services`, - `enabled-services: enabled-services`, - ), - "all-services": map[string]any{ - "a.yml": lines( - `path: "/path"`, - `forward-url: "http://localhost:8080/path"`, - `forward-reduced: true`, - `all-templates: "../all-templates/a"`, - `enabled-templates: "../enabled-templates/a"`, - ), - "b.yml": lines( - `path: /`, - `forward-url: "http://localhost:9090/"`, - `all-templates: "../all-templates/b"`, - `enabled-templates: "../enabled-templates/b"`, - ), - "ignored_file.txt": []byte(`this file should be ignored`), - }, - "all-templates": map[string]any{ - "a": map[string]any{ - "a.gqt": lines( - "---", - `name: "Template A"`, - "tags:", - " - tag_a", - "---", - `query { foo }`, - ), - "b.gqt": lines( - "---", - "tags:", - " - tag_b1", - " - tag_b2", - "---", - `query { bar }`, - ), - }, - "b": map[string]any{ - "c.gqt": []byte(`query { maz }`), - "ignored_file.txt": []byte(`this file should be ignored`), - }, - }, - "irrelevant-file.txt": lines( - `this file is irrelevant and exists only for the purposes`, - `of testing function ReadConfig.`, - ), - "irrelevant-dir": map[string]any{ - "irrelevant_file.txt": lines( - `this file is irrelevant and exists only for the purposes`, - `of testing function ReadConfig.`, - ), - }, - } - links := map[string]string{ - "all-services/a.yml": "enabled-services/a.yml", - "all-services/b.yml": "enabled-services/b.yml", - "all-templates/a/a.gqt": "enabled-templates/a/a.gqt", - "all-templates/a/b.gqt": "enabled-templates/a/b.gqt", - "all-templates/b/c.gqt": "enabled-templates/b/c.gqt", - "all-services/ignored-file.txt": "enabled-services/ignored-file.txt", - "all-templates/b/ignored-file.txt": "enabled-templates/b/ignored-file.txt", - } - - var hashes = make(map[string][]byte) - - if err := createDirs(dirs, base); err != nil { - panic(err) - } - if err := createFiles(files, hashes, base); err != nil { - panic(err) - } - if err := createSymlinks(links, base); err != nil { - panic(err) - } - - path := "" - services := hamap.New[[]byte, *config.Service](0, nil) - serviceATemplates := hamap.New[[]byte, *config.Template](0, nil) - serviceBTemplates := hamap.New[[]byte, *config.Template](0, nil) - path = filepath.Join(base, "all-templates", "a", "a.gqt") - serviceATemplates.Set(hashes[path], - &config.Template{ - ID: "a", - Name: "Template A", - Tags: []string{"tag_a"}, - Source: lines(`query { foo }`), - Document: gqt.Doc{ - Query: []gqt.Selection{ - gqt.SelectionField{ - Name: "foo", - }, - }, - }, - Enabled: true, - FilePath: path, - }, - ) - path = filepath.Join(base, "all-templates", "a", "b.gqt") - serviceATemplates.Set(hashes[path], - &config.Template{ - ID: "b", - Tags: []string{"tag_b1", "tag_b2"}, - Source: lines(`query { bar }`), - Document: gqt.Doc{ - Query: []gqt.Selection{ - gqt.SelectionField{ - Name: "bar", - }, - }, - }, - Enabled: true, - FilePath: path, - }, - ) - path = filepath.Join(base, "all-services", "a.yml") - services.Set(hashes[path], - &config.Service{ - ID: "a", - Path: "/path", - ForwardURL: "http://localhost:8080/path", - ForwardReduced: true, - Templates: serviceATemplates, - TemplatesEnabled: serviceATemplates.Values(), - Enabled: true, - FilePath: path, - }, - ) - path = filepath.Join(base, "all-templates", "b", "c.gqt") - serviceBTemplates.Set(hashes[path], - &config.Template{ - ID: "c", - Source: []byte(`query { maz }`), - Document: gqt.Doc{ - Query: []gqt.Selection{ - gqt.SelectionField{ - Name: "maz", - }, - }, - }, - Enabled: true, - FilePath: path, - }, - ) - path = filepath.Join(base, "all-services", "b.yml") - services.Set(hashes[path], - &config.Service{ - ID: "b", - Path: "/", - ForwardURL: "http://localhost:9090/", - ForwardReduced: false, - Templates: serviceBTemplates, - TemplatesEnabled: serviceBTemplates.Values(), - Enabled: true, - FilePath: path, - }, - ) - - conf := &config.Config{ - Proxy: config.ProxyServerConfig{ - Host: "localhost:443", - TLS: config.TLS{ - CertFile: "proxy.cert", - KeyFile: "proxy.key", - }, - MaxReqBodySizeBytes: config.MinReqBodySize + 256, - }, - API: &config.APIServerConfig{ - Host: "localhost:3000", - TLS: config.TLS{ - CertFile: "api.cert", - KeyFile: "api.key", - }, - }, - Services: services, - ServicesEnabled: services.Values(), - } - - fn(base, conf) -} - -func createDirs(dirs map[string]any, path string) error { - for k, v := range dirs { - p := filepath.Join(path, k) - if err := os.Mkdir(p, 0775); err != nil { - return err - } - if v != nil { - switch vt := v.(type) { - case map[string]any: - if err := createDirs(vt, p); err != nil { - return err - } - } - } - } - - return nil -} - -func createFiles(files map[string]any, hashes map[string][]byte, path string) error { - for k, v := range files { - p := filepath.Join(path, k) - switch vt := v.(type) { - case []byte: - f, err := os.Create(p) - if err != nil { - return err - } else { - if _, err := f.Write(vt); err != nil { - return err - } - } - if hashes != nil { - hashes[p] = calculateHash(f) - } - case map[string]any: - if err := createFiles(vt, hashes, p); err != nil { - return err - } - } - } - - return nil -} - -func createSymlinks(links map[string]string, path string) error { - for k, v := range links { - if err := os.Symlink(filepath.Join(path, k), filepath.Join(path, v)); err != nil { - return err - } - } - - return nil -} - -func lines(lines ...string) []byte { - var b strings.Builder - for i := range lines { - b.WriteString(lines[i]) - b.WriteByte('\n') - } - return []byte(b.String()) -} - -func calculateHash(file *os.File) []byte { - _, err := file.Seek(0, io.SeekStart) - if err != nil { - panic(err) - } - h := md5.New() - _, err = io.Copy(h, file) - if err != nil { - panic(err) - } - - return h.Sum(nil) -} diff --git a/engines/rmap/pquery/pquery.go b/engines/rmap/pquery/pquery.go deleted file mode 100644 index d6a8a66..0000000 --- a/engines/rmap/pquery/pquery.go +++ /dev/null @@ -1,517 +0,0 @@ -package pquery - -import ( - "fmt" - "io" - "strconv" - - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/container/amap" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/stack" - "github.com/graph-guard/ggproxy/utilities/unsafe" - "github.com/graph-guard/ggproxy/utilities/xxhash" - "github.com/graph-guard/gqlscan" -) - -type pathTerminal struct{} -type selectTerminal struct{} -type argumentsTerminal struct{} -type argumentPathTerminal struct{} -type objectTerminal struct{} - -var specialFields []string = []string{"__typename"} - -// QueryPart is a structure of query pash hash and value, -// when united represents a parted query. -// Used for a template fast search by rmap. -type QueryPart struct { - ArgLeafIdx int - Hash uint64 - Value any -} - -// Maker is a meta structure to store the runtime data and the hash seed. -type Maker struct { - mstack *stack.Stack[any] - pstack *stack.Stack[xxhash.Hash] - qmap *amap.Map[uint64, bool] - usedStack *stack.Stack[any] - arrayPool *stack.Stack[*[]any] - mapPool *stack.Stack[*hamap.Map[string, any]] - seed uint64 -} - -// NewMaker creates a new instance of Maker. -// Accepts a hash seed. -func NewMaker(seed uint64) *Maker { - return &Maker{ - mstack: stack.New[any](256), - pstack: stack.New[xxhash.Hash](256), - qmap: amap.New[uint64, bool](256), - mapPool: stack.New[*hamap.Map[string, any]](128), - arrayPool: stack.New[*[]any](128), - usedStack: stack.New[any](128), - seed: seed, - } -} - -// ParseQuery parses query into QueryParts. -// Accepts a token list. -// QueryParts are accessible through the fn function. -func (m *Maker) ParseQuery( - variableValues [][]gqlparse.Token, - queryType gqlscan.Token, - selectionSet []gqlparse.Token, - fn func(qp QueryPart) (stop bool), -) { - m.mstack.Reset() - m.pstack.Reset() - m.qmap.Reset() - - var pathHash uint64 - var insideArray, argLeafIdx int = 0, -1 - var lastObjField string - - switch queryType { - case gqlscan.TokenDefQry: - path := xxhash.New(m.seed) - xxhash.Write(&path, "query") - m.pstack.Push(path) - case gqlscan.TokenDefMut: - path := xxhash.New(m.seed) - xxhash.Write(&path, "mutation") - m.pstack.Push(path) - default: - panic(fmt.Errorf("unsupported query type: %v", queryType)) - } - - for tokenIdx, token := range selectionSet { - if ix := token.VariableIndex(); ix > -1 { - value := variableValues[ix] - for _, token := range value { - switch token.ID { - case gqlscan.TokenArr: - insideArray++ - var arr *[]any - if m.arrayPool.Len() > 0 { - arr = m.arrayPool.Pop() - } else { - arr = &[]any{} - } - m.usedStack.Push(arr) - - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, arr) - case *hamap.Map[string, any]: - t.Set(lastObjField, arr) - } - m.mstack.Push(arr) - case gqlscan.TokenObj: - if insideArray == 0 { - path := m.pstack.Top() - xxhash.Write(&path, ".") - m.pstack.Push(path) - m.mstack.Push(objectTerminal{}) - } else { - var obj *hamap.Map[string, any] - if m.mapPool.Len() > 0 { - obj = m.mapPool.Pop() - } else { - obj = hamap.New[string, any](64, nil) - } - m.usedStack.Push(obj) - - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, obj) - case *hamap.Map[string, any]: - t.Set(lastObjField, obj) - } - m.mstack.Push(obj) - } - case gqlscan.TokenObjField: - if insideArray == 0 { - t := m.pstack.Top() - xxhash.Write(&t, token.Value) - m.pstack.Push(t) - m.mstack.Push(pathTerminal{}) - } else { - lastObjField = unsafe.B2S(token.Value) - } - case gqlscan.TokenStr, gqlscan.TokenEnumVal, gqlscan.TokenInt, - gqlscan.TokenFloat, gqlscan.TokenTrue, gqlscan.TokenFalse, gqlscan.TokenNull: - var val any - var err error - switch token.ID { - case gqlscan.TokenStr, gqlscan.TokenEnumVal: - val = token.Value - case gqlscan.TokenInt: - val, err = strconv.ParseInt(unsafe.B2S(token.Value), 10, 64) - if err != nil { - panic(err) - } - case gqlscan.TokenFloat: - val, err = strconv.ParseFloat(unsafe.B2S(token.Value), 64) - if err != nil { - panic(err) - } - case gqlscan.TokenTrue: - val = true - case gqlscan.TokenFalse: - val = false - } - if insideArray == 0 { - switch t := m.mstack.Top(); t.(type) { - case pathTerminal: - m.mstack.Pop() - path := m.pstack.Pop() - pathHash = path.Sum64() - if _, ok := m.qmap.Get(pathHash); !ok { - argLeafIdx++ - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: argLeafIdx, Hash: pathHash, Value: val}) { - return - } - } - } - } else { - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, val) - case *hamap.Map[string, any]: - t.Set(lastObjField, val) - } - } - case gqlscan.TokenArrEnd, gqlscan.TokenObjEnd: - if token.ID == gqlscan.TokenArrEnd { - insideArray-- - } - for { - switch t := m.mstack.Top(); t.(type) { - case objectTerminal: - m.mstack.Pop() - m.pstack.Pop() - m.mstack.Pop() - m.pstack.Pop() - case *[]any, *hamap.Map[string, any]: - el := m.mstack.Pop() - path := m.pstack.Top() - if insideArray == 0 { - pathHash = path.Sum64() - switch elt := el.(type) { - case *[]any, *hamap.Map[string, any]: - if _, ok := m.qmap.Get(pathHash); !ok { - argLeafIdx++ - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: argLeafIdx, Hash: pathHash, Value: elt}) { - return - } - } - } - m.mstack.Pop() - m.pstack.Pop() - } - } - break - } - } - } - continue - } - - switch token.ID { - case gqlscan.TokenField, gqlscan.TokenFragInline: - switch t := m.mstack.Top(); t.(type) { - case argumentPathTerminal: - m.mstack.Pop() - m.pstack.Pop() - } - switch t := m.mstack.Top(); t.(type) { - case pathTerminal: - m.mstack.Pop() - path := m.pstack.Pop() - pathHash = path.Sum64() - if _, ok := m.qmap.Get(pathHash); !ok { - if !inSlice(specialFields, unsafe.B2S(selectionSet[tokenIdx-1].Value)) { - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: -1, Hash: pathHash, Value: nil}) { - return - } - } - } - } - path := m.pstack.Top() - if token.ID == gqlscan.TokenFragInline { - xxhash.Write(&path, "|") - } - xxhash.Write(&path, token.Value) - m.pstack.Push(path) - m.mstack.Push(pathTerminal{}) - case gqlscan.TokenArgList: - m.mstack.PopPush(argumentPathTerminal{}) - path := m.pstack.Top() - xxhash.Write(&path, ".") - m.pstack.Push(path) - m.mstack.Push(argumentsTerminal{}) - case gqlscan.TokenSet: - path := m.pstack.Top() - xxhash.Write(&path, ".") - m.pstack.Push(path) - m.mstack.Push(selectTerminal{}) - case gqlscan.TokenArgName: - path := m.pstack.Top() - xxhash.Write(&path, token.Value) - m.pstack.Push(path) - m.mstack.Push(pathTerminal{}) - case gqlscan.TokenArr: - insideArray++ - var arr *[]any - if m.arrayPool.Len() > 0 { - arr = m.arrayPool.Pop() - } else { - arr = &[]any{} - } - m.usedStack.Push(arr) - - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, arr) - case *hamap.Map[string, any]: - t.Set(lastObjField, arr) - } - m.mstack.Push(arr) - case gqlscan.TokenObj: - if insideArray == 0 { - path := m.pstack.Top() - xxhash.Write(&path, ".") - m.pstack.Push(path) - m.mstack.Push(objectTerminal{}) - } else { - var obj *hamap.Map[string, any] - if m.mapPool.Len() > 0 { - obj = m.mapPool.Pop() - } else { - obj = hamap.New[string, any](64, nil) - } - m.usedStack.Push(obj) - - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, obj) - case *hamap.Map[string, any]: - t.Set(lastObjField, obj) - } - m.mstack.Push(obj) - } - case gqlscan.TokenObjField: - if insideArray == 0 { - t := m.pstack.Top() - xxhash.Write(&t, token.Value) - m.pstack.Push(t) - m.mstack.Push(pathTerminal{}) - } else { - lastObjField = unsafe.B2S(token.Value) - } - case gqlscan.TokenStr, gqlscan.TokenEnumVal, gqlscan.TokenInt, - gqlscan.TokenFloat, gqlscan.TokenTrue, gqlscan.TokenFalse, gqlscan.TokenNull: - var val any - var err error - switch token.ID { - case gqlscan.TokenStr, gqlscan.TokenEnumVal: - val = token.Value - case gqlscan.TokenNull: - val = nil - case gqlscan.TokenInt: - val, err = strconv.ParseInt(unsafe.B2S(token.Value), 10, 64) - if err != nil { - panic(err) - } - case gqlscan.TokenFloat: - val, err = strconv.ParseFloat(unsafe.B2S(token.Value), 64) - if err != nil { - panic(err) - } - case gqlscan.TokenTrue: - val = true - case gqlscan.TokenFalse: - val = false - } - if insideArray == 0 { - switch t := m.mstack.Top(); t.(type) { - case pathTerminal: - m.mstack.Pop() - path := m.pstack.Pop() - pathHash = path.Sum64() - if _, ok := m.qmap.Get(pathHash); !ok { - argLeafIdx++ - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: argLeafIdx, Hash: pathHash, Value: val}) { - return - } - } - } - } else { - switch t := m.mstack.Top(); t := t.(type) { - case *[]any: - *t = append(*t, val) - case *hamap.Map[string, any]: - t.Set(lastObjField, val) - } - } - case gqlscan.TokenArrEnd, gqlscan.TokenSetEnd, - gqlscan.TokenArgListEnd, gqlscan.TokenObjEnd: - if token.ID == gqlscan.TokenArgListEnd { - argLeafIdx = -1 - } - if token.ID == gqlscan.TokenArrEnd { - insideArray-- - } - for { - switch t := m.mstack.Top(); t.(type) { - case pathTerminal: - m.mstack.Pop() - path := m.pstack.Pop() - pathHash = path.Sum64() - if _, ok := m.qmap.Get(pathHash); !ok { - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: -1, Hash: pathHash, Value: nil}) { - return - } - } - continue - case argumentPathTerminal: - m.mstack.Pop() - m.pstack.Pop() - continue - case argumentsTerminal: - m.mstack.Pop() - m.pstack.Pop() - case selectTerminal, objectTerminal: - m.mstack.Pop() - m.pstack.Pop() - m.mstack.Pop() - m.pstack.Pop() - case *[]any, *hamap.Map[string, any]: - el := m.mstack.Pop() - path := m.pstack.Top() - if insideArray == 0 { - pathHash = path.Sum64() - switch elt := el.(type) { - case *[]any, *hamap.Map[string, any]: - if _, ok := m.qmap.Get(pathHash); !ok { - argLeafIdx++ - m.qmap.Set(pathHash, true) - if fn(QueryPart{ArgLeafIdx: argLeafIdx, Hash: pathHash, Value: elt}) { - return - } - } - } - m.mstack.Pop() - m.pstack.Pop() - } - } - break - } - } - } - - for m.usedStack.Len() > 0 { - switch el := m.usedStack.Pop().(type) { - case *[]any: - *el = (*el)[:0] - m.arrayPool.Push(el) - case *hamap.Map[string, any]: - el.Reset() - m.mapPool.Push(el) - } - } -} - -// PrintNSpaces prints n spaces in a row -func PrintNSpaces(w io.Writer, n uint) { - for i := uint(0); i < n; i++ { - _, _ = w.Write([]byte(" ")) - } -} - -// Print prints out the QueryPart -func (qp QueryPart) Print(w io.Writer) { - qp.print(w, 0) -} - -func (qp QueryPart) print(w io.Writer, indent uint) { - PrintNSpaces(w, indent) - fmt.Fprintf(w, "%d:", qp.Hash) - switch vt := qp.Value.(type) { - case *[]any: - _, _ = w.Write([]byte("\n")) - printArr(*vt, w, indent+2) - case *hamap.Map[string, any]: - _, _ = w.Write([]byte("\n")) - printObj(*vt, w, indent+2) - default: - if qp.Value != nil { - _, _ = w.Write([]byte(" ")) - if s, ok := qp.Value.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, qp.Value) - } - } else { - _, _ = w.Write([]byte("\n")) - } - } -} - -func printArr(arr []any, w io.Writer, indent uint) { - for _, v := range arr { - PrintNSpaces(w, indent) - _, _ = w.Write([]byte("-:\n")) - switch vt := v.(type) { - case *[]any: - printArr(*vt, w, indent+2) - case *hamap.Map[string, any]: - printObj(*vt, w, indent+2) - default: - PrintNSpaces(w, indent+2) - if s, ok := v.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, v) - } - } - } -} - -func printObj(obj hamap.Map[string, any], w io.Writer, indent uint) { - obj.Visit(func(key string, value any) (stop bool) { - PrintNSpaces(w, indent) - fmt.Fprintf(w, "%s:\n", key) - switch vt := value.(type) { - case *[]any: - printArr(*vt, w, indent+2) - case *hamap.Map[string, any]: - printObj(*vt, w, indent+2) - default: - PrintNSpaces(w, indent+2) - if s, ok := value.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, value) - } - } - return false - }) -} - -func inSlice[T comparable](a []T, e T) bool { - for _, el := range a { - if el == e { - return true - } - } - - return false -} diff --git a/engines/rmap/rmap.go b/engines/rmap/rmap.go deleted file mode 100644 index e6a18a0..0000000 --- a/engines/rmap/rmap.go +++ /dev/null @@ -1,1083 +0,0 @@ -package rmap - -import ( - "bytes" - "errors" - "fmt" - "io" - - "github.com/graph-guard/ggproxy/engines/rmap/pquery" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/bitmask" - "github.com/graph-guard/ggproxy/utilities/container/amap" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/xxhash" - "github.com/graph-guard/gqlscan" - "github.com/graph-guard/gqt" -) - -var ErrHashCollision = errors.New("hash collsision") - -const ( - maxRand = 32768 - maxAttempts = 32 -) - -// RulesMap is a graphql query to a template fast search structure. -type RulesMap struct { - seed uint64 - mask *bitmask.Set - rejected *bitmask.Set - qmake pquery.Maker - matchCounter *amap.Map[int, int] - combinations []int - combinationCounters []int - rules map[uint64][]Variant - hashedPaths map[uint64]string - templateIDs []string -} - -// Combination is a auxiliary "max" block structure. -type Combination struct { - Index int - Depth int - RuleIndex int -} - -// Variant is an auxiliary RulesNode structure. -type Variant struct { - Condition bool - Constraint Constraint - Mask *bitmask.Set - Value any - Combinations []Combination -} - -// Elem is an auxiliary Variant structure. -type Elem struct { - Constraint Constraint - Value any -} - -// Array is an auxiliary array structure. -type Array []Elem - -// Object is an auxiliary map structure. -type Object map[string]Elem - -// ConstraintInterface is a generic interface for constraints. -type ConstraintInterface interface { - Key() string - Content() gqt.Constraint - gqt.InputConstraint | gqt.ObjectField -} - -// Equal checks two Elems for equality. -func (e Elem) Equal(x Elem) bool { - if e.Constraint == x.Constraint { - switch ve := e.Value.(type) { - case Elem: - switch vx := x.Value.(type) { - case Elem: - return ve.Equal(vx) - } - case Array: - switch vx := x.Value.(type) { - case Array: - return ve.Equal(vx) - } - case Object: - switch vx := x.Value.(type) { - case Object: - return ve.Equal(vx) - } - case []byte: - switch vx := x.Value.(type) { - case []byte: - return bytes.Equal(ve, vx) - } - default: - return e.Value == x.Value - } - } - - return false -} - -// Equal checks two Arrays for equality. -func (arr Array) Equal(x Array) bool { - if len(arr) != len(x) { - return false - } - - for i := 0; i < len(x); i++ { - if !arr[i].Equal(x[i]) { - return false - } - } - - return true -} - -// Equal checks two Objects for equality. -func (obj Object) Equal(x Object) bool { - if len(obj) != len(x) { - return false - } - - for k, e := range x { - v, ok := obj[k] - if !ok { - return false - } - if !v.Equal(e) { - return false - } - } - - return true -} - -// New creates a new instance of RulesMap. -// Accepts a rules list and a hash seed. -func New(rules map[string]gqt.Doc, seed uint64) (*RulesMap, error) { - rm := &RulesMap{ - seed: seed, - mask: bitmask.New(), - rejected: bitmask.New(), - qmake: *pquery.NewMaker(seed), - matchCounter: amap.New[int, int](0), - combinations: []int{}, - combinationCounters: []int{}, - rules: map[uint64][]Variant{}, - hashedPaths: map[uint64]string{}, - } - var attempt int - var err error - - rm.templateIDs = make([]string, 0, len(rules)) - for id := range rules { - rm.templateIDs = append(rm.templateIDs, id) - } - - for attempt < maxAttempts { - for index, id := range rm.templateIDs { - rule := rules[id] - m := bitmask.New(index) - if rule.Query != nil { - err = buildRulesMapSelections( - rm, rule.Query, nil, m, "query", index, 0, - ) - } - if rule.Mutation != nil { - err = buildRulesMapSelections( - rm, rule.Mutation, nil, m, "mutation", index, 0, - ) - } - if rule.Subscription != nil { - panic("subscriptions are not yet supported") - } - if err == ErrHashCollision { - rm = &RulesMap{ - seed: seed, - mask: bitmask.New(), - rejected: bitmask.New(), - qmake: *pquery.NewMaker(seed), - matchCounter: amap.New[int, int](0), - combinations: []int{}, - combinationCounters: []int{}, - rules: map[uint64][]Variant{}, - hashedPaths: map[uint64]string{}, - } - attempt++ - break - } - } - if err != ErrHashCollision { - break - } - } - - if err != nil { - return nil, err - } - - return rm, nil -} - -func buildRulesMapSelections( - rm *RulesMap, - selections []gqt.Selection, - dependencies []uint64, - mask *bitmask.Set, - path string, - ruleIdx int, - combinationDepth int, -) error { - for _, selection := range selections { - switch selection := selection.(type) { - case gqt.SelectionField: - selPath := path + "." + selection.Name - if len(selection.Selections) == 0 && len(selection.InputConstraints) == 0 { - h := xxhash.New(rm.seed) - xxhash.Write(&h, selPath) - pathHash := h.Sum64() - - v := Variant{ - Mask: mask, - Combinations: []Combination{}, - } - if combinationDepth > 0 { - v.Combinations = append( - v.Combinations, - Combination{len(rm.combinations) - 1, combinationDepth - 1, ruleIdx}, - ) - } - if _, ok := (*rm).rules[pathHash]; ok { - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], v) - } else { - if v, ok := rm.hashedPaths[pathHash]; !ok { - rm.hashedPaths[pathHash] = selPath - } else { - if v != selPath { - return ErrHashCollision - } - } - (*rm).rules[pathHash] = []Variant{v} - } - } else { - var leafs []uint64 - var err error - if len(selection.InputConstraints) > 0 { - leafs, err = buildRulesMapConstraints( - rm, selection.InputConstraints, dependencies, mask, selPath, true, ruleIdx, combinationDepth, - ) - if err != nil { - return err - } - } - if len(selection.Selections) > 0 { - if len(selection.InputConstraints) > 0 { - combinationDepth = 0 - } - err = buildRulesMapSelections( - rm, selection.Selections, append(leafs, dependencies...), mask, selPath, ruleIdx, combinationDepth, - ) - if err != nil { - return err - } - } - } - case gqt.SelectionInlineFragment: - selPath := path + ".|" + selection.TypeName - if err := buildRulesMapSelections( - rm, selection.Selections, dependencies, mask, selPath, ruleIdx, combinationDepth, - ); err != nil { - return err - } - case gqt.ConstraintCombine: - rm.combinations = append(rm.combinations, int(selection.MaxItems)) - rm.combinationCounters = append(rm.combinationCounters, 0) - if err := buildRulesMapSelections( - rm, selection.Items, dependencies, mask, path, ruleIdx, combinationDepth+1, - ); err != nil { - return err - } - } - } - - return nil -} - -func buildRulesMapConstraints[T ConstraintInterface]( - rm *RulesMap, - constraints []T, - dependencies []uint64, - mask *bitmask.Set, - path string, - condition bool, - ruleIdx int, - combinationDepth int, -) ([]uint64, error) { - var leafs []uint64 - for _, constraint := range constraints { - var cv any - var cid Constraint - cond := condition - - cid, cv = ConstraintIdAndValue(constraint.Content()) - if cid == ConstraintValNotEqual { - cond = !cond - } - - conPath := path + "." + constraint.Key() - switch cv := cv.(type) { - case gqt.ValueObject: - l, err := buildRulesMapConstraints( - rm, cv.Fields, dependencies, mask, conPath, cond, ruleIdx, combinationDepth, - ) - if err != nil { - return nil, err - } - leafs = append(leafs, l...) - default: - h := xxhash.New(rm.seed) - xxhash.Write(&h, conPath) - pathHash := h.Sum64() - leafs = append(leafs, pathHash) - if _, ok := (*rm).rules[pathHash]; !ok { - if v, ok := rm.hashedPaths[pathHash]; !ok { - rm.hashedPaths[pathHash] = conPath - } else { - if v != conPath { - return nil, ErrHashCollision - } - } - } - - c := []Combination{} - if combinationDepth > 0 { - c = append( - c, - Combination{len(rm.combinations) - 1, combinationDepth - 1, ruleIdx}, - ) - } - switch cid { - case ConstraintMap: - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], Variant{ - Condition: cond, - Constraint: cid, - Mask: mask, - Value: buildRulesMapConstraintsElem(cv), - Combinations: c, - }) - case ConstraintOr, ConstraintAnd: - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], Variant{ - Condition: cond, - Constraint: cid, - Mask: mask, - Value: buildRulesMapConstraintsArray(cv.([]gqt.Constraint)), - Combinations: c, - }) - default: - switch cv := cv.(type) { - case gqt.ValueArray: - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], Variant{ - Condition: cond, - Constraint: cid, - Mask: mask, - Value: buildRulesMapConstraintsArray(cv.Items), - Combinations: c, - }) - case gqt.EnumValue: - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], Variant{ - Condition: cond, - Constraint: cid, - Mask: mask, - Value: []byte(cv), - Combinations: c, - }) - default: - (*rm).rules[pathHash] = mergeVariants((*rm).rules[pathHash], Variant{ - Condition: cond, - Constraint: cid, - Mask: mask, - Value: cv, - Combinations: c, - }) - } - } - } - } - - return leafs, nil -} - -func buildRulesMapConstraintsElem(constraint gqt.Constraint) (el Elem) { - var cv any - var cid Constraint - - cid, cv = ConstraintIdAndValue(constraint) - - switch cid { - case ConstraintMap: - el = Elem{ - Constraint: cid, - Value: buildRulesMapConstraintsElem(cv), - } - case ConstraintOr, ConstraintAnd: - el = Elem{ - Constraint: cid, - Value: buildRulesMapConstraintsArray(cv.([]gqt.Constraint)), - } - default: - switch cv := cv.(type) { - case gqt.ValueObject: - el = Elem{ - Constraint: cid, - Value: buildRulesMapConstraintsObject(cv.Fields), - } - case gqt.ValueArray: - el = Elem{ - Constraint: cid, - Value: buildRulesMapConstraintsArray(cv.Items), - } - case gqt.EnumValue: - el = Elem{ - Constraint: cid, - Value: []byte(cv), - } - default: - el = Elem{ - Constraint: cid, - Value: cv, - } - } - } - - return -} - -func buildRulesMapConstraintsArray( - constraints []gqt.Constraint, -) (arr Array) { - for _, constraint := range constraints { - arr = append(arr, buildRulesMapConstraintsElem(constraint)) - } - - return -} - -func buildRulesMapConstraintsObject( - constraints []gqt.ObjectField, -) (obj Object) { - obj = Object{} - for _, constraint := range constraints { - obj[constraint.Name] = buildRulesMapConstraintsElem(constraint.Value) - } - - return -} - -func mergeVariants(variants []Variant, x Variant) []Variant { - merge := func(i int) { - variants[i].Mask = variants[i].Mask.Or(x.Mask) - variants[i].Combinations = append(variants[i].Combinations, x.Combinations...) - } - - for i := 0; i < len(variants); i++ { - if variants[i].Constraint == x.Constraint { - switch vt := variants[i].Value.(type) { - case Elem: - switch xt := x.Value.(type) { - case Elem: - if vt.Equal(xt) { - merge(i) - return variants - } - } - case Array: - switch xt := x.Value.(type) { - case Array: - if vt.Equal(xt) { - merge(i) - return variants - } - } - default: - if xb, ok := x.Value.([]byte); ok { - if bytes.Equal(variants[i].Value.([]byte), xb) { - merge(i) - return variants - } - } else if variants[i].Value == x.Value { - merge(i) - return variants - } - } - } - } - variants = append(variants, x) - - return variants -} - -// ConstraintIdAndValue returns constraint Id and Value. -func ConstraintIdAndValue(c gqt.Constraint) (Constraint, any) { - switch c := c.(type) { - case gqt.ConstraintOr: - return ConstraintOr, c.Constraints - case gqt.ConstraintAnd: - return ConstraintAnd, c.Constraints - case gqt.ConstraintMap: - return ConstraintMap, c.Constraint - case gqt.ConstraintAny: - return ConstraintAny, nil - case gqt.ConstraintValEqual: - if s, ok := c.Value.(string); ok { - return ConstraintValEqual, []byte(s) - } - return ConstraintValEqual, c.Value - case gqt.ConstraintValNotEqual: - if s, ok := c.Value.(string); ok { - return ConstraintValNotEqual, []byte(s) - } - return ConstraintValNotEqual, c.Value - case gqt.ConstraintValGreater: - return ConstraintValGreater, c.Value - case gqt.ConstraintValLess: - return ConstraintValLess, c.Value - case gqt.ConstraintValGreaterOrEqual: - return ConstraintValGreaterOrEqual, c.Value - case gqt.ConstraintValLessOrEqual: - return ConstraintValLessOrEqual, c.Value - case gqt.ConstraintBytelenEqual: - return ConstraintBytelenEqual, c.Value - case gqt.ConstraintBytelenNotEqual: - return ConstraintBytelenNotEqual, c.Value - case gqt.ConstraintBytelenGreater: - return ConstraintBytelenGreater, c.Value - case gqt.ConstraintBytelenLess: - return ConstraintBytelenLess, c.Value - case gqt.ConstraintBytelenGreaterOrEqual: - return ConstraintBytelenGreaterOrEqual, c.Value - case gqt.ConstraintBytelenLessOrEqual: - return ConstraintBytelenLessOrEqual, c.Value - case gqt.ConstraintLenEqual: - return ConstraintLenEqual, c.Value - case gqt.ConstraintLenNotEqual: - return ConstraintLenNotEqual, c.Value - case gqt.ConstraintLenGreater: - return ConstraintLenGreater, c.Value - case gqt.ConstraintLenLess: - return ConstraintLenLess, c.Value - case gqt.ConstraintLenGreaterOrEqual: - return ConstraintLenGreaterOrEqual, c.Value - case gqt.ConstraintLenLessOrEqual: - return ConstraintLenLessOrEqual, c.Value - default: - return ConstraintUnknown, nil - } -} - -// Match returns the ID of the first matching template or "" if none was matched. -func (rm *RulesMap) Match( - variableValues [][]gqlparse.Token, - queryType gqlscan.Token, - selectionSet []gqlparse.Token, -) (id string) { - rm.FindMatch(variableValues, queryType, selectionSet, func(mask *bitmask.Set) { - if mask.Size() > 0 { - mask.Visit(func(n int) (skip bool) { - id = rm.templateIDs[n] - return true - }) - } - }) - return id -} - -// MatchAll calls fn for every matching template. -func (rm *RulesMap) MatchAll( - variableValues [][]gqlparse.Token, - queryType gqlscan.Token, - selectionSet []gqlparse.Token, - fn func(id string), -) { - rm.FindMatch(variableValues, queryType, selectionSet, func(mask *bitmask.Set) { - mask.VisitAll(func(n int) { - fn(rm.templateIDs[n]) - }) - }) -} - -// FindMatch matches query to the rules. -func (rm *RulesMap) FindMatch( - variableValues [][]gqlparse.Token, - queryType gqlscan.Token, - selectionSet []gqlparse.Token, - fn func(mask *bitmask.Set), -) { - var qpCount int - rm.matchCounter.Reset() - rm.mask.Reset() - rm.rejected.Reset() - memset(rm.combinationCounters, 0) - rm.qmake.ParseQuery(variableValues, queryType, selectionSet, func(qp pquery.QueryPart) (stop bool) { - qpCount++ - if rn, ok := rm.rules[qp.Hash]; ok { - if len(rn) > 0 { - var match bool - for _, v := range rn { - if len(v.Combinations) > 0 { - if qp.ArgLeafIdx < 1 { - var depth int - for _, c := range v.Combinations { - if rm.combinationCounters[c.Index] == 0 { - depth = c.Depth - } - for i := c.Index - depth; i <= c.Index; i++ { - rm.combinationCounters[i]++ - if rm.combinations[i] < rm.combinationCounters[i] { - rm.rejected.Add(c.RuleIndex) - } - } - } - } - } - - if v.Compare(qp.Value) { - match = true - rm.mask.SetOr(rm.mask, v.Mask) - v.Mask.Visit(func(x int) (skip bool) { - rm.matchCounter.SetFn(x, 1, func(value *int) { *value++ }) - return false - }) - } - } - if !match { - rm.mask.Reset() - return true - } - } - } else { - rm.mask.Reset() - return true - } - - return false - }) - for _, el := range rm.matchCounter.A { - if el.Value < qpCount { - rm.rejected.Add(el.Key) - } - } - rm.mask.SetAndNot(rm.mask, rm.rejected) - - if rm.mask.Empty() { - rm.mask.Reset() - } - - fn(rm.mask) -} - -// CompareValues compares two values according to the provided constraint. -func CompareValues(constraint Constraint, a any, b any) bool { - switch constraint { - case None, ConstraintAny: - return true - case ConstraintValEqual: - if b, ok := b.([]byte); ok { - return bytes.Equal(b, a.([]byte)) - } - return b == a - case ConstraintValNotEqual: - if b, ok := b.([]byte); ok { - return !bytes.Equal(b, a.([]byte)) - } - return b != a - case ConstraintValGreater, ConstraintValLess, - ConstraintValGreaterOrEqual, ConstraintValLessOrEqual: - switch vala := a.(type) { - case int64: - valb, ok := b.(int64) - if !ok { - return false - } - switch constraint { - case ConstraintValGreater: - return valb > vala - case ConstraintValLess: - return valb < vala - case ConstraintValGreaterOrEqual: - return valb >= vala - case ConstraintValLessOrEqual: - return valb <= vala - } - case float64: - valb, ok := b.(float64) - if !ok { - return false - } - switch constraint { - case ConstraintValGreater: - return valb > vala - case ConstraintValLess: - return valb < vala - case ConstraintValGreaterOrEqual: - return valb >= vala - case ConstraintValLessOrEqual: - return valb <= vala - } - } - case ConstraintBytelenEqual, ConstraintBytelenNotEqual, - ConstraintBytelenGreater, ConstraintBytelenLess, - ConstraintBytelenGreaterOrEqual, ConstraintBytelenLessOrEqual: - vala, ok := a.(uint) - if !ok { - return false - } - valb, ok := b.([]byte) - if !ok { - return false - } - switch constraint { - case ConstraintBytelenEqual: - return len(valb) == int(vala) - case ConstraintBytelenNotEqual: - return len(valb) != int(vala) - case ConstraintBytelenGreater: - return len(valb) > int(vala) - case ConstraintBytelenLess: - return len(valb) < int(vala) - case ConstraintBytelenGreaterOrEqual: - return len(valb) >= int(vala) - case ConstraintBytelenLessOrEqual: - return len(valb) <= int(vala) - } - case ConstraintLenEqual, ConstraintLenNotEqual, - ConstraintLenGreater, ConstraintLenLess, - ConstraintLenGreaterOrEqual, ConstraintLenLessOrEqual: - ca, ok := a.(uint) - if !ok { - return false - } - bi, ok := b.(*[]any) - if !ok { - return false - } - switch constraint { - case ConstraintLenEqual: - return len(*bi) == int(ca) - case ConstraintLenNotEqual: - return len(*bi) != int(ca) - case ConstraintLenGreater: - return len(*bi) > int(ca) - case ConstraintLenLess: - return len(*bi) < int(ca) - case ConstraintLenGreaterOrEqual: - return len(*bi) >= int(ca) - case ConstraintLenLessOrEqual: - return len(*bi) <= int(ca) - } - default: - panic(fmt.Errorf("wrong constraint-type pair; constraint: %d", constraint)) - } - - return true -} - -// Compare checks two Varians for equality. -func (v Variant) Compare(x any) bool { - switch v.Constraint { - case ConstraintMap: - switch xt := x.(type) { - case *[]any: - for _, el := range *xt { - switch vt := v.Value.(type) { - case Elem: - if !vt.Compare(el) { - return false - } - } - } - return true - } - case ConstraintOr: - for _, el := range v.Value.(Array) { - if el.Compare(x) { - return true - } - } - return false - case ConstraintAnd: - for _, el := range v.Value.(Array) { - if !el.Compare(x) { - return false - } - } - return true - default: - neq := v.Constraint == ConstraintValNotEqual - switch vt := v.Value.(type) { - case Array: - switch xt := x.(type) { - case *[]any: - return vt.Compare(*xt) != neq - } - default: - return CompareValues(v.Constraint, v.Value, x) - } - } - - return false -} - -// Compare checks two Elems for equality. -func (e Elem) Compare(x any) bool { - switch e.Constraint { - case ConstraintMap: - switch xt := x.(type) { - case *[]any: - for _, el := range *xt { - switch et := e.Value.(type) { - case Elem: - return et.Compare(el) - } - } - } - case ConstraintOr: - for _, el := range e.Value.(Array) { - if el.Compare(x) { - return true - } - } - return false - case ConstraintAnd: - for _, el := range e.Value.(Array) { - if !el.Compare(x) { - return false - } - } - return true - default: - neq := e.Constraint == ConstraintValNotEqual - switch et := e.Value.(type) { - case Array: - switch xt := x.(type) { - case *[]any: - return et.Compare(*xt) != neq - } - case Object: - switch xt := x.(type) { - case *hamap.Map[string, any]: - return et.Compare(xt) != neq - } - default: - return CompareValues(e.Constraint, e.Value, x) - } - } - - return false -} - -// Compare checks two Arrays for equality. -func (arr Array) Compare(x []any) bool { - if len(arr) != len(x) { - return false - } - for i := 0; i < len(x); i++ { - if !arr[i].Compare(x[i]) { - return false - } - } - - return true -} - -// Compare checks two Objects for equality. -func (obj Object) Compare(x *hamap.Map[string, any]) (eq bool) { - var v Elem - eq = true - if len(obj) != x.Len() { - return false - } - x.Visit(func(key string, value any) (stop bool) { - v, eq = obj[key] - if !eq { - return true - } - eq = v.Compare(value) - return !eq - }) - - return -} - -// PrintNSpaces prints n spaces in a row. -func PrintNSpaces(w io.Writer, n uint) { - for i := uint(0); i < n; i++ { - _, _ = w.Write([]byte(" ")) - } -} - -// Print prints out the RulesMap object. -func (rm *RulesMap) Print(w io.Writer) { - rm.print(w, 0) -} - -func (rm *RulesMap) print(w io.Writer, indent uint) { - for hash, rn := range rm.rules { - fmt.Fprintf(w, "%d", hash) - _, _ = w.Write([]byte(":")) - if len(rn) > 0 { - _, _ = w.Write([]byte("\n")) - for _, v := range rn { - v.print(w, indent+4) - } - } - } -} - -func (v *Variant) print(w io.Writer, indent uint) { - PrintNSpaces(w, indent) - _, _ = w.Write(append([]byte(ConstraintLookup[v.Constraint]), []byte(": ")...)) - v.Mask.Visit(func(x int) (skip bool) { - fmt.Fprintf(w, "%d", x) - return false - }) - _, _ = w.Write([]byte("\n")) - if v.Value != nil { - switch v := v.Value.(type) { - case Elem: - v.print(w, indent+2) - case Array: - v.print(w, indent+2) - default: - PrintNSpaces(w, indent+2) - if s, ok := v.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, v) - } - } - } -} - -func (e *Elem) print(w io.Writer, indent uint) { - PrintNSpaces(w, indent) - _, _ = w.Write(append([]byte(ConstraintLookup[e.Constraint]), []byte(":\n")...)) - switch v := e.Value.(type) { - case Elem: - v.print(w, indent+2) - case Array: - v.print(w, indent+2) - case Object: - v.print(w, indent+2) - default: - PrintNSpaces(w, indent+2) - if s, ok := v.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, v) - } - } -} - -func (arr *Array) print(w io.Writer, indent uint) { - for _, el := range *arr { - PrintNSpaces(w, indent) - _, _ = w.Write([]byte("-:\n")) - PrintNSpaces(w, indent+2) - _, _ = w.Write(append([]byte(ConstraintLookup[el.Constraint]), []byte(":\n")...)) - switch v := el.Value.(type) { - case Elem: - v.print(w, indent+4) - case Array: - v.print(w, indent+4) - case Object: - v.print(w, indent+4) - default: - PrintNSpaces(w, indent+4) - if s, ok := v.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, v) - } - } - } -} - -func (obj *Object) print(w io.Writer, indent uint) { - for k, el := range *obj { - PrintNSpaces(w, indent) - _, _ = w.Write(append([]byte(k), []byte(":\n")...)) - PrintNSpaces(w, indent+2) - _, _ = w.Write(append([]byte(ConstraintLookup[el.Constraint]), []byte(":\n")...)) - switch v := el.Value.(type) { - case Elem: - v.print(w, indent+4) - case Array: - v.print(w, indent+4) - case Object: - v.print(w, indent+4) - default: - PrintNSpaces(w, indent+4) - if s, ok := v.([]byte); ok { - fmt.Fprintln(w, string(s)) - } else { - fmt.Fprintln(w, v) - } - } - } -} - -func memset[T comparable](a []T, v T) { - if len(a) == 0 { - return - } - a[0] = v - for i := 1; i < len(a); i *= 2 { - copy(a[i:], a[:i]) - } -} - -// Constraint is a constraint simplified abstraction. -type Constraint uint16 - -const ( - None Constraint = iota - ConstraintUnknown - ConstraintOr - ConstraintAnd - ConstraintAny - ConstraintMap - ConstraintTypeEqual - ConstraintTypeNotEqual - ConstraintValEqual - ConstraintValNotEqual - ConstraintValGreater - ConstraintValLess - ConstraintValGreaterOrEqual - ConstraintValLessOrEqual - ConstraintBytelenEqual - ConstraintBytelenNotEqual - ConstraintBytelenGreater - ConstraintBytelenLess - ConstraintBytelenGreaterOrEqual - ConstraintBytelenLessOrEqual - ConstraintLenEqual - ConstraintLenNotEqual - ConstraintLenGreater - ConstraintLenLess - ConstraintLenGreaterOrEqual - ConstraintLenLessOrEqual -) - -var ConstraintLookup = map[Constraint]string{ - None: "NoConstraint", - ConstraintOr: "ConstraintOr", - ConstraintAnd: "ConstraintAnd", - ConstraintAny: "ConstraintAny", - ConstraintMap: "ConstraintMap", - ConstraintTypeEqual: "ConstraintTypeEqual", - ConstraintTypeNotEqual: "ConstraintTypeNotEqual", - ConstraintValEqual: "ConstraintValEqual", - ConstraintValNotEqual: "ConstraintValNotEqual", - ConstraintValGreater: "ConstraintValGreater", - ConstraintValLess: "ConstraintValLess", - ConstraintValGreaterOrEqual: "ConstraintValGreaterOrEqual", - ConstraintValLessOrEqual: "ConstraintValLessOrEqual", - ConstraintBytelenEqual: "ConstraintBytelenEqual", - ConstraintBytelenNotEqual: "ConstraintBytelenNotEqual", - ConstraintBytelenGreater: "ConstraintBytelenGreater", - ConstraintBytelenLess: "ConstraintBytelenLess", - ConstraintBytelenGreaterOrEqual: "ConstraintBytelenGreaterOrEqual", - ConstraintBytelenLessOrEqual: "ConstraintBytelenLessOrEqual", - ConstraintLenEqual: "ConstraintLenEqual", - ConstraintLenNotEqual: "ConstraintLenNotEqual", - ConstraintLenGreater: "ConstraintLenGreater", - ConstraintLenLess: "ConstraintLenLess", - ConstraintLenGreaterOrEqual: "ConstraintLenGreaterOrEqual", - ConstraintLenLessOrEqual: "ConstraintLenLessOrEqual", -} diff --git a/engines/tests/assets/benchassets/_bench_deep/bench.yml b/engines/tests/assets/benchassets/_bench_deep/bench.yml deleted file mode 100644 index b24538a..0000000 --- a/engines/tests/assets/benchassets/_bench_deep/bench.yml +++ /dev/null @@ -1,120 +0,0 @@ -query: | - query X { - a { - a0 { - a00( - a00_0: { - a00_00: { - a00_000: -273 - } - a00_01: { - a00_010: "eagle" - a00_011: "hawk" - a00_012: "falcon" - } - a00_02: { - a00_020: [ "eagle", "hawk", "falcon" ] - a00_031: [ -273, -273, -273 ] - } - a00_03: { - a00_030: [ - { - a00_030x0: [0, 1] - a00_030x1: "x" - }, - { - a00_030x0: [1, 0] - a00_030x1: "y" - } - ] - } - } - a00_1: { - a00_10: { - a00_100: { - a00_1000: { - a00_10000: [ -13, -88 ] - a00_10001: "deep" - a00_10002: "not shallow" - } - } - a00_101: { - a00_1010: "hohoho" - a00_1011: -1 - } - } - } - ) { - a000( - a000_0: { - a000_00: { - a000_000: "coffe?" - a000_001: [ "yes", "please" ] - } - } - a000_1: { - a000_10: 0 - } - ) { - a0000( - a0000_0: { - a0000_00: 65535 - a0000_01: { - a0000_010: [ - { - a0000_01000: "I need your clothes, boots and motorcycle" - } - ] - } - } - ) - a0001 { - a00000( - a00000_0: { - a00000_00: { - a00000_000: { - a00000_0000: "Not enought wood" - a00000_0001: "Milord" - } - } - } - ) - } - } - a001( - a001_0: "surprise!" - a001_1: { - a001_10: { - a001_100: { - a001_1001: true - a001_1002: [false, true] - } - a001_101: { - a001_1010: { - a001_10100: "enough" - } - } - } - } - ) - } - } - a1 { - a10 { - a100( - a100_0: { - a100_00: [0, 1, 2, 3] - } - ) - a101( - a101_0: { - a101_00: { - a101_000: "putin huilo" - } - } - ) - } - } - } - } -operationName: X diff --git a/engines/tests/assets/benchassets/bench_average/bench.yml b/engines/tests/assets/benchassets/bench_average/bench.yml deleted file mode 100644 index 54396f4..0000000 --- a/engines/tests/assets/benchassets/bench_average/bench.yml +++ /dev/null @@ -1,17 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: { - a0_01: { - a0_010: JEDI - } - a0_00: [[0, 1]] - } - a0_1: 0 - ) { - a01 - } - } - } -operationName: X diff --git a/engines/tests/assets/benchassets/bench_big/bench.yml b/engines/tests/assets/benchassets/bench_big/bench.yml deleted file mode 100644 index 1273028..0000000 --- a/engines/tests/assets/benchassets/bench_big/bench.yml +++ /dev/null @@ -1,145 +0,0 @@ -query: | - query X ( - $b01_0: String! = "alive" - $b00_0: [Input!]! = [ - {b00_0x0: -273} - {b00_0x0: -273} - ] - ) { - ... on Query { - ... f1 - } - a { - a0( - a0_0: { - a0_00: [ - [ - { - a0_00xx0: [1, 0, 1] - a0_00xx1: [0, 1, 2, 3] - } - { - a0_00xx0: [69, -69] - a0_00xx1: [0, -1] - } - ] - [ - { - a0_00xx0: [-1, 0, 1] - a0_00xx1: [0, 1] - } - { - a0_00xx0: [1, 2] - a0_00xx1: [-1, -2] - } - ] - ] - a0_01: { - a0_010: JEDI - } - } - a0_1: -1 - ) { - a00( - a00_0: ["foo", "bar"] - a00_1: { - a00_10: 0 - a00_11: "lol" - } - ) - a01 - } - } - c( - c_0: [ - [ - [ - { - c_0xxx0: "too" - } - { - c_0xxx0: "deep" - } - ] - [ - { - c_0xxx0: "deep" - } - { - c_0xxx0: "again" - } - ] - [ - { - c_0xxx0: "1" - } - { - c_0xxx0: "2" - } - ] - ] - [ - [ - { - c_0xxx0: "what is this!?" - } - { - c_0xxx0: "what is that?!" - } - ] - [ - { - c_0xxx0: "nothing" - } - { - c_0xxx0: "special" - } - ] - [ - { - c_0xxx0: "just the wind" - } - { - c_0xxx0: "..." - } - ] - ] - ] - ) - } - - fragment f1 on Query { - ... f2 - } - - fragment f2 on Query { - b { - ... { - ... f3 - ... f3 - } - } - } - - fragment f3 on Something { - b0 { - b00( - b00_0: $b00_0 - b00_1: [ -13, -88 ] - ) - b01( - b01_0: $b01_0 - ) { - b010 - } - } - } -operationName: X -variables: | - { - "b01_0": "alive", - "b00_0": [ - {"b00_0x0": -273}, - {"b00_0x0": -273} - ] - } diff --git a/engines/tests/assets/benchassets/templates/0.gqt b/engines/tests/assets/benchassets/templates/0.gqt deleted file mode 100644 index 522f972..0000000 --- a/engines/tests/assets/benchassets/templates/0.gqt +++ /dev/null @@ -1,48 +0,0 @@ -query { - a { - combine 1 { - a0( - a0_0: val = { - a0_01: val = { - a0_010: val = JEDI - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 && val >= -9 - ) { - combine 2 { - a00( - a00_0: val = [ ... bytelen = 3 ] - a00_1: val = { - a00_11: val != "kek" && bytelen = 3 - a00_10: val < 1 || val > 9 - } - ) - a01 - a02 - } - } - a1 - } - } - c(c_0: val = [ ... val = [ ... len = 2 ] ]) - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val = -273 - } - ] - b00_1: val = [ ... val <= 0 && val > -99 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } -} diff --git a/engines/tests/assets/benchassets/templates/1.gqt b/engines/tests/assets/benchassets/templates/1.gqt deleted file mode 100644 index 6b786c9..0000000 --- a/engines/tests/assets/benchassets/templates/1.gqt +++ /dev/null @@ -1,47 +0,0 @@ -query { - a { - combine 2 { - a0( - a0_0: val = { - a0_01: val = { - a0_010: val = "JEDI" - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen = 3 ] - a00_1: val = { - a00_11: val != "kek" - a00_10: val < 1 - } - ) - } - a1 - a2 - } - } - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val = -273 - } - ] - b00_1: val = [ ... val <= 0 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - c(c_0: val = [ ... val = [ ... len = 2 ] ]) - d { - d0(d0_0: val > 0) - } -} diff --git a/engines/tests/assets/benchassets/templates/2.gqt b/engines/tests/assets/benchassets/templates/2.gqt deleted file mode 100644 index 239b147..0000000 --- a/engines/tests/assets/benchassets/templates/2.gqt +++ /dev/null @@ -1,25 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_01: val != { - a0_010: val = JEDI - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - combine 1 { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - } - c(c_0: val = [ ... len = 3 ]) -} diff --git a/engines/tests/assets/benchassets/templates/3.gqt b/engines/tests/assets/benchassets/templates/3.gqt deleted file mode 100644 index 7e10db0..0000000 --- a/engines/tests/assets/benchassets/templates/3.gqt +++ /dev/null @@ -1,55 +0,0 @@ -query { - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - val != { - b00_0x0: val > 0 - } - val = { - b00_0x0: val >= -999 - } - ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } - a { - a0( - a0_0: val = { - a0_01: val = { - a0_010: bytelen < 10 - } - a0_00: val = [ - val = [ - val = { - a0_00xx0: len < 5 - a0_00xx1: len >= 1 - } - val = { - a0_00xx0: val = [val >= 69, val <= -69] - a0_00xx1: val = [val = 0, val != 0] - } - ] - val = [ val = 2, val = 42] - ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } -} diff --git a/engines/tests/assets/benchassets/templates/4.gqt b/engines/tests/assets/benchassets/templates/4.gqt deleted file mode 100644 index 04f4dbe..0000000 --- a/engines/tests/assets/benchassets/templates/4.gqt +++ /dev/null @@ -1,46 +0,0 @@ -query { - c(c_0: val = [ ... len = 3 ]) - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val < 0 - } - ] - b00_1: val = [ ... val <= 0 && val > -99 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } - a { - a0( - a0_0: val = { - a0_00: val = [ ... len <= 2 ] - a0_01: val = { - a0_010: val = JEDI - } - } - a0_1: val <= 0 - ) { - combine 2 { - a01 - a02 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - a1 - } -} diff --git a/engines/tests/assets/benchassets/templates/5.gqt b/engines/tests/assets/benchassets/templates/5.gqt deleted file mode 100644 index d029877..0000000 --- a/engines/tests/assets/benchassets/templates/5.gqt +++ /dev/null @@ -1,45 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_01: val != { - a0_010: val != JEDI - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - combine 1 { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - } - b { - b0 { - b00( - b00_0: val = [ - val != { - b00_0x0: val > 0 - } - val = { - b00_0x0: val >= -999 - } - ] - b00_1: val = [ ... val < 0 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - c(c_0: val = [ ... len = 3 ]) -} diff --git a/engines/tests/assets/benchassets/templates/6.gqt b/engines/tests/assets/benchassets/templates/6.gqt deleted file mode 100644 index d08132f..0000000 --- a/engines/tests/assets/benchassets/templates/6.gqt +++ /dev/null @@ -1,118 +0,0 @@ -query { - a { - a0 { - a00( - a00_0: val = { - a00_00: val = { - a00_000: val = -273 - } - a00_01: val = { - a00_010: val = "eagle" - a00_011: val = "hawk" - a00_012: val = "falcon" - } - a00_02: val = { - a00_020: val = [val = "eagle", val = "hawk", val = "falcon"] - a00_031: val = [val = -273, val = -273, val = -273] - } - a00_03: val = { - a00_030: val = [ - val = { - a00_030x0: val = [val = 0, val = 1] - a00_030x1: val = "x" - }, - val = { - a00_030x0: val = [val = 1, val = 0] - a00_030x1: val = "y" - } - ] - } - } - a00_1: val = { - a00_10: val = { - a00_100: val = { - a00_1000: val = { - a00_10000: val = [val = -13, val = -88] - a00_10001: val = "deep" - a00_10002: val = "not shallow" - } - } - a00_101: val = { - a00_1010: val = "hohoho" - a00_1011: val = -1 - } - } - } - ) { - a000( - a000_0: val = { - a000_00: val = { - a000_000: val = "coffe?" - a000_001: val = [val = "yes", val = "please"] - } - } - a000_1: val = { - a000_10: val = 0 - } - ) { - a0000( - a0000_0: val = { - a0000_00: val = 65535 - a0000_01: val = { - a0000_010: val = [ - val = { - a0000_01000: val = "I need your clothes, boots and motorcycle" - } - ] - } - } - ) - a0001 { - a00000( - a00000_0: val = { - a00000_00: val = { - a00000_000: val = { - a00000_0000: val = "Not enought wood" - a00000_0001: val = "Milord" - } - } - } - ) - } - } - a001( - a001_0: val = "surprise!" - a001_1: val = { - a001_10: val = { - a001_100: val = { - a001_1001: val = true - a001_1002: val = [val = false, val = true] - } - a001_101: val = { - a001_1010: val = { - a001_10100: val = "enough" - } - } - } - } - ) - } - } - a1 { - a10 { - a100( - a100_0: val = { - a100_00: val = [val = 0, val = 1, val = 2, val = 3] - } - ) - a101( - a101_0: val = { - a101_00: val = { - a101_000: val = "putin huilo" - } - } - ) - } - } - } -} diff --git a/engines/tests/assets/benchassets/templates/7.gqt b/engines/tests/assets/benchassets/templates/7.gqt deleted file mode 100644 index 9ea67cf..0000000 --- a/engines/tests/assets/benchassets/templates/7.gqt +++ /dev/null @@ -1,118 +0,0 @@ -query { - a { - a0 { - a00( - a00_0: val = { - a00_00: val = { - a00_000: val < 0 - } - a00_01: val = { - a00_010: val != "hawk" - a00_011: val != "falcon" - a00_012: val != "eagle" - } - a00_02: val = { - a00_020: val = [ ... val != "cock" ] - a00_031: val = [ ... val = -273 ] - } - a00_03: val = { - a00_030: val = [ - val = { - a00_030x0: len > 0 - a00_030x1: val != "y" - }, - val != { - a00_030x0: len = 0 - a00_030x1: val != "x" - } - ] - } - } - a00_1: val = { - a00_10: val = { - a00_100: val != { - a00_1000: val != { - a00_10000: val = [val = -13, val = -88] - a00_10001: val = "deep" - a00_10002: val = "not shallow" - } - } - a00_101: val = { - a00_1010: bytelen > 0 - a00_1011: val < 0 - } - } - } - ) { - a000( - a000_0: val = { - a000_00: val = { - a000_000: bytelen > 0 - a000_001: val = [ ... bytelen > 0 ] - } - } - a000_1: val != { - a000_10: val != 0 - } - ) { - a0000( - a0000_0: val = { - a0000_00: val < 65536 - a0000_01: val != { - a0000_010: val != [ - val != { - a0000_01000: val != "I need your clothes, boots and motorcycle" - } - ] - } - } - ) - a0001 { - a00000( - a00000_0: val = { - a00000_00: val != { - a00000_000: val != { - a00000_0000: val = "Not enought wood" - a00000_0001: val = "Milord" - } - } - } - ) - } - } - a001( - a001_0: val != "no prise" - a001_1: val = { - a001_10: val = { - a001_100: val = { - a001_1001: val = true - a001_1002: val = [val = false, val = true] - } - a001_101: val = { - a001_1010: val = { - a001_10100: val = "enough" - } - } - } - } - ) - } - } - a1 { - a10 { - a100( - a100_0: val = { - a100_00: val = [ ... val >= 0 ] - } - ) - a101( - a101_0: val = { - a101_00: val = { - a101_000: val != "putin ne huilo" - } - } - ) - } - } - } -} diff --git a/engines/tests/assets/benchassets/templates/8.tgqt b/engines/tests/assets/benchassets/templates/8.tgqt deleted file mode 100644 index 452d316..0000000 --- a/engines/tests/assets/benchassets/templates/8.tgqt +++ /dev/null @@ -1,43 +0,0 @@ -query { - a { - a0( - a0_0: val = { - a0_01: val = { - a0_010: val = "yo" - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen = 3 ] - a00_1: val = { - a00_11: val != "kek" - a00_10: val < 1 - } - ) - } - } - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val = -273 - } - ] - b00_1: val = [ ... val <= 0 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - c(c_0: val = [ ... val = [ ... len = 2 ] ]) - d { - d0(d0_0: val > 0) - } -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_00/0.gqt b/engines/tests/assets/testassets/test_arguments_merge_00/0.gqt deleted file mode 100644 index 490e9f7..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_00/0.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a( - a_0: val = [val = 0] - a_1: val = [val = [ val = 0 ]] - a_2: val = [ - val = { - a_20: val = 0 - } - ] - a_3: val = [ ... bytelen > 0 ] - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_00/1.gqt b/engines/tests/assets/testassets/test_arguments_merge_00/1.gqt deleted file mode 100644 index 490e9f7..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_00/1.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a( - a_0: val = [val = 0] - a_1: val = [val = [ val = 0 ]] - a_2: val = [ - val = { - a_20: val = 0 - } - ] - a_3: val = [ ... bytelen > 0 ] - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_00/2.gqt b/engines/tests/assets/testassets/test_arguments_merge_00/2.gqt deleted file mode 100644 index 2be40fd..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_00/2.gqt +++ /dev/null @@ -1,13 +0,0 @@ -query { - a( - a_0: val = [val = 0, val = 1] - a_1: val = [val = [ val = 0 ], val = [ val = 1 ]] - a_2: val = [ - val = { - a_20: val = 0 - a_21: val = 1 - } - ] - a_3: val = [ bytelen > 0 ] - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_00/3.gqt b/engines/tests/assets/testassets/test_arguments_merge_00/3.gqt deleted file mode 100644 index d8e2c70..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_00/3.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a( - a_0: val = [val = 1] - a_1: val = [val = [ val = 1 ]] - a_2: val = [ - val = { - a_20: val = 1 - } - ] - a_3: val = [ ... bytelen = 0 ] - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_00/test.yml b/engines/tests/assets/testassets/test_arguments_merge_00/test.yml deleted file mode 100644 index 60373b3..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_00/test.yml +++ /dev/null @@ -1,16 +0,0 @@ -query: | - query X { - a( - a_0: [0] - a_1: [[0]] - a_2: [{ - a_20: 0 - }] - a_3: [ "0", "00" ] - ) - } -operationName: X -variables: -expect: - - 0 - - 1 diff --git a/engines/tests/assets/testassets/test_arguments_merge_01/0.gqt b/engines/tests/assets/testassets/test_arguments_merge_01/0.gqt deleted file mode 100644 index e122030..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_01/0.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a( - a_0: val = 0 - a_1: val = { - a_10: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_01/1.gqt b/engines/tests/assets/testassets/test_arguments_merge_01/1.gqt deleted file mode 100644 index e122030..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_01/1.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a( - a_0: val = 0 - a_1: val = { - a_10: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_01/2.gqt b/engines/tests/assets/testassets/test_arguments_merge_01/2.gqt deleted file mode 100644 index 4aabadd..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_01/2.gqt +++ /dev/null @@ -1,9 +0,0 @@ -query { - a( - a_0: val = 0 - a_1: val = { - a_10: val = 0 - a_11: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_01/3.gqt b/engines/tests/assets/testassets/test_arguments_merge_01/3.gqt deleted file mode 100644 index 4de37d5..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_01/3.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a( - a_0: val != 0 - a_1: val != { - a_10: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_arguments_merge_01/test.yml b/engines/tests/assets/testassets/test_arguments_merge_01/test.yml deleted file mode 100644 index 001863c..0000000 --- a/engines/tests/assets/testassets/test_arguments_merge_01/test.yml +++ /dev/null @@ -1,15 +0,0 @@ -query: | - query X { - a( - a_0: 0 - a_1: { - a_10: 0 - } - ) - } -operationName: X -variables: -expect: - - 0 - - 1 - - 2 diff --git a/engines/tests/assets/testassets/test_complex_00/0.gqt b/engines/tests/assets/testassets/test_complex_00/0.gqt deleted file mode 100644 index 522f972..0000000 --- a/engines/tests/assets/testassets/test_complex_00/0.gqt +++ /dev/null @@ -1,48 +0,0 @@ -query { - a { - combine 1 { - a0( - a0_0: val = { - a0_01: val = { - a0_010: val = JEDI - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 && val >= -9 - ) { - combine 2 { - a00( - a00_0: val = [ ... bytelen = 3 ] - a00_1: val = { - a00_11: val != "kek" && bytelen = 3 - a00_10: val < 1 || val > 9 - } - ) - a01 - a02 - } - } - a1 - } - } - c(c_0: val = [ ... val = [ ... len = 2 ] ]) - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val = -273 - } - ] - b00_1: val = [ ... val <= 0 && val > -99 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } -} diff --git a/engines/tests/assets/testassets/test_complex_00/1.gqt b/engines/tests/assets/testassets/test_complex_00/1.gqt deleted file mode 100644 index 6b786c9..0000000 --- a/engines/tests/assets/testassets/test_complex_00/1.gqt +++ /dev/null @@ -1,47 +0,0 @@ -query { - a { - combine 2 { - a0( - a0_0: val = { - a0_01: val = { - a0_010: val = "JEDI" - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen = 3 ] - a00_1: val = { - a00_11: val != "kek" - a00_10: val < 1 - } - ) - } - a1 - a2 - } - } - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val = -273 - } - ] - b00_1: val = [ ... val <= 0 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - c(c_0: val = [ ... val = [ ... len = 2 ] ]) - d { - d0(d0_0: val > 0) - } -} diff --git a/engines/tests/assets/testassets/test_complex_00/2.gqt b/engines/tests/assets/testassets/test_complex_00/2.gqt deleted file mode 100644 index 97b8cc2..0000000 --- a/engines/tests/assets/testassets/test_complex_00/2.gqt +++ /dev/null @@ -1,23 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_01: val != { - a0_010: val = "yo" - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - c(c_0: val = [ ... len = 3 ]) -} diff --git a/engines/tests/assets/testassets/test_complex_00/3.gqt b/engines/tests/assets/testassets/test_complex_00/3.gqt deleted file mode 100644 index 7e10db0..0000000 --- a/engines/tests/assets/testassets/test_complex_00/3.gqt +++ /dev/null @@ -1,55 +0,0 @@ -query { - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - val != { - b00_0x0: val > 0 - } - val = { - b00_0x0: val >= -999 - } - ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } - a { - a0( - a0_0: val = { - a0_01: val = { - a0_010: bytelen < 10 - } - a0_00: val = [ - val = [ - val = { - a0_00xx0: len < 5 - a0_00xx1: len >= 1 - } - val = { - a0_00xx0: val = [val >= 69, val <= -69] - a0_00xx1: val = [val = 0, val != 0] - } - ] - val = [ val = 2, val = 42] - ] - } - a0_1: val <= 0 - ) { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } -} diff --git a/engines/tests/assets/testassets/test_complex_00/4.gqt b/engines/tests/assets/testassets/test_complex_00/4.gqt deleted file mode 100644 index 04f4dbe..0000000 --- a/engines/tests/assets/testassets/test_complex_00/4.gqt +++ /dev/null @@ -1,46 +0,0 @@ -query { - c(c_0: val = [ ... len = 3 ]) - ... on Query { - b { - b0 { - b00( - b00_0: val = [ - ... val = { - b00_0x0: val < 0 - } - ] - b00_1: val = [ ... val <= 0 && val > -99 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - } - a { - a0( - a0_0: val = { - a0_00: val = [ ... len <= 2 ] - a0_01: val = { - a0_010: val = JEDI - } - } - a0_1: val <= 0 - ) { - combine 2 { - a01 - a02 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - a1 - } -} diff --git a/engines/tests/assets/testassets/test_complex_00/5.gqt b/engines/tests/assets/testassets/test_complex_00/5.gqt deleted file mode 100644 index d029877..0000000 --- a/engines/tests/assets/testassets/test_complex_00/5.gqt +++ /dev/null @@ -1,45 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_01: val != { - a0_010: val != JEDI - } - a0_00: val = [ ... len <= 2 ] - } - a0_1: val <= 0 - ) { - combine 1 { - a01 - a00( - a00_0: val = [ ... bytelen > 0 ] - a00_1: val = { - a00_10: val = 0 - a00_11: val = "lol" - } - ) - } - } - } - b { - b0 { - b00( - b00_0: val = [ - val != { - b00_0x0: val > 0 - } - val = { - b00_0x0: val >= -999 - } - ] - b00_1: val = [ ... val < 0 ] - ) - b01( - b01_0: bytelen > 1 - ) { - b010 - } - } - } - c(c_0: val = [ ... len = 3 ]) -} diff --git a/engines/tests/assets/testassets/test_complex_00/test.yml b/engines/tests/assets/testassets/test_complex_00/test.yml deleted file mode 100644 index c4eaac7..0000000 --- a/engines/tests/assets/testassets/test_complex_00/test.yml +++ /dev/null @@ -1,148 +0,0 @@ -query: | - query X ( - $b01_0: String! = "alive" - $b00_0: [Input!]! = [ - {b00_0x0: -273} - {b00_0x0: -273} - ] - ) { - ... on Query { - ... f1 - } - a { - a0( - a0_0: { - a0_00: [ - [ - { - a0_00xx0: [1, 0, 1] - a0_00xx1: [0, 1, 2, 3] - } - { - a0_00xx0: [69, -69] - a0_00xx1: [0, -1] - } - ] - [ - { - a0_00xx0: [-1, 0, 1] - a0_00xx1: [0, 1] - } - { - a0_00xx0: [1, 2] - a0_00xx1: [-1, -2] - } - ] - ] - a0_01: { - a0_010: JEDI - } - } - a0_1: -1 - ) { - a00( - a00_0: ["foo", "bar"] - a00_1: { - a00_10: 0 - a00_11: "lol" - } - ) - a01 - } - } - c( - c_0: [ - [ - [ - { - c_0xxx0: "too" - } - { - c_0xxx0: "deep" - } - ] - [ - { - c_0xxx0: "deep" - } - { - c_0xxx0: "again" - } - ] - [ - { - c_0xxx0: "1" - } - { - c_0xxx0: "2" - } - ] - ] - [ - [ - { - c_0xxx0: "what is this!?" - } - { - c_0xxx0: "what is that?!" - } - ] - [ - { - c_0xxx0: "nothing" - } - { - c_0xxx0: "special" - } - ] - [ - { - c_0xxx0: "just the wind" - } - { - c_0xxx0: "..." - } - ] - ] - ] - ) - } - - fragment f1 on Query { - ... f2 - } - - fragment f2 on Query { - b { - ... { - ... f3 - ... f3 - } - } - } - - fragment f3 on Something { - b0 { - b00( - b00_0: $b00_0 - b00_1: [ -13, -88 ] - ) - b01( - b01_0: $b01_0 - ) { - b010 - } - } - } -operationName: X -variables: | - { - "b01_0": "alive", - "b00_0": [ - {"b00_0x0": -273}, - {"b00_0x0": -273} - ] - } -expect: - - 0 - - 4 diff --git a/engines/tests/assets/testassets/test_conditions_00/0.gqt b/engines/tests/assets/testassets/test_conditions_00/0.gqt deleted file mode 100644 index 103cb41..0000000 --- a/engines/tests/assets/testassets/test_conditions_00/0.gqt +++ /dev/null @@ -1,34 +0,0 @@ -query { - a( - a_0: val = 0 - a_1: val != 0 - a_2: val < 0 - a_3: val <= 0 - a_4: val > 0 - a_5: val >= 0 - ) - b( - b_0: val = 0.0 - b_1: val != 0.0 - b_2: val < 0.0 - b_3: val <= 0.0 - b_4: val > 0.0 - b_5: val >= 0.0 - ) - c( - c_0: bytelen = 1 - c_1: bytelen != 1 - c_2: bytelen < 1 - c_3: bytelen <= 1 - c_4: bytelen > 1 - c_5: bytelen >= 1 - ) - d( - d_0: len = 1 - d_1: len != 1 - d_2: len < 1 - d_3: len <= 1 - d_4: len > 1 - d_5: len >= 1 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_00/test.yml b/engines/tests/assets/testassets/test_conditions_00/test.yml deleted file mode 100644 index 2d40f1d..0000000 --- a/engines/tests/assets/testassets/test_conditions_00/test.yml +++ /dev/null @@ -1,39 +0,0 @@ -query: | - query X { - a( - a_0: 0 - a_1: 1 - a_2: -1 - a_3: 0 - a_4: 1 - a_5: 0 - ) - b( - b_0: 0.0 - b_1: 1.0 - b_2: -1.0 - b_3: 0.0 - b_4: 1.0 - b_5: 0.0 - ) - c( - c_0: "a" - c_1: "aa" - c_2: "" - c_3: "a" - c_4: "aa" - c_5: "a" - ) - d( - d_0: [0] - d_1: [0 0] - d_2: [] - d_3: [0] - d_4: [0 0] - d_5: [0] - ) - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_conditions_01/0.gqt b/engines/tests/assets/testassets/test_conditions_01/0.gqt deleted file mode 100644 index 8459074..0000000 --- a/engines/tests/assets/testassets/test_conditions_01/0.gqt +++ /dev/null @@ -1,77 +0,0 @@ -query { - a0( - a0_0: val = 0 - ) - a1( - a1_0: val != 0 - ) - a2( - a2_0: val < 0 - ) - a3( - a3_0: val <= 0 - ) - a4( - a4_0: val > 0 - ) - a5( - a5_0: val >= 0 - ) - - b0( - b0_0: val = 0.0 - ) - b1( - b1_0: val != 0.0 - ) - b2( - b2_0: val < 0.0 - ) - b3( - b3_0: val <= 0.0 - ) - b4( - b4_0: val > 0.0 - ) - b5( - b6_0: val >= 0.0 - ) - - c0( - c0_0: bytelen = 1 - ) - c1( - c1_0: bytelen != 1 - ) - c2( - c2_0: bytelen < 1 - ) - c3( - c3_0: bytelen <= 1 - ) - c4( - c4_0: bytelen > 1 - ) - c5( - c5_0: bytelen >= 1 - ) - - d0( - d0_0: len = 1 - ) - d1( - d1_0: len != 1 - ) - d2( - d2_0: len < 1 - ) - d3( - d3_0: len <= 1 - ) - d4( - d4_0: len > 1 - ) - d5( - d5_0: len >= 1 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_01/1.gqt b/engines/tests/assets/testassets/test_conditions_01/1.gqt deleted file mode 100644 index cc76074..0000000 --- a/engines/tests/assets/testassets/test_conditions_01/1.gqt +++ /dev/null @@ -1,77 +0,0 @@ -query { - a0( - a0_0: val = 1 - ) - a1( - a1_0: val != 1 - ) - a2( - a2_0: val < 1 - ) - a3( - a3_0: val <= 1 - ) - a4( - a4_0: val > -1 - ) - a5( - a5_0: val >= -1 - ) - - b0( - b0_0: val = 1.0 - ) - b1( - b1_0: val != 1.0 - ) - b2( - b2_0: val < 1.0 - ) - b3( - b3_0: val <= 1.0 - ) - b4( - b4_0: val > -1.0 - ) - b5( - b5_0: val >= -1.0 - ) - - c0( - c0_0: bytelen = 0 - ) - c1( - c1_0: bytelen != 0 - ) - c2( - c2_0: bytelen < 2 - ) - c3( - c3_0: bytelen <= 2 - ) - c4( - c4_0: bytelen > 0 - ) - c5( - c5_0: bytelen >= 0 - ) - - d0( - d0_0: len = 0 - ) - d1( - d1_0: len != 0 - ) - d2( - d2_0: len < 2 - ) - d3( - d3_0: len <= 2 - ) - d4( - d4_0: len > 0 - ) - d5( - d5_0: len >= 0 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_01/test.yml b/engines/tests/assets/testassets/test_conditions_01/test.yml deleted file mode 100644 index 546ddf2..0000000 --- a/engines/tests/assets/testassets/test_conditions_01/test.yml +++ /dev/null @@ -1,82 +0,0 @@ -query: | - query X { - a0( - a0_0: 1 - ) - a1( - a1_0: 0 - ) - a2( - a2_0: 0 - ) - a3( - a3_0: 1 - ) - a4( - a4_0: 0 - ) - a5( - a5_0: -1 - ) - - b0( - b0_0: 1.0 - ) - b1( - b1_0: 0.0 - ) - b2( - b2_0: 0.0 - ) - b3( - b3_0: 1.0 - ) - b4( - b4_0: 0.0 - ) - b5( - b5_0: -1.0 - ) - - c0( - c0_0: "" - ) - c1( - c1_0: "a" - ) - c2( - c2_0: "a" - ) - c3( - c3_0: "aa" - ) - c4( - c4_0: "a" - ) - c5( - c5_0: "" - ) - - d0( - d0_0: [] - ) - d1( - d1_0: [0] - ) - d2( - d2_0: [0] - ) - d3( - d3_0: [0 0] - ) - d4( - d4_0: [0] - ) - d5( - d5_0: [] - ) - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_conditions_02/0.gqt b/engines/tests/assets/testassets/test_conditions_02/0.gqt deleted file mode 100644 index aa5c15f..0000000 --- a/engines/tests/assets/testassets/test_conditions_02/0.gqt +++ /dev/null @@ -1,20 +0,0 @@ -query { - a0( - a0_0: val < 0 - ) - - b0( - b0_0: val < 0.0 - ) - - c0( - c0_0: bytelen = 1 - ) - - d0( - d0_0: len = 1.0 - ) - d1( - d1_0: len = 1 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_02/1.gqt b/engines/tests/assets/testassets/test_conditions_02/1.gqt deleted file mode 100644 index 6a8145a..0000000 --- a/engines/tests/assets/testassets/test_conditions_02/1.gqt +++ /dev/null @@ -1,20 +0,0 @@ -query { - a0( - a0_0: val < 0.0 - ) - - b0( - b0_0: val < 0 - ) - - c0( - c0_0: val = 0 - ) - - d0( - d0_0: len = 1 - ) - d1( - d1_0: val = 0 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_02/test.yml b/engines/tests/assets/testassets/test_conditions_02/test.yml deleted file mode 100644 index 404ba43..0000000 --- a/engines/tests/assets/testassets/test_conditions_02/test.yml +++ /dev/null @@ -1,25 +0,0 @@ -query: | - query X { - a0( - a0_0: -1.0 - ) - - b0( - b0_0: -1 - ) - - c0( - c0_0: 0 - ) - - d0( - d0_0: [0] - ) - d1( - d1_0: 0 - ) - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_conditions_03/0.gqt b/engines/tests/assets/testassets/test_conditions_03/0.gqt deleted file mode 100644 index ebf2c31..0000000 --- a/engines/tests/assets/testassets/test_conditions_03/0.gqt +++ /dev/null @@ -1,20 +0,0 @@ -query { - a( - a_0: val > 0 && val <= 9 || val < 0 && val >= -9 - a_1: val = NEWHOPE || val = EMPIRE - a_2: val = [ val = { - a_xx0: val = 0 - } - ] - a_3: val = [ val < 0 && val > -9, val > 0 ] - a_4: val = [ val <= 0 ] && val != [ val = -1 ] - a_5: val = [ ... bytelen > 0 && bytelen != 3 ] - a_6: val = [ ... val < 0 || val > 0 ] - a_7: val = [ ... val = [ val != 0 ] ] - a_8: val = [ ... val = { - a_xx0: val = 0 - } - ] - a_9: val = [ ... val = [ ... len > 0 ] ] - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_03/test.yml b/engines/tests/assets/testassets/test_conditions_03/test.yml deleted file mode 100644 index 780791f..0000000 --- a/engines/tests/assets/testassets/test_conditions_03/test.yml +++ /dev/null @@ -1,36 +0,0 @@ -query: | - query X { - a( - a_0: 9 - a_1: EMPIRE - a_2: [ - { - a_xx0: 0 - } - ] - a_3: [ -1, 1 ] - a_4: [ -2 ] - a_5: [ "test", "object" ] - a_6: [ -1 ] - a_7: [ [ 1 ] ] - a_8: [ - { - a_xx0: 0 - }, - { - a_xx0: 0 - } - ] - a_9: [ - [ - [ - 0 - ] - ] - ] - ) - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_conditions_04/0.gqt b/engines/tests/assets/testassets/test_conditions_04/0.gqt deleted file mode 100644 index cbff7df..0000000 --- a/engines/tests/assets/testassets/test_conditions_04/0.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a0( - a0_0: val > 0 && val <= 9 || val < 0 && val >= -9 - ) - a1( - a1_0: val > 0 && val < 9 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_04/1.gqt b/engines/tests/assets/testassets/test_conditions_04/1.gqt deleted file mode 100644 index 2746cb5..0000000 --- a/engines/tests/assets/testassets/test_conditions_04/1.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a0( - a0_0: val >= 0 && val <= 9 || val < 0 && val >= -9 - ) - a1( - a1_0: val >= 0 && val < 9 - ) -} diff --git a/engines/tests/assets/testassets/test_conditions_04/test.yml b/engines/tests/assets/testassets/test_conditions_04/test.yml deleted file mode 100644 index 72dc9a0..0000000 --- a/engines/tests/assets/testassets/test_conditions_04/test.yml +++ /dev/null @@ -1,13 +0,0 @@ -query: | - query X { - a0( - a0_0: 0 - ) - a1( - a1_0: 0 - ) - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_enum_00/0.gqt b/engines/tests/assets/testassets/test_enum_00/0.gqt deleted file mode 100644 index ea0d478..0000000 --- a/engines/tests/assets/testassets/test_enum_00/0.gqt +++ /dev/null @@ -1,6 +0,0 @@ -query { - a( - a_0: val = [ ... val = NEWHOPE || val = EMPIRE || val = JEDI] - a_1: val = NEWHOPE || val = EMPIRE || val = JEDI - ) -} diff --git a/engines/tests/assets/testassets/test_enum_00/1.gqt b/engines/tests/assets/testassets/test_enum_00/1.gqt deleted file mode 100644 index 6704f70..0000000 --- a/engines/tests/assets/testassets/test_enum_00/1.gqt +++ /dev/null @@ -1,6 +0,0 @@ -query { - a( - a_0: val = [ ... val = JEDI || val = EMPIRE || val = NEWHOPE] - a_1: val = JEDI || val = EMPIRE || val = NEWHOPE - ) -} diff --git a/engines/tests/assets/testassets/test_enum_00/test.yml b/engines/tests/assets/testassets/test_enum_00/test.yml deleted file mode 100644 index c24f7c5..0000000 --- a/engines/tests/assets/testassets/test_enum_00/test.yml +++ /dev/null @@ -1,12 +0,0 @@ -query: | - query X { - a( - a_0: [NEWHOPE, JEDI] - a_1: EMPIRE - ) - } -operationName: X -variables: -expect: - - 0 - - 1 diff --git a/engines/tests/assets/testassets/test_fragments_00/0.gqt b/engines/tests/assets/testassets/test_fragments_00/0.gqt deleted file mode 100644 index e726470..0000000 --- a/engines/tests/assets/testassets/test_fragments_00/0.gqt +++ /dev/null @@ -1,6 +0,0 @@ -query { - a - ... on Something { - b - } -} diff --git a/engines/tests/assets/testassets/test_fragments_00/test.yml b/engines/tests/assets/testassets/test_fragments_00/test.yml deleted file mode 100644 index 65c8c55..0000000 --- a/engines/tests/assets/testassets/test_fragments_00/test.yml +++ /dev/null @@ -1,11 +0,0 @@ -query: | - query X { - a - ... on Something { - b - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_fragments_01/0.gqt b/engines/tests/assets/testassets/test_fragments_01/0.gqt deleted file mode 100644 index 815cd78..0000000 --- a/engines/tests/assets/testassets/test_fragments_01/0.gqt +++ /dev/null @@ -1,9 +0,0 @@ -query { - a - ... on Something { - b - } - Something { - b - } -} diff --git a/engines/tests/assets/testassets/test_fragments_01/test.yml b/engines/tests/assets/testassets/test_fragments_01/test.yml deleted file mode 100644 index 86477d6..0000000 --- a/engines/tests/assets/testassets/test_fragments_01/test.yml +++ /dev/null @@ -1,14 +0,0 @@ -query: | - query X { - a - ... on Something { - b - } - Something { - b - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_fragments_02/0.gqt b/engines/tests/assets/testassets/test_fragments_02/0.gqt deleted file mode 100644 index 86e9191..0000000 --- a/engines/tests/assets/testassets/test_fragments_02/0.gqt +++ /dev/null @@ -1,11 +0,0 @@ -query { - a - ... on Query { - b { - b0 { - b00 - b01 - } - } - } -} diff --git a/engines/tests/assets/testassets/test_fragments_02/test.yml b/engines/tests/assets/testassets/test_fragments_02/test.yml deleted file mode 100644 index dddf41a..0000000 --- a/engines/tests/assets/testassets/test_fragments_02/test.yml +++ /dev/null @@ -1,31 +0,0 @@ -query: | - query X { - a - ... on Query { - ... f1 - } - } - - fragment f1 on Query { - ... f2 - } - - fragment f2 on Query { - b { - ... { - ... f3 - ... f3 - } - } - } - - fragment f3 on Something { - b0 { - b00 - b01 - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_fragments_03/0.gqt b/engines/tests/assets/testassets/test_fragments_03/0.gqt deleted file mode 100644 index ff13301..0000000 --- a/engines/tests/assets/testassets/test_fragments_03/0.gqt +++ /dev/null @@ -1,17 +0,0 @@ -query { - a - b { - b0 { - b00 - b01 - } - } - ... on Query { - b { - b0 { - b00 - b01 - } - } - } -} diff --git a/engines/tests/assets/testassets/test_fragments_03/test.yml b/engines/tests/assets/testassets/test_fragments_03/test.yml deleted file mode 100644 index c307955..0000000 --- a/engines/tests/assets/testassets/test_fragments_03/test.yml +++ /dev/null @@ -1,29 +0,0 @@ -query: | - query X { - a - ... f1 - } - - fragment f1 on Query { - ... f2 - } - - fragment f2 on Query { - b { - ... { - ... f3 - ... f3 - } - } - } - - fragment f3 on Something { - b0 { - b00 - b01 - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_general_logic_00/0.gqt b/engines/tests/assets/testassets/test_general_logic_00/0.gqt deleted file mode 100644 index 8d4dd10..0000000 --- a/engines/tests/assets/testassets/test_general_logic_00/0.gqt +++ /dev/null @@ -1,3 +0,0 @@ -query { - b -} diff --git a/engines/tests/assets/testassets/test_general_logic_00/test.yml b/engines/tests/assets/testassets/test_general_logic_00/test.yml deleted file mode 100644 index 3e5ab8c..0000000 --- a/engines/tests/assets/testassets/test_general_logic_00/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -query: | - query X { - a - b - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_general_logic_01/0.gqt b/engines/tests/assets/testassets/test_general_logic_01/0.gqt deleted file mode 100644 index e2cac4a..0000000 --- a/engines/tests/assets/testassets/test_general_logic_01/0.gqt +++ /dev/null @@ -1,8 +0,0 @@ -query { - a { - a0 { - a00 - } - a1 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_01/test.yml b/engines/tests/assets/testassets/test_general_logic_01/test.yml deleted file mode 100644 index fb02813..0000000 --- a/engines/tests/assets/testassets/test_general_logic_01/test.yml +++ /dev/null @@ -1,13 +0,0 @@ -query: | - query X { - a { - a0 { - a00 - } - a1 - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_general_logic_02/0.gqt b/engines/tests/assets/testassets/test_general_logic_02/0.gqt deleted file mode 100644 index 291a7d9..0000000 --- a/engines/tests/assets/testassets/test_general_logic_02/0.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - a { - a0( - a0_0: val = 1 - ) { - a00 - } - a1 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_02/test.yml b/engines/tests/assets/testassets/test_general_logic_02/test.yml deleted file mode 100644 index fe8505c..0000000 --- a/engines/tests/assets/testassets/test_general_logic_02/test.yml +++ /dev/null @@ -1,15 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: 1 - ) { - a00 - } - a1 - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_general_logic_03/0.gqt b/engines/tests/assets/testassets/test_general_logic_03/0.gqt deleted file mode 100644 index 1111c75..0000000 --- a/engines/tests/assets/testassets/test_general_logic_03/0.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_00: val = 1 - a0_01: val >= 0 - } - ) { - a00 - } - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_03/test.yml b/engines/tests/assets/testassets/test_general_logic_03/test.yml deleted file mode 100644 index d96d8da..0000000 --- a/engines/tests/assets/testassets/test_general_logic_03/test.yml +++ /dev/null @@ -1,7 +0,0 @@ -query: | - query X { - a - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_general_logic_04/0.gqt b/engines/tests/assets/testassets/test_general_logic_04/0.gqt deleted file mode 100644 index 1111c75..0000000 --- a/engines/tests/assets/testassets/test_general_logic_04/0.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_00: val = 1 - a0_01: val >= 0 - } - ) { - a00 - } - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_04/test.yml b/engines/tests/assets/testassets/test_general_logic_04/test.yml deleted file mode 100644 index 9487b80..0000000 --- a/engines/tests/assets/testassets/test_general_logic_04/test.yml +++ /dev/null @@ -1,12 +0,0 @@ -query: | - query X { - a { - a0 { - a00 - } - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_general_logic_05/0.gqt b/engines/tests/assets/testassets/test_general_logic_05/0.gqt deleted file mode 100644 index 1111c75..0000000 --- a/engines/tests/assets/testassets/test_general_logic_05/0.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_00: val = 1 - a0_01: val >= 0 - } - ) { - a00 - } - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_05/1.gqt b/engines/tests/assets/testassets/test_general_logic_05/1.gqt deleted file mode 100644 index d09aa45..0000000 --- a/engines/tests/assets/testassets/test_general_logic_05/1.gqt +++ /dev/null @@ -1,5 +0,0 @@ -query { - a { - a0 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_05/test.yml b/engines/tests/assets/testassets/test_general_logic_05/test.yml deleted file mode 100644 index 76e5039..0000000 --- a/engines/tests/assets/testassets/test_general_logic_05/test.yml +++ /dev/null @@ -1,10 +0,0 @@ -query: | - query X { - a { - a0 - } - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_general_logic_06/0.gqt b/engines/tests/assets/testassets/test_general_logic_06/0.gqt deleted file mode 100644 index 2909e60..0000000 --- a/engines/tests/assets/testassets/test_general_logic_06/0.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_00: val = 1 - a0_01: val >= 0 - } - ) - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_06/1.gqt b/engines/tests/assets/testassets/test_general_logic_06/1.gqt deleted file mode 100644 index d09aa45..0000000 --- a/engines/tests/assets/testassets/test_general_logic_06/1.gqt +++ /dev/null @@ -1,5 +0,0 @@ -query { - a { - a0 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_06/test.yml b/engines/tests/assets/testassets/test_general_logic_06/test.yml deleted file mode 100644 index 76e5039..0000000 --- a/engines/tests/assets/testassets/test_general_logic_06/test.yml +++ /dev/null @@ -1,10 +0,0 @@ -query: | - query X { - a { - a0 - } - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_general_logic_07/0.gqt b/engines/tests/assets/testassets/test_general_logic_07/0.gqt deleted file mode 100644 index 5b02b5b..0000000 --- a/engines/tests/assets/testassets/test_general_logic_07/0.gqt +++ /dev/null @@ -1,13 +0,0 @@ -query { - a { - a0( - a0_0: val != { - a0_00: val = 1 - a0_01: val >= 0 - } - a0_1: val != "never" - ) { - a00 - } - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_07/test.yml b/engines/tests/assets/testassets/test_general_logic_07/test.yml deleted file mode 100644 index 6f08eb4..0000000 --- a/engines/tests/assets/testassets/test_general_logic_07/test.yml +++ /dev/null @@ -1,16 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: { - a0_00: 1 - } - a0_1: "never" - ) { - a00 - } - } - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_general_logic_08/0.gqt b/engines/tests/assets/testassets/test_general_logic_08/0.gqt deleted file mode 100644 index 76232f9..0000000 --- a/engines/tests/assets/testassets/test_general_logic_08/0.gqt +++ /dev/null @@ -1,15 +0,0 @@ -mutation { - a { - a0( - a0_0: val = { - a0_00: val = 1 - a0_01: val >= 0 - } - a0_1: val != "never" - ) { - a00 { - a001 - } - } - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_08/test.yml b/engines/tests/assets/testassets/test_general_logic_08/test.yml deleted file mode 100644 index e5e809e..0000000 --- a/engines/tests/assets/testassets/test_general_logic_08/test.yml +++ /dev/null @@ -1,18 +0,0 @@ -query: | - mutation X { - a { - a0( - a0_0: { - a0_00: 1 - } - a0_1: "never" - ) { - a00 { - a000 - } - } - } - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_general_logic_09/0.gqt b/engines/tests/assets/testassets/test_general_logic_09/0.gqt deleted file mode 100644 index 991cd8a..0000000 --- a/engines/tests/assets/testassets/test_general_logic_09/0.gqt +++ /dev/null @@ -1,6 +0,0 @@ -query { - a( - a_0: val < 0 - a_1: val > 0 - ) -} diff --git a/engines/tests/assets/testassets/test_general_logic_09/test.yml b/engines/tests/assets/testassets/test_general_logic_09/test.yml deleted file mode 100644 index ab7a563..0000000 --- a/engines/tests/assets/testassets/test_general_logic_09/test.yml +++ /dev/null @@ -1,10 +0,0 @@ -query: | - query X { - a( - a_0: -1 - ) - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_general_logic_10/0.gqt b/engines/tests/assets/testassets/test_general_logic_10/0.gqt deleted file mode 100644 index 9b0fd65..0000000 --- a/engines/tests/assets/testassets/test_general_logic_10/0.gqt +++ /dev/null @@ -1,12 +0,0 @@ -query { - a( - a_0: val = [val = 1, val = 2, val = 3] - a_1: val = [ - val = { - a_10: val = 1 - a_11: val = 2 - a_12: val = 3 - } - ] - ) -} diff --git a/engines/tests/assets/testassets/test_general_logic_10/1.gqt b/engines/tests/assets/testassets/test_general_logic_10/1.gqt deleted file mode 100644 index cb539c5..0000000 --- a/engines/tests/assets/testassets/test_general_logic_10/1.gqt +++ /dev/null @@ -1,14 +0,0 @@ -query { - a( - a_0: val = [val = 1, val = 2] - a_1: val = [ - val = { - a_10: val = 1 - a_11: val = 2 - } - ] - a_2: val = { - a_20: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_general_logic_10/2.gqt b/engines/tests/assets/testassets/test_general_logic_10/2.gqt deleted file mode 100644 index 1cf47d9..0000000 --- a/engines/tests/assets/testassets/test_general_logic_10/2.gqt +++ /dev/null @@ -1,15 +0,0 @@ -query { - a( - a_0: val = [val = 0, val = 1, val = 2] - a_1: val = [ - val = { - a_10: val = 0 - a_11: val = 1 - a_12: val = 2 - } - ] - a_2: val = { - a_20: val = 0 - } - ) -} diff --git a/engines/tests/assets/testassets/test_general_logic_10/test.yml b/engines/tests/assets/testassets/test_general_logic_10/test.yml deleted file mode 100644 index c705260..0000000 --- a/engines/tests/assets/testassets/test_general_logic_10/test.yml +++ /dev/null @@ -1,14 +0,0 @@ -query: | - query X { - a( - a_0: [1, 2] - a_1: [{ - a_10: 1 - a_11: 2 - }] - ) - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_general_logic_11/0.gqt b/engines/tests/assets/testassets/test_general_logic_11/0.gqt deleted file mode 100644 index 449f3ea..0000000 --- a/engines/tests/assets/testassets/test_general_logic_11/0.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - a( - a_0: bytelen > 0 - a_1: any - a_2: val < 0 - ) { - a0 - a1 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_11/1.gqt b/engines/tests/assets/testassets/test_general_logic_11/1.gqt deleted file mode 100644 index 73cf539..0000000 --- a/engines/tests/assets/testassets/test_general_logic_11/1.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - a( - a_0: bytelen > 0 - a_1: val != null - a_2: val >= 0 - ) { - a0 - a1 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_11/2.gqt b/engines/tests/assets/testassets/test_general_logic_11/2.gqt deleted file mode 100644 index d90cca5..0000000 --- a/engines/tests/assets/testassets/test_general_logic_11/2.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - a( - a_0: bytelen > 0 - a_1: any - a_2: val >= 0 - ) { - a0 - a1 - } -} diff --git a/engines/tests/assets/testassets/test_general_logic_11/test.yml b/engines/tests/assets/testassets/test_general_logic_11/test.yml deleted file mode 100644 index a5c22dd..0000000 --- a/engines/tests/assets/testassets/test_general_logic_11/test.yml +++ /dev/null @@ -1,15 +0,0 @@ -query: | - query X { - a( - a_0: "well" - a_1: null - a_2: 0 - ) { - a0 - a1 - } - } -operationName: X -variables: -expect: - - 2 diff --git a/engines/tests/assets/testassets/test_identical_fields_00/0.gqt b/engines/tests/assets/testassets/test_identical_fields_00/0.gqt deleted file mode 100644 index fcdc1e1..0000000 --- a/engines/tests/assets/testassets/test_identical_fields_00/0.gqt +++ /dev/null @@ -1,3 +0,0 @@ -query { - a -} diff --git a/engines/tests/assets/testassets/test_identical_fields_00/test.yml b/engines/tests/assets/testassets/test_identical_fields_00/test.yml deleted file mode 100644 index d35be77..0000000 --- a/engines/tests/assets/testassets/test_identical_fields_00/test.yml +++ /dev/null @@ -1,9 +0,0 @@ -query: | - query X { - a - a - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_map_00/0.gqt b/engines/tests/assets/testassets/test_map_00/0.gqt deleted file mode 100644 index bde5736..0000000 --- a/engines/tests/assets/testassets/test_map_00/0.gqt +++ /dev/null @@ -1,24 +0,0 @@ -query { - a { - a0( - a0_0: any - a0_1: val = { - a0_10: val = null - } - ) { - a00( - a00_0: val = [any, val = 0, any] - a00_1: val = [ - ... - val = { - a00_1x0: any - } - ] - a00_2: val = [ ... any ] - ) { - a000 - } - } - a1 - } -} diff --git a/engines/tests/assets/testassets/test_map_00/test.yml b/engines/tests/assets/testassets/test_map_00/test.yml deleted file mode 100644 index 0969ccf..0000000 --- a/engines/tests/assets/testassets/test_map_00/test.yml +++ /dev/null @@ -1,27 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: 1 - a0_1: { - a0_10: null - } - ) { - a00( - a00_0: [1, 0, 2] - a00_1: [ - { a00_1x0: 0 } - { a00_1x0: 1 } - ] - a00_2: [0, 1] - ) { - a000 - } - } - a1 - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_max_00/0.gqt b/engines/tests/assets/testassets/test_max_00/0.gqt deleted file mode 100644 index fe5b4b7..0000000 --- a/engines/tests/assets/testassets/test_max_00/0.gqt +++ /dev/null @@ -1,6 +0,0 @@ -query { - combine 1 { - a - b - } -} diff --git a/engines/tests/assets/testassets/test_max_00/test.yml b/engines/tests/assets/testassets/test_max_00/test.yml deleted file mode 100644 index 3e5ab8c..0000000 --- a/engines/tests/assets/testassets/test_max_00/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -query: | - query X { - a - b - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_max_02/0.gqt b/engines/tests/assets/testassets/test_max_02/0.gqt deleted file mode 100644 index 080f2ad..0000000 --- a/engines/tests/assets/testassets/test_max_02/0.gqt +++ /dev/null @@ -1,7 +0,0 @@ -query { - combine 1 { - a - b - c - } -} diff --git a/engines/tests/assets/testassets/test_max_02/1.gqt b/engines/tests/assets/testassets/test_max_02/1.gqt deleted file mode 100644 index 1b303b1..0000000 --- a/engines/tests/assets/testassets/test_max_02/1.gqt +++ /dev/null @@ -1,7 +0,0 @@ -query { - combine 2 { - a - b - c - } -} diff --git a/engines/tests/assets/testassets/test_max_02/test.yml b/engines/tests/assets/testassets/test_max_02/test.yml deleted file mode 100644 index ffce84d..0000000 --- a/engines/tests/assets/testassets/test_max_02/test.yml +++ /dev/null @@ -1,9 +0,0 @@ -query: | - query X { - a - b - } -operationName: X -variables: -expect: - - 1 diff --git a/engines/tests/assets/testassets/test_max_03/0.gqt b/engines/tests/assets/testassets/test_max_03/0.gqt deleted file mode 100644 index 8260b49..0000000 --- a/engines/tests/assets/testassets/test_max_03/0.gqt +++ /dev/null @@ -1,10 +0,0 @@ -query { - combine 1 { - a - b - } - combine 1 { - c - d - } -} diff --git a/engines/tests/assets/testassets/test_max_03/test.yml b/engines/tests/assets/testassets/test_max_03/test.yml deleted file mode 100644 index e41fdb9..0000000 --- a/engines/tests/assets/testassets/test_max_03/test.yml +++ /dev/null @@ -1,9 +0,0 @@ -query: | - query X { - a - b - c - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_max_complex_00/0.gqt b/engines/tests/assets/testassets/test_max_complex_00/0.gqt deleted file mode 100644 index b6e6f55..0000000 --- a/engines/tests/assets/testassets/test_max_complex_00/0.gqt +++ /dev/null @@ -1,38 +0,0 @@ -query { - combine 2 { - a - b - c { - combine 1 { - c0( - c0_0: val > 0 - ) { - combine 1 { - c00( - c00_0: val < 0 - ) { - combine 2 { - c000 - c001 - c002 - } - } - c01 - } - } - c1 - } - } - d { - combine 1 { - d0 - d1 - } - } - } - combine 2 { - e - f - g - } -} diff --git a/engines/tests/assets/testassets/test_max_complex_00/test.yml b/engines/tests/assets/testassets/test_max_complex_00/test.yml deleted file mode 100644 index a9a5747..0000000 --- a/engines/tests/assets/testassets/test_max_complex_00/test.yml +++ /dev/null @@ -1,19 +0,0 @@ -query: | - query X { - a - c { - c0( - c0_0: 1 - ) { - c00(c00_0: -1) { - c000 - c002 - } - } - } - e - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_negation_00/0.gqt b/engines/tests/assets/testassets/test_negation_00/0.gqt deleted file mode 100644 index 55b8d45..0000000 --- a/engines/tests/assets/testassets/test_negation_00/0.gqt +++ /dev/null @@ -1,7 +0,0 @@ -query { - a { - a0( - a0_0: val = [ val != 0 ] - ) - } -} diff --git a/engines/tests/assets/testassets/test_negation_00/test.yml b/engines/tests/assets/testassets/test_negation_00/test.yml deleted file mode 100644 index 0af810a..0000000 --- a/engines/tests/assets/testassets/test_negation_00/test.yml +++ /dev/null @@ -1,11 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: [ 0 ] - ) - } - } -operationName: X -variables: -expect: diff --git a/engines/tests/assets/testassets/test_negation_01/0.gqt b/engines/tests/assets/testassets/test_negation_01/0.gqt deleted file mode 100644 index 568ba34..0000000 --- a/engines/tests/assets/testassets/test_negation_01/0.gqt +++ /dev/null @@ -1,7 +0,0 @@ -query { - a { - a0( - a0_0: val != [ val != 0 ] - ) - } -} diff --git a/engines/tests/assets/testassets/test_negation_01/test.yml b/engines/tests/assets/testassets/test_negation_01/test.yml deleted file mode 100644 index 393efd5..0000000 --- a/engines/tests/assets/testassets/test_negation_01/test.yml +++ /dev/null @@ -1,12 +0,0 @@ -query: | - query X { - a { - a0( - a0_0: [ 0 ] - ) - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_nesting_00/0.gqt b/engines/tests/assets/testassets/test_nesting_00/0.gqt deleted file mode 100644 index 15ac88c..0000000 --- a/engines/tests/assets/testassets/test_nesting_00/0.gqt +++ /dev/null @@ -1,18 +0,0 @@ -query { - a { - a0 { - a00 { - a000 { - a0000( - a0000_0: val = { - a0000_00: val = 65535 - } - ) - } - a001( - a001_0: val = "surprise!" - ) - } - } - } -} diff --git a/engines/tests/assets/testassets/test_nesting_00/test.yml b/engines/tests/assets/testassets/test_nesting_00/test.yml deleted file mode 100644 index e3e5ff2..0000000 --- a/engines/tests/assets/testassets/test_nesting_00/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -query: | - query X { - a { - a0 { - a00 { - a000 { - a0000( - a0000_0: { - a0000_00: 65535 - } - ) - } - a001( - a001_0: "surprise!" - ) - } - } - } - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_special_fields_00/0.gqt b/engines/tests/assets/testassets/test_special_fields_00/0.gqt deleted file mode 100644 index c57ec0f..0000000 --- a/engines/tests/assets/testassets/test_special_fields_00/0.gqt +++ /dev/null @@ -1,4 +0,0 @@ -query { - __typename - a -} diff --git a/engines/tests/assets/testassets/test_special_fields_00/test.yml b/engines/tests/assets/testassets/test_special_fields_00/test.yml deleted file mode 100644 index f5bb561..0000000 --- a/engines/tests/assets/testassets/test_special_fields_00/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -query: | - query X { - a - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_token_null_00/0.gqt b/engines/tests/assets/testassets/test_token_null_00/0.gqt deleted file mode 100644 index f9d3fb5..0000000 --- a/engines/tests/assets/testassets/test_token_null_00/0.gqt +++ /dev/null @@ -1,4 +0,0 @@ -query { - a(a_0: val = null) - b(b_0: val != null) -} diff --git a/engines/tests/assets/testassets/test_token_null_00/test.yml b/engines/tests/assets/testassets/test_token_null_00/test.yml deleted file mode 100644 index 701ba8c..0000000 --- a/engines/tests/assets/testassets/test_token_null_00/test.yml +++ /dev/null @@ -1,9 +0,0 @@ -query: | - query X { - a(a_0: null) - b(b_0: 0) - } -operationName: X -variables: -expect: - - 0 diff --git a/engines/tests/assets/testassets/test_variables_00/0.gqt b/engines/tests/assets/testassets/test_variables_00/0.gqt deleted file mode 100644 index cca8675..0000000 --- a/engines/tests/assets/testassets/test_variables_00/0.gqt +++ /dev/null @@ -1,42 +0,0 @@ -query { - a( - a_0: val = 0 - a_1: val = 0.0 - a_2: val = true - a_3: val = "alive" - a_4: val = "id" - ) - b( - b_0: val = [ val = 0 ] - b_1: val = [ val = 0.0 ] - b_2: val = [ val = true ] - b_3: val = [ val = "alive" ] - b_4: val = [ val = "id" ] - ) - c( - c_0: val = { - c_00: val = 0 - c_01: val = 0.0 - c_02: val = true - c_03: val = "alive" - } - c_1: val = [ - val = { - c_10: val = 0 - c_11: val = 0.0 - c_12: val = true - c_13: val = "alive" - } - ] - ) - d( - d_0: val = { - d_00: val = [ val = 0 ] - } - d_1: val = [ - val = { - d_1x0: val = [ val = 0 ] - } - ] - ) -} diff --git a/engines/tests/assets/testassets/test_variables_00/test.yml b/engines/tests/assets/testassets/test_variables_00/test.yml deleted file mode 100644 index 9b6d823..0000000 --- a/engines/tests/assets/testassets/test_variables_00/test.yml +++ /dev/null @@ -1,83 +0,0 @@ -query: | - query X ( - $a_0: Int! - $a_1: Float! - $a_2: Boolean! - $a_3: String! - $a_4: ID! - - $b_0: [Int!]! - $b_1: [Float!]! - $b_2: [Boolean!]! - $b_3: [String!]! - $b_4: [ID!]! - - $c_0: Input! - $c_1: [Input!]! - - $d_00: [Int!]! - $d_1x: Input! - ) { - a( - a_0: $a_0 - a_1: $a_1 - a_2: $a_2 - a_3: $a_3 - a_4: $a_4 - ) - - b( - b_0: $b_0 - b_1: $b_1 - b_2: $b_2 - b_3: $b_3 - b_4: $b_4 - ) - - c( - c_0: $c_0 - c_1: $c_1 - ) - - d( - d_0: { - d_00: $d_00 - } - d_1: [ $d_1x ] - ) - } -operationName: X -variables: | - { - "a_0": 0, - "a_1": 0.0, - "a_2": true, - "a_3": "alive", - "a_4": "id", - - "b_0": [0], - "b_1": [0.0], - "b_2": [true], - "b_3": ["alive"], - "b_4": ["id"], - - "c_0": { - "c_00": 0, - "c_01": 0.0, - "c_02": true, - "c_03": "alive" - }, - "c_1": [{ - "c_10": 0, - "c_11": 0.0, - "c_12": true, - "c_13": "alive" - }], - - "d_00": [0], - "d_1x": { - "d_1x0": [0] - } - } -expect: - - 0 diff --git a/engines/tests/benchmark_test.go b/engines/tests/benchmark_test.go deleted file mode 100644 index 7aa7233..0000000 --- a/engines/tests/benchmark_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package engine_test - -import ( - "embed" - _ "embed" - "fmt" - "testing" - - "github.com/graph-guard/ggproxy/engines/rmap" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/gqt" -) - -var N int - -//go:embed assets/benchassets -var benchassets embed.FS - -var GS string - -func BenchmarkPartedQuery(b *testing.B) { - templates := readTestAssets(benchassets, "assets/benchassets", "templates")[0].Templates - rules := make(map[string]gqt.Doc, len(templates)) - for _, r := range templates { - rules[r.ID] = r.Document - } - rm, _ := rmap.New(rules, 0) - - for _, td := range readTestAssets(benchassets, "assets/benchassets", "bench_") { - b.Run(td.ID, func(b *testing.B) { - p := gqlparse.NewParser() - query := []byte(td.Query) - operationName := []byte(td.OperationName) - variables := []byte(td.Variables) - b.ResetTimer() - - for n := 0; n < b.N; n++ { - p.Parse( - query, operationName, variables, - func( - varVals [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - rm.MatchAll( - varVals, - operation[0].ID, - selectionSet, - func(id string) { GS = id }, - ) - }, func(err error) { - panic(fmt.Errorf("unexpected error: %w", err)) - }, - ) - } - }) - } -} diff --git a/engines/tests/engine_test.go b/engines/tests/engine_test.go deleted file mode 100644 index 16b35cd..0000000 --- a/engines/tests/engine_test.go +++ /dev/null @@ -1,729 +0,0 @@ -package engine_test - -import ( - "bytes" - "embed" - _ "embed" - "fmt" - "io" - "io/fs" - "path/filepath" - "strings" - "testing" - - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/config/metadata" - "github.com/graph-guard/ggproxy/engines/rmap" - "github.com/graph-guard/ggproxy/engines/rmap/pquery" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/xxhash" - "github.com/graph-guard/gqt" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -func TestConstraintIdAndValue(t *testing.T) { - for _, td := range []struct { - input gqt.Constraint - id rmap.Constraint - value any - err error - }{ - { - input: gqt.ConstraintMap{ - Constraint: new(gqt.Constraint), - }, - id: rmap.ConstraintMap, - value: new(gqt.Constraint), - }, - { - input: gqt.ConstraintAny{}, - id: rmap.ConstraintAny, - value: nil, - }, - { - input: gqt.ConstraintValEqual{ - Value: gqt.ValueObject{ - Fields: []gqt.ObjectField{ - { - Name: "a", - Value: gqt.ConstraintValLessOrEqual{ - Value: 42.0, - }, - }, - }, - }, - }, - id: rmap.ConstraintValEqual, - value: gqt.ValueObject{ - Fields: []gqt.ObjectField{ - { - Name: "a", - Value: gqt.ConstraintValLessOrEqual{ - Value: 42.0, - }, - }, - }, - }, - }, - { - input: gqt.ConstraintValGreater{ - Value: 42.0, - }, - id: rmap.ConstraintValGreater, - value: 42.0, - }, - { - input: gqt.ConstraintValLess{ - Value: 42.0, - }, - id: rmap.ConstraintValLess, - value: 42.0, - }, - { - input: gqt.ConstraintValGreaterOrEqual{ - Value: 69.0, - }, - id: rmap.ConstraintValGreaterOrEqual, - value: 69.0, - }, - { - input: gqt.ConstraintValLessOrEqual{ - Value: 69.0, - }, - id: rmap.ConstraintValLessOrEqual, - value: 69.0, - }, - { - input: gqt.ConstraintBytelenEqual{ - Value: 1984, - }, - id: rmap.ConstraintBytelenEqual, - value: uint(1984), - }, - { - input: gqt.ConstraintBytelenNotEqual{ - Value: 1984, - }, - id: rmap.ConstraintBytelenNotEqual, - value: uint(1984), - }, - { - input: gqt.ConstraintBytelenGreater{ - Value: 282, - }, - id: rmap.ConstraintBytelenGreater, - value: uint(282), - }, - { - input: gqt.ConstraintBytelenLess{ - Value: 282, - }, - id: rmap.ConstraintBytelenLess, - value: uint(282), - }, - { - input: gqt.ConstraintBytelenGreaterOrEqual{ - Value: 27015, - }, - id: rmap.ConstraintBytelenGreaterOrEqual, - value: uint(27015), - }, - { - input: gqt.ConstraintBytelenLessOrEqual{ - Value: 27015, - }, - id: rmap.ConstraintBytelenLessOrEqual, - value: uint(27015), - }, - { - input: gqt.ConstraintLenEqual{ - Value: 997, - }, - id: rmap.ConstraintLenEqual, - value: uint(997), - }, - { - input: gqt.ConstraintLenNotEqual{ - Value: 997, - }, - id: rmap.ConstraintLenNotEqual, - value: uint(997), - }, - { - input: gqt.ConstraintLenGreater{ - Value: 47, - }, - id: rmap.ConstraintLenGreater, - value: uint(47), - }, - { - input: gqt.ConstraintLenLess{ - Value: 47, - }, - id: rmap.ConstraintLenLess, - value: uint(47), - }, - { - input: gqt.ConstraintLenGreaterOrEqual{ - Value: 404, - }, - id: rmap.ConstraintLenGreaterOrEqual, - value: uint(404), - }, - { - input: gqt.ConstraintLenLessOrEqual{ - Value: 404, - }, - id: rmap.ConstraintLenLessOrEqual, - value: uint(404), - }, - } { - t.Run("", func(t *testing.T) { - id, value := rmap.ConstraintIdAndValue(td.input) - require.Equal(t, td.id, id) - require.Equal(t, td.value, value) - }) - } -} - -//go:embed assets/testassets -var testassets embed.FS - -type QueryModel struct { - Query string `yaml:"query"` - OperationName string `yaml:"operationName"` - Variables string `yaml:"variables"` - Expect []string `yaml:"expect"` -} - -type MatchTest struct { - ID string - *QueryModel - Templates []*config.Template -} - -func readTestAsset( - filesystem fs.FS, path string, -) ( - query *QueryModel, templates []*config.Template, -) { - test, err := fs.ReadDir(filesystem, path) - if err != nil { - panic(err) - } - - for _, f := range test { - if f.IsDir() { - continue - } - fn := f.Name() - fp := filepath.Join(path, f.Name()) - if strings.HasSuffix(fn, ".gqt") { - id := strings.ToLower(fn[:len(fn)-len(filepath.Ext(fn))]) - src, err := filesystem.Open(fp) - if err != nil { - panic(err) - } - b, err := io.ReadAll(src) - if err != nil { - panic(err) - } - - meta, template, err := metadata.Parse(b) - if err != nil { - panic(err) - } - doc, errParser := gqt.Parse(template) - if errParser.IsErr() { - panic(errParser) - } - - templates = append(templates, &config.Template{ - ID: id, - Source: template, - Document: doc, - Name: meta.Name, - Tags: meta.Tags, - }) - } - if strings.HasSuffix(fn, ".yml") || strings.HasSuffix(fn, ".yaml") { - src, err := filesystem.Open(fp) - if err != nil { - panic(err) - } - d := yaml.NewDecoder(src) - d.KnownFields(true) - err = d.Decode(&query) - if err != nil { - panic(err) - } - } - } - - return -} - -func readTestAssets(filesystem fs.FS, path, prefix string) (assets []*MatchTest) { - root, err := fs.ReadDir(filesystem, path) - if err != nil { - panic(err) - } - for _, testDir := range root { - if !testDir.IsDir() { - continue - } - testDirName := testDir.Name() - testDirPath := filepath.Join(path, testDirName) - if !strings.HasPrefix(testDirName, prefix) { - continue - } - - query, templates := readTestAsset(filesystem, testDirPath) - assets = append(assets, &MatchTest{ - ID: testDirName, - QueryModel: query, - Templates: templates, - }) - } - - return -} - -func TestMatchAllPartedQuery(t *testing.T) { - for _, td := range readTestAssets(testassets, "assets/testassets", "test_") { - t.Run(td.ID, func(t *testing.T) { - rules := make(map[string]gqt.Doc, len(td.Templates)) - for _, r := range td.Templates { - rules[r.ID] = r.Document - } - - p := gqlparse.NewParser() - rm, _ := rmap.New(rules, 0) - - p.Parse( - []byte(td.Query), - []byte(td.OperationName), - []byte(td.Variables), - func( - varVals [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - actual := []string{} - rm.MatchAll( - varVals, - operation[0].ID, - selectionSet, - func(id string) { - actual = append(actual, id) - }, - ) - require.Len(t, actual, len(td.Expect)) - for _, e := range td.Expect { - require.Contains(t, actual, e) - } - }, - func(err error) { - t.Fatalf("unexpected error: %v", err) - }, - ) - }) - } -} - -func TestPrintPartedQuery(t *testing.T) { - for _, td := range []struct { - template string - expect string - }{ - { - template: ` - query { - a( - a_0: val = 0 - ) - } - `, - expect: fmt.Sprintf(`%d: - ConstraintValEqual: 0 - 0 -`, Hash("query.a.a_0")), - }, - { - template: ` - query { - a( - a_0: val = "a" - ) - } - `, - expect: fmt.Sprintf(`%d: - ConstraintValEqual: 0 - a -`, Hash("query.a.a_0")), - }, - { - template: ` - query { - a( - a_0: val = { - a_00: val = [val = 1, val = 2] - } - ) - } - `, - expect: fmt.Sprintf(`%d: - ConstraintValEqual: 0 - -: - ConstraintValEqual: - 1 - -: - ConstraintValEqual: - 2 -`, Hash("query.a.a_0.a_00")), - }, - { - template: ` - query { - a( - a_0: val = [ ... val = [ ... val <= 0 ] ] - ) - } - `, - expect: fmt.Sprintf(`%d: - ConstraintMap: 0 - ConstraintMap: - ConstraintValLessOrEqual: - 0 -`, Hash("query.a.a_0")), - }, - { - template: ` - query { - a( - a_0: val = [ - val = { - a_000: val > 5 - } - val = { - a_010: val = [val = 0, val = 1] - } - ] - ) - } - `, - expect: fmt.Sprintf(`%d: - ConstraintValEqual: 0 - -: - ConstraintValEqual: - a_000: - ConstraintValGreater: - 5 - -: - ConstraintValEqual: - a_010: - ConstraintValEqual: - -: - ConstraintValEqual: - 0 - -: - ConstraintValEqual: - 1 -`, Hash("query.a.a_0")), - }, - } { - t.Run("", func(t *testing.T) { - b := new(bytes.Buffer) - - rd, err := gqt.Parse([]byte(td.template)) - require.False(t, err.IsErr()) - rm, _ := rmap.New(map[string]gqt.Doc{ - "rd": rd, - }, 0) - rm.Print(b) - - require.Equal(t, td.expect, b.String()) - }) - } -} - -func Hash(s string) uint64 { - h := xxhash.New(0) - xxhash.Write(&h, s) - return h.Sum64() -} - -func TestNewQueryPart(t *testing.T) { - for _, td := range []struct { - query string - operationName string - variablesJSON string - expect []pquery.QueryPart - }{ - { - operationName: "X", - query: ` - query X { - a { - a0( - a0_0: { - a0_00: 1.0 - } - a0_1: "no" - ) { - a00 - } - } - b( - b_0: { - b_00: "go" - } - b_1: [0.0, 1.0] - ) { - b0 - } - c( - c_0: [ - { - c_000: ["hohoho"] - } - ] - c_1: [ - [ - { - c_1000: -1.0 - c_1001: [1.0, 0.0] - } - ] - [ - { - c_1100: "hawk" - } - { - c_1110: "falcon" - } - ] - ] - ) { - c0( - c0_0: 0.0 - ) { - c00 - } - } - } - `, - expect: []pquery.QueryPart{ - {ArgLeafIdx: 0, Hash: Hash("query.a.a0.a0_0.a0_00"), Value: 1.0}, - {ArgLeafIdx: 1, Hash: Hash("query.a.a0.a0_1"), Value: []byte("no")}, - {ArgLeafIdx: -1, Hash: Hash("query.a.a0.a00"), Value: nil}, - {ArgLeafIdx: 0, Hash: Hash("query.b.b_0.b_00"), Value: []byte("go")}, - {ArgLeafIdx: 1, Hash: Hash("query.b.b_1"), Value: &[]any{0.0, 1.0}}, - {ArgLeafIdx: -1, Hash: Hash("query.b.b0"), Value: nil}, - { - ArgLeafIdx: 0, - Hash: Hash("query.c.c_0"), - Value: &[]any{ - MakeMap( - hamap.Pair[string, any]{ - Key: "c_000", - Value: &[]any{ - []byte("hohoho"), - }, - }, - ), - }, - }, - { - ArgLeafIdx: 1, - Hash: Hash("query.c.c_1"), - Value: &[]any{ - &[]any{ - MakeMap( - hamap.Pair[string, any]{ - Key: "c_1000", - Value: -1.0, - }, - hamap.Pair[string, any]{ - Key: "c_1001", - Value: &[]any{1.0, 0.0}, - }, - ), - }, - &[]any{ - MakeMap( - hamap.Pair[string, any]{ - Key: "c_1100", - Value: []byte("hawk"), - }, - ), - MakeMap( - hamap.Pair[string, any]{ - Key: "c_1110", - Value: []byte("falcon"), - }, - ), - }, - }, - }, - {ArgLeafIdx: 0, Hash: Hash("query.c.c0.c0_0"), Value: 0.0}, - {ArgLeafIdx: -1, Hash: Hash("query.c.c0.c00"), Value: nil}, - }, - }, - { - operationName: "X", - query: ` - mutation X { - a { - a0 - } - b( - b_0: 0.0 - ) { - b0 - } - } - `, - expect: []pquery.QueryPart{ - {ArgLeafIdx: -1, Hash: Hash("mutation.a.a0"), Value: nil}, - {ArgLeafIdx: 0, Hash: Hash("mutation.b.b_0"), Value: 0.0}, - {ArgLeafIdx: -1, Hash: Hash("mutation.b.b0"), Value: nil}, - }, - }, - } { - t.Run("", func(t *testing.T) { - var i int - - gqlparse.NewParser().Parse( - []byte(td.query), - []byte(td.operationName), - []byte(td.variablesJSON), - func( - varValues [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - pquery.NewMaker(0).ParseQuery( - varValues, - operation[0].ID, - selectionSet, - func(qp pquery.QueryPart) (stop bool) { - require.Equal(t, td.expect[i], qp) - i++ - return false - }, - ) - }, - func(err error) { - t.Fatalf("unexpected parser error: %v", err) - }, - ) - }) - } -} - -func TestPrint(t *testing.T) { - for _, td := range []struct { - query string - operationName string - variablesJSON string - expect string - }{ - { - operationName: "X", - query: ` - query X { - a { - a0( - a0_0: { - a0_00: 1 - } - ) - } - } - `, - expect: fmt.Sprintf(`%d: 1 -`, Hash("query.a.a0.a0_0.a0_00")), - }, - { - query: ` - query { - a( - a_0: [ 1, 2 ] - ) - } - `, - expect: fmt.Sprintf(`%d: - -: - 1 - -: - 2 -`, Hash("query.a.a_0")), - }, - { - query: ` - query { - a( - a_0: [ - { - a_000: 5 - } - { - a_010: [ 0, 1 ] - } - ] - ) - } - `, - expect: fmt.Sprintf(`%d: - -: - a_000: - 5 - -: - a_010: - -: - 0 - -: - 1 -`, Hash("query.a.a_0")), - }, - } { - t.Run("", func(t *testing.T) { - gqlparse.NewParser().Parse( - []byte(td.query), - []byte(td.operationName), - []byte(td.variablesJSON), - func( - varValues [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - b := new(bytes.Buffer) - pquery.NewMaker(0).ParseQuery( - varValues, - operation[0].ID, - selectionSet, - func(qp pquery.QueryPart) (stop bool) { - qp.Print(b) - return false - }, - ) - require.Equal(t, td.expect, b.String()) - }, - func(err error) { - t.Fatalf("unexpected parser error: %v", err) - }, - ) - }) - } -} - -func MakeMap(items ...hamap.Pair[string, any]) *hamap.Map[string, any] { - m := hamap.New[string, any](len(items), nil) - for i := range items { - m.Set(items[i].Key, items[i].Value) - } - return m -} diff --git a/go.mod b/go.mod index eee661f..b191733 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,17 @@ module github.com/graph-guard/ggproxy go 1.18 require ( - github.com/99designs/gqlgen v0.17.13 - github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/99designs/gqlgen v0.17.24 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.5.8 - github.com/google/uuid v1.3.0 - github.com/graph-guard/backend v0.0.0-20220826171348-e3dcd100c82b github.com/graph-guard/gqlscan v1.0.0 - github.com/graph-guard/gqt v0.0.0-20220830090241-b646278952f9 + github.com/graph-guard/gqt/v4 v4.0.12 github.com/phuslu/log v1.0.81 github.com/pierrec/xxHash v0.1.5 github.com/stretchr/testify v1.7.2 github.com/tidwall/gjson v1.14.2 github.com/valyala/fasthttp v1.38.0 - github.com/vektah/gqlparser/v2 v2.4.6 + github.com/vektah/gqlparser/v2 v2.5.1 github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082 github.com/zeebo/xxh3 v1.0.2 golang.org/x/exp v0.0.0-20220823124025-807a23277127 @@ -41,8 +38,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect + golang.org/x/text v0.3.8 // indirect golang.org/x/tools v0.1.12 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 5f3c27b..9c78f82 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw= -github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ= +github.com/99designs/gqlgen v0.17.24 h1:pcd/HFIoSdRvyADYQG2dHvQN2KZqX/nXzlVm6TMMq7E= +github.com/99designs/gqlgen v0.17.24/go.mod h1:BMhYIhe4bp7OlCo5I2PnowSK/Wimpv/YlxfNkqZGwLo= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= @@ -16,8 +16,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -26,16 +24,12 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/graph-guard/backend v0.0.0-20220826171348-e3dcd100c82b h1:MTZ+FdD6qIy0s0KgUecPGNJ8frrZ00Tqv4J81vdTv8w= -github.com/graph-guard/backend v0.0.0-20220826171348-e3dcd100c82b/go.mod h1:x2XK0889DedSc5iB1AZzg0dJdWaHx8UiRo1vnv55kNc= github.com/graph-guard/gqlscan v1.0.0 h1:hUEDqnJQJ7nV9S8hCPJuYI/H97KP3unu0Gu7wd0wV8A= github.com/graph-guard/gqlscan v1.0.0/go.mod h1:A7TeIFxoiP39gw/r4sWjDnTZJnNy+4kaytP9Kh7Luuk= -github.com/graph-guard/gqt v0.0.0-20220830090241-b646278952f9 h1:aVvTdG6rHXs4ZzE0BMK83P37zacC9t6I7LOhFb79DVI= -github.com/graph-guard/gqt v0.0.0-20220830090241-b646278952f9/go.mod h1:LSgSzLS6fBQ3uD5lREaytQu4qsSkM2EN50Ds1acdu0U= +github.com/graph-guard/gqt/v4 v4.0.12 h1:BjG4wEq5Cze6X7bEsono2fVunWVcK9RyyzAUtbvG70Y= +github.com/graph-guard/gqt/v4 v4.0.12/go.mod h1:tXBLe4Gr3S/y8pM6ABkcIWFcspyU1c1YfrsXdBf0hKE= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= @@ -51,9 +45,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/phuslu/log v1.0.81 h1:07l/g+gde7oD77iCHXst/0iSu40BbcEkvv4pTeQCe+c= @@ -84,57 +77,56 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco= github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE= -github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= +github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= +github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082 h1:AWIZQ6fJPAAZdCUElj007LvHa/ER8nOn3CHWajn+1QY= github.com/yourbasic/bit v0.0.0-20180313074424-45a4409f4082/go.mod h1:SC4yTthuwUIud4hT6D7kJGIYmhnskaQnm3VD2VYM8EM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/lvs/lvs.go b/lvs/lvs.go deleted file mode 100644 index 6708f4c..0000000 --- a/lvs/lvs.go +++ /dev/null @@ -1,94 +0,0 @@ -package lvs - -import ( - "crypto" - "crypto/x509" - _ "embed" - "encoding/pem" - "errors" - - jwt "github.com/dgrijalva/jwt-go" -) - -type Type uint16 -type Plan uint16 - -type LicenseTokenClaim struct { - jwt.StandardClaims - Type Type `json:"type"` - Plan Plan `json:"plan"` -} - -const ( - Beta Type = iota - Community - Commercial -) - -const ( - Tiny Plan = iota - Small - Medium - Big - Unlimited -) - -// Encoded public key -var PublicKey string - -var ErrFailParseClaims = errors.New("failed to parse license token claims") -var ErrLicenseExpired = errors.New("license expired") -var ErrLicenseMalformed = errors.New("license token malformed or empty") -var ErrNoPEMBlock = errors.New("no valid PEM block in public key") - -// ValidateLicenseToken verifies the license and return license key parameters as claims. -func ValidateLicenseToken(licenseToken string) (*LicenseTokenClaim, error) { - if PublicKey == "" { - panic("missing public key") - } - - decodedPublicKey, err := decodePublicKey([]byte(PublicKey)) - if err != nil { - panic(err) - } - - token, err := jwt.ParseWithClaims( - licenseToken, - &LicenseTokenClaim{}, - func(token *jwt.Token) (interface{}, error) { - return decodedPublicKey, nil - }, - ) - - e, ok := err.(*jwt.ValidationError) - if ok { - if e.Errors&jwt.ValidationErrorExpired != 0 { - return nil, ErrLicenseExpired - } - if e.Errors&jwt.ValidationErrorMalformed != 0 { - return nil, ErrLicenseMalformed - } - } - - claims, ok := token.Claims.(*LicenseTokenClaim) - if !ok { - return nil, ErrFailParseClaims - } - - return claims, err -} - -func decodePublicKey(pemEncoded []byte) (crypto.PublicKey, error) { - block, _ := pem.Decode(pemEncoded) - if block == nil { - return nil, ErrNoPEMBlock - } - x509Encoded := block.Bytes - genericPublicKey, err := x509.ParsePKIXPublicKey(x509Encoded) - if err != nil { - return nil, err - } - publicKey := genericPublicKey.(crypto.PublicKey) - - return publicKey, nil -} diff --git a/lvs/lvs_test.go b/lvs/lvs_test.go deleted file mode 100644 index 4f85a0d..0000000 --- a/lvs/lvs_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package lvs_test - -import ( - _ "embed" - "testing" - "time" - - "github.com/google/uuid" - blvs "github.com/graph-guard/backend/lvs" - "github.com/graph-guard/ggproxy/lvs" - "github.com/stretchr/testify/require" -) - -var privateKey = ` ------BEGIN EC PRIVATE KEY----- -MIHcAgEBBEIBK43Z3ATV+af8U8iMBODHssl4FsSvL70DePBubkthHlltuVPxu29X -T7/Q5zSICfpRD0Q8F9lGxum5KPl4T/n6IM2gBwYFK4EEACOhgYkDgYYABAHtTblG -M/FaKHDkVrBOSJ2SJe7+Spyxbn7DQOfZ0B4dVVALGc5j/G+TqeYt6DVO1GEOHL3/ -lMy0L827kdCU5iZopgHJrjeaM38n4HmG/dEh4x7R3P+rNDV0NQ7EJ4W8dY+HwtDb -ripL46GdA8fVwDgom/qe4btdpGJBQcJrmURLxQjAXw== ------END EC PRIVATE KEY----- -` - -var publicKey = ` ------BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7U25RjPxWihw5FawTkidkiXu/kqc -sW5+w0Dn2dAeHVVQCxnOY/xvk6nmLeg1TtRhDhy9/5TMtC/Nu5HQlOYmaKYBya43 -mjN/J+B5hv3RIeMe0dz/qzQ1dDUOxCeFvHWPh8LQ264qS+OhnQPH1cA4KJv6nuG7 -XaRiQUHCa5lES8UIwF8= ------END PUBLIC KEY----- -` - -func TestVerifyLicenseToken(t *testing.T) { - decodedLicenseToken, err := blvs.GenerateLicenseToken( - time.Now().Local(), - time.Now().Local().Add(time.Hour), - lvs.Beta, - lvs.Unlimited, - uuid.New(), - []byte(privateKey), - ) - require.NoError(t, err) - require.NotEmpty(t, decodedLicenseToken) - - lvs.PublicKey = publicKey - - claims, err := lvs.ValidateLicenseToken( - string(decodedLicenseToken), - ) - - require.NoError(t, err) - require.NotNil(t, claims) -} - -func TestLicenseTokenExpired(t *testing.T) { - decodedLicenseToken, err := blvs.GenerateLicenseToken( - time.Now().Local(), - time.Now().Local().Add(-time.Hour), - lvs.Beta, - lvs.Unlimited, - uuid.New(), - []byte(privateKey), - ) - require.NoError(t, err) - require.NotEmpty(t, decodedLicenseToken) - - lvs.PublicKey = publicKey - - claims, err := lvs.ValidateLicenseToken( - string(decodedLicenseToken), - ) - - require.Error(t, lvs.ErrLicenseExpired, err) - require.Nil(t, claims) -} diff --git a/api/gqlgen.yml b/pkg/api/gqlgen.yml similarity index 91% rename from api/gqlgen.yml rename to pkg/api/gqlgen.yml index c1d2533..14b6681 100644 --- a/api/gqlgen.yml +++ b/pkg/api/gqlgen.yml @@ -42,7 +42,7 @@ resolver: # gqlgen will search for any type names in the schema in these go packages # if they match it will use them, otherwise it will generate them. autobind: -# - "github.com/graph-guard/ggproxy/api/graph/model" +# - "github.com/graph-guard/ggproxy/pkg/api/graph/model" # This section declares type mapping between the GraphQL and go type systems # @@ -62,14 +62,14 @@ models: - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Service: - model: github.com/graph-guard/ggproxy/api/graph/model.Service + model: github.com/graph-guard/ggproxy/pkg/api/graph/model.Service fields: matchingTemplates: resolver: true statistics: resolver: true Template: - model: github.com/graph-guard/ggproxy/api/graph/model.Template + model: github.com/graph-guard/ggproxy/pkg/api/graph/model.Template fields: service: resolver: true diff --git a/api/graph/generated/generated.go b/pkg/api/graph/generated/generated.go similarity index 97% rename from api/graph/generated/generated.go rename to pkg/api/graph/generated/generated.go index 55b600d..9862388 100644 --- a/api/graph/generated/generated.go +++ b/pkg/api/graph/generated/generated.go @@ -14,7 +14,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" - "github.com/graph-guard/ggproxy/api/graph/model" + "github.com/graph-guard/ggproxy/pkg/api/graph/model" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" ) @@ -48,7 +48,7 @@ type DirectiveRoot struct { type ComplexityRoot struct { MatchResult struct { Forwarded func(childComplexity int) int - Templates func(childComplexity int) int + Template func(childComplexity int) int TimeMatchingNs func(childComplexity int) int TimeParsingNs func(childComplexity int) int } @@ -65,9 +65,9 @@ type ComplexityRoot struct { ForwardReduced func(childComplexity int) int ForwardURL func(childComplexity int) int ID func(childComplexity int) int - ProxyURL func(childComplexity int) int Match func(childComplexity int, query string, operationName *string, variablesJSON *string) int MatchAll func(childComplexity int, query string, operationName *string, variablesJSON *string) int + ProxyURL func(childComplexity int) int Statistics func(childComplexity int) int TemplatesDisabled func(childComplexity int) int TemplatesEnabled func(childComplexity int) int @@ -110,7 +110,7 @@ type QueryResolver interface { Services(ctx context.Context) ([]*model.Service, error) } type ServiceResolver interface { - MatchAll(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) (*model.MatchResult, error) + MatchAll(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) ([]*model.MatchResult, error) Match(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) (*model.MatchResult, error) Statistics(ctx context.Context, obj *model.Service) (*model.ServiceStatistics, error) } @@ -141,12 +141,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MatchResult.Forwarded(childComplexity), true - case "MatchResult.templates": - if e.complexity.MatchResult.Templates == nil { + case "MatchResult.template": + if e.complexity.MatchResult.Template == nil { break } - return e.complexity.MatchResult.Templates(childComplexity), true + return e.complexity.MatchResult.Template(childComplexity), true case "MatchResult.timeMatchingNS": if e.complexity.MatchResult.TimeMatchingNs == nil { @@ -223,13 +223,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Service.ID(childComplexity), true - case "Service.proxyURL": - if e.complexity.Service.ProxyURL == nil { - break - } - - return e.complexity.Service.ProxyURL(childComplexity), true - case "Service.match": if e.complexity.Service.Match == nil { break @@ -254,6 +247,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Service.MatchAll(childComplexity, args["query"].(string), args["operationName"].(*string), args["variablesJSON"].(*string)), true + case "Service.proxyURL": + if e.complexity.Service.ProxyURL == nil { + break + } + + return e.complexity.Service.ProxyURL(childComplexity), true + case "Service.statistics": if e.complexity.Service.Statistics == nil { break @@ -518,7 +518,7 @@ type Service { query: String! operationName: String variablesJSON: String - ): MatchResult! + ): [MatchResult]! # match provides matching results for the given query. # It's similar to matchAll except that it matches one template only. @@ -533,9 +533,9 @@ type Service { } type MatchResult { - # templates provides all templates that matched the query. + # template provides the template that matched the query. # Provides an empty array if there was no match. - templates: [Template!]! + template: Template! # forwarded provides the forwarded query. # Provides null if there was no match. @@ -763,8 +763,8 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** -func (ec *executionContext) _MatchResult_templates(ctx context.Context, field graphql.CollectedField, obj *model.MatchResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_MatchResult_templates(ctx, field) +func (ec *executionContext) _MatchResult_template(ctx context.Context, field graphql.CollectedField, obj *model.MatchResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MatchResult_template(ctx, field) if err != nil { return graphql.Null } @@ -777,7 +777,7 @@ func (ec *executionContext) _MatchResult_templates(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Templates, nil + return obj.Template, nil }) if err != nil { ec.Error(ctx, err) @@ -789,12 +789,12 @@ func (ec *executionContext) _MatchResult_templates(ctx context.Context, field gr } return graphql.Null } - res := resTmp.([]*model.Template) + res := resTmp.(*model.Template) fc.Result = res - return ec.marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx, field.Selections, res) + return ec.marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplate(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_MatchResult_templates(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_MatchResult_template(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "MatchResult", Field: field, @@ -968,7 +968,6 @@ func (ec *executionContext) _Query_uptime(ctx context.Context, field graphql.Col }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { @@ -1012,7 +1011,6 @@ func (ec *executionContext) _Query_version(ctx context.Context, field graphql.Co }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { @@ -1056,14 +1054,13 @@ func (ec *executionContext) _Query_service(ctx context.Context, field graphql.Co }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*model.Service) fc.Result = res - return ec.marshalOService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx, field.Selections, res) + return ec.marshalOService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_service(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1130,7 +1127,6 @@ func (ec *executionContext) _Query_services(ctx context.Context, field graphql.C }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { @@ -1140,7 +1136,7 @@ func (ec *executionContext) _Query_services(ctx context.Context, field graphql.C } res := resTmp.([]*model.Service) fc.Result = res - return ec.marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐServiceᚄ(ctx, field.Selections, res) + return ec.marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐServiceᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_services(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1196,7 +1192,6 @@ func (ec *executionContext) _Query___type(ctx context.Context, field graphql.Col }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { return graphql.Null @@ -1270,7 +1265,6 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C }) if err != nil { ec.Error(ctx, err) - return graphql.Null } if resTmp == nil { return graphql.Null @@ -1379,7 +1373,7 @@ func (ec *executionContext) _Service_templatesEnabled(ctx context.Context, field } res := resTmp.([]*model.Template) fc.Result = res - return ec.marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx, field.Selections, res) + return ec.marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Service_templatesEnabled(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1437,7 +1431,7 @@ func (ec *executionContext) _Service_templatesDisabled(ctx context.Context, fiel } res := resTmp.([]*model.Template) fc.Result = res - return ec.marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx, field.Selections, res) + return ec.marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Service_templatesDisabled(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1669,9 +1663,9 @@ func (ec *executionContext) _Service_matchAll(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.(*model.MatchResult) + res := resTmp.([]*model.MatchResult) fc.Result = res - return ec.marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐMatchResult(ctx, field.Selections, res) + return ec.marshalNMatchResult2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Service_matchAll(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1682,8 +1676,8 @@ func (ec *executionContext) fieldContext_Service_matchAll(ctx context.Context, f IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "templates": - return ec.fieldContext_MatchResult_templates(ctx, field) + case "template": + return ec.fieldContext_MatchResult_template(ctx, field) case "forwarded": return ec.fieldContext_MatchResult_forwarded(ctx, field) case "timeParsingNS": @@ -1736,7 +1730,7 @@ func (ec *executionContext) _Service_match(ctx context.Context, field graphql.Co } res := resTmp.(*model.MatchResult) fc.Result = res - return ec.marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐMatchResult(ctx, field.Selections, res) + return ec.marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Service_match(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1747,8 +1741,8 @@ func (ec *executionContext) fieldContext_Service_match(ctx context.Context, fiel IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "templates": - return ec.fieldContext_MatchResult_templates(ctx, field) + case "template": + return ec.fieldContext_MatchResult_template(ctx, field) case "forwarded": return ec.fieldContext_MatchResult_forwarded(ctx, field) case "timeParsingNS": @@ -1801,7 +1795,7 @@ func (ec *executionContext) _Service_statistics(ctx context.Context, field graph } res := resTmp.(*model.ServiceStatistics) fc.Result = res - return ec.marshalNServiceStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx, field.Selections, res) + return ec.marshalNServiceStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Service_statistics(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2347,7 +2341,7 @@ func (ec *executionContext) _Template_statistics(ctx context.Context, field grap } res := resTmp.(*model.TemplateStatistics) fc.Result = res - return ec.marshalNTemplateStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx, field.Selections, res) + return ec.marshalNTemplateStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Template_statistics(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2405,7 +2399,7 @@ func (ec *executionContext) _Template_service(ctx context.Context, field graphql } res := resTmp.(*model.Service) fc.Result = res - return ec.marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx, field.Selections, res) + return ec.marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Template_service(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4542,9 +4536,9 @@ func (ec *executionContext) _MatchResult(ctx context.Context, sel ast.SelectionS switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("MatchResult") - case "templates": + case "template": - out.Values[i] = ec._MatchResult_templates(ctx, field, obj) + out.Values[i] = ec._MatchResult_template(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ @@ -4587,7 +4581,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr }) out := graphql.NewFieldSet(fields) - var invalids uint32 for i, field := range fields { innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ Object: field.Name, @@ -4607,9 +4600,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_uptime(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res } @@ -4630,9 +4620,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_version(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res } @@ -4673,9 +4660,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_services(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res } @@ -4703,9 +4687,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } } out.Dispatch() - if invalids > 0 { - return graphql.Null - } return out } @@ -5446,11 +5427,49 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } -func (ec *executionContext) marshalNMatchResult2githubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v model.MatchResult) graphql.Marshaler { +func (ec *executionContext) marshalNMatchResult2githubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v model.MatchResult) graphql.Marshaler { return ec._MatchResult(ctx, sel, &v) } -func (ec *executionContext) marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v *model.MatchResult) graphql.Marshaler { +func (ec *executionContext) marshalNMatchResult2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v []*model.MatchResult) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v *model.MatchResult) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5460,11 +5479,11 @@ func (ec *executionContext) marshalNMatchResult2ᚖgithubᚗcomᚋgraphᚑguard return ec._MatchResult(ctx, sel, v) } -func (ec *executionContext) marshalNService2githubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v model.Service) graphql.Marshaler { +func (ec *executionContext) marshalNService2githubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v model.Service) graphql.Marshaler { return ec._Service(ctx, sel, &v) } -func (ec *executionContext) marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐServiceᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Service) graphql.Marshaler { +func (ec *executionContext) marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐServiceᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Service) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -5488,7 +5507,7 @@ func (ec *executionContext) marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguard if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx, sel, v[i]) + ret[i] = ec.marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx, sel, v[i]) } if isLen1 { f(i) @@ -5508,7 +5527,7 @@ func (ec *executionContext) marshalNService2ᚕᚖgithubᚗcomᚋgraphᚑguard return ret } -func (ec *executionContext) marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v *model.Service) graphql.Marshaler { +func (ec *executionContext) marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v *model.Service) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5518,11 +5537,11 @@ func (ec *executionContext) marshalNService2ᚖgithubᚗcomᚋgraphᚑguardᚋgg return ec._Service(ctx, sel, v) } -func (ec *executionContext) marshalNServiceStatistics2githubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx context.Context, sel ast.SelectionSet, v model.ServiceStatistics) graphql.Marshaler { +func (ec *executionContext) marshalNServiceStatistics2githubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx context.Context, sel ast.SelectionSet, v model.ServiceStatistics) graphql.Marshaler { return ec._ServiceStatistics(ctx, sel, &v) } -func (ec *executionContext) marshalNServiceStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx context.Context, sel ast.SelectionSet, v *model.ServiceStatistics) graphql.Marshaler { +func (ec *executionContext) marshalNServiceStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐServiceStatistics(ctx context.Context, sel ast.SelectionSet, v *model.ServiceStatistics) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5579,7 +5598,7 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } -func (ec *executionContext) marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Template) graphql.Marshaler { +func (ec *executionContext) marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Template) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -5603,7 +5622,7 @@ func (ec *executionContext) marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguard if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplate(ctx, sel, v[i]) + ret[i] = ec.marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplate(ctx, sel, v[i]) } if isLen1 { f(i) @@ -5623,7 +5642,7 @@ func (ec *executionContext) marshalNTemplate2ᚕᚖgithubᚗcomᚋgraphᚑguard return ret } -func (ec *executionContext) marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplate(ctx context.Context, sel ast.SelectionSet, v *model.Template) graphql.Marshaler { +func (ec *executionContext) marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplate(ctx context.Context, sel ast.SelectionSet, v *model.Template) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5633,11 +5652,11 @@ func (ec *executionContext) marshalNTemplate2ᚖgithubᚗcomᚋgraphᚑguardᚋg return ec._Template(ctx, sel, v) } -func (ec *executionContext) marshalNTemplateStatistics2githubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx context.Context, sel ast.SelectionSet, v model.TemplateStatistics) graphql.Marshaler { +func (ec *executionContext) marshalNTemplateStatistics2githubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx context.Context, sel ast.SelectionSet, v model.TemplateStatistics) graphql.Marshaler { return ec._TemplateStatistics(ctx, sel, &v) } -func (ec *executionContext) marshalNTemplateStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx context.Context, sel ast.SelectionSet, v *model.TemplateStatistics) graphql.Marshaler { +func (ec *executionContext) marshalNTemplateStatistics2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐTemplateStatistics(ctx context.Context, sel ast.SelectionSet, v *model.TemplateStatistics) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5941,7 +5960,14 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) marshalOService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v *model.Service) graphql.Marshaler { +func (ec *executionContext) marshalOMatchResult2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐMatchResult(ctx context.Context, sel ast.SelectionSet, v *model.MatchResult) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MatchResult(ctx, sel, v) +} + +func (ec *executionContext) marshalOService2ᚖgithubᚗcomᚋgraphᚑguardᚋggproxyᚋpkgᚋapiᚋgraphᚋmodelᚐService(ctx context.Context, sel ast.SelectionSet, v *model.Service) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/api/graph/model/model.go b/pkg/api/graph/model/model.go similarity index 71% rename from api/graph/model/model.go rename to pkg/api/graph/model/model.go index 5595c07..e379156 100644 --- a/api/graph/model/model.go +++ b/pkg/api/graph/model/model.go @@ -1,14 +1,15 @@ package model import ( - "github.com/graph-guard/ggproxy/engines/rmap" - "github.com/graph-guard/ggproxy/statistics" + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon" + "github.com/graph-guard/ggproxy/pkg/statistics" ) type Service struct { - Matcher *rmap.RulesMap - TemplatesByID map[string]*Template - Stats *statistics.ServiceSync + Engine *playmon.Engine + Templates map[*config.Template]*Template + Stats *statistics.ServiceSync ID string `json:"id"` TemplatesEnabled []*Template `json:"templatesEnabled"` diff --git a/api/graph/model/models_gen.go b/pkg/api/graph/model/models_gen.go similarity index 83% rename from api/graph/model/models_gen.go rename to pkg/api/graph/model/models_gen.go index 0ed5890..8e769bb 100644 --- a/api/graph/model/models_gen.go +++ b/pkg/api/graph/model/models_gen.go @@ -7,10 +7,10 @@ import ( ) type MatchResult struct { - Templates []*Template `json:"templates"` - Forwarded *string `json:"forwarded"` - TimeParsingNs float64 `json:"timeParsingNS"` - TimeMatchingNs float64 `json:"timeMatchingNS"` + Template *Template `json:"template"` + Forwarded *string `json:"forwarded"` + TimeParsingNs float64 `json:"timeParsingNS"` + TimeMatchingNs float64 `json:"timeMatchingNS"` } type ServiceStatistics struct { diff --git a/api/graph/resolver.go b/pkg/api/graph/resolver.go similarity index 75% rename from api/graph/resolver.go rename to pkg/api/graph/resolver.go index e5064e8..5ad2d77 100644 --- a/api/graph/resolver.go +++ b/pkg/api/graph/resolver.go @@ -3,9 +3,8 @@ package graph import ( "time" - "github.com/graph-guard/ggproxy/api/graph/model" - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/gqlparse" + "github.com/graph-guard/ggproxy/pkg/api/graph/model" + "github.com/graph-guard/ggproxy/pkg/config" plog "github.com/phuslu/log" ) @@ -15,7 +14,6 @@ type Resolver struct { Start time.Time Version string Conf *config.Config - Parser *gqlparse.Parser Services map[string]*model.Service Log plog.Logger } diff --git a/api/graph/schema.graphqls b/pkg/api/graph/schema.graphqls similarity index 97% rename from api/graph/schema.graphqls rename to pkg/api/graph/schema.graphqls index c21893f..99bbfd4 100644 --- a/api/graph/schema.graphqls +++ b/pkg/api/graph/schema.graphqls @@ -49,7 +49,7 @@ type Service { query: String! operationName: String variablesJSON: String - ): MatchResult! + ): [MatchResult]! # match provides matching results for the given query. # It's similar to matchAll except that it matches one template only. @@ -64,9 +64,9 @@ type Service { } type MatchResult { - # templates provides all templates that matched the query. + # template provides the template that matched the query. # Provides an empty array if there was no match. - templates: [Template!]! + template: Template! # forwarded provides the forwarded query. # Provides null if there was no match. diff --git a/api/graph/schema.resolvers.go b/pkg/api/graph/schema.resolvers.go similarity index 64% rename from api/graph/schema.resolvers.go rename to pkg/api/graph/schema.resolvers.go index 0628ec1..46e239e 100644 --- a/api/graph/schema.resolvers.go +++ b/pkg/api/graph/schema.resolvers.go @@ -2,16 +2,18 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.24 import ( "bytes" "context" "time" - "github.com/graph-guard/ggproxy/api/graph/generated" - "github.com/graph-guard/ggproxy/api/graph/model" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/tokenwriter" + "github.com/graph-guard/ggproxy/pkg/api/graph/generated" + "github.com/graph-guard/ggproxy/pkg/api/graph/model" + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/tokenwriter" ) // Uptime is the resolver for the uptime field. @@ -46,105 +48,102 @@ func (r *queryResolver) Services(ctx context.Context) ([]*model.Service, error) } // MatchAll is the resolver for the matchAll field. -func (r *serviceResolver) MatchAll(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) (*model.MatchResult, error) { - // Declare here instead of using named return variables - // to avoid code generation overriding them. - m := new(model.MatchResult) +func (r *serviceResolver) MatchAll(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) ([]*model.MatchResult, error) { + m := []*model.MatchResult{} var err error - oprName := []byte(nil) + var oprName []byte if operationName != nil { oprName = []byte(*operationName) } - varsJSON := []byte(nil) + var varsJSON []byte if variablesJSON != nil { varsJSON = []byte(*variablesJSON) } - startParsing := time.Now() - r.Resolver.Parser.Parse( + start := time.Now() + var durParsing float64 + var forwarded string + obj.Engine.Match( []byte(query), oprName, varsJSON, - func( - varVals [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - m.TimeParsingNs = nsToF64(time.Since(startParsing).Nanoseconds()) - startMatching := time.Now() - obj.Matcher.MatchAll( - varVals, - operation[0].ID, - selectionSet, - func(id string) { - t := obj.TemplatesByID[id] - m.Templates = append(m.Templates, t) - }, - ) - m.TimeMatchingNs = nsToF64(time.Since(startMatching).Nanoseconds()) - var forwarded bytes.Buffer - if err = tokenwriter.Write(&forwarded, operation); err != nil { + func(operation, selectionSet []gqlparse.Token) (stop bool) { + durParsing = nsToF64(time.Since(start).Nanoseconds()) + + var b bytes.Buffer + if err = tokenwriter.Write(&b, operation); err != nil { r.Log.Error(). Err(err). Msg("writing parsed") - return + return true } - forwardStr := forwarded.String() - m.Forwarded = &forwardStr + forwarded = b.String() + + start = time.Now() + return false }, - func(errParser error) { - m.TimeParsingNs = nsToF64(time.Since(startParsing).Nanoseconds()) - err = errParser + func(template *config.Template) (stop bool) { + sinceStart := time.Since(start) + m = append(m, &model.MatchResult{ + Template: obj.Templates[template], + TimeMatchingNs: nsToF64(sinceStart.Nanoseconds()), + TimeParsingNs: durParsing, + Forwarded: &forwarded, + }) + + start = time.Now() + return false }, + func(errParser error) { err = errParser }, ) return m, err } // Match is the resolver for the match field. -func (r *serviceResolver) Match(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) (*model.MatchResult, error) { - // Declare here instead of using named return variables - // to avoid code generation overriding them. - m := new(model.MatchResult) - var err error +func (r *serviceResolver) Match(ctx context.Context, obj *model.Service, query string, operationName *string, variablesJSON *string) (m *model.MatchResult, err error) { + m = &model.MatchResult{} - oprName := []byte(nil) + var oprName []byte if operationName != nil { oprName = []byte(*operationName) } - varsJSON := []byte(nil) + var varsJSON []byte if variablesJSON != nil { varsJSON = []byte(*variablesJSON) } - startParsing := time.Now() - r.Resolver.Parser.Parse( + start := time.Now() + var durParsing float64 + var forwarded string + obj.Engine.Match( []byte(query), oprName, varsJSON, - func( - varVals [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - m.TimeParsingNs = nsToF64(time.Since(startParsing).Nanoseconds()) - startMatching := time.Now() - if id := obj.Matcher.Match(varVals, operation[0].ID, selectionSet); id != "" { - m.Templates = []*model.Template{obj.TemplatesByID[id]} - } - m.TimeMatchingNs = nsToF64(time.Since(startMatching).Nanoseconds()) - var forwarded bytes.Buffer - if err = tokenwriter.Write(&forwarded, operation); err != nil { + func(operation, selectionSet []gqlparse.Token) (stop bool) { + durParsing = nsToF64(time.Since(start).Nanoseconds()) + + var b bytes.Buffer + if err = tokenwriter.Write(&b, operation); err != nil { r.Log.Error(). Err(err). Msg("writing parsed") - return + return true } - forwardStr := forwarded.String() - m.Forwarded = &forwardStr + forwarded = b.String() + + start = time.Now() + return false }, - func(errParser error) { - m.TimeParsingNs = nsToF64(time.Since(startParsing).Nanoseconds()) - err = errParser + func(template *config.Template) (stop bool) { + sinceStart := time.Since(start) + m = &model.MatchResult{ + Template: obj.Templates[template], + TimeMatchingNs: nsToF64(sinceStart.Nanoseconds()), + TimeParsingNs: durParsing, + Forwarded: &forwarded, + } + return true // Stop after first match }, + func(errParser error) { err = errParser }, ) return m, err } diff --git a/api/tools.go b/pkg/api/tools.go similarity index 100% rename from api/tools.go rename to pkg/api/tools.go diff --git a/utilities/aset/aset.go b/pkg/aset/aset.go similarity index 97% rename from utilities/aset/aset.go rename to pkg/aset/aset.go index ef9b584..f98ea20 100644 --- a/utilities/aset/aset.go +++ b/pkg/aset/aset.go @@ -1,6 +1,6 @@ package aset -import "github.com/graph-guard/ggproxy/utilities/math" +import "github.com/graph-guard/ggproxy/pkg/math" type ElementInterface interface { uint8 | uint16 | uint32 | uint64 | int | int8 | int16 | int32 | int64 | float32 | float64 diff --git a/utilities/aset/aset_test.go b/pkg/aset/aset_test.go similarity index 94% rename from utilities/aset/aset_test.go rename to pkg/aset/aset_test.go index b5a9e70..c4480f7 100644 --- a/utilities/aset/aset_test.go +++ b/pkg/aset/aset_test.go @@ -3,8 +3,8 @@ package aset_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/aset" - "github.com/graph-guard/ggproxy/utilities/math" + "github.com/graph-guard/ggproxy/pkg/aset" + "github.com/graph-guard/ggproxy/pkg/math" "github.com/stretchr/testify/require" ) diff --git a/utilities/aset/benchmark_test.go b/pkg/aset/benchmark_test.go similarity index 96% rename from utilities/aset/benchmark_test.go rename to pkg/aset/benchmark_test.go index 040ba22..ee847c4 100644 --- a/utilities/aset/benchmark_test.go +++ b/pkg/aset/benchmark_test.go @@ -6,7 +6,7 @@ import ( "fmt" "testing" - "github.com/graph-guard/ggproxy/utilities/aset" + "github.com/graph-guard/ggproxy/pkg/aset" ) func BenchmarkAdd(b *testing.B) { diff --git a/pkg/atoi/atoi.go b/pkg/atoi/atoi.go new file mode 100644 index 0000000..0ac82cf --- /dev/null +++ b/pkg/atoi/atoi.go @@ -0,0 +1,53 @@ +// Package atoi provides functions for efficient & allocation-free +// parsing of []byte and string to int32 and float64. +package atoi + +import ( + "fmt" + "strconv" + + "github.com/graph-guard/ggproxy/pkg/unsafe" +) + +// MustI32 parses s assuming that it's a valid signed 32-bit integer. +// Panics if s contains an invalid number. +func MustI32[S []byte | string](s S) int32 { + const intSize = 32 << (^uint(0) >> 63) + + sLen := len(s) + if intSize == 32 && (0 < sLen && sLen < 10) || + intSize == 64 && (0 < sLen && sLen < 19) { + // Fast path for small integers that fit int type. + s0 := s + if s[0] == '-' || s[0] == '+' { + s = s[1:] + if len(s) < 1 { + panic("syntax error") + } + } + + n := int32(0) + for _, ch := range []byte(s) { + ch -= '0' + if ch > 9 { + panic("syntax error") + } + n = n*10 + int32(ch) + } + if s0[0] == '-' { + n = -n + } + return n + } + panic("syntax error") +} + +// MustF64 parses s assuming that it's a valid signed 64-bit float. +// Panics if s contains an invalid number. +func MustF64[S []byte | string](s S) float64 { + f, err := strconv.ParseFloat(unsafe.B2S([]byte(s)), 64) + if err != nil { + panic(fmt.Errorf("unexpected float64 parsing err: %w", err)) + } + return f +} diff --git a/pkg/atoi/atoi_test.go b/pkg/atoi/atoi_test.go new file mode 100644 index 0000000..4b01140 --- /dev/null +++ b/pkg/atoi/atoi_test.go @@ -0,0 +1,118 @@ +package atoi_test + +import ( + "fmt" + "math" + "strconv" + "testing" + + "github.com/graph-guard/ggproxy/pkg/atoi" + "github.com/stretchr/testify/require" +) + +// Std wraps strconv.Atoi. +func Std[S []byte | string](s S) int32 { + i, _ := strconv.Atoi(string(s)) + return int32(i) +} + +func TestMustI32(t *testing.T) { + require.Equal(t, int32(0), atoi.MustI32("0")) + require.Equal(t, int32(1), atoi.MustI32("1")) + require.Equal(t, int32(8), atoi.MustI32("8")) + require.Equal(t, int32(-1), atoi.MustI32("-1")) + require.Equal(t, int32(1), atoi.MustI32("+1")) + require.Equal(t, int32(123456789), atoi.MustI32("123456789")) + require.Equal(t, int32(1234567890), atoi.MustI32("1234567890")) + require.Equal(t, int32(math.MaxInt32), atoi.MustI32(fmt.Sprintf("%d", math.MaxInt32))) + require.Equal(t, int32(math.MinInt32), atoi.MustI32(fmt.Sprintf("%d", math.MinInt32))) + + // Error + require.Panics(t, func() { atoi.MustI32("a") }) + require.Panics(t, func() { atoi.MustI32("0xa") }) + require.Panics(t, func() { atoi.MustI32(" 1") }) + require.Panics(t, func() { atoi.MustI32("-0xa") }) + require.Panics(t, func() { atoi.MustI32("-") }) + require.Panics(t, func() { atoi.MustI32("") }) +} + +func TestMustF64(t *testing.T) { + require.Equal(t, float64(0), atoi.MustF64("0")) + require.Equal(t, float64(1), atoi.MustF64("1")) + require.Equal(t, float64(3.14), atoi.MustF64("3.14")) + require.Equal(t, float64(-1), atoi.MustF64("-1")) + require.Equal(t, float64(-1.12345), atoi.MustF64("-1.12345")) + require.Equal(t, float64(1), atoi.MustF64("+1")) + require.Equal(t, float64(123456789), atoi.MustF64("123456789")) + require.Equal(t, float64(1234567890), atoi.MustF64("1234567890")) + require.Equal(t, float64(0.1234567890), atoi.MustF64("0.1234567890")) + require.Equal(t, + float64(math.MaxFloat64), + atoi.MustF64(fmt.Sprintf("%f", math.MaxFloat64)), + ) + require.Equal(t, + float64(-math.MaxFloat64), + atoi.MustF64(fmt.Sprintf("%f", -math.MaxFloat64)), + ) + require.Equal(t, float64(1e12), atoi.MustF64("1e12")) + require.Equal(t, float64(1e+12), atoi.MustF64("1e+12")) + require.Equal(t, float64(1e-12), atoi.MustF64("1e-12")) + require.Equal(t, float64(1e-200), atoi.MustF64("1e-200")) + require.Equal(t, float64(1e308), atoi.MustF64("1e308")) + require.Equal(t, float64(1e-308), atoi.MustF64("1e-308")) + + // Error + require.Panics(t, func() { atoi.MustF64("a") }) + require.Panics(t, func() { atoi.MustF64("0xa") }) + require.Panics(t, func() { atoi.MustF64(" 1") }) + require.Panics(t, func() { atoi.MustF64("-0xa") }) + require.Panics(t, func() { atoi.MustF64("-") }) + require.Panics(t, func() { atoi.MustF64("") }) + require.Panics(t, func() { atoi.MustF64(".") }) + require.Panics(t, func() { atoi.MustF64("1.2.3") }) +} + +var GI32 int32 + +func BenchmarkI32(b *testing.B) { + for _, bb := range []struct { + Name string + Input string + }{ + {"min", fmt.Sprintf("%d", math.MinInt32)}, + {"1", fmt.Sprintf("%d", 1)}, + {"123456789", fmt.Sprintf("%d", 123456789)}, + {"max", fmt.Sprintf("%d", math.MaxInt32)}, + {"plus_prefix", "+1"}, + } { + b.Run(bb.Name, func(b *testing.B) { + b.Run("string", func(b *testing.B) { + b.Run("std", func(b *testing.B) { + for i := 0; i < b.N; i++ { + GI32 = Std(bb.Input) + } + }) + b.Run("custom", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + GI32 = atoi.MustI32(bb.Input) + } + }) + }) + b.Run("byte_slice", func(b *testing.B) { + s := []byte(bb.Input) + b.Run("std", func(b *testing.B) { + for i := 0; i < b.N; i++ { + GI32 = Std(s) + } + }) + b.Run("custom", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + GI32 = atoi.MustI32(s) + } + }) + }) + }) + } +} diff --git a/utilities/bitmask/bench_test.go b/pkg/bitmask/bench_test.go similarity index 100% rename from utilities/bitmask/bench_test.go rename to pkg/bitmask/bench_test.go diff --git a/utilities/bitmask/bitmask.go b/pkg/bitmask/bitmask.go similarity index 91% rename from utilities/bitmask/bitmask.go rename to pkg/bitmask/bitmask.go index eb126b2..c7b793c 100644 --- a/utilities/bitmask/bitmask.go +++ b/pkg/bitmask/bitmask.go @@ -1,25 +1,11 @@ -//go:build go1.10 -// +build go1.10 - -// Package bitmask provides a bit array implementation. +// Package bitmask provides a bit array/mask implementation. // -// # Bit set +// Forked from github.com/yourbasic/bit. // // A bit set, or bit array, is an efficient set data structure // that consists of an array of 64-bit words. Because it uses // bit-level parallelism, limits memory access, and efficiently uses // the data cache, a bit set often outperforms other data structures. -// -// # Tutorial -// -// The Basics example shows how to create, combine, compare and -// print bit sets. -// -// Primes contains a short and simple, but still efficient, -// implementation of a prime number sieve. -// -// Union is a more advanced example demonstrating how to build -// an efficient variadic Union function using the SetOr method. package bitmask import ( @@ -410,30 +396,6 @@ func (s *Set) DeleteRange(m, n int) *Set { return s } -// And creates a new set that consists of all elements that belong -// to both s1 and s2. -func (s *Set) And(s1 *Set) *Set { - return new(Set).SetAnd(s1, s1) -} - -// Or creates a new set that contains all elements that belong -// to either s1 or s2. -func (s *Set) Or(s1 *Set) *Set { - return new(Set).SetOr(s, s1) -} - -// Xor creates a new set that contains all elements that belong -// to either s1 or s2, but not to both. -func (s *Set) Xor(s1 *Set) *Set { - return new(Set).SetXor(s, s1) -} - -// AndNot creates a new set that consists of all elements that belong -// to s1, but not to s2. -func (s *Set) AndNot(s1 *Set) *Set { - return new(Set).SetAndNot(s, s1) -} - // Set sets s to s1 and then returns a pointer to the updated set s. func (s *Set) Set(s1 *Set) *Set { s.realloc(len(s1.data)) diff --git a/utilities/bitmask/example_test.go b/pkg/bitmask/example_test.go similarity index 100% rename from utilities/bitmask/example_test.go rename to pkg/bitmask/example_test.go diff --git a/utilities/bitmask/example_union_test.go b/pkg/bitmask/example_union_test.go similarity index 100% rename from utilities/bitmask/example_union_test.go rename to pkg/bitmask/example_union_test.go diff --git a/utilities/bitmask/funcs.go b/pkg/bitmask/funcs.go similarity index 100% rename from utilities/bitmask/funcs.go rename to pkg/bitmask/funcs.go diff --git a/utilities/bitmask/funcs_test.go b/pkg/bitmask/funcs_test.go similarity index 100% rename from utilities/bitmask/funcs_test.go rename to pkg/bitmask/funcs_test.go diff --git a/utilities/bitmask/set_test.go b/pkg/bitmask/set_test.go similarity index 100% rename from utilities/bitmask/set_test.go rename to pkg/bitmask/set_test.go diff --git a/cli/cli.go b/pkg/cli/cli.go similarity index 75% rename from cli/cli.go rename to pkg/cli/cli.go index 186ce43..a5f68c6 100644 --- a/cli/cli.go +++ b/pkg/cli/cli.go @@ -8,10 +8,11 @@ import ( "path/filepath" ) -const EnvAPIUsername = "GGPROXY_API_USERNAME" -const EnvAPIPassword = "GGPROXY_API_PASSWORD" -const EnvLicense = "GGPROXY_LICENSE" -const LinkDashboardDownload = "https://graphguard.io/dashboard#download" +const ( + EnvAPIUsername = "GGPROXY_API_USERNAME" + EnvAPIPassword = "GGPROXY_API_PASSWORD" + LinkDashboardDownload = "https://graphguard.io/dashboard#download" +) // Command can be any of: // @@ -23,7 +24,6 @@ type Command any type CommandServe struct { ConfigDirPath string - LicenseToken string APIUsername string APIPassword string } @@ -35,7 +35,6 @@ type CommandStop struct{} func Parse( w io.Writer, args []string, - validateLicenseToken func(string) error, ) (cmd Command) { fm := fmt.Sprintf @@ -73,7 +72,6 @@ func Parse( c := CommandServe{} c.APIUsername = os.Getenv(EnvAPIUsername) c.APIPassword = os.Getenv(EnvAPIPassword) - c.LicenseToken = os.Getenv(EnvLicense) flags.Usage = func() { writeLines(w, @@ -88,7 +86,6 @@ func Parse( fm("%s: API basic auth username "+ "(enables basic auth if set)", EnvAPIUsername), fm("%s: API basic auth password", EnvAPIPassword), - fm("%s: License key", EnvLicense), ) } @@ -97,25 +94,6 @@ func Parse( return nil } - err := validateLicenseToken(c.LicenseToken) - - if c.LicenseToken == "" { - writeLines(w, - EnvLicense+" isn't set.", - fm("You can get the license key at %s", LinkDashboardDownload), - ) - flags.Usage() - return nil - } else if err != nil { - writeLines(w, - err.Error(), - EnvLicense+" contains an invalid license key!", - fm("You can get a valid license key at %s", LinkDashboardDownload), - ) - flags.Usage() - return nil - } - if c.APIUsername != "" && c.APIPassword == "" { writeLines(w, EnvAPIPassword+" isn't set.", diff --git a/cli/cli_test.go b/pkg/cli/cli_test.go similarity index 51% rename from cli/cli_test.go rename to pkg/cli/cli_test.go index a14b4fc..1e5edf3 100644 --- a/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/graph-guard/ggproxy/cli" + "github.com/graph-guard/ggproxy/pkg/cli" "github.com/stretchr/testify/require" ) @@ -25,48 +25,34 @@ func helpOutput(execName string) string { func TestNoArgs(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse(out, nil, func(s string) error { return nil }) + c := cli.Parse(out, nil) require.Nil(t, c) require.Equal(t, helpOutput("ggproxy"), out.String()) } func TestNoCommand(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"execname"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"execname"}) require.Nil(t, c) require.Equal(t, helpOutput("execname"), out.String()) } func TestUnknownCommand(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"execname", "unknown-command"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"execname", "unknown-command"}) require.Nil(t, c) require.Equal(t, helpOutput("execname"), out.String()) } func TestCommandServe(t *testing.T) { - os.Setenv(cli.EnvLicense, "TESTLICENSETOKEN") os.Setenv(cli.EnvAPIUsername, "testusername") os.Setenv(cli.EnvAPIPassword, "testpassword") t.Run("default_config_path", func(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"ggproxy", "serve"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"ggproxy", "serve"}) require.Equal(t, cli.CommandServe{ ConfigDirPath: "/etc/ggproxy", - LicenseToken: "TESTLICENSETOKEN", APIUsername: "testusername", APIPassword: "testpassword", }, c) @@ -75,16 +61,11 @@ func TestCommandServe(t *testing.T) { t.Run("custom_config_path", func(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{ - "ggproxy", "serve", - "-config", "./custom_config", - }, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{ + "ggproxy", "serve", + "-config", "./custom_config", + }) require.Equal(t, cli.CommandServe{ - LicenseToken: "TESTLICENSETOKEN", ConfigDirPath: "./custom_config", APIUsername: "testusername", APIPassword: "testpassword", @@ -94,14 +75,10 @@ func TestCommandServe(t *testing.T) { t.Run("unknown_flags", func(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{ - "ggproxy", "serve", - "-unknown", "foobar", - }, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{ + "ggproxy", "serve", + "-unknown", "foobar", + }) require.Nil(t, c) require.Equal(t, lines( @@ -118,7 +95,6 @@ func TestCommandServe(t *testing.T) { "GGPROXY_API_USERNAME: API basic auth username "+ "(enables basic auth if set)", "GGPROXY_API_PASSWORD: API basic auth password", - "GGPROXY_LICENSE: License key", ), out.String(), ) @@ -129,11 +105,7 @@ func TestAPIPasswordNotSet(t *testing.T) { out := new(bytes.Buffer) os.Setenv(cli.EnvAPIUsername, "testusername") os.Setenv(cli.EnvAPIPassword, "") - c := cli.Parse( - out, - []string{"ggproxy", "serve"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"ggproxy", "serve"}) require.Nil(t, c) require.Equal(t, lines( @@ -154,87 +126,6 @@ func TestAPIPasswordNotSet(t *testing.T) { "GGPROXY_API_USERNAME: API basic auth username "+ "(enables basic auth if set)", "GGPROXY_API_PASSWORD: API basic auth password", - "GGPROXY_LICENSE: License key", - ), - out.String(), - ) -} - -func TestLicenseTokenNotSet(t *testing.T) { - out := new(bytes.Buffer) - os.Setenv(cli.EnvAPIUsername, "testusername") - os.Setenv(cli.EnvAPIPassword, "testpassword") - os.Setenv(cli.EnvLicense, "") - c := cli.Parse( - out, - []string{"ggproxy", "serve"}, - func(s string) error { return nil }, - ) - require.Nil(t, c) - - require.Equal(t, - lines( - fmt.Sprintf("%s isn't set.", cli.EnvLicense), - fmt.Sprintf( - "You can get the license key at %s", - cli.LinkDashboardDownload, - ), - "", - "usage: ggproxy serve [-config ]", - "", - "flags:", - "-config : "+ - "defines the configuration directory path "+ - "(default: /etc/ggproxy)", - "", - "environment variables:", - "GGPROXY_API_USERNAME: API basic auth username "+ - "(enables basic auth if set)", - "GGPROXY_API_PASSWORD: API basic auth password", - "GGPROXY_LICENSE: License key", - ), - out.String(), - ) -} - -func TestLicenseTokenInvalid(t *testing.T) { - out := new(bytes.Buffer) - os.Setenv(cli.EnvAPIUsername, "testusername") - os.Setenv(cli.EnvAPIPassword, "testpassword") - os.Setenv(cli.EnvLicense, "thiskeyisinvalid") - c := cli.Parse( - out, - []string{"ggproxy", "serve"}, - func(s string) error { - if s != "valid" { - return fmt.Errorf("invalid") - } - return nil - }, - ) - require.Nil(t, c) - - require.Equal(t, - lines( - "invalid", - fmt.Sprintf("%s contains an invalid license key!", cli.EnvLicense), - fmt.Sprintf( - "You can get a valid license key at %s", - cli.LinkDashboardDownload, - ), - "", - "usage: ggproxy serve [-config ]", - "", - "flags:", - "-config : "+ - "defines the configuration directory path "+ - "(default: /etc/ggproxy)", - "", - "environment variables:", - "GGPROXY_API_USERNAME: API basic auth username "+ - "(enables basic auth if set)", - "GGPROXY_API_PASSWORD: API basic auth password", - "GGPROXY_LICENSE: License key", ), out.String(), ) @@ -242,33 +133,21 @@ func TestLicenseTokenInvalid(t *testing.T) { func TestCommandReload(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"execname", "reload"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"execname", "reload"}) require.Equal(t, cli.CommandReload{}, c) require.Equal(t, "", out.String()) } func TestCommandStop(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"execname", "stop"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"execname", "stop"}) require.Equal(t, cli.CommandStop{}, c) require.Equal(t, "", out.String()) } func TestCommandHelp(t *testing.T) { out := new(bytes.Buffer) - c := cli.Parse( - out, - []string{"execname", "help"}, - func(s string) error { return nil }, - ) + c := cli.Parse(out, []string{"execname", "help"}) require.Nil(t, c) e := new(bytes.Buffer) diff --git a/config/config.go b/pkg/config/config.go similarity index 56% rename from config/config.go rename to pkg/config/config.go index 4486c10..28211eb 100644 --- a/config/config.go +++ b/pkg/config/config.go @@ -1,28 +1,30 @@ package config import ( + "bytes" "crypto/md5" + "encoding/base32" "errors" "fmt" - "io" + "io/fs" neturl "net/url" - "os" "path/filepath" - "reflect" "regexp" "strings" "github.com/dustin/go-humanize" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/graph-guard/ggproxy/config/metadata" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/gqt" + "github.com/graph-guard/ggproxy/pkg/config/metadata" + gqt "github.com/graph-guard/gqt/v4" + gqlparser "github.com/vektah/gqlparser/v2" + gqlast "github.com/vektah/gqlparser/v2/ast" + "golang.org/x/exp/slices" yaml "gopkg.in/yaml.v3" ) -var ConfigFileExtension = regexp.MustCompile(`\.(yml|yaml)$`) -var TemplateFileExtension = regexp.MustCompile(`\.gqt$`) +var ( + ConfigFileExtension = regexp.MustCompile(`\.(yml|yaml)$`) + TemplateFileExtension = regexp.MustCompile(`\.gqt$`) +) // MinReqBodySize defines the minimum accepted value for // `max-request-body-size` in bytes. @@ -40,37 +42,10 @@ var msgMaxReqBodySizeTooSmall = fmt.Sprintf( type Config struct { Proxy ProxyServerConfig API *APIServerConfig - Services *hamap.Map[[]byte, *Service] + Services map[string]*Service ServicesEnabled []*Service } -func (c *Config) Equal(d *Config) bool { - eq := true - c.Services.Visit(func(key []byte, value *Service) (stop bool) { - v, ok := d.Services.Get(key) - if !ok { - eq = false - return true - } - if !v.Equal(value) { - eq = false - return true - } - return - }) - if !eq { - return eq - } - - less := func(a, b *Service) bool { return a.ID < b.ID } - eq = eq && - reflect.DeepEqual(c.Proxy, d.Proxy) && - reflect.DeepEqual(c.API, d.API) && - cmp.Equal(c.ServicesEnabled, d.ServicesEnabled, cmpopts.SortSlices(less)) - - return eq -} - type ProxyServerConfig struct { Host string TLS TLS @@ -91,33 +66,22 @@ type Service struct { ID string Path string ForwardURL string - Templates *hamap.Map[[]byte, *Template] + Schema *gqlast.Schema + Templates map[string]*Template TemplatesEnabled []*Template ForwardReduced bool Enabled bool FilePath string } -func (c *Service) Equal(d *Service) bool { - less := func(a, b *Template) bool { return a.ID < b.ID } - return c.ID == d.ID && - c.Path == d.Path && - c.ForwardURL == d.ForwardURL && - c.ForwardReduced == d.ForwardReduced && - c.Enabled == d.Enabled && - c.FilePath == d.FilePath && - reflect.DeepEqual(c.Templates, d.Templates) && - cmp.Equal(c.TemplatesEnabled, d.TemplatesEnabled, cmpopts.SortSlices(less)) -} - type Template struct { - ID string - Source []byte - Document gqt.Doc - Name string - Tags []string - Enabled bool - FilePath string + ID string + Source []byte + GQTTemplate *gqt.Operation + Name string + Tags []string + Enabled bool + FilePath string } type serverConfig struct { @@ -147,28 +111,28 @@ type serviceConfig struct { ForwardReduced bool `yaml:"forward-reduced"` TemplatesAll string `yaml:"all-templates"` TemplatesEnabled string `yaml:"enabled-templates"` + Schema string `yaml:"schema"` } -func New(path string) (c *Config, err error) { +// Read parses configuration files and composes the server config. +func Read(fsys fs.FS, basePath, path string) (c *Config, err error) { // Set default config values c = &Config{ Proxy: ProxyServerConfig{}, API: &APIServerConfig{}, - Services: hamap.New[[]byte, *Service](0, nil), + Services: make(map[string]*Service), } - err = c.readServerConfig(path) - if err != nil { + if err = c.readServerConfig(fsys, basePath, path); err != nil { return nil, err } - - return + return c, nil } -func (c *Config) readServerConfig(path string) (err error) { +func (c *Config) readServerConfig(fsys fs.FS, basePath, path string) (err error) { dirPath := filepath.Dir(path) - file, err := os.Open(path) + file, err := openFile(fsys, basePath, path, "server config") if err != nil { - return fmt.Errorf("opening server config file: %w", err) + return err } sc := &serverConfig{} @@ -176,14 +140,13 @@ func (c *Config) readServerConfig(path string) (err error) { d.KnownFields(true) if err := d.Decode(sc); err != nil { return &ErrorIllegal{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "syntax", Message: err.Error(), } } - err = validateServerConfig(sc, path) - if err != nil { + if err := validateServerConfig(sc, basePath, path); err != nil { return err } @@ -219,24 +182,47 @@ func (c *Config) readServerConfig(path string) (err error) { } // reading all services - err = c.readAllServices(servicesAllPath) - if err != nil { + if err := c.readAllServices(fsys, basePath, servicesAllPath); err != nil { return err } + if len(c.Services) < 1 { + return ErrNoServices + } + // reading enabled services - err = c.readEnabledServices(servicesEnabledPath) - if err != nil { + if err := c.readEnabledServices(fsys, servicesEnabledPath); err != nil { return err } + if len(c.ServicesEnabled) < 1 { + return ErrNoServicesEnabled + } + + { // Make sure there are no collisions on service paths + sk := sortedKeys(c.Services) + paths := make(map[string]*Service, len(c.Services)) + for _, sk := range sk { + s := c.Services[sk] + if s2, ok := paths[s.Path]; ok { + return &ErrorConflict{ + Feature: "path", + Value: s.Path, + Subject1: s.FilePath, + Subject2: s2.FilePath, + } + } + paths[s.Path] = s + } + } + return } -func validateServerConfig(sc *serverConfig, path string) (err error) { +func validateServerConfig(sc *serverConfig, basePath, path string) (err error) { if sc.Proxy.Host == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "proxy.host", } } @@ -248,13 +234,13 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { switch { case c.CertFile != "" && c.KeyFile == "": return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "proxy.tls.key-file", } case (c.KeyFile != "" && c.CertFile == "") || (c.KeyFile == "" && c.CertFile == ""): return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "proxy.tls.cert-file", } } @@ -264,7 +250,7 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { c := sc.API if c.Host == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "api.host", } } @@ -276,13 +262,13 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { switch { case c.CertFile != "" && c.KeyFile == "": return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "api.tls.key-file", } case (c.KeyFile != "" && c.CertFile == "") || (c.KeyFile == "" && c.CertFile == ""): return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "api.tls.cert-file", } } @@ -292,7 +278,7 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { if sc.Proxy.MaxRequestBodySizeBytes != nil { if *sc.Proxy.MaxRequestBodySizeBytes < MinReqBodySize { return &ErrorIllegal{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "proxy.max-request-body-size", Message: msgMaxReqBodySizeTooSmall, } @@ -301,13 +287,13 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { if sc.ServicesAll == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "all-services", } } if sc.ServicesEnabled == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "enabled-services", } } @@ -315,8 +301,8 @@ func validateServerConfig(sc *serverConfig, path string) (err error) { return } -func (c *Config) readAllServices(path string) (err error) { - d, err := os.ReadDir(path) +func (c *Config) readAllServices(fsys fs.FS, basePath, path string) (err error) { + d, err := fs.ReadDir(fsys, path) if err != nil { return fmt.Errorf("reading services directory: %w", err) } @@ -330,34 +316,31 @@ func (c *Config) readAllServices(path string) (err error) { continue } - file, err := openFile(filepath.Join(path, sf.Name())) - if err != nil { - return err - } - h, err := calculateHash(file) + filePath := filepath.Join(path, sf.Name()) + h, err := calculateHash(fsys, filePath) if err != nil { return err } - s, err := readServiceConfig(file) + s, err := readServiceConfig(fsys, basePath, filePath) if err != nil { return err } - original, ok := c.Services.Get(h) + original, ok := c.Services[string(h)] if ok { return &ErrorDuplicate{ Original: original.FilePath, Duplicate: s.FilePath, } } - c.Services.Set(h, s) + c.Services[string(h)] = s } return } -func (c *Config) readEnabledServices(path string) (err error) { - d, err := os.ReadDir(path) +func (c *Config) readEnabledServices(fsys fs.FS, path string) (err error) { + d, err := fs.ReadDir(fsys, path) if err != nil { return fmt.Errorf("reading enabled services directory: %w", err) } @@ -373,15 +356,11 @@ func (c *Config) readEnabledServices(path string) (err error) { } filePath := filepath.Join(path, sf.Name()) - file, err := openFile(filePath) + h, err := calculateHash(fsys, filePath) if err != nil { return err } - h, err := calculateHash(file) - if err != nil { - return err - } - s, ok := c.Services.Get(h) + s, ok := c.Services[string(h)] if ok { s.Enabled = true c.ServicesEnabled = append(c.ServicesEnabled, s) @@ -397,22 +376,36 @@ func (c *Config) readEnabledServices(path string) (err error) { return } -func readServiceConfig(file *os.File) (s *Service, err error) { - filePath := file.Name() - dirPath := filepath.Dir(file.Name()) +func readServiceConfig(fsys fs.FS, basePath, filePath string) (s *Service, err error) { + dirPath := filepath.Dir(filePath) sc := &serviceConfig{} - d := yaml.NewDecoder(file) - d.KnownFields(true) - if err := d.Decode(sc); err != nil { - return nil, &ErrorIllegal{ - FilePath: filePath, - Feature: "syntax", - Message: err.Error(), + { + c, err := fs.ReadFile(fsys, filePath) + if err != nil { + return nil, err + } + d := yaml.NewDecoder(bytes.NewReader(c)) + d.KnownFields(true) + if err := d.Decode(sc); err != nil { + return nil, &ErrorIllegal{ + FilePath: filepath.Join(basePath, filePath), + Feature: "syntax", + Message: err.Error(), + } } } - err = validateServiceConfig(sc, filePath) + if err := validateServiceConfig(sc, basePath, filePath); err != nil { + return nil, err + } + + // TODO: Add support for multiple graphqls files + var schemaPath string + if sc.Schema != "" { + schemaPath = filepath.Join(dirPath, sc.Schema) + } + schema, gqtParser, err := s.readSchema(fsys, basePath, schemaPath) if err != nil { return nil, err } @@ -430,7 +423,7 @@ func readServiceConfig(file *os.File) (s *Service, err error) { id := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) if err := ValidateID(id); err != "" { return nil, &ErrorIllegal{ - FilePath: filePath, + FilePath: filepath.Join(basePath, filePath), Feature: "id", Message: err, } @@ -439,64 +432,73 @@ func readServiceConfig(file *os.File) (s *Service, err error) { s = &Service{ ID: id, - Templates: hamap.New[[]byte, *Template](0, nil), - FilePath: filePath, + Schema: schema, + Templates: map[string]*Template{}, + FilePath: filepath.Join(basePath, filePath), Path: sc.Path, ForwardURL: sc.ForwardURL, ForwardReduced: sc.ForwardReduced, } // reading all templates - err = s.readAllTemplates(templatesAllPath) - if err != nil { + if err := s.readAllTemplates( + fsys, basePath, templatesAllPath, gqtParser, + ); err != nil { return nil, err } + if len(s.Templates) < 1 { + return nil, ErrNoTemplates + } + // reading enabled templates - err = s.readEnabledTemplates(templatesEnabledPath) - if err != nil { + if err := s.readEnabledTemplates(fsys, templatesEnabledPath); err != nil { return nil, err } + if len(s.TemplatesEnabled) < 1 { + return nil, ErrNoTemplatesEnabled + } + return } -func validateServiceConfig(sc *serviceConfig, path string) (err error) { +func validateServiceConfig(sc *serviceConfig, basePath, path string) (err error) { if sc.Path == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "path", } } if err := validatePath(sc.Path); err != nil { return &ErrorIllegal{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "path", Message: err.Error(), } } if sc.ForwardURL == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "forward-url", } } if err := validateURL(sc.ForwardURL); err != nil { return &ErrorIllegal{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "forward-url", Message: err.Error(), } } if sc.TemplatesAll == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "all-templates", } } if sc.TemplatesEnabled == "" { return &ErrorMissing{ - FilePath: path, + FilePath: filepath.Join(basePath, path), Feature: "enabled-templates", } } @@ -504,8 +506,44 @@ func validateServiceConfig(sc *serviceConfig, path string) (err error) { return } -func (s *Service) readAllTemplates(path string) (err error) { - dir, err := os.ReadDir(path) +func (s *Service) readSchema(fsys fs.FS, basePath, path string) (*gqlast.Schema, *gqt.Parser, error) { + if path == "" { + p, err := gqt.NewParser(nil) + return nil, p, err + } + + f, err := fs.ReadFile(fsys, path) + if err != nil { + return nil, nil, &ErrorMissing{ + FilePath: filepath.Join(basePath, path), + Feature: "schema", + } + } + + schema, err := gqlparser.LoadSchema(&gqlast.Source{ + Name: filepath.Join(basePath, path), + Input: string(f), + }) + if err != nil { + return nil, nil, &ErrorIllegal{ + FilePath: filepath.Join(basePath, path), + Feature: "schema", + Message: fmt.Sprintf("invalid schema: %v", err.Error()), + } + } + + gqtParser, err := gqt.NewParser([]gqt.Source{ + {Name: filepath.Join(basePath, path), Content: string(f)}, + }) + return schema, gqtParser, err +} + +func (s *Service) readAllTemplates( + fsys fs.FS, + basePath, path string, + p *gqt.Parser, +) (err error) { + dir, err := fs.ReadDir(fsys, path) if err != nil { return fmt.Errorf("reading templates directory: %w", err) } @@ -519,35 +557,32 @@ func (s *Service) readAllTemplates(path string) (err error) { continue } - file, err := openFile(filepath.Join(path, tf.Name())) - if err != nil { - return err - } - h, err := calculateHash(file) + path := filepath.Join(path, tf.Name()) + h, err := calculateHash(fsys, path) if err != nil { return err } - t, err := readTemplate(file) + t, err := readTemplate(fsys, basePath, path, p) if err != nil { return err } - original, ok := s.Templates.Get(h) + original, ok := s.Templates[string(h)] if ok { return &ErrorDuplicate{ Original: original.FilePath, Duplicate: t.FilePath, } } - s.Templates.Set(h, t) + s.Templates[string(h)] = t } return } -func (s *Service) readEnabledTemplates(path string) (err error) { - dir, err := os.ReadDir(path) +func (s *Service) readEnabledTemplates(fsys fs.FS, path string) (err error) { + dir, err := fs.ReadDir(fsys, path) if err != nil { return fmt.Errorf("reading enabled templates directory: %w", err) } @@ -562,16 +597,12 @@ func (s *Service) readEnabledTemplates(path string) (err error) { continue } - file, err := openFile(filepath.Join(path, tf.Name())) + p := filepath.Join(path, tf.Name()) + h, err := calculateHash(fsys, p) if err != nil { return err } - - h, err := calculateHash(file) - if err != nil { - return err - } - t, ok := s.Templates.Get(h) + t, ok := s.Templates[string(h)] if ok { t.Enabled = true s.TemplatesEnabled = append(s.TemplatesEnabled, t) @@ -587,9 +618,11 @@ func (s *Service) readEnabledTemplates(path string) (err error) { return } -func readTemplate(file *os.File) (t *Template, err error) { - filePath := file.Name() - +func readTemplate( + fsys fs.FS, + basePath, filePath string, + p *gqt.Parser, +) (t *Template, err error) { id := strings.ToLower( strings.TrimSuffix( filepath.Base(filePath), filepath.Ext(filePath), @@ -597,13 +630,13 @@ func readTemplate(file *os.File) (t *Template, err error) { ) if err := ValidateID(id); err != "" { return nil, &ErrorIllegal{ - FilePath: filePath, + FilePath: filepath.Join(basePath, filePath), Feature: "id", Message: err, } } - b, err := io.ReadAll(file) + b, err := fs.ReadFile(fsys, filePath) if err != nil { return nil, fmt.Errorf("reading template %q: %w", filePath, err) } @@ -611,28 +644,35 @@ func readTemplate(file *os.File) (t *Template, err error) { meta, template, err := metadata.Parse(b) if err != nil { return nil, &ErrorIllegal{ - FilePath: filePath, + FilePath: filepath.Join(basePath, filePath), Feature: "metadata", Message: err.Error(), } } - doc, errParser := gqt.Parse(template) - if errParser.IsErr() { + doc, _, errs := p.Parse(template) + if errs != nil { + var msg strings.Builder + for i := range errs { + msg.WriteString(errs[i].Error()) + if i+1 < len(errs) { + msg.WriteString("; ") + } + } return nil, &ErrorIllegal{ - FilePath: filePath, + FilePath: filepath.Join(basePath, filePath), Feature: "template", - Message: errParser.Error(), + Message: msg.String(), } } t = &Template{ - ID: id, - Source: template, - Document: doc, - Name: meta.Name, - Tags: meta.Tags, - FilePath: filePath, + ID: id, + FilePath: filepath.Join(basePath, filePath), + Source: template, + GQTTemplate: doc, + Name: meta.Name, + Tags: meta.Tags, } return @@ -655,18 +695,20 @@ const IDValidCharDict = "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-" -type ErrorDuplicate struct { - Original string - Duplicate string -} +type ErrorDuplicate struct{ Original, Duplicate string } func (e ErrorDuplicate) Error() string { return fmt.Sprintf("%s is a duplicate of %s", e.Duplicate, e.Original) } -type ErrorAlien struct { - Items []string -} +var ( + ErrNoServices = errors.New("no services defined") + ErrNoServicesEnabled = errors.New("no services enabled") + ErrNoTemplates = errors.New("no templates defined") + ErrNoTemplatesEnabled = errors.New("no templates enabled") +) + +type ErrorAlien struct{ Items []string } func (e ErrorAlien) Error() string { var b strings.Builder @@ -680,53 +722,33 @@ func (e ErrorAlien) Error() string { return b.String() } -type ErrorMissing struct { - FilePath string - Feature string +type ErrorConflict struct{ Feature, Value, Subject1, Subject2 string } + +func (e ErrorConflict) Error() string { + return "conflict on " + e.Feature + " (" + e.Value + + ") between " + e.Subject1 + " and " + e.Subject2 } +type ErrorMissing struct{ FilePath, Feature string } + func (e ErrorMissing) Error() string { - var b strings.Builder if e.Feature == "" { - b.Grow(len("missing ") + len(e.FilePath)) - b.WriteString("missing ") - b.WriteString(e.FilePath) - return b.String() - } - b.Grow(len("missing ") + len(e.Feature) + len(" in ") + len(e.FilePath)) - b.WriteString("missing ") - b.WriteString(e.Feature) - b.WriteString(" in ") - b.WriteString(e.FilePath) - return b.String() + return "missing " + e.FilePath + } + return "missing " + e.Feature + " in " + e.FilePath } -type ErrorIllegal struct { - FilePath string - Feature string - Message string -} +type ErrorIllegal struct{ FilePath, Feature, Message string } func (e ErrorIllegal) Error() string { - var b strings.Builder - b.Grow(len("illegal ") + - len(e.Feature) + - len(" in ") + - len(e.FilePath) + - len(": ") + - len(e.Message)) - b.WriteString("illegal ") - b.WriteString(e.Feature) - b.WriteString(" in ") - b.WriteString(e.FilePath) - b.WriteString(": ") - b.WriteString(e.Message) - return b.String() + return "illegal " + e.Feature + " in " + e.FilePath + ": " + e.Message } -var ErrPathNotAbsolute = errors.New("path is not starting with /") -var ErrURLProtocolProblem = errors.New("protocol is not supported or undefined") -var ErrURLNoHost = errors.New("host is not defined") +var ( + ErrPathNotAbsolute = errors.New("path is not starting with /") + ErrURLProtocolProblem = errors.New("protocol is not supported or undefined") + ErrURLNoHost = errors.New("host is not defined") +) var ValidProtocolSchemes = []string{"http", "https"} @@ -764,37 +786,40 @@ func contains[T any](arr []T, x T, equal func(a, b T) bool) int { return -1 } -func openFile(path string) (*os.File, error) { - info, err := os.Lstat(path) +func openFile(fsys fs.FS, basePath, path, feature string) (fs.File, error) { + f, err := fsys.Open(path) if err != nil { - return nil, fmt.Errorf("getting information about file: %w", err) - } - if info.Mode()&os.ModeSymlink != 0 { - path, err = filepath.EvalSymlinks(path) - if err != nil { - return nil, fmt.Errorf("reading link: %w", err) + if errors.Is(err, fs.ErrNotExist) { + return nil, &ErrorMissing{ + FilePath: filepath.Join(basePath, path), + Feature: feature, + } } - } - f, err := os.Open(path) - if err != nil { return nil, fmt.Errorf("opening file: %w", err) } - return f, nil } -func calculateHash(file *os.File) (sum []byte, err error) { +// calculateHash returns a base32 encoded MD5 hash of file. +func calculateHash(fsys fs.FS, path string) (string, error) { h := md5.New() - - _, err = io.Copy(h, file) + c, err := fs.ReadFile(fsys, path) if err != nil { - return nil, err + return "", fmt.Errorf("reading file: %w", err) } - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return nil, err + if _, err := h.Write(c); err != nil { + return "", fmt.Errorf("writing: %w", err) } - sum = h.Sum(nil) + sum := h.Sum(nil) + s := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(sum) + return s, nil +} - return +func sortedKeys[T any](m map[string]T) []string { + l := make([]string, 0, len(m)) + for k := range m { + l = append(l, k) + } + slices.Sort(l) + return l } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..3160bcc --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,992 @@ +package config_test + +import ( + "crypto/md5" + "encoding/base32" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "testing/fstest" + + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/config/metadata" + "github.com/graph-guard/gqt/v4" + "github.com/stretchr/testify/require" + gqlparser "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +type TestOK struct { + Path string + Expect *config.Config +} + +type TestError struct { + Filesystem fstest.MapFS + Check func(*testing.T, error) +} + +var ServerConfigFileName = "config.yml" + +func TestRead(t *testing.T) { + basePath, expect := validFS(t) + actual, err := config.Read( + os.DirFS(basePath), + basePath, + ServerConfigFileName, + ) + require.NoError(t, err) + require.Equal(t, expect, actual) +} + +func TestReadDefaultMaxReqBodySize(t *testing.T) { + basePath, conf := validFS(t) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:443`, + ` tls:`, + ` cert-file: proxy.cert`, + ` key-file: proxy.key`, + ` # max-request-body-size: 1234`, + `api:`, + ` host: localhost:3000`, + ` tls:`, + ` cert-file: api.cert`, + ` key-file: api.key`, + `all-services: all-services`, + `enabled-services: enabled-services`, + ), + }, nil, basePath) + conf.Proxy.MaxReqBodySizeBytes = config.DefaultMaxReqBodySize +} + +func TestErrMissingServerConfig(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + err := os.Remove(p) + require.NoError(t, err) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "server config", + }, err) + require.Nil(t, c) +} + +func TestErrMalformedServerConfig(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines("not a valid config"), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: p, + Feature: "syntax", + Message: "yaml: unmarshal errors:\n " + + "line 1: cannot unmarshal !!str `not a v...` " + + "into config.serverConfig", + }, err) + require.Nil(t, c) +} + +func TestErrMissingProxyHostConfig(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: `, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "proxy.host", + }, err) + require.Nil(t, c) +} + +func TestErrMissingAPIHostConfig(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + `api:`, + ` host: `, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "api.host", + }, err) + require.Nil(t, c) +} + +func TestErrMissingProxyTLSCert(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + ` tls:`, + ` key-file: proxy.key`, + `api:`, + ` host: localhost:9090`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "proxy.tls.cert-file", + }, err) + require.Nil(t, c) +} + +func TestErrMissingProxyTLSKey(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + ` tls:`, + ` cert-file: proxy.cert`, + `api:`, + ` host: localhost:9090`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "proxy.tls.key-file", + }, err) + require.Nil(t, c) +} + +func TestErrMissingAPITLSCert(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + `api:`, + ` host: localhost:9090`, + ` tls:`, + ` key-file: api.key`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "api.tls.cert-file", + }, err) + require.Nil(t, c) +} + +func TestErrMissingAPITLSKey(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + `api:`, + ` host: localhost:9090`, + ` tls:`, + ` cert-file: api.cert`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "api.tls.key-file", + }, err) + require.Nil(t, c) +} + +func TestErrIllegalProxyMaxReqBodySize(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, ServerConfigFileName) + createFiles(t, map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:8080`, + ` max-request-body-size: 255`, + `api:`, + ` host: localhost:9090`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: p, + Feature: "proxy.max-request-body-size", + Message: fmt.Sprintf( + "maximum request body size should not be smaller than %d B", + config.MinReqBodySize, + ), + }, err) + require.Nil(t, c) +} + +func TestErrNoServices(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "irrelevant_file.txt": `this file only keeps the directory`, + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, config.ErrNoServices, err) + require.Nil(t, c) +} + +func TestErrNoServicesEnabled(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ), + }, + "all-templates": map[string]any{ + "a": map[string]any{ + "a.gqt": `query { foo }`, + }, + }, + "enabled-templates": map[string]any{ + "a": map[string]any{ + "a.gqt": `query { foo }`, + }, + }, + "enabled-services": map[string]any{ + "placeholder.txt": "no services here", + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, config.ErrNoServicesEnabled, err) + require.Nil(t, c) +} + +func TestErrNoTemplates(t *testing.T) { + basePath := minValidFS(t) + serviceAConf := lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": serviceAConf, + }, + "all-templates": map[string]any{ + "placeholder.txt": "no templates here", + }, + "enabled-services": map[string]any{ + "a.yml": serviceAConf, + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, config.ErrNoTemplates, err) + require.Nil(t, c) +} + +func TestErrMalformedMetadata(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join("all-templates", "a", "a.gqt") + createFiles(t, map[string]any{ + p: lines( + "---", + "malformed metadata", + "---", + `query { foo }`, + ), + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, p), + Feature: "metadata", + Message: "decoding yaml: yaml: " + + "unmarshal errors:\n " + + "line 1: cannot unmarshal !!str `malform...` " + + "into metadata.Metadata", + }, err) + require.Nil(t, c) +} + +func TestErrDuplicateTemplate(t *testing.T) { + basePath, _ := validFS(t) + t1 := filepath.Join("all-templates", "a", "d1.gqt") + t2 := filepath.Join("all-templates", "a", "d2.gqt") + createFiles(t, map[string]any{ + t1: `query { foo }`, + t2: `query { foo }`, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorDuplicate{ + Original: filepath.Join(basePath, t1), + Duplicate: filepath.Join(basePath, t2), + }, err) + require.Nil(t, c) +} + +func TestErrDuplicateService(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ), + "b.yml": lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ), + }, + "all-templates": map[string]any{ + "a": map[string]any{"a.gqt": `query {foo}`}, + }, + "enabled-templates": map[string]any{ + "a": map[string]any{"a.gqt": `query {foo}`}, + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorDuplicate{ + Original: filepath.Join(basePath, "all-services", "a.yml"), + Duplicate: filepath.Join(basePath, "all-services", "b.yml"), + }, err) + require.Nil(t, c) +} + +func TestErrConflictServicePath(t *testing.T) { + basePath := minValidFS(t) + serviceAConf := lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ) + serviceBConf := lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/b`, + `enabled-templates: ../enabled-templates/b`, + ) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": serviceAConf, + "b.yml": serviceBConf, + }, + "enabled-services": map[string]any{ + "a.yml": serviceAConf, + "b.yml": serviceBConf, + }, + "all-templates": map[string]any{ + "a": map[string]any{"a.gqt": `query {foo}`}, + "b": map[string]any{"b.gqt": `query {foo}`}, + }, + "enabled-templates": map[string]any{ + "a": map[string]any{"a.gqt": `query {foo}`}, + "b": map[string]any{"b.gqt": `query {foo}`}, + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Error(t, err) + require.Equal(t, &config.ErrorConflict{ + Feature: "path", + Value: "/", + Subject1: filepath.Join(basePath, "all-services", "b.yml"), + Subject2: filepath.Join(basePath, "all-services", "a.yml"), + }, err) + require.Nil(t, c) +} + +func TestErrMissingPath(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `forward-url: http://localhost:8080/`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: filepath.Join( + basePath, "all-services", "a.yml", + ), + Feature: "path", + }, err) + require.Nil(t, c) +} + +func TestErrMissingForwardURL(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `path: /`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: filepath.Join( + basePath, "all-services", "a.yml", + ), + Feature: "forward-url", + }, err) + require.Nil(t, c) +} + +func TestErrInvalidPath(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `path: invalid_path`, + `forward-url: http://localhost:8080/`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, "all-services", "a.yml"), + Feature: "path", + Message: `path is not starting with /`, + }, err) + require.Nil(t, c) +} + +func TestErrInvalidForwardURLInvalidScheme(t *testing.T) { + basePath := minValidFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `path: /`, + `forward-url: localhost:8080`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, "all-services", "a.yml"), + Feature: "forward-url", + Message: `protocol is not supported or undefined`, + }, err) + require.Nil(t, c) +} + +func TestErrInvalidSchema(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join("all-services", "schema_a.graphqls") + createFiles(t, map[string]any{p: `type Query{ invalid }`}, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, p), + Feature: "schema", + Message: fmt.Sprintf( + `invalid schema: %s:1: Expected :, found }`, + filepath.Join(basePath, p), + ), + }, err) + require.Nil(t, c) +} + +func TestErrMissingSchema(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join(basePath, "all-services", "schema_a.graphqls") + err := os.Remove(p) + require.NoError(t, err) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorMissing{ + FilePath: p, + Feature: "schema", + }, err) + require.Nil(t, c) +} + +func TestErrInvalidTemplate(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join("all-templates", "a", "invalid_template.gqt") + createFiles(t, map[string]any{p: `invalid { template }`}, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, p), + Feature: "template", + Message: `1:1: unexpected token, expected ` + + `query, mutation, or subscription operation definition`, + }, err) + require.Nil(t, c) +} + +func TestErrInvalidTemplateID(t *testing.T) { + basePath, _ := validFS(t) + p := filepath.Join("all-templates", "a", "invalid_template#.gqt") + createFiles(t, map[string]any{p: `invalid { template }`}, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, p), + Feature: "id", + Message: `contains illegal character at index 16`, + }, err) + require.Nil(t, c) +} + +func TestErrInvalidServiceID(t *testing.T) { + basePath, _ := validFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a#.yml": lines( + `path: /`, + `forward-url: http://localhost:8080/`, + `all-templates: ../all-templates/a`, + `enabled-templates: ../enabled-templates/a`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join( + basePath, + "all-services", + "a#.yml", + ), + Feature: "id", + Message: `contains illegal character at index 1`, + }, err) + require.Nil(t, c) +} + +func TestErrMalformedConfig(t *testing.T) { + basePath, _ := validFS(t) + createFiles(t, map[string]any{ + "all-services": map[string]any{ + "a.yml": lines( + `malformed yaml`, + ), + }, + }, nil, basePath) + c, err := config.Read(os.DirFS(basePath), basePath, ServerConfigFileName) + require.Equal(t, &config.ErrorIllegal{ + FilePath: filepath.Join(basePath, "all-services", "a.yml"), + Feature: "syntax", + Message: "yaml: unmarshal errors:\n " + + "line 1: cannot unmarshal !!str `malform...` " + + "into config.serviceConfig", + }, err) + require.Nil(t, c) +} + +func TestErrorString(t *testing.T) { + for _, td := range []struct { + name string + input error + expect string + }{ + { + name: "missing_feature_in", + input: config.ErrorMissing{ + FilePath: "path/to/file.txt", + Feature: "some_feature", + }, + expect: "missing some_feature in path/to/file.txt", + }, + { + name: "missing_file", + input: config.ErrorMissing{ + FilePath: "path/to/file.txt", + }, + expect: "missing path/to/file.txt", + }, + { + name: "illegal_feature_in", + input: config.ErrorIllegal{ + FilePath: "path/to/file.txt", + Feature: "some_feature", + Message: "some message", + }, + expect: "illegal some_feature in path/to/file.txt: some message", + }, + { + name: "duplicate", + input: config.ErrorDuplicate{ + Original: "path/to/file_a.txt", + Duplicate: "path/to/file_b.txt", + }, + expect: "path/to/file_b.txt is a duplicate of path/to/file_a.txt", + }, + } { + t.Run(td.name, func(t *testing.T) { + require.Equal(t, td.expect, td.input.Error()) + }) + } +} + +func minValidFS(t *testing.T) (base string) { + base = t.TempDir() + dirs := map[string]any{ + "all-services": nil, + "enabled-services": nil, + "all-templates": map[string]any{ + "a": nil, + "b": nil, + }, + "enabled-templates": map[string]any{ + "a": nil, + "b": nil, + }, + "irrelevant-dir": nil, + } + files := map[string]any{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:443`, + `all-services: all-services`, + `enabled-services: enabled-services`, + ), + "irrelevant-file.txt": lines( + `this file is irrelevant and exists only for the purposes`, + `of testing function Read.`, + ), + "irrelevant-dir": map[string]any{ + "irrelevant_file.txt": lines( + `this file is irrelevant and exists only for the purposes`, + `of testing function Read.`, + ), + }, + } + + hashes := make(map[string]string) + + createDirs(t, dirs, base) + createFiles(t, files, hashes, base) + + return base +} + +// validFS calls fn providing a valid setup filesystem. +func validFS(t *testing.T) (base string, conf *config.Config) { + base = t.TempDir() + type M = map[string]any + + dirs := M{ + "all-services": nil, + "enabled-services": nil, + "all-templates": M{ + "a": nil, + "b": nil, + }, + "enabled-templates": M{ + "a": nil, + "b": nil, + }, + "irrelevant-dir": nil, + } + files := M{ + ServerConfigFileName: lines( + `proxy:`, + ` host: localhost:443`, + ` tls:`, + ` cert-file: proxy.cert`, + ` key-file: proxy.key`, + fmt.Sprintf( + ` max-request-body-size: %d`, + config.MinReqBodySize+256, + ), + `api:`, + ` host: localhost:3000`, + ` tls:`, + ` cert-file: api.cert`, + ` key-file: api.key`, + `all-services: all-services`, + `enabled-services: enabled-services`, + ), + "all-services": M{ + "a.yml": lines( + `path: "/path"`, + `forward-url: "http://localhost:8080/path"`, + `forward-reduced: true`, + `schema: "schema_a.graphqls"`, + `all-templates: "../all-templates/a"`, + `enabled-templates: "../enabled-templates/a"`, + ), + "schema_a.graphqls": lines(`type Query { foo:Int bar:String! }`), + "b.yml": lines( + `path: /`, + `forward-url: "http://localhost:9090/"`, + // Schemaless. + `all-templates: "../all-templates/b"`, + `enabled-templates: "../enabled-templates/b"`, + ), + "ignored_file.txt": `this file should be ignored`, + }, + "all-templates": M{ + "a": M{ + "a.gqt": lines( + "---", + `name: "Template A"`, + "tags:", + " - tag_a", + "---", + `query { foo }`, + ), + "b.gqt": lines( + "---", + "tags:", + " - tag_b1", + " - tag_b2", + "---", + `query { bar }`, + ), + }, + "b": M{ + "c.gqt": `query { maz }`, + "ignored_file.txt": `this file should be ignored`, + }, + }, + "irrelevant-file.txt": lines( + `this file is irrelevant and exists only for the purposes`, + `of testing function Read.`, + ), + "irrelevant-dir": M{ + "irrelevant_file.txt": lines( + `this file is irrelevant and exists only for the purposes`, + `of testing function Read.`, + ), + }, + } + links := map[string]string{ + "all-services/a.yml": "enabled-services/a.yml", + "all-services/b.yml": "enabled-services/b.yml", + "all-templates/a/a.gqt": "enabled-templates/a/a.gqt", + "all-templates/a/b.gqt": "enabled-templates/a/b.gqt", + "all-templates/b/c.gqt": "enabled-templates/b/c.gqt", + "all-services/ignored-file.txt": "enabled-services/ignored-file.txt", + "all-templates/b/ignored-file.txt": "enabled-templates/b/ignored-file.txt", + } + + hashes := make(map[string]string) + + createDirs(t, dirs, base) + createFiles(t, files, hashes, base) + createSymlinks(t, links, base) + + serviceASchema, err := gqlparser.LoadSchema(&ast.Source{ + Name: filepath.Join(base, "all-services", "schema_a.graphqls"), + Input: files["all-services"].(M)["schema_a.graphqls"].(string), + }) + require.NoError(t, err) + + serviceASchemaParser, err := gqt.NewParser([]gqt.Source{{ + Name: filepath.Join(base, "all-services", "schema_a.graphqls"), + Content: files["all-services"].(M)["schema_a.graphqls"].(string), + }}) + require.NoError(t, err) + + services := make(map[string]*config.Service) + templatesA := map[string]*config.Template{} + templatesB := map[string]*config.Template{} + + { + p := filepath.Join(base, "all-templates", "a", "a.gqt") + templatesA[hashes[p]] = &config.Template{ + ID: "a", + Name: "Template A", + Tags: []string{"tag_a"}, + Source: []byte(lines(`query { foo }`)), + GQTTemplate: func() *gqt.Operation { + _, body, err := metadata.Parse( + []byte(files["all-templates"].(M)["a"].(M)["a.gqt"].(string)), + ) + require.NoError(t, err) + template, _, errs := serviceASchemaParser.Parse(body) + require.Nil(t, errs) + return template + }(), + Enabled: true, + FilePath: p, + } + } + + { + p := filepath.Join(base, "all-templates", "a", "b.gqt") + templatesA[hashes[p]] = &config.Template{ + ID: "b", + Tags: []string{"tag_b1", "tag_b2"}, + Source: []byte(lines(`query { bar }`)), + GQTTemplate: func() *gqt.Operation { + _, body, err := metadata.Parse( + []byte(files["all-templates"].(M)["a"].(M)["b.gqt"].(string)), + ) + require.NoError(t, err) + templates, _, errs := serviceASchemaParser.Parse(body) + require.Nil(t, errs) + return templates + }(), + Enabled: true, + FilePath: p, + } + } + + { + p := filepath.Join(base, "all-services", "a.yml") + services[hashes[p]] = &config.Service{ + ID: "a", + Path: "/path", + ForwardURL: "http://localhost:8080/path", + ForwardReduced: true, + Schema: serviceASchema, + Templates: templatesA, + TemplatesEnabled: []*config.Template{ + templatesA[hashes[filepath.Join(base, "all-templates", "a", "a.gqt")]], + templatesA[hashes[filepath.Join(base, "all-templates", "a", "b.gqt")]], + }, + Enabled: true, + FilePath: p, + } + } + + { + p := filepath.Join(base, "all-templates", "b", "c.gqt") + templatesB[hashes[p]] = &config.Template{ + ID: "c", + Source: []byte(`query { maz }`), + GQTTemplate: func() *gqt.Operation { + _, body, err := metadata.Parse( + []byte(files["all-templates"].(M)["b"].(M)["c.gqt"].(string)), + ) + require.NoError(t, err) + template, _, errs := gqt.Parse(body) + require.Nil(t, errs) + return template + }(), + Enabled: true, + FilePath: p, + } + } + + { + p := filepath.Join(base, "all-services", "b.yml") + services[hashes[p]] = &config.Service{ + ID: "b", + Path: "/", + ForwardURL: "http://localhost:9090/", + ForwardReduced: false, + Templates: templatesB, + TemplatesEnabled: []*config.Template{ + templatesB[hashes[filepath.Join(base, "all-templates", "b", "c.gqt")]], + }, + Enabled: true, + FilePath: p, + } + } + + return base, &config.Config{ + Proxy: config.ProxyServerConfig{ + Host: "localhost:443", + TLS: config.TLS{ + CertFile: "proxy.cert", + KeyFile: "proxy.key", + }, + MaxReqBodySizeBytes: config.MinReqBodySize + 256, + }, + API: &config.APIServerConfig{ + Host: "localhost:3000", + TLS: config.TLS{ + CertFile: "api.cert", + KeyFile: "api.key", + }, + }, + Services: services, + ServicesEnabled: []*config.Service{ + services[hashes[filepath.Join(base, "all-services", "a.yml")]], + services[hashes[filepath.Join(base, "all-services", "b.yml")]], + }, + } +} + +func createDirs(t *testing.T, dirs map[string]any, basePath string) { + for k, v := range dirs { + p := filepath.Join(basePath, k) + err := os.Mkdir(p, 0o775) + require.NoError(t, err) + if v != nil { + switch vt := v.(type) { + case map[string]any: + createDirs(t, vt, p) + default: + panic(fmt.Errorf("unsupported dir content type: %v", v)) + } + } + } +} + +func createFiles( + t *testing.T, + files map[string]any, + hashes map[string]string, + basePath string, +) { + for k, v := range files { + p := filepath.Join(basePath, k) + switch vt := v.(type) { + case string: + f, err := os.Create(p) + require.NoError(t, err) + _, err = f.Write([]byte(vt)) + require.NoError(t, err) + if hashes != nil { + hashes[p] = calculateHash(t, f) + } + case map[string]any: + createFiles(t, vt, hashes, p) + default: + panic(fmt.Errorf("unsupported file content type: %#v", v)) + } + } +} + +func createSymlinks(t *testing.T, links map[string]string, path string) { + for k, v := range links { + err := os.Symlink(filepath.Join(path, k), filepath.Join(path, v)) + require.NoError(t, err) + } +} + +func lines(lines ...string) string { + var b strings.Builder + for i := range lines { + b.WriteString(lines[i]) + b.WriteByte('\n') + } + return b.String() +} + +func calculateHash(t *testing.T, file *os.File) string { + _, err := file.Seek(0, io.SeekStart) + require.NoError(t, err) + h := md5.New() + _, err = io.Copy(h, file) + require.NoError(t, err) + sum := h.Sum(nil) + s := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(sum) + return s +} diff --git a/config/metadata/metadata.go b/pkg/config/metadata/metadata.go similarity index 100% rename from config/metadata/metadata.go rename to pkg/config/metadata/metadata.go diff --git a/config/metadata/metadata_test.go b/pkg/config/metadata/metadata_test.go similarity index 98% rename from config/metadata/metadata_test.go rename to pkg/config/metadata/metadata_test.go index bc93a03..3c830f5 100644 --- a/config/metadata/metadata_test.go +++ b/pkg/config/metadata/metadata_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/graph-guard/ggproxy/config/metadata" + "github.com/graph-guard/ggproxy/pkg/config/metadata" "github.com/stretchr/testify/require" ) diff --git a/utilities/container/amap/amap.go b/pkg/container/amap/amap.go similarity index 98% rename from utilities/container/amap/amap.go rename to pkg/container/amap/amap.go index 795d201..2e99ec7 100644 --- a/utilities/container/amap/amap.go +++ b/pkg/container/amap/amap.go @@ -1,7 +1,7 @@ package amap import ( - "github.com/graph-guard/ggproxy/utilities/math" + "github.com/graph-guard/ggproxy/pkg/math" ) type KeyInterface interface { diff --git a/utilities/container/amap/amap_test.go b/pkg/container/amap/amap_test.go similarity index 98% rename from utilities/container/amap/amap_test.go rename to pkg/container/amap/amap_test.go index 4c8b534..587465b 100644 --- a/utilities/container/amap/amap_test.go +++ b/pkg/container/amap/amap_test.go @@ -3,7 +3,7 @@ package amap_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/container/amap" + "github.com/graph-guard/ggproxy/pkg/container/amap" "github.com/stretchr/testify/require" ) diff --git a/utilities/container/benchmark_test.go b/pkg/container/benchmark_test.go similarity index 96% rename from utilities/container/benchmark_test.go rename to pkg/container/benchmark_test.go index 06c7828..342f17e 100644 --- a/utilities/container/benchmark_test.go +++ b/pkg/container/benchmark_test.go @@ -5,7 +5,7 @@ import ( "math/rand" "testing" - "github.com/graph-guard/ggproxy/utilities/container" + "github.com/graph-guard/ggproxy/pkg/container" ) func forEachImplB( @@ -19,8 +19,10 @@ func forEachImplB( } } -var GI int -var GB bool +var ( + GI int + GB bool +) func BenchmarkAdd(b *testing.B) { for _, td := range []int{8, 64, 192, 512, 1024} { diff --git a/utilities/container/container.go b/pkg/container/container.go similarity index 100% rename from utilities/container/container.go rename to pkg/container/container.go diff --git a/utilities/container/container_test.go b/pkg/container/container_test.go similarity index 93% rename from utilities/container/container_test.go rename to pkg/container/container_test.go index 0021aa8..7fbf5b2 100644 --- a/utilities/container/container_test.go +++ b/pkg/container/container_test.go @@ -4,10 +4,10 @@ import ( "strconv" "testing" - "github.com/graph-guard/ggproxy/utilities/container" - "github.com/graph-guard/ggproxy/utilities/container/gomap" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/container/linear" + "github.com/graph-guard/ggproxy/pkg/container" + "github.com/graph-guard/ggproxy/pkg/container/gomap" + "github.com/graph-guard/ggproxy/pkg/container/hamap" + "github.com/graph-guard/ggproxy/pkg/container/linear" "github.com/stretchr/testify/require" ) diff --git a/utilities/container/gomap/gomap.go b/pkg/container/gomap/gomap.go similarity index 100% rename from utilities/container/gomap/gomap.go rename to pkg/container/gomap/gomap.go diff --git a/utilities/container/hamap/hamap.go b/pkg/container/hamap/hamap.go similarity index 98% rename from utilities/container/hamap/hamap.go rename to pkg/container/hamap/hamap.go index 6671b50..6229fcb 100644 --- a/utilities/container/hamap/hamap.go +++ b/pkg/container/hamap/hamap.go @@ -10,7 +10,7 @@ package hamap import ( "github.com/google/go-cmp/cmp" - "github.com/graph-guard/ggproxy/utilities/math" + "github.com/graph-guard/ggproxy/pkg/math" "github.com/zeebo/xxh3" ) @@ -53,8 +53,10 @@ func (h *HasherXXH3[K]) Hash(k K) uint64 { return xxh3.HashSeed([]byte(k), h.Seed) } -var defaultHasherS = &HasherXXH3[string]{} -var defaultHasherB = &HasherXXH3[[]byte]{} +var ( + defaultHasherS = &HasherXXH3[string]{} + defaultHasherB = &HasherXXH3[[]byte]{} +) // New creates a new map instance. func New[K KeyInterface, V any]( diff --git a/utilities/container/hamap/hamap_test.go b/pkg/container/hamap/hamap_test.go similarity index 99% rename from utilities/container/hamap/hamap_test.go rename to pkg/container/hamap/hamap_test.go index d539865..e174403 100644 --- a/utilities/container/hamap/hamap_test.go +++ b/pkg/container/hamap/hamap_test.go @@ -5,7 +5,7 @@ import ( "strconv" "testing" - "github.com/graph-guard/ggproxy/utilities/container/hamap" + "github.com/graph-guard/ggproxy/pkg/container/hamap" "github.com/stretchr/testify/require" ) diff --git a/utilities/container/linear/linear.go b/pkg/container/linear/linear.go similarity index 100% rename from utilities/container/linear/linear.go rename to pkg/container/linear/linear.go diff --git a/utilities/decl/decl.go b/pkg/decl/decl.go similarity index 100% rename from utilities/decl/decl.go rename to pkg/decl/decl.go diff --git a/pkg/engine/playmon/bench_test.go b/pkg/engine/playmon/bench_test.go new file mode 100644 index 0000000..654929a --- /dev/null +++ b/pkg/engine/playmon/bench_test.go @@ -0,0 +1,35 @@ +package playmon_test + +// import ( +// "testing" + +// "github.com/graph-guard/ggproxy/pkg/config" +// "github.com/graph-guard/ggproxy/pkg/engine/playmon" +// "github.com/graph-guard/ggproxy/pkg/gqlparse" +// "github.com/graph-guard/ggproxy/pkg/testsetup" +// ) + +// // var GS string + +// // func BenchmarkMatchStarwars(b *testing.B) { +// // s := testsetup.Starwars() +// // service := s.Config.ServicesEnabled[0] +// // e := playmon.New(service) +// // b.ResetTimer() +// // for n := 0; n < b.N; n++ { +// // e.Match( +// // []byte(s.Tests[1].Client.Input.BodyJSON["query"].(string)), +// // nil, nil, +// // func(operation, selectionSet []gqlparse.Token) (stop bool) { +// // return false +// // }, +// // func(t *config.Template) (stop bool) { +// // GS = t.ID +// // return false +// // }, +// // func(err error) { +// // b.Fatal("unexpected error:", err) +// // }, +// // ) +// // } +// // } diff --git a/pkg/engine/playmon/internal/constrcheck/bench_test.go b/pkg/engine/playmon/internal/constrcheck/bench_test.go new file mode 100644 index 0000000..d444e01 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/bench_test.go @@ -0,0 +1,188 @@ +package constrcheck_test + +// import ( +// "strconv" +// "strings" +// "testing" + +// "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck" +// "github.com/graph-guard/ggproxy/pkg/gqlparse" +// "github.com/graph-guard/gqlscan" +// "github.com/graph-guard/gqt/v4" +// "github.com/vektah/gqlparser/v2" +// "github.com/vektah/gqlparser/v2/ast" +// ) + +// var GB bool + +// func Token(t gqlscan.Token, value string) gqlparse.Token { +// v := []byte(nil) +// if value != "" { +// v = []byte(value) +// } +// return gqlparse.Token{ID: t, Value: v} +// } + +// func BenchmarkSimple(b *testing.B) { +// fn := makeBenchmarkFn( +// b, +// `type Query { f(a:Int!):Int! }`, +// `query { f(a:42) }`, +// "Query.f|a", +// Token(gqlscan.TokenInt, "42"), +// ) + +// b.ResetTimer() +// for n := 0; n < b.N; n++ { +// if GB = fn(); !GB { +// b.Fatal("unexpected result: ", GB) +// } +// } +// } + +// func BenchmarkSimpleArray(b *testing.B) { +// fn := makeBenchmarkFn( +// b, +// `type Query { f(a:[Int!]!):Int! }`, +// `query { f(a:[1,2,3,4,5]) }`, +// "Query.f|a", +// Token(gqlscan.TokenArr, ""), +// Token(gqlscan.TokenInt, "1"), +// Token(gqlscan.TokenInt, "2"), +// Token(gqlscan.TokenInt, "3"), +// Token(gqlscan.TokenInt, "4"), +// Token(gqlscan.TokenInt, "5"), +// Token(gqlscan.TokenArrEnd, ""), +// ) + +// b.ResetTimer() +// for n := 0; n < b.N; n++ { +// if GB = fn(); !GB { +// b.Fatal("unexpected result: ", GB) +// } +// } +// } + +// func BenchmarkBigArray(b *testing.B) { +// items := 1024 + +// tokens := make([]gqlparse.Token, items+2) +// tokens[0] = Token(gqlscan.TokenArr, "") +// tokens[len(tokens)-1] = Token(gqlscan.TokenArrEnd, "") +// for i := 1; i <= items; i++ { +// tokens[i] = Token(gqlscan.TokenInt, strconv.Itoa(i)) +// } + +// var tmpl strings.Builder +// tmpl.WriteString(`query { f(a:[`) +// for i := 1; i <= items; i++ { +// tmpl.WriteString(strconv.Itoa(i)) +// if i != items { +// tmpl.WriteByte(',') +// } +// } +// tmpl.WriteString(`]) }`) + +// fn := makeBenchmarkFn( +// b, +// `type Query { f(a:[Int!]!):Int! }`, +// tmpl.String(), +// "Query.f|a", +// tokens..., +// ) + +// b.ResetTimer() +// for n := 0; n < b.N; n++ { +// if GB = fn(); !GB { +// b.Fatal("unexpected result: ", GB) +// } +// } +// } + +// func BenchmarkObject(b *testing.B) { +// fn := makeBenchmarkFn( +// b, +// ` +// type Query { f(object:Object!):Int } +// input Object { subobject: SubObject! } +// input SubObject { array: [ArrayObject!]! } +// input ArrayObject { name: String!, index: Int! } +// `, +// `query { f(object:{ +// subobject: { +// array: [{ +// name: "first", index: 0 +// }, { +// name: "second", index: 1 +// }] +// } +// })}`, +// "Query.f|object", +// Token(gqlscan.TokenObj, ""), +// Token(gqlscan.TokenObjField, "subobject"), +// Token(gqlscan.TokenObj, ""), +// Token(gqlscan.TokenObjField, "array"), +// Token(gqlscan.TokenArr, ""), + +// Token(gqlscan.TokenObj, ""), +// Token(gqlscan.TokenObjField, "name"), +// Token(gqlscan.TokenStr, "first"), +// Token(gqlscan.TokenObjField, "index"), +// Token(gqlscan.TokenInt, "0"), +// Token(gqlscan.TokenObjEnd, ""), + +// Token(gqlscan.TokenObj, ""), +// Token(gqlscan.TokenObjField, "name"), +// Token(gqlscan.TokenStr, "second"), +// Token(gqlscan.TokenObjField, "index"), +// Token(gqlscan.TokenInt, "1"), +// Token(gqlscan.TokenObjEnd, ""), + +// Token(gqlscan.TokenArrEnd, ""), +// Token(gqlscan.TokenObjEnd, ""), +// Token(gqlscan.TokenObjEnd, ""), +// ) + +// b.ResetTimer() +// for n := 0; n < b.N; n++ { +// if GB = fn(); !GB { +// b.Fatal("unexpected result: ", GB) +// } +// } +// } + +// func makeBenchmarkFn( +// b interface{ Fatal(...any) }, +// schema, template, path string, tokens ...gqlparse.Token, +// ) func() bool { +// s, err := gqlparser.LoadSchema(&ast.Source{ +// Name: "schema.graphqls", +// Input: schema, +// }) +// if err != nil { +// b.Fatal(err) +// } + +// p, err := gqt.NewParser([]gqt.Source{{ +// Name: "schema.graphqls", +// Content: schema, +// }}) +// if err != nil { +// b.Fatal(err) +// } + +// opr, _, errs := p.Parse([]byte(template)) +// if errs != nil { +// b.Fatal(errs) +// } + +// m := constrcheck.New(opr, s) + +// inputs := map[string][]gqlparse.Token{path: tokens} + +// m.Init(t) + +// return func() bool { +// return m.Check(path) +// } +// } diff --git a/pkg/engine/playmon/internal/constrcheck/constrcheck.go b/pkg/engine/playmon/internal/constrcheck/constrcheck.go new file mode 100644 index 0000000..4e1b329 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/constrcheck.go @@ -0,0 +1,1579 @@ +package constrcheck + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/graph-guard/ggproxy/pkg/atoi" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck/internal/union" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/countval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/scanval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/graph-guard/gqt/v4" + gqlast "github.com/vektah/gqlparser/v2/ast" +) + +// Enum is a GraphQL enum value. +type Enum string + +// Checker is a constraint checker instance. +// Before calling Check, make sure you initialize the Checker using Init. +type Checker struct { + operation *gqt.Operation + schema *gqlast.Schema + checkers map[uint64]check + pathByVarDecl map[*gqt.VariableDeclaration]uint64 + + // varValues is set in every call to Check. + varValues map[uint64][]gqlparse.Token + + // inputValue is set in every call to Check. + inputValue []gqlparse.Token + + reader *tokenreader.Reader + + // stack is reset in every call to Check. + stack []union.Union +} + +func (c *Checker) stackTopType() union.Type { + return c.stack[len(c.stack)-1].Type() +} + +func (c *Checker) popStack() union.Union { + t := c.stack[len(c.stack)-1] + c.stack = c.stack[:len(c.stack)-1] + return t +} + +func (c *Checker) popStackArrays() (left, right []union.Union) { + if c.stack[len(c.stack)-1].Type() != union.TypeArray { + return + } + i := len(c.stack) - 2 + var count int +LOOP_R: + for l := 1; i > 0; i, count = i-1, count+1 { + switch c.stack[i].Type() { + case union.TypeArray: + l++ + case union.TypeArrayEnd: + l-- + if l < 1 { + break LOOP_R + } + } + } + right = c.stack[i+1 : len(c.stack)-1] + + i-- + iv := i + if c.stack[i].Type() != union.TypeArray { + return + } + count = 0 + i-- +LOOP_L: + for l := 1; i > 0; i, count = i-1, count+1 { + switch c.stack[i].Type() { + case union.TypeArray: + l++ + case union.TypeArrayEnd: + l-- + if l < 1 { + break LOOP_L + } + } + } + + left = c.stack[i+1 : iv] + c.stack = c.stack[:i] + return left, right +} + +func (c *Checker) pushStackArray() { + c.stack = append(c.stack, union.Array()) +} + +func (c *Checker) pushStackArrayEnd() { + c.stack = append(c.stack, union.ArrayEnd()) +} + +func (c *Checker) pushStackInt(v int32) { + c.stack = append(c.stack, union.Int(v)) +} + +func (c *Checker) pushStackFloat(v float64) { + c.stack = append(c.stack, union.Float(v)) +} + +func (c *Checker) pushStackBool(v bool) { + if v { + c.stack = append(c.stack, union.True()) + return + } + c.stack = append(c.stack, union.False()) +} + +// Check returns true if the value for the given path is accepted, +// otherwise returns false. +func (c *Checker) Check( + gqlVarVals [][]gqlparse.Token, + varVals map[uint64][]gqlparse.Token, + path uint64, + value []gqlparse.Token, +) bool { + c.stack = c.stack[:0] + cf := c.checkers[path] + if cf == nil { + return false + } + c.reader.Vars = gqlVarVals + c.varValues = varVals + c.inputValue = value + c.reader.Main = value + // fmt.Println("CHECKED VALUE: ", len(c.checkedValue)) + // for i, v := range c.checkedValue { + // fmt.Printf(" %d: %s\n", i, v) + // } + return cf(c) +} + +// check is a constraint check function which returns true +// if the input value matches the constraint and can be accepted, +// otherwise returns false. +type check func(*Checker) (match bool) + +// resolveExpr resolves expression e into a union and pushes it onto the stack. +func (c *Checker) resolveExpr(e gqt.Expression) union.Type { + switch e := e.(type) { + case *gqt.Array: + c.pushStackArrayEnd() + for i := len(e.Items) - 1; i >= 0; i-- { + c.resolveExpr(e.Items[i].(*gqt.ConstrEquals).Value) + } + c.pushStackArray() + return union.TypeArray + case *gqt.Variable: + p, ok := c.pathByVarDecl[e.Declaration] + if !ok { + if s := c.varValues[p]; s != nil { + c.stack = append(c.stack, union.Tokens(s)) + return union.TypeTokens + } + } + c.stack = append(c.stack, union.Null()) + return union.TypeNull + case *gqt.Number: + if i, ok := e.Int(); ok { + c.stack = append(c.stack, union.Int(int32(i))) + return union.TypeInt + } + f, _ := e.Float() + c.stack = append(c.stack, union.Float(f)) + return union.TypeFloat + case *gqt.True: + c.stack = append(c.stack, union.True()) + return union.TypeBoolean + case *gqt.False: + c.stack = append(c.stack, union.False()) + return union.TypeBoolean + case *gqt.Enum: + c.stack = append(c.stack, union.Enum(e.Value)) + return union.TypeEnum + case *gqt.String: + c.stack = append(c.stack, union.String(e.Value)) + return union.TypeString + case *gqt.Null: + return union.TypeNull + case *gqt.ExprAddition: + c.resolveExpr(e.AddendLeft) + c.resolveExpr(e.AddendRight) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackInt(l + r) + return union.TypeInt + } + { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackFloat(l + r) + } + return union.TypeFloat + case *gqt.ExprSubtraction: + c.resolveExpr(e.Minuend) + c.resolveExpr(e.Subtrahend) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackInt(l - r) + return union.TypeInt + } + { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackFloat(l - r) + } + return union.TypeFloat + case *gqt.ExprMultiplication: + c.resolveExpr(e.Multiplicant) + c.resolveExpr(e.Multiplicator) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackInt(l * r) + return union.TypeInt + } + { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackFloat(l * r) + } + return union.TypeFloat + case *gqt.ExprDivision: + c.resolveExpr(e.Dividend) + c.resolveExpr(e.Divisor) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackInt(l / r) + return union.TypeInt + } + { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackFloat(l / r) + } + return union.TypeFloat + case *gqt.ExprModulo: + c.resolveExpr(e.Dividend) + c.resolveExpr(e.Divisor) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackInt(l % r) + return union.TypeInt + } + { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackFloat(math.Mod(l, r)) + } + return union.TypeFloat + case *gqt.ExprNumericNegation: + c.resolveExpr(e.Expression) + i := c.popStack() + switch i.Type() { + case union.TypeFloat: + i, _ := i.Float() + c.pushStackFloat(-i) + return union.TypeFloat + case union.TypeInt: + i, _ := i.Int() + c.pushStackInt(-i) + return union.TypeInt + case union.TypeTokens: + switch i.Tokens()[0].ID { + case gqlscan.TokenInt: + i := atoi.MustI32(i.Tokens()[0].Value) + c.pushStackInt(-i) + return union.TypeInt + case gqlscan.TokenFloat: + f := atoi.MustF64(i.Tokens()[0].Value) + c.pushStackFloat(-f) + return union.TypeFloat + } + panic(fmt.Errorf("unexpected token type: %q", i.Tokens()[0].ID.String())) + } + panic(fmt.Errorf("unexpected union type: %q", i.Type().String())) + case *gqt.ExprEqual: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + equal := false + if c.stackTopType() == union.TypeArray { + left, right := c.popStackArrays() + equal = unionsEqual(left, right) + } else { + r := c.popStack() + l := c.popStack() + equal = union.Equal(l, r) + } + c.pushStackBool(equal) + return union.TypeBoolean + case *gqt.ExprNotEqual: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + notEqual := false + if c.stackTopType() == union.TypeArray { + left, right := c.popStackArrays() + notEqual = !unionsEqual(left, right) + } else { + r := c.popStack() + l := c.popStack() + notEqual = !union.Equal(l, r) + } + c.pushStackBool(notEqual) + return union.TypeBoolean + case *gqt.ExprLogicalNegation: + c.resolveExpr(e.Expression) + u := c.popStack() + b, _ := u.Bool() + c.pushStackBool(!b) + return union.TypeBoolean + case *gqt.ExprGreater: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackBool(l > r) + } else { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackBool(l > r) + } + return union.TypeBoolean + case *gqt.ExprLess: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackBool(l < r) + } else { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackBool(l < r) + } + return union.TypeBoolean + case *gqt.ExprGreaterOrEqual: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackBool(l >= r) + } else { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackBool(l >= r) + } + return union.TypeBoolean + case *gqt.ExprLessOrEqual: + c.resolveExpr(e.Left) + c.resolveExpr(e.Right) + r := c.popStack() + l := c.popStack() + if l.Type() == union.TypeInt && r.Type() == union.TypeInt { + l, _ := l.Int() + r, _ := r.Int() + c.pushStackBool(l <= r) + } else { + l, _ := l.Float() + r, _ := r.Float() + c.pushStackBool(l <= r) + } + return union.TypeBoolean + case *gqt.ExprLogicalOr: + for _, x := range e.Expressions { + c.resolveExpr(x) + u := c.popStack() + b, _ := u.Bool() + if b { + c.pushStackBool(true) + return union.TypeBoolean + } + } + c.pushStackBool(false) + return union.TypeBoolean + case *gqt.ExprLogicalAnd: + for _, x := range e.Expressions { + c.resolveExpr(x) + u := c.popStack() + b, _ := u.Bool() + if !b { + c.pushStackBool(false) + return union.TypeBoolean + } + } + c.pushStackBool(true) + return union.TypeBoolean + case *gqt.ExprParentheses: + return c.resolveExpr(e.Expression) + } + panic(fmt.Errorf("unhandled value expression type: %T", e)) +} + +// New creates a constraint checker instance for each path of o. +func New(o *gqt.Operation, s *gqlast.Schema) *Checker { + c := &Checker{ + operation: o, + schema: s, + checkers: make(map[uint64]check), + stack: make([]union.Union, 1024), + pathByVarDecl: make(map[*gqt.VariableDeclaration]uint64), + reader: &tokenreader.Reader{}, + } + if errs := pathscan.InAST( + o, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On structural + return false + }, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On argument + a := e.(*gqt.Argument) + var expect *gqlast.Type + if a.Def != nil { + expect = a.Def.Type + } + if fn := makeCheck(a.Constraint, expect, s); fn != nil { + c.checkers[pathHash] = fn + } + return false + }, + func( + path string, + pathHash uint64, + e *gqt.VariableDeclaration, + ) (stop bool) { + // On variable + c.pathByVarDecl[e] = pathHash + return false + }, + ); errs != nil { + panic(errs) + } + return c +} + +func makeCheck( + e gqt.Expression, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + switch e := e.(type) { + case *gqt.ConstrAny: + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, schema) { + return false + } + *c.reader = rBefore + + // Make sure the value is semantically valid. + return !scanval.InArrays( + c.reader, + func(r *tokenreader.Reader) (stop bool) { + if r.ReadOne().ID != gqlscan.TokenObj { + return false + } + checkedFields := map[string]struct{}{} + for r.PeekOne().ID != gqlscan.TokenObjEnd { + fieldName := r.ReadOne().Value + if _, ok := checkedFields[string(fieldName)]; ok { + // Duplicate field! Invalid object value. + return true + } + checkedFields[string(fieldName)] = struct{}{} + } + return false + }, + ) + } + case *gqt.ExprParentheses: + return makeCheck(e.Expression, expect, schema) + case *gqt.ExprLogicalOr: + exprCheckers := make([]check, len(e.Expressions)) + for i, e := range e.Expressions { + exprCheckers[i] = makeCheck(e, expect, schema) + } + return func(c *Checker) (match bool) { + rBefore := *c.reader + for _, e := range exprCheckers { + if e(c) { + return true + } + // Reset value to start checking from the same index + *c.reader = rBefore + } + return false + } + case *gqt.ExprLogicalAnd: + exprCheckers := make([]check, len(e.Expressions)) + for i, e := range e.Expressions { + exprCheckers[i] = makeCheck(e, expect, schema) + } + return func(c *Checker) (match bool) { + rBefore := *c.reader + for _, e := range exprCheckers { + if !e(c) { + return false + } + // Reset value to start checking from the same index + *c.reader = rBefore + } + return true + } + case *gqt.ConstrGreater: + return func(c *Checker) (match bool) { + if c.expectOrNum(expect) { + return false + } + c.resolveExpr(e.Value) + u := c.popStack() + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenInt: + i := atoi.MustI32(read.Value) + if u, ok := u.Int(); ok != union.ValueNone { + match = i > u + break + } + u, _ := u.Float() + match = float64(i) > u + case gqlscan.TokenFloat: + i := atoi.MustF64(read.Value) + u, _ := u.Float() + match = i > u + } + return match + } + case *gqt.ConstrGreaterOrEqual: + return func(c *Checker) (match bool) { + if c.expectOrNum(expect) { + return false + } + c.resolveExpr(e.Value) + u := c.popStack() + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenInt: + i := atoi.MustI32(read.Value) + if u, ok := u.Int(); ok != union.ValueNone { + match = i >= u + break + } + u, _ := u.Float() + match = float64(i) >= u + case gqlscan.TokenFloat: + i := atoi.MustF64(read.Value) + u, _ := u.Float() + match = i >= u + } + return match + } + case *gqt.ConstrLess: + return func(c *Checker) (match bool) { + if c.expectOrNum(expect) { + return false + } + c.resolveExpr(e.Value) + u := c.popStack() + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenInt: + i := atoi.MustI32(read.Value) + if u, ok := u.Int(); ok != union.ValueNone { + match = i < u + break + } + u, _ := u.Float() + match = float64(i) < u + case gqlscan.TokenFloat: + i := atoi.MustF64(read.Value) + u, _ := u.Float() + match = i < u + } + return match + } + case *gqt.ConstrLessOrEqual: + return func(c *Checker) (match bool) { + if c.expectOrNum(expect) { + return false + } + c.resolveExpr(e.Value) + u := c.popStack() + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenInt: + i := atoi.MustI32(read.Value) + if u, ok := u.Int(); ok != union.ValueNone { + match = i <= u + break + } + u, _ := u.Float() + match = float64(i) <= u + case gqlscan.TokenFloat: + i := atoi.MustF64(read.Value) + u, _ := u.Float() + match = i <= u + } + return match + } + case *gqt.ConstrEquals: + switch v := e.Value.(type) { + case *gqt.Array: + return makeEqArray(v, expect, schema) + case *gqt.Object: + return makeEqObject(v, expect, schema) + case *gqt.Number: + return makeEqNumber(v, expect, schema) + case *gqt.String: + return makeEqString(v, expect, schema) + case *gqt.Enum: + return makeEqEnum(v, expect, schema) + case *gqt.Null: + return makeEqNull(expect, schema) + case *gqt.False: + return makeEqBool(false, expect, schema) + case *gqt.True: + return makeEqBool(true, expect, schema) + } + // Expression + return makeEqExpression(e.Value, expect, schema) + case *gqt.ConstrNotEquals: + switch v := e.Value.(type) { + case *gqt.Array: + return makeNotEqArray(v, expect, schema) + case *gqt.Object: + return makeNotEqObject(v, expect, schema) + case *gqt.Number: + return makeNotEqNumber(v, expect, schema) + case *gqt.String: + return makeNotEqString(v, expect, schema) + case *gqt.Enum: + return makeNotEqEnum(v, expect, schema) + case *gqt.Null: + return makeNotEqNull(expect, schema) + case *gqt.False: + return makeNotEqBool(false, expect, schema) + case *gqt.True: + return makeNotEqBool(true, expect, schema) + } + fn := makeEqExpression(e.Value, expect, schema) + return func(c *Checker) (match bool) { + return !fn(c) + } + case *gqt.ConstrLenEquals: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until(c.reader, gqlscan.TokenArrEnd) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) == u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) == u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrLenNotEquals: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until( + c.reader, gqlscan.TokenArrEnd, + ) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) != u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) != u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrLenGreater: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until( + c.reader, gqlscan.TokenArrEnd, + ) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) > u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) > u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrLenGreaterOrEqual: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until( + c.reader, gqlscan.TokenArrEnd, + ) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) >= u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) >= u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrLenLess: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until( + c.reader, gqlscan.TokenArrEnd, + ) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) < u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) < u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrLenLessOrEqual: + return func(c *Checker) (match bool) { + if c.expectOrHasLen(expect) { + return false + } + + var length int + switch read := c.reader.ReadOne(); read.ID { + case gqlscan.TokenArr: + length, _ = countval.Until( + c.reader, gqlscan.TokenArrEnd, + ) + case gqlscan.TokenStr: + length = len(read.Value) + case gqlscan.TokenStrBlock: + length = len(read.Value) + } + + c.resolveExpr(e.Value) + u := c.popStack() + if u, ok := u.Int(); ok != union.ValueNone { + return int32(length) <= u + } + if u, ok := u.Float(); ok != union.ValueNone { + return float64(length) <= u + } + panic(fmt.Errorf("unexpected value type: %s", u.Type().String())) + } + case *gqt.ConstrMap: + var expectItem *gqlast.Type + if expect != nil { + expectItem = expect.Elem + } + itemCheck := makeCheck(e.Constraint, expectItem, schema) + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, schema) { + return false + } + *c.reader = rBefore + for c.reader.PeekOne().ID != gqlscan.TokenArr { + return false + } + for c.reader.PeekOne().ID != gqlscan.TokenArrEnd { + if !itemCheck(c) { + return false + } + } + return true + } + } + return nil +} + +// Designation creates a textual explanation for the given expression. +func Designation(c *Checker, e gqt.Expression) string { + switch e := e.(type) { + case *gqt.ConstrAny: + return "can be any value" + case *gqt.ConstrEquals: + return "must be equal " + Designation(c, e.Value) + case *gqt.ConstrNotEquals: + return "must not be equal " + Designation(c, e.Value) + case *gqt.ConstrLenEquals: + return "length must be equal " + Designation(c, e.Value) + case *gqt.ConstrLenNotEquals: + return "length must not be equal " + Designation(c, e.Value) + case *gqt.ConstrLenLess: + return "length must be less than " + Designation(c, e.Value) + case *gqt.ConstrLenGreater: + return "length must be greater than " + Designation(c, e.Value) + case *gqt.ConstrLenLessOrEqual: + return "length must be less than or equal " + Designation(c, e.Value) + case *gqt.ConstrLenGreaterOrEqual: + return "length must be greater than or equal " + Designation(c, e.Value) + case *gqt.ConstrMap: + return "each item: " + Designation(c, e.Constraint) + case *gqt.ConstrLess: + return "must be less than " + Designation(c, e.Value) + case *gqt.ConstrGreater: + return "must be greater than " + Designation(c, e.Value) + case *gqt.ConstrLessOrEqual: + return "must be less than or equal " + Designation(c, e.Value) + case *gqt.ConstrGreaterOrEqual: + return "must be greater than or equal " + Designation(c, e.Value) + case *gqt.ExprParentheses: + return "(" + Designation(c, e.Expression) + ")" + case *gqt.ExprAddition: + return Designation(c, e.AddendLeft) + + " + " + + Designation(c, e.AddendRight) + case *gqt.ExprSubtraction: + return Designation(c, e.Minuend) + + " - " + + Designation(c, e.Subtrahend) + case *gqt.ExprMultiplication: + return Designation(c, e.Multiplicant) + + " * " + + Designation(c, e.Multiplicator) + case *gqt.ExprDivision: + return Designation(c, e.Dividend) + + " / " + + Designation(c, e.Divisor) + case *gqt.ExprModulo: + return Designation(c, e.Dividend) + + " % " + + Designation(c, e.Divisor) + case *gqt.ExprEqual: + return Designation(c, e.Left) + + " equal " + + Designation(c, e.Right) + case *gqt.ExprNotEqual: + return Designation(c, e.Left) + + " not equal " + + Designation(c, e.Right) + case *gqt.ExprGreater: + return Designation(c, e.Left) + + " greater than " + + Designation(c, e.Right) + case *gqt.ExprLess: + return Designation(c, e.Left) + + " less than " + + Designation(c, e.Right) + case *gqt.ExprLessOrEqual: + return Designation(c, e.Left) + + " less than or equal " + + Designation(c, e.Right) + case *gqt.ExprGreaterOrEqual: + return Designation(c, e.Left) + + " greater than or equal " + + Designation(c, e.Right) + case *gqt.ExprLogicalNegation: + return "not(" + Designation(c, e.Expression) + ")" + case *gqt.ExprNumericNegation: + return "negative(" + Designation(c, e.Expression) + ")" + case *gqt.Number: + i, ok := e.Int() + if ok { + return strconv.FormatInt(int64(i), 10) + } + f, _ := e.Float() + return fmt.Sprintf("%f", f) + case *gqt.String: + return fmt.Sprintf("%q", e.Value) + case *gqt.True: + return "true" + case *gqt.False: + return "false" + case *gqt.Null: + return "null" + case *gqt.Array: + var b strings.Builder + b.WriteByte('[') + for i, v := range e.Items { + if i > 0 { + b.WriteString(" ,") + } + fmt.Fprintf(&b, "%d: %v", i, Designation(c, v)) + } + b.WriteByte(']') + return b.String() + case *gqt.Enum: + return e.Value + case *gqt.Variable: + p, ok := c.pathByVarDecl[e.Declaration] + if !ok { + return strconv.FormatUint(p, 10) + } + case *gqt.ExprLogicalOr: + var b strings.Builder + for i, v := range e.Expressions { + if i > 0 { + b.WriteString(", or ") + } + b.WriteString(Designation(c, v)) + } + return b.String() + case *gqt.ExprLogicalAnd: + var b strings.Builder + for i, v := range e.Expressions { + if i > 0 { + b.WriteString(", and ") + } + b.WriteString(Designation(c, v)) + } + return b.String() + case *gqt.Object: + var b strings.Builder + b.WriteByte('{') + for i, v := range e.Fields { + if i > 0 { + b.WriteString(" ,") + } + fmt.Fprintf(&b, "%q: %v", v.Name.Name, Designation(c, v.Constraint)) + } + b.WriteByte('}') + return b.String() + } + panic(fmt.Sprintf("unhandled expression: %T", e)) +} + +func (c *Checker) expectOrNum(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenInt && v.ID != gqlscan.TokenFloat +} + +func (c *Checker) expectOrInt(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenInt +} + +func (c *Checker) expectOrFloat(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenFloat +} + +func (c *Checker) expectOrString(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenStr && v.ID != gqlscan.TokenStrBlock +} + +func (c *Checker) expectOrEnum(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenEnumVal +} + +func (c *Checker) expectOrBool(expect *gqlast.Type) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenTrue && v.ID != gqlscan.TokenFalse +} + +func (c *Checker) expectOrHasLen( + expect *gqlast.Type, +) (wrongType bool) { + if expect != nil { + rBefore := *c.reader + isWrongType := isWrongType(c.reader, expect, c.schema) + *c.reader = rBefore + return isWrongType + } + v := c.reader.PeekOne() + return v.ID != gqlscan.TokenArr && + v.ID != gqlscan.TokenStr && + v.ID != gqlscan.TokenStrBlock +} + +func makeEqArray( + v *gqt.Array, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + checks := make([]check, len(v.Items)) + for i := 0; i < len(v.Items); i++ { + var expect *gqlast.Type + if expect != nil { + expect = expect.Elem + } + checks[i] = makeCheck(v.Items[i], expect, schema) + } + return func(c *Checker) (match bool) { + if c.reader.ReadOne().ID != gqlscan.TokenArr { + return false + } + count := 0 + for ; ; count++ { + t := c.reader.ReadOne() + if t.ID != gqlscan.TokenArrEnd { + break + } + if count >= len(checks) { + return false + } + if !checks[count](c) { + return false + } + } + return count == len(checks) + } +} + +func makeNotEqArray( + v *gqt.Array, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + checks := make([]check, len(v.Items)) + for i := 0; i < len(v.Items); i++ { + var expect *gqlast.Type + if expect != nil { + expect = expect.Elem + } + checks[i] = makeCheck(v.Items[i], expect, schema) + } + return func(c *Checker) (match bool) { + if c.reader.ReadOne().ID != gqlscan.TokenArr { + return false + } + count := 0 + for ; c.reader.PeekOne().ID != gqlscan.TokenArrEnd; count++ { + if count >= len(checks) { + // Skip all following values to return correct tokenRead + for c.reader.PeekOne().ID != gqlscan.TokenArrEnd { + scanval.Length(c.reader) + } + return true + } + if !checks[count](c) { + // Skip all following values to return correct tokenRead + for c.reader.PeekOne().ID != gqlscan.TokenArrEnd { + scanval.Length(c.reader) + } + return true + } + } + c.reader.ReadOne() + // All item checks were matched, finally check length + return count != len(checks) + } +} + +func makeEqNumber( + v *gqt.Number, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + if i, ok := v.Int(); ok { + return func(c *Checker) (match bool) { + if c.expectOrInt(expect) { + return false + } + a := atoi.MustI32(c.reader.ReadOne().Value) + return int32(i) == a + } + } + f, _ := v.Float() + return func(c *Checker) (match bool) { + if c.expectOrFloat(expect) { + return false + } + a := atoi.MustF64(c.reader.ReadOne().Value) + return f == a + } +} + +func makeNotEqNumber( + v *gqt.Number, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + if i, ok := v.Int(); ok { + return func(c *Checker) (match bool) { + if c.expectOrInt(expect) { + return false + } + a := atoi.MustI32(c.reader.ReadOne().Value) + return int32(i) != a + } + } + f, _ := v.Float() + return func(c *Checker) (match bool) { + if c.expectOrFloat(expect) { + return false + } + a := atoi.MustF64(c.reader.ReadOne().Value) + return f != a + } +} + +func makeEqString( + v *gqt.String, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + if c.expectOrString(expect) { + return false + } + t := c.reader.ReadOne() + switch t.ID { + case gqlscan.TokenStr: + return v.Value == string(t.Value) + case gqlscan.TokenStrBlock: + return v.Value == string(t.Value) + } + return false + } +} + +func makeNotEqString( + v *gqt.String, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + if c.expectOrString(expect) { + return false + } + t := c.reader.ReadOne() + switch t.ID { + case gqlscan.TokenStr: + return v.Value != string(t.Value) + case gqlscan.TokenStrBlock: + return v.Value != string(t.Value) + } + return false + } +} + +func makeEqEnum( + v *gqt.Enum, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + if c.expectOrEnum(expect) { + return false + } + b := c.reader.ReadOne().Value + return v.Value == string(b) + } +} + +func makeNotEqEnum( + v *gqt.Enum, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + if c.expectOrEnum(expect) { + return false + } + b := c.reader.ReadOne().Value + return v.Value != string(b) + } +} + +func makeEqNull( + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, c.schema) { + return false + } + *c.reader = rBefore + t := c.reader.ReadOne().ID + return t == gqlscan.TokenNull + } +} + +func makeNotEqNull( + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, c.schema) { + return false + } + *c.reader = rBefore + t := c.reader.ReadOne().ID + return t != gqlscan.TokenNull + } +} + +func makeEqBool( + value bool, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + v := gqlscan.TokenFalse + if value { + v = gqlscan.TokenTrue + } + return func(c *Checker) (match bool) { + if c.expectOrBool(expect) { + return false + } + t := c.reader.ReadOne().ID + return t == v + } +} + +func makeNotEqBool( + value bool, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + v := gqlscan.TokenFalse + if value { + v = gqlscan.TokenTrue + } + return func(c *Checker) (match bool) { + if c.expectOrBool(expect) { + return false + } + t := c.reader.ReadOne().ID + return t != v + } +} + +func makeEqObject( + v *gqt.Object, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + checks := make(map[string]check, len(v.Fields)) + fieldChecked := make(map[string]bool, len(v.Fields)) + var requiredChecks int + for _, v := range v.Fields { + var expect *gqlast.Type + if v.Def != nil { + expect = v.Def.Type + if expect.NonNull { + requiredChecks++ + } + } + checks[v.Name.Name] = makeCheck(v.Constraint, expect, schema) + fieldChecked[v.Name.Name] = false + } + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, c.schema) { + return false + } + *c.reader = rBefore + if c.reader.ReadOne().ID != gqlscan.TokenObj { + return false + } + + // Reset check status + for k := range fieldChecked { + fieldChecked[k] = false + } + + count := 0 + for ; ; count++ { + read := c.reader.ReadOne() + if read.ID == gqlscan.TokenObjEnd { + break + } else if count >= len(checks) { + return false + } + + check := checks[string(read.Value)] + if check == nil { + // Unknown field, wrong object type + return false + } + + if fieldChecked[string(read.Value)] { + // Field provided twice, invalid object + return false + } + fieldChecked[string(read.Value)] = true + + if !check(c) { + return false + } + } + if requiredChecks < 1 && count != len(checks) || count < requiredChecks { + // Not all required fields were provided, wrong object type + return false + } + return true + } +} + +func makeNotEqObject( + v *gqt.Object, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + checks := make(map[string]check, len(v.Fields)) + fieldChecked := make(map[string]bool, len(v.Fields)) + var requiredChecks int + for _, v := range v.Fields { + var expect *gqlast.Type + if v.Def != nil { + expect = v.Def.Type + if expect.NonNull { + requiredChecks++ + } + } + checks[v.Name.Name] = makeCheck(v.Constraint, expect, schema) + fieldChecked[v.Name.Name] = false + } + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, c.schema) { + return false + } + *c.reader = rBefore + if c.reader.ReadOne().ID != gqlscan.TokenObj { + return false + } + + // Reset check status + for k := range fieldChecked { + fieldChecked[k] = false + } + + count := 0 + for ; ; count++ { + read := c.reader.ReadOne() + if read.ID == gqlscan.TokenObjEnd { + break + } else if count >= len(checks) { + return false + } + + check := checks[string(read.Value)] + if check == nil { + // Unknown field, wrong object type + return false + } + + if fieldChecked[string(read.Value)] { + // Field provided twice, invalid object + return false + } + fieldChecked[string(read.Value)] = true + + if check(c) { + // Don't return just yet as we're not sure whether the + // object type was correct. + match = true + } + } + if requiredChecks < 1 && count != len(checks) || count < requiredChecks { + // Not all required fields were provided, wrong object type + return false + } + return !match + } +} + +func makeEqExpression( + v gqt.Expression, + expect *gqlast.Type, + schema *gqlast.Schema, +) check { + return func(c *Checker) (match bool) { + rBefore := *c.reader + if isWrongType(c.reader, expect, c.schema) { + return false + } + *c.reader = rBefore + c.resolveExpr(v) + u := c.popStack() + + switch u.Type() { + case union.TypeNull: + return c.reader.ReadOne().ID == gqlscan.TokenNull + case union.TypeBoolean: + b, _ := u.Bool() + if b { + return c.reader.ReadOne().ID == gqlscan.TokenTrue + } + return c.reader.ReadOne().ID == gqlscan.TokenFalse + case union.TypeInt: + if read := c.reader.ReadOne(); read.ID == gqlscan.TokenInt { + u, _ := u.Int() + return u == atoi.MustI32(read.Value) + } + return false + case union.TypeFloat: + if read := c.reader.ReadOne(); read.ID == gqlscan.TokenFloat { + u, _ := u.Float() + return u == atoi.MustF64(read.Value) + } + return false + case union.TypeString: + if read := c.reader.ReadOne(); read.ID == gqlscan.TokenStr { + u, _ := u.String() + return u == string(read.Value) + } + return false + case union.TypeEnum: + if read := c.reader.ReadOne(); read.ID == gqlscan.TokenEnumVal { + u, _ := u.Enum() + return u == string(read.Value) + } + return false + case union.TypeTokens: + // if len(u.Tokens()) != len(c.checkedValue) { + // return false + // } + for i := 0; !c.reader.EOF(); i++ { + read := c.reader.ReadOne() + if read.ID != u.Tokens()[i].ID { + return false + } + switch read.ID { + case gqlscan.TokenStrBlock: + if string(read.Value) != string(u.Tokens()[i].Value) { + return false + } + default: + if string(read.Value) != string(u.Tokens()[i].Value) { + return false + } + } + } + } + + return true + } +} + +func unionsEqual(a, b []union.Union) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !union.Equal(a[i], b[i]) { + return false + } + } + return true +} diff --git a/pkg/engine/playmon/internal/constrcheck/constrcheck_test.go b/pkg/engine/playmon/internal/constrcheck/constrcheck_test.go new file mode 100644 index 0000000..6b34b73 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/constrcheck_test.go @@ -0,0 +1,121 @@ +package constrcheck_test + +import ( + "embed" + "io/fs" + "sort" + "strings" + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck/internal/test" + "github.com/graph-guard/gqt/v4" + "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/v2" + gqlast "github.com/vektah/gqlparser/v2/ast" +) + +//go:embed tests +var testsFS embed.FS + +func TestCheck(t *testing.T) { + d, err := fs.ReadDir(testsFS, "tests") + require.NoError(t, err) + + for _, do := range d { + fileName := do.Name() + if do.IsDir() { + t.Run(fileName, func(t *testing.T) { + t.Skipf("ignoring directory %q", fileName) + }) + continue + } + if !strings.HasSuffix(fileName, ".yml") { + t.Run(fileName, func(t *testing.T) { + t.Skipf("ignoring file %q", fileName) + }) + continue + } + + t.Run(strings.TrimSuffix(fileName, ".yml"), func(t *testing.T) { + ts, err := test.Parse(testsFS, fileName) + require.NoError(t, err) + var withschema, schemaless *gqt.Operation + var schemaAST *gqlast.Schema + { + p, err := gqt.NewParser([]gqt.Source{ + {Name: "schema.graphqls", Content: ts.Schema}, + }) + require.NoError(t, err, "unexpected error in schema") + opr, _, errs := p.Parse([]byte(ts.Template)) + require.Len(t, errs, 0, "unexpected errors: %#v", errs) + withschema = opr + + schemaAST, err = gqlparser.LoadSchema(&gqlast.Source{ + Name: "schema.graphqls", Input: ts.Schema, + }) + require.NoError(t, err) + } + { + opr, _, errs := gqt.Parse([]byte(ts.Template)) + require.Len(t, errs, 0, "unexpected errors: %#v", errs) + schemaless = opr + } + + run := func(t *testing.T, opr *gqt.Operation) { + t.Helper() + c := constrcheck.New(opr, schemaAST) + require.NotNil(t, c) + + paths := make([]string, 0, len(ts.Inputs)) + for path := range ts.Inputs { + paths = append(paths, path) + } + sort.Strings(paths) + + // TODO: implement test + + // variables := make(map[string][]gqlparse.Token, len(ts.Inputs)) + // for path, v := range ts.Inputs { + // variables[path] = v.Tokens + // } + + // c.Init(inputs) + // for _, path := range paths { + // if len(ts.Inputs[path].Tokens) < 1 { + // t.Errorf("missing test value for path %q", path) + // continue + // } + + // if ts.Inputs[path].Match == nil { + // i := ts.Inputs[path] + // t := false + // i.Match = &t + // ts.Inputs[path] = i + // } + // if ts.Inputs[path].MatchSchemaless == nil { + // i := ts.Inputs[path] + // i.MatchSchemaless = i.Match + // ts.Inputs[path] = i + // } + + // actual := c.Check(path) + // if opr.Def != nil { + // require.Equal( + // t, *ts.Inputs[path].Match, actual, + // "checking %q", path, + // ) + // } else { + // require.Equal( + // t, *ts.Inputs[path].MatchSchemaless, actual, + // "checking %q", path, + // ) + // } + // } + } + + t.Run("schema", func(t *testing.T) { run(t, withschema) }) + t.Run("schemaless", func(t *testing.T) { run(t, schemaless) }) + }) + } +} diff --git a/pkg/engine/playmon/internal/constrcheck/doc.go b/pkg/engine/playmon/internal/constrcheck/doc.go new file mode 100644 index 0000000..1814a94 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/doc.go @@ -0,0 +1,27 @@ +// package constrcheck provides function Make which +// creates constraint checker functions for each input in a GQT operation. +// Example: +// +// query { +// f( +// a: * +// b=$v: > 10 +// c: [...["a", != "b"]] +// d: len < (4+2) * $v +// ) +// } +// +// The above template will produce 3 constraint checker functions because +// there is no need to check argument "a" since it accepts any value. +// Argument "b" will accept Int values greater 10; +// argument "c" will accept arrays where every item is an array +// that contains exactly 2 items, first of which must be equal String("a") +// and second must not be equal String("b"); +// argument "d" will accept String values (or arrays depending on schema type) +// the length of which is less than (4+2) multiplied by +// the value of argument "b". +// Constraint checker functions will be returned as a map of path -> function. +// and require argument inputs to be passed as map[string]any, where +// int32, float64, string, Enum, bool values as well as (nested) slices of any +// of the mentioned types are matched. +package constrcheck diff --git a/pkg/engine/playmon/internal/constrcheck/internal/test/test.go b/pkg/engine/playmon/internal/constrcheck/internal/test/test.go new file mode 100644 index 0000000..6777ea6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/internal/test/test.go @@ -0,0 +1,144 @@ +package test + +import ( + "bytes" + "embed" + "fmt" + "path/filepath" + + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "gopkg.in/yaml.v3" +) + +type T struct { + Schema string `yaml:"schema"` + Template string `yaml:"template"` + Inputs map[string]Input `yaml:"inputs"` +} + +type Input struct { + Match *bool `yaml:"match"` + MatchSchemaless *bool `yaml:"match-schemaless"` + Tokens Tokens `yaml:"tokens"` +} + +type Tokens []gqlparse.Token + +func (t *Tokens) UnmarshalYAML(value *yaml.Node) error { + for _, pair := range value.Content { + if len(pair.Content) < 1 { + return fmt.Errorf("invalid tokens dictionary") + } + tp, err := parseTokenType(pair.Content[0].Value) + if err != nil { + return err + } + *t = append(*t, gqlparse.Token{ID: tp, Value: []byte(pair.Content[1].Value)}) + } + return nil +} + +func Parse(fs embed.FS, fileName string) (ts T, err error) { + f, err := fs.ReadFile(filepath.Join("tests", fileName)) + if err != nil { + return ts, fmt.Errorf("reading YAML test file: %w", err) + } + + d := yaml.NewDecoder(bytes.NewReader(f)) + d.KnownFields(true) + if err := d.Decode(&ts); err != nil { + return ts, fmt.Errorf("parsing YAML test definition: %w", err) + } + + // Make sure each input has only one value + for path, i := range ts.Inputs { + if len(i.Tokens) < 1 { + return ts, fmt.Errorf("%q: missing input value", path) + } + } + + return ts, nil +} + +func parseTokenType(name string) (gqlscan.Token, error) { + switch name { + case "DefQry": + return gqlscan.TokenDefQry, nil + case "DefMut": + return gqlscan.TokenDefMut, nil + case "DefSub": + return gqlscan.TokenDefSub, nil + case "DefFrag": + return gqlscan.TokenDefFrag, nil + case "OprName": + return gqlscan.TokenOprName, nil + case "DirName": + return gqlscan.TokenDirName, nil + case "VarList": + return gqlscan.TokenVarList, nil + case "VarListEnd": + return gqlscan.TokenVarListEnd, nil + case "ArgList": + return gqlscan.TokenArgList, nil + case "ArgListEnd": + return gqlscan.TokenArgListEnd, nil + case "Set": + return gqlscan.TokenSet, nil + case "SetEnd": + return gqlscan.TokenSetEnd, nil + case "FragTypeCond": + return gqlscan.TokenFragTypeCond, nil + case "FragName": + return gqlscan.TokenFragName, nil + case "FragInline": + return gqlscan.TokenFragInline, nil + case "NamedSpread": + return gqlscan.TokenNamedSpread, nil + case "FieldAlias": + return gqlscan.TokenFieldAlias, nil + case "Field": + return gqlscan.TokenField, nil + case "ArgName": + return gqlscan.TokenArgName, nil + case "EnumVal": + return gqlscan.TokenEnumVal, nil + case "Arr": + return gqlscan.TokenArr, nil + case "ArrEnd": + return gqlscan.TokenArrEnd, nil + case "Str": + return gqlscan.TokenStr, nil + case "StrBlock": + return gqlscan.TokenStrBlock, nil + case "Int": + return gqlscan.TokenInt, nil + case "Float": + return gqlscan.TokenFloat, nil + case "True": + return gqlscan.TokenTrue, nil + case "False": + return gqlscan.TokenFalse, nil + case "Null": + return gqlscan.TokenNull, nil + case "VarName": + return gqlscan.TokenVarName, nil + case "VarTypeName": + return gqlscan.TokenVarTypeName, nil + case "VarTypeArr": + return gqlscan.TokenVarTypeArr, nil + case "VarTypeArrEnd": + return gqlscan.TokenVarTypeArrEnd, nil + case "VarTypeNotNull": + return gqlscan.TokenVarTypeNotNull, nil + case "VarRef": + return gqlscan.TokenVarRef, nil + case "Obj": + return gqlscan.TokenObj, nil + case "ObjEnd": + return gqlscan.TokenObjEnd, nil + case "ObjField": + return gqlscan.TokenObjField, nil + } + return 0, fmt.Errorf("unknown token type: %q", name) +} diff --git a/pkg/engine/playmon/internal/constrcheck/internal/union/union.go b/pkg/engine/playmon/internal/constrcheck/internal/union/union.go new file mode 100644 index 0000000..3710092 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/internal/union/union.go @@ -0,0 +1,291 @@ +package union + +import ( + "github.com/graph-guard/ggproxy/pkg/atoi" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/unsafe" + "github.com/graph-guard/gqlscan" +) + +type Type int8 + +const ( + _ Type = iota + TypeNull + TypeTokens + TypeString + TypeEnum + TypeFloat + TypeInt + TypeBoolean + TypeArray + TypeArrayEnd +) + +func Array() Union { return Union{unionType: TypeArray} } +func ArrayEnd() Union { return Union{unionType: TypeArrayEnd} } +func Null() Union { return Union{unionType: TypeNull} } +func Int(v int32) Union { return Union{unionType: TypeInt, i: v} } +func Float(v float64) Union { return Union{unionType: TypeFloat, f: v} } +func String(v string) Union { return Union{unionType: TypeString, s: v} } +func Enum(v string) Union { return Union{unionType: TypeEnum, s: v} } +func Tokens(v []gqlparse.Token) Union { return Union{unionType: TypeTokens, t: v} } +func True() Union { return Union{unionType: TypeBoolean, i: 1} } +func False() Union { return Union{unionType: TypeBoolean, i: 0} } + +func (t Type) String() string { + switch t { + case TypeNull: + return "Null" + case TypeTokens: + return "Tokens" + case TypeString: + return "String" + case TypeEnum: + return "Enum" + case TypeFloat: + return "Float" + case TypeInt: + return "Int" + case TypeBoolean: + return "Boolean" + case TypeArray: + return "array" + case TypeArrayEnd: + return "array_end" + } + return "" +} + +// Union represents any of Float, Int, String, Enum, Boolean +type Union struct { + t []gqlparse.Token + s string + f float64 + i int32 + unionType Type +} + +// Value defines whether a value could be extracted (>ValueNone) +// and how it was extracted. +type Value int8 + +const ( + // ValueNone that there is no value. + ValueNone Value = iota - 1 + // ValueConv indicates that the value matched exactly. + ValueExact + // ValueConv indicates that the value was converted. + ValueConv + // ValueInf indicates that the value was inferred. + ValueInf + // ValueInfConv indicates that the value was inferred and converted. + ValueInfConv +) + +// Type returns the type of the value the union is holding, +// or 0 if the union has zero value. +func (u *Union) Type() Type { return u.unionType } + +// Float returns a float64 value or ValueNone if +// the union is storing a value of a different type. +func (u *Union) Float() (float64, Value) { + switch u.unionType { + case TypeFloat: + return u.f, ValueExact + case TypeInt: + return float64(u.i), ValueConv + case TypeTokens: + switch u.t[0].ID { + case gqlscan.TokenInt: + return float64(atoi.MustI32(u.t[0].Value)), ValueInfConv + case gqlscan.TokenFloat: + return atoi.MustF64(u.t[0].Value), ValueInf + } + } + return 0, ValueNone +} + +// Int returns an int32 value or ValueNone if +// the union is storing a value of a different type. +func (u *Union) Int() (value int32, ok Value) { + switch u.unionType { + case TypeInt: + return u.i, ValueExact + case TypeTokens: + if u.t[0].ID == gqlscan.TokenInt { + return atoi.MustI32(u.t[0].Value), ValueInf + } + } + return 0, ValueNone +} + +// Enum returns an enum value or ValueNone if +// the union is storing a value of a different type. +func (u *Union) Enum() (value string, ok Value) { + switch u.unionType { + case TypeEnum: + return u.s, ValueExact + case TypeTokens: + if u.t[0].ID == gqlscan.TokenEnumVal { + return unsafe.B2S(u.t[0].Value), ValueInf + } + } + return "", ValueNone +} + +// Bool returns a boolean value or ValueNone if +// the union is storing a value of a different type. +func (u *Union) Bool() (value bool, ok Value) { + if u.unionType == TypeBoolean { + return u.i > 0, ValueExact + } else if u.unionType == TypeTokens { + switch u.t[0].ID { + case gqlscan.TokenTrue: + return true, ValueInf + case gqlscan.TokenFalse: + return false, ValueInf + } + } + return false, ValueNone +} + +// String returns a string value or ValueNone if +// the union is storing a value of a different type. +func (u *Union) String() (value string, ok Value) { + if u.unionType == TypeString { + return u.s, ValueExact + } else if u.unionType == TypeTokens { + switch u.t[0].ID { + case gqlscan.TokenStr: + return unsafe.B2S(u.t[0].Value), ValueInf + case gqlscan.TokenStrBlock: + panic("todo") + // return unsafe.B2S(u.t[0].Value), true + } + } + return "", ValueNone +} + +// Tokens returns the tokens slice or nil if +// the union is storing a value of a different type. +func (u *Union) Tokens() (value []gqlparse.Token) { + if u.unionType == TypeTokens { + return u.t + } + return nil +} + +// IsNull returns true if the union represents a null value. +func (u *Union) IsNull() bool { + return u.unionType == TypeNull || + u.unionType == TypeTokens && u.t[0].ID == gqlscan.TokenNull +} + +func Equal(l, r Union) bool { + if l.unionType == TypeArray && r.unionType == TypeArray || + l.unionType == TypeArrayEnd && r.unionType == TypeArrayEnd { + return true + } + if l.IsNull() && r.IsNull() { + return true + } + if l, v := l.Bool(); v != ValueNone { + r, v := r.Bool() + return v != ValueNone && l == r + } + if l, v := l.Int(); v != ValueNone { + r, v := r.Int() + return v != ValueNone && l == r + } + if l, v := l.Float(); v != ValueNone { + r, v := r.Float() + return v != ValueNone && l == r + } + if l, v := l.String(); v != ValueNone { + r, v := r.String() + return v != ValueNone && l == r + } + if l, v := l.Enum(); v != ValueNone { + r, v := r.Enum() + return v != ValueNone && l == r + } + + if r.unionType != TypeTokens || len(l.t) != len(r.t) { + return false + } + for i := range r.t { + if l.t[i].ID != r.t[i].ID || + string(l.t[i].Value) != string(r.t[i].Value) { + return false + } + } + return l.unionType != TypeArray && l.unionType != TypeArrayEnd + + /* OLD IMPLEMENTATION */ + // switch l.unionType { + // case TypeInt: + // switch r.unionType { + // case TypeInt: + // return l.i == r.i + // case TypeFloat: + // return l.Float() == r.f + // case TypeTokens: + // switch r.t[0].Type { + // case gqlscan.TokenInt: + // return l.i == atoi.MustI32(r.t[0].Value) + // case gqlscan.TokenFloat: + // return l.Float() == atoi.MustF64(r.t[0].Value) + // } + // } + // case TypeFloat: + // switch r.unionType { + // case TypeInt: + // return l.f == r.Float() + // case TypeFloat: + // return l.f == r.f + // case TypeTokens: + // switch r.t[0].Type { + // case gqlscan.TokenInt: + // return l.f == float64(atoi.MustI32(r.t[0].Value)) + // case gqlscan.TokenFloat: + // return l.f == atoi.MustF64(r.t[0].Value) + // } + // } + // case TypeEnum: + // return l.Enum() == r.Enum() + // case TypeBoolean: + // return l.Bool() == r.Bool() + // case TypeNull: + // return r.unionType == TypeNull + // case TypeString: + // switch r.unionType { + // case TypeString: + // return l.s == r.s + // case TypeTokens: + // switch r.t[0].Type { + // case gqlscan.TokenStr: + // return l.s == string(r.t[0].Value) + // case gqlscan.TokenStrBlock: + // panic("todo") + // } + // } + // case TypeTokens: + // if r.unionType != TypeTokens { + // return false + // } + // if len(l.t) != len(r.t) { + // return false + // } + // for i := range r.t { + // if l.t[i].Type != r.t[i].Type { + // return false + // } + // if string(l.t[i].Value) != string(r.t[i].Value) { + // return false + // } + // } + // return true + // } + // return false +} diff --git a/pkg/engine/playmon/internal/constrcheck/internal/union/union_test.go b/pkg/engine/playmon/internal/constrcheck/internal/union/union_test.go new file mode 100644 index 0000000..2db8656 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/internal/union/union_test.go @@ -0,0 +1,304 @@ +package union_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck/internal/union" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/require" +) + +func TestAreUnionsEqual(t *testing.T) { + for _, tt := range []struct { + Name string + Left union.Union + Right union.Union + Expect bool + }{ + { + Name: "same_type_int_equal", + Left: union.Int(42), + Right: union.Int(42), + Expect: true, + }, + { + Name: "same_type_int_diff", + Left: union.Int(42), + Right: union.Int(43), + Expect: false, + }, + + { + Name: "same_type_float_equal", + Left: union.Float(3.14), + Right: union.Float(3.14), + Expect: true, + }, + { + Name: "same_type_float_diff", + Left: union.Float(3.14), + Right: union.Float(3.15), + Expect: false, + }, + + { + Name: "same_type_bool_equal", + Left: union.True(), + Right: union.True(), + Expect: true, + }, + { + Name: "same_type_bool_diff", + Left: union.True(), + Right: union.False(), + Expect: false, + }, + + { + Name: "same_type_string_equal", + Left: union.String("okay"), + Right: union.String("okay"), + Expect: true, + }, + { + Name: "same_type_string_diff", + Left: union.String("okay"), + Right: union.String("!okay"), + Expect: false, + }, + + { + Name: "same_type_enum_equal", + Left: union.Enum("red"), + Right: union.Enum("red"), + Expect: true, + }, + { + Name: "same_type_enum_diff", + Left: union.Enum("red"), + Right: union.Enum("redd"), + Expect: false, + }, + + { + Name: "same_type_null_equal", + Left: union.Null(), + Right: union.Null(), + Expect: true, + }, + { + Name: "same_type_null_diff", + Left: union.Null(), + Right: union.Int(42), + Expect: false, + }, + + { + Name: "same_type_tokens_int_equal", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }), + Expect: true, + }, + { + Name: "same_type_tokens_int_diff", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("43")}, + }), + Expect: false, + }, + + { + Name: "inf_int_tokens", + Left: union.Int(42), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }), + Expect: true, + }, + { + Name: "inf_tokens_int", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }), + Right: union.Int(42), + Expect: true, + }, + + { + Name: "inf_float_tokens", + Left: union.Float(3.1415), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenFloat, Value: []byte("3.1415")}, + }), + Expect: true, + }, + { + Name: "inf_tokens_float", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenFloat, Value: []byte("3.1415")}, + }), + Right: union.Float(3.1415), + Expect: true, + }, + + { + Name: "inf_string_tokens", + Left: union.String("okay"), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("okay")}, + }), + Expect: true, + }, + { + Name: "inf_tokens_string", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("okay")}, + }), + Right: union.String("okay"), + Expect: true, + }, + + { + Name: "inf_enum_tokens", + Left: union.Enum("red"), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }), + Expect: true, + }, + { + Name: "inf_tokens_enum", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }), + Right: union.Enum("red"), + Expect: true, + }, + + { + Name: "inf_true_tokens", + Left: union.True(), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenTrue}, + }), + Expect: true, + }, + { + Name: "inf_tokens_true", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenTrue}, + }), + Right: union.True(), + Expect: true, + }, + + { + Name: "inf_false_tokens", + Left: union.False(), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenFalse}, + }), + Expect: true, + }, + { + Name: "inf_tokens_false", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenFalse}, + }), + Right: union.False(), + Expect: true, + }, + + { + Name: "inf_null_tokens", + Left: union.Null(), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }), + Expect: true, + }, + { + Name: "inf_tokens_null", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }), + Right: union.Null(), + Expect: true, + }, + + { + Name: "equal_tokens_array", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenInt, Value: []byte("23")}, + {ID: gqlscan.TokenArr}, + }), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenInt, Value: []byte("23")}, + {ID: gqlscan.TokenArr}, + }), + Expect: true, + }, + { + Name: "not_equal_tokens_array", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenInt, Value: []byte("23")}, + {ID: gqlscan.TokenArr}, + }), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("23")}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenArr}, + }), + Expect: false, + }, + { + Name: "not_equal_tokens_array_empty", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenInt, Value: []byte("23")}, + {ID: gqlscan.TokenArr}, + }), + Right: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + }), + Expect: false, + }, + + { + Name: "inf_tokens_null", + Left: union.Tokens([]gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }), + Right: union.Null(), + Expect: true, + }, + + { + Name: "diff_type_int_float", + Left: union.Int(42), + Right: union.Float(42), + Expect: false, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + require.Equal(t, tt.Expect, union.Equal(tt.Left, tt.Right)) + }) + } +} diff --git a/pkg/engine/playmon/internal/constrcheck/iswrongtype.go b/pkg/engine/playmon/internal/constrcheck/iswrongtype.go new file mode 100644 index 0000000..63a4a62 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/iswrongtype.go @@ -0,0 +1,137 @@ +package constrcheck + +import ( + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/gqlscan" + gqlast "github.com/vektah/gqlparser/v2/ast" +) + +// isWrongType returns true if the value represented in input +// doesn't correspond to the type defined by expect. +func isWrongType( + r *tokenreader.Reader, + expect *gqlast.Type, + schema *gqlast.Schema, +) bool { + var def *gqlast.Definition + if expect != nil { + def = schema.Types[expect.NamedType] + } + + switch read := r.ReadOne(); read.ID { + case gqlscan.TokenNull: + if expect == nil { + return false + } + return expect.NonNull + case gqlscan.TokenArr: + if expect != nil { + if expect.Elem == nil { + return true + } + expect = expect.Elem + } + for { + rBefore := r + if r.PeekOne().ID == gqlscan.TokenArrEnd { + r.ReadOne() + return false + } + if isWrongType(rBefore, expect, schema) { + return true + } + } + case gqlscan.TokenTrue, gqlscan.TokenFalse: + if expect == nil || (def != nil && def.Kind == gqlast.Scalar && + def.Name != "ID" && + def.Name != "Int" && + def.Name != "Float" && + def.Name != "String") { + return false + } + return expect.NamedType != "Boolean" + case gqlscan.TokenInt: + if expect == nil || (def != nil && def.Kind == gqlast.Scalar && + def.Name != "ID" && + def.Name != "String" && + def.Name != "Boolean") { + return false + } + return expect.NamedType != "Int" && + expect.NamedType != "Float" + case gqlscan.TokenFloat: + if expect == nil || (def != nil && def.Kind == gqlast.Scalar && + def.Name != "ID" && + def.Name != "Int" && + def.Name != "String" && + def.Name != "Boolean") { + return false + } + return expect.NamedType != "Float" + case gqlscan.TokenStr, gqlscan.TokenStrBlock: + if expect == nil || (def != nil && def.Kind == gqlast.Scalar && + def.Name != "Int" && + def.Name != "Float" && + def.Name != "Boolean") { + return false + } + return expect.NamedType != "String" && + expect.NamedType != "ID" + case gqlscan.TokenEnumVal: + if def == nil || def.Kind == gqlast.Scalar && + def.Name != "ID" && + def.Name != "Int" && + def.Name != "Float" && + def.Name != "String" && + def.Name != "Boolean" { + // No expectation or custom scalar. + return false + } + for i := range def.EnumValues { + if def.EnumValues[i].Name == string(read.Value) { + return false + } + } + return true + case gqlscan.TokenObj: + if def == nil || def.Kind == gqlast.Scalar { + // No expectation or custom scalar type. + // r.ReadOne() + SKIP_OBJECT: + for levelObj := 1; ; { + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break SKIP_OBJECT + } + } + } + return false + } else if def.Kind != gqlast.InputObject { + return true + } + SCAN_OBJECT: + for !r.EOF() { + if read = r.ReadOne(); read.ID == gqlscan.TokenObjEnd { + break SCAN_OBJECT + } + + for i := range def.Fields { + if def.Fields[i].Name == string(read.Value) { + // Check field value + if isWrongType(r, def.Fields[i].Type, schema) { + return true + } + continue SCAN_OBJECT + } + } + // Field not found in expected input object type + return true + } + return false + } + return true +} diff --git a/pkg/engine/playmon/internal/constrcheck/iswrongtype_test.go b/pkg/engine/playmon/internal/constrcheck/iswrongtype_test.go new file mode 100644 index 0000000..df25c34 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/iswrongtype_test.go @@ -0,0 +1,544 @@ +package constrcheck + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/require" + gqlparser "github.com/vektah/gqlparser/v2" + gqlast "github.com/vektah/gqlparser/v2/ast" +) + +func TestIsWrongType_False(t *testing.T) { + for _, tt := range []struct { + Name string + Schema string + GQLVarVals [][]gqlparse.Token + ExpectType *gqlast.Type + Input []gqlparse.Token + }{ + { + Name: "int", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Int"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }, + }, + { + Name: "float", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Float"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + }, + }, + { + Name: "string", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "String"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("text")}, + }, + }, + { + Name: "string_block", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "String"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStrBlock}, + }, + }, + { + Name: "id", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "ID"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("someid")}, + }, + }, + { + Name: "bool_true", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Boolean"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenTrue}, + }, + }, + { + Name: "bool_false", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Boolean"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenFalse}, + }, + }, + { + Name: "enum", + Schema: `enum Color {red}`, + ExpectType: &gqlast.Type{NamedType: "Color"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }, + }, + { + Name: "null", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Int", NonNull: false}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }, + }, + { + Name: "array_int", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{Elem: &gqlast.Type{ + NamedType: "Int", + NonNull: true, + }}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenInt, Value: []byte("100500")}, + {ID: gqlscan.TokenArrEnd}, + }, + }, + { + Name: "array_int_empty", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{Elem: &gqlast.Type{ + NamedType: "Int", + NonNull: true, + }}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + }, + }, + { + Name: "array_int_null", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{Elem: &gqlast.Type{ + NamedType: "Int", + NonNull: false, + }}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenArrEnd}, + }, + }, + { + Name: "input_object_1_field", + Schema: `input InputObject { x: Int! }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {x:0} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "input_object_3_fields", + Schema: `input InputObject { x: Int! y:String! z:Boolean }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {x:0, y:"text" z:null} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenObjField, Value: []byte("z")}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "nested_input_object", + Schema: ` + input InputObject { x: O! y:String! z:Boolean } + input O { i: Int! } + `, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {x:{i:42}, y:"text" z:null} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("i")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenObjField, Value: []byte("z")}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "2d_array_nested_input_objects", + Schema: ` + input InputObject { x: O! } + input O { i: Int! } + `, + ExpectType: &gqlast.Type{ + Elem: &gqlast.Type{ + Elem: &gqlast.Type{ + NamedType: "InputObject", + }, + }, + }, + Input: []gqlparse.Token{ + // [[{x:{i:42}}]] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("i")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + }, + { + Name: "custom_scalar_int", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("0")}, + }, + }, + { + Name: "custom_scalar_string", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("custom")}, + }, + }, + { + Name: "custom_scalar_string_block", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStrBlock}, + }, + }, + { + Name: "custom_scalar_enum", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("customenum")}, + }, + }, + { + Name: "custom_scalar_boolean_true", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenTrue}, + }, + }, + { + Name: "custom_scalar_boolean_false", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenFalse}, + }, + }, + { + Name: "custom_scalar_object", + Schema: `scalar Custom`, + ExpectType: &gqlast.Type{ + NamedType: "Custom", + }, + Input: []gqlparse.Token{ + // {x:{y:[0]}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "tst", + Schema: ` + type Query { f(object:Object!):Int } + input Object { subobject: SubObject! } + input SubObject { array: [ArrayObject!]! } + input ArrayObject { name: String!, index: Int! } + `, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("subobject")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("array")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("name")}, + {ID: gqlscan.TokenStr, Value: []byte("first")}, + {ID: gqlscan.TokenObjField, Value: []byte("index")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("name")}, + {ID: gqlscan.TokenStr, Value: []byte("second")}, + {ID: gqlscan.TokenObjField, Value: []byte("index")}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "expect_float_get_int", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Float"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }, + }, + { + Name: "expect_float_get_int", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Float"}, + GQLVarVals: [][]gqlparse.Token{ + { /*Not to be used*/ }, + {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + }, + Input: []gqlparse.Token{ + {ID: gqlparse.TokenTypeValIndexOffset + 1}, + }, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + var s *gqlast.Schema + if tt.Schema != "" { + var err error + s, err = gqlparser.LoadSchema(&gqlast.Source{ + Name: "schema.graphqls", + Input: tt.Schema, + }) + require.NoError(t, err) + } else { + require.Nil( + t, tt.ExpectType, + "type expectations are always schema-aware", + ) + } + + r := isWrongType( + &tokenreader.Reader{ + Main: tt.Input, + Vars: tt.GQLVarVals, + }, tt.ExpectType, s, + ) + require.False(t, r) + }) + } +} + +func TestIsWrongType_True(t *testing.T) { + for _, tt := range []struct { + Name string + Schema string + GQLVarVals [][]gqlparse.Token + ExpectType *gqlast.Type + Input []gqlparse.Token + }{ + { + Name: "expect_non-null_int_get_null", + Schema: `type Query { x:Int }`, + GQLVarVals: [][]gqlparse.Token{ + { /*Not used*/ }, + {{ID: gqlscan.TokenNull}}, + }, + ExpectType: &gqlast.Type{NamedType: "Int", NonNull: true}, + Input: []gqlparse.Token{ + {ID: gqlparse.TokenTypeValIndexOffset + 1}, + }, + }, + { + Name: "expect_non-null_int_get_null", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Int", NonNull: true}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }, + }, + { + Name: "expect_int_get_float", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Int"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + }, + }, + { + Name: "expect_int_get_enum", + Schema: ` + type Query { x:Int } + enum Color { red } + `, + ExpectType: &gqlast.Type{NamedType: "Int"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }, + }, + { + Name: "expect_enum_get_int", + Schema: ` + type Query { x:Int } + enum Color { red } + `, + ExpectType: &gqlast.Type{NamedType: "Color"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }, + }, + { + Name: "expect_boolean_get_enum", + Schema: `type Query { x:Int }`, + ExpectType: &gqlast.Type{NamedType: "Boolean"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("tru")}, + }, + }, + { + Name: "expect_enum_get_boolean", + Schema: `enum Color { red green blue }`, + ExpectType: &gqlast.Type{NamedType: "Color"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenTrue}, + }, + }, + { + Name: "expect_enum_get_wrong_enum", + Schema: ` + enum Color { red green blue } + enum Fruit { banana orange apple } + `, + ExpectType: &gqlast.Type{NamedType: "Color"}, + Input: []gqlparse.Token{ + {ID: gqlscan.TokenEnumVal, Value: []byte("orange")}, + }, + }, + { + Name: "expect_object_get_str_block", + Schema: `input InputObject { x: Int! }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // """not an object""" + {ID: gqlscan.TokenStrBlock}, + }, + }, + { + Name: "expect_different_object_missing_fields", + Schema: `input InputObject { x: Int! }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {y:42} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "expect_different_object_superfluous_fields", + Schema: `input InputObject { x: Int! }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {x:42, y:43} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenInt, Value: []byte("43")}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + { + Name: "expect_different_field_type", + Schema: `input InputObject { x: Int! }`, + ExpectType: &gqlast.Type{ + NamedType: "InputObject", + }, + Input: []gqlparse.Token{ + // {x:3.14} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + var s *gqlast.Schema + if tt.Schema != "" { + var err error + s, err = gqlparser.LoadSchema(&gqlast.Source{ + Name: "schema.graphqls", + Input: tt.Schema, + }) + require.NoError(t, err) + } else { + require.Nil( + t, tt.ExpectType, + "type expectations are always schema-aware", + ) + } + r := isWrongType( + &tokenreader.Reader{ + Main: tt.Input, + Vars: tt.GQLVarVals, + }, tt.ExpectType, s, + ) + require.True(t, r) + }) + } +} diff --git a/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_div.yml b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_div.yml new file mode 100644 index 0000000..a2a5789 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_div.yml @@ -0,0 +1,33 @@ +schema: > + type Query {f( + t: Float! + f: Float! + t2: Float + f2: Float + ):Int} + +template: > + query {f( + t: 3.14 / 3.14, + f: 3.14 / 3.14, + t2: 3.14 / 2, + f2: 3.14 / 2, + )} + +inputs: + Query.f|t: + match: true + tokens: + - Float: 1 + Query.f|f: + match: false + tokens: + - Float: 1.5 + Query.f|t2: + match: true + tokens: + - Float: 1.57 + Query.f|f2: + match: false + tokens: + - Float: 1.58 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mod.yml b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mod.yml new file mode 100644 index 0000000..c9a0e54 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mod.yml @@ -0,0 +1,33 @@ +schema: > + type Query {f( + t: Float! + f: Float! + t2: Float + f2: Float + ):Int} + +template: > + query {f( + t: 12.5 % 5.5, + f: 12.5 % 5.5, + t2: 20.5 % 10, + f2: 20.5 % 10, + )} + +inputs: + Query.f|t: + match: true + tokens: + - Float: 1.5 + Query.f|f: + match: false + tokens: + - Float: 15 + Query.f|t2: + match: true + tokens: + - Float: 0.5 + Query.f|f2: + match: false + tokens: + - Float: 5 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mul.yml b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mul.yml new file mode 100644 index 0000000..4bde2e5 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_mul.yml @@ -0,0 +1,33 @@ +schema: > + type Query {f( + t: Float! + f: Float! + t2: Float + f2: Float + ):Int} + +template: > + query {f( + t: 3.14 * 3.14, + f: 3.14 * 3.14, + t2: 3.14 * -1, + f2: 3.14 * -1, + )} + +inputs: + Query.f|t: + match: true + tokens: + - Float: 9.8596 + Query.f|f: + match: false + tokens: + - Float: 9.8597 + Query.f|t2: + match: true + tokens: + - Float: -3.14 + Query.f|f2: + match: false + tokens: + - Float: -3 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_sub.yml b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_sub.yml new file mode 100644 index 0000000..7137984 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/arithmetic_sub.yml @@ -0,0 +1,33 @@ +schema: > + type Query {f( + t: Float! + f: Float! + t2: Float + f2: Float + ):Int} + +template: > + query {f( + t: 3.14 - 2.14, + f: 3.14 - 2.14, + t2: 12 - 1.1, + f2: 12 - 1.1, + )} + +inputs: + Query.f|t: + match: true + tokens: + - Float: 1 + Query.f|f: + match: false + tokens: + - Float: 1.0001 + Query.f|t2: + match: true + tokens: + - Float: 10.9 + Query.f|f2: + match: false + tokens: + - Float: 10.91 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/equal_strblock_false.todo.yaml b/pkg/engine/playmon/internal/constrcheck/tests/equal_strblock_false.todo.yaml new file mode 100644 index 0000000..4aea73e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/equal_strblock_false.todo.yaml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + string: String! + ):Int} + +template: > + query {f( + string: "text", + )} + +inputs: + Query.f|string: + match: false + tokens: + - StrBlock: "tex" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_arithmetic_add.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_arithmetic_add.yml new file mode 100644 index 0000000..804d080 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_arithmetic_add.yml @@ -0,0 +1,27 @@ +schema: > + type Query {f( + a: Float! + b: Float! + c: Float + ):Int} + +template: > + query {f( + a: 3.14 + 3.14, + b: 12 + 1.1, + c: 3 + 1, + )} + +inputs: + Query.f|a: + match: false + tokens: + - Float: 6.281 + Query.f|b: + match: false + tokens: + - Float: 13 + Query.f|c: + match: false + tokens: + - Null: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_constr_and.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_constr_and.yml new file mode 100644 index 0000000..7339d60 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_constr_and.yml @@ -0,0 +1,27 @@ +schema: > + type Query {f( + a: Int! + b: Int! + c: Int! + ):Int} + +template: > + query {f( + a: >0 && <10, + b: >0 && <10, + c: >0 && <10 && != 5, + )} + +inputs: + Query.f|a: + match: false + tokens: + - Int: 10 + Query.f|b: + match: false + tokens: + - Int: 0 + Query.f|c: + match: false + tokens: + - Int: 5 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_constr_or.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_constr_or.yml new file mode 100644 index 0000000..4303476 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_constr_or.yml @@ -0,0 +1,27 @@ +schema: > + type Query {f( + a: Int! + b: Int! + c: Int! + ):Int} + +template: > + query {f( + a: 1 || 0, + b: 1 || 0, + c: 1 || 0 || 2, + )} + +inputs: + Query.f|a: + match: false + tokens: + - Int: 42 + Query.f|b: + match: false + tokens: + - Int: 42 + Query.f|c: + match: false + tokens: + - Int: 42 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_int.yml new file mode 100644 index 0000000..13fb514 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_int.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + ints1: [Int!]! + ints2: [Int!]! + ints3: [Int!]! + ):Int} + +template: > + query {f( + ints1: [1,2,3], + ints2: [1,2,3], + ints3: [1,2,3], + )} + +inputs: + Query.f|ints1: + match: false + tokens: + - Arr: + - Int: 1 + - Int: 2 + - ArrEnd: + Query.f|ints2: + match: false + tokens: + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - Int: 4 + - ArrEnd: + Query.f|ints3: + match: false + tokens: + - Arr: + - Int: 3 + - Int: 2 + - Int: 1 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_str.yml new file mode 100644 index 0000000..eb19bd7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_arr_str.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + strs1: [String!]! + strs2: [String!]! + strs3: [String!]! + ):String} + +template: > + query {f( + strs1: ["1","2","3"], + strs2: ["1","2","3"], + strs3: ["1","2","3"], + )} + +inputs: + Query.f|strs1: + match: false + tokens: + - Arr: + - Str: "1" + - Str: "2" + - ArrEnd: + Query.f|strs2: + match: false + tokens: + - Arr: + - Str: "1" + - Str: "2" + - Str: "3" + - Str: "4" + - ArrEnd: + Query.f|strs3: + match: false + tokens: + - Arr: + - Str: "3" + - Str: "2" + - Str: "1" + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_bool.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_bool.yml new file mode 100644 index 0000000..d2c2734 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_bool.yml @@ -0,0 +1,21 @@ +schema: > + type Query {f( + bool_t: Boolean! + bool_f: Boolean! + ):Int} + +template: > + query {f( + bool_t: true, + bool_f: false, + )} + +inputs: + Query.f|bool_t: + match: false + tokens: + - False: + Query.f|bool_f: + match: false + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_enum.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_enum.yml new file mode 100644 index 0000000..62835db --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_enum.yml @@ -0,0 +1,16 @@ +schema: > + type Query {f( + enum: Color! + ):Int} + enum Color { red green blue } + +template: > + query {f( + enum: green, + )} + +inputs: + Query.f|enum: + match: false + tokens: + - EnumVal: "red" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_float.yml new file mode 100644 index 0000000..f42df8b --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_float.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + float: Float! + ):Int} + +template: > + query {f( + float: 3.14159, + )} + +inputs: + Query.f|float: + match: false + tokens: + - Float: 3.1415 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_int.yml new file mode 100644 index 0000000..1004b4f --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_int.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + int: Int! + ):Int} + +template: > + query {f( + int: 42, + )} + +inputs: + Query.f|int: + match: false + tokens: + - Int: 142 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_obj.yml new file mode 100644 index 0000000..181a56e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_obj.yml @@ -0,0 +1,35 @@ +schema: > + type Query {f( + a: A! + ):Int} + input A { b: B! } + input B { + s: String! + i: Int! + optional: Boolean + } + +template: > + query {f( + a: { + b: { + s: "okay", + i: 42, + optional: * + } + } + )} + +inputs: + Query.f|a: + match: false + tokens: # {b:{s:"okay",i:24}} + - Obj: + - ObjField: "b" + - Obj: + - ObjField: "s" + - Str: "okay" + - ObjField: "i" + - Int: 24 # mismatch + - ObjEnd: + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_str.yml new file mode 100644 index 0000000..5037036 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_str.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + string: String! + ):Int} + +template: > + query {f( + string: "text", + )} + +inputs: + Query.f|string: + match: false + tokens: + - Str: "tex" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_equal_strblock.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_strblock.yml new file mode 100644 index 0000000..4aea73e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_equal_strblock.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + string: String! + ):Int} + +template: > + query {f( + string: "text", + )} + +inputs: + Query.f|string: + match: false + tokens: + - StrBlock: "tex" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_interface.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_interface.yml new file mode 100644 index 0000000..76e38a7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_interface.yml @@ -0,0 +1,40 @@ +schema: > + type Query { + iface: Iface! + } + interface Iface { + i(x:Int!):Int! + } + type Foo implements Iface { + i(x:Int!):Int! + } + type Bar implements Iface { + i(x:Int!):Int! + } + +template: > + query { + iface { + i(x:>42) + ... on Foo { + i(x:>20) + } + ... on Bar { + i(x:>30) + } + } + } + +inputs: + Query.iface.i|x: + match: false + tokens: + - Int: 42 + Query.iface&Foo.i|x: + match: false + tokens: + - Int: 20 + Query.iface&Bar.i|x: + match: false + tokens: + - Int: 30 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_int.yml new file mode 100644 index 0000000..27b43ee --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_int.yml @@ -0,0 +1,62 @@ +schema: > + type Query { f( + eq:[Int]! + neq:[Int]! + gr:[Int]! + le:[Int]! + greq:[Int]! + leeq:[Int]! + ):Int } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: false + tokens: # len diff: +1 + - Arr: + - Int: 1 + - ArrEnd: + Query.f|neq: + match: false + tokens: # len eq + - Arr: + - ArrEnd: + Query.f|gr: + match: false + tokens: # len diff: -1 + - Arr: + - Int: 1 + - ArrEnd: + Query.f|le: + match: false + tokens: # len diff: +1 + - Arr: + - Int: 1 + - Int: 2 + - ArrEnd: + Query.f|greq: + match: false + tokens: # len diff: -1 + - Arr: + - Int: 1 + - Int: 2 + - ArrEnd: + Query.f|leeq: + match: false + tokens: # len diff: +1 + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - Int: 4 + - Int: 5 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_obj.yml new file mode 100644 index 0000000..bbdf9d7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_array_obj.yml @@ -0,0 +1,96 @@ +schema: > + type Query { f( + eq:[I!]! + neq:[I!]! + gr:[I!]! + le:[I!]! + greq:[I!]! + leeq:[I!]! + ):Int } + input I { x:Int! } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: false + tokens: # len diff: +1 + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - ArrEnd: + Query.f|neq: + match: false + tokens: # len eq + - Arr: + - ArrEnd: + Query.f|gr: + match: false + tokens: # len diff: -1 + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - ArrEnd: + Query.f|le: + match: false + tokens: # len diff: +1 + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - ArrEnd: + Query.f|greq: + match: false + tokens: # len diff: -1 + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - ArrEnd: + Query.f|leeq: + match: false + tokens: # len diff: +1 + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 3 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 4 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 5 + - ObjEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_eq.yml new file mode 100644 index 0000000..9c9f7bb --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + eq: String! + ):Int } + +template: > + query { f( + eq: len 0, + ) } + +inputs: + Query.f|eq: + match: false + tokens: + - Str: "1" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_gr.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_gr.yml new file mode 100644 index 0000000..c07a147 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_gr.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + gr: String! + ):Int } + +template: > + query { f( + gr: len > 1, + ) } + +inputs: + Query.f|gr: + match: false + tokens: + - Str: "1" # diff: -1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_greq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_greq.yml new file mode 100644 index 0000000..6c7f65f --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_greq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + greq: String! + ):Int } + +template: > + query { f( + greq: len >= 3, + ) } + +inputs: + Query.f|greq: + match: false + tokens: + - Str: "12" # diff: -1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_leeq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_leeq.yml new file mode 100644 index 0000000..1db9dc0 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_leeq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + leeq: String! + ):Int } + +template: > + query { f( + leeq: len <= 4, + ) } + +inputs: + Query.f|leeq: + match: false + tokens: + - Str: "12345" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_neq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_neq.yml new file mode 100644 index 0000000..0511463 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_str_neq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + neq: String! + ):Int } + +template: > + query { f( + neq: len != 0, + ) } + +inputs: + Query.f|neq: + match: false + tokens: + - Str: "" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_eq.yml new file mode 100644 index 0000000..a1a5105 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + eq: String! + ):Int } + +template: > + query { f( + eq: len 0, + ) } + +inputs: + Query.f|eq: + match: false + tokens: + - StrBlock: "1" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_gr.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_gr.yml new file mode 100644 index 0000000..5d6d5f0 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_gr.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + gr: String! + ):Int } + +template: > + query { f( + gr: len > 1, + ) } + +inputs: + Query.f|gr: + match: false + tokens: + - StrBlock: "1" # diff: -1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_greq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_greq.yml new file mode 100644 index 0000000..09186da --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_greq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + greq: String! + ):Int } + +template: > + query { f( + greq: len >= 3, + ) } + +inputs: + Query.f|greq: + match: false + tokens: + - StrBlock: "12" # diff: -1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yaml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yaml new file mode 100644 index 0000000..f1d8f0e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yaml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + le: String! + ):Int } + +template: > + query { f( + le: len < 2, + ) } + +inputs: + Query.f|le: + match: false + tokens: + - StrBlock: "12" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yml new file mode 100644 index 0000000..f1d8f0e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_le.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + le: String! + ):Int } + +template: > + query { f( + le: len < 2, + ) } + +inputs: + Query.f|le: + match: false + tokens: + - StrBlock: "12" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_leeq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_leeq.yml new file mode 100644 index 0000000..970a5d9 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_leeq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + leeq: String! + ):Int } + +template: > + query { f( + leeq: len <= 4, + ) } + +inputs: + Query.f|leeq: + match: false + tokens: + - StrBlock: "12345" # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_neq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_neq.yml new file mode 100644 index 0000000..ed00d3e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_len_strblock_neq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + neq: String! + ):Int } + +template: > + query { f( + neq: len != 0, + ) } + +inputs: + Query.f|neq: + match: false + tokens: + - StrBlock: "" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_equal.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_equal.yml new file mode 100644 index 0000000..f399597 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_equal.yml @@ -0,0 +1,52 @@ +schema: > + type Query {f( + ints: Boolean! + floats: Boolean! + strings: Boolean! + enums: Boolean! + bools1: Boolean! + bools2: Boolean! + arrs: Boolean! + ):Int} + enum Color { red green blue } + +template: > + query {f( + ints: 10 == 11, + floats: 3.14 == 3.145, + strings: "foo" == "fo", + enums: red == green, + bools1: true == false, + bools2: false == true, + arrs: [1,2] == [1,2,3], + )} + +inputs: + Query.f|ints: + match: false + tokens: + - True: + Query.f|floats: + match: false + tokens: + - True: + Query.f|strings: + match: false + tokens: + - True: + Query.f|enums: + match: false + tokens: + - True: + Query.f|bools1: + match: false + tokens: + - True: + Query.f|bools2: + match: false + tokens: + - True: + Query.f|arrs: + match: false + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational.yml new file mode 100644 index 0000000..2bb15b9 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + i: Int! + gr: Boolean! + le: Boolean! + greq: Boolean! + leeq: Boolean! + ):Int} + +template: > + query {f( + i=$v: 2, + gr: $v > 2, + le: $v < 2, + greq: $v >= 2, + leeq: $v <= 2, + )} + +inputs: + Query.f|i: + match: true + tokens: + - Int: 2 + Query.f|gr: + match: false + tokens: + - True: + Query.f|le: + match: false + tokens: + - True: + Query.f|greq: + match: false + tokens: + - False: + Query.f|leeq: + match: false + tokens: + - False: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational_float.yml new file mode 100644 index 0000000..5bde5d6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_expr_relational_float.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + i: Float! + gr: Boolean! + le: Boolean! + greq: Boolean! + leeq: Boolean! + ):Int} + +template: > + query {f( + i=$v: 3.14, + gr: $v > 3.14, + le: $v < 3.14, + greq: $v >= 3.14, + leeq: $v <= 3.14, + )} + +inputs: + Query.f|i: + match: true + tokens: + - Float: 3.14 + Query.f|gr: + match: false + tokens: + - True: + Query.f|le: + match: false + tokens: + - True: + Query.f|greq: + match: false + tokens: + - False: + Query.f|leeq: + match: false + tokens: + - False: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_notequal.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_notequal.yml new file mode 100644 index 0000000..ebd63c6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_notequal.yml @@ -0,0 +1,51 @@ +schema: > + type Query {f( + ints: Boolean! + strs: Boolean! + floats: Boolean! + arrays_int0: Boolean! + arrays_int1: Boolean! + arrays_strs: Boolean! + arrays_3d: Boolean! + ):Int} + +template: > + query {f( + ints: 11 != 11, + strs: "a" != "a", + floats: 3.14 != 3.14, + arrays_int0: [] != [], + arrays_int1: [1,2] != [1,2], + arrays_strs: ["okay"] != ["okay"], + arrays_3d: [[[]]] != [[[]]], + )} + +inputs: + Query.f|ints: + match: false + tokens: + - True: + Query.f|strs: + match: false + tokens: + - True: + Query.f|floats: + match: false + tokens: + - True: + Query.f|arrays_int0: + match: false + tokens: + - True: + Query.f|arrays_int1: + match: false + tokens: + - True: + Query.f|arrays_strs: + match: false + tokens: + - True: + Query.f|arrays_2d: + match: false + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_eq.yml new file mode 100644 index 0000000..64491b7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + x: Int! + ):Int} + +template: > + query {f( + x: 1 || 2, + )} + +inputs: + Query.f|x: + match: false + tokens: + - Int: 3 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_gr_le.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_gr_le.yml new file mode 100644 index 0000000..9797e6f --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_logical_or_gr_le.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + x: Int! + ):Int} + +template: > + query {f( + x: <10 || >20, + )} + +inputs: + Query.f|x: + match: false + tokens: + - Int: 15 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_map_2d.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_map_2d.yml new file mode 100644 index 0000000..53fc714 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_map_2d.yml @@ -0,0 +1,47 @@ +schema: > + type Query { f( + a: [[Int!]!]! + b: [[Int!]!]! + empty: [[Int!]!]! + ):Int } + +template: > + query { f( + a: [...[...>20]], + b: [...[...>20]], + empty: [...[...>20]], + ) } + +inputs: + Query.f|a: + match: false + tokens: + - Arr: + - Arr: + - Int: 6 + - Int: 7 + - Int: 8 + - ArrEnd: + - ArrEnd: + Query.f|b: + match: false + tokens: + - Arr: + + - Arr: + - Int: 10 + - ArrEnd: + + - Arr: + - Int: 12 + - ArrEnd: + + - Arr: + - ArrEnd: + + - ArrEnd: + Query.f|empty: + match: true + tokens: + - Arr: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_map_array_object.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_map_array_object.yml new file mode 100644 index 0000000..a6d8b92 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_map_array_object.yml @@ -0,0 +1,32 @@ +schema: > + type Query { f( + a: [I!]! + ):Int } + input I { + x: Float! + y: Float! + } + +template: > + query { f( + a: [ ...{x: > 0, y: > 0} ], + ) } + +inputs: + Query.f|a: + match: false + tokens: + - Arr: + - Obj: + - ObjField: "x" + - Float: 1 + - ObjField: "y" + - Float: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Float: 0.1 + - ObjField: "y" + - Float: -1 # mismatch + - ObjEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_map_var_arithmetic.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_map_var_arithmetic.yml new file mode 100644 index 0000000..4fc8ef0 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_map_var_arithmetic.yml @@ -0,0 +1,25 @@ +schema: > + type Query { f( + v:Int! + nonempty:[Int!]! + ):Int } + +template: > + query { f( + v=$v: *, + nonempty: [... > ($v + 2 * 200 / 10 % 7) - 3], + ) } + +inputs: + Query.f|v: + match: true + tokens: + - Int: 2 + Query.f|nonempty: + match: false + tokens: # [20,10,3] # not > 4 + - Arr: + - Int: 20 + - Int: 10 + - Int: 3 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_int.yml new file mode 100644 index 0000000..e0d4fdd --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_int.yml @@ -0,0 +1,19 @@ +schema: > + type Query {f( + ints: [Int!]! + ):Int} + +template: > + query {f( + ints: != [1,2,3], + )} + +inputs: + Query.f|ints: + match: false + tokens: # [1,2,3] + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_str.yml new file mode 100644 index 0000000..bb0b645 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_arr_str.yml @@ -0,0 +1,19 @@ +schema: > + type Query {f( + strs: [String!]! + ):String} + +template: > + query {f( + strs: != ["1","2","3"], + )} + +inputs: + Query.f|strs: + match: false + tokens: + - Arr: + - Str: "1" + - Str: "2" + - Str: "3" + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_obj.yml new file mode 100644 index 0000000..93663ff --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_not_equal_obj.yml @@ -0,0 +1,36 @@ +schema: > + type Query {f( + a: A! + ):Int} + input A { b: B! } + input B { + s: String! + i: Int! + optional: Boolean + } + +template: > + query {f( + a: != { + b: { + s: "okay", + i: 42, + optional: * + } + } + )} + +inputs: + Query.f|a: + match: false + match-schemaless: true + tokens: + - Obj: + - ObjField: "b" + - Obj: + - ObjField: "s" + - Str: "okay" + - ObjField: "i" + - Int: 42 + - ObjEnd: + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_relational_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_relational_float.yml new file mode 100644 index 0000000..53d194f --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_relational_float.yml @@ -0,0 +1,33 @@ +schema: > + type Query { f( + gr: Float! + le: Float! + greq: Float! + leeq: Float! + ):Int } + +template: > + query { f( + gr: > 1.1, + le: < 2.22, + greq: >= 3.333, + leeq: <= 4.4444, + ) } + +inputs: + Query.f|gr: + match: false + tokens: + - Float: 1.1 # diff: 0 + Query.f|le: + match: false + tokens: + - Float: 2.22 # diff: 0 + Query.f|greq: + match: false + tokens: + - Float: 3.332 # diff: -0.001 + Query.f|leeq: + match: false + tokens: + - Float: 4.4445 # diff: +0.0001 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_relational_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_relational_int.yml new file mode 100644 index 0000000..bc60d36 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_relational_int.yml @@ -0,0 +1,33 @@ +schema: > + type Query { f( + gr: Int! + le: Int! + greq: Int! + leeq: Int! + ):Int } + +template: > + query { f( + gr: > 1, + le: < 20, + greq: >= 300, + leeq: <= 4000, + ) } + +inputs: + Query.f|gr: + match: false + tokens: + - Int: 1 # diff: -1 + Query.f|le: + match: false + tokens: + - Int: 20 # diff: +1 + Query.f|greq: + match: false + tokens: + - Int: 299 # diff: -1 + Query.f|leeq: + match: false + tokens: + - Int: 4001 # diff: +1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_union.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_union.yml new file mode 100644 index 0000000..3cf463a --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_union.yml @@ -0,0 +1,33 @@ +schema: > + type Query { + u: U! + } + union U = Foo | Bar + type Foo { + i(x:Int!):Int! + } + type Bar { + i(x:Int!):Int! + } + +template: > + query { + u { + ... on Foo { + i(x:>20) + } + ... on Bar { + i(x:>30) + } + } + } + +inputs: + Query.u&Foo.i|x: + match: false + tokens: + - Int: 20 + Query.u&Bar.i|x: + match: false + tokens: + - Int: 30 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_var_bool.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_var_bool.yml new file mode 100644 index 0000000..f7bfb97 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_var_bool.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: Boolean! + b: Boolean! + ):Int } + +template: > + query { f( + a = $a: false, + b: (true && true) && $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - False: + Query.f|b: + match: false + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_var_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_var_str.yml new file mode 100644 index 0000000..350edd4 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_var_str.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: String! + b: String! + ):Int } + +template: > + query { f( + a = $a: "okay", + b: $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - Str: "okay" + Query.f|b: + match: false + tokens: + - Str: "not okay" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/false_var_strblock.yml b/pkg/engine/playmon/internal/constrcheck/tests/false_var_strblock.yml new file mode 100644 index 0000000..a7934c7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/false_var_strblock.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: String! + b: String! + ):Int } + +template: > + query { f( + a = $a: "okay", + b: $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - StrBlock: "okay" + Query.f|b: + match: false + tokens: + - StrBlock: "not okay" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs.yml b/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs.yml new file mode 100644 index 0000000..5b4722b --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs.yml @@ -0,0 +1,35 @@ +schema: > + type Query { f( + eq: I! + neq: I! + ):Int } + input I { + x: Int! + y: Int! + } + +template: > + query { f( + eq: {x: *, y: *}, + neq: != {x: *, y: *}, + ) } + +inputs: + Query.f|ie: + match: false + tokens: # {x:42,x:42} + - Obj: + - ObjField: "x" + - Int: 42 + - ObjField: "x" + - Int: 42 + - ObjEnd: + Query.f|in: + match: false + tokens: # {x:42,x:42} + - Obj: + - ObjField: "i" + - Int: 42 + - ObjField: "x" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs_in_any.yml b/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs_in_any.yml new file mode 100644 index 0000000..1deee31 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/invalid_obj_duplicate_inputs_in_any.yml @@ -0,0 +1,24 @@ +schema: > + type Query { f( + any: I! + ):Int } + input I { + x: Int! + y: Int! + } + +template: > + query { f( + any: *, + ) } + +inputs: + Query.f|any: + match: false + tokens: # {x:42,x:42} + - Obj: + - ObjField: "x" + - Int: 42 + - ObjField: "x" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/logical_negation.yml b/pkg/engine/playmon/internal/constrcheck/tests/logical_negation.yml new file mode 100644 index 0000000..a6ea1bf --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/logical_negation.yml @@ -0,0 +1,46 @@ +schema: > + type Query { f( + neg_t: Boolean! + neg_f: Boolean! + + var_t: Boolean! + var_f: Boolean! + neg_var_t: Boolean! + neg_var_f: Boolean! + ):Int } + +template: > + query { f( + neg_t: !true, + neg_f: !false, + var_t=$var_t: true, + var_f=$var_f: false, + neg_var_t: !$var_t, + neg_var_f: !$var_f, + ) } + +inputs: + Query.f|neg_t: + match: true + tokens: + - False: + Query.f|neg_f: + match: true + tokens: + - True: + Query.f|var_t: + match: true + tokens: + - True: + Query.f|var_f: + match: true + tokens: + - False: + Query.f|neg_var_t: + match: true + tokens: + - False: + Query.f|neg_var_f: + match: true + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/numeric_negation.yml b/pkg/engine/playmon/internal/constrcheck/tests/numeric_negation.yml new file mode 100644 index 0000000..329c441 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/numeric_negation.yml @@ -0,0 +1,45 @@ +schema: > + type Query { f( + neg_i: Int! + neg_f: Float! + var_i: Int! + var_f: Float! + neg_var_i: Int! + neg_var_f: Float! + ):Int } + +template: > + query { f( + neg_i: -(3), + neg_f: -(3.14), + var_i=$var_i: 3, + var_f=$var_f: 3.14, + neg_var_i: -$var_i, + neg_var_f: -$var_f, + ) } + +inputs: + Query.f|neg_i: + match: true + tokens: + - Int: -3 + Query.f|neg_f: + match: true + tokens: + - Float: -3.14 + Query.f|var_i: + match: true + tokens: + - Int: 3 + Query.f|var_f: + match: true + tokens: + - Float: 3.14 + Query.f|neg_var_i: + match: true + tokens: + - Int: -3 + Query.f|neg_var_f: + match: true + tokens: + - Float: -3.14 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_arithmetic_add.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_arithmetic_add.yml new file mode 100644 index 0000000..afff359 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_arithmetic_add.yml @@ -0,0 +1,21 @@ +schema: > + type Query {f( + a: Float! + b: Float + ):Int} + +template: > + query {f( + a: 3.14 + 3.14, + b: 12 + 1.1, + )} + +inputs: + Query.f|a: + match: true + tokens: + - Float: 6.28 + Query.f|b: + match: true + tokens: + - Float: 13.1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_constr_and.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_constr_and.yml new file mode 100644 index 0000000..710c37d --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_constr_and.yml @@ -0,0 +1,27 @@ +schema: > + type Query {f( + a: Int! + b: Int! + c: Int! + ):Int} + +template: > + query {f( + a: >0 && <10, + b: >0 && <10, + c: >0 && <10 && != 5, + )} + +inputs: + Query.f|a: + match: true + tokens: + - Int: 4 + Query.f|b: + match: true + tokens: + - Int: 4 + Query.f|c: + match: true + tokens: + - Int: 4 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_constr_or.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_constr_or.yml new file mode 100644 index 0000000..dd5a5e7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_constr_or.yml @@ -0,0 +1,27 @@ +schema: > + type Query {f( + a: Int! + b: Int! + c: Int! + ):Int} + +template: > + query {f( + a: 1 || 0, + b: 1 || 0, + c: 1 || 0 || 2, + )} + +inputs: + Query.f|a: + match: true + tokens: + - Int: 1 + Query.f|b: + match: true + tokens: + - Int: 0 + Query.f|c: + match: true + tokens: + - Int: 2 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_int.yml new file mode 100644 index 0000000..2097c24 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_int.yml @@ -0,0 +1,19 @@ +schema: > + type Query {f( + ints: [Int!]! + ):Int} + +template: > + query {f( + ints: [1,2,3], + )} + +inputs: + Query.f|ints: + match: true + tokens: + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_str.yml new file mode 100644 index 0000000..4e82b61 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_arr_str.yml @@ -0,0 +1,19 @@ +schema: > + type Query {f( + strs: [String!]! + ):String} + +template: > + query {f( + strs: ["1","2","3"], + )} + +inputs: + Query.f|strs: + match: true + tokens: + - Arr: + - Str: "1" + - Str: "2" + - Str: "3" + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_bool.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_bool.yml new file mode 100644 index 0000000..527eb30 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_bool.yml @@ -0,0 +1,21 @@ +schema: > + type Query {f( + bool_t: Boolean! + bool_f: Boolean! + ):Int} + +template: > + query {f( + bool_t: true, + bool_f: false, + )} + +inputs: + Query.f|bool_t: + match: true + tokens: + - True: + Query.f|bool_f: + match: true + tokens: + - False: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_enum.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_enum.yml new file mode 100644 index 0000000..8bf8c7e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_enum.yml @@ -0,0 +1,16 @@ +schema: > + type Query {f( + enum: Color! + ):Int} + enum Color { red green blue } + +template: > + query {f( + enum: green, + )} + +inputs: + Query.f|enum: + match: true + tokens: + - EnumVal: "green" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_float.yml new file mode 100644 index 0000000..3a51283 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_float.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + float: Float! + ):Int} + +template: > + query {f( + float: 3.14159, + )} + +inputs: + Query.f|float: + match: true + tokens: + - Float: 3.14159 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_int.yml new file mode 100644 index 0000000..8e3d96b --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_int.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + int: Int! + ):Int} + +template: > + query {f( + int: 42, + )} + +inputs: + Query.f|int: + match: true + tokens: + - Int: 42 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_obj.yml new file mode 100644 index 0000000..9822bc8 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_obj.yml @@ -0,0 +1,36 @@ +schema: > + type Query {f( + a: A! + ):Int} + input A { b: B! } + input B { + s: String! + i: Int! + optional: Boolean + } + +template: > + query {f( + a: { + b: { + s: "okay", + i: 42, + optional: * + } + } + )} + +inputs: + Query.f|a: + match: true + match-schemaless: false + tokens: # {b:{s:"okay",i:42}} + - Obj: + - ObjField: "b" + - Obj: + - ObjField: "s" + - Str: "okay" + - ObjField: "i" + - Int: 42 + - ObjEnd: + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_str.yml new file mode 100644 index 0000000..4731de2 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_str.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + string: String! + ):Int} + +template: > + query {f( + string: "text", + )} + +inputs: + Query.f|string: + match: true + tokens: + - Str: "text" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_equal_strblock.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_strblock.yml new file mode 100644 index 0000000..07c2a0a --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_equal_strblock.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + string: String! + ):Int} + +template: > + query {f( + string: "text", + )} + +inputs: + Query.f|string: + match: true + tokens: + - StrBlock: "text" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_interface.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_interface.yml new file mode 100644 index 0000000..b23122c --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_interface.yml @@ -0,0 +1,40 @@ +schema: > + type Query { + iface: Iface! + } + interface Iface { + i(x:Int!):Int! + } + type Foo implements Iface { + i(x:Int!):Int! + } + type Bar implements Iface { + i(x:Int!):Int! + } + +template: > + query { + iface { + i(x:>42) + ... on Foo { + i(x:>20) + } + ... on Bar { + i(x:>30) + } + } + } + +inputs: + Query.iface.i|x: + match: true + tokens: + - Int: 43 + Query.iface&Foo.i|x: + match: true + tokens: + - Int: 21 + Query.iface&Bar.i|x: + match: true + tokens: + - Int: 31 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_int.yml new file mode 100644 index 0000000..c607bf7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_int.yml @@ -0,0 +1,70 @@ +schema: > + type Query { f( + eq:[Int]! + neq:[Int]! + gr:[Int]! + le:[Int]! + greq:[Int]! + leeq:[Int]! + ):Int } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: true + tokens: # len eq + - Arr: + - ArrEnd: + Query.f|neq: + match: true + tokens: # len not eq + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - Int: 4 + - Int: 5 + - Int: 6 + - Int: 7 + - Int: 8 + - Int: 9 + - ArrEnd: + Query.f|gr: + match: true + tokens: # len gr + - Arr: + - Int: 1 + - Int: 2 + - ArrEnd: + Query.f|le: + match: true + tokens: # len le + - Arr: + - Int: 1 + - ArrEnd: + Query.f|greq: + match: true + tokens: # len eq + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - ArrEnd: + Query.f|leeq: + match: true + tokens: # len eq + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - Int: 4 + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_obj.yml new file mode 100644 index 0000000..08b6128 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_array_obj.yml @@ -0,0 +1,96 @@ +schema: > + type Query { f( + eq:[I!]! + neq:[I!]! + gr:[I!]! + le:[I!]! + greq:[I!]! + leeq:[I!]! + ):Int } + input I { x:Int! } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: true + tokens: # len eq + - Arr: + - ArrEnd: + Query.f|neq: + match: true + tokens: # len not eq + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - ArrEnd: + Query.f|gr: + match: true + tokens: # len gr + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - ArrEnd: + Query.f|le: + match: true + tokens: # len le + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - ArrEnd: + Query.f|greq: + match: true + tokens: # len eq + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 3 + - ObjEnd: + - ArrEnd: + Query.f|leeq: + match: true + tokens: # len eq + - Arr: + - Obj: + - ObjField: "x" + - Int: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 2 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 3 + - ObjEnd: + - Obj: + - ObjField: "x" + - Int: 4 + - ObjEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str.yml new file mode 100644 index 0000000..8a1c0ee --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str.yml @@ -0,0 +1,45 @@ +schema: > + type Query { f( + eq:String! + neq:String! + gr:String! + le:String! + greq:String! + leeq:String! + ):Int } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: true + tokens: + - Str: "" # eq + Query.f|neq: + match: true + tokens: + - Str: "123456789" # not eq + Query.f|gr: + match: true + tokens: + - Str: "12" # gr + Query.f|le: + match: true + tokens: + - Str: "1" # le + Query.f|greq: + match: true + tokens: + - Str: "123" # eq + Query.f|leeq: + match: true + tokens: + - Str: "1234" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_eq.yml new file mode 100644 index 0000000..091760c --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + eq: String! + ):Int } + +template: > + query { f( + eq: len 0, + ) } + +inputs: + Query.f|eq: + match: true + tokens: + - Str: "" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_gr.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_gr.yml new file mode 100644 index 0000000..e498c98 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_gr.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + gr: String! + ):Int } + +template: > + query { f( + gr: len > 1, + ) } + +inputs: + Query.f|gr: + match: true + tokens: + - Str: "12" # gr diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_greq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_greq.yml new file mode 100644 index 0000000..31206c7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_greq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + greq: String! + ):Int } + +template: > + query { f( + greq: len >= 3, + ) } + +inputs: + Query.f|greq: + match: true + tokens: + - Str: "123" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_le.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_le.yml new file mode 100644 index 0000000..66b966a --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_le.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + le: String! + ):Int } + +template: > + query { f( + le: len < 2, + ) } + +inputs: + Query.f|le: + match: true + tokens: + - Str: "1" # le diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_leeq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_leeq.yml new file mode 100644 index 0000000..500f47d --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_leeq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + leeq: String! + ):Int } + +template: > + query { f( + leeq: len <= 4, + ) } + +inputs: + Query.f|leeq: + match: true + tokens: + - Str: "1234" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_neq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_neq.yml new file mode 100644 index 0000000..70bb3db --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_str_neq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + neq: String! + ):Int } + +template: > + query { f( + neq: len != 0, + ) } + +inputs: + Query.f|neq: + match: true + tokens: + - Str: "123456789" # not eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock.yml new file mode 100644 index 0000000..4269858 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock.yml @@ -0,0 +1,45 @@ +schema: > + type Query { f( + eq:String! + neq:String! + gr:String! + le:String! + greq:String! + leeq:String! + ):Int } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: true + tokens: + - StrBlock: "" # eq + Query.f|neq: + match: true + tokens: + - StrBlock: "123456789" # not eq + Query.f|gr: + match: true + tokens: + - StrBlock: "12" # gr + Query.f|le: + match: true + tokens: + - StrBlock: "1" # le + Query.f|greq: + match: true + tokens: + - StrBlock: "123" # eq + Query.f|leeq: + match: true + tokens: + - StrBlock: "1234" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_eq.yml new file mode 100644 index 0000000..ea55ea6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + eq: String! + ):Int } + +template: > + query { f( + eq: len 0, + ) } + +inputs: + Query.f|eq: + match: true + tokens: + - StrBlock: "" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_gr.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_gr.yml new file mode 100644 index 0000000..cade3ee --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_gr.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + gr: String! + ):Int } + +template: > + query { f( + gr: len > 1, + ) } + +inputs: + Query.f|gr: + match: true + tokens: + - StrBlock: "12" # gr diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_greq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_greq.yml new file mode 100644 index 0000000..e0b7053 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_greq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + greq: String! + ):Int } + +template: > + query { f( + greq: len >= 3, + ) } + +inputs: + Query.f|greq: + match: true + tokens: + - StrBlock: "123" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_le.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_le.yml new file mode 100644 index 0000000..1f99bd9 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_le.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + le: String! + ):Int } + +template: > + query { f( + le: len < 2, + ) } + +inputs: + Query.f|le: + match: true + tokens: + - StrBlock: "1" # le diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_leeq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_leeq.yml new file mode 100644 index 0000000..d74c0e2 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_leeq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + leeq: String! + ):Int } + +template: > + query { f( + leeq: len <= 4, + ) } + +inputs: + Query.f|leeq: + match: true + tokens: + - StrBlock: "1234" # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_neq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_neq.yml new file mode 100644 index 0000000..1f631dc --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_len_strblock_neq.yml @@ -0,0 +1,15 @@ +schema: > + type Query { f( + neq: String! + ):Int } + +template: > + query { f( + neq: len != 0, + ) } + +inputs: + Query.f|neq: + match: true + tokens: + - StrBlock: "123456789" # not eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_equal.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_equal.yml new file mode 100644 index 0000000..5dcc11f --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_equal.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + x: Boolean! + ):Int} + +template: > + query {f( + x: != (10 == 10), + )} + +inputs: + Query.f|x: + match: true + tokens: + - False: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational.yml new file mode 100644 index 0000000..93e9c5c --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + i: Int! + gr: Boolean! + le: Boolean! + greq: Boolean! + leeq: Boolean! + ):Int} + +template: > + query {f( + i=$v: 2, + gr: $v > 2, + le: $v < 2, + greq: $v >= 2, + leeq: $v <= 2, + )} + +inputs: + Query.f|i: + match: true + tokens: + - Int: 2 + Query.f|gr: + match: true + tokens: + - False: + Query.f|le: + match: true + tokens: + - False: + Query.f|greq: + match: true + tokens: + - True: + Query.f|leeq: + match: true + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational_float.yml new file mode 100644 index 0000000..bd12c71 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_expr_relational_float.yml @@ -0,0 +1,39 @@ +schema: > + type Query {f( + i: Float! + gr: Boolean! + le: Boolean! + greq: Boolean! + leeq: Boolean! + ):Int} + +template: > + query {f( + i=$v: 3.14, + gr: $v > 3.14, + le: $v < 3.14, + greq: $v >= 3.14, + leeq: $v <= 3.14, + )} + +inputs: + Query.f|i: + match: true + tokens: + - Float: 3.14 + Query.f|gr: + match: true + tokens: + - False: + Query.f|le: + match: true + tokens: + - False: + Query.f|greq: + match: true + tokens: + - True: + Query.f|leeq: + match: true + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_notequal.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_notequal.yml new file mode 100644 index 0000000..5c6df77 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_notequal.yml @@ -0,0 +1,69 @@ +schema: > + type Query {f( + ints: Boolean! + strs: Boolean! + floats: Boolean! + arrays_int0: Boolean! + arrays_int1: Boolean! + arrays_int2: Boolean! + arrays_strs: Boolean! + arrays_2d: Boolean! + arrays_2d_diff: Boolean! + arrays_3d_diff: Boolean! + ):Int} + +template: > + query {f( + ints: 10 != 11, + strs: "a" != "b", + floats: 3.1415 != 3.14, + arrays_int0: [] != [1], + arrays_int1: [1,2] != [1], + arrays_int2: [1,2] != [1,3], + arrays_strs: [] != ["okay"], + arrays_2d: [[]] != [[], []], + arrays_2d_diff: [[],[2]] != [[],[3]], + arrays_3d_diff: [[[],[2]]] != [[[],[3]]], + )} + +inputs: + Query.f|ints: + match: true + tokens: + - True: + Query.f|strs: + match: true + tokens: + - True: + Query.f|floats: + match: true + tokens: + - True: + Query.f|arrays_int0: + match: true + tokens: + - True: + Query.f|arrays_int1: + match: true + tokens: + - True: + Query.f|arrays_int2: + match: true + tokens: + - True: + Query.f|arrays_strs: + match: true + tokens: + - True: + Query.f|arrays_2d: + match: true + tokens: + - True: + Query.f|arrays_2d_diff: + match: true + tokens: + - True: + Query.f|arrays_3d_diff: + match: true + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_eq.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_eq.yml new file mode 100644 index 0000000..33b3bb9 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_eq.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + x: Int! + ):Int} + +template: > + query {f( + x: 1 || 2, + )} + +inputs: + Query.f|x: + match: true + tokens: + - Int: 1 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_gr_le.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_gr_le.yml new file mode 100644 index 0000000..ec0e12d --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_logical_or_gr_le.yml @@ -0,0 +1,22 @@ +schema: > + type Query {f( + a: Int! + b: Int! + ):Int} + +template: > + query {f( + a: <10 || >20, + b: <10 || >20, + )} + +inputs: + Query.f|a: + match: true + tokens: + - Int: 5 + Query.f|b: + match: true + tokens: + - Int: 25 + diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_map_2d.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_map_2d.yml new file mode 100644 index 0000000..d72cf51 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_map_2d.yml @@ -0,0 +1,47 @@ +schema: > + type Query { f( + a: [[Int!]!]! + b: [[Int!]!]! + empty: [[Int!]!]! + ):Int } + +template: > + query { f( + a: [...[...>5]], + b: [...[...>5]], + empty: [...[...>5]], + ) } + +inputs: + Query.f|a: + match: true + tokens: + - Arr: + - Arr: + - Int: 6 + - Int: 7 + - Int: 8 + - ArrEnd: + - ArrEnd: + Query.f|b: + match: true + tokens: + - Arr: + + - Arr: + - Int: 10 + - ArrEnd: + + - Arr: + - Int: 12 + - ArrEnd: + + - Arr: + - ArrEnd: + + - ArrEnd: + Query.f|empty: + match: true + tokens: + - Arr: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_map_array_object.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_map_array_object.yml new file mode 100644 index 0000000..a37a0a6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_map_array_object.yml @@ -0,0 +1,32 @@ +schema: > + type Query { f( + a: [I!]! + ):Int } + input I { + x: Float! + y: Float! + } + +template: > + query { f( + a: [ ...{x: > 0, y: > 0} ], + ) } + +inputs: + Query.f|a: + match: true + tokens: + - Arr: + - Obj: + - ObjField: "x" + - Float: 1 + - ObjField: "y" + - Float: 1 + - ObjEnd: + - Obj: + - ObjField: "x" + - Float: 0.1 + - ObjField: "y" + - Float: 4.1 + - ObjEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_map_var_arithmetic.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_map_var_arithmetic.yml new file mode 100644 index 0000000..3b6e8c6 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_map_var_arithmetic.yml @@ -0,0 +1,32 @@ +schema: > + type Query { f( + v:Int! + nonempty:[Int!]! + empty:[Int!]! + ):Int } + +template: > + query { f( + v=$v: *, + nonempty: [... > ($v + 2 * 200 / 10 % 7) - 3], + empty: [... > ($v + 2 * 200 / 10 % 7) - 3], + ) } + +inputs: + Query.f|v: + match: true + tokens: + - Int: 2 + Query.f|nonempty: + match: true + tokens: + - Arr: + - Int: 5 + - Int: 6 + - Int: 7 + - ArrEnd: + Query.f|empty: + match: true + tokens: + - Arr: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_int.yml new file mode 100644 index 0000000..6d8b1e5 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_int.yml @@ -0,0 +1,46 @@ +schema: > + type Query {f( + intsDiff: [Int!]! + intsShort: [Int!]! + intsLong: [Int!]! + intsEmpty: [Int!]! + ):Int} + +template: > + query {f( + intsDiff: != [1,2,3], + intsShort: != [1,2,3], + intsLong: != [1,2,3], + intsEmpty: != [1,2,3], + )} + +inputs: + Query.f|intsDiff: + match: true + tokens: + - Arr: + - Int: 1 + - Int: 2 + - Int: 5 + - ArrEnd: + Query.f|intsShort: + match: true + tokens: + - Arr: + - Int: 1 + - Int: 2 + - ArrEnd: + Query.f|intsLong: + match: true + tokens: + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - Int: 3 + - ArrEnd: + Query.f|intsEmpty: + match: true + tokens: + - Arr: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_str.yml new file mode 100644 index 0000000..d8f3c73 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_arr_str.yml @@ -0,0 +1,46 @@ +schema: > + type Query {f( + strsDiff: [String!]! + strsShort: [String!]! + strsLong: [String!]! + strsEmpty: [String!]! + ):Int} + +template: > + query {f( + strsDiff: != ["1","2","3"], + strsShort: != ["1","2","3"], + strsLong: != ["1","2","3"], + strsEmpty: != ["1","2","3"], + )} + +inputs: + Query.f|strsDiff: + match: true + tokens: + - Arr: + - Str: "1" + - Str: "2" + - Str: "5" + - ArrEnd: + Query.f|strsShort: + match: true + tokens: + - Arr: + - Str: "1" + - Str: "2" + - ArrEnd: + Query.f|strsLong: + match: true + tokens: + - Arr: + - Str: "1" + - Str: "2" + - Str: "3" + - Str: "3" + - ArrEnd: + Query.f|strsEmpty: + match: true + tokens: + - Arr: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_obj.yml new file mode 100644 index 0000000..b748f77 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_not_equal_obj.yml @@ -0,0 +1,35 @@ +schema: > + type Query {f( + a: A! + ):Int} + input A { b: B! } + input B { + s: String! + i: Int! + optional: Boolean + } + +template: > + query {f( + a: != { + b: { + s: "okay", + i: 42, + optional: * + } + } + )} + +inputs: + Query.f|a: + match: true + tokens: # {b:{s:"okay",i:24}} + - Obj: + - ObjField: "b" + - Obj: + - ObjField: "s" + - Str: "okay" + - ObjField: "i" + - Int: 24 + - ObjEnd: + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_relational_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_relational_float.yml new file mode 100644 index 0000000..5622540 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_relational_float.yml @@ -0,0 +1,33 @@ +schema: > + type Query { f( + gr: Float! + le: Float! + greq: Float! + leeq: Float! + ):Int } + +template: > + query { f( + gr: > 1.1, + le: < 2.22, + greq: >= 3.333, + leeq: <= 4.4444, + ) } + +inputs: + Query.f|gr: + match: true + tokens: + - Float: 1.11 # gr + Query.f|le: + match: true + tokens: + - Float: 2.21 # le + Query.f|greq: + match: true + tokens: + - Float: 3.333 # eq + Query.f|leeq: + match: true + tokens: + - Float: 4.4444 # eq diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_relational_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_relational_int.yml new file mode 100644 index 0000000..f49faba --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_relational_int.yml @@ -0,0 +1,33 @@ +schema: > + type Query { f( + gr: Int! + le: Int! + greq: Int! + leeq: Int! + ):Int } + +template: > + query { f( + gr: > 1, + le: < 20, + greq: >= 300, + leeq: <= 4000, + ) } + +inputs: + Query.f|gr: + match: true + tokens: + - Int: 2 + Query.f|le: + match: true + tokens: + - Int: 19 + Query.f|greq: + match: true + tokens: + - Int: 300 + Query.f|leeq: + match: true + tokens: + - Int: 4000 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_union.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_union.yml new file mode 100644 index 0000000..627964a --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_union.yml @@ -0,0 +1,33 @@ +schema: > + type Query { + u: U! + } + union U = Foo | Bar + type Foo { + i(x:Int!):Int! + } + type Bar { + i(x:Int!):Int! + } + +template: > + query { + u { + ... on Foo { + i(x:>20) + } + ... on Bar { + i(x:>30) + } + } + } + +inputs: + Query.u&Foo.i|x: + match: true + tokens: + - Int: 21 + Query.u&Bar.i|x: + match: true + tokens: + - Int: 31 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_var_bool.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_var_bool.yml new file mode 100644 index 0000000..4cf48fb --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_var_bool.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: Boolean! + b: Boolean! + ):Int } + +template: > + query { f( + a = $a: true, + b: (true && true) && $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - True: + Query.f|b: + match: true + tokens: + - True: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_var_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_var_str.yml new file mode 100644 index 0000000..b136895 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_var_str.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: String! + b: String! + ):Int } + +template: > + query { f( + a = $a: "okay", + b: $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - Str: "okay" + Query.f|b: + match: true + tokens: + - Str: "okay" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/true_var_strblock.yml b/pkg/engine/playmon/internal/constrcheck/tests/true_var_strblock.yml new file mode 100644 index 0000000..ec2ebfa --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/true_var_strblock.yml @@ -0,0 +1,21 @@ +schema: > + type Query { f( + a: String! + b: String! + ):Int } + +template: > + query { f( + a = $a: "okay", + b: $a, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - StrBlock: "okay" + Query.f|b: + match: true + tokens: + - StrBlock: "okay" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/var_equal_nonexistent.yml b/pkg/engine/playmon/internal/constrcheck/tests/var_equal_nonexistent.yml new file mode 100644 index 0000000..3dd9b03 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/var_equal_nonexistent.yml @@ -0,0 +1,24 @@ +# Make sure that missing values are treated as `null`. +schema: > + type Query { f( + v: Int + a: Int + b: Int + ):Int } + +template: > + query { f( + v = $v: *, + a: $v, + b: != $v, + ) } + +inputs: + Query.f|a: + match: true + tokens: + - Null: + Query.f|b: + match: true + tokens: + - Int: 42 diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal.yml new file mode 100644 index 0000000..5953b97 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal.yml @@ -0,0 +1,58 @@ +schema: > + type Query {f( + int: Int! + float: Float! + string: String! + bool_t: Boolean! + bool_f: Boolean! + enum: TestEnum! + arrint: [Int!]! + nullable: Int + ):Int} + enum TestEnum { something } + +template: > + query {f( + int: 42, + float: 3.14159, + string: "something", + bool_t: true, + bool_f: false, + enum: something, + arrint: [1,2,3], + nullable: null + )} + +inputs: + Query.f|int: + match: false + tokens: + - Str: "wrong type" + Query.f|float: + match: false + tokens: + - Str: "wrong type" + Query.f|string: + match: false + tokens: + - EnumVal: "something" + Query.f|bool_t: + match: false + tokens: + - Str: "wrong type" + Query.f|bool_f: + match: false + tokens: + - Str: "wrong type" + Query.f|enum: + match: false + tokens: + - Str: "wrong type" + Query.f|arrint: + match: false + tokens: + - Str: "wrong type" + Query.f|nullable: + match: false + tokens: + - Str: "wrong type" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_arrint.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_arrint.yml new file mode 100644 index 0000000..2f0c73e --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_arrint.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + arrint: [Int!]! + ):Int} + +template: > + query {f( + arrint: [1,2,3], + )} + +inputs: + Query.f|arrint: + match: false + tokens: + - Str: "[1,2,3]" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_f.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_f.yml new file mode 100644 index 0000000..0ba67b7 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_f.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + bool_f: Boolean! + ):Int} + +template: > + query {f( + bool_f: false, + )} + +inputs: + Query.f|bool_f: + match: false + tokens: + - Str: "false" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_t.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_t.yml new file mode 100644 index 0000000..b699450 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_bool_t.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + bool_t: Boolean! + ):Int} + +template: > + query {f( + bool_t: true, + )} + +inputs: + Query.f|bool_t: + match: false + tokens: + - Str: "true" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_enum.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_enum.yml new file mode 100644 index 0000000..8b7a880 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_enum.yml @@ -0,0 +1,16 @@ +schema: > + type Query {f( + enum: TestEnum! + ):Int} + enum TestEnum { something } + +template: > + query {f( + enum: something, + )} + +inputs: + Query.f|enum: + match: false + tokens: + - Str: "something" # not an enum value diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_float.yml new file mode 100644 index 0000000..a402ca3 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_float.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + float: Float! + ):Int} + +template: > + query {f( + float: 3.14159, + )} + +inputs: + Query.f|float: + match: false + tokens: + - Str: "3.14159" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_int.yml new file mode 100644 index 0000000..c030f18 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_int.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + int: Int! + ):Int} + +template: > + query {f( + int: 42, + )} + +inputs: + Query.f|int: + match: false + tokens: + - Str: "42" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_missing_field.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_missing_field.yml new file mode 100644 index 0000000..22f17ba --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_missing_field.yml @@ -0,0 +1,22 @@ +schema: > + type Query { f( + in: In + ):Int } + input In { + i: Int! + x: Int! + } + +template: > + query { f( + in: {i: 42, x: 42} + ) } + +inputs: + Query.f|in: + match: false + tokens: + - Obj: + - ObjField: "i" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_null.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_null.yml new file mode 100644 index 0000000..a688ef2 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_null.yml @@ -0,0 +1,15 @@ +schema: > + type Query {f( + nullable: Int + ):Int} + +template: > + query {f( + nullable: null + )} + +inputs: + Query.f|nullable: + match: false + tokens: + - Str: "null" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_str.yml new file mode 100644 index 0000000..4d6b8be --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_equal_str.yml @@ -0,0 +1,16 @@ +schema: > + type Query {f( + string: String! + ):Int} + enum TestEnum { something } + +template: > + query {f( + string: "something", + )} + +inputs: + Query.f|string: + match: false + tokens: + - EnumVal: "something" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_equal_missing_field.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_equal_missing_field.yml new file mode 100644 index 0000000..d1df458 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_equal_missing_field.yml @@ -0,0 +1,22 @@ +schema: > + type Query { f( + in: In + ):Int } + input In { + i: Int! + x: Int! + } + +template: > + query { f( + in: {i: 42, x: 42} + ) } + +inputs: + Query.f|in: + match: false + tokens: # {i:42} + - Obj: + - ObjField: "i" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_not_equal_missing_field.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_not_equal_missing_field.yml new file mode 100644 index 0000000..dfbfab3 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_false_not_equal_missing_field.yml @@ -0,0 +1,22 @@ +schema: > + type Query { f( + in: In + ):Int } + input In { + i: Int! + x: Int! + } + +template: > + query { f( + in: != {i: 42, x: 42} + ) } + +inputs: + Query.f|in: + match: false + tokens: # {i:42} + - Obj: + - ObjField: "i" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_int.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_int.yml new file mode 100644 index 0000000..b291d78 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_int.yml @@ -0,0 +1,51 @@ +schema: > + type Query { f( + eq:[Int!]! + neq:[Int!]! + gr:[Int!]! + le:[Int!]! + greq:[Int!]! + leeq:[Int!]! + ):Int } + enum Color { red } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: false + tokens: + - Int: 42 + Query.f|neq: + match: false + match-schemaless: true + tokens: + - Arr: + - Str: "a" + - Str: "b" + - Str: "c" + - ArrEnd: + Query.f|gr: + match: false + tokens: + - EnumVal: "red" + Query.f|le: + match: false + tokens: + - True: + Query.f|greq: + match: false + tokens: + - False: + Query.f|leeq: + match: false + tokens: + - Null: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_obj.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_obj.yml new file mode 100644 index 0000000..5fefa33 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_arr_obj.yml @@ -0,0 +1,52 @@ +schema: > + type Query { f( + eq:[I!]! + neq:[I!]! + gr:[I!]! + le:[I!]! + greq:[I!]! + leeq:[I!]! + ):Int } + enum Color { red } + input I { x:Int } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: false + tokens: + - Int: 42 + Query.f|neq: + match: false + match-schemaless: true + tokens: + - Arr: + - Str: "a" + - Str: "b" + - Str: "c" + - ArrEnd: + Query.f|gr: + match: false + tokens: + - EnumVal: "red" + Query.f|le: + match: false + tokens: + - True: + Query.f|greq: + match: false + tokens: + - False: + Query.f|leeq: + match: false + tokens: + - Null: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_str.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_str.yml new file mode 100644 index 0000000..513ecbd --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_len_str.yml @@ -0,0 +1,51 @@ +schema: > + type Query { f( + eq:String! + neq:String! + gr:String! + le:String! + greq:String! + leeq:String! + ):Int } + enum Color { red } + +template: > + query { f( + eq: len 0, + neq: len != 0, + gr: len > 1, + le: len < 2, + greq: len >= 3, + leeq: len <= 4, + ) } + +inputs: + Query.f|eq: + match: false + tokens: + - Int: 42 + Query.f|neq: + match: false + match-schemaless: true + tokens: + - Arr: + - Int: 1 + - Int: 2 + - Int: 3 + - ArrEnd: + Query.f|gr: + match: false + tokens: + - EnumVal: "red" + Query.f|le: + match: false + tokens: + - True: + Query.f|greq: + match: false + tokens: + - False: + Query.f|leeq: + match: false + tokens: + - Null: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_2d.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_2d.yml new file mode 100644 index 0000000..a14536d --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_2d.yml @@ -0,0 +1,47 @@ +schema: > + type Query { f( + a: [[Int!]!]! + b: [[Int!]!]! + empty: [[Int!]!]! + ):Int } + +template: > + query { f( + a: [...[...>0]], + b: [...[...>0]], + empty: [...[...>0]], + ) } + +inputs: + Query.f|a: + match: false + tokens: + - Arr: + - Int: 6 + - Int: 7 + - Int: 8 + - ArrEnd: + Query.f|b: + match: false + tokens: + - Arr: + + - Arr: + - Int: 10 + - ArrEnd: + + - Null: + + - Arr: + - ArrEnd: + + - ArrEnd: + Query.f|empty: + match: false + tokens: + - Arr: + - Arr: + - Arr: + - ArrEnd: + - ArrEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_obj_too_many_fields.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_obj_too_many_fields.yml new file mode 100644 index 0000000..2cb6abc --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_map_obj_too_many_fields.yml @@ -0,0 +1,38 @@ +schema: > + type Query { f( + ae: [I!]! + an: [I!]! + ):Int } + input I { + i: Int! + } + +template: > + query { f( + ae: [ ... {i: 42} ], + an: [... != {i: 42} ], + ) } + +inputs: + Query.f|ae: + match: false + tokens: + - Arr: + - Obj: + - ObjField: "i" + - Int: 42 + - ObjField: "inexistent" + - Int: 42 + - ObjEnd: + - ArrEnd: + Query.f|an: + match: false + tokens: + - Arr: + - Obj: + - ObjField: "i" + - Int: 42 + - ObjField: "inexistent" + - Int: 42 + - ObjEnd: + - ArrEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_not_equal.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_not_equal.yml new file mode 100644 index 0000000..9d5eb93 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_not_equal.yml @@ -0,0 +1,59 @@ +schema: > + type Query {f( + int: Int! + float: Float! + str: String! + bool_t: Boolean! + bool_f: Boolean! + enum: TestEnum! + arrint: [Int!]! + nullable: Int + ):Int} + enum TestEnum { something } + +template: > + query {f( + int: != 42, + float: != 3.14159, + str: != "something", + bool_t: != true, + bool_f: != false, + enum: != something, + arrint: != [1,2,3], + nullable: != null, + )} + +inputs: + Query.f|int: + match: false + tokens: + - Str: "wrong type" + Query.f|float: + match: false + tokens: + - Str: "wrong type" + Query.f|str: + match: false + tokens: + - EnumVal: "something" + Query.f|bool_t: + match: false + tokens: + - Str: "wrong type" + Query.f|bool_f: + match: false + tokens: + - Str: "wrong type" + Query.f|enum: + match: false + tokens: + - Str: "wrong type" + Query.f|arrint: + match: false + tokens: + - Str: "wrong type" + Query.f|nullable: + match: false + match-schemaless: true + tokens: + - Str: "wrong type" diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_obj_too_many_inputs.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_obj_too_many_inputs.yml new file mode 100644 index 0000000..d4a6993 --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_obj_too_many_inputs.yml @@ -0,0 +1,34 @@ +schema: > + type Query { f( + ie: I! + in: I! + ):Int } + input I { + i: Int! + } + +template: > + query { f( + ie: {i: 42}, + in: != {i: 42}, + ) } + +inputs: + Query.f|ie: + match: false + tokens: # {i:42,inexistent:42} + - Obj: + - ObjField: "i" + - Int: 42 + - ObjField: "inexistent" + - Int: 42 + - ObjEnd: + Query.f|in: + match: false + tokens: # {i:42,inexistent:42} + - Obj: + - ObjField: "i" + - Int: 42 + - ObjField: "inexistent" + - Int: 42 + - ObjEnd: diff --git a/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_relational_float.yml b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_relational_float.yml new file mode 100644 index 0000000..ba2c04a --- /dev/null +++ b/pkg/engine/playmon/internal/constrcheck/tests/wrong_type_relational_float.yml @@ -0,0 +1,33 @@ +schema: > + type Query { f( + gr: Float! + le: Float! + greq: Float! + leeq: Float! + ):Int } + +template: > + query { f( + gr: > 1.1, + le: < 2.22, + greq: >= 3.333, + leeq: <= 4.4444, + ) } + +inputs: + Query.f|gr: + match: false + tokens: + - Str: "wrong type" + Query.f|le: + match: false + tokens: + - Str: "wrong type" + Query.f|greq: + match: false + tokens: + - Str: "wrong type" + Query.f|leeq: + match: false + tokens: + - Str: "wrong type" diff --git a/pkg/engine/playmon/internal/countval/count_values.go b/pkg/engine/playmon/internal/countval/count_values.go new file mode 100644 index 0000000..700fa32 --- /dev/null +++ b/pkg/engine/playmon/internal/countval/count_values.go @@ -0,0 +1,56 @@ +package countval + +import ( + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/gqlscan" +) + +// Until counts the number of tokens and values +// until term is reached. +// Immediately returns true if any call to fn returned true as well. +func Until(r *tokenreader.Reader, term gqlscan.Token) (values, tokens int) { + for ; !r.EOF() && r.PeekOne().ID != term; tokens++ { + switch r.ReadOne().ID { + case gqlscan.TokenTrue, + gqlscan.TokenFalse, + gqlscan.TokenStr, + gqlscan.TokenStrBlock, + gqlscan.TokenFloat, + gqlscan.TokenInt, + gqlscan.TokenEnumVal, + gqlscan.TokenNull: + values++ + case gqlscan.TokenObj: + tokens++ + SCAN_OBJ: + for levelObj := 1; ; tokens++ { + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break SCAN_OBJ + } + } + } + values++ + case gqlscan.TokenArr: + tokens++ + SCAN_ARR: + for levelArr := 1; ; tokens++ { + switch r.ReadOne().ID { + case gqlscan.TokenArr: + levelArr++ + case gqlscan.TokenArrEnd: + levelArr-- + if levelArr < 1 { + break SCAN_ARR + } + } + } + values++ + } + } + return values, tokens +} diff --git a/pkg/engine/playmon/internal/countval/count_values_test.go b/pkg/engine/playmon/internal/countval/count_values_test.go new file mode 100644 index 0000000..70e88fb --- /dev/null +++ b/pkg/engine/playmon/internal/countval/count_values_test.go @@ -0,0 +1,395 @@ +package countval_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/countval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/require" +) + +func TestCountValuesUntil(t *testing.T) { + for _, tt := range []struct { + Name string + Input *tokenreader.Reader + Term gqlscan.Token + ExpectValues int + ExpectTokens int + }{ + { + Name: "sequence", + Term: gqlscan.TokenArrEnd, + Input: &tokenreader.Reader{ + Main: []gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenFloat, Value: []byte("3.1415")}, + {ID: gqlscan.TokenTrue}, + {ID: gqlscan.TokenFalse}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenStrBlock}, + // {o:{f:[[null]]}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("o")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + // {f:42} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + // [[]] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + + // Term + {ID: gqlscan.TokenArrEnd}, + }, + }, + ExpectValues: 12, + ExpectTokens: 29, + }, + + // { + // Name: "string", + // Input: []Token{ + // // "text" + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + // }, + // }, + // { + // Name: "string block", + // Input: []Token{ + // // """text""" + // {ID: gqlscan.TokenStrBlock}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStrBlock}}, + // }, + // }, + // { + // Name: "float", + // Input: []Token{ + // // 3.14 + // {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + // }, + // }, + // { + // Name: "enum", + // Input: []Token{ + // // red + // {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + // }, + // }, + // { + // Name: "boolean(true)", + // Input: []Token{ + // // true + // {ID: gqlscan.TokenTrue}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenTrue}}, + // }, + // }, + // { + // Name: "boolean(false)", + // Input: []Token{ + // // false + // {ID: gqlscan.TokenFalse}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFalse}}, + // }, + // }, + // { + // Name: "null", + // Input: []Token{ + // {ID: gqlscan.TokenNull}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // }, + // }, + // { + // Name: "object with array inside", + // Input: []Token{ + // // {field:["text"]} + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("field")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("field")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "object nested", + // Input: []Token{ + // // {x:{y:{z:1}}} + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("y")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("z")}, + // {ID: gqlscan.TokenInt, Value: []byte("1")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("y")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("z")}, + // {ID: gqlscan.TokenInt, Value: []byte("1")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "empty array", + // Input: []Token{ + // // [] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{}, + // }, + // { + // Name: "array with 1 int", + // Input: []Token{ + // // [42] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenInt, Value: []byte("42")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + // }, + // }, + // { + // Name: "array with 3 int", + // Input: []Token{ + // // [42, 0, 100500] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenInt, Value: []byte("42")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenInt, Value: []byte("100500")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + // {{ID: gqlscan.TokenInt, Value: []byte("0")}}, + // {{ID: gqlscan.TokenInt, Value: []byte("100500")}}, + // }, + // }, + // { + // Name: "array with 1 string", + // Input: []Token{ + // // ["text"] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + // }, + // }, + // { + // Name: "array with 1 string block", + // Input: []Token{ + // // ["""text"""] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStrBlock}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStrBlock}}, + // }, + // }, + // { + // Name: "array with 1 float", + // Input: []Token{ + // // [3.14] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + // }, + // }, + // { + // Name: "array with 1 enum", + // Input: []Token{ + // // [red] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + // }, + // }, + // { + // Name: "array with 1 boolean(true)", + // Input: []Token{ + // // [true] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenTrue}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenTrue}}, + // }, + // }, + // { + // Name: "array with 1 boolean(false)", + // Input: []Token{ + // // [false] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenFalse}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFalse}}, + // }, + // }, + // { + // Name: "array with 1 null", + // Input: []Token{ + // // [null] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenNull}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // }, + // }, + // { + // Name: "array with 1 object", + // Input: []Token{ + // // [{x:0}] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "3d array string", + // Input: []Token{ + // // [[["1"],["2"]],[["3"]]] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("1")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("2")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("3")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("1")}}, + // {{ID: gqlscan.TokenStr, Value: []byte("2")}}, + // {{ID: gqlscan.TokenStr, Value: []byte("3")}}, + // }, + // }, + // { + // Name: "array with null and nested object with array inside", + // Input: []Token{ + // // [null, {object:{array:["text"]}}] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenNull}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("object")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("array")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("object")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("array")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + } { + t.Run(tt.Name, func(t *testing.T) { + values, tokens := countval.Until(tt.Input, tt.Term) + require.Equal(t, tt.ExpectValues, values) + require.Equal(t, tt.ExpectTokens, tokens) + }) + } +} diff --git a/pkg/engine/playmon/internal/pathinfo/pathinfo.go b/pkg/engine/playmon/internal/pathinfo/pathinfo.go new file mode 100644 index 0000000..677b15f --- /dev/null +++ b/pkg/engine/playmon/internal/pathinfo/pathinfo.go @@ -0,0 +1,17 @@ +package pathinfo + +import "github.com/graph-guard/gqt/v4" + +// Info returns the number of `max` sets expression e is inside +// and the most relevant (parent) `max` set, if any. +func Info(e gqt.Expression) (depth int, parent *gqt.SelectionMax) { + for ; e != nil; e = e.GetParent() { + if s, ok := e.(*gqt.SelectionMax); ok { + if parent == nil { + parent = s + } + depth++ + } + } + return depth, parent +} diff --git a/pkg/engine/playmon/internal/pathinfo/pathinfo_test.go b/pkg/engine/playmon/internal/pathinfo/pathinfo_test.go new file mode 100644 index 0000000..40429d6 --- /dev/null +++ b/pkg/engine/playmon/internal/pathinfo/pathinfo_test.go @@ -0,0 +1,92 @@ +package pathinfo_test + +import ( + "testing" + + maxsets "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathinfo" + "github.com/graph-guard/gqt/v4" + "github.com/stretchr/testify/require" +) + +func TestInfo(t *testing.T) { + opr, _, errs := gqt.Parse([]byte(`query { + maz { + muzz + } + max 1 { + foo(x:>1,y:!="ok") { + max 2 { + bar { + ... on Baz { + baz + } + } + bar2 + bar3 + } + } + foo2 + } + }`)) + require.Nil(t, errs) + { + depth, parent := maxsets.Info(nil) + require.Equal(t, 0, depth) + require.Nil(t, parent) + } + { + depth, parent := maxsets.Info(opr) + require.Equal(t, 0, depth) + require.Nil(t, parent) + } + { + maz := opr.Selections[0].(*gqt.SelectionField) + require.Equal(t, "maz", maz.Name.Name) + + iDepth, iParent := maxsets.Info(maz) + require.Equal(t, 0, iDepth) + require.Nil(t, iParent) + } + { + maz := opr.Selections[0].(*gqt.SelectionField) + muzz := maz.Selections[0].(*gqt.SelectionField) + require.Equal(t, "muzz", muzz.Name.Name) + + iDepth, iParent := maxsets.Info(muzz) + require.Equal(t, 0, iDepth) + require.Nil(t, iParent) + } + { + firstMax := opr.Selections[1].(*gqt.SelectionMax) + foo := firstMax.Options.Selections[0].(*gqt.SelectionField) + require.Equal(t, "foo", foo.Name.Name) + + iDepth, iParent := maxsets.Info(foo) + require.Equal(t, 1, iDepth) + require.Equal(t, 1, iParent.Limit) + } + { + firstMax := opr.Selections[1].(*gqt.SelectionMax) + foo := firstMax.Options.Selections[0].(*gqt.SelectionField) + secondMax := foo.Selections[0].(*gqt.SelectionMax) + bar := secondMax.Options.Selections[0].(*gqt.SelectionField) + require.Equal(t, "bar", bar.Name.Name) + + iDepth, iParent := maxsets.Info(bar) + require.Equal(t, 2, iDepth) + require.Equal(t, 2, iParent.Limit) + } + { + firstMax := opr.Selections[1].(*gqt.SelectionMax) + foo := firstMax.Options.Selections[0].(*gqt.SelectionField) + secondMax := foo.Selections[0].(*gqt.SelectionMax) + bar := secondMax.Options.Selections[0].(*gqt.SelectionField) + onBaz := bar.Selections[0].(*gqt.SelectionInlineFrag) + baz := onBaz.Selections[0].(*gqt.SelectionField) + require.Equal(t, "baz", baz.Name.Name) + + iDepth, iParent := maxsets.Info(baz) + require.Equal(t, 2, iDepth) + require.Equal(t, 2, iParent.Limit) + } +} diff --git a/pkg/engine/playmon/internal/pathmatch/bench_test.go b/pkg/engine/playmon/internal/pathmatch/bench_test.go new file mode 100644 index 0000000..a9171f5 --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/bench_test.go @@ -0,0 +1,33 @@ +package pathmatch_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" +) + +var GI int + +func BenchmarkPathmatch(b *testing.B) { + for _, bb := range tests { + b.Run(bb.name, func(b *testing.B) { + m := prepareTestSetup(b, bb.conf) + for _, bb := range bb.tests { + b.Run(bb.name, func(b *testing.B) { + paths := make([]uint64, len(bb.paths)) + for i := range bb.paths { + paths[i] = pathscan.Hash(bb.paths[i]) + } + b.ResetTimer() + for n := 0; n < b.N; n++ { + m.Match(paths, func(tm *config.Template) (stop bool) { + GI++ + return false + }) + } + }) + } + }) + } +} diff --git a/pkg/engine/playmon/internal/pathmatch/pathmatch.go b/pkg/engine/playmon/internal/pathmatch/pathmatch.go new file mode 100644 index 0000000..12138ce --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/pathmatch.go @@ -0,0 +1,187 @@ +package pathmatch + +import ( + "github.com/graph-guard/ggproxy/pkg/bitmask" + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathinfo" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" + "github.com/graph-guard/gqt/v4" +) + +type structuralPath struct { + Name string + Mask *bitmask.Set + Combinators []combination +} + +type combination struct { + Index int + Depth int + TemplateIndex int +} + +type template struct { + *config.Template + paths []uint64 +} + +type Matcher struct { + conf *config.Service + templates []template + paths map[uint64]structuralPath + + // combinatorsLimits defines the limit of the `max` set of every combination. + combinatorsLimits []int + + lengths []int + + /* Operational data, reset for every match call */ + + // combinatorCounters keeps the counters for every combination. + combinatorCounters []int + + matchMask, rejectedMask *bitmask.Set + matchesPerTemplate []int +} + +func (m *Matcher) reset() { + m.matchMask.Reset() + m.rejectedMask.Reset() + for i := range m.matchesPerTemplate { + m.matchesPerTemplate[i] = 0 + } + for i := range m.combinatorCounters { + m.combinatorCounters[i] = 0 + } +} + +// Match calls onMatch for every template matching paths. +func (m *Matcher) Match( + paths []uint64, + onMatch func(*config.Template) (stop bool), +) { + m.reset() + + for i := range paths { + b, ok := m.paths[paths[i]] + if !ok { + return // Unknown path, can't match any template + } + + for _, c := range b.Combinators { + depth := 0 + if m.combinatorCounters[c.Index] < 1 { + depth = c.Depth + } + for i := c.Index - depth; i <= c.Index; i++ { + m.combinatorCounters[i]++ + if m.combinatorsLimits[i] < m.combinatorCounters[i] { + m.rejectedMask.Add(c.TemplateIndex) + } + } + } + + b.Mask.VisitAll(func(n int) { m.matchesPerTemplate[n]++ }) + m.matchMask.SetOr(m.matchMask, b.Mask) + } + for i := range m.matchesPerTemplate { + if m.matchesPerTemplate[i] < len(paths) { + m.rejectedMask.Add(i) + } + } + m.matchMask.SetAndNot(m.matchMask, m.rejectedMask) + m.matchMask.Visit(func(n int) (stop bool) { + return onMatch(m.conf.TemplatesEnabled[n]) + }) +} + +func New(conf *config.Service) *Matcher { + m := &Matcher{ + conf: conf, + paths: make(map[uint64]structuralPath, len(conf.TemplatesEnabled)), + templates: make([]template, len(conf.TemplatesEnabled)), + lengths: make([]int, len(conf.TemplatesEnabled)), + + matchMask: bitmask.New(), + rejectedMask: bitmask.New(), + matchesPerTemplate: make([]int, len(conf.TemplatesEnabled)), + } + var maxCombinators []*gqt.SelectionMax + for i := range conf.TemplatesEnabled { + if errs := pathscan.InAST( + conf.TemplatesEnabled[i].GQTTemplate, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On structural + m.templates[i].paths = append(m.templates[i].paths, pathHash) + + var v structuralPath + var ok bool + if v, ok = m.paths[pathHash]; !ok { + v.Mask = bitmask.New() + v.Name = path + m.paths[pathHash] = v + } + + if depth, parentMax := pathinfo.Info(e); depth > 0 { + index := indexOf(maxCombinators, parentMax) + if index < 0 { + index = len(maxCombinators) + maxCombinators = append(maxCombinators, parentMax) + m.combinatorsLimits = append( + m.combinatorsLimits, parentMax.Limit, + ) + m.combinatorCounters = append(m.combinatorCounters, 0) + } + + // Register combinator for paths inside `max` sets + v.Combinators = append(v.Combinators, combination{ + Index: index, + Depth: depth - 1, + TemplateIndex: i, + }) + m.paths[pathHash] = v + } + return false + }, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On argument + return false + }, + func( + path string, + pathHash uint64, + e *gqt.VariableDeclaration, + ) (stop bool) { + // On variable + return false + }, + ); errs != nil { + panic(errs) + } + + // Initialize path bitmasks + for _, p := range m.templates[i].paths { + m.paths[p].Mask.Add(i) + } + m.lengths[i] = len(m.templates[i].paths) + } + + return m +} + +func indexOf[T comparable](s []T, x T) (index int) { + for i := range s { + if s[i] == x { + return i + } + } + return -1 +} diff --git a/pkg/engine/playmon/internal/pathmatch/pathmatch_test.go b/pkg/engine/playmon/internal/pathmatch/pathmatch_test.go new file mode 100644 index 0000000..a4e2e6d --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/pathmatch_test.go @@ -0,0 +1,324 @@ +package pathmatch_test + +import ( + "embed" + "testing" + + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathmatch" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" + "github.com/graph-guard/gqt/v4" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := prepareTestSetup(t, tt.conf) + for _, tt := range tt.tests { + t.Run(tt.name, func(t *testing.T) { + var actualMatches []string + paths := make([]uint64, len(tt.paths)) + for i := range tt.paths { + paths[i] = pathscan.Hash(tt.paths[i]) + } + m.Match(paths, func(tm *config.Template) (stop bool) { + actualMatches = append(actualMatches, tm.ID) + return false + }) + require.Equal(t, tt.expectIDs, actualMatches) + }) + } + }) + } +} + +//go:embed test_setups +var embeddedTestSetups embed.FS + +type test struct { + name string + paths []string + expectIDs []string +} + +var tests = []struct { + name string + conf *config.Service + tests []test +}{ + { + name: "no paths", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{{ + ID: "A", Source: []byte(`query { foo }`), + }}, + }, + tests: []test{{ + paths: nil, + expectIDs: nil, + }}, + }, + { + name: "unknown path", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{{ + ID: "A", Source: []byte(`query { foo }`), + }}, + }, + tests: []test{{ + paths: []string{"Q.bar"}, + expectIDs: nil, + }}, + }, + { + name: "1_of_1", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{{ + ID: "A", Source: []byte(`query { foo }`), + }}, + }, + tests: []test{{ + paths: []string{"Q.foo"}, + expectIDs: []string{"A"}, + }}, + }, + { + name: "first_of_2", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { foo }`)}, + {ID: "B", Source: []byte(`query { bar }`)}, + }, + }, + tests: []test{{ + paths: []string{"Q.foo"}, + expectIDs: []string{"A"}, + }}, + }, + { + name: "second_of_2", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { foo }`)}, + {ID: "B", Source: []byte(`query { bar }`)}, + }, + }, + tests: []test{{ + paths: []string{"Q.bar"}, + expectIDs: []string{"B"}, + }}, + }, + { + name: "not_both", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { foo }`)}, + {ID: "B", Source: []byte(`query { bar }`)}, + }, + }, + tests: []test{{ + paths: []string{"Q.foo", "Q.bar"}, + expectIDs: nil, + }}, + }, + { + name: "1_of_3", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { foo bazz }`)}, + {ID: "B", Source: []byte(`query { bar bazz }`)}, + }, + }, + tests: []test{{ + paths: []string{"Q.bazz", "Q.bar"}, + expectIDs: []string{"B"}, + }}, + }, + { + name: "3_of_3", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { foo bazz }`)}, + {ID: "B", Source: []byte(`query { bar bazz }`)}, + {ID: "C", Source: []byte(`query { bazz }`)}, + }, + }, + tests: []test{{ + paths: []string{"Q.bazz"}, + expectIDs: []string{"A", "B", "C"}, + }}, + }, + { + name: "starwars", + conf: func() *config.Service { + s, err := config.Read( + embeddedTestSetups, + "test_setups/starwars/", + "test_setups/starwars/config.yml", + ) + if err != nil { + panic(err) + } + return s.ServicesEnabled[0] + }(), + /* query { hero(episode: EMPIRE || JEDI) { + id, name, appearsIn + friends { + id, name, appearsIn + friends { id, name, appearsIn } + } + friendsConnection(first: >= 0, after: len > 0) { + totalCount + friends { id, name, appearsIn } + } + } } */ + tests: []test{{ + paths: []string{ + "Q.hero|episode,.id", + "Q.hero|episode,.name", + "Q.hero|episode,.appearsIn", + "Q.hero|episode,.friends.id", + "Q.hero|episode,.friends.name", + "Q.hero|episode,.friends.appearsIn", + "Q.hero|episode,.friends.friends.id", + "Q.hero|episode,.friends.friends.name", + "Q.hero|episode,.friends.friends.appearsIn", + "Q.hero|episode,.friendsConnection|after,first,.totalCount", + "Q.hero|episode,.friendsConnection|after,first,.friends.id", + "Q.hero|episode,.friendsConnection|after,first,.friends.name", + "Q.hero|episode,.friendsConnection|after,first,.friends.appearsIn", + }, + expectIDs: []string{"c"}, + }}, + }, + { + name: "max1_1_template", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { + max 1 { a b c } + } + `)}, + }, + }, + tests: []test{{ + name: "field_a", + paths: []string{"Q.a"}, + expectIDs: []string{"A"}, + }, { + name: "field_b", + paths: []string{"Q.b"}, + expectIDs: []string{"A"}, + }, { + name: "field_c", + paths: []string{"Q.c"}, + expectIDs: []string{"A"}, + }, { + name: "violate_c_a", + paths: []string{"Q.c", "Q.a"}, + expectIDs: nil, + }, { + name: "violate_b_a", + paths: []string{"Q.b", "Q.a"}, + expectIDs: nil, + }, { + name: "violate_a_c", + paths: []string{"Q.a", "Q.c"}, + expectIDs: nil, + }, { + name: "violate_a_b_c", + paths: []string{"Q.a", "Q.b", "Q.c"}, + expectIDs: nil, + }}, + }, + { + name: "max_2_multitemplates", + conf: &config.Service{ + TemplatesEnabled: []*config.Template{ + {ID: "A", Source: []byte(`query { + max 1 { + a { + max 1 { a0 a1 } + } + b + } + } + `)}, + {ID: "B", Source: []byte(`query { + max 2 { + b + a { + max 2 { a0 a1 a2 } + } + c + } + } + `)}, + }, + }, + tests: []test{{ + name: "field_b", + paths: []string{"Q.b"}, + expectIDs: []string{"A", "B"}, + }, { + name: "field_c", + paths: []string{"Q.c"}, + expectIDs: []string{"B"}, + }, { + name: "inexistent_path", + paths: []string{"Q.d"}, + expectIDs: nil, + }, { + name: "violate_A_max", + paths: []string{ + "Q.a.a0", + "Q.b", + }, + expectIDs: []string{"B"}, + }, { + name: "violate_A", + paths: []string{ + "Q.a.a1", + "Q.a.a0", + }, + expectIDs: []string{"B"}, + }, { + name: "violate_A_all_levels", + paths: []string{ + "Q.b", + "Q.a.a1", + "Q.a.a0", + }, + expectIDs: []string{"B"}, + }, { + name: "a1", + paths: []string{ + "Q.a.a1", + }, + expectIDs: []string{"A", "B"}, + }, { + name: "a2", + paths: []string{ + "Q.a.a2", + }, + expectIDs: []string{"B"}, + }}, + }, +} + +func prepareTestSetup(t testing.TB, c *config.Service) *pathmatch.Matcher { + p, err := gqt.NewParser(nil) + if err != nil { + t.Fatalf("initializing gqt parser: %v", err) + } + for _, tmpl := range c.TemplatesEnabled { + opr, _, errs := p.Parse(tmpl.Source) + if errs != nil { + t.Fatalf("parsing template: %v", errs) + } + tmpl.GQTTemplate, tmpl.Enabled = opr, true + } + m := pathmatch.New(c) + return m +} diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/config.yml b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/config.yml new file mode 100644 index 0000000..f5f9715 --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/config.yml @@ -0,0 +1,13 @@ +proxy: + # Address and port of the proxy server. + host: localhost:8000 + # Optional, in bytes, default: 4MiB. + max-request-body-size: 4096 + +# Optional, enables API server. +api: + # Address and port of the API server. + host: localhost:3000 + +all-services: services_enabled +enabled-services: services_enabled diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars.yml b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars.yml new file mode 100644 index 0000000..2f8f239 --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars.yml @@ -0,0 +1,13 @@ +# The service's display name +name: "Starwars" +# Source URL path +path: "/starwars" +# Destination URL (where to proxy requests to) +forward-url: "http://localhost:8080/starwars" + +# false for forwarding the original request, +# true for the reduced version. +forward-reduced: true + +all-templates: starwars/templates_enabled +enabled-templates: starwars/templates_enabled diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/a.gqt b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/a.gqt new file mode 100644 index 0000000..dcdbdfa --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/a.gqt @@ -0,0 +1,13 @@ +--- +name: "A" +--- +query { + hero(episode: *) { + id + name + friends { + id + name + } + } +} diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/b.gqt b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/b.gqt new file mode 100644 index 0000000..47fefd9 --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/b.gqt @@ -0,0 +1,20 @@ +--- +name: "B" +--- +query { + hero(episode: EMPIRE || JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + } +} diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/c.gqt b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/c.gqt new file mode 100644 index 0000000..6cbb20e --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/c.gqt @@ -0,0 +1,28 @@ +--- +name: "C" +--- +query { + hero(episode: EMPIRE || JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + friendsConnection(first: >= 0, after: len > 0) { + totalCount + friends { + id + name + appearsIn + } + } + } +} diff --git a/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/d.gqt b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/d.gqt new file mode 100644 index 0000000..f5aafe8 --- /dev/null +++ b/pkg/engine/playmon/internal/pathmatch/test_setups/starwars/services_enabled/starwars/templates_enabled/d.gqt @@ -0,0 +1,20 @@ +--- +name: "D" +--- +query { + hero(episode: EMPIRE || JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + reviews(episode: EMPIRE || JEDI) { + stars + commentary + time + } +} diff --git a/pkg/engine/playmon/internal/pathscan/pathscan.go b/pkg/engine/playmon/internal/pathscan/pathscan.go new file mode 100644 index 0000000..6763bca --- /dev/null +++ b/pkg/engine/playmon/internal/pathscan/pathscan.go @@ -0,0 +1,405 @@ +// Package pathscan provides functions for extraction of paths +// from token slices and GQT ASTs. Paths address structural leaf nodes and +// variable values in a GraphQL operation. +// Query operations always begin with "Q", mutation operations always begin +// with "M" and subscription operations always begin with "S". +// +// Consider the following example: +// +// query { +// foo { +// bar { +// burr(x: != $i2) +// } +// baz { +// buzz(b:5, a:null, c=$c:true) +// ... on Kraz { +// fraz +// graz(argument:{i:$c,i2=$i2:"bar"}) { +// lum +// } +// } +// } +// } +// mazz +// } +// +// The above query operation contains 5 structural leafs with +// the following paths: +// +// - Q.foo.bar.burr|x +// - Q.foo.baz.buzz|a,b,c +// - Q.foo.baz&Kraz.fraz +// - Q.foo.baz&Kraz.graz|argument.lum +// - Q.mazz +package pathscan + +import ( + "bytes" + "fmt" + "sort" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/travgqt" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/stack" + "github.com/graph-guard/ggproxy/pkg/xxhash" + gqlscan "github.com/graph-guard/gqlscan" + gqt "github.com/graph-guard/gqt/v4" + + "golang.org/x/exp/slices" +) + +// PathScanner is reset in every call to InTokens +type PathScanner struct { + valPathBuf []byte + valStack stack.Stack[int] + argBuf [][]byte + structuralPathBuf []byte + structuralStack stack.Stack[int] +} + +func New(preallocateStack, preallocatePathBuffer int) *PathScanner { + return &PathScanner{ + structuralPathBuf: make([]byte, 0, preallocatePathBuffer), + structuralStack: stack.New[int](preallocateStack), + } +} + +// Magic identifier and divider bytes +const ( + initQuery = 'Q' + initMutation = 'M' + initSubscription = 'S' + divSel = '.' + divArgList = '|' + divArg = ',' + divTypeCond = '&' + divObjField = '/' +) + +// InTokens calls onStructural for every encountered structural path, +// onArg for every argument and onGQTVarVal for every encountered +// GQT variable value. +// operation is expected to be an operation initialization token and +// is used to determine whether the path should start with +// "Q" (query), "M" (mutation) or "S" (subscription). +// InTokens will panic if operation contains any other token. +// gqtVarPaths provides a set of all known GQT variable paths. +// +// WARNING: Aliasing provided paths and using them after +// onStructural or onVariable return may cause data corruption +// because path refers to an internal buffer of PathScanner! +func (s *PathScanner) InTokens( + operation gqlscan.Token, + tokens []gqlparse.Token, + gqtVarPaths map[uint64][]gqlparse.Token, + onStructural func(pathHash uint64) (stop bool), + onArg, onGQTVarVal func(pathHash uint64, i int) (stop bool), +) { + s.valPathBuf = s.valPathBuf[:0] + s.valStack.Reset() + s.structuralPathBuf = s.structuralPathBuf[:0] + s.structuralStack.Reset() + + switch operation { + case gqlscan.TokenDefQry: + s.valPathBuf = append(s.valPathBuf, initQuery) + s.structuralPathBuf = append(s.structuralPathBuf, initQuery) + case gqlscan.TokenDefMut: + s.valPathBuf = append(s.valPathBuf, initMutation) + s.structuralPathBuf = append(s.structuralPathBuf, initMutation) + case gqlscan.TokenDefSub: + s.valPathBuf = append(s.valPathBuf, initSubscription) + s.structuralPathBuf = append(s.structuralPathBuf, initSubscription) + default: + panic(fmt.Errorf("unexpected operation: %v", operation)) + } + for i, level := 0, 0; i < len(tokens); i++ { + switch tokens[i].ID { + case gqlscan.TokenSet: + level++ + case gqlscan.TokenSetEnd: + level-- + s.structuralPop() + s.valPop() + case gqlscan.TokenFragInline: + if level <= s.structuralStack.Len() { + s.structuralPop() + s.valPop() + } + s.structuralWithDiv(divTypeCond, tokens[i].Value) + s.valWithDiv(divTypeCond, tokens[i].Value) + case gqlscan.TokenField: + if level <= s.structuralStack.Len() { + s.structuralPop() + s.valPop() + } + s.valWithDiv(divSel, tokens[i].Value) + s.structuralPathBuf = append(s.structuralPathBuf, divSel) + s.structuralPathBuf = append(s.structuralPathBuf, tokens[i].Value...) + l := len(tokens[i].Value) + 1 + + switch tokens[i+1].ID { + case gqlscan.TokenArgList: + s.argBuf = s.argBuf[:0] + // Collect and sort arguments + level++ + for i += 2; tokens[i].ID != gqlscan.TokenArgListEnd; i++ { + switch tokens[i].ID { + case gqlscan.TokenArgName: + if level <= s.valStack.Len() { + s.valPop() + } + s.argBuf = append(s.argBuf, tokens[i].Value) + s.valWithDiv(divArgList, tokens[i].Value) + h := Hash(s.valPathBuf) + onArg(h, i) + if _, ok := gqtVarPaths[h]; ok { + if onGQTVarVal(h, i+1) { + return + } + } + case gqlscan.TokenObj: + if tokens[i+1].ID == gqlscan.TokenObjEnd { + // Empty object + i++ + break + } + level++ + case gqlscan.TokenObjEnd: + level-- + s.valPop() + case gqlscan.TokenObjField: + if level <= s.valStack.Len() { + s.valPop() + } + s.valWithDiv(divObjField, tokens[i].Value) + h := Hash(s.valPathBuf) + if _, ok := gqtVarPaths[h]; ok { + if onGQTVarVal(h, i+1) { + return + } + } + } + } + level-- + s.valPop() // Pop last argument + slices.SortFunc(s.argBuf, func(i, j []byte) bool { + return bytes.Compare(i, j) < 0 + }) + + // Write args to path + s.structuralPathBuf = append(s.structuralPathBuf, divArgList) + l++ + for i := range s.argBuf { + s.structuralPathBuf = append(s.structuralPathBuf, s.argBuf[i]...) + s.structuralPathBuf = append(s.structuralPathBuf, divArg) + l += len(s.argBuf[i]) + 1 + } + s.structuralStack.Push(l) + + // Check for leaf + if tokens[i+1].ID != gqlscan.TokenSet { + if onStructural(Hash(s.structuralPathBuf)) { + return + } + s.structuralPop() + s.valPop() + continue + } + case gqlscan.TokenSet: + s.structuralStack.Push(l) + default: + s.structuralStack.Push(l) + if onStructural(Hash(s.structuralPathBuf)) { + return + } + s.structuralPop() + s.valPop() + } + } + } +} + +func (s *PathScanner) valWithDiv(div byte, element []byte) { + s.valStack.Push(1 + len(element)) + s.valPathBuf = append(s.valPathBuf, div) + s.valPathBuf = append(s.valPathBuf, element...) +} + +func (s *PathScanner) valPop() { + t := s.valStack.Top() + s.valPathBuf = s.valPathBuf[:len(s.valPathBuf)-t] + s.valStack.Pop() +} + +func (s *PathScanner) structuralWithDiv(div byte, element []byte) { + s.structuralStack.Push(1 + len(element)) + s.structuralPathBuf = append(s.structuralPathBuf, div) + s.structuralPathBuf = append(s.structuralPathBuf, element...) +} + +func (s *PathScanner) structuralPop() { + t := s.structuralStack.Top() + s.structuralPathBuf = s.structuralPathBuf[:len(s.structuralPathBuf)-t] + s.structuralStack.Pop() +} + +// InAST calls onStructural for every structural path that can be used for +// (sub)matching. onVariable is called for every path to an argument or an +// object field that has a variable associated. onArg is called for every +// argument. +func InAST( + o *gqt.Operation, + onStructural func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool), + onArg func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool), + onVariable func( + path string, + pathHash uint64, + e *gqt.VariableDeclaration, + ) (stop bool), +) (errs []error) { + hashes := map[uint64]string{} + hash := func(path []byte) uint64 { + h := Hash(path) + s := string(path) + if x, ok := hashes[h]; ok { + if x != s { + errs = append(errs, fmt.Errorf( + "hash collision between %q and %q (%d)", + x, s, h, + )) + } + } + hashes[h] = s + return h + } + + travgqt.Traverse(o, func(e gqt.Expression) (stop, skipChildren bool) { + switch e := e.(type) { + case *gqt.SelectionField: + for _, a := range e.Arguments { + p := makePathVar(a) + if onArg(string(p), hash(p), a) { + return true, true + } + if a.AssociatedVariable != nil { + p := makePathVar(a) + if onVariable(string(p), hash(p), a.AssociatedVariable) { + return true, true + } + } + } + if len(e.Selections) > 0 { + break + } + p := makePathStructural(e) + hashes[hash(p)] = string(p) + if onStructural(string(p), hash(p), e) { + return true, true + } + case *gqt.ObjectField: + if e.AssociatedVariable != nil { + p := makePathVar(e) + if onVariable(string(p), hash(p), e.AssociatedVariable) { + return true, true + } + } + } + return false, false // Continue traversal + }) + return errs +} + +// makePathStructural generates a structural path for the given expression. +func makePathStructural(e *gqt.SelectionField) []byte { + var p []gqt.Expression // Reversed path + for e := gqt.Expression(e); e != nil; e = e.GetParent() { + p = append(p, e) + } + var s bytes.Buffer + for i := len(p) - 1; i >= 0; i-- { + switch v := p[i].(type) { + case *gqt.SelectionInlineFrag: + _ = s.WriteByte('&') + _, _ = s.WriteString(v.TypeCondition.TypeName) + case *gqt.SelectionField: + _ = s.WriteByte('.') + _, _ = s.WriteString(v.Name.Name) + if len(v.Arguments) > 0 { + _ = s.WriteByte('|') + argNames := make([]string, len(v.Arguments)) + for i := range v.Arguments { + argNames[i] = v.Arguments[i].Name.Name + } + sort.Strings(argNames) + for i := range argNames { + _, _ = s.WriteString(argNames[i]) + _ = s.WriteByte(divArg) + } + } + case *gqt.Operation: + switch v.Type { + case gqt.OperationTypeQuery: + _, _ = s.WriteString("Q") + case gqt.OperationTypeMutation: + _, _ = s.WriteString("M") + case gqt.OperationTypeSubscription: + _, _ = s.WriteString("S") + default: + panic(fmt.Errorf("unknown operation type: %d", v.Type)) + } + } + } + return s.Bytes() +} + +func makePathVar(e gqt.Expression) []byte { + var p []gqt.Expression // Reversed path + for e := e; e != nil; e = e.GetParent() { + p = append(p, e) + } + var s bytes.Buffer + for i := len(p) - 1; i >= 0; i-- { + switch v := p[i].(type) { + case *gqt.SelectionInlineFrag: + _ = s.WriteByte(divTypeCond) + _, _ = s.WriteString(v.TypeCondition.TypeName) + case *gqt.SelectionField: + _ = s.WriteByte(divSel) + _, _ = s.WriteString(v.Name.Name) + case *gqt.Argument: + _ = s.WriteByte(divArgList) + _, _ = s.WriteString(v.Name.Name) + case *gqt.ObjectField: + _ = s.WriteByte(divObjField) + _, _ = s.WriteString(v.Name.Name) + case *gqt.Operation: + switch v.Type { + case gqt.OperationTypeQuery: + _ = s.WriteByte(initQuery) + case gqt.OperationTypeMutation: + _ = s.WriteByte(initMutation) + case gqt.OperationTypeSubscription: + _ = s.WriteByte(initSubscription) + default: + panic(fmt.Errorf("unknown operation type: %d", v.Type)) + } + } + } + return s.Bytes() +} + +func Hash[B []byte | string](b B) uint64 { + h := xxhash.New(0) + xxhash.Write(&h, b) + return h.Sum64() +} diff --git a/pkg/engine/playmon/internal/pathscan/pathscan_test.go b/pkg/engine/playmon/internal/pathscan/pathscan_test.go new file mode 100644 index 0000000..0f6d404 --- /dev/null +++ b/pkg/engine/playmon/internal/pathscan/pathscan_test.go @@ -0,0 +1,644 @@ +package pathscan_test + +import ( + "sort" + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/graph-guard/gqt/v4" + "github.com/stretchr/testify/require" + "golang.org/x/exp/constraints" + "golang.org/x/exp/slices" +) + +var testsInTokens = []struct { + Name string + GraphQLOperation string + VariablePaths []string + ExpectedStructural []string + ExpectedVarVal map[string]int // path->index + ExpectedArg map[string]int // path->index +}{ + { + Name: "query_selections_noargs", + GraphQLOperation: `query { + foo { foo2 } + bar { bar2 bar3 bar4 } + bazz + fuzz + maz { kraz { glaz { traz } } } + jazz + }`, + ExpectedStructural: []string{ + "Q.foo.foo2", + "Q.bar.bar2", + "Q.bar.bar3", + "Q.bar.bar4", + "Q.bazz", + "Q.fuzz", + "Q.maz.kraz.glaz.traz", + "Q.jazz", + }, + }, + { + Name: "query_type_conditions", + GraphQLOperation: `query { + u { + ... on Foo { + foo foo2 + ... on Far { + far + far2 + } + } + ... on Bar { bar bar2 } + } + }`, + ExpectedStructural: []string{ + "Q.u&Foo.foo", + "Q.u&Foo.foo2", + "Q.u&Foo&Far.far", + "Q.u&Foo&Far.far2", + "Q.u&Bar.bar", + "Q.u&Bar.bar2", + }, + }, + { + Name: "subscription", + GraphQLOperation: `subscription { s }`, + ExpectedStructural: []string{"S.s"}, + }, + { + Name: "mutation", + GraphQLOperation: `mutation { m }`, + ExpectedStructural: []string{"M.m"}, + }, + { + Name: "args", + GraphQLOperation: `query{ + titles(options:{lang: DE}) + entities(filter:["filter","this","out"]) + }`, + VariablePaths: []string{ + "Q.titles|options", + "Q.entities|filter", + }, + ExpectedStructural: []string{ + "Q.titles|options,", + "Q.entities|filter,", + }, + ExpectedArg: map[string]int{ + "Q.titles|options": 3, + "Q.entities|filter": 11, + }, + ExpectedVarVal: map[string]int{ + "Q.titles|options": 4, + "Q.entities|filter": 12, + }, + }, + { + Name: "args_complex", + GraphQLOperation: `mutation($variable:Int! = 42){ + foo(i:42, t:true, f:false, n:null) + maz(var:$variable) { + kraz(x:"""text""") { + fraz(x:"more text") { + graz + } + } + } + bazz(object:{array:[1,2,3,null], enum: ENUM_VALUE}) + bar(strings:["array","of","strings"]) + }`, + VariablePaths: []string{ + "M.foo|i", + "M.foo|t", + "M.foo|f", + "M.foo|n", + "M.maz|var", + "M.maz.kraz|x", + "M.maz.kraz.fraz|x", + "M.bazz|object", + "M.bar|strings", + }, + ExpectedStructural: []string{ + "M.foo|f,i,n,t,", + "M.maz|var,.kraz|x,.fraz|x,.graz", + "M.bazz|object,", + "M.bar|strings,", + }, + ExpectedArg: map[string]int{ + "M.foo|i": 3, + "M.foo|t": 5, + "M.foo|f": 7, + "M.foo|n": 9, + "M.maz|var": 14, + "M.maz.kraz|x": 20, + "M.maz.kraz.fraz|x": 26, + "M.bazz|object": 36, + "M.bar|strings": 51, + }, + ExpectedVarVal: map[string]int{ + "M.foo|i": 4, + "M.foo|t": 6, + "M.foo|f": 8, + "M.foo|n": 10, + "M.maz|var": 15, + "M.maz.kraz|x": 21, + "M.maz.kraz.fraz|x": 27, + "M.bazz|object": 37, + "M.bar|strings": 52, + }, + }, + { + Name: "query_complex", + GraphQLOperation: `query { + foo { + bar { + burr(x:4) + } + baz { + ... on Kraz { + fraz + graz(argument:{i:"foo",i2:"bar"}) { + lum + klum + } + } + buzz(b:5, a:null, c:true) + brazz(b:5, a:null, c:true) + ... on Guz { + guz + guzz { + blaz + } + } + } + } + mazz + laz(x:ENUM_VALUE) + }`, + ExpectedStructural: []string{ + "Q.foo.bar.burr|x,", + "Q.foo.baz&Kraz.fraz", + "Q.foo.baz&Kraz.graz|argument,.lum", + "Q.foo.baz&Kraz.graz|argument,.klum", + "Q.foo.baz.buzz|a,b,c,", + "Q.foo.baz.brazz|a,b,c,", + "Q.foo.baz&Guz.guz", + "Q.foo.baz&Guz.guzz.blaz", + "Q.mazz", + "Q.laz|x,", + }, + ExpectedArg: map[string]int{ + "Q.foo.bar.burr|x": 7, + "Q.foo.baz&Kraz.graz|argument": 18, + "Q.foo.baz.buzz|b": 33, + "Q.foo.baz.buzz|a": 35, + "Q.foo.baz.buzz|c": 37, + "Q.foo.baz.brazz|b": 42, + "Q.foo.baz.brazz|a": 44, + "Q.foo.baz.brazz|c": 46, + "Q.laz|x": 62, + }, + }, + { + Name: "query_gqtvar_object", + GraphQLOperation: `query { + f(obj:{foo:{bar:1,baz:2}, fraz:3}) + }`, + VariablePaths: []string{ + "Q.f|obj", + "Q.f|obj/foo", + "Q.f|obj/foo/bar", + "Q.f|obj/foo/baz", + "Q.f|obj/fraz", + }, + ExpectedStructural: []string{ + "Q.f|obj,", + }, + ExpectedArg: map[string]int{ + "Q.f|obj": 3, + }, + ExpectedVarVal: map[string]int{ + "Q.f|obj": 4, + "Q.f|obj/foo": 6, + "Q.f|obj/foo/bar": 8, + "Q.f|obj/foo/baz": 10, + "Q.f|obj/fraz": 13, + }, + }, + { + Name: "query_gqtvar_object_partialvars", + GraphQLOperation: `query { + f(obj:{foo:{bar:1,baz:2}, fraz:3}) + }`, + VariablePaths: []string{ + "Q.f|obj/foo/baz", + "Q.f|obj/fraz", + }, + ExpectedStructural: []string{ + "Q.f|obj,", + }, + ExpectedArg: map[string]int{ + "Q.f|obj": 3, + }, + ExpectedVarVal: map[string]int{ + "Q.f|obj/foo/baz": 10, + "Q.f|obj/fraz": 13, + }, + }, +} + +func TestInTokens(t *testing.T) { + for _, tt := range testsInTokens { + t.Run(tt.Name, func(t *testing.T) { + ps := pathscan.New(0, 0) + p := gqlparse.NewParser(nil) + actualStructuralPaths := []uint64{} + actualArgPaths := make(map[uint64]int) + actualVarPaths := make(map[uint64]int) + variablePaths := make(map[uint64][]gqlparse.Token, len(tt.VariablePaths)) + for _, p := range tt.VariablePaths { + variablePaths[pathscan.Hash(p)] = nil + } + p.Parse( + []byte(tt.GraphQLOperation), nil, nil, + func( + varValues [][]gqlparse.Token, + operation, selectionSet []gqlparse.Token, + ) { + ps.InTokens( + operation[0].ID, + selectionSet, + variablePaths, + func(pathHash uint64) (stop bool) { // On structural + actualStructuralPaths = append( + actualStructuralPaths, pathHash, + ) + return false + }, + func(pathHash uint64, i int) (stop bool) { // On argument + actualArgPaths[pathHash] = i + return false + }, + func(pathHash uint64, i int) (stop bool) { // On GQT variable + actualVarPaths[pathHash] = i + return false + }, + ) + }, + func(err error) { + t.Fatalf("unexpected GraphQL parsing error: %v", err) + }, + ) + compareStringsByHash( + t, tt.ExpectedStructural, actualStructuralPaths, "structural path", + ) + + argPathHash := map[uint64]int{} + if tt.ExpectedArg != nil { + for k, v := range tt.ExpectedArg { + argPathHash[pathscan.Hash(k)] = v + } + } + require.Equal(t, argPathHash, actualArgPaths, "argument paths") + + varPathHash := map[uint64]int{} + if tt.ExpectedVarVal != nil { + for k, v := range tt.ExpectedVarVal { + varPathHash[pathscan.Hash(k)] = v + } + } + require.Equal(t, varPathHash, actualVarPaths, "variable paths") + }) + } +} + +func TestInTokensPanic(t *testing.T) { + ps := pathscan.New(0, 0) + require.Panics(t, func() { + ps.InTokens( + gqlscan.TokenOprName, + []gqlparse.Token{ + {ID: gqlscan.TokenSet}, + {ID: gqlscan.TokenField, Value: []byte("foo")}, + {ID: gqlscan.TokenSetEnd}, + }, + map[uint64][]gqlparse.Token{ /* No variable paths */ }, + func(pathHash uint64) (stop bool) { // On structural + t.Fatal("this function isn't expected to be called") + return false + }, + func(pathHash uint64, i int) (stop bool) { // On argument + t.Fatal("this function isn't expected to be called") + return false + }, + func(pathHash uint64, i int) (stop bool) { // On variable + t.Fatal("this function isn't expected to be called") + return false + }, + ) + }) +} + +var testsInAST = []struct { + Name string + GQTTemplateSrc string + ExpectStructural []string + ExpectedArgPaths []string + ExpectedVarPaths []string +}{ + { + Name: "subscription_single", + GQTTemplateSrc: `subscription{foo}`, + ExpectStructural: []string{ + "S.foo", + }, + }, + { + Name: "subscription_multiple", + GQTTemplateSrc: `subscription{foo bar}`, + ExpectStructural: []string{ + "S.bar", + "S.foo", + }, + }, + { + Name: "args", + GQTTemplateSrc: `query{foo(b:*,a:"t") bar(c:42)}`, + ExpectStructural: []string{ + "Q.bar|c,", + "Q.foo|a,b,", + }, + ExpectedArgPaths: []string{ + "Q.foo|b", + "Q.foo|a", + "Q.bar|c", + }, + }, + { + Name: "args_with_subselections", + GQTTemplateSrc: `query{foo(b:*,a:"t") { fraz kraz(c:*) }}`, + ExpectStructural: []string{ + "Q.foo|a,b,.kraz|c,", + "Q.foo|a,b,.fraz", + }, + ExpectedArgPaths: []string{ + "Q.foo|b", + "Q.foo|a", + "Q.foo.kraz|c", + }, + }, + { + Name: "mutation_with_vars", + GQTTemplateSrc: `mutation{foo(bar=$bar:{x:*,y:*})}`, + ExpectStructural: []string{ + "M.foo|bar,", + }, + ExpectedArgPaths: []string{ + "M.foo|bar", + }, + ExpectedVarPaths: []string{ + "M.foo|bar", + }, + }, + { + Name: "mutation_with_vars_on_multiple_levels", + GQTTemplateSrc: `mutation{ + foo(bar=$bar:*){ + fo2(b=$b:*,a=$a:*){ + fo3(c=$c:*) + fa3 + } + fa2 + } + bazz(x:*) + }`, + ExpectStructural: []string{ + "M.bazz|x,", + "M.foo|bar,.fa2", + "M.foo|bar,.fo2|a,b,.fa3", + "M.foo|bar,.fo2|a,b,.fo3|c,", + }, + ExpectedArgPaths: []string{ + "M.foo|bar", + "M.foo.fo2|b", + "M.foo.fo2|a", + "M.foo.fo2.fo3|c", + "M.bazz|x", + }, + ExpectedVarPaths: []string{ + "M.foo|bar", + "M.foo.fo2|b", + "M.foo.fo2|a", + "M.foo.fo2.fo3|c", + }, + }, + { + Name: "mutation_var_in_obj", + GQTTemplateSrc: `mutation{ + foo { + bar(bar1:*,bar2:*) { + baz( + o: { + so=$so: { + x=$x: *, + y=$y: *, + } + }, + o2: $so, + x=$x2: $x, + y: $x2 || $y, + ) + } + } + }`, + ExpectStructural: []string{ + "M.foo.bar|bar1,bar2,.baz|o,o2,x,y,", + }, + ExpectedArgPaths: []string{ + "M.foo.bar|bar1", + "M.foo.bar|bar2", + "M.foo.bar.baz|o", + "M.foo.bar.baz|o2", + "M.foo.bar.baz|x", + "M.foo.bar.baz|y", + }, + ExpectedVarPaths: []string{ + "M.foo.bar.baz|o/so", + "M.foo.bar.baz|o/so/x", + "M.foo.bar.baz|o/so/y", + "M.foo.bar.baz|x", + }, + }, + { + Name: "type_condition", + GQTTemplateSrc: `query{ + u(u2:*,u1:*) { + ... on Foo { + foo1(x=$foo1x:*, y:*) + foo2(x:*) + } + ... on Bar { + bar1(x:*) + bar2(x=$bar2x:*, y:*) + } + } + }`, + ExpectStructural: []string{ + "Q.u|u1,u2,&Bar.bar2|x,y,", + "Q.u|u1,u2,&Bar.bar1|x,", + "Q.u|u1,u2,&Foo.foo2|x,", + "Q.u|u1,u2,&Foo.foo1|x,y,", + }, + ExpectedArgPaths: []string{ + "Q.u|u2", + "Q.u|u1", + "Q.u&Foo.foo1|x", + "Q.u&Foo.foo1|y", + "Q.u&Foo.foo2|x", + "Q.u&Bar.bar1|x", + "Q.u&Bar.bar2|x", + "Q.u&Bar.bar2|y", + }, + ExpectedVarPaths: []string{ + "Q.u&Foo.foo1|x", + "Q.u&Bar.bar2|x", + }, + }, + { + Name: "complex", + GQTTemplateSrc: `query { + foo { + bar { + burr(x:4) + } + baz { + ... on Kraz { + fraz + graz(argument:{i:"foo",i2:"bar"}) { + lum + } + } + buzz(b:5, a:null, c:true) + ... on Guz { + guz + } + } + } + mazz + }`, + ExpectStructural: []string{ + "Q.mazz", + "Q.foo.baz&Guz.guz", + "Q.foo.baz.buzz|a,b,c,", + "Q.foo.baz&Kraz.graz|argument,.lum", + "Q.foo.baz&Kraz.fraz", + "Q.foo.bar.burr|x,", + }, + ExpectedArgPaths: []string{ + "Q.foo.bar.burr|x", + "Q.foo.baz&Kraz.graz|argument", + "Q.foo.baz.buzz|b", + "Q.foo.baz.buzz|a", + "Q.foo.baz.buzz|c", + }, + }, +} + +func TestInAST(t *testing.T) { + for _, tt := range testsInAST { + t.Run(tt.Name, func(t *testing.T) { + o, _, errs := gqt.Parse([]byte(tt.GQTTemplateSrc)) + require.Nil(t, errs) + + var actualPathNames []string + var actualPaths, actualArgPaths []uint64 + actualVarPaths := map[uint64]string{} // Hash -> variable name + errsP := pathscan.InAST( + o, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On structural + actualPathNames = append(actualPathNames, path) + actualPaths = append(actualPaths, pathHash) + require.NotNil(t, e) + return false + }, func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // On argument + actualArgPaths = append(actualArgPaths, pathHash) + return false + }, func( + path string, + pathHash uint64, + e *gqt.VariableDeclaration, + ) (stop bool) { + // On variable + actualVarPaths[pathHash] = e.Name + return false + }, + ) + require.Nil(t, errsP) + + require.Equal(t, tt.ExpectStructural, actualPathNames) + compareStringsByHash( + t, tt.ExpectStructural, actualPaths, "structural paths", + ) + compareStringsByHash( + t, tt.ExpectedArgPaths, actualArgPaths, "argument paths", + ) + compareStringsByHash( + t, tt.ExpectedVarPaths, mapKeys(actualVarPaths), "variable paths", + ) + }) + } +} + +func compareStringsByHash( + t *testing.T, + expected []string, + actual []uint64, + msg string, +) { + e := hashStrings(expected) + sort.Slice(e, func(i, j int) bool { + return e[i] < e[j] + }) + sort.Slice(actual, func(i, j int) bool { + return actual[i] < actual[j] + }) + require.Equal(t, e, actual, msg) +} + +func hashStrings(s []string) []uint64 { + if len(s) < 1 { + return nil + } + h := make([]uint64, len(s)) + for i, s := range s { + h[i] = pathscan.Hash(s) + } + return h +} + +func mapKeys[K constraints.Ordered, T any](m map[K]T) []K { + if len(m) < 1 { + return nil + } + ks := make([]K, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + slices.Sort(ks) + return ks +} diff --git a/pkg/engine/playmon/internal/scanval/length.go b/pkg/engine/playmon/internal/scanval/length.go new file mode 100644 index 0000000..d359f4c --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/length.go @@ -0,0 +1,52 @@ +package scanval + +import ( + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/gqlscan" +) + +// Length reads the length in tokens from the first value in t. +// For example: +// +// `[1,2,3]` has a length of 5 tokens +// `{x:""}` has a length of 4 tokens +// `null` has a length of 1 tokens +func Length(r *tokenreader.Reader) (length int) { + switch r.ReadOne().ID { + case gqlscan.TokenNull, gqlscan.TokenInt, gqlscan.TokenFloat, + gqlscan.TokenStr, gqlscan.TokenStrBlock, gqlscan.TokenEnumVal, + gqlscan.TokenTrue, gqlscan.TokenFalse: + return 1 + case gqlscan.TokenArr: + length += 1 + SCAN_ARR: + for levelArr := 1; ; { + length++ + switch r.ReadOne().ID { + case gqlscan.TokenArr: + levelArr++ + case gqlscan.TokenArrEnd: + levelArr-- + if levelArr < 1 { + break SCAN_ARR + } + } + } + case gqlscan.TokenObj: + length += 1 + SCAN_OBJ: + for levelObj := 1; !r.EOF(); { + length++ + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break SCAN_OBJ + } + } + } + } + return length +} diff --git a/pkg/engine/playmon/internal/scanval/length_test.go b/pkg/engine/playmon/internal/scanval/length_test.go new file mode 100644 index 0000000..1c11906 --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/length_test.go @@ -0,0 +1,178 @@ +package scanval_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/scanval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/require" +) + +func TestLength(t *testing.T) { + for _, tt := range []struct { + Name string + Input []gqlparse.Token + Expect int + }{ + { + Name: "bool_true", + Input: []gqlparse.Token{{ID: gqlscan.TokenTrue}}, + Expect: 1, + }, + { + Name: "bool_false", + Input: []gqlparse.Token{{ID: gqlscan.TokenFalse}}, + Expect: 1, + }, + { + Name: "int", + Input: []gqlparse.Token{{ID: gqlscan.TokenInt, Value: []byte("-123")}}, + Expect: 1, + }, + { + Name: "float", + Input: []gqlparse.Token{{ID: gqlscan.TokenFloat, Value: []byte("-3.14")}}, + Expect: 1, + }, + { + Name: "string", + Input: []gqlparse.Token{{ID: gqlscan.TokenStr, Value: []byte("text")}}, + Expect: 1, + }, + { + Name: "string_block", + Input: []gqlparse.Token{{ID: gqlscan.TokenStrBlock}}, + Expect: 1, + }, + { + Name: "enum", + Input: []gqlparse.Token{{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + Expect: 1, + }, + { + Name: "null", + Input: []gqlparse.Token{{ID: gqlscan.TokenNull}}, + Expect: 1, + }, + { + Name: "array", + Input: []gqlparse.Token{ + // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: 2, + }, + { + Name: "array_int_null", + Input: []gqlparse.Token{ + // [1,null,3] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenInt, Value: []byte("3")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: 5, + }, + { + Name: "array_2d_int_null", + Input: []gqlparse.Token{ + // [[1],[],[null],null] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenArrEnd}, + + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: 11, + }, + { + Name: "object", + Input: []gqlparse.Token{ + // {foo:42} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("foo")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: 4, + }, + { + Name: "object_nested", + Input: []gqlparse.Token{ + // {foo:{bar:42}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("foo")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("bar")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: 7, + }, + { + Name: "object_nested_with_array", + Input: []gqlparse.Token{ + // {foo:{bar:[42]}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("foo")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("bar")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: 9, + }, + { + Name: "array_with_tail", + Input: []gqlparse.Token{ + // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + + // [123] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("123")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: 2, + }, + { + Name: "object_with_tail", + Input: []gqlparse.Token{ + // {x:"0"} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenStr, Value: []byte("0")}, + {ID: gqlscan.TokenObjEnd}, + + // {x:"1"} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenStr, Value: []byte("1")}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: 4, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + a := scanval.Length(&tokenreader.Reader{Main: tt.Input}) + require.Equal(t, tt.Expect, a) + }) + } +} diff --git a/pkg/engine/playmon/internal/scanval/scan_values.go b/pkg/engine/playmon/internal/scanval/scan_values.go new file mode 100644 index 0000000..7b92f1c --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/scan_values.go @@ -0,0 +1,72 @@ +package scanval + +import ( + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/gqlscan" +) + +// ScanValues calls fn for every value in a. +// Immediately returns true if any call to fn returned true as well. +func ScanValues( + r *tokenreader.Reader, + fn func(r *tokenreader.Reader) (stop bool), +) (stopped bool) { + for !r.EOF() { + rBefore := *r + switch r.ReadOne().ID { + case gqlscan.TokenTrue, + gqlscan.TokenFalse, + gqlscan.TokenStr, + gqlscan.TokenStrBlock, + gqlscan.TokenFloat, + gqlscan.TokenInt, + gqlscan.TokenEnumVal, + gqlscan.TokenNull: + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + case gqlscan.TokenObj: + SCAN_OBJ: + for levelObj := 1; ; { + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break SCAN_OBJ + } + } + } + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + case gqlscan.TokenArr: + SCAN_ARR: + for levelArr := 1; ; { + switch r.ReadOne().ID { + case gqlscan.TokenArr: + levelArr++ + case gqlscan.TokenArrEnd: + levelArr-- + if levelArr < 1 { + break SCAN_ARR + } + } + } + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + } + } + return false +} diff --git a/pkg/engine/playmon/internal/scanval/scan_values_in_arrays.go b/pkg/engine/playmon/internal/scanval/scan_values_in_arrays.go new file mode 100644 index 0000000..ac4665c --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/scan_values_in_arrays.go @@ -0,0 +1,95 @@ +package scanval + +import ( + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/gqlscan" +) + +// InArrays calls fn for every non-array value contained in x recursively. +// Immediately returns true if any call to fn returned true as well. +func InArrays( + r *tokenreader.Reader, + fn func(r *tokenreader.Reader) (stop bool), +) (stopped bool) { + for levelArr := 0; !r.EOF(); { + rBefore := *r + switch r.ReadOne().ID { + case gqlscan.TokenTrue, + gqlscan.TokenFalse, + gqlscan.TokenStr, + gqlscan.TokenStrBlock, + gqlscan.TokenFloat, + gqlscan.TokenInt, + gqlscan.TokenEnumVal, + gqlscan.TokenNull: + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + case gqlscan.TokenObj: + OBJ_SCAN_1: + for levelObj := 1; ; { + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break OBJ_SCAN_1 + } + } + } + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + case gqlscan.TokenArr: + levelArr++ + VAL_SCAN: + for levelArr > 0 { + rBefore := *r + switch r.ReadOne().ID { + case gqlscan.TokenArrEnd: + levelArr-- + if levelArr < 1 { + break VAL_SCAN + } + goto VAL_SCAN + case gqlscan.TokenArr: + levelArr++ + case gqlscan.TokenObj: + OBJ_SCAN_2: + for levelObj := 1; ; { + switch r.ReadOne().ID { + case gqlscan.TokenObj: + levelObj++ + case gqlscan.TokenObjEnd: + levelObj-- + if levelObj < 1 { + break OBJ_SCAN_2 + } + } + } + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + default: + rAfter := *r + *r = rBefore + if fn(r) { + return true + } + *r = rAfter + } + } + } + } + return false +} diff --git a/pkg/engine/playmon/internal/scanval/scan_values_in_arrays_test.go b/pkg/engine/playmon/internal/scanval/scan_values_in_arrays_test.go new file mode 100644 index 0000000..c717a85 --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/scan_values_in_arrays_test.go @@ -0,0 +1,377 @@ +package scanval_test + +import ( + "fmt" + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/scanval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInValues(t *testing.T) { + for _, tt := range []struct { + Name string + Input []gqlparse.Token + Expect [][]gqlparse.Token + }{ + { + Name: "string", + Input: []gqlparse.Token{ + // "text" + {ID: gqlscan.TokenStr, Value: []byte("text")}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + }, + }, + { + Name: "string_block", + Input: []gqlparse.Token{ + // """text""" + {ID: gqlscan.TokenStrBlock}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenStrBlock}}, + }, + }, + { + Name: "float", + Input: []gqlparse.Token{ + // 3.14 + {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + }, + }, + { + Name: "enum", + Input: []gqlparse.Token{ + // red + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + }, + }, + { + Name: "boolean(true)", + Input: []gqlparse.Token{ + // true + {ID: gqlscan.TokenTrue}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenTrue}}, + }, + }, + { + Name: "boolean(false)", + Input: []gqlparse.Token{ + // false + {ID: gqlscan.TokenFalse}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenFalse}}, + }, + }, + { + Name: "null", + Input: []gqlparse.Token{ + {ID: gqlscan.TokenNull}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenNull}}, + }, + }, + { + Name: "object_with_array_inside", + Input: []gqlparse.Token{ + // {field:["text"]} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("field")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: [][]gqlparse.Token{ + { + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("field")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + }, + { + Name: "object_nested", + Input: []gqlparse.Token{ + // {x:{y:{z:1}}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("z")}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + Expect: [][]gqlparse.Token{ + { + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("y")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("z")}, + {ID: gqlscan.TokenInt, Value: []byte("1")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + }, + { + Name: "empty_array", + Input: []gqlparse.Token{ + // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{}, + }, + { + Name: "array_with_1_int", + Input: []gqlparse.Token{ + // [42] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + }, + }, + { + Name: "array_with_3_int", + Input: []gqlparse.Token{ + // [42, 0, 100500] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenInt, Value: []byte("100500")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + {{ID: gqlscan.TokenInt, Value: []byte("0")}}, + {{ID: gqlscan.TokenInt, Value: []byte("100500")}}, + }, + }, + { + Name: "array_with_1_string", + Input: []gqlparse.Token{ + // ["text"] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + }, + }, + { + Name: "array_with_1_string_block", + Input: []gqlparse.Token{ + // ["""text"""] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStrBlock}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenStrBlock}}, + }, + }, + { + Name: "array_with_1_float", + Input: []gqlparse.Token{ + // [3.14] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + }, + }, + { + Name: "array_with_1_enum", + Input: []gqlparse.Token{ + // [red] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + }, + }, + { + Name: "array_with_1_boolean(true)", + Input: []gqlparse.Token{ + // [true] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenTrue}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenTrue}}, + }, + }, + { + Name: "array_with_1_boolean(false)", + Input: []gqlparse.Token{ + // [false] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenFalse}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenFalse}}, + }, + }, + { + Name: "array_with_1_null", + Input: []gqlparse.Token{ + // [null] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenNull}}, + }, + }, + { + Name: "array_with_1_object", + Input: []gqlparse.Token{ + // [{x:0}] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + { + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("x")}, + {ID: gqlscan.TokenInt, Value: []byte("0")}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + }, + { + Name: "3d_array_string", + Input: []gqlparse.Token{ + // [[["1"],["2"]],[["3"]]] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("1")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("2")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("3")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenStr, Value: []byte("1")}}, + {{ID: gqlscan.TokenStr, Value: []byte("2")}}, + {{ID: gqlscan.TokenStr, Value: []byte("3")}}, + }, + }, + { + Name: "array_with_null_and_nested_object_with_array_inside", + Input: []gqlparse.Token{ + // [null, {object:{array:["text"]}}] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("object")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("array")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + {{ID: gqlscan.TokenNull}}, + { + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("object")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("array")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + }, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + actual := [][]gqlparse.Token{} + actualCounter := 0 + stopped := scanval.InArrays( + &tokenreader.Reader{Main: tt.Input}, + func(r *tokenreader.Reader) (stop bool) { + var cp []gqlparse.Token + for i := 0; !r.EOF() && i < len(tt.Expect[actualCounter]); i++ { + cp = append(cp, r.ReadOne()) + } + actual = append(actual, cp) + actualCounter++ + return false + }, + ) + // Manually print diff for better readability + printSet := func(title string, t [][]gqlparse.Token) { + fmt.Printf("%s (%d)\n", title, len(t)) + for i, t := range t { + fmt.Printf(" item %d (%d token(s)):\n", i, len(t)) + for i, t := range t { + fmt.Printf(" %d: %s\n", i, t.String()) + } + } + } + isEqual := assert.ObjectsAreEqual(tt.Expect, actual) + if !isEqual { + printSet("expect", tt.Expect) + printSet("actual", actual) + } + require.True(t, isEqual) + require.False(t, stopped) + require.Equal(t, tt.Expect, actual) + }) + } +} diff --git a/pkg/engine/playmon/internal/scanval/scan_values_test.go b/pkg/engine/playmon/internal/scanval/scan_values_test.go new file mode 100644 index 0000000..0237384 --- /dev/null +++ b/pkg/engine/playmon/internal/scanval/scan_values_test.go @@ -0,0 +1,471 @@ +package scanval_test + +import ( + "fmt" + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/scanval" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScanValues(t *testing.T) { + for _, tt := range []struct { + Name string + Input []gqlparse.Token + Expect [][]gqlparse.Token + }{ + { + Name: "sequence", + Input: []gqlparse.Token{ + {ID: gqlscan.TokenStr, Value: []byte("text")}, + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenFloat, Value: []byte("3.1415")}, + {ID: gqlscan.TokenTrue}, + {ID: gqlscan.TokenFalse}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenStrBlock}, + // {o:{f:[[null]]}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("o")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + // {f:42} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + // [[]] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + Expect: [][]gqlparse.Token{ + { + {ID: gqlscan.TokenStr, Value: []byte("text")}, + }, + { + {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + }, + { + {ID: gqlscan.TokenInt, Value: []byte("42")}, + }, + { + {ID: gqlscan.TokenFloat, Value: []byte("3.1415")}, + }, + { + {ID: gqlscan.TokenTrue}, + }, + { + {ID: gqlscan.TokenFalse}, + }, + { + {ID: gqlscan.TokenNull}, + }, + { + {ID: gqlscan.TokenStrBlock}, + }, + { // {o:{f:[[null]]}} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("o")}, + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenNull}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenObjEnd}, + {ID: gqlscan.TokenObjEnd}, + }, + { // {f:42} + {ID: gqlscan.TokenObj}, + {ID: gqlscan.TokenObjField, Value: []byte("f")}, + {ID: gqlscan.TokenInt, Value: []byte("42")}, + {ID: gqlscan.TokenObjEnd}, + }, + { // [] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + }, + { // [[]] + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArr}, + {ID: gqlscan.TokenArrEnd}, + {ID: gqlscan.TokenArrEnd}, + }, + }, + }, + + // { + // Name: "string", + // Input: []Token{ + // // "text" + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + // }, + // }, + // { + // Name: "string block", + // Input: []Token{ + // // """text""" + // {ID: gqlscan.TokenStrBlock}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStrBlock}}, + // }, + // }, + // { + // Name: "float", + // Input: []Token{ + // // 3.14 + // {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + // }, + // }, + // { + // Name: "enum", + // Input: []Token{ + // // red + // {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + // }, + // }, + // { + // Name: "boolean(true)", + // Input: []Token{ + // // true + // {ID: gqlscan.TokenTrue}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenTrue}}, + // }, + // }, + // { + // Name: "boolean(false)", + // Input: []Token{ + // // false + // {ID: gqlscan.TokenFalse}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFalse}}, + // }, + // }, + // { + // Name: "null", + // Input: []Token{ + // {ID: gqlscan.TokenNull}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // }, + // }, + // { + // Name: "object with array inside", + // Input: []Token{ + // // {field:["text"]} + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("field")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("field")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "object nested", + // Input: []Token{ + // // {x:{y:{z:1}}} + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("y")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("z")}, + // {ID: gqlscan.TokenInt, Value: []byte("1")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("y")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("z")}, + // {ID: gqlscan.TokenInt, Value: []byte("1")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "empty array", + // Input: []Token{ + // // [] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{}, + // }, + // { + // Name: "array with 1 int", + // Input: []Token{ + // // [42] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenInt, Value: []byte("42")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + // }, + // }, + // { + // Name: "array with 3 int", + // Input: []Token{ + // // [42, 0, 100500] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenInt, Value: []byte("42")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenInt, Value: []byte("100500")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenInt, Value: []byte("42")}}, + // {{ID: gqlscan.TokenInt, Value: []byte("0")}}, + // {{ID: gqlscan.TokenInt, Value: []byte("100500")}}, + // }, + // }, + // { + // Name: "array with 1 string", + // Input: []Token{ + // // ["text"] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("text")}}, + // }, + // }, + // { + // Name: "array with 1 string block", + // Input: []Token{ + // // ["""text"""] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStrBlock}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStrBlock}}, + // }, + // }, + // { + // Name: "array with 1 float", + // Input: []Token{ + // // [3.14] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenFloat, Value: []byte("3.14")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFloat, Value: []byte("3.14")}}, + // }, + // }, + // { + // Name: "array with 1 enum", + // Input: []Token{ + // // [red] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenEnumVal, Value: []byte("red")}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenEnumVal, Value: []byte("red")}}, + // }, + // }, + // { + // Name: "array with 1 boolean(true)", + // Input: []Token{ + // // [true] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenTrue}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenTrue}}, + // }, + // }, + // { + // Name: "array with 1 boolean(false)", + // Input: []Token{ + // // [false] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenFalse}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenFalse}}, + // }, + // }, + // { + // Name: "array with 1 null", + // Input: []Token{ + // // [null] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenNull}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // }, + // }, + // { + // Name: "array with 1 object", + // Input: []Token{ + // // [{x:0}] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("x")}, + // {ID: gqlscan.TokenInt, Value: []byte("0")}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + // { + // Name: "3d array string", + // Input: []Token{ + // // [[["1"],["2"]],[["3"]]] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("1")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("2")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("3")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenStr, Value: []byte("1")}}, + // {{ID: gqlscan.TokenStr, Value: []byte("2")}}, + // {{ID: gqlscan.TokenStr, Value: []byte("3")}}, + // }, + // }, + // { + // Name: "array with null and nested object with array inside", + // Input: []Token{ + // // [null, {object:{array:["text"]}}] + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenNull}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("object")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("array")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenArrEnd}, + // }, + // Expect: [][]Token{ + // {{ID: gqlscan.TokenNull}}, + // { + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("object")}, + // {ID: gqlscan.TokenObj}, + // {ID: gqlscan.TokenObjField, Value: []byte("array")}, + // {ID: gqlscan.TokenArr}, + // {ID: gqlscan.TokenStr, Value: []byte("text")}, + // {ID: gqlscan.TokenArrEnd}, + // {ID: gqlscan.TokenObjEnd}, + // {ID: gqlscan.TokenObjEnd}, + // }, + // }, + // }, + } { + t.Run(tt.Name, func(t *testing.T) { + actual := [][]gqlparse.Token{} + actualCounter := 0 + stopped := scanval.ScanValues( + &tokenreader.Reader{Main: tt.Input}, + func(r *tokenreader.Reader) (stop bool) { + var cp []gqlparse.Token + for i := 0; !r.EOF() && i < len(tt.Expect[actualCounter]); i++ { + cp = append(cp, r.ReadOne()) + } + actual = append(actual, cp) + actualCounter++ + return false + }, + ) + // Manually print diff for better readability + printSet := func(title string, t [][]gqlparse.Token) { + fmt.Printf("%s (%d)\n", title, len(t)) + for i, t := range t { + fmt.Printf(" item %d (%d token(s)):\n", i, len(t)) + for i, t := range t { + fmt.Printf(" %d: %s\n", i, t.String()) + } + } + } + isEqual := assert.ObjectsAreEqual(tt.Expect, actual) + if !isEqual { + printSet("expect", tt.Expect) + printSet("actual", actual) + } + require.True(t, isEqual) + require.False(t, stopped) + require.Equal(t, tt.Expect, actual) + }) + } +} diff --git a/pkg/engine/playmon/internal/tokenreader/tokenreader.go b/pkg/engine/playmon/internal/tokenreader/tokenreader.go new file mode 100644 index 0000000..98ff283 --- /dev/null +++ b/pkg/engine/playmon/internal/tokenreader/tokenreader.go @@ -0,0 +1,55 @@ +// Package tokenreader provides Reader which reads a continuous +// stream of tokens respecting variable index tokens. +package tokenreader + +import ( + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" +) + +// Reader reads a continuous stream of tokens respecting +// variable index tokens. +type Reader struct { + Vars [][]gqlparse.Token + Var, Main []gqlparse.Token +} + +// ReadOne advances the reader by one token and returns it. +func (r *Reader) ReadOne() (t gqlparse.Token) { + if len(r.Var) > 0 { + t, r.Var = r.Var[0], r.Var[1:] + return t + } + if vi := r.Main[0].VariableIndex(); vi > -1 { + t, r.Var, r.Main = r.Vars[vi][0], r.Vars[vi][1:], r.Main[1:] + return t + } + t, r.Main = r.Main[0], r.Main[1:] + return t +} + +// SkipUntil skips all tokens until (including) the first token +// that has the given id. +func (r *Reader) SkipUntil(id gqlscan.Token) { + for r.ReadOne().ID != id { + } +} + +// EOF returns true if the reader is currently at the end of the file, +// otherwise returns false. +func (r *Reader) EOF() bool { + return len(r.Var) < 1 && len(r.Main) < 1 +} + +// PeekOne returns true if the reader is currently at the end of the file, +// otherwise returns false. +func (r *Reader) PeekOne() gqlparse.Token { + if len(r.Var) > 0 { + return r.Var[0] + } + if vi := r.Main[0].VariableIndex(); vi > -1 { + r.Var, r.Main = r.Vars[vi], r.Main[1:] + return r.Var[0] + } + return r.Main[0] +} diff --git a/pkg/engine/playmon/internal/tokenreader/tokenreader_test.go b/pkg/engine/playmon/internal/tokenreader/tokenreader_test.go new file mode 100644 index 0000000..c840876 --- /dev/null +++ b/pkg/engine/playmon/internal/tokenreader/tokenreader_test.go @@ -0,0 +1,168 @@ +package tokenreader_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/tokenreader" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/require" +) + +// No need to test the case when Main is exhausted, it will never happen. + +func TestReadOne(t *testing.T) { + r := &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenStr, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Main: []gqlparse.Token{ + T(gqlparse.TokenTypeValIndexOffset+1, "irrelevant_text"), + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenStrBlock, "last_main"), + }, + } + + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStr, "var2_first"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStr, "var2_first"), r.ReadOne()) + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStr, "var2_second"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStr, "var2_second"), r.ReadOne()) + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStrBlock, "intermediate"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStrBlock, "intermediate"), r.ReadOne()) + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStr, "var1_first"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStr, "var1_first"), r.ReadOne()) + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStr, "var1_second"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStr, "var1_second"), r.ReadOne()) + require.False(t, r.EOF()) + TokEq(t, T(gqlscan.TokenStrBlock, "last_main"), r.PeekOne()) + TokEq(t, T(gqlscan.TokenStrBlock, "last_main"), r.ReadOne()) + require.True(t, r.EOF()) + require.True(t, r.EOF()) +} + +func TestSkipUntil(t *testing.T) { + r := &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenStr, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Main: []gqlparse.Token{ + T(gqlparse.TokenTypeValIndexOffset+1, "irrelevant_text"), + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenArrEnd, "last_main"), + }, + } + + r.SkipUntil(gqlscan.TokenArrEnd) + require.Equal(t, &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenStr, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Var: []gqlparse.Token{}, + Main: []gqlparse.Token{}, + }, r) +} + +func TestSkipUntil_InVarVal(t *testing.T) { + r := &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenEnumVal, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Main: []gqlparse.Token{ + T(gqlparse.TokenTypeValIndexOffset+1, "irrelevant_text"), + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenArrEnd, "last_main"), + }, + } + + r.SkipUntil(gqlscan.TokenEnumVal) + require.Equal(t, &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenEnumVal, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Var: []gqlparse.Token{ + T(gqlscan.TokenStr, "var2_second"), + }, + Main: []gqlparse.Token{ + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenArrEnd, "last_main"), + }, + }, r) +} + +func TokEq(t *testing.T, expect, actual gqlparse.Token) { + t.Helper() + require.Equal(t, expect.String(), actual.String()) +} + +func T(id gqlscan.Token, value string) gqlparse.Token { + var v []byte + if value != "" { + v = []byte(value) + } + return gqlparse.Token{ID: gqlscan.Token(id), Value: v} +} + +var GT gqlparse.Token + +func BenchmarkReadOne(b *testing.B) { + r := &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenStr, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Main: []gqlparse.Token{ + T(gqlparse.TokenTypeValIndexOffset+1, "irrelevant_text"), + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenStrBlock, "last_main"), + }, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + resetTo := *r + GT = r.ReadOne() // var2_first + GT = r.ReadOne() // var2_second + GT = r.ReadOne() // intermediate + GT = r.ReadOne() // var1_first + GT = r.ReadOne() // var1_second + GT = r.ReadOne() // last_main + *r = resetTo // Reset reader + } +} + +func BenchmarkSkipUntil(b *testing.B) { + r := &tokenreader.Reader{ + Vars: [][]gqlparse.Token{ + {T(gqlscan.TokenStr, "var1_first"), T(gqlscan.TokenStr, "var1_second")}, + {T(gqlscan.TokenStr, "var2_first"), T(gqlscan.TokenStr, "var2_second")}, + }, + Main: []gqlparse.Token{ + T(gqlparse.TokenTypeValIndexOffset+1, "irrelevant_text"), + T(gqlscan.TokenStrBlock, "intermediate"), + T(gqlparse.TokenTypeValIndexOffset, "irrelevant_text2"), + T(gqlscan.TokenArrEnd, "last_main"), + }, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + resetTo := *r + r.SkipUntil(gqlscan.TokenArrEnd) + *r = resetTo // Reset reader + } +} diff --git a/pkg/engine/playmon/internal/travgqt/travgqt.go b/pkg/engine/playmon/internal/travgqt/travgqt.go new file mode 100644 index 0000000..6d9750c --- /dev/null +++ b/pkg/engine/playmon/internal/travgqt/travgqt.go @@ -0,0 +1,145 @@ +// Package travgqt provides a GQT expression traversal function. +package travgqt + +import ( + "fmt" + + "github.com/graph-guard/gqt/v4" +) + +// Traverse returns true after BFS-traversing the entire tree under e +// calling onExpression for every discovered expression. +// Returns true immediatelly if onExpression returns stop=true. +func Traverse( + e gqt.Expression, + onExpression func(gqt.Expression) (stop, skipChildren bool), +) (stopped bool) { + stack := make([]gqt.Expression, 0, 64) + push := func(expression gqt.Expression) { + stack = append(stack, expression) + } + + push(e) + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + stop, skipChildren := onExpression(top) + if stop { + return true + } else if skipChildren { + continue + } + + switch e := top.(type) { + case *gqt.Operation: + for _, f := range e.Selections { + push(f) + } + case *gqt.SelectionInlineFrag: + for _, f := range e.Selections { + push(f) + } + case *gqt.SelectionField: + for _, f := range e.Selections { + push(f) + } + for _, a := range e.Arguments { + push(a) + } + case *gqt.SelectionMax: + for _, f := range e.Options.Selections { + push(f) + } + case *gqt.Argument: + push(e.Constraint) + case *gqt.ConstrEquals: + push(e.Value) + case *gqt.ConstrNotEquals: + push(e.Value) + case *gqt.ConstrGreater: + push(e.Value) + case *gqt.ConstrGreaterOrEqual: + push(e.Value) + case *gqt.ConstrLess: + push(e.Value) + case *gqt.ConstrLessOrEqual: + push(e.Value) + case *gqt.ConstrLenEquals: + push(e.Value) + case *gqt.ConstrLenNotEquals: + push(e.Value) + case *gqt.ConstrLenGreater: + push(e.Value) + case *gqt.ConstrLenLess: + push(e.Value) + case *gqt.ConstrLenGreaterOrEqual: + push(e.Value) + case *gqt.ConstrLenLessOrEqual: + push(e.Value) + case *gqt.ConstrMap: + push(e.Constraint) + case *gqt.ExprParentheses: + push(e.Expression) + case *gqt.ExprEqual: + push(e.Left) + push(e.Right) + case *gqt.ExprNotEqual: + push(e.Left) + push(e.Right) + case *gqt.ExprLogicalNegation: + push(e.Expression) + case *gqt.ExprNumericNegation: + push(e.Expression) + case *gqt.ExprLogicalOr: + for _, e := range e.Expressions { + push(e) + } + case *gqt.ExprLogicalAnd: + for _, e := range e.Expressions { + push(e) + } + case *gqt.ExprAddition: + push(e.AddendLeft) + push(e.AddendRight) + case *gqt.ExprSubtraction: + push(e.Minuend) + push(e.Subtrahend) + case *gqt.ExprMultiplication: + push(e.Multiplicant) + push(e.Multiplicator) + case *gqt.ExprDivision: + push(e.Dividend) + push(e.Divisor) + case *gqt.ExprModulo: + push(e.Dividend) + push(e.Divisor) + case *gqt.ExprGreater: + push(e.Left) + push(e.Right) + case *gqt.ExprGreaterOrEqual: + push(e.Left) + push(e.Right) + case *gqt.ExprLess: + push(e.Left) + push(e.Right) + case *gqt.ExprLessOrEqual: + push(e.Left) + push(e.Right) + case *gqt.Array: + for _, i := range e.Items { + push(i) + } + case *gqt.Object: + for _, f := range e.Fields { + push(f) + } + case *gqt.ObjectField: + push(e.Constraint) + case *gqt.Number, *gqt.True, *gqt.False, *gqt.Null, + *gqt.Enum, *gqt.String, *gqt.ConstrAny, *gqt.Variable: + default: + panic(fmt.Errorf("unhandled type: %T", top)) + } + } + return false +} diff --git a/pkg/engine/playmon/playmon.go b/pkg/engine/playmon/playmon.go new file mode 100644 index 0000000..a39b304 --- /dev/null +++ b/pkg/engine/playmon/playmon.go @@ -0,0 +1,183 @@ +package playmon + +import ( + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/constrcheck" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathmatch" + "github.com/graph-guard/ggproxy/pkg/engine/playmon/internal/pathscan" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/gqlscan" + "github.com/graph-guard/gqt/v4" +) + +type arg struct { + PathHash uint64 + Value []gqlparse.Token +} + +type Engine struct { + parser *gqlparse.Parser + pathScanner *pathscan.PathScanner + matcher *pathmatch.Matcher + templates map[string]*template + argumentPaths map[uint64]struct{} + + structuralPaths []uint64 + varValues map[uint64][]gqlparse.Token + argumentsSet []arg +} + +type template struct { + Index int + ID string + GQTOpr *gqt.Operation + ConstraintChecker *constrcheck.Checker +} + +// New expects templates to be initialized and valid. +func New(s *config.Service) *Engine { + e := &Engine{ + parser: gqlparse.NewParser(s.Schema), + pathScanner: pathscan.New(128, 2048), + templates: make(map[string]*template, len(s.Templates)), + argumentPaths: make(map[uint64]struct{}), + + structuralPaths: make([]uint64, 1024), + varValues: make(map[uint64][]gqlparse.Token), + argumentsSet: make([]arg, 1024), + } + idCounter := 0 + for _, t := range s.Templates { + c := constrcheck.New(t.GQTTemplate, s.Schema) + tmpl := &template{ + Index: idCounter, + ID: t.ID, + GQTOpr: t.GQTTemplate, + ConstraintChecker: c, + } + e.templates[tmpl.ID] = tmpl + if errs := pathscan.InAST( + tmpl.GQTOpr, + func( + path string, + pathHash uint64, + e gqt.Expression, + ) (stop bool) { + // Structural + return false + }, + func( + path string, + pathHash uint64, + _ gqt.Expression, + ) (stop bool) { + // Argument + e.argumentPaths[pathHash] = struct{}{} + return false + }, + func( + path string, + pathHash uint64, + _ *gqt.VariableDeclaration, + ) (stop bool) { + // Variable + e.varValues[pathHash] = nil + return false + }, + ); errs != nil { + panic(errs) + } + idCounter++ + } + e.matcher = pathmatch.New(s) + return e +} + +func (e *Engine) reset() { + e.structuralPaths = e.structuralPaths[:0] + e.argumentsSet = e.argumentsSet[:0] + for path := range e.varValues { + e.varValues[path] = nil + } +} + +// match returns the ID of the first matching template or "" if none was matched. +func (e *Engine) match( + variableValues [][]gqlparse.Token, + queryType gqlscan.Token, + selectionSet []gqlparse.Token, + onMatch func(*config.Template) (stop bool), +) { + e.reset() + var mismatch bool + e.pathScanner.InTokens( + queryType, + selectionSet, + e.varValues, + func(path uint64) (stop bool) { // Structural path + e.structuralPaths = append(e.structuralPaths, path) + return false + }, + func(path uint64, i int) (stop bool) { // Argument + if _, ok := e.argumentPaths[path]; !ok { + mismatch = true + return false + } + e.argumentsSet = append(e.argumentsSet, arg{ + PathHash: path, + Value: selectionSet[i+1:], + }) + return false + }, + func(path uint64, i int) (stop bool) { // Variable value + e.varValues[path] = selectionSet[i:] + return false + }, + ) + if mismatch { + return + } + + e.matcher.Match(e.structuralPaths, func(t *config.Template) (stop bool) { + tm := e.templates[t.ID] + for i := range e.argumentsSet { + if !tm.ConstraintChecker.Check( + variableValues, + e.varValues, + e.argumentsSet[i].PathHash, + e.argumentsSet[i].Value, + ) { + return false + } + } + return onMatch(t) + }) +} + +// Match calls onMatch for every matched template until onMatch returns true. +// onErr is invoked in case of an error. +func (e *Engine) Match( + query, operationName, variablesJSON []byte, + onParsed func(operation, selectionSet []gqlparse.Token) (stop bool), + onMatch func(template *config.Template) (stop bool), + onErr func(err error), +) { + e.parser.Parse( + query, operationName, variablesJSON, + func( + varVals [][]gqlparse.Token, + operation, selectionSet []gqlparse.Token, + ) { + if onParsed(operation, selectionSet) { + return + } + e.match( + varVals, operation[0].ID, selectionSet, + func(t *config.Template) (stop bool) { + return onMatch(t) + }, + ) + }, + onErr, + ) +} diff --git a/pkg/engine/tests/benchmark_test.go b/pkg/engine/tests/benchmark_test.go new file mode 100644 index 0000000..d866c01 --- /dev/null +++ b/pkg/engine/tests/benchmark_test.go @@ -0,0 +1,60 @@ +package engine_test + +// TODO: either reimplement the benchmark or make it playmon compatible. + +// import ( +// "embed" +// _ "embed" +// "fmt" +// "testing" + +// "github.com/graph-guard/ggproxy/engines/rmap" +// "github.com/graph-guard/ggproxy/pkg/gqlparse" +// "github.com/graph-guard/gqt" +// ) + +// var N int + +// //go:embed assets/benchassets +// var benchassets embed.FS + +// var GS string + +// func BenchmarkPartedQuery(b *testing.B) { +// templates := readTestAssets(benchassets, "assets/benchassets", "templates")[0].Templates +// rules := make(map[string]gqt.Doc, len(templates)) +// for _, r := range templates { +// rules[r.ID] = r.Document +// } +// rm, _ := rmap.New(rules, 0) + +// for _, td := range readTestAssets(benchassets, "assets/benchassets", "bench_") { +// b.Run(td.ID, func(b *testing.B) { +// p := gqlparse.NewParser() +// query := []byte(td.Query) +// operationName := []byte(td.OperationName) +// variables := []byte(td.Variables) +// b.ResetTimer() + +// for n := 0; n < b.N; n++ { +// p.Parse( +// query, operationName, variables, +// func( +// varVals [][]gqlparse.Token, +// operation []gqlparse.Token, +// selectionSet []gqlparse.Token, +// ) { +// rm.MatchAll( +// varVals, +// operation[0].ID, +// selectionSet, +// func(id string) { GS = id }, +// ) +// }, func(err error) { +// panic(fmt.Errorf("unexpected error: %w", err)) +// }, +// ) +// } +// }) +// } +// } diff --git a/pkg/engine/tests/engine_test.go b/pkg/engine/tests/engine_test.go new file mode 100644 index 0000000..c826a5a --- /dev/null +++ b/pkg/engine/tests/engine_test.go @@ -0,0 +1,621 @@ +package engine_test + +import ( + "embed" + _ "embed" + "io/fs" + "path/filepath" + "strings" + "testing" + + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/testsetup" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +//go:embed tests +var testsFS embed.FS + +func TestPlaymonMatch(t *testing.T) { + testSets, err := testsFS.ReadDir("tests") + require.NoError(t, err) + for _, d := range testSets { + t.Run(d.Name(), func(t *testing.T) { + if !d.IsDir() { + t.Skip("not a directory") + } + setup, ok := testsetup.ByName(d.Name()) + if !ok { + t.Fatalf("unknown test setup: %q", d.Name()) + } + + engine := playmon.New(setup.Config.ServicesEnabled[0]) + + tests, err := fs.ReadDir(testsFS, filepath.Join("tests", d.Name())) + require.NoError(t, err) + for _, f := range tests { + name := strings.TrimSuffix(f.Name(), ".yaml") + t.Run(name, func(t *testing.T) { + if !strings.HasSuffix(f.Name(), ".yaml") { + t.Skip("missing '.yaml' extension") + } + if f.IsDir() { + t.Skip("directory") + } + c, err := fs.ReadFile(testsFS, filepath.Join("tests", d.Name(), f.Name())) + require.NoError(t, err) + var ts Test + err = yaml.Unmarshal(c, &ts) + require.NoError(t, err) + var errMsg string + var matches []string + + var operationName []byte + if ts.OperationName != "" { + operationName = []byte(ts.OperationName) + } + var variablesJSON []byte + if ts.VariablesJSON != "" { + variablesJSON = []byte(ts.VariablesJSON) + } + + engine.Match( + []byte(ts.Query), operationName, variablesJSON, + func(operation, selectionSet []gqlparse.Token) (stop bool) { + return false + }, + func(template *config.Template) (stop bool) { + matches = append(matches, template.ID) + return false + }, func(err error) { + errMsg = err.Error() + }, + ) + require.Equal(t, ts.ExpectError, errMsg) + require.Equal(t, ts.ExpectMatches, matches) + }) + } + }) + } +} + +type Test struct { + Query string `yaml:"query"` + OperationName string `yaml:"operation-name"` + VariablesJSON string `yaml:"variables-json"` + ExpectError string `yaml:"expect-error"` + ExpectMatches []string `yaml:"expect-matches"` +} + +// type MatchTest struct { +// ID string +// *QueryModel +// Templates []*config.Template +// } + +// func readTestAsset( +// filesystem fs.FS, path string, +// ) ( +// query *QueryModel, templates []*config.Template, +// ) { +// test, err := fs.ReadDir(filesystem, path) +// if err != nil { +// panic(err) +// } + +// for _, f := range test { +// if f.IsDir() { +// continue +// } +// fn := f.Name() +// fp := filepath.Join(path, f.Name()) +// if strings.HasSuffix(fn, ".gqt") { +// id := strings.ToLower(fn[:len(fn)-len(filepath.Ext(fn))]) +// src, err := filesystem.Open(fp) +// if err != nil { +// panic(err) +// } +// b, err := io.ReadAll(src) +// if err != nil { +// panic(err) +// } + +// meta, template, err := metadata.Parse(b) +// if err != nil { +// panic(err) +// } +// doc, errParser := gqt.Parse(template) +// if errParser.IsErr() { +// panic(errParser) +// } + +// templates = append(templates, &config.Template{ +// ID: id, +// Source: template, +// Document: doc, +// Name: meta.Name, +// Tags: meta.Tags, +// }) +// } +// if strings.HasSuffix(fn, ".yml") || strings.HasSuffix(fn, ".yaml") { +// src, err := filesystem.Open(fp) +// if err != nil { +// panic(err) +// } +// d := yaml.NewDecoder(src) +// d.KnownFields(true) +// err = d.Decode(&query) +// if err != nil { +// panic(err) +// } +// } +// } + +// return +// } + +// func readTestAssets(filesystem fs.FS, path, prefix string) (assets []*MatchTest) { +// root, err := fs.ReadDir(filesystem, path) +// if err != nil { +// panic(err) +// } +// for _, testDir := range root { +// if !testDir.IsDir() { +// continue +// } +// testDirName := testDir.Name() +// testDirPath := filepath.Join(path, testDirName) +// if !strings.HasPrefix(testDirName, prefix) { +// continue +// } + +// query, templates := readTestAsset(filesystem, testDirPath) +// assets = append(assets, &MatchTest{ +// ID: testDirName, +// QueryModel: query, +// Templates: templates, +// }) +// } + +// return +// } + +// func TestMatchAllPartedQuery(t *testing.T) { +// for _, td := range readTestAssets(testsFS, "assets/testassets", "test_") { +// t.Run(td.ID, func(t *testing.T) { +// rules := make(map[string]gqt.Doc, len(td.Templates)) +// for _, r := range td.Templates { +// rules[r.ID] = r.Document +// } + +// p := gqlparse.NewParser() +// rm, _ := rmap.New(rules, 0) + +// p.Parse( +// []byte(td.Query), +// []byte(td.OperationName), +// []byte(td.Variables), +// func( +// varVals [][]gqlparse.Token, +// operation []gqlparse.Token, +// selectionSet []gqlparse.Token, +// ) { +// actual := []string{} +// rm.MatchAll( +// varVals, +// operation[0].ID, +// selectionSet, +// func(id string) { +// actual = append(actual, id) +// }, +// ) +// require.Len(t, actual, len(td.Expect)) +// for _, e := range td.Expect { +// require.Contains(t, actual, e) +// } +// }, +// func(err error) { +// t.Fatalf("unexpected error: %v", err) +// }, +// ) +// }) +// } +// } + +// func TestPrintPartedQuery(t *testing.T) { +// for _, td := range []struct { +// template string +// expect string +// }{ +// { +// template: ` +// query { +// a( +// a_0: val = 0 +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// ConstraintValEqual: 0 +// 0 +// `, Hash("query.a.a_0")), +// }, +// { +// template: ` +// query { +// a( +// a_0: val = "a" +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// ConstraintValEqual: 0 +// a +// `, Hash("query.a.a_0")), +// }, +// { +// template: ` +// query { +// a( +// a_0: val = { +// a_00: val = [val = 1, val = 2] +// } +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// ConstraintValEqual: 0 +// -: +// ConstraintValEqual: +// 1 +// -: +// ConstraintValEqual: +// 2 +// `, Hash("query.a.a_0.a_00")), +// }, +// { +// template: ` +// query { +// a( +// a_0: val = [ ... val = [ ... val <= 0 ] ] +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// ConstraintMap: 0 +// ConstraintMap: +// ConstraintValLessOrEqual: +// 0 +// `, Hash("query.a.a_0")), +// }, +// { +// template: ` +// query { +// a( +// a_0: val = [ +// val = { +// a_000: val > 5 +// } +// val = { +// a_010: val = [val = 0, val = 1] +// } +// ] +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// ConstraintValEqual: 0 +// -: +// ConstraintValEqual: +// a_000: +// ConstraintValGreater: +// 5 +// -: +// ConstraintValEqual: +// a_010: +// ConstraintValEqual: +// -: +// ConstraintValEqual: +// 0 +// -: +// ConstraintValEqual: +// 1 +// `, Hash("query.a.a_0")), +// }, +// } { +// t.Run("", func(t *testing.T) { +// b := new(bytes.Buffer) + +// rd, err := gqt.Parse([]byte(td.template)) +// require.False(t, err.IsErr()) +// rm, _ := rmap.New(map[string]gqt.Doc{ +// "rd": rd, +// }, 0) +// rm.Print(b) + +// require.Equal(t, td.expect, b.String()) +// }) +// } +// } + +// func Hash(s string) uint64 { +// h := xxhash.New(0) +// xxhash.Write(&h, s) +// return h.Sum64() +// } + +// func TestNewQueryPart(t *testing.T) { +// for _, td := range []struct { +// query string +// operationName string +// variablesJSON string +// expect []pquery.QueryPart +// }{ +// { +// operationName: "X", +// query: ` +// query X { +// a { +// a0( +// a0_0: { +// a0_00: 1.0 +// } +// a0_1: "no" +// ) { +// a00 +// } +// } +// b( +// b_0: { +// b_00: "go" +// } +// b_1: [0.0, 1.0] +// ) { +// b0 +// } +// c( +// c_0: [ +// { +// c_000: ["hohoho"] +// } +// ] +// c_1: [ +// [ +// { +// c_1000: -1.0 +// c_1001: [1.0, 0.0] +// } +// ] +// [ +// { +// c_1100: "hawk" +// } +// { +// c_1110: "falcon" +// } +// ] +// ] +// ) { +// c0( +// c0_0: 0.0 +// ) { +// c00 +// } +// } +// } +// `, +// expect: []pquery.QueryPart{ +// {ArgLeafIdx: 0, Hash: Hash("query.a.a0.a0_0.a0_00"), Value: 1.0}, +// {ArgLeafIdx: 1, Hash: Hash("query.a.a0.a0_1"), Value: []byte("no")}, +// {ArgLeafIdx: -1, Hash: Hash("query.a.a0.a00"), Value: nil}, +// {ArgLeafIdx: 0, Hash: Hash("query.b.b_0.b_00"), Value: []byte("go")}, +// {ArgLeafIdx: 1, Hash: Hash("query.b.b_1"), Value: &[]any{0.0, 1.0}}, +// {ArgLeafIdx: -1, Hash: Hash("query.b.b0"), Value: nil}, +// { +// ArgLeafIdx: 0, +// Hash: Hash("query.c.c_0"), +// Value: &[]any{ +// MakeMap( +// hamap.Pair[string, any]{ +// Key: "c_000", +// Value: &[]any{ +// []byte("hohoho"), +// }, +// }, +// ), +// }, +// }, +// { +// ArgLeafIdx: 1, +// Hash: Hash("query.c.c_1"), +// Value: &[]any{ +// &[]any{ +// MakeMap( +// hamap.Pair[string, any]{ +// Key: "c_1000", +// Value: -1.0, +// }, +// hamap.Pair[string, any]{ +// Key: "c_1001", +// Value: &[]any{1.0, 0.0}, +// }, +// ), +// }, +// &[]any{ +// MakeMap( +// hamap.Pair[string, any]{ +// Key: "c_1100", +// Value: []byte("hawk"), +// }, +// ), +// MakeMap( +// hamap.Pair[string, any]{ +// Key: "c_1110", +// Value: []byte("falcon"), +// }, +// ), +// }, +// }, +// }, +// {ArgLeafIdx: 0, Hash: Hash("query.c.c0.c0_0"), Value: 0.0}, +// {ArgLeafIdx: -1, Hash: Hash("query.c.c0.c00"), Value: nil}, +// }, +// }, +// { +// operationName: "X", +// query: ` +// mutation X { +// a { +// a0 +// } +// b( +// b_0: 0.0 +// ) { +// b0 +// } +// } +// `, +// expect: []pquery.QueryPart{ +// {ArgLeafIdx: -1, Hash: Hash("mutation.a.a0"), Value: nil}, +// {ArgLeafIdx: 0, Hash: Hash("mutation.b.b_0"), Value: 0.0}, +// {ArgLeafIdx: -1, Hash: Hash("mutation.b.b0"), Value: nil}, +// }, +// }, +// } { +// t.Run("", func(t *testing.T) { +// var i int + +// gqlparse.NewParser().Parse( +// []byte(td.query), +// []byte(td.operationName), +// []byte(td.variablesJSON), +// func( +// varValues [][]gqlparse.Token, +// operation []gqlparse.Token, +// selectionSet []gqlparse.Token, +// ) { +// pquery.NewMaker(0).ParseQuery( +// varValues, +// operation[0].ID, +// selectionSet, +// func(qp pquery.QueryPart) (stop bool) { +// require.Equal(t, td.expect[i], qp) +// i++ +// return false +// }, +// ) +// }, +// func(err error) { +// t.Fatalf("unexpected parser error: %v", err) +// }, +// ) +// }) +// } +// } + +// func TestPrint(t *testing.T) { +// for _, td := range []struct { +// query string +// operationName string +// variablesJSON string +// expect string +// }{ +// { +// operationName: "X", +// query: ` +// query X { +// a { +// a0( +// a0_0: { +// a0_00: 1 +// } +// ) +// } +// } +// `, +// expect: fmt.Sprintf(`%d: 1 +// `, Hash("query.a.a0.a0_0.a0_00")), +// }, +// { +// query: ` +// query { +// a( +// a_0: [ 1, 2 ] +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// -: +// 1 +// -: +// 2 +// `, Hash("query.a.a_0")), +// }, +// { +// query: ` +// query { +// a( +// a_0: [ +// { +// a_000: 5 +// } +// { +// a_010: [ 0, 1 ] +// } +// ] +// ) +// } +// `, +// expect: fmt.Sprintf(`%d: +// -: +// a_000: +// 5 +// -: +// a_010: +// -: +// 0 +// -: +// 1 +// `, Hash("query.a.a_0")), +// }, +// } { +// t.Run("", func(t *testing.T) { +// gqlparse.NewParser().Parse( +// []byte(td.query), +// []byte(td.operationName), +// []byte(td.variablesJSON), +// func( +// varValues [][]gqlparse.Token, +// operation []gqlparse.Token, +// selectionSet []gqlparse.Token, +// ) { +// b := new(bytes.Buffer) +// pquery.NewMaker(0).ParseQuery( +// varValues, +// operation[0].ID, +// selectionSet, +// func(qp pquery.QueryPart) (stop bool) { +// qp.Print(b) +// return false +// }, +// ) +// require.Equal(t, td.expect, b.String()) +// }, +// func(err error) { +// t.Fatalf("unexpected parser error: %v", err) +// }, +// ) +// }) +// } +// } + +// func MakeMap(items ...hamap.Pair[string, any]) *hamap.Map[string, any] { +// m := hamap.New[string, any](len(items), nil) +// for i := range items { +// m.Set(items[i].Key, items[i].Value) +// } +// return m +// } diff --git a/pkg/engine/tests/tests/starwars/friends_connection_args.yaml b/pkg/engine/tests/tests/starwars/friends_connection_args.yaml new file mode 100644 index 0000000..b28c10f --- /dev/null +++ b/pkg/engine/tests/tests/starwars/friends_connection_args.yaml @@ -0,0 +1,15 @@ +query: > + query { + hero(episode: EMPIRE) { + friendsConnection(first: 5, after: "x") { + friends { + appearsIn + name + id + } + totalCount + } + } + } +expect-matches: + - query_hero_expanded diff --git a/pkg/engine/tests/tests/starwars/friends_connection_noargs.yaml b/pkg/engine/tests/tests/starwars/friends_connection_noargs.yaml new file mode 100644 index 0000000..32b4dc4 --- /dev/null +++ b/pkg/engine/tests/tests/starwars/friends_connection_noargs.yaml @@ -0,0 +1,15 @@ +query: > + query { + hero(episode: EMPIRE) { + friendsConnection { + friends { + appearsIn + name + id + } + totalCount + } + } + } +expect-matches: + - query_hero_friends_connection_and_starship diff --git a/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags.yaml b/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags.yaml new file mode 100644 index 0000000..298ed20 --- /dev/null +++ b/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags.yaml @@ -0,0 +1,24 @@ +query: > + query { + hero(episode: EMPIRE) { + friendsConnection { + friends { + appearsIn + name + id + ...humanFrag + ...droidFrag + } + totalCount + } + } + } + fragment humanFrag on Human { + mass + height(unit: METER) + } + fragment droidFrag on Droid { + primaryFunction + } +expect-matches: + - query_hero_friends_connection_and_starship diff --git a/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags_inline.yaml b/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags_inline.yaml new file mode 100644 index 0000000..a108941 --- /dev/null +++ b/pkg/engine/tests/tests/starwars/friends_connection_noargs_frags_inline.yaml @@ -0,0 +1,22 @@ +query: > + query { + hero(episode: EMPIRE) { + friendsConnection { + friends { + appearsIn + name + id + ... on Human { + mass + height(unit: METER) + } + ...on Droid { + primaryFunction + } + } + totalCount + } + } + } +expect-matches: + - query_hero_friends_connection_and_starship diff --git a/pkg/engine/tests/tests/starwars/hero_expanded.yaml b/pkg/engine/tests/tests/starwars/hero_expanded.yaml new file mode 100644 index 0000000..3747276 --- /dev/null +++ b/pkg/engine/tests/tests/starwars/hero_expanded.yaml @@ -0,0 +1,28 @@ +query: > + query { + hero(episode: EMPIRE) { + id + name + appearsIn + friends { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + friendsConnection(first: 1, after: "something") { + totalCount + friends { + id + name + appearsIn + } + } + } + } +expect-matches: + - query_hero_expanded \ No newline at end of file diff --git a/pkg/engine/tests/tests/starwars/hero_minimal.yaml b/pkg/engine/tests/tests/starwars/hero_minimal.yaml new file mode 100644 index 0000000..5b6baa9 --- /dev/null +++ b/pkg/engine/tests/tests/starwars/hero_minimal.yaml @@ -0,0 +1,12 @@ +query: > + query { + hero(episode: JEDI) { + id + appearsIn + } + } +expect-matches: + - query_hero_and_reviews + - query_hero_expanded + - query_hero_friends + - query_hero_with_friends_and_reviews \ No newline at end of file diff --git a/gqlparse/bench_test.go b/pkg/gqlparse/bench_test.go similarity index 52% rename from gqlparse/bench_test.go rename to pkg/gqlparse/bench_test.go index 82adefa..7916e6b 100644 --- a/gqlparse/bench_test.go +++ b/pkg/gqlparse/bench_test.go @@ -3,14 +3,26 @@ package gqlparse_test import ( "testing" - "github.com/graph-guard/ggproxy/gqlparse" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" ) var GI int func BenchmarkParse(b *testing.B) { - r := gqlparse.NewParser() for _, td := range testdata { + var schema *ast.Schema + if td.Data.Schema != "" { + var err error + if schema, err = gqlparser.LoadSchema(&ast.Source{ + Name: "schema.graphqls", Input: td.Data.Schema, + }); err != nil { + b.Fatalf("parsing schema: %v", err) + } + } + + r := gqlparse.NewParser(schema) b.Run(td.Decl, func(b *testing.B) { src := []byte(td.Data.Src) opr := []byte(td.Data.OprName) @@ -32,12 +44,22 @@ func BenchmarkParse(b *testing.B) { } func BenchmarkParseErr(b *testing.B) { - r := gqlparse.NewParser() - for _, td := range testdataErr { - b.Run(td.Decl, func(b *testing.B) { - src := []byte(td.Data.Src) - opr := []byte(td.Data.OprName) - varJSON := []byte(td.Data.VarsJSON) + for _, td := range testsErr { + var schema *ast.Schema + if td.Schema != "" { + var err error + if schema, err = gqlparser.LoadSchema(&ast.Source{ + Name: "schema.graphqls", Input: td.Schema, + }); err != nil { + b.Fatalf("parsing schema: %v", err) + } + } + + r := gqlparse.NewParser(schema) + b.Run(td.Name, func(b *testing.B) { + src := []byte(td.Src) + opr := []byte(td.OprName) + varJSON := []byte(td.VarsJSON) b.ResetTimer() for n := 0; n < b.N; n++ { r.Parse(src, opr, varJSON, func( diff --git a/gqlparse/gqlparse.go b/pkg/gqlparse/gqlparse.go similarity index 70% rename from gqlparse/gqlparse.go rename to pkg/gqlparse/gqlparse.go index e0142a5..deb7d5d 100644 --- a/gqlparse/gqlparse.go +++ b/pkg/gqlparse/gqlparse.go @@ -8,13 +8,14 @@ import ( "io" "strings" - "github.com/graph-guard/ggproxy/gqlparse/internal/graph" - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/segmented" - "github.com/graph-guard/ggproxy/utilities/stack" - "github.com/graph-guard/ggproxy/utilities/unsafe" + "github.com/graph-guard/ggproxy/pkg/container/hamap" + "github.com/graph-guard/ggproxy/pkg/gqlparse/internal/graph" + "github.com/graph-guard/ggproxy/pkg/segmented" + "github.com/graph-guard/ggproxy/pkg/stack" + "github.com/graph-guard/ggproxy/pkg/unsafe" "github.com/graph-guard/gqlscan" "github.com/tidwall/gjson" + "github.com/vektah/gqlparser/v2/ast" ) type Token struct { @@ -24,27 +25,47 @@ type Token struct { Value []byte } +func (t Token) String() string { + if t.ID >= TokenTypeValIndexOffset && + t.ID < TokenTypeFragHostTypeIndexOffset { + return fmt.Sprintf("gqlparse_var_index [%d]", t.VariableIndex()) + } else if t.ID >= TokenTypeFragHostTypeIndexOffset { + return fmt.Sprintf("gqlparse_fht_index [%d]", t.FragHostTypeIndex()) + } + if t.Value == nil { + return t.ID.String() + } + return fmt.Sprintf("%s (%q)", t.ID.String(), string(t.Value)) +} + // VariableIndex returns the index of the varible value if // its a variable index token, otherwise returns -1. func (t Token) VariableIndex() (index int) { - if t.ID >= TokenTypeValIndexOffset { + if t.ID >= TokenTypeValIndexOffset && + t.ID < TokenTypeFragHostTypeIndexOffset { return int(t.ID - TokenTypeValIndexOffset) } return -1 } -// MakeVariableIndexToken creates a variable index token. -func MakeVariableIndexToken(index int, name string) Token { - return Token{ - ID: gqlscan.Token(TokenTypeValIndexOffset + index), - Value: []byte(name), +// FragHostTypeIndex returns the index of the fragment host type, +// if it's a host-type reference token, otherwise returns -1. +func (t Token) FragHostTypeIndex() (index int) { + if t.ID >= TokenTypeFragHostTypeIndexOffset { + return int(t.ID - TokenTypeFragHostTypeIndexOffset) } + return -1 } // TokenTypeValIndexOffset defines the offset that needs to be subtracted // from Token.ID in order to get the index of the value when Token.ID is > 99 const TokenTypeValIndexOffset = 100 +// TokenTypeFragHostTypeIndexOffset defines the offset that needs to be subtracted +// from Token.ID in order to get the index of the fragment host type +// when Token.ID is > 99999 +const TokenTypeFragHostTypeIndexOffset = 100000 + type indexRange struct { IndexStart int IndexEnd int @@ -75,8 +96,9 @@ type typeUnion struct { // NewParser creates a new parser instance. // It's adviced to create only one parser per goroutine // as calling (*Parser).Parse will reset it. -func NewParser() *Parser { +func NewParser(schema *ast.Schema) *Parser { return &Parser{ + schema: schema, gi: graph.NewInspector(), buffer: make([]Token, 0), bufferOpr: make([]Token, 0), @@ -99,7 +121,21 @@ func NewParser() *Parser { } } +type typeStackFrame struct { + HostType *ast.Definition + FieldType *ast.Definition +} + type Parser struct { + schema *ast.Schema + + // schemaTypeStack is used during parsing to track the schema type + // of a particular token's context. + schemaTypeStack stack.Stack[typeStackFrame] + + // fragSpreadHostTypes associates fragment spreads with the host type. + fragSpreadHostTypes []*ast.Definition + // buffer holds the original source tokens buffer []Token @@ -138,7 +174,7 @@ type Parser struct { varsConstructed *segmented.Array[[]byte, Token] // typeStack is used during variable default value parsing - typeStack *stack.Stack[typeUnion] + typeStack stack.Stack[typeUnion] // operations holds index ranges of all operation definitions operations []indexRange @@ -146,6 +182,10 @@ type Parser struct { // fragmentGraphEdges buffers the edges that are passed to gi fragmentGraphEdges []graph.Edge + errTypeUndef ErrorTypeUndef + errFieldUndef ErrorFieldUndef + errArgUndef ErrorArgUndef + errCantBeOfType ErrorCantBeOfType errSyntax ErrorSyntax errOprAnonNonExcl ErrorOprAnonNonExcl errOprNotFound ErrorOprNotFound @@ -164,6 +204,7 @@ type Parser struct { } func (r *Parser) reset() { + r.schemaTypeStack.Reset() r.buffer = r.buffer[:0] r.bufferOpr = r.bufferOpr[:0] r.ordered = r.ordered[:0] @@ -178,6 +219,12 @@ func (r *Parser) reset() { r.fragmentGraphEdges = r.fragmentGraphEdges[:0] } +var ( + typeNameQuery = []byte("Query") + typeNameMutation = []byte("Mutation") + typeNameSubscription = []byte("Subscription") +) + // Parse calls onSuccess in case of success where operation // only contains the relevant set of tokens. // onError is called in case of an error. @@ -199,37 +246,126 @@ func (r *Parser) Parse( var stackCounter int var fragStackCounter int var recentFragDef []byte + var recentHost Token + var recentField *ast.FieldDefinition if serr := gqlscan.Scan(src, func(i *gqlscan.Iterator) bool { - r.buffer = append(r.buffer, Token{ + tk := Token{ ID: i.Token(), Value: i.Value(), - }) + } + r.buffer = append(r.buffer, tk) switch i.Token() { case gqlscan.TokenDefMut: + if r.schema != nil { + if r.schema.Mutation == nil { + r.errTypeUndef.Location = locFromItr(i) + r.errTypeUndef.TypeName = typeNameMutation + isErr = true + onError(&r.errTypeUndef) + return true + } + recentHost = tk + } recentDef = gqlscan.TokenDefMut r.operations = append(r.operations, indexRange{ IndexStart: len(r.buffer) - 1, }) case gqlscan.TokenDefQry: + if r.schema != nil { + if r.schema.Query == nil { + r.errTypeUndef.Location = locFromItr(i) + r.errTypeUndef.TypeName = typeNameQuery + isErr = true + onError(&r.errTypeUndef) + return true + } + recentHost = tk + } recentDef = gqlscan.TokenDefQry r.operations = append(r.operations, indexRange{ IndexStart: len(r.buffer) - 1, }) case gqlscan.TokenDefSub: + if r.schema != nil { + if r.schema.Subscription == nil { + r.errTypeUndef.Location = locFromItr(i) + r.errTypeUndef.TypeName = typeNameSubscription + isErr = true + onError(&r.errTypeUndef) + return true + } + recentHost = tk + } recentDef = gqlscan.TokenDefSub r.operations = append(r.operations, indexRange{ IndexStart: len(r.buffer) - 1, }) + case gqlscan.TokenArgName: + if r.schema != nil { + name := i.Value() + h := r.schemaTypeStack.Top() + a := argByName(recentField, name) + if a == nil { + r.errArgUndef.Location = locFromItr(i) + r.errArgUndef.ArgName = name + r.errArgUndef.FieldName = recentField.Name + r.errArgUndef.HostTypeName = h.HostType.Name + isErr = true + onError(&r.errArgUndef) + return true + } + } + + case gqlscan.TokenField: + recentHost = tk + if r.schema != nil { + name := i.Value() + h := r.schemaTypeStack.Top() + if recentField = fieldByName(h.HostType, name); recentField == nil { + r.errFieldUndef.Location = locFromItr(i) + r.errFieldUndef.FieldName = name + r.errFieldUndef.HostTypeName = h.HostType.Name + isErr = true + onError(&r.errFieldUndef) + return true + } + tp := r.schema.Types[getTypeName(recentField.Type)] + r.schemaTypeStack.TopOffsetFn(0, func(f *typeStackFrame) { + f.FieldType = tp + }) + } + case gqlscan.TokenFragInline: - if i.Value() != nil { - break + recentHost.ID = 0 + if typeName := i.Value(); typeName == nil { + // Anonymous fragment + r.buffer = r.buffer[:len(r.buffer)-1] + fragStackCounter++ + } else if r.schema != nil { + tp := r.schema.Types[string(typeName)] + if tp == nil { + r.errTypeUndef.Location = locFromItr(i) + r.errTypeUndef.TypeName = typeName + isErr = true + onError(&r.errTypeUndef) + return true + } + host := r.schemaTypeStack.Top().HostType + if !isPossibleType(r.schema, typeName, host) { + r.errCantBeOfType.Location = locFromItr(i) + r.errCantBeOfType.Kind = astTypeKindToString(host) + r.errCantBeOfType.HostTypeName = host.Name + r.errCantBeOfType.SpreadTypeName = typeName + isErr = true + onError(&r.errCantBeOfType) + return true + } + r.schemaTypeStack.Push(typeStackFrame{HostType: tp}) } - r.buffer = r.buffer[:len(r.buffer)-1] - fragStackCounter++ case gqlscan.TokenSet: if fragStackCounter > 0 { @@ -237,8 +373,30 @@ func (r *Parser) Parse( break } stackCounter++ + if r.schema != nil { + switch recentHost.ID { + case gqlscan.TokenDefQry: + r.schemaTypeStack.Push(typeStackFrame{ + HostType: r.schema.Query, + }) + case gqlscan.TokenDefMut: + r.schemaTypeStack.Push(typeStackFrame{ + HostType: r.schema.Mutation, + }) + case gqlscan.TokenDefSub: + r.schemaTypeStack.Push(typeStackFrame{ + HostType: r.schema.Subscription, + }) + case gqlscan.TokenField, gqlscan.TokenFragInline: + r.schemaTypeStack.Push(typeStackFrame{ + HostType: r.schemaTypeStack.Top().FieldType, + }) + } + } case gqlscan.TokenSetEnd: + recentHost.ID = 0 + r.schemaTypeStack.Pop() if fragStackCounter > 0 { r.buffer = r.buffer[:len(r.buffer)-1] fragStackCounter-- @@ -260,6 +418,16 @@ func (r *Parser) Parse( recentDef, recentFragDef = 0, nil } case gqlscan.TokenNamedSpread: + if r.schema != nil { + hostType := r.schemaTypeStack.Top().HostType + index := len(r.fragSpreadHostTypes) + r.fragSpreadHostTypes = append(r.fragSpreadHostTypes, hostType) + + // Override the spread token to encode + // the index of the host type in it. + r.buffer[len(r.buffer)-1].ID = makeFragSpreadHostTypeID(index) + } + if recentFragDef == nil { r.entryFrags.Set( r.buffer[len(r.buffer)-1].Value, struct{}{}, @@ -289,14 +457,29 @@ func (r *Parser) Parse( r.fragDefs.Set(i.Value(), fragDef{ indexRange: indexRange{ IndexStart: len(r.buffer) - 1, - }}, - ) + }, + }) + case gqlscan.TokenFragTypeCond: + if r.schema != nil { + r.schemaTypeStack.Reset() + d := r.schema.Types[string(i.Value())] + if d == nil { + r.errTypeUndef.Location = locFromItr(i) + r.errTypeUndef.TypeName = i.Value() + isErr = true + onError(&r.errTypeUndef) + return true + } + r.schemaTypeStack.Push(typeStackFrame{HostType: d}) + } } return false }); serr.IsErr() { if isErr { return } + r.errSyntax.Location.IndexHead = serr.Index + r.errSyntax.Location.IndexTail = serr.Index + 1 r.errSyntax.ScanErr = serr onError(&r.errSyntax) return @@ -511,25 +694,58 @@ func (r *Parser) Parse( // Fill operation buffer selectionSetIndex := len(r.bufferOpr) for ; i < len(o); i++ { - switch o[i].ID { + switch id := o[i].ID; { default: r.bufferOpr = append(r.bufferOpr, o[i]) - case gqlscan.TokenNamedSpread: - // Inline fragment spread outside of fragment definitions + case id >= TokenTypeFragHostTypeIndexOffset && r.schema != nil: + fd, _ := r.fragDefs.Get(o[i].Value) + host := r.fragSpreadHostTypes[id-TokenTypeFragHostTypeIndexOffset] + fragType := r.schema.Types[string(r.buffer[fd.IndexStart+1].Value)] var fragContents []Token - r.fragDefs.GetFn(o[i].Value, func(v *fragDef) { - fragContents = r.buffer[v.IndexStart+3 : v.IndexEnd-1] - v.Used = true - }) + if isTypeDirectlyInlinable(r.schema, host, fragType) { + // No need to wrap the contents in a type condition. + // Inline fragment spread outside of fragment definitions. + if v := r.fragsConstructed.GetItems(o[i].Value); v != nil { + fragContents = v[2 : len(v)-1] + } else { + r.fragDefs.GetFn(o[i].Value, func(v *fragDef) { + fragContents = r.buffer[v.IndexStart+3 : v.IndexEnd] + fragContents[0].ID = gqlscan.TokenFragInline + v.Used = true + }) + } + } else { + // Wrap inlined fields in a type condition. + // Inline fragment spread outside of fragment definitions. + if v := r.fragsConstructed.GetItems(o[i].Value); v != nil { + fragContents = v + } else { + var fragContents []Token + r.fragDefs.GetFn(o[i].Value, func(v *fragDef) { + fragContents = r.buffer[v.IndexStart+1 : v.IndexEnd] + fragContents[0].ID = gqlscan.TokenFragInline + v.Used = true + }) + } + } + r.bufferOpr = append(r.bufferOpr, fragContents...) + case id == gqlscan.TokenNamedSpread: + // Inline fragment spread outside of fragment definitions if v := r.fragsConstructed.GetItems(o[i].Value); v != nil { r.bufferOpr = append(r.bufferOpr, v...) } else { + var fragContents []Token + r.fragDefs.GetFn(o[i].Value, func(v *fragDef) { + fragContents = r.buffer[v.IndexStart+1 : v.IndexEnd] + fragContents[0].ID = gqlscan.TokenFragInline + v.Used = true + }) r.bufferOpr = append(r.bufferOpr, fragContents...) } - case gqlscan.TokenVarRef: + case id == gqlscan.TokenVarRef: // Append variable value index value := r.varsConstructed.Get(o[i].Value) if value.Index == -1 { @@ -561,7 +777,66 @@ func (r *Parser) Parse( onSuccess(r.varValues, r.bufferOpr, r.bufferOpr[selectionSetIndex:]) } +type Location struct{ IndexHead, IndexTail int } + +func locFromItr(i *gqlscan.Iterator) Location { + return Location{IndexHead: i.IndexHead(), IndexTail: i.IndexTail()} +} + +type ErrorTypeUndef struct { + Location + TypeName []byte +} + +func (e *ErrorTypeUndef) Error() string { + return "undefined type: " + string(e.TypeName) +} + +type ErrorFieldUndef struct { + Location + HostTypeName string + FieldName []byte +} + +func (e *ErrorFieldUndef) Error() string { + return "undefined field (" + + string(e.FieldName) + + ") in type " + + e.HostTypeName +} + +type ErrorArgUndef struct { + Location + ArgName []byte + HostTypeName string + FieldName string +} + +func (e *ErrorArgUndef) Error() string { + return "undefined argument (" + + string(e.ArgName) + + ") on field " + + e.HostTypeName + + "." + + string(e.FieldName) +} + +type ErrorCantBeOfType struct { + Location + Kind string + HostTypeName string + SpreadTypeName []byte +} + +func (e *ErrorCantBeOfType) Error() string { + return e.Kind + " " + + e.HostTypeName + + " can never be of type " + + string(e.SpreadTypeName) +} + type ErrorSyntax struct { + Location ScanErr gqlscan.Error } @@ -569,8 +844,7 @@ func (e *ErrorSyntax) Error() string { return fmt.Sprintf("syntax error: %s", e.ScanErr.Error()) } -type ErrorOprAnonNonExcl struct { -} +type ErrorOprAnonNonExcl struct{} func (e *ErrorOprAnonNonExcl) Error() string { return "non-exclusive anonymous operation" @@ -791,6 +1065,14 @@ func (r *Parser) validateAndIndexVars( string(expect.TypeName) != "ID" { isErr = true } + case gqlscan.TokenEnumVal: + i++ + if r.schema != nil { + if t := r.schema.Types[string(expect.TypeName)]; t != nil && + t.Kind != ast.Enum { + isErr = true + } + } case gqlscan.TokenObj: i++ SKIP_OBJ_INTERNALS: @@ -913,16 +1195,18 @@ func WriteValue(w io.Writer, definition []Token) { } } -var strNotNull = []byte("!") -var strSqrBrackLeft = []byte("[") -var strSqrBrackRight = []byte("]") -var strCurlBrackLeft = []byte("{") -var strCurlBrackRight = []byte("}") -var strColSp = []byte(":") -var strComSp = []byte(",") -var strTrue = []byte("true") -var strFalse = []byte("false") -var strNull = []byte("null") +var ( + strNotNull = []byte("!") + strSqrBrackLeft = []byte("[") + strSqrBrackRight = []byte("]") + strCurlBrackLeft = []byte("{") + strCurlBrackRight = []byte("}") + strColSp = []byte(":") + strComSp = []byte(",") + strTrue = []byte("true") + strFalse = []byte("false") + strNull = []byte("null") +) func (r *Parser) writeTypeToStack(t []Token, i int) (typeIndex indexRange) { r.typeStack.Reset() @@ -1068,7 +1352,7 @@ func (r *Parser) constructFrag( rn indexRange, onError func(err error), ) (err bool) { - fragSelections := r.buffer[rn.IndexStart+3 : rn.IndexEnd-1] + fragSelections := r.buffer[rn.IndexStart+1 : rn.IndexEnd] for _, t := range fragSelections { switch t.ID { case gqlscan.TokenNamedSpread: @@ -1086,6 +1370,11 @@ func (r *Parser) constructFrag( r.fragsConstructed.Append( makeVarValueRefTok(valueSeg.Index, t.Value), ) + case gqlscan.TokenFragTypeCond: + r.fragsConstructed.Append(Token{ + ID: gqlscan.TokenFragInline, + Value: t.Value, + }) default: r.fragsConstructed.Append(t) } @@ -1100,3 +1389,58 @@ func makeVarValueRefTok(index int, variableName []byte) Token { Value: variableName, } } + +func makeFragSpreadHostTypeID(index int) gqlscan.Token { + return gqlscan.Token(TokenTypeFragHostTypeIndexOffset + index) +} + +func fieldByName(d *ast.Definition, name []byte) *ast.FieldDefinition { + for _, f := range d.Fields { + if f.Name == string(name) { + return f + } + } + return nil +} + +func argByName(d *ast.FieldDefinition, name []byte) *ast.ArgumentDefinition { + for _, f := range d.Arguments { + if f.Name == string(name) { + return f + } + } + return nil +} + +// isTypeDirectlyInlinable returns true if properties from child +// are directly inlinable into type host without the need for a type condition, +// otherwise returns false. +func isTypeDirectlyInlinable(schema *ast.Schema, host, child *ast.Definition) bool { + return host.Name == child.Name +} + +func isPossibleType(schema *ast.Schema, typeName []byte, host *ast.Definition) bool { + possibleTypes := schema.PossibleTypes[host.Name] + for i := range possibleTypes { + if possibleTypes[i].Name == string(typeName) { + return true + } + } + return false +} + +func astTypeKindToString(d *ast.Definition) string { + switch d.Kind { + case ast.Union: + return "union" + case ast.Interface: + return "interface" + } + return string(d.Kind) +} + +func getTypeName(t *ast.Type) string { + for ; t.Elem != nil; t = t.Elem { + } + return t.NamedType +} diff --git a/gqlparse/gqlparse_test.go b/pkg/gqlparse/gqlparse_test.go similarity index 68% rename from gqlparse/gqlparse_test.go rename to pkg/gqlparse/gqlparse_test.go index 4ff3846..153f6ee 100644 --- a/gqlparse/gqlparse_test.go +++ b/pkg/gqlparse/gqlparse_test.go @@ -6,14 +6,19 @@ import ( "strings" "testing" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/decl" - "github.com/graph-guard/ggproxy/utilities/testeq" + "github.com/graph-guard/ggproxy/pkg/decl" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/testeq" "github.com/graph-guard/gqlscan" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" ) var testdata = []decl.Declaration[TestSuccess]{ + /* SCHEMALESS */ + decl.New(TestSuccess{ Src: "query($e: Episode!) {hero(episode: $e) {id}}", VarsJSON: `{"e": "EMPIRE"}`, @@ -32,7 +37,7 @@ var testdata = []decl.Declaration[TestSuccess]{ Token(gqlscan.TokenField, "hero"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "episode"), - gqlparse.MakeVariableIndexToken(0, "e"), + MakeVariableIndexToken(0, "e"), Token(gqlscan.TokenArgListEnd), Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "id"), @@ -72,7 +77,7 @@ var testdata = []decl.Declaration[TestSuccess]{ // $v Token(gqlscan.TokenArgName, "a"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), Token(gqlscan.TokenSetEnd), @@ -117,8 +122,11 @@ var testdata = []decl.Declaration[TestSuccess]{ ExpectOpr: []gqlparse.Token{ Token(gqlscan.TokenDefQry), Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x"), Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), }, }), // Inline nested named fragments @@ -135,20 +143,65 @@ var testdata = []decl.Declaration[TestSuccess]{ ExpectOpr: []gqlparse.Token{ Token(gqlscan.TokenDefQry), Token(gqlscan.TokenSet), + + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x"), Token(gqlscan.TokenField, "y"), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x2"), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x"), Token(gqlscan.TokenField, "y"), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x2"), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x"), Token(gqlscan.TokenField, "y"), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenField, "s"), Token(gqlscan.TokenSet), + + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "x"), Token(gqlscan.TokenField, "y"), Token(gqlscan.TokenSetEnd), Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), }, }), // Inline variables inside fragments @@ -173,11 +226,14 @@ var testdata = []decl.Declaration[TestSuccess]{ Token(gqlscan.TokenVarListEnd), Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "foo"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "bar"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSetEnd), Token(gqlscan.TokenSetEnd), }, @@ -205,29 +261,43 @@ var testdata = []decl.Declaration[TestSuccess]{ Token(gqlscan.TokenVarListEnd), Token(gqlscan.TokenSet), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenField, "foo"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "bar"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "bar"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "baz"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "bar"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "baz"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenFragInline, "Query"), + Token(gqlscan.TokenSet), Token(gqlscan.TokenField, "baz"), Token(gqlscan.TokenArgList), Token(gqlscan.TokenArgName, "fuz"), - gqlparse.MakeVariableIndexToken(0, "v"), + MakeVariableIndexToken(0, "v"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSetEnd), Token(gqlscan.TokenSetEnd), }, @@ -529,71 +599,71 @@ var testdata = []decl.Declaration[TestSuccess]{ // $v_s: String! = """default value""", Token(gqlscan.TokenArgName, "a1"), - gqlparse.MakeVariableIndexToken(0, "v_s"), + MakeVariableIndexToken(0, "v_s"), // $v_i: Int! = 42, Token(gqlscan.TokenArgName, "a2"), - gqlparse.MakeVariableIndexToken(1, "v_i"), + MakeVariableIndexToken(1, "v_i"), // $v_f: Float! = 3.14, Token(gqlscan.TokenArgName, "a3"), - gqlparse.MakeVariableIndexToken(2, "v_f"), + MakeVariableIndexToken(2, "v_f"), // $v_b: Boolean! = true, Token(gqlscan.TokenArgName, "a4"), - gqlparse.MakeVariableIndexToken(3, "v_b"), + MakeVariableIndexToken(3, "v_b"), // $v_d: ID! = "default ID", Token(gqlscan.TokenArgName, "a5"), - gqlparse.MakeVariableIndexToken(4, "v_d"), + MakeVariableIndexToken(4, "v_d"), // $v_o: InputObj! = {foo: "bar"}, Token(gqlscan.TokenArgName, "a6"), - gqlparse.MakeVariableIndexToken(5, "v_o"), + MakeVariableIndexToken(5, "v_o"), // $v_so: String = null, Token(gqlscan.TokenArgName, "a7"), - gqlparse.MakeVariableIndexToken(6, "v_so"), + MakeVariableIndexToken(6, "v_so"), // $v_io: Int = null, Token(gqlscan.TokenArgName, "a8"), - gqlparse.MakeVariableIndexToken(7, "v_io"), + MakeVariableIndexToken(7, "v_io"), // $v_fo: Float = null, Token(gqlscan.TokenArgName, "a9"), - gqlparse.MakeVariableIndexToken(8, "v_fo"), + MakeVariableIndexToken(8, "v_fo"), // $v_bo: Boolean = null, Token(gqlscan.TokenArgName, "a10"), - gqlparse.MakeVariableIndexToken(9, "v_bo"), + MakeVariableIndexToken(9, "v_bo"), // $v_do: ID = null, Token(gqlscan.TokenArgName, "a11"), - gqlparse.MakeVariableIndexToken(10, "v_do"), + MakeVariableIndexToken(10, "v_do"), // $v_oo: InputObj = null, Token(gqlscan.TokenArgName, "a12"), - gqlparse.MakeVariableIndexToken(11, "v_oo"), + MakeVariableIndexToken(11, "v_oo"), // $v_aon: [String] = null, Token(gqlscan.TokenArgName, "a13"), - gqlparse.MakeVariableIndexToken(12, "v_aon"), + MakeVariableIndexToken(12, "v_aon"), // $v_aoy: [String] = [], Token(gqlscan.TokenArgName, "a14"), - gqlparse.MakeVariableIndexToken(13, "v_aoy"), + MakeVariableIndexToken(13, "v_aoy"), // $v_a_so: [String]! = ["okay", null], Token(gqlscan.TokenArgName, "a15"), - gqlparse.MakeVariableIndexToken(14, "v_a_so"), + MakeVariableIndexToken(14, "v_a_so"), // $v_a_ao_so: [[String]]! = [["okay", null], [], null], Token(gqlscan.TokenArgName, "a16"), - gqlparse.MakeVariableIndexToken(15, "v_a_ao_so"), + MakeVariableIndexToken(15, "v_a_ao_so"), // $v_a_io: [InputObj]! = [{a: "1", b: null, c: 42, d: false}, null], Token(gqlscan.TokenArgName, "a17"), - gqlparse.MakeVariableIndexToken(16, "v_a_io"), + MakeVariableIndexToken(16, "v_a_io"), Token(gqlscan.TokenArgListEnd), Token(gqlscan.TokenSetEnd), @@ -847,73 +917,142 @@ var testdata = []decl.Declaration[TestSuccess]{ // $v_s: String! = """default value""", Token(gqlscan.TokenArgName, "a1"), - gqlparse.MakeVariableIndexToken(0, "v_s"), + MakeVariableIndexToken(0, "v_s"), // $v_i: Int! = 42, Token(gqlscan.TokenArgName, "a2"), - gqlparse.MakeVariableIndexToken(1, "v_i"), + MakeVariableIndexToken(1, "v_i"), // $v_f: Float! = 3.14, Token(gqlscan.TokenArgName, "a3"), - gqlparse.MakeVariableIndexToken(2, "v_f"), + MakeVariableIndexToken(2, "v_f"), // $v_b: Boolean! = true, Token(gqlscan.TokenArgName, "a4"), - gqlparse.MakeVariableIndexToken(3, "v_b"), + MakeVariableIndexToken(3, "v_b"), // $v_d: ID! = "default ID", Token(gqlscan.TokenArgName, "a5"), - gqlparse.MakeVariableIndexToken(4, "v_d"), + MakeVariableIndexToken(4, "v_d"), // $v_o: InputObj! = {foo: "bar"}, Token(gqlscan.TokenArgName, "a6"), - gqlparse.MakeVariableIndexToken(5, "v_o"), + MakeVariableIndexToken(5, "v_o"), // $v_so: String = null, Token(gqlscan.TokenArgName, "a7"), - gqlparse.MakeVariableIndexToken(6, "v_so"), + MakeVariableIndexToken(6, "v_so"), // $v_io: Int = null, Token(gqlscan.TokenArgName, "a8"), - gqlparse.MakeVariableIndexToken(7, "v_io"), + MakeVariableIndexToken(7, "v_io"), // $v_fo: Float = null, Token(gqlscan.TokenArgName, "a9"), - gqlparse.MakeVariableIndexToken(8, "v_fo"), + MakeVariableIndexToken(8, "v_fo"), // $v_bo: Boolean = null, Token(gqlscan.TokenArgName, "a10"), - gqlparse.MakeVariableIndexToken(9, "v_bo"), + MakeVariableIndexToken(9, "v_bo"), // $v_do: ID = null, Token(gqlscan.TokenArgName, "a11"), - gqlparse.MakeVariableIndexToken(10, "v_do"), + MakeVariableIndexToken(10, "v_do"), // $v_oo: InputObj = null, Token(gqlscan.TokenArgName, "a12"), - gqlparse.MakeVariableIndexToken(11, "v_oo"), + MakeVariableIndexToken(11, "v_oo"), // $v_aon: [String] = null, Token(gqlscan.TokenArgName, "a13"), - gqlparse.MakeVariableIndexToken(12, "v_aon"), + MakeVariableIndexToken(12, "v_aon"), // $v_aoy: [String] = [], Token(gqlscan.TokenArgName, "a14"), - gqlparse.MakeVariableIndexToken(13, "v_aoy"), + MakeVariableIndexToken(13, "v_aoy"), // $v_a_so: [String]! = ["okay", null], Token(gqlscan.TokenArgName, "a15"), - gqlparse.MakeVariableIndexToken(14, "v_a_so"), + MakeVariableIndexToken(14, "v_a_so"), // $v_a_ao_so: [[String]]! = [["okay", null], [], null], Token(gqlscan.TokenArgName, "a16"), - gqlparse.MakeVariableIndexToken(15, "v_a_ao_so"), + MakeVariableIndexToken(15, "v_a_ao_so"), // $v_a_io: [InputObj]! = [{a: "1", b: null, c: 42, d: false}, null], Token(gqlscan.TokenArgName, "a17"), - gqlparse.MakeVariableIndexToken(16, "v_a_io"), + MakeVariableIndexToken(16, "v_a_io"), + + Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), + }, + }), + + /* SCHEMA AWARE */ + decl.New(TestSuccess{ + Schema: "type Query { i:Int! }", + Src: ` + query { ...f } + fragment f on Query { i } + `, + ExpectOpr: []gqlparse.Token{ + Token(gqlscan.TokenDefQry), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenField, "i"), + Token(gqlscan.TokenSetEnd), + }, + }), + decl.New(TestSuccess{ + Schema: ` + type Query { + hero(episode: Episode!):Character! + } + enum Episode { JEDI, EMPIRE } + interface Character { id:ID!, name:String! } + type Droid implements Character { + id:ID!, name:String!, primaryFunction:String! } + type Human implements Character { + id:ID!, name:String!, mass:Float + } + `, + Src: ` + query { + hero(episode: EMPIRE) { + ...onChar + ...onDroid + ...onHuman + } + } + fragment onChar on Character { id name } + fragment onDroid on Droid { primaryFunction } + fragment onHuman on Human { mass } + `, + ExpectOpr: []gqlparse.Token{ + Token(gqlscan.TokenDefQry), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenField, "hero"), + Token(gqlscan.TokenArgList), + Token(gqlscan.TokenArgName, "episode"), + Token(gqlscan.TokenEnumVal, "EMPIRE"), Token(gqlscan.TokenArgListEnd), + Token(gqlscan.TokenSet), + + // Inlined directly + Token(gqlscan.TokenField, "id"), + Token(gqlscan.TokenField, "name"), + + // Type-cond wrapped inlines + Token(gqlscan.TokenFragInline, "Droid"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenField, "primaryFunction"), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenFragInline, "Human"), + Token(gqlscan.TokenSet), + Token(gqlscan.TokenField, "mass"), + Token(gqlscan.TokenSetEnd), + Token(gqlscan.TokenSetEnd), Token(gqlscan.TokenSetEnd), }, @@ -932,7 +1071,17 @@ func TestOK(t *testing.T) { }) require.False(t, err.IsErr()) - gqlparse.NewParser().Parse( + var schema *ast.Schema + if td.Data.Schema != "" { + var err error + schema, err = gqlparser.LoadSchema(&ast.Source{ + Name: "schema.graphqls", + Input: td.Data.Schema, + }) + require.NoError(t, err, "parsing schema") + } + + gqlparse.NewParser(schema).Parse( []byte(td.Data.Src), []byte(td.Data.OprName), []byte(td.Data.VarsJSON), @@ -941,53 +1090,55 @@ func TestOK(t *testing.T) { operation []gqlparse.Token, selectionSet []gqlparse.Token, ) { - // fmt.Printf("expected: (%d)\n", len(td.Data.ExpectOpr)) - // for i, x := range td.Data.ExpectOpr { - // fmt.Printf(" %d: ", i) - // if i := x.VariableIndex(); i > -1 { - // fmt.Printf("variable value identifier (%d)", i) - // } else { - // fmt.Printf(" %v", x.ID) - // } - // if x.Value == nil { - // fmt.Print("\n") - // } else { - // fmt.Printf(" (%q)\n", string(x.Value)) - // } - // } - // fmt.Println(" ") - // fmt.Printf("operation: (%d)\n", len(operation)) - // for i, x := range operation { - // fmt.Printf(" %d: ", i) - // if i := x.VariableIndex(); i > -1 { - // fmt.Printf("variable value identifier (%d)", i) - // } else { - // fmt.Printf(" %v", x.ID) - // } - // if x.Value == nil { - // fmt.Print("\n") - // } else { - // fmt.Printf(" (%q)\n", string(x.Value)) - // } - // } - // fmt.Println(" ") - - testeq.Slices( - t, "token", - td.Data.ExpectOpr, operation, - func(expected, actual gqlparse.Token) (errMsg string) { - if expected.ID != actual.ID || - string(expected.Value) != string(actual.Value) { - return fmt.Sprintf( - "expected {%s}; received: {%s}", - stringifyToken(expected), - stringifyToken(actual), - ) + if !assert.ObjectsAreEqual(td.Data.ExpectOpr, operation) { + fmt.Printf("expected: (%d)\n", len(td.Data.ExpectOpr)) + for i, x := range td.Data.ExpectOpr { + fmt.Printf(" %d: ", i) + if i := x.VariableIndex(); i > -1 { + fmt.Printf("variable value identifier (%d)", i) + } else { + fmt.Printf(" %v", x.ID) } - return "" - }, - stringifyToken, - ) + if x.Value == nil { + fmt.Print("\n") + } else { + fmt.Printf(" (%q)\n", string(x.Value)) + } + } + fmt.Println(" ") + fmt.Printf("operation: (%d)\n", len(operation)) + for i, x := range operation { + fmt.Printf(" %d: ", i) + if i := x.VariableIndex(); i > -1 { + fmt.Printf("variable value identifier (%d)", i) + } else { + fmt.Printf(" %v", x.ID) + } + if x.Value == nil { + fmt.Print("\n") + } else { + fmt.Printf(" (%q)\n", string(x.Value)) + } + } + t.Error("unexpected tokens") + } + + // testeq.Slices( + // t, "token", + // td.Data.ExpectOpr, operation, + // func(expected, actual gqlparse.Token) (errMsg string) { + // if expected.ID != actual.ID || + // string(expected.Value) != string(actual.Value) { + // return fmt.Sprintf( + // "expected {%s}; received: {%s}", + // stringifyToken(expected), + // stringifyToken(actual), + // ) + // } + // return "" + // }, + // stringifyToken, + // ) variableValues := make(map[string][]gqlparse.Token) for _, t := range operation { @@ -1046,18 +1197,20 @@ func TestOK(t *testing.T) { } } -var testdataErr = []decl.Declaration[TestError]{ +var testsErr = []TestError{ // Operation not found - decl.New(TestError{ - Src: `query A {x}, query B {x}`, + { + Name: "operation_not_found_empty_operation_name", + Src: `query A {x}, query B {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorOprNotFound{ OperationName: []byte(""), }, err) require.Equal(t, `operation "" not found`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "operation_not_found", Src: `query A {x}, query B {x}`, OprName: "C", Check: func(t *testing.T, err error) { @@ -1066,8 +1219,9 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `operation "C" not found`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "operation_not_found_single_anonymous", Src: `{x}`, OprName: "A", Check: func(t *testing.T, err error) { @@ -1076,62 +1230,77 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `operation "A" not found`, err.Error()) }, - }), + }, // Non-exclusive anonymous operation - decl.New(TestError{ - Src: `{x}, query{x}`, + { + Name: "non_exclusive_anonymous_operation_query_1", + Src: `{x}, query{x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorOprAnonNonExcl{}, err) require.Equal(t, `non-exclusive anonymous operation`, err.Error()) }, - }), - decl.New(TestError{ - Src: `{x}, mutation M {x}`, + }, + { + Name: "non_exclusive_anonymous_operation_query_2", + Src: `query A {x}, query {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorOprAnonNonExcl{}, err) require.Equal(t, `non-exclusive anonymous operation`, err.Error()) }, - }), - decl.New(TestError{ - Src: `query A {x}, query {x}`, + }, + { + Name: "non_exclusive_anonymous_operation_mutation", + Src: `{x}, mutation M {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorOprAnonNonExcl{}, err) require.Equal(t, `non-exclusive anonymous operation`, err.Error()) }, - }), + }, + { + Name: "non_exclusive_anonymous_operation_subscription", + Src: `{x}, subscription S {x}`, + Check: func(t *testing.T, err error) { + require.Equal(t, &gqlparse.ErrorOprAnonNonExcl{}, err) + require.Equal(t, `non-exclusive anonymous operation`, err.Error()) + }, + }, // Redeclared operation - decl.New(TestError{ - Src: `query A {x}, query A {x}`, + { + Name: "redeclared_operation_query", + Src: `query A {x}, query A {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorRedecOpr{ OperationName: []byte("A"), }, err) require.Equal(t, `operation "A" redeclared`, err.Error()) }, - }), - decl.New(TestError{ - Src: `query M {x}, mutation M {x}`, + }, + { + Name: "redeclared_operation_query_mutation", + Src: `query M {x}, mutation M {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorRedecOpr{ OperationName: []byte("M"), }, err) require.Equal(t, `operation "M" redeclared`, err.Error()) }, - }), - decl.New(TestError{ - Src: `query S {x}, subscription S {x}`, + }, + { + Name: "redeclared_operation_subscription", + Src: `query S {x}, subscription S {x}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorRedecOpr{ OperationName: []byte("S"), }, err) require.Equal(t, `operation "S" redeclared`, err.Error()) }, - }), + }, // Redeclared fragment - decl.New(TestError{ + { + Name: "redeclared_fragment", Src: `{...f} fragment f on Query {x} fragment f on Query {x}`, @@ -1141,10 +1310,11 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `fragment "f" redeclared`, err.Error()) }, - }), + }, // Unused fragment - decl.New(TestError{ + { + Name: "unused_fragment", Src: `{...f} fragment f on Query {x} fragment a on Query {x}`, @@ -1154,10 +1324,11 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `fragment "a" unused`, err.Error()) }, - }), + }, // Recursive fragment - decl.New(TestError{ + { + Name: "recursive_fragment", Src: `{...a} fragment a on Query {...a}`, Check: func(t *testing.T, err error) { @@ -1171,8 +1342,9 @@ var testdataErr = []decl.Declaration[TestError]{ t, `fragment recursion detected at: a.a`, err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "redeclared_fragment_level2", Src: `{...a} fragment a on Query {...b} fragment b on Query {...a}`, @@ -1188,8 +1360,9 @@ var testdataErr = []decl.Declaration[TestError]{ t, `fragment recursion detected at: a.b.a`, err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "redeclared_fragment_complex", Src: `{...a, ...a1} fragment a on Query {...b} fragment a1 on Query {...b} @@ -1208,19 +1381,21 @@ var testdataErr = []decl.Declaration[TestError]{ t, `fragment recursion detected at: b.c.a1.b`, err.Error(), ) }, - }), + }, // Redeclared variable - decl.New(TestError{ - Src: `query($v1:String, $v1:Int){f(a:$v1)}`, + { + Name: "redeclared_variable", + Src: `query($v1:String, $v1:Int){f(a:$v1)}`, Check: func(t *testing.T, err error) { require.Equal(t, &gqlparse.ErrorRedeclVar{ VariableName: []byte("v1"), }, err) require.Equal(t, `variable "v1" redeclared`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "redeclared_variable_in_named_query", Src: `query Q ($v2:String, $v2:Int){f(a:$v2)}`, OprName: "Q", Check: func(t *testing.T, err error) { @@ -1229,11 +1404,12 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `variable "v2" redeclared`, err.Error()) }, - }), + }, // Default value wrong type - decl.New(TestError{ - Src: `query ($v:String=true) { f(a:$v) }`, + { + Name: "default_value_wrong_type_boolean_as_string", + Src: `query ($v:String=true) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1245,9 +1421,11 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:[String]="okay") { f(a:$v) }`, + }, + { + // TODO: make sure this is really illegal and not a case of legal coersion + Name: "default_value_wrong_type_string_as_array_string", + Src: `query ($v:[String]="okay") { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1259,9 +1437,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Int=42.5) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_float_as_int", + Src: `query ($v:Int=42.5) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1273,9 +1452,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Float=false) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_boolean_as_float", + Src: `query ($v:Float=false) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1287,9 +1467,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Boolean=1) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_int_as_boolean", + Src: `query ($v:Boolean=1) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1301,9 +1482,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Input=1) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_int_as_input", + Src: `query ($v:Input=1) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1315,9 +1497,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Int={foo:"bar", baz:42}) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_input_as_int", + Src: `query ($v:Int={foo:"bar", baz:42}) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1329,9 +1512,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Int=[]) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_array_empty_as_int", + Src: `query ($v:Int=[]) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1343,9 +1527,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Int=[{x:2,y:4}, null, {x:8,y:8}]) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_array_input_as_int", + Src: `query ($v:Int=[{x:2,y:4}, null, {x:8,y:8}]) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1357,9 +1542,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ - Src: `query ($v:Int! = null) { f(a:$v) }`, + }, + { + Name: "default_value_wrong_type_null_as_int_notnull", + Src: `query ($v:Int! = null) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorUnexpValType{}, err) @@ -1371,10 +1557,11 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), + }, // JSON variable wrong type - decl.New(TestError{ + { + Name: "json_variable_wrong_type_boolean_as_string", Src: `query ($v:String) { f(a:$v) }`, VarsJSON: `{"v":true}`, Check: func(t *testing.T, err error) { @@ -1388,8 +1575,10 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + // TODO: make sure this is really illegal and not a case of legal coersion + Name: "json_variable_wrong_type_string_as_array_string", Src: `query ($v:[String]) { f(a:$v) }`, VarsJSON: `{"v":"okay"}`, Check: func(t *testing.T, err error) { @@ -1403,8 +1592,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_float_as_int", Src: `query ($v:Int) { f(a:$v) }`, VarsJSON: `{"v":42.5}`, Check: func(t *testing.T, err error) { @@ -1418,8 +1608,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_boolean_as_float", Src: `query ($v:Float) { f(a:$v) }`, VarsJSON: `{"v":false}`, Check: func(t *testing.T, err error) { @@ -1433,8 +1624,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_int_as_boolean", Src: `query ($v:Boolean) { f(a:$v) }`, VarsJSON: `{"v":1}`, Check: func(t *testing.T, err error) { @@ -1448,8 +1640,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_int_as_input", Src: `query ($v:Input) { f(a:$v) }`, VarsJSON: `{"v":1}`, Check: func(t *testing.T, err error) { @@ -1463,8 +1656,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_input_as_int", Src: `query ($v:Int) { f(a:$v) }`, VarsJSON: `{"v":{"foo":"bar","baz":42}}`, Check: func(t *testing.T, err error) { @@ -1478,8 +1672,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_array_empty_as_int", Src: `query ($v:Int) { f(a:$v) }`, VarsJSON: `{"v":[]}`, Check: func(t *testing.T, err error) { @@ -1493,8 +1688,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_array_input_as_int", Src: `query ($v:Int) { f(a:$v) }`, VarsJSON: `{"v":[{"x":2,"y":4}, null, {"x":8,"y":8}]}`, Check: func(t *testing.T, err error) { @@ -1508,8 +1704,9 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), - decl.New(TestError{ + }, + { + Name: "json_variable_wrong_type_null_as_int_notnull", Src: `query ($v:Int!) { f(a:$v) }`, VarsJSON: `{"v":null}`, Check: func(t *testing.T, err error) { @@ -1523,10 +1720,11 @@ var testdataErr = []decl.Declaration[TestError]{ err.Error(), ) }, - }), + }, // Invalid variables JSON (non-object) - decl.New(TestError{ + { + Name: "invalid_variable_json_array_string", Src: `query ($v:String) { f(a:$v) }`, VarsJSON: `["okay"]`, Check: func(t *testing.T, err error) { @@ -1535,8 +1733,9 @@ var testdataErr = []decl.Declaration[TestError]{ require.Equal(t, `expected JSON object for variables, `+ `received: ["okay"]`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "invalid_variable_json_int", Src: `query ($v:Int!) { f(a:$v) }`, VarsJSON: `42`, Check: func(t *testing.T, err error) { @@ -1545,21 +1744,23 @@ var testdataErr = []decl.Declaration[TestError]{ require.Equal(t, `expected JSON object for variables, `+ `received: 42`, err.Error()) }, - }), + }, // Query syntax error - decl.New(TestError{ - Src: `query ($v:String = ) { f(a:$v) }`, + { + Name: "invalid_query_syntax", + Src: `query ($v:String = ) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorSyntax{}, err) require.Equal(t, `syntax error: error at index 19 (')'):`+ ` unexpected token; expected enum value`, err.Error()) }, - }), + }, // Invalid variables JSON (syntax error) - decl.New(TestError{ + { + Name: "invalid_variable_json_syntax_missing_comma", Src: `query ($v:String) { f(a:$v) }`, VarsJSON: `{"v":"first" "missing-comma": "second"}`, Check: func(t *testing.T, err error) { @@ -1567,8 +1768,9 @@ var testdataErr = []decl.Declaration[TestError]{ require.IsType(t, &gqlparse.ErrorVarJSONSyntax{}, err) require.Equal(t, `variables JSON syntax error`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "invalid_variable_json_syntax_noquotes_key", Src: `query ($v:Int!) { f(a:$v) }`, VarsJSON: `{v:42}`, Check: func(t *testing.T, err error) { @@ -1576,8 +1778,9 @@ var testdataErr = []decl.Declaration[TestError]{ require.IsType(t, &gqlparse.ErrorVarJSONSyntax{}, err) require.Equal(t, `variables JSON syntax error`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "invalid_variable_json_syntax_noquotes_value", Src: `query ($v:String!) { f(a:$v) }`, VarsJSON: `{"v": missing_quotes}`, Check: func(t *testing.T, err error) { @@ -1585,10 +1788,11 @@ var testdataErr = []decl.Declaration[TestError]{ require.IsType(t, &gqlparse.ErrorVarJSONSyntax{}, err) require.Equal(t, `variables JSON syntax error`, err.Error()) }, - }), + }, // Undeclared variable - decl.New(TestError{ + { + Name: "undeclared_variable", Src: `{ f(a:$u) }`, VarsJSON: `{"u":42}`, Check: func(t *testing.T, err error) { @@ -1596,8 +1800,9 @@ var testdataErr = []decl.Declaration[TestError]{ require.IsType(t, &gqlparse.ErrorVarUndeclared{}, err) require.Equal(t, `variable "u" undeclared`, err.Error()) }, - }), - decl.New(TestError{ + }, + { + Name: "undeclared_variable_json", Src: `query ($v:String!) { f(a:$u) }`, VarsJSON: `{"v":"okay","u":42}`, Check: func(t *testing.T, err error) { @@ -1605,21 +1810,23 @@ var testdataErr = []decl.Declaration[TestError]{ require.IsType(t, &gqlparse.ErrorVarUndeclared{}, err) require.Equal(t, `variable "u" undeclared`, err.Error()) }, - }), + }, // Undefined variable value - decl.New(TestError{ - Src: `query ($v:String!) { f(a:$v) }`, + { + Name: "undefined_variable_value", + Src: `query ($v:String!) { f(a:$v) }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.IsType(t, &gqlparse.ErrorVarUndefined{}, err) require.Equal(t, `variable "v" undefined`, err.Error()) }, - }), + }, // Fragment undefined - decl.New(TestError{ - Src: `{ ...f }`, + { + Name: "fragment_undefined", + Src: `{ ...f }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.Equal(t, &gqlparse.ErrorFragUndefined{ @@ -1627,9 +1834,10 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `fragment "f" undefined`, err.Error()) }, - }), - decl.New(TestError{ - Src: `{ ...f }, fragment f on Query { ...x }`, + }, + { + Name: "fragment_undefined_level2", + Src: `{ ...f }, fragment f on Query { ...x }`, Check: func(t *testing.T, err error) { require.Error(t, err) require.Equal(t, &gqlparse.ErrorFragUndefined{ @@ -1637,10 +1845,11 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, `fragment "x" undefined`, err.Error()) }, - }), + }, // Fragment limit exceeded - decl.New(TestError{ + { + Name: "fragment_limit_exceeded", Src: `{ ...f0, ...f1, ...f2, ...f3, ...f4, ...f5, ...f6, ...f7, ...f8, ...f9, @@ -1806,16 +2015,364 @@ var testdataErr = []decl.Declaration[TestError]{ }, err) require.Equal(t, "fragment limit (128) exceeded", err.Error()) }, - }), + }, + + /* TYPE ERRORS */ + + { + Name: "undefined_field_in_query_type", + Schema: "type Query { foo:String! }", + Src: `{ bar }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 2, IndexHead: 5}, + FieldName: []byte("bar"), + HostTypeName: "Query", + }, err) + require.Equal(t, `undefined field (bar) in type Query`, err.Error()) + }, + }, + { + Name: "undefined_field_in_mutation_type", + Schema: "type Mutation { foo:String! }", + Src: `mutation { bar }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 11, IndexHead: 14}, + FieldName: []byte("bar"), + HostTypeName: "Mutation", + }, err) + require.Equal(t, `undefined field (bar) in type Mutation`, err.Error()) + }, + }, + { + Name: "undefined_field_in_subscription_type", + Schema: "type Subscription { foo:String! }", + Src: `subscription { bar }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 15, IndexHead: 18}, + FieldName: []byte("bar"), + HostTypeName: "Subscription", + }, err) + require.Equal(t, `undefined field (bar) in type Subscription`, err.Error()) + }, + }, + { + Name: "undefined_field_in_named_type", + Schema: ` + type Query { foo:Foo! } + type Foo { bar:String! } + `, + Src: `{ foo { bazz } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 8, IndexHead: 12}, + FieldName: []byte("bazz"), + HostTypeName: "Foo", + }, err) + require.Equal(t, `undefined field (bazz) in type Foo`, err.Error()) + }, + }, + { + Name: "undefined_field_in_named_subtype", + Schema: ` + type Query { foo:Foo! } + type Foo { bar:Bar! } + type Bar { bar:String! } + `, + Src: `{ foo { bar { bazz } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 14, IndexHead: 18}, + FieldName: []byte("bazz"), + HostTypeName: "Bar", + }, err) + require.Equal(t, `undefined field (bazz) in type Bar`, err.Error()) + }, + }, + { + Name: "undefined_field_in_interface", + Schema: ` + type Query { interface:Interface! x:X! } + interface Interface { bar:String! } + type X implements Interface { bar:String! } + `, + Src: `{ interface { bazz } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 14, IndexHead: 18}, + FieldName: []byte("bazz"), + HostTypeName: "Interface", + }, err) + require.Equal(t, `undefined field (bazz) in type Interface`, err.Error()) + }, + }, + { + Name: "undefined_field_in_inline_frag", + Schema: ` + type Query { u:U } + union U = Foo | Bar + type Foo { foo:String } + type Bar { bar:String } + `, + Src: `{ u { ... on Foo { bar } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 19, IndexHead: 22}, + FieldName: []byte("bar"), + HostTypeName: "Foo", + }, err) + require.Equal(t, `undefined field (bar) in type Foo`, err.Error()) + }, + }, + { + Name: "undefined_field_in_sametype_inline_frag_inside_query", + Schema: ` + type Query { foo:String! } + `, + Src: `{ ... on Query { bar } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 17, IndexHead: 20}, + FieldName: []byte("bar"), + HostTypeName: "Query", + }, err) + require.Equal(t, `undefined field (bar) in type Query`, err.Error()) + }, + }, + { + Name: "undefined_field_in_2d_sametype_inline_frag_inside_query", + Schema: ` + type Query { foo:String! } + `, + Src: `{ ... on Query { ... on Query { bar } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 32, IndexHead: 35}, + FieldName: []byte("bar"), + HostTypeName: "Query", + }, err) + require.Equal(t, `undefined field (bar) in type Query`, err.Error()) + }, + }, + { + Name: "undefined_field_in_anonymous_inline_frag_inside_query", + Schema: ` + type Query { foo:String! } + `, + Src: `{ ... { bar } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 8, IndexHead: 11}, + FieldName: []byte("bar"), + HostTypeName: "Query", + }, err) + require.Equal(t, `undefined field (bar) in type Query`, err.Error()) + }, + }, + { + Name: "undefined_field_in_2d_anonymous_inline_frag_inside_query", + Schema: ` + type Query { foo:String! } + `, + Src: `{ ... { ... { bar } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorFieldUndef{ + Location: gqlparse.Location{IndexTail: 14, IndexHead: 17}, + FieldName: []byte("bar"), + HostTypeName: "Query", + }, err) + require.Equal(t, `undefined field (bar) in type Query`, err.Error()) + }, + }, + + { + Name: "undefined_argument_1", + Schema: "type Query { foo(ok:String!):String! }", + Src: `{ foo(ok:"ok", inexistent:"inexistent") }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorArgUndef{ + Location: gqlparse.Location{IndexTail: 15, IndexHead: 25}, + ArgName: []byte("inexistent"), + FieldName: "foo", + HostTypeName: "Query", + }, err) + require.Equal( + t, + `undefined argument (inexistent) on field Query.foo`, + err.Error(), + ) + }, + }, + { + Name: "undefined_argument_2", + Schema: ` + type Query { foo:Foo! } + type Foo { i:Int! } + `, + Src: `{ foo { i(x:null) } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorArgUndef{ + Location: gqlparse.Location{IndexTail: 10, IndexHead: 11}, + ArgName: []byte("x"), + FieldName: "i", + HostTypeName: "Foo", + }, err) + require.Equal( + t, `undefined argument (x) on field Foo.i`, err.Error(), + ) + }, + }, + { + Name: "undefined_argument_2", + Schema: ` + type Query { mainCharacter:Character! } + interface Character { name: String! } + type Droid implements Character { name: String! } + `, + Src: `{ mainCharacter { ... on Droid { name(lang:DE) } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorArgUndef{ + Location: gqlparse.Location{IndexTail: 38, IndexHead: 42}, + ArgName: []byte("lang"), + FieldName: "name", + HostTypeName: "Droid", + }, err) + require.Equal( + t, `undefined argument (lang) on field Droid.name`, err.Error(), + ) + }, + }, + + { + Name: "undefined_type_in_inline_fragment", + Schema: ` + type Query { u: U! } + union U = Bar | Bazz + type Bar { bar:String! } + type Bazz { bazz:String! } + `, + Src: `{ u { ... on Foo { foo } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorTypeUndef{ + Location: gqlparse.Location{IndexTail: 13, IndexHead: 16}, + TypeName: []byte("Foo"), + }, err) + require.Equal(t, `undefined type: Foo`, err.Error()) + }, + }, + { + Name: "undefined_type_in_fragment", + Schema: ` + type Query { foo:String } + `, + Src: `{ ...f } fragment f on Foo { foo }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorTypeUndef{ + Location: gqlparse.Location{IndexTail: 23, IndexHead: 26}, + TypeName: []byte("Foo"), + }, err) + require.Equal(t, `undefined type: Foo`, err.Error()) + }, + }, + + { + Name: "unsupported_type_in_union", + Schema: ` + type Query { u: U! } + union U = Bar | Baz + type Foo { foo:String! } + type Bar { bar:String! } + type Baz { baz:String! } + `, + Src: `{ u { ... on Foo { foo } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorCantBeOfType{ + Location: gqlparse.Location{IndexTail: 13, IndexHead: 16}, + Kind: "union", + HostTypeName: "U", + SpreadTypeName: []byte("Foo"), + }, err) + require.Equal(t, `union U can never be of type Foo`, err.Error()) + }, + }, + { + Name: "unsupported_type_in_interface", + Schema: ` + type Query { i:Iface! } + interface Iface { x:String! } + type Foo { x:String! } + type Bar implements Iface { x:String! } + type Baz implements Iface { x:String! } + `, + Src: `{ i { ... on Foo { x } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorCantBeOfType{ + Location: gqlparse.Location{IndexTail: 13, IndexHead: 16}, + Kind: "interface", + HostTypeName: "Iface", + SpreadTypeName: []byte("Foo"), + }, err) + require.Equal(t, `interface Iface can never be of type Foo`, err.Error()) + }, + }, + { + Name: "unsupported_type_in_object", + Schema: ` + type Query { foo:Foo! } + type Foo { foo:String! } + type Bar { bar:String! } + `, + Src: `{ foo { ... on Bar { bar } } }`, + Check: func(t *testing.T, err error) { + require.Error(t, err) + require.Equal(t, &gqlparse.ErrorCantBeOfType{ + Location: gqlparse.Location{IndexTail: 15, IndexHead: 18}, + Kind: "OBJECT", + HostTypeName: "Foo", + SpreadTypeName: []byte("Bar"), + }, err) + require.Equal(t, `OBJECT Foo can never be of type Bar`, err.Error()) + }, + }, } func TestErr(t *testing.T) { - for _, td := range testdataErr { - t.Run(td.Decl, func(t *testing.T) { - gqlparse.NewParser().Parse( - []byte(td.Data.Src), - []byte(td.Data.OprName), - []byte(td.Data.VarsJSON), + for _, td := range testsErr { + t.Run(td.Name, func(t *testing.T) { + var schema *ast.Schema + if td.Schema != "" { + var err error + schema, err = gqlparser.LoadSchema(&ast.Source{ + Name: "schema.graphqls", + Input: td.Schema, + }) + require.NoError(t, err, "parsing schema") + } + + gqlparse.NewParser(schema).Parse( + []byte(td.Src), + []byte(td.OprName), + []byte(td.VarsJSON), func( varVals [][]gqlparse.Token, operation []gqlparse.Token, @@ -1825,7 +2382,7 @@ func TestErr(t *testing.T) { }, func(err error) { require.Error(t, err) - td.Data.Check(t, err) + td.Check(t, err) }, ) }) @@ -1833,6 +2390,7 @@ func TestErr(t *testing.T) { } type TestSuccess struct { + Schema string Src string VarsJSON string OprName string @@ -1869,6 +2427,8 @@ func Token(t gqlscan.Token, value ...string) gqlparse.Token { } type TestError struct { + Name string + Schema string Src string VarsJSON string OprName string @@ -1901,3 +2461,11 @@ func (t *TestWriter) Errorf(format string, v ...any) { t.t.Helper() t.t.Errorf(format, v...) } + +// MakeVariableIndexToken creates a variable index token. +func MakeVariableIndexToken(index int, name string) gqlparse.Token { + return gqlparse.Token{ + ID: gqlscan.Token(gqlparse.TokenTypeValIndexOffset + index), + Value: []byte(name), + } +} diff --git a/gqlparse/internal/graph/bench_test.go b/pkg/gqlparse/internal/graph/bench_test.go similarity index 90% rename from gqlparse/internal/graph/bench_test.go rename to pkg/gqlparse/internal/graph/bench_test.go index aa86f7d..59a40f6 100644 --- a/gqlparse/internal/graph/bench_test.go +++ b/pkg/gqlparse/internal/graph/bench_test.go @@ -3,11 +3,13 @@ package graph_test import ( "testing" - "github.com/graph-guard/ggproxy/gqlparse/internal/graph" + "github.com/graph-guard/ggproxy/pkg/gqlparse/internal/graph" ) -var GL bool -var GB []byte +var ( + GL bool + GB []byte +) func BenchmarkIsCyclic(b *testing.B) { for _, td := range testdataCyclic { diff --git a/gqlparse/internal/graph/graph.go b/pkg/gqlparse/internal/graph/graph.go similarity index 94% rename from gqlparse/internal/graph/graph.go rename to pkg/gqlparse/internal/graph/graph.go index 21e9527..496d5dd 100644 --- a/gqlparse/internal/graph/graph.go +++ b/pkg/gqlparse/internal/graph/graph.go @@ -1,8 +1,8 @@ package graph import ( - "github.com/graph-guard/ggproxy/utilities/container/hamap" - "github.com/graph-guard/ggproxy/utilities/stack" + "github.com/graph-guard/ggproxy/pkg/container/hamap" + "github.com/graph-guard/ggproxy/pkg/stack" ) const MaxFragments = 128 @@ -16,7 +16,7 @@ func NewInspector() *Inspector { type Inspector struct { ni *hamap.Map[[]byte, int] - stack *stack.Stack[int] + stack stack.Stack[int] n int cl [MaxFragments]uint8 g [MaxFragments][MaxFragments]bool diff --git a/gqlparse/internal/graph/graph_test.go b/pkg/gqlparse/internal/graph/graph_test.go similarity index 98% rename from gqlparse/internal/graph/graph_test.go rename to pkg/gqlparse/internal/graph/graph_test.go index eed2722..7f06557 100644 --- a/gqlparse/internal/graph/graph_test.go +++ b/pkg/gqlparse/internal/graph/graph_test.go @@ -4,8 +4,8 @@ import ( "strconv" "testing" - "github.com/graph-guard/ggproxy/gqlparse/internal/graph" - "github.com/graph-guard/ggproxy/utilities/decl" + "github.com/graph-guard/ggproxy/pkg/decl" + "github.com/graph-guard/ggproxy/pkg/gqlparse/internal/graph" "github.com/stretchr/testify/require" ) diff --git a/utilities/math/math.go b/pkg/math/math.go similarity index 100% rename from utilities/math/math.go rename to pkg/math/math.go diff --git a/utilities/math/math_test.go b/pkg/math/math_test.go similarity index 86% rename from utilities/math/math_test.go rename to pkg/math/math_test.go index 834e5ef..0c68ad4 100644 --- a/utilities/math/math_test.go +++ b/pkg/math/math_test.go @@ -3,7 +3,7 @@ package math_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/math" + "github.com/graph-guard/ggproxy/pkg/math" "github.com/stretchr/testify/require" ) diff --git a/utilities/mhstore/benchmark_test.go b/pkg/mhstore/benchmark_test.go similarity index 92% rename from utilities/mhstore/benchmark_test.go rename to pkg/mhstore/benchmark_test.go index a67eebb..123bba7 100644 --- a/utilities/mhstore/benchmark_test.go +++ b/pkg/mhstore/benchmark_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/graph-guard/ggproxy/utilities/mhstore" + "github.com/graph-guard/ggproxy/pkg/mhstore" ) func BenchmarkAdd(b *testing.B) { diff --git a/utilities/mhstore/mhstore.go b/pkg/mhstore/mhstore.go similarity index 96% rename from utilities/mhstore/mhstore.go rename to pkg/mhstore/mhstore.go index b7c81f4..b2869f3 100644 --- a/utilities/mhstore/mhstore.go +++ b/pkg/mhstore/mhstore.go @@ -1,6 +1,6 @@ package mhstore -import "github.com/graph-guard/ggproxy/utilities/aset" +import "github.com/graph-guard/ggproxy/pkg/aset" // MHStore stands for Mask-Hash-Store. // Storage for faster calculation of matching hashes per mask. diff --git a/utilities/mhstore/mhstore_test.go b/pkg/mhstore/mhstore_test.go similarity index 94% rename from utilities/mhstore/mhstore_test.go rename to pkg/mhstore/mhstore_test.go index a399fce..d16b119 100644 --- a/utilities/mhstore/mhstore_test.go +++ b/pkg/mhstore/mhstore_test.go @@ -5,7 +5,7 @@ import ( "math/rand" "testing" - "github.com/graph-guard/ggproxy/utilities/mhstore" + "github.com/graph-guard/ggproxy/pkg/mhstore" "github.com/stretchr/testify/require" ) diff --git a/utilities/segmented/segmented.go b/pkg/segmented/segmented.go similarity index 97% rename from utilities/segmented/segmented.go rename to pkg/segmented/segmented.go index 2d0eeef..f8b951a 100644 --- a/utilities/segmented/segmented.go +++ b/pkg/segmented/segmented.go @@ -2,7 +2,7 @@ package segmented import ( - "github.com/graph-guard/ggproxy/utilities/container/hamap" + "github.com/graph-guard/ggproxy/pkg/container/hamap" ) // Segment defines the logical index and diff --git a/utilities/segmented/segmented_test.go b/pkg/segmented/segmented_test.go similarity index 98% rename from utilities/segmented/segmented_test.go rename to pkg/segmented/segmented_test.go index 600aaeb..68c6ba4 100644 --- a/utilities/segmented/segmented_test.go +++ b/pkg/segmented/segmented_test.go @@ -3,7 +3,7 @@ package segmented_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/segmented" + "github.com/graph-guard/ggproxy/pkg/segmented" "github.com/stretchr/testify/require" ) diff --git a/server/api.go b/pkg/server/api.go similarity index 82% rename from server/api.go rename to pkg/server/api.go index d29bb51..41b6225 100644 --- a/server/api.go +++ b/pkg/server/api.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "errors" - "fmt" stdlog "log" "net" "net/http" @@ -16,13 +15,12 @@ import ( "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/transport" - "github.com/graph-guard/ggproxy/api/graph" - "github.com/graph-guard/ggproxy/api/graph/generated" - "github.com/graph-guard/ggproxy/api/graph/model" - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/engines/rmap" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/gqt" + "github.com/graph-guard/ggproxy/pkg/api/graph" + "github.com/graph-guard/ggproxy/pkg/api/graph/generated" + "github.com/graph-guard/ggproxy/pkg/api/graph/model" + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon" + gqt "github.com/graph-guard/gqt/v4" plog "github.com/phuslu/log" "github.com/valyala/fasthttp" ) @@ -141,7 +139,6 @@ func (s *API) Serve(listener net.Listener) { // TLS disabled if listener != nil { err = s.server.Serve(listener) - } else { err = s.server.ListenAndServe() } @@ -205,14 +202,12 @@ func makeGraphServer( conf *config.Config, proxyServer *Proxy, ) *handler.Server { - parser := gqlparse.NewParser() services := makeServices(conf, proxyServer) s := handler.NewDefaultServer( generated.NewExecutableSchema( generated.Config{Resolvers: &graph.Resolver{ Start: start, Conf: conf, - Parser: parser, Services: services, Log: proxyServer.log, }}, @@ -241,14 +236,10 @@ func makeServices( conf *config.Config, proxyServer *Proxy, ) map[string]*model.Service { - m := make( - map[string]*model.Service, - conf.Services.Len(), - ) - conf.Services.Visit(func(key []byte, s *config.Service) (stop bool) { + m := make(map[string]*model.Service, len(conf.Services)) + for _, s := range conf.Services { m[s.ID] = makeService(conf, s, proxyServer) - return - }) + } return m } @@ -260,43 +251,37 @@ func makeService( stats := proxyServer.GetServiceStatistics(s.ID) service := &model.Service{ Stats: stats, - TemplatesByID: make( - map[string]*model.Template, - s.Templates.Len(), + Templates: make( + map[*config.Template]*model.Template, + len(s.Templates), + ), + ID: s.ID, + ForwardURL: s.ForwardURL, + Enabled: s.Enabled, + TemplatesEnabled: make([]*model.Template, len(s.TemplatesEnabled)), + TemplatesDisabled: make( + []*model.Template, + len(s.Templates)-len(s.TemplatesEnabled), ), - ID: s.ID, - ForwardURL: s.ForwardURL, - Enabled: s.Enabled, - TemplatesEnabled: make([]*model.Template, len(s.TemplatesEnabled)), - TemplatesDisabled: make([]*model.Template, s.Templates.Len()-len(s.TemplatesEnabled)), } - { // Initialize matcher engine - d := make(map[string]gqt.Doc, s.Templates.Len()) - s.Templates.Visit(func(key []byte, t *config.Template) (stop bool) { - d[t.ID] = t.Document + { // Initialize engine + d := make(map[string]*gqt.Operation, len(s.Templates)) + for _, t := range s.Templates { + d[t.ID] = t.GQTTemplate tm := &model.Template{ Service: service, Stats: proxyServer.GetTemplateStatistics(s.ID, t.ID), - ID: t.ID, Tags: t.Tags, Source: string(t.Source), Enabled: t.Enabled, } service.TemplatesEnabled = append(service.TemplatesEnabled, tm) - service.TemplatesByID[t.ID] = tm - return - }) - - var err error - service.Matcher, err = rmap.New(d, 0) - if err != nil { - panic(fmt.Errorf( - "initializing matcher for service %q: %w", - s.ID, err, - )) + service.Templates[t] = tm } + + service.Engine = playmon.New(s) } { // Set proxy URL diff --git a/server/proxy.go b/pkg/server/proxy.go similarity index 85% rename from server/proxy.go rename to pkg/server/proxy.go index 67be781..4220a32 100644 --- a/server/proxy.go +++ b/pkg/server/proxy.go @@ -2,17 +2,16 @@ package server import ( "crypto/tls" - "fmt" "net" "sync" "time" - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/engines/rmap" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/statistics" - "github.com/graph-guard/ggproxy/utilities/tokenwriter" - "github.com/graph-guard/gqt" + "github.com/graph-guard/ggproxy/pkg/config" + "github.com/graph-guard/ggproxy/pkg/engine/playmon" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/statistics" + "github.com/graph-guard/ggproxy/pkg/tokenwriter" + gqt "github.com/graph-guard/gqt/v4" plog "github.com/phuslu/log" "github.com/tidwall/gjson" "github.com/valyala/fasthttp" @@ -32,14 +31,14 @@ type service struct { forwardURL string forwardReduced bool log plog.Logger - matcherpool sync.Pool + enginePool sync.Pool statistics *statistics.ServiceSync templateStatistics map[string]*statistics.TemplateSync } -type matcher struct { +type engine struct { Parser *gqlparse.Parser - Engine *rmap.RulesMap + Engine *playmon.Engine } func NewProxy( @@ -92,23 +91,17 @@ func NewProxy( forwardURL: s.ForwardURL, forwardReduced: s.ForwardReduced, log: log, - matcherpool: sync.Pool{ + enginePool: sync.Pool{ New: func() any { - d := make(map[string]gqt.Doc, len(s.TemplatesEnabled)) + d := make(map[string]*gqt.Operation, len(s.TemplatesEnabled)) for _, t := range s.TemplatesEnabled { - d[t.ID] = t.Document + d[t.ID] = t.GQTTemplate } - engine, err := rmap.New(d, 0) - if err != nil { - panic(fmt.Errorf( - "initializing engine for service %q: %w", - s.ID, err, - )) - } - parser := gqlparse.NewParser() - return &matcher{ - Parser: parser, - Engine: engine, + p := gqlparse.NewParser(s.Schema) + e := playmon.New(s) + return &engine{ + Parser: p, + Engine: e, } }, }, @@ -116,16 +109,16 @@ func NewProxy( templateStatistics: templateStatistics, } - // Warm up matcher pool + // Warm up engine pool func() { // n := runtime.NumCPU() n := 1 - m := make([]*matcher, n) + m := make([]*engine, n) for i := 0; i < n; i++ { - m[i] = services[s.Path].matcherpool.Get().(*matcher) + m[i] = services[s.Path].enginePool.Get().(*engine) } for i := 0; i < n; i++ { - services[s.Path].matcherpool.Put(m[i]) + services[s.Path].enginePool.Put(m[i]) } }() } @@ -190,18 +183,18 @@ func (s *Proxy) handle(ctx *fasthttp.RequestCtx) { return } - m := service.matcherpool.Get().(*matcher) - defer service.matcherpool.Put(m) + m := service.enginePool.Get().(*engine) + defer service.enginePool.Put(m) - m.Parser.Parse( + var operation []gqlparse.Token + m.Engine.Match( query, operationName, variablesJSON, - func( - varVals [][]gqlparse.Token, - operation []gqlparse.Token, - selectionSet []gqlparse.Token, - ) { - templateID := m.Engine.Match(varVals, operation[0].ID, selectionSet) - if templateID == "" { + func(o, selectionSet []gqlparse.Token) (stop bool) { + operation = o + return false + }, + func(t *config.Template) (stop bool) { + if t == nil { timeProcessing := time.Since(start) service.statistics.Update( len(body), 0, @@ -214,7 +207,7 @@ func (s *Proxy) handle(ctx *fasthttp.RequestCtx) { return } - templateStatistics := service.templateStatistics[templateID] + templateStatistics := service.templateStatistics[t.ID] timeProcessing := time.Since(start) startForward := time.Now() @@ -231,8 +224,7 @@ func (s *Proxy) handle(ctx *fasthttp.RequestCtx) { if service.forwardReduced { if err := tokenwriter.Write( - ctx.Request.BodyWriter(), - operation, + ctx.Request.BodyWriter(), operation, ); err != nil { s.log.Error(). Err(err). @@ -280,6 +272,7 @@ func (s *Proxy) handle(ctx *fasthttp.RequestCtx) { templateStatistics.Update( timeProcessing, timeForwarding, ) + return false }, func(err error) { s.log.Error().Err(err).Msg("parser error") @@ -330,7 +323,6 @@ func (s *Proxy) Serve(listener net.Listener) { // TLS disabled if listener != nil { err = s.server.Serve(listener) - } else { err = s.server.ListenAndServe(s.config.Proxy.Host) } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go new file mode 100644 index 0000000..135ed14 --- /dev/null +++ b/pkg/server/server_test.go @@ -0,0 +1,353 @@ +package server_test + +// func TestProxy(t *testing.T) { +// setups := GetSetups(t) +// for _, setup := range setups { +// t.Run(setup.Name, func(t *testing.T) { +// clientProxy, forwarded, respSetter, logs := launchSetup(t, setup) + +// for _, test := range setup.Tests { +// t.Run(test.Name, func(t *testing.T) { +// if test.Destination != nil { +// body := test.Destination.Response.Body +// if j := test.Destination.Response.BodyJSON; j != nil { +// b, err := json.Marshal(j) +// require.NoError(t, err) +// body = string(b) +// } +// respSetter.Set(&SendResponse{ +// Status: test.Destination.Response.Status, +// Body: body, +// Headers: copyMap(test.Destination.Response.Headers), +// }) +// } else { +// respSetter.Set(nil) +// } + +// respStatus, respHeaders, respBody := doRequest( +// t, clientProxy, +// test.Client.Input.Method, +// "localhost:8000", +// test.Client.Input.Endpoint, +// func(r *fasthttp.Request) { +// r.Header.Set("Content-Type", "application/json") +// body := test.Client.Input.Body +// if j := test.Client.Input.BodyJSON; j != nil { +// b, err := json.Marshal(j) +// require.NoError(t, err) +// body = string(b) +// } +// r.SetBodyString(body) +// }, +// ) + +// if test.Destination != nil { +// var f ReceivedRequest +// ok := false +// select { +// case x := <-forwarded: +// ok = true +// f = x +// default: +// t.Errorf("the request wansn't forwarded as expected") +// } +// if ok { +// compareHeaders( +// t, "forwarded", +// test.Destination.ExpectForwarded.Headers, +// f.Headers, +// ) +// j := test.Destination.ExpectForwarded.BodyJSON +// body := test.Destination.ExpectForwarded.Body +// if j != nil { +// b, err := json.Marshal(j) +// require.NoError(t, err) +// body = string(b) +// } +// assert.Equal( +// t, body, f.Body, +// "unexpected body was forwarded to destination", +// ) +// } +// } + +// // Compare results +// if e := test.Client.ExpectResponse.Status; e != respStatus { +// t.Errorf( +// "unexpected response status: %d; expected: %d", +// respStatus, e, +// ) +// } +// compareHeaders( +// t, "response", +// test.Client.ExpectResponse.Headers, respHeaders, +// ) +// { +// body := test.Client.ExpectResponse.Body +// if j := test.Client.ExpectResponse.BodyJSON; j != nil { +// b, err := json.Marshal(j) +// require.NoError(t, err) +// body = string(b) +// } +// assert.Equal( +// t, body, respBody, +// "unexpected response body", +// ) +// } + +// // Check logs +// logs.ReadLogs(func(m []map[string]any) { +// for i, x := range m { +// if i >= len(test.Logs) { +// t.Errorf("unexpected log: %v", m[i]) +// continue +// } +// assert.Equal(t, +// test.Logs[i], x, +// "unexpected log at index %d", i, +// ) +// } +// }) +// logs.Reset() +// }) +// } +// }) +// } +// } + +// func GetSetups(t *testing.T) []testsetup.Setup { +// return []testsetup.Setup{ +// testsetup.Starwars(), +// testsetup.InputsSchema(), +// } +// } + +// type SendResponse struct { +// Status int +// Body string +// Headers map[string]string +// } +// type ReceivedRequest struct { +// Body string +// Headers map[string]string +// } + +// func launchSetup(t *testing.T, s testsetup.Setup) ( +// clientProxy *fasthttp.Client, +// forwarded <-chan ReceivedRequest, +// resp *Syncronized[*SendResponse], +// logRecorder *LogRecorder, +// ) { +// resp = new(Syncronized[*SendResponse]) + +// lnDest := fasthttputil.NewInmemoryListener() +// t.Cleanup(func() { lnDest.Close() }) + +// lnProxy := fasthttputil.NewInmemoryListener() +// t.Cleanup(func() { lnProxy.Close() }) + +// forwardedRW := make(chan ReceivedRequest, 1) +// forwarded = forwardedRW + +// go func() { +// s := &fasthttp.Server{ +// Handler: func(ctx *fasthttp.RequestCtx) { +// // Send the received request context for the check +// var rr ReceivedRequest +// rr.Headers = make(map[string]string, ctx.Request.Header.Len()) +// ctx.Request.Header.VisitAll(func(key, value []byte) { +// rr.Headers[string(key)] = string(value) +// }) +// rr.Body = string(ctx.Request.Body()) +// forwardedRW <- rr + +// // Send response +// sr := resp.Get() +// if sr == nil { +// ctx.Error( +// fasthttp.StatusMessage(fasthttp.StatusInternalServerError), +// fasthttp.StatusInternalServerError, +// ) +// return +// } +// ctx.Response.SetStatusCode(sr.Status) +// for k, v := range sr.Headers { +// ctx.Response.Header.Set(k, v) +// } +// ctx.Response.SetBodyString(sr.Body) +// }, +// } +// if err := s.Serve(lnDest); err != nil { +// panic(err) +// } +// }() + +// // Launch proxy server +// logRecorder = new(LogRecorder) +// log := plog.Logger{ +// Level: plog.DebugLevel, +// TimeField: "time", +// TimeFormat: "23:59:59", +// Writer: &plog.IOWriter{Writer: logRecorder}, +// } +// server := server.NewProxy( +// s.Config, +// time.Second*10, +// time.Second*10, +// 1024*64, +// 1024*64, +// log, +// &fasthttp.Client{ +// Dial: func(addr string) (net.Conn, error) { +// return lnDest.Dial() +// }, +// }, +// nil, +// ) + +// go func() { +// server.Serve(lnProxy) +// }() + +// clientProxy = &fasthttp.Client{ +// Dial: func(addr string) (net.Conn, error) { +// return lnProxy.Dial() +// }, +// } + +// return +// } + +// func doRequest( +// t *testing.T, +// client *fasthttp.Client, +// method, host, path string, +// prepareReq func(*fasthttp.Request), +// ) (status int, headers map[string]string, body string) { +// req := fasthttp.AcquireRequest() +// resp := fasthttp.AcquireResponse() +// defer fasthttp.ReleaseRequest(req) +// defer fasthttp.ReleaseResponse(resp) + +// if prepareReq != nil { +// prepareReq(req) +// } +// req.Header.SetMethod(method) +// req.SetHost(host) +// req.URI().SetPath(path) + +// err := client.Do(req, resp) +// require.NoError(t, err) + +// status = resp.StatusCode() +// headers = make(map[string]string, resp.Header.Len()) +// resp.Header.VisitAll(func(key, value []byte) { +// headers[string(key)] = string(value) +// }) +// body = string(resp.Body()) +// return +// } + +// type Syncronized[T any] struct { +// lock sync.Mutex +// t T +// } + +// func (s *Syncronized[T]) Get() T { +// s.lock.Lock() +// defer s.lock.Unlock() +// return s.t +// } + +// func (s *Syncronized[T]) Set(t T) { +// s.lock.Lock() +// defer s.lock.Unlock() +// s.t = t +// } + +// func compareHeaders(t *testing.T, title string, expected, actual map[string]string) { +// t.Helper() +// e := make(map[string]string, len(expected)) +// for k, v := range expected { +// e[k] = v +// } +// a := make(map[string]string, len(actual)) +// for k, v := range actual { +// a[k] = v +// } +// for k, ev := range expected { +// delete(e, k) +// delete(a, k) +// if av, ok := actual[k]; ok { +// expr, err := regexp.Compile(ev) +// if err != nil { +// t.Fatalf( +// "compiling regexp for %s header %q (%q): %v", +// title, k, ev, err, +// ) +// } +// if !expr.MatchString(av) { +// t.Errorf( +// "%s header %q expected regexp: %q; received: %q", +// title, k, ev, av, +// ) +// } +// } +// } +// for k, v := range e { +// t.Errorf("missing %s header %q (%q)", title, k, v) +// } +// for k, v := range a { +// t.Errorf("unexpected %s header %q (%q)", title, k, v) +// } +// } + +// type LogRecorder struct { +// Lock sync.Mutex +// Recorded []map[string]any +// } + +// func (w *LogRecorder) Write(d []byte) (int, error) { +// var m map[string]any +// if err := json.Unmarshal(d, &m); err != nil { +// return 0, fmt.Errorf("unmarshalling JSON: %w", err) +// } +// delete(m, "time") // We don't need to check the log time +// w.Lock.Lock() +// defer w.Lock.Unlock() +// w.Recorded = append(w.Recorded, m) +// return len(d), nil +// } + +// func (w *LogRecorder) Reset() { +// w.Lock.Lock() +// defer w.Lock.Unlock() +// w.Recorded = nil +// } + +// func (w *LogRecorder) ReadLogs(fn func([]map[string]any)) { +// w.Lock.Lock() +// defer w.Lock.Unlock() +// fn(w.Recorded) +// } + +// func copyMap[K comparable, V any](m map[K]V) (copy map[K]V) { +// copy = make(map[K]V, len(m)) +// for k, v := range m { +// copy[k] = v +// } +// return copy +// } + +// func checkLogs(t *testing.T, expected, actual []map[string]any) { +// for i, x := range expected { +// if i >= len(expected) { +// t.Errorf("unexpected log: %v", actual[i]) +// continue +// } +// assert.Equal(t, +// expected[i], x, +// "unexpected log at index %d", i, +// ) +// } +// } diff --git a/utilities/set/bench_test.go b/pkg/set/bench_test.go similarity index 96% rename from utilities/set/bench_test.go rename to pkg/set/bench_test.go index 17f46d4..6a97dee 100644 --- a/utilities/set/bench_test.go +++ b/pkg/set/bench_test.go @@ -6,7 +6,7 @@ import ( "math/rand" "testing" - "github.com/graph-guard/ggproxy/utilities/set" + "github.com/graph-guard/ggproxy/pkg/set" ) var GB bool diff --git a/utilities/set/set.go b/pkg/set/set.go similarity index 100% rename from utilities/set/set.go rename to pkg/set/set.go diff --git a/utilities/set/set_test.go b/pkg/set/set_test.go similarity index 97% rename from utilities/set/set_test.go rename to pkg/set/set_test.go index cd2fa62..e275639 100644 --- a/utilities/set/set_test.go +++ b/pkg/set/set_test.go @@ -3,7 +3,7 @@ package set_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/set" + "github.com/graph-guard/ggproxy/pkg/set" "github.com/stretchr/testify/require" ) diff --git a/pkg/stack/stack.go b/pkg/stack/stack.go new file mode 100644 index 0000000..1c710a5 --- /dev/null +++ b/pkg/stack/stack.go @@ -0,0 +1,78 @@ +package stack + +// Stack is an implementation of stack container. +type Stack[T any] struct{ s []T } + +// New creates a new instance of Stack with preallocated capacity. +func New[T any](capacity int) Stack[T] { + return Stack[T]{s: make([]T, 0, capacity)} +} + +// Reset resets the stack. +func (s *Stack[T]) Reset() { s.s = (s.s)[:0] } + +// Push adds an element to the stack. +func (s *Stack[T]) Push(f T) { s.s = append(s.s, f) } + +// Pop deletes the last stack element. +func (s *Stack[T]) Pop() { + if l := len(s.s) - 1; l >= 0 { + s.s = (s.s)[:l] + } +} + +// TopPop returns and deletes the last stack element. +func (s *Stack[T]) TopPop() (top T) { + if l := len(s.s) - 1; l >= 0 { + top = (s.s)[l] + s.s = (s.s)[:l] + } + return top +} + +// PopPush executes Pop and Push operations in sequence. +func (s *Stack[T]) PopPush(f T) { + if l := len(s.s) - 1; l >= 0 { + s.s = (s.s)[:l] + } + s.s = append(s.s, f) +} + +// TopPopPush executes Pop and Push operations in sequence. +func (s *Stack[T]) TopPopPush(f T) (popped T) { + if l := len(s.s) - 1; l >= 0 { + popped = (s.s)[l] + s.s = (s.s)[:l] + } + s.s = append(s.s, f) + return popped +} + +// Top returns the last stack element. +func (s *Stack[T]) Top() (top T) { + if l := len(s.s) - 1; l >= 0 { + return (s.s)[l] + } + return top +} + +// TopOffset returns the last stack element relative to the offset. +func (s *Stack[T]) TopOffset(offset int) (top T) { + if l := len(s.s) - (1 + offset); l >= 0 { + return (s.s)[l] + } + return top +} + +// TopOffsetFn calls fn with the last stack element at offset. +func (s *Stack[T]) TopOffsetFn(offset int, fn func(*T)) { + if l := len(s.s) - (1 + offset); l >= 0 { + fn(&(s.s)[l]) + } +} + +// Get returns the element at index from bottom. +func (s *Stack[T]) Get(index int) T { return (s.s)[index] } + +// Len returns the stack length. +func (s *Stack[T]) Len() int { return len(s.s) } diff --git a/pkg/stack/stack_test.go b/pkg/stack/stack_test.go new file mode 100644 index 0000000..604ae7d --- /dev/null +++ b/pkg/stack/stack_test.go @@ -0,0 +1,125 @@ +package stack_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/stack" + "github.com/stretchr/testify/require" +) + +func TestReset(t *testing.T) { + st := stack.New[uint16](2) + st.Push(0) + st.Reset() + require.Equal(t, stack.New[uint16](2), st) +} + +func TestPushLen(t *testing.T) { + st := stack.New[int16](4) + st.Push(0) + st.Push(1) + st.Push(-1) + require.Equal(t, 3, st.Len()) +} + +func TestPop(t *testing.T) { + st := stack.New[int64](4) + st.Push(0) + st.Push(1) + st.Push(-1) + require.Equal(t, int64(-1), st.TopPop()) + st.Pop() + st.Pop() + require.Equal(t, int64(0), st.TopPop()) +} + +func TestTopPopPush(t *testing.T) { + st := stack.New[float64](2) + st.Push(0.0) + require.Equal(t, 0.0, st.TopPopPush(-1)) + require.Equal(t, -1.0, st.TopPop()) +} + +func TestPopPush(t *testing.T) { + st := stack.New[float64](2) + st.Push(0.0) + st.PopPush(-1) + require.Equal(t, -1.0, st.Top()) + st.PopPush(3.14) + require.Equal(t, 3.14, st.Top()) +} + +func TestTop(t *testing.T) { + st := stack.New[int](2) + st.Push(0) + st.Push(-1) + require.Equal(t, -1, st.Top()) + st.Pop() + st.Pop() + require.Equal(t, 0, st.Top()) +} + +func TestTopOffset(t *testing.T) { + st := stack.New[int](2) + st.Push(1) + st.Push(2) + st.Push(3) + require.Equal(t, 3, st.TopOffset(0)) + require.Equal(t, 2, st.TopOffset(1)) + require.Equal(t, 1, st.TopOffset(2)) + require.Zero(t, st.TopOffset(3)) + st.Pop() + require.Equal(t, 2, st.TopOffset(0)) + require.Equal(t, 1, st.TopOffset(1)) + require.Zero(t, st.TopOffset(2)) + require.Zero(t, st.TopOffset(3)) +} + +func TestTopOffsetNeg(t *testing.T) { + st := stack.New[int](2) + st.Push(1) + st.Push(2) + require.Panics(t, func() { + require.Zero(t, st.TopOffset(-1)) + }) + require.Panics(t, func() { + require.Zero(t, st.TopOffset(-2)) + }) +} + +func TestTopOffsetFn(t *testing.T) { + st := stack.New[int](2) + st.Push(0) + st.Push(-1) + st.TopOffsetFn(0, func(i *int) { + require.Equal(t, -1, *i) + *i = 20 + }) + st.TopOffsetFn(1, func(i *int) { + require.Equal(t, 0, *i) + *i = 10 + }) + require.Equal(t, 20, st.TopPop()) + st.Pop() // 10 + require.Equal(t, 0, st.Top()) +} + +func TestTopOffsetFnNeg(t *testing.T) { + st := stack.New[int](2) + st.Push(1) + st.Push(2) + require.Panics(t, func() { + st.TopOffsetFn(-1, func(t *int) {}) + }) + require.Panics(t, func() { + st.TopOffsetFn(-2, func(t *int) {}) + }) +} + +func TestGet(t *testing.T) { + st := stack.New[int](2) + st.Push(0) + st.Push(-1) + require.Equal(t, 0, st.Get(0)) + require.Equal(t, -1, st.Get(1)) +} diff --git a/statistics/statistics.go b/pkg/statistics/statistics.go similarity index 100% rename from statistics/statistics.go rename to pkg/statistics/statistics.go diff --git a/statistics/statistics_test.go b/pkg/statistics/statistics_test.go similarity index 98% rename from statistics/statistics_test.go rename to pkg/statistics/statistics_test.go index 67c927e..5aa6f53 100644 --- a/statistics/statistics_test.go +++ b/pkg/statistics/statistics_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/graph-guard/ggproxy/statistics" + "github.com/graph-guard/ggproxy/pkg/statistics" "github.com/stretchr/testify/require" ) diff --git a/utilities/testeq/testeq.go b/pkg/testeq/testeq.go similarity index 100% rename from utilities/testeq/testeq.go rename to pkg/testeq/testeq.go diff --git a/utilities/testeq/testeq_test.go b/pkg/testeq/testeq_test.go similarity index 98% rename from utilities/testeq/testeq_test.go rename to pkg/testeq/testeq_test.go index 1ea0c30..872c5e8 100644 --- a/utilities/testeq/testeq_test.go +++ b/pkg/testeq/testeq_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/graph-guard/ggproxy/utilities/testeq" + "github.com/graph-guard/ggproxy/pkg/testeq" "github.com/stretchr/testify/require" ) diff --git a/server/tests/setup_1/all-services/service_a.yml b/pkg/testsetup/inputs_schema/all-services/service_a.yaml similarity index 86% rename from server/tests/setup_1/all-services/service_a.yml rename to pkg/testsetup/inputs_schema/all-services/service_a.yaml index 4ce506f..d7d5fba 100644 --- a/server/tests/setup_1/all-services/service_a.yml +++ b/pkg/testsetup/inputs_schema/all-services/service_a.yaml @@ -4,3 +4,4 @@ forward-url: "http://localhost:8081/service_a" forward-reduced: false all-templates: ../all-templates/service_a enabled-templates: ../enabled-templates/service_a +schema: ../service_a.graphqls diff --git a/pkg/testsetup/inputs_schema/all-templates/service_a/mut_arr_str.gqt b/pkg/testsetup/inputs_schema/all-templates/service_a/mut_arr_str.gqt new file mode 100644 index 0000000..46a9c75 --- /dev/null +++ b/pkg/testsetup/inputs_schema/all-templates/service_a/mut_arr_str.gqt @@ -0,0 +1,10 @@ +--- +name: "Mutation Array of Strings" +tags: + - mutation + - array + - string +--- +mutation { + arr_str_eq(a: ["foo", "bar"]) +} diff --git a/server/tests/setup_0/config.yaml b/pkg/testsetup/inputs_schema/config.yml similarity index 100% rename from server/tests/setup_0/config.yaml rename to pkg/testsetup/inputs_schema/config.yml diff --git a/pkg/testsetup/inputs_schema/enabled-services/service_a.yaml b/pkg/testsetup/inputs_schema/enabled-services/service_a.yaml new file mode 100644 index 0000000..d7d5fba --- /dev/null +++ b/pkg/testsetup/inputs_schema/enabled-services/service_a.yaml @@ -0,0 +1,7 @@ +name: "Service A" +path: "/service_a" +forward-url: "http://localhost:8081/service_a" +forward-reduced: false +all-templates: ../all-templates/service_a +enabled-templates: ../enabled-templates/service_a +schema: ../service_a.graphqls diff --git a/pkg/testsetup/inputs_schema/enabled-templates/service_a/mut_arr_str.gqt b/pkg/testsetup/inputs_schema/enabled-templates/service_a/mut_arr_str.gqt new file mode 100644 index 0000000..46a9c75 --- /dev/null +++ b/pkg/testsetup/inputs_schema/enabled-templates/service_a/mut_arr_str.gqt @@ -0,0 +1,10 @@ +--- +name: "Mutation Array of Strings" +tags: + - mutation + - array + - string +--- +mutation { + arr_str_eq(a: ["foo", "bar"]) +} diff --git a/pkg/testsetup/inputs_schema/service_a.graphqls b/pkg/testsetup/inputs_schema/service_a.graphqls new file mode 100644 index 0000000..e91228e --- /dev/null +++ b/pkg/testsetup/inputs_schema/service_a.graphqls @@ -0,0 +1,3 @@ +type Mutation { + arr_str_eq(a: [String!]!): [String!]! +} diff --git a/pkg/testsetup/starwars/config.yml b/pkg/testsetup/starwars/config.yml new file mode 100644 index 0000000..f5f9715 --- /dev/null +++ b/pkg/testsetup/starwars/config.yml @@ -0,0 +1,13 @@ +proxy: + # Address and port of the proxy server. + host: localhost:8000 + # Optional, in bytes, default: 4MiB. + max-request-body-size: 4096 + +# Optional, enables API server. +api: + # Address and port of the API server. + host: localhost:3000 + +all-services: services_enabled +enabled-services: services_enabled diff --git a/pkg/testsetup/starwars/services_enabled/starwars.graphqls b/pkg/testsetup/starwars/services_enabled/starwars.graphqls new file mode 100644 index 0000000..48af8e5 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars.graphqls @@ -0,0 +1,131 @@ +# The query type, represents all of the entry points into our object graph +type Query { + hero(episode: Episode = NEWHOPE): Character + reviews(episode: Episode!, since: Time): [Review!]! + search(text: String!): [SearchResult!]! + character(id: ID!): Character + droid(id: ID!): Droid + human(id: ID!): Human + starship(id: ID!): Starship +} +# The mutation type, represents all updates we can make to our data +type Mutation { + createReview(episode: Episode!, review: ReviewInput!): Review +} +# The episodes in the Star Wars trilogy +enum Episode { + # Star Wars Episode IV: A New Hope, released in 1977. + NEWHOPE + # Star Wars Episode V: The Empire Strikes Back, released in 1980. + EMPIRE + # Star Wars Episode VI: Return of the Jedi, released in 1983. + JEDI +} +# A character from the Star Wars universe +interface Character { + # The ID of the character + id: ID! + # The name of the character + name: String! + # The friends of the character, or an empty list if they have none + friends: [Character!] + # The friends of the character exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this character appears in + appearsIn: [Episode!]! +} +# Units of height +enum LengthUnit { + # The standard unit around the world + METER + # Primarily used in the United States + FOOT +} +# A humanoid creature from the Star Wars universe +type Human implements Character { + # The ID of the human + id: ID! + # What this human calls themselves + name: String! + # Height in the preferred unit, default is meters + height(unit: LengthUnit = METER): Float! + # Mass in kilograms, or null if unknown + mass: Float + # This human's friends, or an empty list if they have none + friends: [Character!] + # The friends of the human exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this human appears in + appearsIn: [Episode!]! + # A list of starships this person has piloted, or an empty list if none + starships: [Starship!] +} +# An autonomous mechanical character in the Star Wars universe +type Droid implements Character { + # The ID of the droid + id: ID! + # What others call this droid + name: String! + # This droid's friends, or an empty list if they have none + friends: [Character!] + # The friends of the droid exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this droid appears in + appearsIn: [Episode!]! + # This droid's primary function + primaryFunction: String +} +# A connection object for a character's friends +type FriendsConnection { + # The total number of friends + totalCount: Int! + # The edges for each of the character's friends. + edges: [FriendsEdge!] + # A list of the friends, as a convenience when edges are not needed. + friends: [Character!] + # Information for paginating this connection + pageInfo: PageInfo! +} +# An edge object for a character's friends +type FriendsEdge { + # A cursor used for pagination + cursor: ID! + # The character represented by this friendship edge + node: Character +} +# Information for paginating this connection +type PageInfo { + startCursor: ID! + endCursor: ID! + hasNextPage: Boolean! +} +# Represents a review for a movie +type Review { + # The number of stars this review gave, 1-5 + stars: Int! + # Comment about the movie + commentary: String + # when the review was posted + time: Time +} +# The input object sent when someone is creating a new review +input ReviewInput { + # 0-5 stars + stars: Int! + # Comment about the movie, optional + commentary: String + # when the review was posted + time: Time +} +type Starship { + # The ID of the starship + id: ID! + # The name of the starship + name: String! + # Length of the starship, along the longest axis + length(unit: LengthUnit = METER): Float! + # coordinates tracking this ship + history: [[Int!]!]! +} +union SearchResult = Human | Droid | Starship +scalar Time diff --git a/pkg/testsetup/starwars/services_enabled/starwars.yml b/pkg/testsetup/starwars/services_enabled/starwars.yml new file mode 100644 index 0000000..8342501 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars.yml @@ -0,0 +1,15 @@ +# The service's display name +name: "Starwars" +# Source URL path +path: "/starwars" +# Destination URL (where to proxy requests to) +forward-url: "http://localhost:8080/starwars" + +# false for forwarding the original request, +# true for the reduced version. +forward-reduced: true + +schema: starwars.graphqls + +all-templates: starwars/templates_enabled +enabled-templates: starwars/templates_enabled diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_and_reviews.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_and_reviews.gqt new file mode 100644 index 0000000..f6738a7 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_and_reviews.gqt @@ -0,0 +1,19 @@ +--- +tags: + - query + - field:hero + - field:reviews + - constraint:equal + - expression:or +--- +query { + hero(episode: JEDI) { + id + appearsIn + } + reviews(episode: EMPIRE || JEDI) { + stars + commentary + time + } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_expanded.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_expanded.gqt new file mode 100644 index 0000000..ce3f151 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_expanded.gqt @@ -0,0 +1,38 @@ +--- +tags: + - query + - field:hero + - field:hero.friends + - field:hero.friendsConnection + - field:reviews + - constraint:equal + - constraint:greater + - constraint:greaterOrEqual + - expression:or + - recursive +--- +query { + hero(episode: EMPIRE || JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + friendsConnection(first: > 0, after: len > 0 || null) { + totalCount + friends { + id + name + appearsIn + } + } + } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends.gqt new file mode 100644 index 0000000..6aceda2 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends.gqt @@ -0,0 +1,26 @@ +--- +tags: + - query + - field:hero + - field:hero.friends + - constraint:equal + - expression:or + - recursive +--- +query { + hero(episode: EMPIRE || JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends_connection_and_starship.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends_connection_and_starship.gqt new file mode 100644 index 0000000..fd39751 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_friends_connection_and_starship.gqt @@ -0,0 +1,30 @@ +--- +tags: + - query + - field:hero + - field:starship + - field:hero.friendsConnection + - fragments:inline + - constraint:any + - constraint:equal + - constraint:notEqual + - expression:or +--- +query { + hero(episode: EMPIRE) { + id + name + ... on Droid { primaryFunction } + ... on Human { height(unit: *) mass } + appearsIn + friendsConnection { + totalCount + friends { + id name appearsIn + ...on Droid { primaryFunction } + ... on Human { height(unit: *) mass } + } + } + } + starship(id:"3001") { name length(unit: != METER) } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_minimal.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_minimal.gqt new file mode 100644 index 0000000..9f5b8a7 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_minimal.gqt @@ -0,0 +1,17 @@ +--- +tags: + - query + - field:hero + - field:hero.friends + - constraint:any +--- +query { + hero(episode: *) { + id + name + friends { + id + name + } + } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_with_friends_and_reviews.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_with_friends_and_reviews.gqt new file mode 100644 index 0000000..a202694 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_hero_with_friends_and_reviews.gqt @@ -0,0 +1,26 @@ +--- +tags: + - query + - field:hero + - field:hero.friends + - field:reviews + - constraint:equal + - expression:or +--- +query { + hero(episode: JEDI) { + id + name + appearsIn + friends { + id + name + appearsIn + } + } + reviews(episode: EMPIRE || JEDI) { + stars + commentary + time + } +} diff --git a/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_human_max2.gqt b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_human_max2.gqt new file mode 100644 index 0000000..c638e02 --- /dev/null +++ b/pkg/testsetup/starwars/services_enabled/starwars/templates_enabled/query_human_max2.gqt @@ -0,0 +1,16 @@ +--- +tags: + - query + - field:human + - constraint:any + - expression:max2 +--- +query { + human(id: *) { + max 2 { + id + name + appearsIn + } + } +} diff --git a/pkg/testsetup/starwars/test_qry2_ok.yaml b/pkg/testsetup/starwars/test_qry2_ok.yaml new file mode 100644 index 0000000..bbd1728 --- /dev/null +++ b/pkg/testsetup/starwars/test_qry2_ok.yaml @@ -0,0 +1,212 @@ +client: + input: + method: POST + endpoint: /testservice + body(JSON): + query: > + { + hero(episode: EMPIRE) { + id, name, appearsIn + ...droidFrag + ...humanFrag + friendsConnection { + totalCount + friends { + id, name, appearsIn + ...droidFrag + ...humanFrag + } + } + } + starship(id:"3001") { name, length(unit:FOOT) } + } + fragment droidFrag on Droid { primaryFunction } + fragment humanFrag on Human { height(unit: FOOT), mass } + + + expect-response: + status: 200 + headers: + Content-Length: ^166$ + Content-Type: ^text/plain; charset=utf-8$ + Server: ^fasthttp$ + Date: . + X-Custom-Header: value + body(JSON): + data: + "hero": { + "id": "1000", + "name": "Luke Skywalker", + "height": 5.6430448, + "mass": 77, + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "friendsConnection": { + "totalCount": 4, + "friends": [ + { + "id": "1002", + "name": "Han Solo", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "height": 5.905512, + "mass": 80 + }, + { + "id": "1003", + "name": "Leia Organa", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "height": 4.92126, + "mass": 49 + }, + { + "id": "2000", + "name": "C-3PO", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "primaryFunction": "Protocol" + }, + { + "id": "2001", + "name": "R2-D2", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "primaryFunction": "Astromech" + } + ] + } + } + "starship": { + "name": "X-Wing", + "length": 41.0105 + } + +destination: + expect-forwarded: + headers: + X-Forwarded-Host: ^localhost:8000$ + X-Forwarded-For: 0.0.0.0 + X-Forwarded-Proto: ^HTTP/1.1$ + Host: ^localhost:8081$ + Content-Length: ^97$ + Content-Type: ^application/json$ + User-Agent: ^fasthttp$ + Date: . + body(JSON): + query: > + { + hero(episode: EMPIRE) { + id, name, appearsIn + ...droidFrag + ...humanFrag + friendsConnection { + totalCount + friends { + id, name, appearsIn + ...droidFrag + ...humanFrag + } + } + } + starship(id:"3001") { name, length(unit:FOOT) } + } + fragment droidFrag on Droid { primaryFunction } + fragment humanFrag on Human { height(unit: FOOT), mass } + + response: + status: 200 + headers: + X-Custom-Header: value + body(JSON): + data: + "hero": { + "id": "1000", + "name": "Luke Skywalker", + "height": 5.6430448, + "mass": 77, + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "friendsConnection": { + "totalCount": 4, + "friends": [ + { + "id": "1002", + "name": "Han Solo", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "height": 5.905512, + "mass": 80 + }, + { + "id": "1003", + "name": "Leia Organa", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "height": 4.92126, + "mass": 49 + }, + { + "id": "2000", + "name": "C-3PO", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "primaryFunction": "Protocol" + }, + { + "id": "2001", + "name": "R2-D2", + "appearsIn": [ + "NEWHOPE", + "EMPIRE", + "JEDI" + ], + "primaryFunction": "Astromech" + } + ] + } + } + "starship": { + "name": "X-Wing", + "length": 41.0105 + } +logs: + - level: info + message: 'listening' + host: localhost:8080 + tls: false + services: + - testservice + - level: info + message: 'handling request' + path: /testservice + - level: debug + path: /testservice + query: '{"query":"query { queryFirstField { queryFirstSubfield querySecondSubfield } querySecondField }"}' diff --git a/server/tests/setup_0/test_qry_ok.yaml b/pkg/testsetup/starwars/test_qry_ok.yaml similarity index 52% rename from server/tests/setup_0/test_qry_ok.yaml rename to pkg/testsetup/starwars/test_qry_ok.yaml index f3a74d0..1638a60 100644 --- a/server/tests/setup_0/test_qry_ok.yaml +++ b/pkg/testsetup/starwars/test_qry_ok.yaml @@ -3,13 +3,17 @@ client: method: POST endpoint: /testservice body(JSON): - query: 'query { - queryFirstField { - queryFirstSubfield - querySecondSubfield + query: > + query { + hero(episode: EMPIRE) { + id + name + friends { + id + name + } } - querySecondField - }' + } expect-response: status: 200 @@ -21,10 +25,18 @@ client: X-Custom-Header: value body(JSON): data: - queryFirstField: - queryFirstSubfield: 'first subfield value' - querySecondSubfield: 'second subfield value' - querySecondField: 'second query field value' + hero: + id: "1000" + name: "Luke Skywalker" + friends: + - id: "1002" + name: "Han Solo" + - id: "1003" + name: "Leia Organa" + - id: "2000" + name: "C-3PO" + - id: "2001" + name: "R2-D2" destination: expect-forwarded: @@ -38,13 +50,17 @@ destination: User-Agent: ^fasthttp$ Date: . body(JSON): - query: 'query { - queryFirstField { - queryFirstSubfield - querySecondSubfield + query: > + query { + hero(episode: EMPIRE) { + id + name + friends { + id + name + } } - querySecondField - }' + } response: status: 200 @@ -52,10 +68,18 @@ destination: X-Custom-Header: value body(JSON): data: - queryFirstField: - queryFirstSubfield: 'first subfield value' - querySecondSubfield: 'second subfield value' - querySecondField: 'second query field value' + hero: + id: "1000" + name: "Luke Skywalker" + friends: + - id: "1002" + name: "Han Solo" + - id: "1003" + name: "Leia Organa" + - id: "2000" + name: "C-3PO" + - id: "2001" + name: "R2-D2" logs: - level: info @@ -70,4 +94,3 @@ logs: - level: debug path: /testservice query: '{"query":"query { queryFirstField { queryFirstSubfield querySecondSubfield } querySecondField }"}' - diff --git a/pkg/testsetup/testsetup.go b/pkg/testsetup/testsetup.go new file mode 100644 index 0000000..5aa6311 --- /dev/null +++ b/pkg/testsetup/testsetup.go @@ -0,0 +1,49 @@ +package testsetup + +import ( + "embed" + "io/fs" + "path/filepath" + + "github.com/graph-guard/ggproxy/pkg/config" +) + +/* SPECIAL NOTE: *\ +\* Symlinks are not allowed in embedded filesystems! */ + +const ( + SetupNameStarwars = "starwars" + SetupNameInputsSchema = "inputs_schema" +) + +func ByName(name string) (s Setup, ok bool) { + switch name { + case SetupNameStarwars: + return read(fsStarwars, SetupNameStarwars), true + case SetupNameInputsSchema: + return read(fsInputsSchema, SetupNameInputsSchema), true + } + return s, false +} + +//go:embed starwars +var fsStarwars embed.FS + +//go:embed inputs_schema +var fsInputsSchema embed.FS + +func read(fsys fs.FS, root string) Setup { + c, err := config.Read(fsys, root, filepath.Join(root, "config.yml")) + if err != nil { + panic(err) + } + return Setup{ + Name: root, + Config: c, + } +} + +type Setup struct { + Name string + Config *config.Config +} diff --git a/pkg/testsetup/testsetup_test.go b/pkg/testsetup/testsetup_test.go new file mode 100644 index 0000000..618fabe --- /dev/null +++ b/pkg/testsetup/testsetup_test.go @@ -0,0 +1,25 @@ +package testsetup_test + +import ( + "testing" + + "github.com/graph-guard/ggproxy/pkg/testsetup" + "github.com/stretchr/testify/require" +) + +func TestStarwars(t *testing.T) { + s, ok := testsetup.ByName(testsetup.SetupNameStarwars) + require.True(t, ok) + checkSetup(t, s) +} + +func TestInputsSchema(t *testing.T) { + s, ok := testsetup.ByName(testsetup.SetupNameInputsSchema) + require.True(t, ok) + checkSetup(t, s) +} + +func checkSetup(t *testing.T, s testsetup.Setup) { + require.NotZero(t, s.Config) + require.NotZero(t, s.Name) +} diff --git a/utilities/tokenwriter/bench_test.go b/pkg/tokenwriter/bench_test.go similarity index 85% rename from utilities/tokenwriter/bench_test.go rename to pkg/tokenwriter/bench_test.go index ab2e1ac..033a778 100644 --- a/utilities/tokenwriter/bench_test.go +++ b/pkg/tokenwriter/bench_test.go @@ -4,15 +4,15 @@ import ( "io" "testing" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/tokenwriter" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/tokenwriter" ) func BenchmarkWrite(b *testing.B) { for _, td := range testdata { b.Run("", func(b *testing.B) { var opr []gqlparse.Token - r := gqlparse.NewParser() + r := gqlparse.NewParser(nil) r.Parse( []byte(td.Request), []byte(td.OperationName), diff --git a/utilities/tokenwriter/tokenwriter.go b/pkg/tokenwriter/tokenwriter.go similarity index 89% rename from utilities/tokenwriter/tokenwriter.go rename to pkg/tokenwriter/tokenwriter.go index 1356b53..536321d 100644 --- a/utilities/tokenwriter/tokenwriter.go +++ b/pkg/tokenwriter/tokenwriter.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/graph-guard/ggproxy/gqlparse" + "github.com/graph-guard/ggproxy/pkg/gqlparse" "github.com/graph-guard/gqlscan" ) @@ -319,27 +319,29 @@ func Write(w io.Writer, tokens []gqlparse.Token) (err error) { return nil } -var partSpace = []byte(" ") -var partDoubleQuotes = []byte("\"") -var part3DoubleQuotes = []byte("\"\"\"") -var partSquareBracketL = []byte("[") -var partSquareBracketR = []byte("]") -var partCurlyBracketL = []byte("{") -var partCurlyBracketR = []byte("}") -var partColumn = []byte(":") -var partExlMark = []byte("!") -var partEq = []byte("=") -var partDefQry = []byte("query") -var partDefMut = []byte("mutation ") -var partDefSub = []byte("subscription ") -var partDirName = []byte(" @") -var partParenthesisL = []byte("(") -var partParenthesisR = []byte(")") -var partFragInline = []byte("...on ") -var partTrue = []byte("true") -var partFalse = []byte("false") -var partNull = []byte("null") -var partVarDollar = []byte("$") +var ( + partSpace = []byte(" ") + partDoubleQuotes = []byte("\"") + part3DoubleQuotes = []byte("\"\"\"") + partSquareBracketL = []byte("[") + partSquareBracketR = []byte("]") + partCurlyBracketL = []byte("{") + partCurlyBracketR = []byte("}") + partColumn = []byte(":") + partExlMark = []byte("!") + partEq = []byte("=") + partDefQry = []byte("query") + partDefMut = []byte("mutation ") + partDefSub = []byte("subscription ") + partDirName = []byte(" @") + partParenthesisL = []byte("(") + partParenthesisR = []byte(")") + partFragInline = []byte("...on ") + partTrue = []byte("true") + partFalse = []byte("false") + partNull = []byte("null") + partVarDollar = []byte("$") +) func isTokenEndOfVal(t gqlscan.Token) bool { switch t { diff --git a/utilities/tokenwriter/tokenwriter_test.go b/pkg/tokenwriter/tokenwriter_test.go similarity index 97% rename from utilities/tokenwriter/tokenwriter_test.go rename to pkg/tokenwriter/tokenwriter_test.go index 5877941..808b5a6 100644 --- a/utilities/tokenwriter/tokenwriter_test.go +++ b/pkg/tokenwriter/tokenwriter_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/graph-guard/ggproxy/gqlparse" - "github.com/graph-guard/ggproxy/utilities/tokenwriter" + "github.com/graph-guard/ggproxy/pkg/gqlparse" + "github.com/graph-guard/ggproxy/pkg/tokenwriter" "github.com/graph-guard/gqlscan" "github.com/stretchr/testify/require" ) @@ -180,7 +180,7 @@ var testdata = []struct { func TestWrite(t *testing.T) { for _, td := range testdata { t.Run("", func(t *testing.T) { - r := gqlparse.NewParser() + r := gqlparse.NewParser(nil) r.Parse( []byte(td.Request), []byte(td.OperationName), diff --git a/utilities/unsafe/unsafe.go b/pkg/unsafe/unsafe.go similarity index 100% rename from utilities/unsafe/unsafe.go rename to pkg/unsafe/unsafe.go diff --git a/utilities/unsafe/unsafe_test.go b/pkg/unsafe/unsafe_test.go similarity index 85% rename from utilities/unsafe/unsafe_test.go rename to pkg/unsafe/unsafe_test.go index deffd88..f689e44 100644 --- a/utilities/unsafe/unsafe_test.go +++ b/pkg/unsafe/unsafe_test.go @@ -3,7 +3,7 @@ package unsafe_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/unsafe" + "github.com/graph-guard/ggproxy/pkg/unsafe" "github.com/stretchr/testify/require" ) diff --git a/utilities/xxhash/bench_test.go b/pkg/xxhash/bench_test.go similarity index 91% rename from utilities/xxhash/bench_test.go rename to pkg/xxhash/bench_test.go index 6894026..a70811a 100644 --- a/utilities/xxhash/bench_test.go +++ b/pkg/xxhash/bench_test.go @@ -3,7 +3,7 @@ package xxhash_test import ( "testing" - "github.com/graph-guard/ggproxy/utilities/xxhash" + "github.com/graph-guard/ggproxy/pkg/xxhash" "github.com/pierrec/xxHash/xxHash64" ) @@ -22,6 +22,7 @@ func BenchmarkOriginal(b *testing.B) { h.Reset() } } + func BenchmarkCustom(b *testing.B) { s1 := []byte("foobar") s2 := []byte("bazzfuzz") diff --git a/utilities/xxhash/xxhash.go b/pkg/xxhash/xxhash.go similarity index 95% rename from utilities/xxhash/xxhash.go rename to pkg/xxhash/xxhash.go index 5fb884a..a2733c4 100644 --- a/utilities/xxhash/xxhash.go +++ b/pkg/xxhash/xxhash.go @@ -1,5 +1,6 @@ -// Package xxhash borrows code from github.com/pierrec/xxHash/xxHash64 -// and adapts it to the needs of the matcher. +// Package xxhash provides XXH64 hashing capabilities. +// +// Forked from github.com/pierrec/xxHash. package xxhash const ( @@ -60,7 +61,7 @@ func Write[B []byte | string](h *Hash, input B) { // Causes compiler to work directly from registers instead of stack: v1, v2, v3, v4 := h.v1, h.v2, h.v3, h.v4 for n := n - 32; p <= n; p += 32 { - sub := input[p:][:32] //BCE hint for compiler + sub := input[p:][:32] // BCE hint for compiler v1 = rol31(v1+u64(sub[:])*prime64_2) * prime64_1 v2 = rol31(v2+u64(sub[8:])*prime64_2) * prime64_1 v3 = rol31(v3+u64(sub[16:])*prime64_2) * prime64_1 @@ -104,7 +105,7 @@ func Write8(h *Hash, input [8]byte) { // Causes compiler to work directly from registers instead of stack: v1, v2, v3, v4 := h.v1, h.v2, h.v3, h.v4 for n := n - 32; p <= n; p += 32 { - sub := input[p:][:32] //BCE hint for compiler + sub := input[p:][:32] // BCE hint for compiler v1 = rol31(v1+u64(sub[:])*prime64_2) * prime64_1 v2 = rol31(v2+u64(sub[8:])*prime64_2) * prime64_1 v3 = rol31(v3+u64(sub[16:])*prime64_2) * prime64_1 diff --git a/utilities/xxhash/xxhash_test.go b/pkg/xxhash/xxhash_test.go similarity index 96% rename from utilities/xxhash/xxhash_test.go rename to pkg/xxhash/xxhash_test.go index 3b0a193..95d6ef0 100644 --- a/utilities/xxhash/xxhash_test.go +++ b/pkg/xxhash/xxhash_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/graph-guard/ggproxy/utilities/xxhash" + "github.com/graph-guard/ggproxy/pkg/xxhash" "github.com/pierrec/xxHash/xxHash64" "github.com/stretchr/testify/require" diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index 33abc5b..0000000 --- a/server/server_test.go +++ /dev/null @@ -1,512 +0,0 @@ -package server_test - -import ( - "embed" - "encoding/json" - "fmt" - "io/fs" - "net" - "path/filepath" - "regexp" - "strings" - "sync" - "testing" - "time" - - "github.com/graph-guard/ggproxy/config" - "github.com/graph-guard/ggproxy/server" - plog "github.com/phuslu/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/fasthttputil" - "gopkg.in/yaml.v3" -) - -//go:embed tests -var testsFS embed.FS - -type TestModel struct { - Client struct { - Input struct { - Method string `yaml:"method"` - Endpoint string `yaml:"endpoint"` - Body string `yaml:"body"` - BodyJSON map[string]any `yaml:"body(JSON)"` - } `yaml:"input"` - ExpectResponse struct { - Status int `yaml:"status"` - Headers map[string]string `yaml:"headers"` // Key -> Regexp - Body string `yaml:"body"` - BodyJSON map[string]any `yaml:"body(JSON)"` - } `yaml:"expect-response"` - } `yaml:"client"` - Destination *struct { - ExpectForwarded struct { - Headers map[string]string `yaml:"headers"` // Key -> Regexp - Body string `yaml:"body"` - BodyJSON map[string]any `yaml:"body(JSON)"` - } `yaml:"expect-forwarded"` - Response struct { - Status int `yaml:"status"` - Headers map[string]string `yaml:"headers"` - Body string `yaml:"body"` - BodyJSON map[string]any `yaml:"body(JSON)"` - } - } `yaml:"destination"` - Logs []map[string]any `yaml:"logs"` -} - -type Setup struct { - Name string - Config *config.Config - Tests []Test -} - -type Test struct { - Name string - TestModel -} - -func TestProxy(t *testing.T) { - setups := GetSetups(t, testsFS, "tests") - for _, setup := range setups { - t.Run(setup.Name, func(t *testing.T) { - clientProxy, forwarded, respSetter, logs := launchSetup(t, setup) - - for _, test := range setup.Tests { - t.Run(test.Name, func(t *testing.T) { - if test.Destination != nil { - body := test.Destination.Response.Body - if j := test.Destination.Response.BodyJSON; j != nil { - b, err := json.Marshal(j) - require.NoError(t, err) - body = string(b) - } - respSetter.Set(&SendResponse{ - Status: test.Destination.Response.Status, - Body: body, - Headers: copyMap(test.Destination.Response.Headers), - }) - } else { - respSetter.Set(nil) - } - - respStatus, respHeaders, respBody := doRequest( - t, clientProxy, - test.Client.Input.Method, - "localhost:8000", - test.Client.Input.Endpoint, - func(r *fasthttp.Request) { - r.Header.Set("Content-Type", "application/json") - body := test.Client.Input.Body - if j := test.Client.Input.BodyJSON; j != nil { - b, err := json.Marshal(j) - require.NoError(t, err) - body = string(b) - } - r.SetBodyString(body) - }, - ) - - if test.Destination != nil { - var f ReceivedRequest - ok := false - select { - case x := <-forwarded: - ok = true - f = x - default: - t.Errorf("the request wansn't forwarded as expected") - } - if ok { - compareHeaders( - t, "forwarded", - test.Destination.ExpectForwarded.Headers, - f.Headers, - ) - j := test.Destination.ExpectForwarded.BodyJSON - body := test.Destination.ExpectForwarded.Body - if j != nil { - b, err := json.Marshal(j) - require.NoError(t, err) - body = string(b) - } - assert.Equal( - t, body, f.Body, - "unexpected body was forwarded to destination", - ) - } - } - - // Compare results - if e := test.Client.ExpectResponse.Status; e != respStatus { - t.Errorf( - "unexpected response status: %d; expected: %d", - respStatus, e, - ) - } - compareHeaders( - t, "response", - test.Client.ExpectResponse.Headers, respHeaders, - ) - { - body := test.Client.ExpectResponse.Body - if j := test.Client.ExpectResponse.BodyJSON; j != nil { - b, err := json.Marshal(j) - require.NoError(t, err) - body = string(b) - } - assert.Equal( - t, body, respBody, - "unexpected response body", - ) - } - - // Check logs - logs.ReadLogs(func(m []map[string]any) { - for i, x := range m { - if i >= len(test.Logs) { - t.Errorf("unexpected log: %v", m[i]) - continue - } - assert.Equal(t, - test.Logs[i], x, - "unexpected log at index %d", i, - ) - } - }) - logs.Reset() - }) - } - }) - } -} - -func GetSetups(t *testing.T, filesystem fs.FS, path string) []Setup { - var setups []Setup - - d, err := fs.ReadDir(filesystem, path) - require.NoError(t, err) - for _, setupDir := range d { - if !setupDir.IsDir() { - continue - } - n := setupDir.Name() - if !strings.HasPrefix(n, "setup_") { - t.Logf("ignoring %q", filepath.Join(n)) - continue - } - - c, err := config.New(filepath.Join(path, n, "config.yaml")) - require.NoError(t, err) - - tests := GetTests(t, filesystem, filepath.Join(path, n)) - - setups = append(setups, Setup{ - Name: n, - Config: c, - Tests: tests, - }) - } - - return setups -} - -func GetTests(t *testing.T, filesystem fs.FS, root string) []Test { - var tests []Test - d, err := fs.ReadDir(filesystem, root) - require.NoError(t, err) - for _, testDir := range d { - n := testDir.Name() - if !strings.HasPrefix(n, "test_") || !strings.HasSuffix(n, ".yaml") { - continue - } - - f, err := filesystem.Open(filepath.Join(root, n)) - require.NoError(t, err) - defer f.Close() - var m TestModel - d := yaml.NewDecoder(f) - d.KnownFields(true) - err = d.Decode(&m) - require.NoError(t, err) - - isXOR(t, - m.Client.Input.Body, - m.Client.Input.BodyJSON, - "client.input.body", - "client.input.body(JSON)", - ) - isXOR(t, - m.Client.ExpectResponse.Body, - m.Client.ExpectResponse.BodyJSON, - "client.expect-response.body", - "client.expect-response.body(JSON)", - ) - if m.Destination != nil { - isXOR(t, - m.Destination.ExpectForwarded.Body, - m.Destination.ExpectForwarded.BodyJSON, - "destination.expect-forwarded.body", - "destination.expect-forwarded.body(JSON)", - ) - isXOR(t, - m.Destination.Response.Body, - m.Destination.Response.BodyJSON, - "destination.expect-forwarded.body", - "destination.expect-forwarded.body(JSON)", - ) - } - - tests = append(tests, Test{ - Name: n, - TestModel: m, - }) - } - return tests -} - -type SendResponse struct { - Status int - Body string - Headers map[string]string -} -type ReceivedRequest struct { - Body string - Headers map[string]string -} - -func launchSetup(t *testing.T, s Setup) ( - clientProxy *fasthttp.Client, - forwarded <-chan ReceivedRequest, - resp *Syncronized[*SendResponse], - logRecorder *LogRecorder, -) { - resp = new(Syncronized[*SendResponse]) - - lnDest := fasthttputil.NewInmemoryListener() - t.Cleanup(func() { lnDest.Close() }) - - lnProxy := fasthttputil.NewInmemoryListener() - t.Cleanup(func() { lnProxy.Close() }) - - forwardedRW := make(chan ReceivedRequest, 1) - forwarded = forwardedRW - - go func() { - s := &fasthttp.Server{ - Handler: func(ctx *fasthttp.RequestCtx) { - // Send the received request context for the check - var rr ReceivedRequest - rr.Headers = make(map[string]string, ctx.Request.Header.Len()) - ctx.Request.Header.VisitAll(func(key, value []byte) { - rr.Headers[string(key)] = string(value) - }) - rr.Body = string(ctx.Request.Body()) - forwardedRW <- rr - - // Send response - sr := resp.Get() - if sr == nil { - ctx.Error( - fasthttp.StatusMessage(fasthttp.StatusInternalServerError), - fasthttp.StatusInternalServerError, - ) - return - } - ctx.Response.SetStatusCode(sr.Status) - for k, v := range sr.Headers { - ctx.Response.Header.Set(k, v) - } - ctx.Response.SetBodyString(sr.Body) - }, - } - if err := s.Serve(lnDest); err != nil { - panic(err) - } - }() - - // Launch proxy server - logRecorder = new(LogRecorder) - log := plog.Logger{ - Level: plog.DebugLevel, - TimeField: "time", - TimeFormat: "23:59:59", - Writer: &plog.IOWriter{Writer: logRecorder}, - } - server := server.NewProxy( - s.Config, - time.Second*10, - time.Second*10, - 1024*64, - 1024*64, - log, - &fasthttp.Client{ - Dial: func(addr string) (net.Conn, error) { - return lnDest.Dial() - }, - }, - nil, - ) - - go func() { - server.Serve(lnProxy) - }() - - clientProxy = &fasthttp.Client{ - Dial: func(addr string) (net.Conn, error) { - return lnProxy.Dial() - }, - } - - return -} - -func doRequest( - t *testing.T, - client *fasthttp.Client, - method, host, path string, - prepareReq func(*fasthttp.Request), -) (status int, headers map[string]string, body string) { - req := fasthttp.AcquireRequest() - resp := fasthttp.AcquireResponse() - defer fasthttp.ReleaseRequest(req) - defer fasthttp.ReleaseResponse(resp) - - if prepareReq != nil { - prepareReq(req) - } - req.Header.SetMethod(method) - req.SetHost(host) - req.URI().SetPath(path) - - err := client.Do(req, resp) - require.NoError(t, err) - - status = resp.StatusCode() - headers = make(map[string]string, resp.Header.Len()) - resp.Header.VisitAll(func(key, value []byte) { - headers[string(key)] = string(value) - }) - body = string(resp.Body()) - return -} - -type Syncronized[T any] struct { - lock sync.Mutex - t T -} - -func (s *Syncronized[T]) Get() T { - s.lock.Lock() - defer s.lock.Unlock() - return s.t -} - -func (s *Syncronized[T]) Set(t T) { - s.lock.Lock() - defer s.lock.Unlock() - s.t = t -} - -func compareHeaders(t *testing.T, title string, expected, actual map[string]string) { - t.Helper() - e := make(map[string]string, len(expected)) - for k, v := range expected { - e[k] = v - } - a := make(map[string]string, len(actual)) - for k, v := range actual { - a[k] = v - } - for k, ev := range expected { - delete(e, k) - delete(a, k) - if av, ok := actual[k]; ok { - expr, err := regexp.Compile(ev) - if err != nil { - t.Fatalf( - "compiling regexp for %s header %q (%q): %v", - title, k, ev, err, - ) - } - if !expr.MatchString(av) { - t.Errorf( - "%s header %q expected regexp: %q; received: %q", - title, k, ev, av, - ) - } - } - } - for k, v := range e { - t.Errorf("missing %s header %q (%q)", title, k, v) - } - for k, v := range a { - t.Errorf("unexpected %s header %q (%q)", title, k, v) - } -} - -type LogRecorder struct { - Lock sync.Mutex - Recorded []map[string]any -} - -func (w *LogRecorder) Write(d []byte) (int, error) { - var m map[string]any - if err := json.Unmarshal(d, &m); err != nil { - return 0, fmt.Errorf("unmarshalling JSON: %w", err) - } - delete(m, "time") // We don't need to check the log time - w.Lock.Lock() - defer w.Lock.Unlock() - w.Recorded = append(w.Recorded, m) - return len(d), nil -} - -func (w *LogRecorder) Reset() { - w.Lock.Lock() - defer w.Lock.Unlock() - w.Recorded = nil -} - -func (w *LogRecorder) ReadLogs(fn func([]map[string]any)) { - w.Lock.Lock() - defer w.Lock.Unlock() - fn(w.Recorded) -} - -func copyMap[K comparable, V any](m map[K]V) (copy map[K]V) { - copy = make(map[K]V, len(m)) - for k, v := range m { - copy[k] = v - } - return copy -} - -func isXOR( - t *testing.T, - a string, b map[string]any, - aTitle, bTitle string, -) { - if (a != "" && b == nil) || (a == "" && b != nil) { - return - } - t.Fatalf(`"%s" (%q) and "%s" (%v) are mutually exclusive, `+ - `make sure you're using either of them, not both at the same time!`, - aTitle, a, bTitle, b, - ) -} - -// func checkLogs(t *testing.T, expected, actual []map[string]any) { -// for i, x := range expected { -// if i >= len(expected) { -// t.Errorf("unexpected log: %v", actual[i]) -// continue -// } -// assert.Equal(t, -// expected[i], x, -// "unexpected log at index %d", i, -// ) -// } -// } diff --git a/server/tests/README.md b/server/tests/README.md deleted file mode 100644 index 164ca4a..0000000 --- a/server/tests/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Tests - -Directory `tests` must contain all declarative test setups where each setup directory must have the prefix `setup_` followed by the name of the setup. -The setup directory must contain the full server configuration and test declarations which must have the prefix `test_` followed by the name of the test and the `.yaml` file extension. - -A test defines the clients inputs and expectations: -- `client.input.method` -- `client.input.endpoint` -- `client.input.body` -- `client.expect-response.status` -- `client.expect-response.headers` -- `client.expect-response.body` - -...and optionally, the destination server's outputs and expectations: -- `destination.expect-forwarded.headers` -- `destination.expect-forwarded.body` -- `destination.response.status` -- `destination.response.headers` -- `destination.response.body` - -An example test structure: - -``` -tests -├─ setup_A -│ ├─ config.yaml -│ ├─ services_enabled -│ │ └─ service_1 -│ │ ├─ config.yaml -│ │ └─ templates_enabled -│ │ └─ template_1.gqt -│ └─ test_1.yaml -└─ setup_B - ├─ config.yaml - ├─ services_enabled - │ └─ service_1 - │ ├─ config.yaml - │ └─ templates_enabled - │ └─ template_1.gqt - └─ test_1.yaml -``` - -An example test file: -```yaml -client: - input: - method: POST - endpoint: "/service_a" - body(JSON): - this: will - be: "sent to ggproxy" - expect-response: - status: 200 - headers: - Content-Length: ^74$ - Content-Type: ^text/plain; charset=utf-8$ - Server: "fasthttp" - Date: . - X-Custom-Header: value - body: > - this is what we expect to get from ggproxy - -destination: - expect-forwarded: - headers: - Host: ^http://localhost:8081/service_a$ - Content-Length: ^103$ - Content-Type: ^application/json$ - User-Agent: "fasthttp" - Date: . - body: > - this is what we expect to receive - on the destination server - response: - status: 200 - headers: - X-Custom-Header: value - body: > - this is what the destination server - will respond with - -logs: - - level: info - host: localhost:8080 - message: "listening" - services: - - service_a - - level: info - path: "/service_a" - message: "handling request" - - level: debug - path: "/service_a" - query: > - { - "query": "mutation X { a { a0(a0_0: [ 0 ]) } }", - "operationName": "X", - "variables": {} - } -``` \ No newline at end of file diff --git a/server/tests/setup_0/all-services/testservice.yaml b/server/tests/setup_0/all-services/testservice.yaml deleted file mode 100644 index cfac56d..0000000 --- a/server/tests/setup_0/all-services/testservice.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: "Test Service" -path: "/testservice" -forward-url: "http://localhost:8081/test" -forward-reduced: false -all-templates: ../all-templates/testservice -enabled-templates: ../enabled-templates/testservice diff --git a/server/tests/setup_0/all-templates/testservice/template_mut.gqt b/server/tests/setup_0/all-templates/testservice/template_mut.gqt deleted file mode 100644 index 6eee3f9..0000000 --- a/server/tests/setup_0/all-templates/testservice/template_mut.gqt +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: "Template Mutation" -tags: - - mutation ---- -mutation { - someMutations( - firstArg: val = "first" - secondArg: val = "second" - ) { - fieldA - fieldB { - subFieldC - } - } -} diff --git a/server/tests/setup_0/all-templates/testservice/template_qry.gqt b/server/tests/setup_0/all-templates/testservice/template_qry.gqt deleted file mode 100644 index be5a0f0..0000000 --- a/server/tests/setup_0/all-templates/testservice/template_qry.gqt +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "Template Query" -tags: - - query ---- -query { - queryFirstField { - queryFirstSubfield - querySecondSubfield - } - querySecondField -} diff --git a/server/tests/setup_0/enabled-services/testservice.yml b/server/tests/setup_0/enabled-services/testservice.yml deleted file mode 120000 index a761b41..0000000 --- a/server/tests/setup_0/enabled-services/testservice.yml +++ /dev/null @@ -1 +0,0 @@ -../all-services/testservice.yaml \ No newline at end of file diff --git a/server/tests/setup_0/enabled-templates/testservice/template_mut.gqt b/server/tests/setup_0/enabled-templates/testservice/template_mut.gqt deleted file mode 120000 index e4de39b..0000000 --- a/server/tests/setup_0/enabled-templates/testservice/template_mut.gqt +++ /dev/null @@ -1 +0,0 @@ -../../all-templates/testservice/template_mut.gqt \ No newline at end of file diff --git a/server/tests/setup_0/enabled-templates/testservice/template_qry.gqt b/server/tests/setup_0/enabled-templates/testservice/template_qry.gqt deleted file mode 120000 index 01e483d..0000000 --- a/server/tests/setup_0/enabled-templates/testservice/template_qry.gqt +++ /dev/null @@ -1 +0,0 @@ -../../all-templates/testservice/template_qry.gqt \ No newline at end of file diff --git a/server/tests/setup_0/x_test_mut_ok.yaml b/server/tests/setup_0/x_test_mut_ok.yaml deleted file mode 100644 index 45d027e..0000000 --- a/server/tests/setup_0/x_test_mut_ok.yaml +++ /dev/null @@ -1,65 +0,0 @@ -client: - input: - method: POST - endpoint: /testservice - body(JSON): - query: 'mutation { - someMutations( - firstArg: val = "first" - secondArg: val = "second" - ) { - fieldA - fieldB { - subFieldC - } - } - }' - - expect-response: - status: 200 - headers: - Content-Length: ^166$ - Content-Type: ^text/plain; charset=utf-8$ - Server: ^fasthttp$ - Date: . - X-Custom-Header: value - body(JSON): - data: - someMutations: - fieldA - fieldB - subFieldC - -destination: - expect-forwarded: - headers: - X-Forwarded-Host: ^localhost:8000$ - X-Forwarded-For: 0.0.0.0 - X-Forwarded-Proto: ^HTTP/1.1$ - Host: ^localhost:8081$ - Content-Length: ^97$ - Content-Type: ^application/json$ - User-Agent: ^fasthttp$ - Date: . - body(JSON): - query: 'query { - queryFirstField { - queryFirstSubfield - querySecondSubfield - } - querySecondField - }' - - response: - status: 200 - headers: - X-Custom-Header: value - body(JSON): - data: - queryFirstField: - queryFirstSubfield: 'first subfield value' - querySecondSubfield: 'second subfield value' - querySecondField: 'second query field value' - -logs: - diff --git a/server/tests/setup_1/all-templates/service_a/template_a.gqt b/server/tests/setup_1/all-templates/service_a/template_a.gqt deleted file mode 100644 index cd9d980..0000000 --- a/server/tests/setup_1/all-templates/service_a/template_a.gqt +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "Template A1" -tags: - - tag_a - - mutation ---- -mutation { - a { - a0( - a0_0: val = [ val <= 0 ] && val != [ val = -1 ] - ) - } -} diff --git a/server/tests/setup_1/config.yaml b/server/tests/setup_1/config.yaml deleted file mode 100644 index 76a0490..0000000 --- a/server/tests/setup_1/config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -proxy: - host: localhost:8080 -all-services: all-services -enabled-services: enabled-services diff --git a/server/tests/setup_1/enabled-services/service_a.yaml b/server/tests/setup_1/enabled-services/service_a.yaml deleted file mode 120000 index 953dca3..0000000 --- a/server/tests/setup_1/enabled-services/service_a.yaml +++ /dev/null @@ -1 +0,0 @@ -../all-services/service_a.yml \ No newline at end of file diff --git a/server/tests/setup_1/enabled-templates/service_a/template_a.gqt b/server/tests/setup_1/enabled-templates/service_a/template_a.gqt deleted file mode 120000 index a796d97..0000000 --- a/server/tests/setup_1/enabled-templates/service_a/template_a.gqt +++ /dev/null @@ -1 +0,0 @@ -../../all-templates/service_a/template_a.gqt \ No newline at end of file diff --git a/server/tests/setup_1/test_1.yaml b/server/tests/setup_1/test_1.yaml deleted file mode 100644 index d42ea98..0000000 --- a/server/tests/setup_1/test_1.yaml +++ /dev/null @@ -1,58 +0,0 @@ -client: - input: - method: POST - endpoint: /service_a - body(JSON): - query: 'mutation X { a { a0(a0_0: [ 0 ]) } }' - operationName: 'X' - - expect-response: - status: 200 - headers: - Content-Length: ^27$ - Content-Type: ^text/plain; charset=utf-8$ - Server: ^fasthttp$ - Date: . - X-Custom-Header: value - body(JSON): - data: - a: - a0: 'foo' - -destination: - expect-forwarded: - headers: - X-Forwarded-Host: ^localhost:8000$ - X-Forwarded-For: 0.0.0.0 - X-Forwarded-Proto: ^HTTP/1.1$ - Host: ^localhost:8081$ - Content-Length: ^68$ - Content-Type: ^application/json$ - User-Agent: ^fasthttp$ - Date: . - body(JSON): - query: 'mutation X { a { a0(a0_0: [ 0 ]) } }' - operationName: 'X' - - response: - status: 200 - headers: - X-Custom-Header: value - body(JSON): - data: - a: - a0: 'foo' - -logs: - - level: info - message: 'listening' - host: localhost:8080 - tls: false - services: - - service_a - - level: info - message: 'handling request' - path: /service_a - - level: debug - path: /service_a - query: '{"operationName":"X","query":"mutation X { a { a0(a0_0: [ 0 ]) } }"}' diff --git a/utilities/stack/stack.go b/utilities/stack/stack.go deleted file mode 100644 index 849fd3d..0000000 --- a/utilities/stack/stack.go +++ /dev/null @@ -1,59 +0,0 @@ -package stack - -// Stack is an implementation of stack container. -type Stack[T any] struct{ s []T } - -// New creates a new instance of Stack. -func New[T any](capacity int) *Stack[T] { - return &Stack[T]{s: make([]T, 0, capacity)} -} - -// Reset resets the stack. -func (s *Stack[T]) Reset() { s.s = s.s[:0] } - -// Push adds an element to the stack. -func (s *Stack[T]) Push(f T) { s.s = append(s.s, f) } - -// Pop returns and deletes the last stack element. -func (s *Stack[T]) Pop() (top T) { - if l := len(s.s) - 1; l >= 0 { - top = s.s[l] - s.s = s.s[:l] - } - return -} - -// PopPush executes Pop and Push operations in sequence. -func (s *Stack[T]) PopPush(f T) (popped T) { - if l := len(s.s) - 1; l >= 0 { - popped = s.s[l] - s.s = s.s[:l] - } - s.s = append(s.s, f) - return -} - -// Top returns the last stack element. -func (s *Stack[T]) Top() (top T) { - if l := len(s.s) - 1; l >= 0 { - return s.s[l] - } - return -} - -// TopOffsetFn calls fn with the last stack element at offset. -func (s *Stack[T]) TopOffsetFn(offset int, fn func(*T)) { - if l := len(s.s) - 1; l >= 0 { - fn(&s.s[l-offset]) - } -} - -// Get returns the element at index from bottom. -func (s *Stack[T]) Get(index int) T { - return s.s[index] -} - -// Len returns the stack length. -func (s *Stack[T]) Len() int { - return len(s.s) -} diff --git a/utilities/stack/stack_test.go b/utilities/stack/stack_test.go deleted file mode 100644 index 19ac954..0000000 --- a/utilities/stack/stack_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package stack_test - -import ( - "testing" - - "github.com/graph-guard/ggproxy/utilities/stack" - "github.com/stretchr/testify/require" -) - -func TestReset(t *testing.T) { - st := stack.New[uint16](2) - st.Push(0) - st.Reset() - require.Equal(t, stack.New[uint16](2), st) -} - -func TestPushLen(t *testing.T) { - st := stack.New[int16](4) - st.Push(0) - st.Push(1) - st.Push(-1) - require.Equal(t, 3, st.Len()) -} - -func TestPop(t *testing.T) { - st := stack.New[int64](4) - st.Push(0) - st.Push(1) - st.Push(-1) - require.Equal(t, int64(-1), st.Pop()) - st.Pop() - st.Pop() - require.Equal(t, int64(0), st.Pop()) -} - -func TestPopPush(t *testing.T) { - st := stack.New[float64](2) - st.Push(0.0) - require.Equal(t, 0.0, st.PopPush(-1)) - require.Equal(t, -1.0, st.Pop()) -} - -func TestTop(t *testing.T) { - st := stack.New[int](2) - st.Push(0) - st.Push(-1) - require.Equal(t, -1, st.Top()) - st.Pop() - st.Pop() - require.Equal(t, 0, st.Top()) -} - -func TestTopOffsetFn(t *testing.T) { - st := stack.New[int](2) - st.Push(0) - st.Push(-1) - st.TopOffsetFn(0, func(i *int) { - require.Equal(t, -1, *i) - *i = 20 - }) - st.TopOffsetFn(1, func(i *int) { - require.Equal(t, 0, *i) - *i = 10 - }) - require.Equal(t, 20, st.Pop()) - require.Equal(t, 10, st.Pop()) - require.Equal(t, 0, st.Top()) -} - -func TestGet(t *testing.T) { - st := stack.New[int](2) - st.Push(0) - st.Push(-1) - require.Equal(t, 0, st.Get(0)) - require.Equal(t, -1, st.Get(1)) -}