diff --git a/internal/cmd/preview.go b/internal/cmd/preview.go index 279e310d..7f0230b4 100644 --- a/internal/cmd/preview.go +++ b/internal/cmd/preview.go @@ -1,23 +1,7 @@ package cmd import ( - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/ccoveille/go-safecast" - "github.com/jzelinskie/cobrautil/v2" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "golang.org/x/term" - - newcompiler "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" - newinput "github.com/authzed/spicedb/pkg/composableschemadsl/input" - "github.com/authzed/spicedb/pkg/schemadsl/compiler" - "github.com/authzed/spicedb/pkg/schemadsl/generator" - - "github.com/authzed/zed/internal/commands" ) func registerPreviewCmd(rootCmd *cobra.Command) { @@ -26,7 +10,6 @@ func registerPreviewCmd(rootCmd *cobra.Command) { previewCmd.AddCommand(schemaCmd) schemaCmd.AddCommand(schemaCompileCmd) - schemaCompileCmd.Flags().String("out", "", "output filepath; omitting writes to stdout") } var previewCmd = &cobra.Command{ @@ -35,86 +18,7 @@ var previewCmd = &cobra.Command{ } var schemaCmd = &cobra.Command{ - Use: "schema ", - Short: "Manage schema for a permissions system", -} - -var schemaCompileCmd = &cobra.Command{ - Use: "compile ", - Args: commands.ValidationWrapper(cobra.ExactArgs(1)), - Short: "Compile a schema that uses extended syntax into one that can be written to SpiceDB", - Example: ` - Write to stdout: - zed preview schema compile root.zed - Write to an output file: - zed preview schema compile root.zed --out compiled.zed - `, - ValidArgsFunction: commands.FileExtensionCompletions("zed"), - RunE: schemaCompileCmdFunc, -} - -// Compiles an input schema written in the new composable schema syntax -// and produces it as a fully-realized schema -func schemaCompileCmdFunc(cmd *cobra.Command, args []string) error { - stdOutFd, err := safecast.ToInt(uint(os.Stdout.Fd())) - if err != nil { - return err - } - outputFilepath := cobrautil.MustGetString(cmd, "out") - if outputFilepath == "" && !term.IsTerminal(stdOutFd) { - return fmt.Errorf("must provide stdout or output file path") - } - - inputFilepath := args[0] - inputSourceFolder := filepath.Dir(inputFilepath) - var schemaBytes []byte - schemaBytes, err = os.ReadFile(inputFilepath) - if err != nil { - return fmt.Errorf("failed to read schema file: %w", err) - } - log.Trace().Str("schema", string(schemaBytes)).Str("file", args[0]).Msg("read schema from file") - - if len(schemaBytes) == 0 { - return errors.New("attempted to compile empty schema") - } - - compiled, err := newcompiler.Compile(newcompiler.InputSchema{ - Source: newinput.Source(inputFilepath), - SchemaString: string(schemaBytes), - }, newcompiler.AllowUnprefixedObjectType(), - newcompiler.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) - if err != nil { - return fmt.Errorf("could not generate resulting schema: %w", err) - } - - // Add a newline at the end for hygiene's sake - terminated := generated + "\n" - - if outputFilepath == "" { - // Print to stdout - fmt.Print(terminated) - } else { - err = os.WriteFile(outputFilepath, []byte(terminated), 0o_600) - if err != nil { - return err - } - } - - return nil + Use: "schema ", + Short: "Manage schema for a permissions system", + Deprecated: "please use `zed schema compile`", } diff --git a/internal/cmd/preview_test.go b/internal/cmd/preview_test.go deleted file mode 100644 index 14ca30b7..00000000 --- a/internal/cmd/preview_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package cmd - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - zedtesting "github.com/authzed/zed/internal/testing" -) - -func TestPreview(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - files []string - out string - expectErr string - expectStr string - }{ - `file_not_found`: { - files: []string{ - filepath.Join("preview-test", "nonexistent.zed"), - }, - expectErr: `no such file or directory`, - }, - `happy_path`: { - files: []string{ - filepath.Join("preview-test", "composable-schema-root.zed"), - }, - expectStr: `definition user {} - -definition resource { - relation user: user - permission view = user -} -`, - }, - `cannot_be_compiled_because_using_reserved_keyword`: { - files: []string{ - filepath.Join("preview-test", "composable-schema-invalid-root.zed"), - }, - expectErr: "line 4, column 12: Expected identifier, found token TokenTypeKeyword", - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - require := require.New(t) - - tempOutFile := filepath.Join(t.TempDir(), "out.zed") - cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, - zedtesting.StringFlag{FlagName: "out", FlagValue: tempOutFile}) - - err := schemaCompileCmdFunc(cmd, tc.files) - if tc.expectErr == "" { - require.NoError(err) - tempOutString, err := os.ReadFile(tempOutFile) - require.NoError(err) - require.Equal(tc.expectStr, string(tempOutString)) - } else { - require.Error(err) - require.Contains(err.Error(), tc.expectErr) - } - }) - } -} diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go index bbe52f39..4a14ec0d 100644 --- a/internal/cmd/schema.go +++ b/internal/cmd/schema.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "github.com/ccoveille/go-safecast" @@ -17,6 +18,8 @@ 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" @@ -37,6 +40,9 @@ func registerAdditionalSchemaCmds(schemaCmd *cobra.Command) { schemaWriteCmd.Flags().String("schema-definition-prefix", "", "prefix to add to the schema's definition(s) before writing") schemaCmd.AddCommand(schemaDiffCmd) + + schemaCmd.AddCommand(schemaCompileCmd) + schemaCompileCmd.Flags().String("out", "", "output filepath; omitting writes to stdout") } var schemaWriteCmd = &cobra.Command{ @@ -62,6 +68,20 @@ var schemaDiffCmd = &cobra.Command{ RunE: schemaDiffCmdFunc, } +var schemaCompileCmd = &cobra.Command{ + Use: "compile ", + Args: commands.ValidationWrapper(cobra.ExactArgs(1)), + Short: "Compile a schema that uses extended syntax into one that can be written to SpiceDB", + Example: ` + Write to stdout: + zed preview schema compile root.zed + Write to an output file: + zed preview schema compile root.zed --out compiled.zed + `, + ValidArgsFunction: commands.FileExtensionCompletions("zed"), + RunE: schemaCompileCmdFunc, +} + func schemaDiffCmdFunc(_ *cobra.Command, args []string) error { beforeBytes, err := os.ReadFile(args[0]) if err != nil { @@ -317,3 +337,69 @@ func determinePrefixForSchema(ctx context.Context, specifiedPrefix string, clien return "", nil } + +// Compiles an input schema written in the new composable schema syntax +// and produces it as a fully-realized schema +func schemaCompileCmdFunc(cmd *cobra.Command, args []string) error { + stdOutFd, err := safecast.ToInt(uint(os.Stdout.Fd())) + if err != nil { + return err + } + outputFilepath := cobrautil.MustGetString(cmd, "out") + if outputFilepath == "" && !term.IsTerminal(stdOutFd) { + return fmt.Errorf("must provide stdout or output file path") + } + + inputFilepath := args[0] + inputSourceFolder := filepath.Dir(inputFilepath) + var schemaBytes []byte + schemaBytes, err = os.ReadFile(inputFilepath) + if err != nil { + return fmt.Errorf("failed to read schema file: %w", err) + } + log.Trace().Str("schema", string(schemaBytes)).Str("file", args[0]).Msg("read schema from file") + + if len(schemaBytes) == 0 { + return errors.New("attempted to compile empty schema") + } + + compiled, err := newcompiler.Compile(newcompiler.InputSchema{ + Source: newinput.Source(inputFilepath), + SchemaString: string(schemaBytes), + }, newcompiler.AllowUnprefixedObjectType(), + newcompiler.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) + if err != nil { + return fmt.Errorf("could not generate resulting schema: %w", err) + } + + // Add a newline at the end for hygiene's sake + terminated := generated + "\n" + + if outputFilepath == "" { + // Print to stdout + fmt.Print(terminated) + } else { + err = os.WriteFile(outputFilepath, []byte(terminated), 0o_600) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/cmd/schema_test.go b/internal/cmd/schema_test.go index e118f65b..b3ec0cfe 100644 --- a/internal/cmd/schema_test.go +++ b/internal/cmd/schema_test.go @@ -1,9 +1,13 @@ package cmd import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" + + zedtesting "github.com/authzed/zed/internal/testing" ) func TestDeterminePrefixForSchema(t *testing.T) { @@ -114,3 +118,61 @@ caveat test/some_caveat(someCondition int) { }) } } + +func TestSchemaCompile(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + files []string + out string + expectErr string + expectStr string + }{ + `file_not_found`: { + files: []string{ + filepath.Join("preview-test", "nonexistent.zed"), + }, + expectErr: `no such file or directory`, + }, + `happy_path`: { + files: []string{ + filepath.Join("preview-test", "composable-schema-root.zed"), + }, + expectStr: `definition user {} + +definition resource { + relation user: user + permission view = user +} +`, + }, + `cannot_be_compiled_because_using_reserved_keyword`: { + files: []string{ + filepath.Join("preview-test", "composable-schema-invalid-root.zed"), + }, + expectErr: "line 4, column 12: Expected identifier, found token TokenTypeKeyword", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + require := require.New(t) + + tempOutFile := filepath.Join(t.TempDir(), "out.zed") + cmd := zedtesting.CreateTestCobraCommandWithFlagValue(t, + zedtesting.StringFlag{FlagName: "out", FlagValue: tempOutFile}) + + err := schemaCompileCmdFunc(cmd, tc.files) + if tc.expectErr == "" { + require.NoError(err) + tempOutString, err := os.ReadFile(tempOutFile) + require.NoError(err) + require.Equal(tc.expectStr, string(tempOutString)) + } else { + require.Error(err) + require.Contains(err.Error(), tc.expectErr) + } + }) + } +}