diff --git a/cmd/compare.go b/cmd/compare.go new file mode 100644 index 0000000..8122924 --- /dev/null +++ b/cmd/compare.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "fmt" + + "github.com/open-feature/cli/internal/manifest" + "github.com/spf13/cobra" +) + +func GetCompareCmd() *cobra.Command { + return &cobra.Command{ + Use: "compare", + Short: "Compare two manifest files", + Long: `Compare two manifest files and list the changes`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("please provide two manifest files to compare") + } + + oldManifest, err := manifest.Load(args[0]) + if err != nil { + return fmt.Errorf("failed to load old manifest: %w", err) + } + + newManifest, err := manifest.Load(args[1]) + if err != nil { + return fmt.Errorf("failed to load new manifest: %w", err) + } + + changes, err := manifest.Compare(oldManifest, newManifest) + if err != nil { + return fmt.Errorf("failed to compare manifests: %w", err) + } + + for _, change := range changes { + fmt.Printf("%s: %s\n", change.Type, change.Path) + } + + return nil + }, + } +} diff --git a/cmd/root.go b/cmd/root.go index 993145b..b14cf79 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,6 +62,7 @@ func GetRootCmd() *cobra.Command { rootCmd.AddCommand(GetVersionCmd()) rootCmd.AddCommand(GetInitCmd()) rootCmd.AddCommand(GetGenerateCmd()) + rootCmd.AddCommand(GetCompareCmd()) // Add a custom error handler after the command is created rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { diff --git a/docs/commands/openfeature.md b/docs/commands/openfeature.md index f9c8efc..0e2993d 100644 --- a/docs/commands/openfeature.md +++ b/docs/commands/openfeature.md @@ -23,6 +23,7 @@ openfeature [flags] ### SEE ALSO +* [openfeature compare](openfeature_compare.md) - Compare two manifest files * [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors. * [openfeature init](openfeature_init.md) - Initialize a new project * [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI diff --git a/docs/commands/openfeature_compare.md b/docs/commands/openfeature_compare.md new file mode 100644 index 0000000..2f28723 --- /dev/null +++ b/docs/commands/openfeature_compare.md @@ -0,0 +1,32 @@ + + +## openfeature compare + +Compare two manifest files + +### Synopsis + +Compare two manifest files and list the changes + +``` +openfeature compare [flags] +``` + +### Options + +``` + -h, --help help for compare +``` + +### Options inherited from parent commands + +``` + --debug Enable debug logging + -m, --manifest string Path to the flag manifest (default "flags.json") + --no-input Disable interactive prompts +``` + +### SEE ALSO + +* [openfeature](openfeature.md) - CLI for OpenFeature. + diff --git a/internal/manifest/compare_test.go b/internal/manifest/compare_test.go new file mode 100644 index 0000000..878f2c5 --- /dev/null +++ b/internal/manifest/compare_test.go @@ -0,0 +1,65 @@ +package manifest + +import ( + "reflect" + "sort" + "testing" +) + +func TestCompareDifferentManifests(t *testing.T) { + oldManifest := &Manifest{ + Flags: map[string]any{ + "flag1": "value1", + "flag2": "value2", + }, + } + + newManifest := &Manifest{ + Flags: map[string]any{ + "flag1": "value1", + "flag2": "newValue2", + "flag3": "value3", + }, + } + + changes, err := Compare(oldManifest, newManifest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expectedChanges := []Change{ + {Type: "change", Path: "flags.flag2", OldValue: "value2", NewValue: "newValue2"}, + {Type: "add", Path: "flags.flag3", NewValue: "value3"}, + } + + sortChanges(changes) + sortChanges(expectedChanges) + + if !reflect.DeepEqual(changes, expectedChanges) { + t.Errorf("expected %v, got %v", expectedChanges, changes) + } +} + +func TestCompareIdenticalManifests(t *testing.T) { + manifest := &Manifest{ + Flags: map[string]any{ + "flag1": "value1", + "flag2": "value2", + }, + } + + changes, err := Compare(manifest, manifest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(changes) != 0 { + t.Errorf("expected no changes, got %v", changes) + } +} + +func sortChanges(changes []Change) { + sort.Slice(changes, func(i, j int) bool { + return changes[i].Path < changes[j].Path + }) +} diff --git a/internal/manifest/manage.go b/internal/manifest/manage.go index 29dc053..ecab40d 100644 --- a/internal/manifest/manage.go +++ b/internal/manifest/manage.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/open-feature/cli/internal/filesystem" + "github.com/spf13/afero" ) type initManifest struct { @@ -14,7 +15,7 @@ type initManifest struct { // Create creates a new manifest file at the given path. func Create(path string) error { m := &initManifest{ - Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json", + Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json", Manifest: Manifest{ Flags: map[string]any{}, }, @@ -25,3 +26,19 @@ func Create(path string) error { } return filesystem.WriteFile(path, formattedInitManifest) } + +// Load loads a manifest from a JSON file, unmarshals it, and returns a Manifest object. +func Load(path string) (*Manifest, error) { + fs := filesystem.FileSystem() + data, err := afero.ReadFile(fs, path) + if err != nil { + return nil, err + } + + var m Manifest + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + + return &m, nil +}