diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index 5e8fdaf..2ee3f78 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -2,6 +2,7 @@ package generate import ( "codegen/cmd/generate/golang" + "codegen/cmd/generate/react" "codegen/internal/flagkeys" "github.com/spf13/cobra" @@ -18,6 +19,7 @@ var Root = &cobra.Command{ func init() { // Add subcommands. Root.AddCommand(golang.Cmd) + Root.AddCommand(react.Cmd) // Add flags. Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.") diff --git a/cmd/generate/react/react.go b/cmd/generate/react/react.go new file mode 100644 index 0000000..02450b0 --- /dev/null +++ b/cmd/generate/react/react.go @@ -0,0 +1,24 @@ +package react + +import ( + "codegen/internal/generate" + "codegen/internal/generate/plugins/react" + + "github.com/spf13/cobra" +) + +// Cmd for "generate" command, handling code generation for flag accessors +var Cmd = &cobra.Command{ + Use: "react", + Short: "Generate typesafe React Hooks.", + Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`, + RunE: func(cmd *cobra.Command, args []string) error { + params := react.Params{} + gen := react.NewGenerator(params) + err := generate.CreateFlagAccessors(gen) + return err + }, +} + +func init() { +} diff --git a/internal/generate/generate.go b/internal/generate/generate.go index 858f5d1..c7b3d97 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -16,7 +16,7 @@ import ( ) // GenerateFile receives data for the Go template engine and outputs the contents to the file. -// Intended to be invoked by each language generator with appropiate data. +// Intended to be invoked by each language generator with appropriate data. func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataInterface) error { contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents) if err != nil { @@ -47,7 +47,7 @@ func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataIn return nil } -// Takes as input a generator and outputs file with the appropiate flag accessors. +// Takes as input a generator and outputs file with the appropriate flag accessors. // The flag data is taken from the provided flag manifest. func CreateFlagAccessors(gen types.Generator) error { bt, err := manifestutils.LoadData(viper.GetString(flagkeys.FlagManifestPath), gen.SupportedFlagTypes()) diff --git a/internal/generate/plugins/react/react.go b/internal/generate/plugins/react/react.go new file mode 100644 index 0000000..c8f396a --- /dev/null +++ b/internal/generate/plugins/react/react.go @@ -0,0 +1,121 @@ +package react + +import ( + _ "embed" + "sort" + "strconv" + "text/template" + + "codegen/internal/generate" + "codegen/internal/generate/types" + + "github.com/iancoleman/strcase" +) + +type TmplData struct { + *types.BaseTmplData +} + +type genImpl struct { +} + +// 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 Go 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 react.tmpl +var reactTmpl string + +func flagVarName(flagName string) string { + return strcase.ToCamel(flagName) +} + +func flagInitParam(flagName string) string { + return strconv.Quote(flagName) +} + +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 "" + } +} + +func supportImports(flags []*types.FlagTmplData) []string { + imports := make(map[string]struct{}) + for _, flag := range flags { + imports[flagAccessFunc(flag.Type)] = struct{}{} + } + var result []string + for k := range imports { + result = append(result, k) + } + sort.Strings(result) + return result +} + +func defaultValueLiteral(flag *types.FlagTmplData) string { + switch flag.Type { + case types.StringType: + return strconv.Quote(flag.DefaultValue) + default: + return flag.DefaultValue + } +} + +func typeString(flagType types.FlagType) string { + switch flagType { + case types.StringType: + return "string" + case types.IntType, types.FloatType: + return "number" + case types.BoolType: + return "boolean" + default: + return "" + } +} + +func (g *genImpl) Generate(input types.Input) error { + funcs := template.FuncMap{ + "FlagVarName": flagVarName, + "FlagInitParam": flagInitParam, + "FlagAccessFunc": flagAccessFunc, + "SupportImports": supportImports, + "DefaultValueLiteral": defaultValueLiteral, + "TypeString": typeString, + } + td := TmplData{ + BaseTmplData: input.BaseData, + } + return generate.GenerateFile(funcs, reactTmpl, &td) +} + +// Params are parameters for creating a Generator +type Params struct { +} + +// NewGenerator creates a generator for React. +func NewGenerator(params Params) types.Generator { + return &genImpl{} +} diff --git a/internal/generate/plugins/react/react.tmpl b/internal/generate/plugins/react/react.tmpl new file mode 100644 index 0000000..d37581b --- /dev/null +++ b/internal/generate/plugins/react/react.tmpl @@ -0,0 +1,20 @@ +'use client'; + +import { +{{- range $_, $p := SupportImports .Flags}} + {{$p}}, +{{- end}} +} from "@openfeature/react-sdk"; +{{ range .Flags}} +/** +* {{.Docs}} +* +* **Details:** +* - flag key: `{{ .Name}}` +* - default value: `{{ .DefaultValue}}` +* - type: `{{TypeString .Type}}` +*/ +export const use{{FlagVarName .Name}} = (options: Parameters[2]) => { + return {{FlagAccessFunc .Type}}({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options); +}; +{{ end}} \ No newline at end of file