Skip to content

Commit 3549cf7

Browse files
authored
feat(cli): support custom templates via --template flag (#198)
* feat(cli): Support custom templates via --template flag Signed-off-by: Thomas Wurmitzer <[email protected]> * chore(docs): Add custom templates doc Signed-off-by: Thomas Wurmitzer <[email protected]> * fix: use params.TemplatePath and revert func signature Signed-off-by: Thomas Wurmitzer <[email protected]> * chore: create template section and link to official text/template doc Signed-off-by: Thomas Wurmitzer <[email protected]> --------- Signed-off-by: Thomas Wurmitzer <[email protected]>
1 parent ea5023b commit 3549cf7

34 files changed

+571
-28
lines changed

docs/commands/openfeature_generate.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ openfeature generate [flags]
1111
### Options
1212

1313
```
14-
-h, --help help for generate
15-
-o, --output string Path to where the generated files should be saved
14+
-h, --help help for generate
15+
-o, --output string Path to where the generated files should be saved
16+
-t, --template string Path to a custom template file. If not specified, the default template is used
1617
```
1718

1819
### Options inherited from parent commands

docs/commands/openfeature_generate_csharp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ openfeature generate csharp [flags]
2929
-m, --manifest string Path to the flag manifest (default "flags.json")
3030
--no-input Disable interactive prompts
3131
-o, --output string Path to where the generated files should be saved
32+
-t, --template string Path to a custom template file. If not specified, the default template is used
3233
```
3334

3435
### SEE ALSO

docs/commands/openfeature_generate_go.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ openfeature generate go [flags]
2929
-m, --manifest string Path to the flag manifest (default "flags.json")
3030
--no-input Disable interactive prompts
3131
-o, --output string Path to where the generated files should be saved
32+
-t, --template string Path to a custom template file. If not specified, the default template is used
3233
```
3334

3435
### SEE ALSO

docs/commands/openfeature_generate_java.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ openfeature generate java [flags]
2929
-m, --manifest string Path to the flag manifest (default "flags.json")
3030
--no-input Disable interactive prompts
3131
-o, --output string Path to where the generated files should be saved
32+
-t, --template string Path to a custom template file. If not specified, the default template is used
3233
```
3334

3435
### SEE ALSO

docs/commands/openfeature_generate_nestjs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ openfeature generate nestjs [flags]
2828
-m, --manifest string Path to the flag manifest (default "flags.json")
2929
--no-input Disable interactive prompts
3030
-o, --output string Path to where the generated files should be saved
31+
-t, --template string Path to a custom template file. If not specified, the default template is used
3132
```
3233

3334
### SEE ALSO

docs/commands/openfeature_generate_nodejs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ openfeature generate nodejs [flags]
2828
-m, --manifest string Path to the flag manifest (default "flags.json")
2929
--no-input Disable interactive prompts
3030
-o, --output string Path to where the generated files should be saved
31+
-t, --template string Path to a custom template file. If not specified, the default template is used
3132
```
3233

3334
### SEE ALSO

docs/commands/openfeature_generate_python.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ openfeature generate python [flags]
2828
-m, --manifest string Path to the flag manifest (default "flags.json")
2929
--no-input Disable interactive prompts
3030
-o, --output string Path to where the generated files should be saved
31+
-t, --template string Path to a custom template file. If not specified, the default template is used
3132
```
3233

3334
### SEE ALSO

docs/commands/openfeature_generate_react.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ openfeature generate react [flags]
2828
-m, --manifest string Path to the flag manifest (default "flags.json")
2929
--no-input Disable interactive prompts
3030
-o, --output string Path to where the generated files should be saved
31+
-t, --template string Path to a custom template file. If not specified, the default template is used
3132
```
3233

3334
### SEE ALSO

docs/custom-templates.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Custom Templates
2+
3+
The OpenFeature CLI supports custom templates that allow you to customize the generated code output. This is useful when you need to:
4+
5+
- Modify the structure of generated code to fit your project conventions
6+
- Add custom imports, utilities, or wrappers
7+
- Change naming conventions or code style
8+
- Generate code for frameworks or libraries not yet supported
9+
10+
## Usage
11+
12+
Use the `--template` flag with any generate subcommand:
13+
14+
```bash
15+
openfeature generate go --template ./my-custom-template.tmpl
16+
openfeature generate react --template ./custom-react.tmpl
17+
openfeature generate nodejs --template ./custom-nodejs.tmpl
18+
```
19+
20+
## Getting Started
21+
22+
The easiest way to create a custom template is to start from an existing one:
23+
24+
1. Copy the default template for your target language from the CLI source:
25+
- Go: `internal/generators/golang/golang.tmpl`
26+
- React: `internal/generators/react/react.tmpl`
27+
- Node.js: `internal/generators/nodejs/nodejs.tmpl`
28+
- Python: `internal/generators/python/python.tmpl`
29+
- C#: `internal/generators/csharp/csharp.tmpl`
30+
- Java: `internal/generators/java/java.tmpl`
31+
- NestJS: `internal/generators/nestjs/nestjs.tmpl`
32+
33+
2. Modify the template to suit your needs
34+
35+
3. Use the `--template` flag to generate code with your custom template
36+
37+
## Template Syntax
38+
39+
Custom templates use Go's [text/template](https://pkg.go.dev/text/template) package. Refer to the official documentation for the full syntax, including conditionals, loops, and pipelines.
40+
41+
## Template Data
42+
43+
Templates have access to the following data structure:
44+
45+
```go
46+
type TemplateData struct {
47+
Flagset struct {
48+
Flags []Flag
49+
}
50+
Params struct {
51+
OutputPath string
52+
Custom any // Language-specific parameters
53+
}
54+
}
55+
56+
type Flag struct {
57+
Key string // The flag key (e.g., "enable-feature")
58+
Type FlagType // The flag type (boolean, string, integer, float, object)
59+
Description string // Optional description of the flag
60+
DefaultValue any // The default value for the flag
61+
}
62+
```
63+
64+
### Language-Specific Parameters
65+
66+
Some generators provide additional parameters in `.Params.Custom`:
67+
68+
**Go:**
69+
- `.Params.Custom.GoPackage` - The Go package name
70+
- `.Params.Custom.CLIVersion` - The CLI version used for generation
71+
72+
**C#:**
73+
- `.Params.Custom.Namespace` - The C# namespace
74+
75+
**Java:**
76+
- `.Params.Custom.JavaPackage` - The Java package name
77+
78+
## Template Functions
79+
80+
### Common Functions (Available in All Templates)
81+
82+
These functions are available in all templates:
83+
84+
| Function | Description | Example |
85+
|----------|-------------|---------|
86+
| `ToPascal` | Convert to PascalCase | `{{ .Key \| ToPascal }}``EnableFeature` |
87+
| `ToCamel` | Convert to camelCase | `{{ .Key \| ToCamel }}``enableFeature` |
88+
| `ToKebab` | Convert to kebab-case | `{{ .Key \| ToKebab }}``enable-feature` |
89+
| `ToScreamingKebab` | Convert to SCREAMING-KEBAB-CASE | `{{ .Key \| ToScreamingKebab }}``ENABLE-FEATURE` |
90+
| `ToSnake` | Convert to snake_case | `{{ .Key \| ToSnake }}``enable_feature` |
91+
| `ToScreamingSnake` | Convert to SCREAMING_SNAKE_CASE | `{{ .Key \| ToScreamingSnake }}``ENABLE_FEATURE` |
92+
| `ToUpper` | Convert to UPPERCASE | `{{ .Key \| ToUpper }}``ENABLE-FEATURE` |
93+
| `ToLower` | Convert to lowercase | `{{ .Key \| ToLower }}``enable-feature` |
94+
| `Quote` | Add double quotes | `{{ .Key \| Quote }}``"enable-feature"` |
95+
| `QuoteString` | Quote if string type | `{{ .DefaultValue \| QuoteString }}` |
96+
97+
### Go-Specific Functions
98+
99+
| Function | Description |
100+
|----------|-------------|
101+
| `OpenFeatureType` | Convert flag type to OpenFeature method name (`Boolean`, `String`, `Int`, `Float`, `Object`) |
102+
| `TypeString` | Convert flag type to Go type (`bool`, `string`, `int64`, `float64`, `map[string]any`) |
103+
| `SupportImports` | Generate required imports based on flags |
104+
| `ToMapLiteral` | Convert object value to Go map literal |
105+
106+
### React/Node.js/NestJS-Specific Functions
107+
108+
| Function | Description |
109+
|----------|-------------|
110+
| `OpenFeatureType` | Convert flag type to TypeScript type (`boolean`, `string`, `number`, `object`) |
111+
| `ToJSONString` | Convert value to JSON string |
112+
113+
### Python-Specific Functions
114+
115+
| Function | Description |
116+
|----------|-------------|
117+
| `OpenFeatureType` | Convert flag type to Python type (`bool`, `str`, `int`, `float`, `object`) |
118+
| `TypedGetMethodSync` | Get synchronous getter method name |
119+
| `TypedGetMethodAsync` | Get async getter method name |
120+
| `TypedDetailsMethodSync` | Get synchronous details method name |
121+
| `TypedDetailsMethodAsync` | Get async details method name |
122+
| `PythonBoolLiteral` | Convert boolean to Python literal (`True`/`False`) |
123+
| `ToPythonDict` | Convert object value to Python dict literal |
124+
125+
### C#-Specific Functions
126+
127+
| Function | Description |
128+
|----------|-------------|
129+
| `OpenFeatureType` | Convert flag type to C# type (`bool`, `string`, `int`, `double`, `object`) |
130+
| `FormatDefaultValue` | Format default value for C# |
131+
| `ToCSharpDict` | Convert object value to C# dictionary literal |
132+
133+
### Java-Specific Functions
134+
135+
| Function | Description |
136+
|----------|-------------|
137+
| `OpenFeatureType` | Convert flag type to Java type (`Boolean`, `String`, `Integer`, `Double`, `Object`) |
138+
| `FormatDefaultValue` | Format default value for Java |
139+
| `ToMapLiteral` | Convert object value to Java Map literal |
140+
141+
## Example: Simple Go Template
142+
143+
Here's a minimal example of a custom Go template:
144+
145+
```go
146+
// Code generated by OpenFeature CLI with custom template
147+
package {{ .Params.Custom.GoPackage }}
148+
149+
import (
150+
"context"
151+
"github.com/open-feature/go-sdk/openfeature"
152+
)
153+
154+
var client = openfeature.NewDefaultClient()
155+
156+
{{- range .Flagset.Flags }}
157+
// Get{{ .Key | ToPascal }} returns the value of the "{{ .Key }}" flag.
158+
// {{ if .Description }}{{ .Description }}{{ end }}
159+
func Get{{ .Key | ToPascal }}(ctx context.Context, evalCtx openfeature.EvaluationContext) {{ .Type | TypeString }} {
160+
return client.{{ .Type | OpenFeatureType }}(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
161+
}
162+
{{- end }}
163+
```
164+
165+
## Example: Custom React Template
166+
167+
Here's an example that generates simple hooks without suspense:
168+
169+
```typescript
170+
import { useFlag } from "@openfeature/react-sdk";
171+
172+
{{ range .Flagset.Flags }}
173+
/**
174+
* {{ if .Description }}{{ .Description }}{{ else }}Feature flag{{ end }}
175+
* Default: {{ .DefaultValue }}
176+
*/
177+
export const use{{ .Key | ToPascal }} = () => {
178+
return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }});
179+
};
180+
{{ end }}
181+
```
182+
183+
## Tips
184+
185+
1. **Test incrementally**: Make small changes and test the output frequently
186+
2. **Use `--output` flag**: Direct output to a test directory while developing your template
187+
3. **Preserve formatting**: The generators apply language-specific formatters after template execution (e.g., `gofmt` for Go)
188+
4. **Handle edge cases**: Consider empty flag lists, missing descriptions, and different flag types
189+
5. **Check the source**: Review the default templates in the CLI source for comprehensive examples

internal/cmd/generate.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ func getGenerateNodeJSCmd() *cobra.Command {
8080
RunE: func(cmd *cobra.Command, args []string) error {
8181
manifestPath := config.GetManifestPath(cmd)
8282
outputPath := config.GetOutputPath(cmd)
83+
templatePath := config.GetTemplatePath(cmd)
8384

8485
logger.Default.GenerationStarted("Node.js")
8586

8687
params := generators.Params[nodejs.Params]{
87-
OutputPath: outputPath,
88-
Custom: nodejs.Params{},
88+
OutputPath: outputPath,
89+
TemplatePath: templatePath,
90+
Custom: nodejs.Params{},
8991
}
9092
flagset, err := manifest.LoadFlagSet(manifestPath)
9193
if err != nil {
@@ -124,12 +126,14 @@ func getGenerateReactCmd() *cobra.Command {
124126
RunE: func(cmd *cobra.Command, args []string) error {
125127
manifestPath := config.GetManifestPath(cmd)
126128
outputPath := config.GetOutputPath(cmd)
129+
templatePath := config.GetTemplatePath(cmd)
127130

128131
logger.Default.GenerationStarted("React")
129132

130133
params := generators.Params[react.Params]{
131-
OutputPath: outputPath,
132-
Custom: react.Params{},
134+
OutputPath: outputPath,
135+
TemplatePath: templatePath,
136+
Custom: react.Params{},
133137
}
134138
flagset, err := manifest.LoadFlagSet(manifestPath)
135139
if err != nil {
@@ -168,6 +172,7 @@ func GetGenerateNestJsCmd() *cobra.Command {
168172
RunE: func(cmd *cobra.Command, args []string) error {
169173
manifestPath := config.GetManifestPath(cmd)
170174
outputPath := config.GetOutputPath(cmd)
175+
templatePath := config.GetTemplatePath(cmd)
171176

172177
logger.Default.GenerationStarted("NestJS")
173178

@@ -177,8 +182,9 @@ func GetGenerateNestJsCmd() *cobra.Command {
177182
}
178183

179184
nestjsParams := generators.Params[nestjs.Params]{
180-
OutputPath: outputPath,
181-
Custom: nestjs.Params{},
185+
OutputPath: outputPath,
186+
TemplatePath: templatePath,
187+
Custom: nestjs.Params{},
182188
}
183189
nestjsGenerator := nestjs.NewGenerator(flagset)
184190
logger.Default.Debug("Executing NestJS generator")
@@ -223,11 +229,13 @@ func getGenerateCSharpCmd() *cobra.Command {
223229
namespace := config.GetCSharpNamespace(cmd)
224230
manifestPath := config.GetManifestPath(cmd)
225231
outputPath := config.GetOutputPath(cmd)
232+
templatePath := config.GetTemplatePath(cmd)
226233

227234
logger.Default.GenerationStarted("C#")
228235

229236
params := generators.Params[csharp.Params]{
230-
OutputPath: outputPath,
237+
OutputPath: outputPath,
238+
TemplatePath: templatePath,
231239
Custom: csharp.Params{
232240
Namespace: namespace,
233241
},
@@ -273,11 +281,13 @@ func getGenerateJavaCmd() *cobra.Command {
273281
manifestPath := config.GetManifestPath(cmd)
274282
javaPackageName := config.GetJavaPackageName(cmd)
275283
outputPath := config.GetOutputPath(cmd)
284+
templatePath := config.GetTemplatePath(cmd)
276285

277286
logger.Default.GenerationStarted("Java")
278287

279288
params := generators.Params[java.Params]{
280-
OutputPath: outputPath,
289+
OutputPath: outputPath,
290+
TemplatePath: templatePath,
281291
Custom: java.Params{
282292
JavaPackage: javaPackageName,
283293
},
@@ -324,11 +334,13 @@ func getGenerateGoCmd() *cobra.Command {
324334
goPackageName := config.GetGoPackageName(cmd)
325335
manifestPath := config.GetManifestPath(cmd)
326336
outputPath := config.GetOutputPath(cmd)
337+
templatePath := config.GetTemplatePath(cmd)
327338

328339
logger.Default.GenerationStarted("Go")
329340

330341
params := generators.Params[golang.Params]{
331-
OutputPath: outputPath,
342+
OutputPath: outputPath,
343+
TemplatePath: templatePath,
332344
Custom: golang.Params{
333345
GoPackage: goPackageName,
334346
CLIVersion: Version,
@@ -372,12 +384,14 @@ func getGeneratePythonCmd() *cobra.Command {
372384
RunE: func(cmd *cobra.Command, args []string) error {
373385
manifestPath := config.GetManifestPath(cmd)
374386
outputPath := config.GetOutputPath(cmd)
387+
templatePath := config.GetTemplatePath(cmd)
375388

376389
logger.Default.GenerationStarted("Python")
377390

378391
params := generators.Params[python.Params]{
379-
OutputPath: outputPath,
380-
Custom: python.Params{},
392+
OutputPath: outputPath,
393+
TemplatePath: templatePath,
394+
Custom: python.Params{},
381395
}
382396
flagset, err := manifest.LoadFlagSet(manifestPath)
383397
if err != nil {

0 commit comments

Comments
 (0)