Skip to content

Commit

Permalink
feat: add basic react support (#31)
Browse files Browse the repository at this point in the history
## 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]>
  • Loading branch information
beeme1mr authored Oct 15, 2024
1 parent 850c694 commit 757ab66
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 2 deletions.
2 changes: 2 additions & 0 deletions cmd/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package generate

import (
"codegen/cmd/generate/golang"
"codegen/cmd/generate/react"
"codegen/internal/flagkeys"

"github.com/spf13/cobra"
Expand All @@ -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.")
Expand Down
24 changes: 24 additions & 0 deletions cmd/generate/react/react.go
Original file line number Diff line number Diff line change
@@ -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() {
}
4 changes: 2 additions & 2 deletions internal/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
121 changes: 121 additions & 0 deletions internal/generate/plugins/react/react.go
Original file line number Diff line number Diff line change
@@ -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{}
}
20 changes: 20 additions & 0 deletions internal/generate/plugins/react/react.tmpl
Original file line number Diff line number Diff line change
@@ -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<typeof {{FlagAccessFunc .Type}}>[2]) => {
return {{FlagAccessFunc .Type}}({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
};
{{ end}}

0 comments on commit 757ab66

Please sign in to comment.