diff --git a/docs/zed.md b/docs/zed.md index 848825e8..78dc5219 100644 --- a/docs/zed.md +++ b/docs/zed.md @@ -43,7 +43,7 @@ zed permission check --explain document:firstdoc writer user:emilia - [zed relationship](#reference-zed-relationship) - Query and mutate the relationships in a permissions system - [zed schema](#reference-zed-schema) - Manage schema for a permissions system - [zed use](#reference-zed-use) - Alias for `zed context use` -- [zed validate](#reference-zed-validate) - Validates the given validation file (.yaml, .zaml) or schema file (.zed) +- [zed validate](#reference-zed-validate) - Validates the given validation files (.yaml, .zaml) or schema files (.zed) - [zed version](#reference-zed-version) - Display zed and SpiceDB version information @@ -1399,10 +1399,10 @@ zed use ## Reference: `zed validate` -Validates the given validation file (.yaml, .zaml) or schema file (.zed) +Validates the given validation files (.yaml, .zaml) or schema files (.zed) ``` -zed validate [flags] +zed validate [flags] ``` ### Examples @@ -1423,17 +1423,13 @@ zed validate [flags] From pastebin: zed validate https://pastebin.com/8qU45rVK - - From a devtools instance: - zed validate https://localhost:8443/download ``` ### Options ``` - --fail-on-warn treat warnings as errors during validation - --force-color force color code output even in non-tty environments - --schema-type string force validation according to specific schema syntax ("", "composable", "standard") + --fail-on-warn treat warnings as errors during validation + --force-color force color code output even in non-tty environments ``` ### Options Inherited From Parent Flags diff --git a/go.mod b/go.mod index 25b74a1b..d169e733 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 github.com/authzed/authzed-go v1.8.0 github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b - github.com/authzed/spicedb v1.49.2 + github.com/authzed/spicedb v1.50.1-0.20260320043636-2f18c9efbd63 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/ccoveille/go-safecast/v2 v2.0.0 github.com/cenkalti/backoff/v4 v4.3.0 @@ -42,6 +42,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/xlab/treeprint v1.2.0 + go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/mod v0.32.0 @@ -60,6 +61,7 @@ require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.10-20240617172848-e1dbca2775a7.1 // indirect buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251118093737-4105057cc7d4.1 // indirect + buf.build/go/protovalidate v1.1.0 // indirect cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.122.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect @@ -102,7 +104,7 @@ require ( github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect github.com/authzed/cel-go v0.20.2 // indirect @@ -165,8 +167,6 @@ require ( github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlmiddlecote/sqlstats v1.0.2 // indirect - github.com/docker/cli v29.2.0+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect @@ -223,6 +223,7 @@ require ( github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect @@ -295,9 +296,6 @@ require ( github.com/mgechev/revive v1.13.0 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/moby/api v1.53.0 // indirect - github.com/moby/moby/client v0.2.2 // indirect - github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/moricho/tparallel v0.3.2 // indirect @@ -312,7 +310,6 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -361,7 +358,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tetafro/godot v1.5.4 // indirect @@ -406,7 +403,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/go.sum b/go.sum index 8c04c9df..7301ef54 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.10-20240617172848-e1dbca buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.10-20240617172848-e1dbca2775a7.1/go.mod h1:3ddKE6u98YQFS1jpuYmVEmU1fdAiHqB5Re6S3E16/mI= buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251118093737-4105057cc7d4.1 h1:aKwzrmsRDQkiEzGmjiMVjyYfwHHsFFm7tmQgVA2vyOM= buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251118093737-4105057cc7d4.1/go.mod h1:BdURQlk1lXab5ov60A7yLZZONSP0Cho+RkOntf+FZF8= +buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY= +buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -721,8 +723,8 @@ github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEW github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= @@ -742,8 +744,8 @@ github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b h1:wbh8IK+aMLTCey github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b/go.mod h1:s3qC7V7XIbiNWERv7Lfljy/Lx25/V1Qlexb0WJuA8uQ= github.com/authzed/jitterbug v0.0.0-20260128162915-e97d76daaa24 h1:BXaWSanmHFu3P0xWfTDPpwcJIQ/oSol29+CWe4lSGSU= github.com/authzed/jitterbug v0.0.0-20260128162915-e97d76daaa24/go.mod h1:WvEk4YHnUsmbUaWA/VseQty3X91f6/jEHek5mjYDZUg= -github.com/authzed/spicedb v1.49.2 h1:6LKOxiNN7K18x4xs2NB0dhnDNDypbpuOP7s06AwjCH8= -github.com/authzed/spicedb v1.49.2/go.mod h1:I9t8PtFBxUHsSZKfrkK6bxbMy8La7LYjkpgw6UpNHQs= +github.com/authzed/spicedb v1.50.1-0.20260320043636-2f18c9efbd63 h1:KHAJIFqN3Z4M+QbyvEt2kq7NN6+F4sjzoOuLfj3N85o= +github.com/authzed/spicedb v1.50.1-0.20260320043636-2f18c9efbd63/go.mod h1:kV6L+7b1bDVeoHfKPSJt+uLHDUl9hAT/yjNYayF3iyM= github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU= @@ -1149,6 +1151,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= @@ -1451,10 +1455,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= -github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= -github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= +github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= +github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= +github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -1603,6 +1607,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1689,8 +1695,8 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go index c18475f4..48413e10 100644 --- a/internal/cmd/cmd_test.go +++ b/internal/cmd/cmd_test.go @@ -41,7 +41,7 @@ func TestCommandOutput(t *testing.T) { command: []string{"zed", "validate"}, expectFlagErrorCalled: true, flagErrorContains: "requires at least 1 arg(s), only received 0", - expectUsageContains: "zed validate [flags]", + expectUsageContains: "zed validate [flags]", }, { name: "prints correct usage", diff --git a/internal/cmd/import.go b/internal/cmd/import.go index 37e35be0..5349f2e4 100644 --- a/internal/cmd/import.go +++ b/internal/cmd/import.go @@ -13,7 +13,6 @@ import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/authzed/spicedb/pkg/tuple" - "github.com/authzed/spicedb/pkg/validationfile" "github.com/authzed/zed/internal/client" "github.com/authzed/zed/internal/commands" @@ -82,12 +81,12 @@ func registerImportCmd(rootCmd *cobra.Command) { func importCmdFunc(cmd *cobra.Command, schemaClient v1.SchemaServiceClient, relationshipsClient v1.PermissionsServiceClient, prefix string, u *url.URL) error { prefix = strings.TrimRight(prefix, "/") - decoder, err := decode.DecoderForURL(u) + d, err := decode.DecoderFromURL(u) if err != nil { return err } - var p validationfile.ValidationFile - if _, _, err := decoder(&p); err != nil { + p, err := d.UnmarshalYAMLValidationFile() + if err != nil { return err } diff --git a/internal/cmd/preview-test/composable-schema-invalid-root.zed b/internal/cmd/preview-test/composable-schema-invalid-root.zed index bc5138dd..67889974 100644 --- a/internal/cmd/preview-test/composable-schema-invalid-root.zed +++ b/internal/cmd/preview-test/composable-schema-invalid-root.zed @@ -1,6 +1,9 @@ +use import + import "./composable-schema-user.zed" definition resource { - relation and: user - permission viewer = and -} \ No newline at end of file + // definition is a keyword, so we expect it to fail here. + relation definition: user + permission viewer = definition +} diff --git a/internal/cmd/preview-test/composable-schema-root.zed b/internal/cmd/preview-test/composable-schema-root.zed index b58c62a3..40885370 100644 --- a/internal/cmd/preview-test/composable-schema-root.zed +++ b/internal/cmd/preview-test/composable-schema-root.zed @@ -1,6 +1,8 @@ +use import + import "./composable-schema-user.zed" definition resource { relation user: user permission view = user -} \ No newline at end of file +} diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go index 912b1dbd..54ba2688 100644 --- a/internal/cmd/schema.go +++ b/internal/cmd/schema.go @@ -18,8 +18,6 @@ import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/authzed/spicedb/pkg/caveats/types" - newcompiler "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" - newinput "github.com/authzed/spicedb/pkg/composableschemadsl/input" "github.com/authzed/spicedb/pkg/diff" "github.com/authzed/spicedb/pkg/schemadsl/compiler" "github.com/authzed/spicedb/pkg/schemadsl/generator" @@ -435,27 +433,17 @@ func schemaCompileInner(args []string, writer io.Writer) error { return errors.New("attempted to compile empty schema") } - compiled, err := newcompiler.Compile(newcompiler.InputSchema{ - Source: newinput.Source(inputFilepath), + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source(inputFilepath), SchemaString: string(schemaBytes), - }, newcompiler.AllowUnprefixedObjectType(), - newcompiler.SourceFolder(inputSourceFolder)) + }, compiler.AllowUnprefixedObjectType(), + compiler.SourceFolder(inputSourceFolder)) if err != nil { return err } - // Attempt to cast one kind of OrderedDefinition to another - oldDefinitions := make([]compiler.SchemaDefinition, 0, len(compiled.OrderedDefinitions)) - for _, definition := range compiled.OrderedDefinitions { - oldDefinition, ok := definition.(compiler.SchemaDefinition) - if !ok { - return fmt.Errorf("could not convert definition to old schemadefinition: %v", oldDefinition) - } - oldDefinitions = append(oldDefinitions, oldDefinition) - } - - // This is where we functionally assert that the two systems are compatible - generated, _, err := generator.GenerateSchema(oldDefinitions) + // Generate the schema, which compiles over import and partial syntax + generated, _, err := generator.GenerateSchema(compiled.OrderedDefinitions) if err != nil { return fmt.Errorf("could not generate resulting schema: %w", err) } diff --git a/internal/cmd/schema_test.go b/internal/cmd/schema_test.go index e4bef19d..ad658113 100644 --- a/internal/cmd/schema_test.go +++ b/internal/cmd/schema_test.go @@ -18,7 +18,7 @@ import ( "google.golang.org/grpc" v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" - "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" "github.com/authzed/zed/internal/zedtesting" ) diff --git a/internal/cmd/validate-test/composable-schema-imported-with-error.zed b/internal/cmd/validate-test/composable-schema-imported-with-error.zed new file mode 100644 index 00000000..69c9e08c --- /dev/null +++ b/internal/cmd/validate-test/composable-schema-imported-with-error.zed @@ -0,0 +1,10 @@ +// this is a userrr +definition user {} + +definition group { + permission view = unknownrel +} + +caveat is_raining(day string) { + day == "sat" || day == "sun" +} \ No newline at end of file diff --git a/internal/cmd/validate-test/composable-schema-imports-file-with-error.zed b/internal/cmd/validate-test/composable-schema-imports-file-with-error.zed new file mode 100644 index 00000000..b3c705bb --- /dev/null +++ b/internal/cmd/validate-test/composable-schema-imports-file-with-error.zed @@ -0,0 +1,7 @@ +use import + +import "composable-schema-imported-with-error.zed" + +definition resource { + relation view: user with is_raining +} \ No newline at end of file diff --git a/internal/cmd/validate-test/composable-schema-root.zed b/internal/cmd/validate-test/composable-schema-root.zed index 264c4314..40885370 100644 --- a/internal/cmd/validate-test/composable-schema-root.zed +++ b/internal/cmd/validate-test/composable-schema-root.zed @@ -1,3 +1,5 @@ +use import + import "./composable-schema-user.zed" definition resource { diff --git a/internal/cmd/validate-test/composable-schema-warning-root.zed b/internal/cmd/validate-test/composable-schema-warning-root.zed index 892f3472..50346ba9 100644 --- a/internal/cmd/validate-test/composable-schema-warning-root.zed +++ b/internal/cmd/validate-test/composable-schema-warning-root.zed @@ -1,7 +1,9 @@ +use partial + partial edit_partial { permission edit = edit } definition resource { ...edit_partial -} \ No newline at end of file +} diff --git a/internal/cmd/validate-test/composable-schema-with-import-error-imported.zed b/internal/cmd/validate-test/composable-schema-with-import-error-imported.zed new file mode 100644 index 00000000..6e397e43 --- /dev/null +++ b/internal/cmd/validate-test/composable-schema-with-import-error-imported.zed @@ -0,0 +1,6 @@ +definition user {} + +definition group { + relation member: user + permission view = unknownrel +} diff --git a/internal/cmd/validate-test/composable-schema-with-import-error-root.zed b/internal/cmd/validate-test/composable-schema-with-import-error-root.zed new file mode 100644 index 00000000..a6f369f1 --- /dev/null +++ b/internal/cmd/validate-test/composable-schema-with-import-error-root.zed @@ -0,0 +1,8 @@ +use import + +import "./composable-schema-with-import-error-imported.zed" + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/internal/cmd/validate-test/external-composable-with-error.yaml b/internal/cmd/validate-test/external-composable-with-error.yaml new file mode 100644 index 00000000..4c4c49e8 --- /dev/null +++ b/internal/cmd/validate-test/external-composable-with-error.yaml @@ -0,0 +1,7 @@ +--- +schemaFile: "./composable-schema-with-import-error-root.zed" +relationships: >- + resource:1#viewer@user:1 +assertions: + assertTrue: + - "resource:1#viewer@user:1" diff --git a/internal/cmd/validate-test/external-schema-escape.yaml b/internal/cmd/validate-test/external-schema-escape.yaml new file mode 100644 index 00000000..4a23cf79 --- /dev/null +++ b/internal/cmd/validate-test/external-schema-escape.yaml @@ -0,0 +1,7 @@ +--- +schemaFile: "../some-schema.zed" +relationships: >- + resource:1#user@user:1 +assertions: + assertTrue: + - "resource:1#user@user:1" diff --git a/internal/cmd/validate-test/nested-composable-schema.zed b/internal/cmd/validate-test/nested-composable-schema.zed new file mode 100644 index 00000000..b41dfe4f --- /dev/null +++ b/internal/cmd/validate-test/nested-composable-schema.zed @@ -0,0 +1,3 @@ +use import + +import "./nonexistant-import.zed" // this file exists but it imports a file that doesn't exist \ No newline at end of file diff --git a/internal/cmd/validate-test/nonexistant-import.zed b/internal/cmd/validate-test/nonexistant-import.zed new file mode 100644 index 00000000..140ca505 --- /dev/null +++ b/internal/cmd/validate-test/nonexistant-import.zed @@ -0,0 +1,7 @@ +use import + +definition user {} + +definition resource {} + +import "doesnotexist.zed" diff --git a/internal/cmd/validate-test/only-passes-composable.zed b/internal/cmd/validate-test/only-passes-composable.zed deleted file mode 100644 index 57cbf2fc..00000000 --- a/internal/cmd/validate-test/only-passes-composable.zed +++ /dev/null @@ -1,5 +0,0 @@ -partial foo {} - -definition bar { - ...foo -} diff --git a/internal/cmd/validate-test/only-passes-standard.zed b/internal/cmd/validate-test/only-passes-standard.zed deleted file mode 100644 index 87044628..00000000 --- a/internal/cmd/validate-test/only-passes-standard.zed +++ /dev/null @@ -1,2 +0,0 @@ -// "and" is a reserved keyword in composable schemas -definition and {} diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 6cc981a2..49425e56 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -3,8 +3,10 @@ package cmd import ( "errors" "fmt" + "io/fs" "net/url" "os" + "path/filepath" "strconv" "strings" @@ -14,12 +16,9 @@ import ( "github.com/muesli/termenv" "github.com/spf13/cobra" - composable "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" "github.com/authzed/spicedb/pkg/development" core "github.com/authzed/spicedb/pkg/proto/core/v1" devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" - "github.com/authzed/spicedb/pkg/schemadsl/compiler" - "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/validationfile" "github.com/authzed/zed/internal/commands" @@ -50,8 +49,8 @@ var ( func registerValidateCmd(cmd *cobra.Command) { validateCmd := &cobra.Command{ - Use: "validate ", - Short: "Validates the given validation file (.yaml, .zaml) or schema file (.zed)", + Use: "validate ", + Short: "Validates the given validation files (.yaml, .zaml) or schema files (.zed)", Example: ` From a local file (with prefix): zed validate file:///Users/zed/Downloads/authzed-x7izWU8_2Gw3.yaml @@ -66,12 +65,9 @@ func registerValidateCmd(cmd *cobra.Command) { zed validate https://play.authzed.com/s/iksdFvCtvnkR/schema From pastebin: - zed validate https://pastebin.com/8qU45rVK - - From a devtools instance: - zed validate https://localhost:8443/download`, + zed validate https://pastebin.com/8qU45rVK`, Args: commands.ValidationWrapper(cobra.MinimumNArgs(1)), - ValidArgsFunction: commands.FileExtensionCompletions("zed", "yaml", "zaml"), + ValidArgsFunction: commands.FileExtensionCompletions("zed", "yaml", "yml", "zaml"), PreRunE: validatePreRunE, RunE: func(cmd *cobra.Command, filenames []string) error { result, shouldExit, err := validateCmdFunc(cmd, filenames) @@ -93,12 +89,9 @@ func registerValidateCmd(cmd *cobra.Command) { validateCmd.Flags().Bool("force-color", false, "force color code output even in non-tty environments") validateCmd.Flags().Bool("fail-on-warn", false, "treat warnings as errors during validation") - validateCmd.Flags().String("schema-type", "", "force validation according to specific schema syntax (\"\", \"composable\", \"standard\")") cmd.AddCommand(validateCmd) } -var validSchemaTypes = []string{"", "standard", "composable"} - func validatePreRunE(cmd *cobra.Command, _ []string) error { // Override lipgloss's autodetection of whether it's in a terminal environment // and display things in color anyway. This can be nice in CI environments that @@ -108,17 +101,6 @@ func validatePreRunE(cmd *cobra.Command, _ []string) error { lipgloss.SetColorProfile(termenv.ANSI256) } - schemaType := cobrautil.MustGetString(cmd, "schema-type") - schemaTypeValid := false - for _, validType := range validSchemaTypes { - if schemaType == validType { - schemaTypeValid = true - } - } - if !schemaTypeValid { - return fmt.Errorf("schema-type must be one of \"\", \"standard\", \"composable\". received: %s", schemaType) - } - return nil } @@ -130,7 +112,6 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro successfullyValidatedFiles = 0 shouldExit = false toPrint = &strings.Builder{} - schemaType = cobrautil.MustGetString(cmd, "schema-type") failOnWarn = cobrautil.MustGetBool(cmd, "fail-on-warn") ) @@ -142,56 +123,51 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro u, err := url.Parse(filename) if err != nil { - return "", false, err + return "", true, err } - decoder, err := decode.DecoderForURL(u) + d, err := decode.DecoderFromURL(u) if err != nil { - return "", false, err + return "", true, err } + validateContents := d.Contents - var parsed validationfile.ValidationFile - // the decoder is also where compilation happens. - validateContents, isOnlySchema, err := decoder(&parsed) - standardErrors, composableErrs, otherErrs := classifyErrors(err) - - switch schemaType { - case "standard": - if standardErrors != nil { - var errWithSource spiceerrors.WithSourceError - if errors.As(standardErrors, &errWithSource) { - outputErrorWithSource(toPrint, validateContents, errWithSource) - shouldExit = true - } - return "", shouldExit, standardErrors - } - case "composable": - if composableErrs != nil { - var errWithSource spiceerrors.WithSourceError - if errors.As(composableErrs, &errWithSource) { - outputErrorWithSource(toPrint, validateContents, errWithSource) - shouldExit = true - } - return "", shouldExit, composableErrs - } + // Root the filesystem at the directory containing the schema file, so + // that relative imports resolve correctly. + fileDir := filepath.Dir(filename) + if fileDir == "" { + fileDir = "." + } + filesystem := os.DirFS(fileDir) + + // Parse the zed or YAML file + fileType := ".zed" + ext := filepath.Ext(filename) + if ext != "" { + fileType = ext + } + + var parsed *validationfile.ValidationFile + switch fileType { + case ".yaml", ".yml": + parsed, err = d.UnmarshalYAMLValidationFile() + case ".zed": + parsed = d.UnmarshalSchemaValidationFile() default: - // By default, validate will attempt to validate a schema first according to composable schema rules, - // then standard schema rules, - // and if both fail it will show the errors from composable schema. - if composableErrs != nil && standardErrors != nil { - var errWithSource spiceerrors.WithSourceError - if errors.As(composableErrs, &errWithSource) { - outputErrorWithSource(toPrint, validateContents, errWithSource) - shouldExit = true - } - return "", shouldExit, composableErrs - } + parsed, err = d.UnmarshalAsYAMLOrSchema() + } + // This block handles the error regardless of which case statement is hit + if err != nil { + return "", true, err } - if otherErrs != nil { - return "", false, otherErrs + // Ensure that either schema or schemaFile is present + if parsed.Schema.Schema == "" && parsed.SchemaFile == "" { + return "", false, errors.New("either schema or schemaFile must be present") } + // This logic will use the zero value of the struct, so we don't need + // to do it conditionally. tuples := make([]*core.RelationTuple, 0) totalAssertions := 0 totalRelationsValidated := 0 @@ -205,20 +181,18 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro devCtx, devErrs, err := development.NewDevContext(ctx, &devinterface.RequestContext{ Schema: parsed.Schema.Schema, Relationships: tuples, - }) + }, development.WithSourceFS(filesystem), development.WithRootFileName(filepath.Base(filename))) if err != nil { return "", false, err } if devErrs != nil { - // Calculate the schema offset, used for outputting errors and warnings - // and having them point to the right place regardless of zed vs yaml - schemaOffset := parsed.Schema.SourcePosition.LineNumber - if isOnlySchema { - schemaOffset = 0 + schemaOffset := 0 + if fileType != ".zed" { + schemaOffset = parsed.Schema.SourcePosition.LineNumber } // Output errors - outputDeveloperErrorsWithLineOffset(toPrint, validateContents, devErrs.InputErrors, schemaOffset) + outputDeveloperErrorsWithLineOffset(toPrint, validateContents, devErrs.InputErrors, schemaOffset, filesystem) return toPrint.String(), true, nil } // Run assertions @@ -227,7 +201,7 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro return "", false, aerr } if adevErrs != nil { - outputDeveloperErrors(toPrint, validateContents, adevErrs) + outputDeveloperErrors(toPrint, validateContents, adevErrs, filesystem) return toPrint.String(), true, nil } successfullyValidatedFiles++ @@ -238,7 +212,7 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro return "", false, rerr } if erDevErrs != nil { - outputDeveloperErrors(toPrint, validateContents, erDevErrs) + outputDeveloperErrors(toPrint, validateContents, erDevErrs, filesystem) return toPrint.String(), true, nil } // Print out any warnings for file @@ -277,11 +251,6 @@ func validateCmdFunc(cmd *cobra.Command, filenames []string) (string, bool, erro return toPrint.String(), shouldExit, nil } -func outputErrorWithSource(sb *strings.Builder, validateContents []byte, errWithSource spiceerrors.WithSourceError) { - fmt.Fprintf(sb, "%s%s\n", errorPrefix(), errorMessageStyle().Render(errWithSource.Error())) - outputForLine(sb, validateContents, errWithSource.LineNumber, errWithSource.SourceCodeString, 0) // errWithSource.LineNumber is 1-indexed -} - func outputForLine(sb *strings.Builder, validateContents []byte, oneIndexedLineNumber uint64, sourceCodeString string, oneIndexedColumnPosition uint64) { lines := strings.Split(string(validateContents), "\n") intLineNumber, err := safecast.Convert[int](oneIndexedLineNumber) @@ -304,14 +273,20 @@ func outputForLine(sb *strings.Builder, validateContents []byte, oneIndexedLineN } } -func outputDeveloperErrors(sb *strings.Builder, validateContents []byte, devErrors []*devinterface.DeveloperError) { - outputDeveloperErrorsWithLineOffset(sb, validateContents, devErrors, 0) +func outputDeveloperErrors(sb *strings.Builder, validateContents []byte, devErrors []*devinterface.DeveloperError, sourceFS fs.FS) { + outputDeveloperErrorsWithLineOffset(sb, validateContents, devErrors, 0, sourceFS) } -func outputDeveloperErrorsWithLineOffset(sb *strings.Builder, validateContents []byte, devErrors []*devinterface.DeveloperError, lineOffset int) { - lines := strings.Split(string(validateContents), "\n") - +func outputDeveloperErrorsWithLineOffset(sb *strings.Builder, validateContents []byte, devErrors []*devinterface.DeveloperError, lineOffset int, sourceFS fs.FS) { for _, devErr := range devErrors { + // If the error has a Path, read the contents from that file instead. + if len(devErr.Path) > 0 { + fileContents, err := fs.ReadFile(sourceFS, devErr.Path[0]) + if err == nil { + validateContents = fileContents + } + } + lines := strings.Split(string(validateContents), "\n") outputDeveloperError(sb, devErr, lines, lineOffset) } } @@ -404,28 +379,3 @@ func renderLine(sb *strings.Builder, lines []string, index int, highlight string highlightedSourceStyle().Render(strings.Repeat("~", highlightLength))) } } - -// classifyErrors returns errors from the composable DSL, the standard DSL, and any other parsing errors. -func classifyErrors(err error) (error, error, error) { - if err == nil { - return nil, nil, nil - } - var standardErr compiler.BaseCompilerError - var composableErr composable.BaseCompilerError - var retStandard, retComposable, allOthers error - - ok := errors.As(err, &standardErr) - if ok { - retStandard = standardErr - } - ok = errors.As(err, &composableErr) - if ok { - retComposable = composableErr - } - - if retStandard == nil && retComposable == nil { - allOthers = err - } - - return retStandard, retComposable, allOthers -} diff --git a/internal/cmd/validate_test.go b/internal/cmd/validate_test.go index 39b4f32c..e9e4aa62 100644 --- a/internal/cmd/validate_test.go +++ b/internal/cmd/validate_test.go @@ -47,53 +47,23 @@ func normalizeNewlines(s string) string { func TestValidatePreRun(t *testing.T) { t.Parallel() - testCases := map[string]struct { - schemaTypeFlag string - expectErr bool - }{ - `invalid`: { - schemaTypeFlag: "invalid", - expectErr: true, - }, - `composable`: { - schemaTypeFlag: "composable", - expectErr: false, - }, - `standard`: { - schemaTypeFlag: "standard", - expectErr: false, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, - zedtesting.BoolFlag{FlagName: "force-color", FlagValue: false}, - zedtesting.StringFlag{FlagName: "schema-type", FlagValue: tc.schemaTypeFlag}, - zedtesting.BoolFlag{FlagName: "fail-on-warn", FlagValue: false}, - ) + cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, + zedtesting.BoolFlag{FlagName: "force-color", FlagValue: false}, + zedtesting.BoolFlag{FlagName: "fail-on-warn", FlagValue: false}, + ) - err := validatePreRunE(cmd, []string{}) - if tc.expectErr { - require.ErrorContains(t, err, "schema-type must be one of \"\", \"standard\", \"composable\"") - } else { - require.NoError(t, err) - } - }) - } + err := validatePreRunE(cmd, []string{}) + require.NoError(t, err) } func TestValidate(t *testing.T) { t.Parallel() testCases := map[string]struct { - schemaTypeFlag string files []string expectErr string expectStr string - expectNonZeroStatusCode bool + expectNonZeroStatusCode bool // when there is an error with the validation process OR an error in the schema }{ `standard_passes`: { files: []string{ @@ -124,7 +94,10 @@ total files: 2, successfully validated files: 2 filepath.Join("validate-test", "standard-validation.yaml"), filepath.Join("validate-test", "invalid-schema.zed"), }, - expectErr: "Unexpected token at root level", + expectStr: filepath.Join("validate-test", "standard-validation.yaml") + ` +Success! - 1 relationships loaded, 2 assertions run, 0 expected relations validated +` + filepath.Join("validate-test", "invalid-schema.zed") + "\nerror: parse error in `something`, line 1, column 1: Unexpected token at root level: TokenTypeIdentifier \n 1 > something something {}\n > ^~~~~~~~~\n 2 | \n\n\n", + expectNonZeroStatusCode: true, }, `schema_only_passes`: { files: []string{ @@ -142,13 +115,8 @@ total files: 2, successfully validated files: 2 files: []string{ filepath.Join("validate-test", "invalid-schema.zed"), }, - expectErr: "Unexpected token at root level", - }, - `standard_only_without_flag_passes`: { - files: []string{ - filepath.Join("validate-test", "only-passes-standard.zed"), - }, - expectStr: "Success! - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", + expectStr: "error: parse error in `something`, line 1, column 1: Unexpected token at root level: TokenTypeIdentifier \n 1 > something something {}\n > ^~~~~~~~~\n 2 | \n\n\n", + expectNonZeroStatusCode: true, }, `without_schema_fails`: { files: []string{ @@ -208,13 +176,15 @@ complete - 0 relationships loaded, 0 assertions run, 0 expected relations valida files: []string{ "http://%zz", }, - expectErr: "invalid URL escape", + expectErr: "invalid URL escape", + expectNonZeroStatusCode: true, }, `url_does_not_exist_fails`: { files: []string{ "https://unknown-url", }, - expectErr: "Get \"https://unknown-url\": dial tcp: lookup unknown-url", + expectErr: "Get \"https://unknown-url\": dial tcp: lookup unknown-url", + expectNonZeroStatusCode: true, }, `missing_relation_fails`: { files: []string{ @@ -254,66 +224,64 @@ complete - 0 relationships loaded, 0 assertions run, 0 expected relations valida }, expectStr: "Success! - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", }, - `composable_schema_only_without_flag_passes`: { + `composable_in_validation_yaml_with_composable_passes`: { + files: []string{ + filepath.Join("validate-test", "external-and-composable.yaml"), + }, + expectStr: "Success! - 1 relationships loaded, 2 assertions run, 0 expected relations validated\n", + }, + `warnings_in_composable_fail`: { files: []string{ - filepath.Join("validate-test", "only-passes-composable.zed"), + filepath.Join("validate-test", "composable-schema-warning-root.zed"), }, - expectStr: "Success! - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", + expectStr: "warning: Permission \"edit\" references itself, which will cause an error to be raised due to infinite recursion (permission-references-itself)\n 1 | use partial\n 2 | \n 3 | partial edit_partial {\n 4 > permission edit = edit\n > ^~~~\n 5 | }\n 6 | \n\ncomplete - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", }, - `standard_only_with_composable_flag_fails`: { - schemaTypeFlag: "composable", + `warnings_point_at_correct_line_in_zed`: { files: []string{ - filepath.Join("validate-test", "only-passes-standard.zed"), + filepath.Join("validate-test", "warnings-point-at-right-line.zed"), }, - expectErr: "Expected identifier, found token TokenTypeKeyword", + expectStr: "warning: Permission \"delete_resource\" references parent type \"resource\" in its name; it is recommended to drop the suffix (relation-name-references-parent)\n 23 | permission can_admin = admin\n 24 | \n 25 | /** delete_resource allows a user to delete the resource. */\n 26 > permission delete_resource = can_admin\n > ^~~~~~~~~~~~~~~\n 27 | }\n 28 | \n\ncomplete - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", }, - `composable_only_with_standard_flag_fails`: { - schemaTypeFlag: "standard", + `warnings_point_at_correct_line_in_yaml`: { files: []string{ - filepath.Join("validate-test", "only-passes-composable.zed"), + filepath.Join("validate-test", "warnings-point-at-right-line.yaml"), }, - expectErr: "Unexpected token at root level", + expectStr: "warning: Permission \"delete_resource\" references parent type \"resource\" in its name; it is recommended to drop the suffix (relation-name-references-parent)\n 23 | /** can_admin allows a user to administer the resource */\n 24 | permission can_admin = admin\n 25 | \n 26 > /** delete_resource allows a user to delete the resource. */\n > ^~~~~~~~~~~~~~~\n 27 | permission delete_resource = can_admin\n 28 | }\n\ncomplete - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", }, - `composable_in_validation_yaml_with_standard_fails`: { - schemaTypeFlag: "standard", + `missing_import_file_fails`: { files: []string{ - filepath.Join("validate-test", "external-and-composable.yaml"), + filepath.Join("validate-test", "nonexistant-import.zed"), }, - expectErr: "Unexpected token at root level", + expectNonZeroStatusCode: true, + expectStr: "error: parse error in `nonexistant-import.zed`, line 7, column 1: failed to read import \"doesnotexist.zed\": open doesnotexist.zed: no such file or\ndirectory \n 4 | \n 5 | definition resource {}\n 6 | \n 7 > import \"doesnotexist.zed\"\n 8 | \n\n\n", }, - `composable_in_validation_yaml_with_composable_passes`: { - schemaTypeFlag: "composable", + `error_in_imported_file_fails_with_correct_file_and_line_pointer`: { files: []string{ - filepath.Join("validate-test", "external-and-composable.yaml"), + filepath.Join("validate-test", "nested-composable-schema.zed"), }, - expectStr: "Success! - 1 relationships loaded, 2 assertions run, 0 expected relations validated\n", + expectNonZeroStatusCode: true, + expectStr: "error: parse error in `nonexistant-import.zed`, line 7, column 1: failed to read import \"doesnotexist.zed\": open doesnotexist.zed: no such file or\ndirectory \n 4 | \n 5 | definition resource {}\n 6 | \n 7 > import \"doesnotexist.zed\"\n 8 | \n\n\n", }, - `warnings_in_composable_fail`: { - schemaTypeFlag: "composable", + `error_in_imported_file_fails_with_correct_file_and_line_pointer_2`: { files: []string{ - filepath.Join("validate-test", "composable-schema-warning-root.zed"), + filepath.Join("validate-test", "composable-schema-imports-file-with-error.zed"), }, - expectStr: `warning: Permission "edit" references itself, which will cause an error to be raised due to infinite recursion (permission-references-itself) - 1 | partial edit_partial { - 2 > permission edit = edit - > ^~~~ - 3 | } - 4 | - -complete - 0 relationships loaded, 0 assertions run, 0 expected relations validated -`, + expectNonZeroStatusCode: true, + expectStr: "error: parse error in `unknownrel`, line 5, column 23: relation/permission `unknownrel` not found under definition `group` \n 2 | definition user {}\n 3 | \n 4 | definition group {\n 5 > permission view = unknownrel\n > ^~~~~~~~~~\n 6 | }\n 7 | \n\n\n", }, - `warnings_point_at_correct_line_in_zed`: { + `yaml_with_composable_schemaFile_with_import_error`: { files: []string{ - filepath.Join("validate-test", "warnings-point-at-right-line.zed"), + filepath.Join("validate-test", "external-composable-with-error.yaml"), }, - expectStr: "warning: Permission \"delete_resource\" references parent type \"resource\" in its name; it is recommended to drop the suffix (relation-name-references-parent)\n 23 | permission can_admin = admin\n 24 | \n 25 | /** delete_resource allows a user to delete the resource. */\n 26 > permission delete_resource = can_admin\n > ^~~~~~~~~~~~~~~\n 27 | }\n 28 | \n\ncomplete - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", + expectNonZeroStatusCode: true, + expectStr: "error: parse error in `unknownrel`, line 5, column 23: relation/permission `unknownrel` not found under definition `group` \n 3 | definition group {\n 4 | relation member: user\n 5 | permission view = unknownrel\n 6 > }\n 7 | \n\n\n", }, - `warnings_point_at_correct_line_in_yaml`: { + `yaml_with_schemaFile_escape_attempt_fails`: { files: []string{ - filepath.Join("validate-test", "warnings-point-at-right-line.yaml"), + filepath.Join("validate-test", "external-schema-escape.yaml"), }, - expectStr: "warning: Permission \"delete_resource\" references parent type \"resource\" in its name; it is recommended to drop the suffix (relation-name-references-parent)\n 23 | /** can_admin allows a user to administer the resource */\n 24 | permission can_admin = admin\n 25 | \n 26 > /** delete_resource allows a user to delete the resource. */\n > ^~~~~~~~~~~~~~~\n 27 | permission delete_resource = can_admin\n 28 | }\n\ncomplete - 0 relationships loaded, 0 assertions run, 0 expected relations validated\n", + expectNonZeroStatusCode: true, + expectErr: `schema filepath "../some-schema.zed" must be local to where the command was invoked`, }, } @@ -323,7 +291,6 @@ complete - 0 relationships loaded, 0 assertions run, 0 expected relations valida require := require.New(t) cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, - zedtesting.StringFlag{FlagName: "schema-type", FlagValue: tc.schemaTypeFlag}, zedtesting.IntFlag{FlagName: "batch-size", FlagValue: 100}, zedtesting.IntFlag{FlagName: "workers", FlagValue: 1}, zedtesting.BoolFlag{FlagName: "fail-on-warn", FlagValue: false}, @@ -337,7 +304,7 @@ complete - 0 relationships loaded, 0 assertions run, 0 expected relations valida require.Error(err) require.Contains(err.Error(), tc.expectErr) } - require.Equal(tc.expectNonZeroStatusCode, shouldError) + require.Equal(tc.expectNonZeroStatusCode, shouldError, "non-zero status code value didn't match expectation") }) } } @@ -349,7 +316,6 @@ func TestFailOnWarn(t *testing.T) { // Run once with fail-on-warn set to false cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, - zedtesting.StringFlag{FlagName: "schema-type", FlagValue: ""}, zedtesting.BoolFlag{FlagName: "force-color", FlagValue: false}, zedtesting.IntFlag{FlagName: "batch-size", FlagValue: 100}, zedtesting.IntFlag{FlagName: "workers", FlagValue: 1}, zedtesting.BoolFlag{FlagName: "fail-on-warn", FlagValue: false}, @@ -360,7 +326,6 @@ func TestFailOnWarn(t *testing.T) { // Run again with fail-on-warn set to true cmd = zedtesting.CreateTestCobraCommandWithFlagValue(t, - zedtesting.StringFlag{FlagName: "schema-type", FlagValue: ""}, zedtesting.BoolFlag{FlagName: "force-color", FlagValue: false}, zedtesting.IntFlag{FlagName: "batch-size", FlagValue: 100}, zedtesting.IntFlag{FlagName: "workers", FlagValue: 1}, diff --git a/internal/decode/decoder.go b/internal/decode/decoder.go index 028eeaf5..7f48e8da 100644 --- a/internal/decode/decoder.go +++ b/internal/decode/decoder.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "net/url" "os" @@ -15,70 +16,76 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" - composable "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" - "github.com/authzed/spicedb/pkg/composableschemadsl/generator" - "github.com/authzed/spicedb/pkg/schemadsl/compiler" - "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/validationfile" "github.com/authzed/spicedb/pkg/validationfile/blocks" ) -var playgroundPattern = regexp.MustCompile("^.*/s/.*/schema|relationships|assertions|expected.*$") - // yamlKeyPatterns match YAML top-level keys that indicate a validation file format. // These patterns look for the key at the start of a line (column 0), followed by a colon. // This avoids false positives like "relation schema: parent" in a schema file being // mistaken for the "schema:" YAML key. var ( - yamlSchemaKeyPattern = regexp.MustCompile(`(?m)^schema\s*:`) - yamlSchemaFileKeyPattern = regexp.MustCompile(`(?m)^schemaFile\s*:`) - yamlRelationshipsKeyPattern = regexp.MustCompile(`(?m)^relationships\s*:`) + yamlSchemaKeyPattern = regexp.MustCompile(`(?m)^\s*schema\s*:`) + yamlSchemaFileKeyPattern = regexp.MustCompile(`(?m)^\s*schemaFile\s*:`) + yamlRelationshipsKeyPattern = regexp.MustCompile(`(?m)^\s*relationships\s*:`) + + playgroundPattern = regexp.MustCompile("^.*/s/.*/schema|relationships|assertions|expected.*$") ) -// SchemaRelationships holds the schema (as a string) and a list of -// relationships (as a string) in the format from the devtools download API. -type SchemaRelationships struct { - Schema string `yaml:"schema"` - Relationships string `yaml:"relationships"` -} +const ( + FileTypeUnknown = iota + FileTypeYaml + FileTypeZed +) -// Func will decode into the supplied object. -type Func func(out any) ([]byte, bool, error) +// Decoder holds fetched file contents along with a filesystem for resolving +// relative paths (e.g. schemaFile references). For remote URLs the filesystem +// is nil because relative file references are not supported. +type Decoder struct { + Contents []byte + // FS is rooted at the directory of the fetched file. It is non-nil only + // for local (file://) URLs. + FS fs.FS +} -// DecoderForURL returns the appropriate decoder for a given URL. -// Some URLs have special handling to dereference to the actual file. -func DecoderForURL(u *url.URL) (d Func, err error) { +// DecoderFromURL interprets the URL, fetches the content, and returns a +// Decoder. For local files the Decoder's FS is rooted at the file's directory +// so that relative schemaFile paths can be resolved. +func DecoderFromURL(u *url.URL) (*Decoder, error) { switch s := u.Scheme; s { case "", "file": - d = fileDecoder(u) + return decoderFromFile(u) case "http", "https": - d = httpDecoder(u) + return decoderFromHTTP(u) default: - err = fmt.Errorf("%s scheme not supported", s) + return nil, fmt.Errorf("%s scheme not supported", s) } - return d, err } -func fileDecoder(u *url.URL) Func { - return func(out any) ([]byte, bool, error) { - fs := os.DirFS(".") - file, err := fs.Open(u.Path) - if err != nil { - return nil, false, err - } - data, err := io.ReadAll(file) - if err != nil { - return nil, false, err - } - isOnlySchema, err := unmarshalAsYAMLOrSchemaWithFile(data, out, u.Path) - return data, isOnlySchema, err +func decoderFromFile(u *url.URL) (*Decoder, error) { + filePath := u.Path + data, err := os.ReadFile(filePath) //nolint:gosec // not an issue + if err != nil { + return nil, err } + dir := filepath.Dir(filePath) + return &Decoder{ + Contents: data, + FS: os.DirFS(dir), + }, nil } -func httpDecoder(u *url.URL) Func { +func decoderFromHTTP(u *url.URL) (*Decoder, error) { rewriteURL(u) - return directHTTPDecoder(u) + data, err := fetchHTTPDirectly(u) + if err != nil { + return nil, err + } + return &Decoder{ + Contents: data, + FS: nil, + }, nil } func rewriteURL(u *url.URL) { @@ -101,88 +108,92 @@ func rewriteURL(u *url.URL) { } } -func directHTTPDecoder(u *url.URL) Func { - return func(out any) ([]byte, bool, error) { - log.Debug().Stringer("url", u).Send() - r, err := http.Get(u.String()) - if err != nil { - return nil, false, err - } - defer r.Body.Close() - data, err := io.ReadAll(r.Body) - if err != nil { - return nil, false, err - } +func fetchHTTPDirectly(u *url.URL) ([]byte, error) { + log.Debug().Stringer("url", u).Send() + r, err := http.Get(u.String()) + if err != nil { + return nil, err + } + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + + return data, err +} - isOnlySchema, err := unmarshalAsYAMLOrSchema("", data, out) - return data, isOnlySchema, err +var ErrInvalidYamlTryZed = errors.New("invalid yaml") + +// UnmarshalAsYAMLOrSchema tries to unmarshal as YAML first, falling back to +// treating the contents as a raw schema. +func (d *Decoder) UnmarshalAsYAMLOrSchema() (*validationfile.ValidationFile, error) { + vFile, err := d.UnmarshalYAMLValidationFile() + if err == nil { + return vFile, nil + } + if !errors.Is(err, ErrInvalidYamlTryZed) { + return nil, err } + + return d.UnmarshalSchemaValidationFile(), nil } -// Uses the files passed in the args and looks for the specified schemaFile to parse the YAML. -func unmarshalAsYAMLOrSchemaWithFile(data []byte, out any, filename string) (bool, error) { - inputString := string(data) - if hasYAMLSchemaFileKey(inputString) && !hasYAMLSchemaKey(inputString) { - if err := yaml.Unmarshal(data, out); err != nil { - return false, err - } - validationFile, ok := out.(*validationfile.ValidationFile) - if !ok { - return false, errors.New("could not cast unmarshalled file to validationfile") - } +// UnmarshalYAMLValidationFile unmarshals YAML validation file contents. If the +// YAML contains a schemaFile reference, the Decoder's FS is used to resolve it. +func (d *Decoder) UnmarshalYAMLValidationFile() (*validationfile.ValidationFile, error) { + inputString := string(d.Contents) + + // Only attempt YAML unmarshaling if the input looks like a YAML validation file. + if !hasYAMLSchemaKey(inputString) && !hasYAMLSchemaFileKey(inputString) && !yamlRelationshipsKeyPattern.MatchString(inputString) { + return nil, fmt.Errorf("%w: input does not appear to be a YAML validation file", ErrInvalidYamlTryZed) + } + + var validationFile validationfile.ValidationFile + err := yaml.Unmarshal(d.Contents, &validationFile) + if err != nil { + return nil, err + } - // Need to join the original filepath with the requested filepath - // to construct the path to the referenced schema file. - // NOTE: This does not allow for yaml files to transitively reference - // each other's schemaFile fields. - // TODO: enable this behavior - schemaPath := filepath.Join(filepath.Dir(filename), validationFile.SchemaFile) + // If schemaFile is specified, resolve it using the Decoder's filesystem. + if validationFile.SchemaFile != "" { + // Clean the path for use with fs.FS (which doesn't accept ./ prefix). + schemaPath := filepath.Clean(validationFile.SchemaFile) if !filepath.IsLocal(schemaPath) { - // We want to prevent access of files that are outside of the folder - // where the command was originally invoked. This should do that. - return false, fmt.Errorf("schema filepath %s must be local to where the command was invoked", schemaPath) + return nil, fmt.Errorf("schema filepath %q must be local to where the command was invoked", schemaPath) } - fs := os.DirFS(".") - file, err := fs.Open(schemaPath) + if d.FS == nil { + return nil, fmt.Errorf("cannot resolve schemaFile %q: no local filesystem context (remote URL?)", schemaPath) + } + + file, err := d.FS.Open(schemaPath) if err != nil { - return false, err + return nil, err } - data, err = io.ReadAll(file) + schemaBytes, err := io.ReadAll(file) if err != nil { - return false, err + return nil, err } - } - return unmarshalAsYAMLOrSchema(filename, data, out) -} - -func unmarshalAsYAMLOrSchema(filename string, data []byte, out any) (bool, error) { - inputString := string(data) - - // Check for indications of a schema-only file by looking for YAML top-level keys. - // We use regex patterns to match keys at the start of a line to avoid false positives - // like "relation schema: parent" in a schema file being mistaken for the "schema:" YAML key. - if !hasYAMLSchemaKey(inputString) && !hasYAMLRelationshipsKey(inputString) { - if err := compileSchemaFromData(filename, inputString, out); err != nil { - return false, err + validationFile.SchemaFile = "" + validationFile.Schema = blocks.SchemaWithPosition{ + SourcePosition: spiceerrors.SourcePosition{LineNumber: 1, ColumnPosition: 1}, + Schema: string(schemaBytes), } - return true, nil } - if !hasYAMLSchemaKey(inputString) && !hasYAMLSchemaFileKey(inputString) { - // If there is no schema and no schemaFile and it doesn't compile then it must be yaml with missing fields - if err := compileSchemaFromData(filename, inputString, out); err != nil { - return false, errors.New("either schema or schemaFile must be present") - } - return true, nil - } - // Try to unparse as YAML for the validation file format. - if err := yaml.Unmarshal(data, out); err != nil { - return false, err - } + return &validationFile, nil +} - return false, nil +// UnmarshalSchemaValidationFile wraps raw schema bytes into a ValidationFile. +func (d *Decoder) UnmarshalSchemaValidationFile() *validationfile.ValidationFile { + return &validationfile.ValidationFile{ + Schema: blocks.SchemaWithPosition{ + SourcePosition: spiceerrors.SourcePosition{LineNumber: 1, ColumnPosition: 1}, + Schema: string(d.Contents), + }, + } } // hasYAMLSchemaKey returns true if the input contains a "schema:" YAML key at the start of a line. @@ -194,54 +205,3 @@ func hasYAMLSchemaKey(input string) bool { func hasYAMLSchemaFileKey(input string) bool { return yamlSchemaFileKeyPattern.MatchString(input) } - -// hasYAMLRelationshipsKey returns true if the input contains a "relationships:" YAML key at the start of a line. -func hasYAMLRelationshipsKey(input string) bool { - return yamlRelationshipsKeyPattern.MatchString(input) -} - -// compileSchemaFromData attempts to compile using the old DSL and the new composable DSL, -// but prefers the new DSL. -// It returns the errors returned by both compilations. -func compileSchemaFromData(filename, schemaString string, out any) error { - var ( - standardCompileErr error - composableCompiled *composable.CompiledSchema - composableCompileErr error - vfile validationfile.ValidationFile - ) - - vfile = *out.(*validationfile.ValidationFile) - vfile.Schema = blocks.SchemaWithPosition{ - SourcePosition: spiceerrors.SourcePosition{LineNumber: 1, ColumnPosition: 1}, - } - - _, standardCompileErr = compiler.Compile(compiler.InputSchema{ - Source: input.Source("schema"), - SchemaString: schemaString, - }, compiler.AllowUnprefixedObjectType()) - - if standardCompileErr == nil { - vfile.Schema.Schema = schemaString - } - - inputSourceFolder := filepath.Dir(filename) - composableCompiled, composableCompileErr = composable.Compile(composable.InputSchema{ - SchemaString: schemaString, - }, composable.AllowUnprefixedObjectType(), composable.SourceFolder(inputSourceFolder)) - - // We'll only attempt to generate the composable schema string if we don't already - // have one from standard schema compilation - if composableCompileErr == nil && vfile.Schema.Schema == "" { - compiledSchemaString, _, err := generator.GenerateSchema(composableCompiled.OrderedDefinitions) - if err != nil { - return fmt.Errorf("could not generate string schema: %w", err) - } - vfile.Schema.Schema = compiledSchemaString - } - - err := errors.Join(standardCompileErr, composableCompileErr) - - *out.(*validationfile.ValidationFile) = vfile - return err -} diff --git a/internal/decode/decoder_test.go b/internal/decode/decoder_test.go index c789edc2..08f254a5 100644 --- a/internal/decode/decoder_test.go +++ b/internal/decode/decoder_test.go @@ -1,14 +1,152 @@ package decode import ( + "net/http" + "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" - - "github.com/authzed/spicedb/pkg/validationfile" + "go.uber.org/goleak" ) +func TestDecoderFromURL(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + yamlContent := `--- +schema: |- + definition user {} +relationships: |- + resource:1#reader@user:1 +` + invalidYamlContent := `--- +schemaFile: "./external-schema.zed" +relationships: |- + resource:1#reader@user:1 +` + schemaContent := "definition user {}\n" + + // Spin up a test HTTP server that serves the file contents based on path. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/valid.yaml": + _, _ = w.Write([]byte(yamlContent)) + case "/invalid.yaml": + _, _ = w.Write([]byte(invalidYamlContent)) + case "/valid.zed": + _, _ = w.Write([]byte(schemaContent)) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(srv.Close) + + t.Run("valid yaml file over http", func(t *testing.T) { + u, err := url.Parse(srv.URL + "/valid.yaml") + require.NoError(t, err) + + d, err := DecoderFromURL(u) + require.NoError(t, err) + require.Nil(t, d.FS) + require.YAMLEq(t, yamlContent, string(d.Contents)) + + vFile, err := d.UnmarshalAsYAMLOrSchema() + require.NoError(t, err) + require.Equal(t, "definition user {}", vFile.Schema.Schema) + require.Equal(t, "resource:1#reader@user:1", vFile.Relationships.RelationshipsString) + }) + + t.Run("valid zed schema file over http", func(t *testing.T) { + u, err := url.Parse(srv.URL + "/valid.zed") + require.NoError(t, err) + + d, err := DecoderFromURL(u) + require.NoError(t, err) + require.Nil(t, d.FS) + require.Equal(t, []byte(schemaContent), d.Contents) + + vFile, err := d.UnmarshalAsYAMLOrSchema() + require.NoError(t, err) + require.Equal(t, schemaContent, vFile.Schema.Schema) + }) + + t.Run("invalid yaml file over http", func(t *testing.T) { + u, err := url.Parse(srv.URL + "/invalid.yaml") + require.NoError(t, err) + + d, err := DecoderFromURL(u) + require.NoError(t, err) + require.Nil(t, d.FS) + require.YAMLEq(t, invalidYamlContent, string(d.Contents)) + + vFile, err := d.UnmarshalAsYAMLOrSchema() + // arbitrary decision: we could fetch the remote URL, but I don't want to. + require.ErrorContains(t, err, "cannot resolve schemaFile \"external-schema.zed\": no local filesystem context (remote URL?)") + require.Nil(t, vFile) + }) +} + +func TestUnmarshalYAMLValidationFile(t *testing.T) { + schemaContent := "definition user {}\ndefinition resource {\nrelation reader: user\n}\n" + + // Write real files to a temp directory so DecoderFromURL -> decoderFromFile is exercised. + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "schema.zed"), []byte(schemaContent), 0o600)) + + tests := []struct { + name string + yamlContent string + expectedSchema string + expectedRels string + expectedErrText string + }{ + { + name: "resolves_local_schemaFile", + yamlContent: `--- +schemaFile: "./schema.zed" +relationships: |- + resource:1#reader@user:1 +`, + expectedSchema: schemaContent, + expectedRels: "resource:1#reader@user:1", + }, + { + name: "rejects_schemaFile_pointing_to_above_directory", + yamlContent: `--- +schemaFile: "../secret/schema.zed" +relationships: |- + resource:1#reader@user:1 +`, + expectedErrText: "must be local to where the command was invoked", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := filepath.Join(dir, tt.name+".yaml") + require.NoError(t, os.WriteFile(f, []byte(tt.yamlContent), 0o600)) + u, err := url.Parse(f) + require.NoError(t, err) + + d, err := DecoderFromURL(u) + require.NoError(t, err) + + vFile, err := d.UnmarshalYAMLValidationFile() + if tt.expectedErrText != "" { + require.ErrorContains(t, err, tt.expectedErrText) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedSchema, vFile.Schema.Schema) + require.Empty(t, vFile.SchemaFile) + require.Equal(t, tt.expectedRels, vFile.Relationships.RelationshipsString) + }) + } +} + func TestRewriteURL(t *testing.T) { tests := []struct { name string @@ -131,11 +269,10 @@ func TestRewriteURL(t *testing.T) { func TestUnmarshalAsYAMLOrSchema(t *testing.T) { tests := []struct { - name string - in []byte - isOnlySchema bool - outSchema string - wantErr bool + name string + in []byte + outSchema string + wantErr string }{ { name: "valid yaml", @@ -143,23 +280,23 @@ func TestUnmarshalAsYAMLOrSchema(t *testing.T) { schema: definition user {} `), - outSchema: `definition user {}`, - isOnlySchema: false, - wantErr: false, + outSchema: `definition user {}`, }, { - name: "valid schema", - in: []byte(`definition user {}`), - isOnlySchema: true, - outSchema: `definition user {}`, - wantErr: false, + name: "valid schema", + in: []byte(`definition user {}`), + outSchema: `definition user {}`, }, { - name: "invalid yaml", - in: []byte(`invalid yaml`), - isOnlySchema: false, - outSchema: "", - wantErr: true, + name: "invalid yaml", + in: []byte(` + schema: "" + relationships: + some: key + bad: indentation + `), + outSchema: "", + wantErr: "yaml: line 2: found character that cannot start any token", }, { name: "schema with relation named schema", @@ -176,7 +313,6 @@ definition child { } definition user {}`), - isOnlySchema: true, outSchema: `definition parent { relation owner: user @@ -190,7 +326,6 @@ definition child { } definition user {}`, - wantErr: false, }, { name: "schema with permission named schema", @@ -207,7 +342,6 @@ definition child { } definition user {}`), - isOnlySchema: true, outSchema: `definition parent { relation owner: user @@ -221,7 +355,6 @@ definition child { } definition user {}`, - wantErr: false, }, { name: "schema with relation named something_schema", @@ -236,7 +369,6 @@ definition child { } definition user {}`), - isOnlySchema: true, outSchema: `definition parent { relation owner: user permission manage = owner @@ -248,7 +380,6 @@ definition child { } definition user {}`, - wantErr: false, }, { name: "schema with relation named relationships", @@ -263,7 +394,6 @@ definition child { } definition user {}`), - isOnlySchema: true, outSchema: `definition parent { relation owner: user permission manage = owner @@ -275,7 +405,6 @@ definition child { } definition user {}`, - wantErr: false, }, { name: "valid yaml with relation named schema inside", @@ -292,7 +421,6 @@ definition user {}`, definition user {} `), - isOnlySchema: false, outSchema: `definition parent { relation owner: user permission manage = owner @@ -304,141 +432,18 @@ definition child { } definition user {}`, - wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - block := validationfile.ValidationFile{} - isOnlySchema, err := unmarshalAsYAMLOrSchema("", tt.in, &block) - require.Equal(t, tt.wantErr, err != nil) - require.Equal(t, tt.isOnlySchema, isOnlySchema) - if !tt.wantErr { - require.Equal(t, tt.outSchema, block.Schema.Schema) + d := &Decoder{Contents: tt.in} + vFile, err := d.UnmarshalAsYAMLOrSchema() + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return } - }) - } -} - -func TestHasYAMLSchemaKey(t *testing.T) { - tests := []struct { - name string - input string - expected bool - }{ - { - name: "yaml schema key at start of line", - input: "schema:\n definition user {}", - expected: true, - }, - { - name: "yaml schema key with space before colon", - input: "schema :\n definition user {}", - expected: true, - }, - { - name: "relation named schema in definition", - input: "definition child {\n\trelation schema: parent\n}", - expected: false, - }, - { - name: "relation named something_schema in definition", - input: "definition child {\n\trelation something_schema: parent\n}", - expected: false, - }, - { - name: "schema arrow expression", - input: "definition child {\n\tpermission access = schema->manage\n}", - expected: false, - }, - { - name: "no schema at all", - input: "definition user {}", - expected: false, - }, - { - name: "schema in single line comment should not trigger", - input: "// schema: this is a comment\ndefinition user {}", - expected: false, - }, - { - name: "schema in block comment should not trigger", - input: "/* schema: block comment */\ndefinition user {}", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := hasYAMLSchemaKey(tt.input) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHasYAMLRelationshipsKey(t *testing.T) { - tests := []struct { - name string - input string - expected bool - }{ - { - name: "yaml relationships key at start of line", - input: "schema:\n definition user {}\nrelationships:\n user:1#member@user:2", - expected: true, - }, - { - name: "no relationships key", - input: "schema:\n definition user {}", - expected: false, - }, - { - name: "relationships in middle of line", - input: " relationships: user:1#member@user:2", - expected: false, - }, - { - name: "relation named relationships in definition", - input: "definition child {\n\trelation relationships: parent\n}", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := hasYAMLRelationshipsKey(tt.input) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHasYAMLSchemaFileKey(t *testing.T) { - tests := []struct { - name string - input string - expected bool - }{ - { - name: "yaml schemaFile key at start of line", - input: "schemaFile: ./schema.zed\nrelationships:\n user:1#member@user:2", - expected: true, - }, - { - name: "no schemaFile key", - input: "schema:\n definition user {}", - expected: false, - }, - { - name: "relation named schemaFile in definition", - input: "definition child {\n\trelation schemaFile: parent\n}", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := hasYAMLSchemaFileKey(tt.input) - require.Equal(t, tt.expected, result) + require.NoError(t, err) + require.Equal(t, tt.outSchema, vFile.Schema.Schema) }) } } diff --git a/pkg/wasm/main.go b/pkg/wasm/main.go index 18e274f4..185d869f 100644 --- a/pkg/wasm/main.go +++ b/pkg/wasm/main.go @@ -171,7 +171,7 @@ func runZedCommand(rootCmd *cobra.Command, requestContextJSON string, stringPara reader := devCtx.DataLayer.SnapshotReader(headRev) relationships := []*core.RelationTuple{} - schemaReader, err := reader.ReadSchema() + schemaReader, err := reader.ReadSchema(ctx) if err != nil { return zedCommandResult{Error: err.Error()} }