diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go new file mode 100644 index 0000000..6aec68b --- /dev/null +++ b/cmd/generate/generate_test.go @@ -0,0 +1,102 @@ +package generate + +import ( + "codegen/internal/flagkeys" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func TestGenerateGoSuccess(t *testing.T) { + // Constant paths. + const memoryManifestPath = "manifest/path.json" + const memoryOutputPath = "output/path.go" + const packageName = "testpackage" + const testFileManifest = "testdata/success_manifest.golden" + const testFileGo = "testdata/success_go.golden" + + // Prepare in-memory files. + fs := afero.NewMemMapFs() + viper.Set(flagkeys.FileSystem, fs) + readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs) + + // Prepare command. + Root.SetArgs([]string{"go", + "--flag_manifest_path", memoryManifestPath, + "--output_path", memoryOutputPath, + "--package_name", packageName, + }) + + // Run command. + Root.Execute() + + // Compare result. + compareOutput(t, testFileGo, memoryOutputPath, fs) +} + +func TestGenerateReactSuccess(t *testing.T) { + // Constant paths. + const memoryManifestPath = "manifest/path.json" + const memoryOutputPath = "output/path.ts" + const testFileManifest = "testdata/success_manifest.golden" + const testFileReact = "testdata/success_react.golden" + + // Prepare in-memory files. + fs := afero.NewMemMapFs() + viper.Set(flagkeys.FileSystem, fs) + readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs) + + // Prepare command. + Root.SetArgs([]string{"react", + "--flag_manifest_path", memoryManifestPath, + "--output_path", memoryOutputPath, + }) + + // Run command. + Root.Execute() + + // Compare result. + compareOutput(t, testFileReact, memoryOutputPath, fs) +} + +func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) { + data, err := os.ReadFile(inputPath) + if err != nil { + t.Fatalf("error reading file %q: %v", inputPath, err) + } + if err := memFs.MkdirAll(filepath.Dir(memPath), 0660); err != nil { + t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err) + } + f, err := memFs.Create(memPath) + if err != nil { + t.Fatalf("error creating file %q: %v", memPath, err) + } + defer f.Close() + writtenBytes, err := f.Write(data) + if err != nil { + t.Fatalf("error writing contents to file %q: %v", memPath, err) + } + if writtenBytes != len(data) { + t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath) + } +} + +func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) { + want, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("error reading file %q: %v", testFile, err) + + } + got, err := afero.ReadFile(fs, memoryOutputPath) + if err != nil { + t.Fatalf("error reading file %q: %v", memoryOutputPath, err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } +} diff --git a/cmd/generate/testdata/success_go.golden b/cmd/generate/testdata/success_go.golden new file mode 100644 index 0000000..ccf281e --- /dev/null +++ b/cmd/generate/testdata/success_go.golden @@ -0,0 +1,49 @@ +package testpackage + +import ( + "context" + "github.com/open-feature/go-sdk/openfeature" +) + +type BooleanProvider func(ctx context.Context) (bool, error) +type FloatProvider func(ctx context.Context) (float64, error) +type IntProvider func(ctx context.Context) (int64, error) +type StringProvider func(ctx context.Context) (string, error) + +var client *openfeature.Client = nil +// Discount percentage applied to purchases. +var DiscountPercentage = struct { + Value FloatProvider +}{ + Value: func(ctx context.Context) (float64, error) { + return client.FloatValue(ctx, "discountPercentage", 0.15, openfeature.EvaluationContext{}) + }, +} +// Controls whether Feature A is enabled. +var EnableFeatureA = struct { + Value BooleanProvider +}{ + Value: func(ctx context.Context) (bool, error) { + return client.BooleanValue(ctx, "enableFeatureA", false, openfeature.EvaluationContext{}) + }, +} +// The message to use for greeting users. +var GreetingMessage = struct { + Value StringProvider +}{ + Value: func(ctx context.Context) (string, error) { + return client.StringValue(ctx, "greetingMessage", "Hello there!", openfeature.EvaluationContext{}) + }, +} +// Maximum allowed length for usernames. +var UsernameMaxLength = struct { + Value IntProvider +}{ + Value: func(ctx context.Context) (int64, error) { + return client.IntValue(ctx, "usernameMaxLength", 50, openfeature.EvaluationContext{}) + }, +} + +func init() { + client = openfeature.NewClient("testpackage") +} diff --git a/cmd/generate/testdata/success_manifest.golden b/cmd/generate/testdata/success_manifest.golden new file mode 100644 index 0000000..2df98b6 --- /dev/null +++ b/cmd/generate/testdata/success_manifest.golden @@ -0,0 +1,32 @@ +{ + "flags": { + "enableFeatureA": { + "flagType": "boolean", + "defaultValue": false, + "description": "Controls whether Feature A is enabled." + }, + "usernameMaxLength": { + "flagType": "integer", + "defaultValue": 50, + "description": "Maximum allowed length for usernames." + }, + "greetingMessage": { + "flagType": "string", + "defaultValue": "Hello there!", + "description": "The message to use for greeting users." + }, + "discountPercentage": { + "flagType": "float", + "defaultValue": 0.15, + "description": "Discount percentage applied to purchases." + }, + "themeCustomization": { + "flagType": "object", + "defaultValue": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d" + }, + "description": "Allows customization of theme colors." + } + } + } \ No newline at end of file diff --git a/cmd/generate/testdata/success_react.golden b/cmd/generate/testdata/success_react.golden new file mode 100644 index 0000000..9a68ec5 --- /dev/null +++ b/cmd/generate/testdata/success_react.golden @@ -0,0 +1,55 @@ +'use client'; + +import { + useBooleanFlagDetails, + useNumberFlagDetails, + useStringFlagDetails, +} from "@openfeature/react-sdk"; + +/** +* Discount percentage applied to purchases. +* +* **Details:** +* - flag key: `discountPercentage` +* - default value: `0.15` +* - type: `number` +*/ +export const useDiscountPercentage = (options: Parameters[2]) => { + return useNumberFlagDetails("discountPercentage", 0.15, options); +}; + +/** +* Controls whether Feature A is enabled. +* +* **Details:** +* - flag key: `enableFeatureA` +* - default value: `false` +* - type: `boolean` +*/ +export const useEnableFeatureA = (options: Parameters[2]) => { + return useBooleanFlagDetails("enableFeatureA", false, options); +}; + +/** +* The message to use for greeting users. +* +* **Details:** +* - flag key: `greetingMessage` +* - default value: `Hello there!` +* - type: `string` +*/ +export const useGreetingMessage = (options: Parameters[2]) => { + return useStringFlagDetails("greetingMessage", "Hello there!", options); +}; + +/** +* Maximum allowed length for usernames. +* +* **Details:** +* - flag key: `usernameMaxLength` +* - default value: `50` +* - type: `number` +*/ +export const useUsernameMaxLength = (options: Parameters[2]) => { + return useNumberFlagDetails("usernameMaxLength", 50, options); +}; diff --git a/go.mod b/go.mod index de23b4e..97d9d17 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( require ( github.com/go-logr/logr v1.4.2 // indirect + github.com/google/go-cmp v0.6.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect diff --git a/go.sum b/go.sum index 39f15cf..acfce07 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go new file mode 100644 index 0000000..b2e8e6a --- /dev/null +++ b/internal/filesystem/filesystem.go @@ -0,0 +1,17 @@ +// Package filesystem contains the filesystem interface. +package filesystem + +import ( + "codegen/internal/flagkeys" + + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func FileSystem() afero.Fs { + return viper.Get(flagkeys.FileSystem).(afero.Fs) +} + +func init() { + viper.SetDefault(flagkeys.FileSystem, afero.NewOsFs()) +} diff --git a/internal/flagkeys/flagkeys.go b/internal/flagkeys/flagkeys.go index 61d8518..90f1976 100644 --- a/internal/flagkeys/flagkeys.go +++ b/internal/flagkeys/flagkeys.go @@ -1,6 +1,8 @@ // Package commonflags contains keys for all command-line flags related to openfeature CLI. package flagkeys +import "github.com/spf13/viper" + const ( // `generate` flags: // FlagManifestPath is the key for the flag that stores the flag manifest path. @@ -11,4 +13,12 @@ const ( // `generate go` flags: // GoPackageName is the key for the flag that stores the Golang package name. GoPackageName = "package_name" + + //internal keys: + // FileSystem is the key for the flag that stores the filesystem interface. + FileSystem = "filesystem" ) + +func init() { + viper.SetDefault(FileSystem, "local") +} diff --git a/internal/generate/generate.go b/internal/generate/generate.go index c7b3d97..e92d2c5 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -3,11 +3,11 @@ package generate import ( "bytes" + "codegen/internal/filesystem" "codegen/internal/flagkeys" "codegen/internal/generate/manifestutils" "codegen/internal/generate/types" "fmt" - "os" "path" "path/filepath" "text/template" @@ -28,10 +28,11 @@ func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataIn return fmt.Errorf("error executing template: %v", err) } outputPath := data.BaseTmplDataInfo().OutputPath - if err := os.MkdirAll(filepath.Dir(outputPath), 0770); err != nil { + fs := filesystem.FileSystem() + if err := fs.MkdirAll(filepath.Dir(outputPath), 0660); err != nil { return err } - f, err := os.Create(path.Join(outputPath)) + f, err := fs.Create(path.Join(outputPath)) if err != nil { return fmt.Errorf("error creating file %q: %v", outputPath, err) } diff --git a/internal/generate/manifestutils/manifestutils.go b/internal/generate/manifestutils/manifestutils.go index ac19db0..3ef464d 100644 --- a/internal/generate/manifestutils/manifestutils.go +++ b/internal/generate/manifestutils/manifestutils.go @@ -3,20 +3,23 @@ package manifestutils import ( flagmanifest "codegen/docs/schema/v0" + "codegen/internal/filesystem" "codegen/internal/flagkeys" "codegen/internal/generate/types" "encoding/json" "fmt" - "os" + "sort" "strconv" "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/spf13/afero" "github.com/spf13/viper" ) // LoadData loads the data from the flag manifest. func LoadData(manifestPath string, supportedFlagTypes map[types.FlagType]bool) (*types.BaseTmplData, error) { - data, err := os.ReadFile(manifestPath) + fs := filesystem.FileSystem() + data, err := afero.ReadFile(fs, manifestPath) if err != nil { return nil, fmt.Errorf("error reading contents from file %q", manifestPath) } @@ -101,5 +104,9 @@ func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) { Docs: docs, }) } + // Ensure consistency of order of flag generation. + sort.Slice(btData.Flags, func(i, j int) bool { + return btData.Flags[i].Name < btData.Flags[j].Name + }) return &btData, nil } diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go new file mode 100644 index 0000000..95c4dc9 --- /dev/null +++ b/internal/testutils/testutils.go @@ -0,0 +1 @@ +package testutils diff --git a/main.go b/main.go index b7bd958..f6d681d 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,9 @@ import "codegen/cmd" var ( // Overridden by Go Releaser at build time - version = "dev" - commit = "HEAD" - date = "unknown" + version = "dev" + commit = "HEAD" + date = "unknown" ) func main() {