Skip to content

Commit 757ab66

Browse files
authored
feat: add basic react support (#31)
## This PR - proof-of-concept React code gen implementation ### How to test #### Run: `go run main.go generate react --flag_manifest_path ./sample/sample_manifest.json --output_path ./output.ts` #### Output ```ts '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<typeof useNumberFlagDetails>[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<typeof useBooleanFlagDetails>[2]) => { return useBooleanFlagDetails("enableFeatureA", false, options); }; /** * Maximum allowed length for usernames. * * **Details:** * - flag key: `usernameMaxLength` * - default value: `50` * - type: `number` */ export const useUsernameMaxLength = (options: Parameters<typeof useNumberFlagDetails>[2]) => { return useNumberFlagDetails("usernameMaxLength", 50, options); }; /** * The message to use for greeting users. * * **Details:** * - flag key: `greetingMessage` * - default value: `Hello there!` * - type: `string` */ export const useGreetingMessage = (options: Parameters<typeof useStringFlagDetails>[2]) => { return useStringFlagDetails("greetingMessage", "Hello there!", options); }; ``` Signed-off-by: Michael Beemer <[email protected]>
1 parent 850c694 commit 757ab66

File tree

5 files changed

+169
-2
lines changed

5 files changed

+169
-2
lines changed

cmd/generate/generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package generate
22

33
import (
44
"codegen/cmd/generate/golang"
5+
"codegen/cmd/generate/react"
56
"codegen/internal/flagkeys"
67

78
"github.com/spf13/cobra"
@@ -18,6 +19,7 @@ var Root = &cobra.Command{
1819
func init() {
1920
// Add subcommands.
2021
Root.AddCommand(golang.Cmd)
22+
Root.AddCommand(react.Cmd)
2123

2224
// Add flags.
2325
Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.")

cmd/generate/react/react.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package react
2+
3+
import (
4+
"codegen/internal/generate"
5+
"codegen/internal/generate/plugins/react"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// Cmd for "generate" command, handling code generation for flag accessors
11+
var Cmd = &cobra.Command{
12+
Use: "react",
13+
Short: "Generate typesafe React Hooks.",
14+
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
15+
RunE: func(cmd *cobra.Command, args []string) error {
16+
params := react.Params{}
17+
gen := react.NewGenerator(params)
18+
err := generate.CreateFlagAccessors(gen)
19+
return err
20+
},
21+
}
22+
23+
func init() {
24+
}

internal/generate/generate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
// GenerateFile receives data for the Go template engine and outputs the contents to the file.
19-
// Intended to be invoked by each language generator with appropiate data.
19+
// Intended to be invoked by each language generator with appropriate data.
2020
func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataInterface) error {
2121
contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents)
2222
if err != nil {
@@ -47,7 +47,7 @@ func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataIn
4747
return nil
4848
}
4949

50-
// Takes as input a generator and outputs file with the appropiate flag accessors.
50+
// Takes as input a generator and outputs file with the appropriate flag accessors.
5151
// The flag data is taken from the provided flag manifest.
5252
func CreateFlagAccessors(gen types.Generator) error {
5353
bt, err := manifestutils.LoadData(viper.GetString(flagkeys.FlagManifestPath), gen.SupportedFlagTypes())
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package react
2+
3+
import (
4+
_ "embed"
5+
"sort"
6+
"strconv"
7+
"text/template"
8+
9+
"codegen/internal/generate"
10+
"codegen/internal/generate/types"
11+
12+
"github.com/iancoleman/strcase"
13+
)
14+
15+
type TmplData struct {
16+
*types.BaseTmplData
17+
}
18+
19+
type genImpl struct {
20+
}
21+
22+
// BaseTmplDataInfo provides the base template data for the codegen.
23+
func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData {
24+
return td.BaseTmplData
25+
}
26+
27+
// supportedFlagTypes is the flag types supported by the Go template.
28+
var supportedFlagTypes = map[types.FlagType]bool{
29+
types.FloatType: true,
30+
types.StringType: true,
31+
types.IntType: true,
32+
types.BoolType: true,
33+
types.ObjectType: false,
34+
}
35+
36+
func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool {
37+
return supportedFlagTypes
38+
}
39+
40+
//go:embed react.tmpl
41+
var reactTmpl string
42+
43+
func flagVarName(flagName string) string {
44+
return strcase.ToCamel(flagName)
45+
}
46+
47+
func flagInitParam(flagName string) string {
48+
return strconv.Quote(flagName)
49+
}
50+
51+
func flagAccessFunc(t types.FlagType) string {
52+
switch t {
53+
case types.IntType, types.FloatType:
54+
return "useNumberFlagDetails"
55+
case types.BoolType:
56+
return "useBooleanFlagDetails"
57+
case types.StringType:
58+
return "useStringFlagDetails"
59+
default:
60+
return ""
61+
}
62+
}
63+
64+
func supportImports(flags []*types.FlagTmplData) []string {
65+
imports := make(map[string]struct{})
66+
for _, flag := range flags {
67+
imports[flagAccessFunc(flag.Type)] = struct{}{}
68+
}
69+
var result []string
70+
for k := range imports {
71+
result = append(result, k)
72+
}
73+
sort.Strings(result)
74+
return result
75+
}
76+
77+
func defaultValueLiteral(flag *types.FlagTmplData) string {
78+
switch flag.Type {
79+
case types.StringType:
80+
return strconv.Quote(flag.DefaultValue)
81+
default:
82+
return flag.DefaultValue
83+
}
84+
}
85+
86+
func typeString(flagType types.FlagType) string {
87+
switch flagType {
88+
case types.StringType:
89+
return "string"
90+
case types.IntType, types.FloatType:
91+
return "number"
92+
case types.BoolType:
93+
return "boolean"
94+
default:
95+
return ""
96+
}
97+
}
98+
99+
func (g *genImpl) Generate(input types.Input) error {
100+
funcs := template.FuncMap{
101+
"FlagVarName": flagVarName,
102+
"FlagInitParam": flagInitParam,
103+
"FlagAccessFunc": flagAccessFunc,
104+
"SupportImports": supportImports,
105+
"DefaultValueLiteral": defaultValueLiteral,
106+
"TypeString": typeString,
107+
}
108+
td := TmplData{
109+
BaseTmplData: input.BaseData,
110+
}
111+
return generate.GenerateFile(funcs, reactTmpl, &td)
112+
}
113+
114+
// Params are parameters for creating a Generator
115+
type Params struct {
116+
}
117+
118+
// NewGenerator creates a generator for React.
119+
func NewGenerator(params Params) types.Generator {
120+
return &genImpl{}
121+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import {
4+
{{- range $_, $p := SupportImports .Flags}}
5+
{{$p}},
6+
{{- end}}
7+
} from "@openfeature/react-sdk";
8+
{{ range .Flags}}
9+
/**
10+
* {{.Docs}}
11+
*
12+
* **Details:**
13+
* - flag key: `{{ .Name}}`
14+
* - default value: `{{ .DefaultValue}}`
15+
* - type: `{{TypeString .Type}}`
16+
*/
17+
export const use{{FlagVarName .Name}} = (options: Parameters<typeof {{FlagAccessFunc .Type}}>[2]) => {
18+
return {{FlagAccessFunc .Type}}({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
19+
};
20+
{{ end}}

0 commit comments

Comments
 (0)