diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index 39640d5..691838b 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -2,6 +2,7 @@ package generate import ( "github.com/open-feature/cli/cmd/generate/golang" + "github.com/open-feature/cli/cmd/generate/java" "github.com/open-feature/cli/cmd/generate/react" "github.com/open-feature/cli/internal/flagkeys" @@ -20,6 +21,7 @@ func init() { // Add subcommands. Root.AddCommand(golang.Cmd) Root.AddCommand(react.Cmd) + Root.AddCommand(java.Cmd) // Add flags. Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.") diff --git a/cmd/generate/generate_test.go b/cmd/generate/generate_test.go index 6500386..2595d9a 100644 --- a/cmd/generate/generate_test.go +++ b/cmd/generate/generate_test.go @@ -65,6 +65,32 @@ func TestGenerateReactSuccess(t *testing.T) { compareOutput(t, testFileReact, memoryOutputPath, fs) } +func TestGenerateJavaSuccess(t *testing.T) { + // Constant paths. + const memoryManifestPath = "manifest/path.json" + const memoryOutputPath = "example_java/ExperimentFlags.java" + const testFileManifest = "testdata/success_manifest.golden" + const testFileReact = "testdata/success_java.golden" + + // Prepare in-memory files. + fs := afero.NewMemMapFs() + viper.Set(flagkeys.FileSystem, fs) + readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs) + + // Prepare command. + Root.SetArgs([]string{"java", + "--flag_manifest_path", memoryManifestPath, + "--output_path", memoryOutputPath, + "--package_name", "example_java", + }) + + // 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 { diff --git a/cmd/generate/golang/golang.go b/cmd/generate/golang/golang.go index 76e5b5f..4301a4b 100644 --- a/cmd/generate/golang/golang.go +++ b/cmd/generate/golang/golang.go @@ -9,7 +9,8 @@ import ( "github.com/spf13/viper" ) -// Cmd for `generate“ command, handling code generation for flag accessors +// Cmd for `generate go` command, handling code generation for flag accessors +// for Golang. var Cmd = &cobra.Command{ Use: "go", Short: "Generate Golang flag accessors for OpenFeature.", diff --git a/cmd/generate/java/java.go b/cmd/generate/java/java.go new file mode 100644 index 0000000..19b0508 --- /dev/null +++ b/cmd/generate/java/java.go @@ -0,0 +1,33 @@ +package java + +import ( + "github.com/open-feature/cli/internal/flagkeys" + "github.com/open-feature/cli/internal/generate" + "github.com/open-feature/cli/internal/generate/plugins/java" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Cmd for `generate java“ command, handling code generation for flag accessors +// for Java. +var Cmd = &cobra.Command{ + Use: "java", + Short: "Generate Java flag accessors for OpenFeature.", + Long: `Generate Java flag accessors for OpenFeature.`, + RunE: func(cmd *cobra.Command, args []string) error { + params := java.Params{ + JavaPackage: viper.GetString(flagkeys.JavaPackageName), + } + gen := java.NewGenerator(params) + err := generate.CreateFlagAccessors(gen) + return err + }, +} + +func init() { + Cmd.Flags().String(flagkeys.JavaPackageName, "", "Name of the Java package to be generated.") + Cmd.MarkFlagRequired(flagkeys.JavaPackageName) + viper.BindPFlag(flagkeys.JavaPackageName, Cmd.Flags().Lookup(flagkeys.JavaPackageName)) + +} diff --git a/cmd/generate/testdata/success_java.golden b/cmd/generate/testdata/success_java.golden new file mode 100644 index 0000000..b2dc029 --- /dev/null +++ b/cmd/generate/testdata/success_java.golden @@ -0,0 +1,56 @@ +// AUTOMATICALLY GENERATED BY OPENFEATURE CODEGEN, DO NOT EDIT. +package example_java; +import dev.openfeature.sdk.*; + + +public class ExperimentFlags { + private static Client client = OpenFeatureAPI.getInstance().getClient(); + /** + * Discount percentage applied to purchases. + */ + public static class DiscountPercentage { + public static Double getValue(EvaluationContext evaluationContext) { + return client.getDoubleValue("discountPercentage", 0.15, evaluationContext); + } + + public static FlagEvaluationDetails getValueDetails(EvaluationContext evaluationContext) { + return client.getDoubleDetails("discountPercentage", 0.15, evaluationContext); + } + } + /** + * Controls whether Feature A is enabled. + */ + public static class EnableFeatureA { + public static Boolean getValue(EvaluationContext evaluationContext) { + return client.getBooleanValue("enableFeatureA", false, evaluationContext); + } + + public static FlagEvaluationDetails getValueDetails(EvaluationContext evaluationContext) { + return client.getBooleanDetails("enableFeatureA", false, evaluationContext); + } + } + /** + * The message to use for greeting users. + */ + public static class GreetingMessage { + public static String getValue(EvaluationContext evaluationContext) { + return client.getStringValue("greetingMessage", "Hello there!", evaluationContext); + } + + public static FlagEvaluationDetails getValueDetails(EvaluationContext evaluationContext) { + return client.getStringDetails("greetingMessage", "Hello there!", evaluationContext); + } + } + /** + * Maximum allowed length for usernames. + */ + public static class UsernameMaxLength { + public static Integer getValue(EvaluationContext evaluationContext) { + return client.getIntegerValue("usernameMaxLength", 50, evaluationContext); + } + + public static FlagEvaluationDetails getValueDetails(EvaluationContext evaluationContext) { + return client.getIntegerDetails("usernameMaxLength", 50, evaluationContext); + } + } +} \ No newline at end of file diff --git a/internal/flagkeys/flagkeys.go b/internal/flagkeys/flagkeys.go index 90f1976..a8ac06d 100644 --- a/internal/flagkeys/flagkeys.go +++ b/internal/flagkeys/flagkeys.go @@ -14,6 +14,10 @@ const ( // GoPackageName is the key for the flag that stores the Golang package name. GoPackageName = "package_name" + // `generate java` flags: + // JavaPackageName is the key for the flag that stores the Java package name. + JavaPackageName = "package_name" + //internal keys: // FileSystem is the key for the flag that stores the filesystem interface. FileSystem = "filesystem" diff --git a/internal/generate/plugins/java/java.go b/internal/generate/plugins/java/java.go new file mode 100644 index 0000000..617d9a6 --- /dev/null +++ b/internal/generate/plugins/java/java.go @@ -0,0 +1,123 @@ +package java + +import ( + _ "embed" + "html/template" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/iancoleman/strcase" + "github.com/open-feature/cli/internal/generate" + "github.com/open-feature/cli/internal/generate/types" +) + +// TmplData contains the Java-specific data and the base data for the codegen. +type TmplData struct { + *types.BaseTmplData + JavaPackage string +} + +type genImpl struct { + javaPackage string +} + +// BaseTmplDataInfo provides the base template data for the codegen. +func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData { + return td.BaseTmplData +} + +// supportedFlagTypes is the flag types supported by the Java template. +var supportedFlagTypes = map[types.FlagType]bool{ + types.FloatType: true, + types.StringType: true, + types.IntType: true, + types.BoolType: true, + types.ObjectType: false, +} + +func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool { + return supportedFlagTypes +} + +//go:embed java.tmpl +var javaTmpl string + +// Java Funcs BEGIN + +func flagClass(flagName string) string { + return strcase.ToCamel(flagName) +} + +func topLevelClass(outputPath string) string { + return strings.TrimSuffix(filepath.Base(outputPath), filepath.Ext(outputPath)) +} + +func flagInitParam(flagName string) string { + return strconv.Quote(flagName) +} + +func javaType(t types.FlagType) string { + switch t { + case types.IntType: + return "Integer" + case types.FloatType: + return "Double" + case types.BoolType: + return "Boolean" + case types.StringType: + return "String" + default: + return "" + } +} + +func supportImports(flags []*types.FlagTmplData) []string { + var res []string + if len(flags) > 0 { + res = append(res, "dev.openfeature.sdk.*") + } + sort.Strings(res) + return res +} + +func defaultValueLiteral(flag *types.FlagTmplData) string { + switch flag.Type { + case types.StringType: + return strconv.Quote(flag.DefaultValue) + default: + return flag.DefaultValue + } +} + +// Java Funcs END + +// Generate generates the Java flag accessors for OpenFeature. +func (g *genImpl) Generate(input types.Input) error { + funcs := template.FuncMap{ + "FlagClass": flagClass, + "TopLevelClass": topLevelClass, + "FlagInitParam": flagInitParam, + "JavaType": javaType, + "SupportImports": supportImports, + "DefaultValueLiteral": defaultValueLiteral, + } + td := TmplData{ + BaseTmplData: input.BaseData, + JavaPackage: g.javaPackage, + } + return generate.GenerateFile(funcs, javaTmpl, &td) +} + +// Params are parameters for creating a Generator +type Params struct { + JavaPackage string +} + +// NewGenerator creates a generator for Java. +func NewGenerator(params Params) types.Generator { + return &genImpl{ + javaPackage: params.JavaPackage, + } +} diff --git a/internal/generate/plugins/java/java.tmpl b/internal/generate/plugins/java/java.tmpl new file mode 100644 index 0000000..48f5c5e --- /dev/null +++ b/internal/generate/plugins/java/java.tmpl @@ -0,0 +1,27 @@ +// AUTOMATICALLY GENERATED BY OPENFEATURE CODEGEN, DO NOT EDIT. +package {{.JavaPackage}}; + + +{{- range $_, $p := SupportImports .Flags}} +import {{$p}}; +{{- end}} + + +public class {{TopLevelClass .OutputPath}} { + private static Client client = OpenFeatureAPI.getInstance().getClient(); + +{{- range .Flags}} + /** + * {{.Docs}} + */ + public static class {{FlagClass .Name}} { + public static {{JavaType .Type}} getValue(EvaluationContext evaluationContext) { + return client.get{{JavaType .Type}}Value({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evaluationContext); + } + + public static FlagEvaluationDetails<{{JavaType .Type}}> getValueDetails(EvaluationContext evaluationContext) { + return client.get{{JavaType .Type}}Details({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evaluationContext); + } + } +{{- end}} +} \ No newline at end of file