From ecf1bf2616906d24310bdb82e390dea91b4b32fd Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Mon, 6 Jan 2025 22:12:50 +0000 Subject: [PATCH] add doc gen, move schema path, add tests, fix react gen Signed-off-by: Michael Beemer --- Makefile | 11 +++ cmd/docs.go | 23 ++++++ cmd/generate/testdata/success_manifest.golden | 10 +-- cmd/generate/testdata/success_react.golden | 22 +++--- cmd/root.go | 3 +- docs/commands/openfeature.md | 21 ++++++ docs/commands/openfeature_generate.md | 24 +++++++ docs/commands/openfeature_generate_go.md | 32 +++++++++ docs/commands/openfeature_generate_react.md | 31 ++++++++ docs/commands/openfeature_version.md | 20 ++++++ docs/generate-commands.go | 16 +++++ go.mod | 2 + go.sum | 2 + .../generate/manifestutils/manifestutils.go | 16 ++--- internal/generate/plugins/golang/golang.go | 8 +-- internal/generate/plugins/golang/golang.tmpl | 4 +- internal/generate/plugins/react/react.go | 19 ++--- internal/generate/plugins/react/react.tmpl | 12 ++-- internal/generate/types/types.go | 2 +- sample/sample_manifest.json | 10 +-- {docs/schema => schema}/v0/flag_manifest.json | 61 +++++++++------- {docs/schema => schema}/v0/flagmanifest.go | 4 +- schema/v0/flagmanifest_test.go | 72 +++++++++++++++++++ .../v0/tests/negative/missing-flag-type.json | 8 +++ .../tests/negative/no-flags-in-manifest.json | 4 ++ .../v0/tests/positive/min-flag-manifest.json | 28 ++++++++ 26 files changed, 378 insertions(+), 87 deletions(-) create mode 100644 Makefile create mode 100644 cmd/docs.go create mode 100644 docs/commands/openfeature.md create mode 100644 docs/commands/openfeature_generate.md create mode 100644 docs/commands/openfeature_generate_go.md create mode 100644 docs/commands/openfeature_generate_react.md create mode 100644 docs/commands/openfeature_version.md create mode 100644 docs/generate-commands.go rename {docs/schema => schema}/v0/flag_manifest.json (71%) rename {docs/schema => schema}/v0/flagmanifest.go (60%) create mode 100644 schema/v0/flagmanifest_test.go create mode 100644 schema/v0/tests/negative/missing-flag-type.json create mode 100644 schema/v0/tests/negative/no-flags-in-manifest.json create mode 100644 schema/v0/tests/positive/min-flag-manifest.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e8c8563 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + +.PHONY: test +test: + @echo "Running tests..." + go test -v ./... + @echo "Tests passed successfully!" + +generate-docs: + @echo "Generating documentation..." + go run ./docs/generate-commands.go + @echo "Documentation generated successfully!" \ No newline at end of file diff --git a/cmd/docs.go b/cmd/docs.go new file mode 100644 index 0000000..4ae3175 --- /dev/null +++ b/cmd/docs.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra/doc" +) + +// GenerateDoc generates cobra docs of the cmd +func GenerateDoc(path string) error { + linkHandler := func(name string) string { + return name + } + + filePrepender := func(filename string) string { + return "\n\n" + } + + if err := doc.GenMarkdownTreeCustom(rootCmd, path, filePrepender, linkHandler); err != nil { + return fmt.Errorf("error generating docs: %w", err) + } + return nil +} diff --git a/cmd/generate/testdata/success_manifest.golden b/cmd/generate/testdata/success_manifest.golden index 2df98b6..5c40021 100644 --- a/cmd/generate/testdata/success_manifest.golden +++ b/cmd/generate/testdata/success_manifest.golden @@ -2,27 +2,27 @@ "flags": { "enableFeatureA": { "flagType": "boolean", - "defaultValue": false, + "codeDefault": false, "description": "Controls whether Feature A is enabled." }, "usernameMaxLength": { "flagType": "integer", - "defaultValue": 50, + "codeDefault": 50, "description": "Maximum allowed length for usernames." }, "greetingMessage": { "flagType": "string", - "defaultValue": "Hello there!", + "codeDefault": "Hello there!", "description": "The message to use for greeting users." }, "discountPercentage": { "flagType": "float", - "defaultValue": 0.15, + "codeDefault": 0.15, "description": "Discount percentage applied to purchases." }, "themeCustomization": { "flagType": "object", - "defaultValue": { + "codeDefault": { "primaryColor": "#007bff", "secondaryColor": "#6c757d" }, diff --git a/cmd/generate/testdata/success_react.golden b/cmd/generate/testdata/success_react.golden index 9a68ec5..c3f9e2c 100644 --- a/cmd/generate/testdata/success_react.golden +++ b/cmd/generate/testdata/success_react.golden @@ -1,10 +1,6 @@ 'use client'; -import { - useBooleanFlagDetails, - useNumberFlagDetails, - useStringFlagDetails, -} from "@openfeature/react-sdk"; +import { type ReactFlagEvaluationOptions, useFlag } from "@openfeature/react-sdk"; /** * Discount percentage applied to purchases. @@ -14,8 +10,8 @@ import { * - default value: `0.15` * - type: `number` */ -export const useDiscountPercentage = (options: Parameters[2]) => { - return useNumberFlagDetails("discountPercentage", 0.15, options); +export const useDiscountPercentage = (options: ReactFlagEvaluationOptions) => { + return useFlag("discountPercentage", 0.15, options); }; /** @@ -26,8 +22,8 @@ export const useDiscountPercentage = (options: Parameters[2]) => { - return useBooleanFlagDetails("enableFeatureA", false, options); +export const useEnableFeatureA = (options: ReactFlagEvaluationOptions) => { + return useFlag("enableFeatureA", false, options); }; /** @@ -38,8 +34,8 @@ export const useEnableFeatureA = (options: Parameters[2]) => { - return useStringFlagDetails("greetingMessage", "Hello there!", options); +export const useGreetingMessage = (options: ReactFlagEvaluationOptions) => { + return useFlag("greetingMessage", "Hello there!", options); }; /** @@ -50,6 +46,6 @@ export const useGreetingMessage = (options: Parameters[2]) => { - return useNumberFlagDetails("usernameMaxLength", 50, options); +export const useUsernameMaxLength = (options: ReactFlagEvaluationOptions) => { + return useFlag("usernameMaxLength", 50, options); }; diff --git a/cmd/root.go b/cmd/root.go index c30fa50..f4d71e8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,7 +10,7 @@ import ( ) var ( - Version string + Version = "dev" Commit string Date string ) @@ -20,6 +20,7 @@ var rootCmd = &cobra.Command{ Use: "openfeature", Short: "CLI for OpenFeature.", Long: `CLI for OpenFeature related functionalities.`, + DisableAutoGenTag: true, } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/docs/commands/openfeature.md b/docs/commands/openfeature.md new file mode 100644 index 0000000..08d5ce1 --- /dev/null +++ b/docs/commands/openfeature.md @@ -0,0 +1,21 @@ + + +## openfeature + +CLI for OpenFeature. + +### Synopsis + +CLI for OpenFeature related functionalities. + +### Options + +``` + -h, --help help for openfeature +``` + +### SEE ALSO + +* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature. +* [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI + diff --git a/docs/commands/openfeature_generate.md b/docs/commands/openfeature_generate.md new file mode 100644 index 0000000..68f9447 --- /dev/null +++ b/docs/commands/openfeature_generate.md @@ -0,0 +1,24 @@ + + +## openfeature generate + +Code generation for flag accessors for OpenFeature. + +### Synopsis + +Code generation for flag accessors for OpenFeature. + +### Options + +``` + --flag_manifest_path string Path to the flag manifest. + -h, --help help for generate + --output_path string Output path for the codegen +``` + +### SEE ALSO + +* [openfeature](openfeature.md) - CLI for OpenFeature. +* [openfeature generate go](openfeature_generate_go.md) - Generate Golang flag accessors for OpenFeature. +* [openfeature generate react](openfeature_generate_react.md) - Generate typesafe React Hooks. + diff --git a/docs/commands/openfeature_generate_go.md b/docs/commands/openfeature_generate_go.md new file mode 100644 index 0000000..bac03e5 --- /dev/null +++ b/docs/commands/openfeature_generate_go.md @@ -0,0 +1,32 @@ + + +## openfeature generate go + +Generate Golang flag accessors for OpenFeature. + +### Synopsis + +Generate Golang flag accessors for OpenFeature. + +``` +openfeature generate go [flags] +``` + +### Options + +``` + -h, --help help for go + --package_name string Name of the Go package to be generated. +``` + +### Options inherited from parent commands + +``` + --flag_manifest_path string Path to the flag manifest. + --output_path string Output path for the codegen +``` + +### SEE ALSO + +* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature. + diff --git a/docs/commands/openfeature_generate_react.md b/docs/commands/openfeature_generate_react.md new file mode 100644 index 0000000..2514b18 --- /dev/null +++ b/docs/commands/openfeature_generate_react.md @@ -0,0 +1,31 @@ + + +## openfeature generate react + +Generate typesafe React Hooks. + +### Synopsis + +Generate typesafe React Hooks compatible with the OpenFeature React SDK. + +``` +openfeature generate react [flags] +``` + +### Options + +``` + -h, --help help for react +``` + +### Options inherited from parent commands + +``` + --flag_manifest_path string Path to the flag manifest. + --output_path string Output path for the codegen +``` + +### SEE ALSO + +* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature. + diff --git a/docs/commands/openfeature_version.md b/docs/commands/openfeature_version.md new file mode 100644 index 0000000..44eba8e --- /dev/null +++ b/docs/commands/openfeature_version.md @@ -0,0 +1,20 @@ + + +## openfeature version + +Print the version number of the OpenFeature CLI + +``` +openfeature version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### SEE ALSO + +* [openfeature](openfeature.md) - CLI for OpenFeature. + diff --git a/docs/generate-commands.go b/docs/generate-commands.go new file mode 100644 index 0000000..f420f0f --- /dev/null +++ b/docs/generate-commands.go @@ -0,0 +1,16 @@ +package main + +import ( + "log" + + "github.com/open-feature/cli/cmd" +) + +const docPath = "./docs/commands" + +// GenerateDoc generates cobra docs of the cmd +func main() { + if err := cmd.GenerateDoc(docPath); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 9d306d5..2eae4f6 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,14 @@ require ( ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index 8d43603..ee6e884 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -30,6 +31,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= diff --git a/internal/generate/manifestutils/manifestutils.go b/internal/generate/manifestutils/manifestutils.go index 35f6aa9..da5b3e2 100644 --- a/internal/generate/manifestutils/manifestutils.go +++ b/internal/generate/manifestutils/manifestutils.go @@ -7,10 +7,10 @@ import ( "sort" "strconv" - flagmanifest "github.com/open-feature/cli/docs/schema/v0" "github.com/open-feature/cli/internal/filesystem" "github.com/open-feature/cli/internal/flagkeys" "github.com/open-feature/cli/internal/generate/types" + flagmanifest "github.com/open-feature/cli/schema/v0" "github.com/santhosh-tekuri/jsonschema/v5" "github.com/spf13/afero" @@ -54,19 +54,19 @@ var stringToFlagType = map[string]types.FlagType{ "object": types.ObjectType, } -func getDefaultValue(defaultValue interface{}, flagType types.FlagType) string { +func getCodeDefault(codeDefault interface{}, flagType types.FlagType) string { switch flagType { case types.BoolType: - return strconv.FormatBool(defaultValue.(bool)) + return strconv.FormatBool(codeDefault.(bool)) case types.IntType: //the conversion to float64 instead of integer typically occurs //due to how JSON is parsed in Go. In Go's encoding/json package, //all JSON numbers are unmarshaled into float64 by default when decoding into an interface{}. - return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) + return strconv.FormatFloat(codeDefault.(float64), 'g', -1, 64) case types.FloatType: - return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64) + return strconv.FormatFloat(codeDefault.(float64), 'g', -1, 64) case types.StringType: - return defaultValue.(string) + return codeDefault.(string) default: return "" } @@ -97,11 +97,11 @@ func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) { flagTypeString := flagData["flagType"].(string) flagType := stringToFlagType[flagTypeString] docs := flagData["description"].(string) - defaultValue := getDefaultValue(flagData["defaultValue"], flagType) + codeDefault := getCodeDefault(flagData["codeDefault"], flagType) btData.Flags = append(btData.Flags, &types.FlagTmplData{ Name: flagKey, Type: flagType, - DefaultValue: defaultValue, + CodeDefault: codeDefault, Docs: docs, }) } diff --git a/internal/generate/plugins/golang/golang.go b/internal/generate/plugins/golang/golang.go index 8f12f30..264b6ed 100644 --- a/internal/generate/plugins/golang/golang.go +++ b/internal/generate/plugins/golang/golang.go @@ -78,12 +78,12 @@ func supportImports(flags []*types.FlagTmplData) []string { return res } -func defaultValueLiteral(flag *types.FlagTmplData) string { +func codeDefaultValueLiteral(flag *types.FlagTmplData) string { switch flag.Type { case types.StringType: - return strconv.Quote(flag.DefaultValue) + return strconv.Quote(flag.CodeDefault) default: - return flag.DefaultValue + return flag.CodeDefault } } @@ -111,7 +111,7 @@ func (g *genImpl) Generate(input types.Input) error { "FlagInitParam": flagInitParam, "OpenFeatureType": openFeatureType, "SupportImports": supportImports, - "DefaultValueLiteral": defaultValueLiteral, + "CodeDefaultValueLiteral": codeDefaultValueLiteral, "TypeString": typeString, } td := TmplData{ diff --git a/internal/generate/plugins/golang/golang.tmpl b/internal/generate/plugins/golang/golang.tmpl index be1ee8a..e7df88a 100644 --- a/internal/generate/plugins/golang/golang.tmpl +++ b/internal/generate/plugins/golang/golang.tmpl @@ -31,10 +31,10 @@ var {{FlagVarName .Name}} = struct { ValueWithDetails {{OpenFeatureType .Type}}ProviderDetails }{ Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{TypeString .Type}}, error) { - return client.{{OpenFeatureType .Type}}Value(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evalCtx) + return client.{{OpenFeatureType .Type}}Value(ctx, {{FlagInitParam .Name}}, {{CodeDefaultValueLiteral .}}, evalCtx) }, ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{OpenFeatureType .Type}}EvaluationDetails, error){ - return client.{{OpenFeatureType .Type}}ValueDetails(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evalCtx) + return client.{{OpenFeatureType .Type}}ValueDetails(ctx, {{FlagInitParam .Name}}, {{CodeDefaultValueLiteral .}}, evalCtx) }, } {{- end}} diff --git a/internal/generate/plugins/react/react.go b/internal/generate/plugins/react/react.go index c8bb4b9..64f7ee2 100644 --- a/internal/generate/plugins/react/react.go +++ b/internal/generate/plugins/react/react.go @@ -49,16 +49,7 @@ func flagInitParam(flagName string) string { } func flagAccessFunc(t types.FlagType) string { - switch t { - case types.IntType, types.FloatType: - return "useNumberFlagDetails" - case types.BoolType: - return "useBooleanFlagDetails" - case types.StringType: - return "useStringFlagDetails" - default: - return "" - } + return "useFlag" } func supportImports(flags []*types.FlagTmplData) []string { @@ -74,12 +65,12 @@ func supportImports(flags []*types.FlagTmplData) []string { return result } -func defaultValueLiteral(flag *types.FlagTmplData) string { +func codeDefaultValueLiteral(flag *types.FlagTmplData) string { switch flag.Type { case types.StringType: - return strconv.Quote(flag.DefaultValue) + return strconv.Quote(flag.CodeDefault) default: - return flag.DefaultValue + return flag.CodeDefault } } @@ -102,7 +93,7 @@ func (g *genImpl) Generate(input types.Input) error { "FlagInitParam": flagInitParam, "FlagAccessFunc": flagAccessFunc, "SupportImports": supportImports, - "DefaultValueLiteral": defaultValueLiteral, + "CodeDefaultValueLiteral": codeDefaultValueLiteral, "TypeString": typeString, } td := TmplData{ diff --git a/internal/generate/plugins/react/react.tmpl b/internal/generate/plugins/react/react.tmpl index d37581b..5ac6606 100644 --- a/internal/generate/plugins/react/react.tmpl +++ b/internal/generate/plugins/react/react.tmpl @@ -1,20 +1,16 @@ 'use client'; -import { -{{- range $_, $p := SupportImports .Flags}} - {{$p}}, -{{- end}} -} from "@openfeature/react-sdk"; +import { type ReactFlagEvaluationOptions, useFlag } from "@openfeature/react-sdk"; {{ range .Flags}} /** * {{.Docs}} * * **Details:** * - flag key: `{{ .Name}}` -* - default value: `{{ .DefaultValue}}` +* - default value: `{{ .CodeDefault }}` * - type: `{{TypeString .Type}}` */ -export const use{{FlagVarName .Name}} = (options: Parameters[2]) => { - return {{FlagAccessFunc .Type}}({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options); +export const use{{FlagVarName .Name}} = (options: ReactFlagEvaluationOptions) => { + return useFlag({{FlagInitParam .Name}}, {{CodeDefaultValueLiteral .}}, options); }; {{ end}} \ No newline at end of file diff --git a/internal/generate/types/types.go b/internal/generate/types/types.go index 861d2e7..ab50346 100644 --- a/internal/generate/types/types.go +++ b/internal/generate/types/types.go @@ -19,7 +19,7 @@ const ( type FlagTmplData struct { Name string Type FlagType - DefaultValue string + CodeDefault string Docs string } diff --git a/sample/sample_manifest.json b/sample/sample_manifest.json index 1a65d68..d060460 100644 --- a/sample/sample_manifest.json +++ b/sample/sample_manifest.json @@ -2,27 +2,27 @@ "flags": { "enableFeatureA": { "flagType": "boolean", - "defaultValue": false, + "codeDefault": false, "description": "Controls whether Feature A is enabled." }, "usernameMaxLength": { "flagType": "integer", - "defaultValue": 50, + "codeDefault": 50, "description": "Maximum allowed length for usernames." }, "greetingMessage": { "flagType": "string", - "defaultValue": "Hello there!", + "codeDefault": "Hello there!", "description": "The message to use for greeting users." }, "discountPercentage": { "flagType": "float", - "defaultValue": 0.15, + "codeDefault": 0.15, "description": "Discount percentage applied to purchases." }, "themeCustomization": { "flagType": "object", - "defaultValue": { + "codeDefault": { "primaryColor": "#007bff", "secondaryColor": "#6c757d" }, diff --git a/docs/schema/v0/flag_manifest.json b/schema/v0/flag_manifest.json similarity index 71% rename from docs/schema/v0/flag_manifest.json rename to schema/v0/flag_manifest.json index da02e97..844dd2c 100644 --- a/docs/schema/v0/flag_manifest.json +++ b/schema/v0/flag_manifest.json @@ -1,4 +1,5 @@ { + "$id": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Flag Manifest", "description": "Describes a configuration of OpenFeature flags, including info such as their types and default values.", @@ -13,10 +14,13 @@ "$ref": "#/$defs/flag" } }, - "additionalProperties": false + "additionalProperties": false, + "minProperties": 1 } }, - "required": ["flags"], + "required": [ + "flags" + ], "$defs": { "flag": { "oneOf": [ @@ -36,87 +40,96 @@ "$ref": "#/$defs/objectType" } ], - "required": ["flagType", "defaultValue"] + "required": [ + "flagType", + "codeDefault" + ] }, "booleanType": { "type": "object", "properties": { "flagType": { "type": "string", - "enum": ["boolean"] + "enum": [ + "boolean" + ] }, - "defaultValue": { + "codeDefault": { + "description": "The default value returned in code if a flag evaluation is unsuccessful", "type": "boolean" }, "description": { "type": "string" } - }, - "additionalProperties": false + } }, "stringType": { "type": "object", "properties": { "flagType": { "type": "string", - "enum": ["string"] + "enum": [ + "string" + ] }, - "defaultValue": { + "codeDefault": { "type": "string" }, "description": { "type": "string" } - }, - "additionalProperties": false + } }, "integerType": { "type": "object", "properties": { "flagType": { "type": "string", - "enum": ["integer"] + "enum": [ + "integer" + ] }, - "defaultValue": { + "codeDefault": { "type": "integer" }, "description": { "type": "string" } - }, - "additionalProperties": false + } }, "floatType": { "type": "object", "properties": { "flagType": { "type": "string", - "enum": ["float"] + "enum": [ + "float" + ] }, - "defaultValue": { + "codeDefault": { "type": "number" }, "description": { "type": "string" } - }, - "additionalProperties": false + } }, "objectType": { "type": "object", "properties": { "flagType": { "type": "string", - "enum": ["object"] + "enum": [ + "object" + ] }, - "defaultValue": { + "codeDefault": { "type": "object" }, "description": { "type": "string" } - }, - "additionalProperties": false + } } } -} +} \ No newline at end of file diff --git a/docs/schema/v0/flagmanifest.go b/schema/v0/flagmanifest.go similarity index 60% rename from docs/schema/v0/flagmanifest.go rename to schema/v0/flagmanifest.go index dd66f32..8d0a596 100644 --- a/docs/schema/v0/flagmanifest.go +++ b/schema/v0/flagmanifest.go @@ -8,5 +8,5 @@ import _ "embed" //go:embed flag_manifest.json var Schema string -// SchemaPath proviees the current path and version of flag manifest. -const SchemaPath = "github.com/open-feature/cli/docs/schema/v0/flag_manifest.json" +// SchemaPath provides the current path and version of flag manifest. +const SchemaPath = "github.com/open-feature/cli/schema/v0/flag_manifest.json" diff --git a/schema/v0/flagmanifest_test.go b/schema/v0/flagmanifest_test.go new file mode 100644 index 0000000..cfffa27 --- /dev/null +++ b/schema/v0/flagmanifest_test.go @@ -0,0 +1,72 @@ +package flagmanifest + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/santhosh-tekuri/jsonschema/v5" +) + +var compiledFlagManifestSchema *jsonschema.Schema + +func init() { + sch, err := jsonschema.CompileString(SchemaPath, Schema) + if err != nil { + log.Fatal(fmt.Errorf("error compiling JSON schema: %v", err)) + } + compiledFlagManifestSchema = sch +} + +func TestPositiveFlagManifest(t *testing.T) { + if err := walkPath(true, "./tests/positive"); err != nil { + t.Error(err) + t.FailNow() + } +} + +func TestNegativeFlagManifest(t *testing.T) { + if err := walkPath(false, "./tests/negative"); err != nil { + t.Error(err) + t.FailNow() + } +} + + +func walkPath(shouldPass bool, root string) error { + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + ps := strings.Split(path, ".") + if ps[len(ps)-1] != "json" { + return nil + } + + file, err := os.ReadFile(path) + if err != nil { + return err + } + + var v interface{} + if err := json.Unmarshal([]byte(file), &v); err != nil { + log.Fatal(err) + } + + err = compiledFlagManifestSchema.Validate(v) + + if (err != nil && shouldPass == true) { + return fmt.Errorf("file %s should not have failed validation, but did: %s", path, err) + } + + if (err == nil && shouldPass == false) { + return fmt.Errorf("file %s should have failed validation, but did not", path) + } + + return nil + }) +} diff --git a/schema/v0/tests/negative/missing-flag-type.json b/schema/v0/tests/negative/missing-flag-type.json new file mode 100644 index 0000000..3e37e76 --- /dev/null +++ b/schema/v0/tests/negative/missing-flag-type.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../flag_manifest.json", + "flags": { + "booleanFlag": { + "codeDefault": true + } + } +} \ No newline at end of file diff --git a/schema/v0/tests/negative/no-flags-in-manifest.json b/schema/v0/tests/negative/no-flags-in-manifest.json new file mode 100644 index 0000000..818c777 --- /dev/null +++ b/schema/v0/tests/negative/no-flags-in-manifest.json @@ -0,0 +1,4 @@ +{ + "$schema": "../../flag_manifest.json", + "flags": {} +} \ No newline at end of file diff --git a/schema/v0/tests/positive/min-flag-manifest.json b/schema/v0/tests/positive/min-flag-manifest.json new file mode 100644 index 0000000..27756fa --- /dev/null +++ b/schema/v0/tests/positive/min-flag-manifest.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../flag_manifest.json", + "flags": { + "booleanFlag": { + "flagType": "boolean", + "codeDefault": true + }, + "stringFlag": { + "flagType": "string", + "codeDefault": "default" + }, + "integerFlag": { + "flagType": "integer", + "codeDefault": 50 + }, + "floatFlag": { + "flagType": "float", + "codeDefault": 0.15 + }, + "objectFlag": { + "flagType": "object", + "codeDefault": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d" + } + } + } +} \ No newline at end of file