Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/cmd/testdata/success_angular.golden
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ export type FlagKey = (typeof FlagKeys)[keyof typeof FlagKeys];
export class GeneratedFeatureFlagService {
private readonly flagService = inject(FeatureFlagService);

/**
* The underlying FeatureFlagService for ad-hoc flag evaluations
* beyond what's defined in the manifest.
*/
get client(): FeatureFlagService {
return this.flagService;
}


/**
* Get evaluation details for the `discountPercentage` flag.
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/testdata/success_csharp.golden
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ namespace TestNamespace
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}

/// <summary>
/// The underlying OpenFeature client for ad-hoc flag evaluations.
/// </summary>
public IFeatureClient Client => _client;
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
Expand Down
24 changes: 13 additions & 11 deletions internal/cmd/testdata/success_go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ type (
evaluationDetails[T any] func(context.Context, openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[T], error)
)

var client = openfeature.NewDefaultClient()
// Client is the underlying OpenFeature client used for flag evaluations.
// It can be used directly for ad-hoc flag evaluations beyond what is defined in the manifest.
var Client = openfeature.NewDefaultClient()

// DiscountPercentage returns the value of the "discountPercentage" feature flag.
// Discount percentage applied to purchases.
Expand All @@ -41,10 +43,10 @@ var DiscountPercentage = struct {
}{
Stringer: stringer("discountPercentage"),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) float64 {
return client.Float(ctx, "discountPercentage", 0.15, evalCtx)
return Client.Float(ctx, "discountPercentage", 0.15, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[float64], error) {
return client.FloatValueDetails(ctx, "discountPercentage", 0.15, evalCtx)
return Client.FloatValueDetails(ctx, "discountPercentage", 0.15, evalCtx)
},
}

Expand All @@ -63,10 +65,10 @@ var EnableFeatureA = struct {
}{
Stringer: stringer("enableFeatureA"),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) bool {
return client.Boolean(ctx, "enableFeatureA", false, evalCtx)
return Client.Boolean(ctx, "enableFeatureA", false, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[bool], error) {
return client.BooleanValueDetails(ctx, "enableFeatureA", false, evalCtx)
return Client.BooleanValueDetails(ctx, "enableFeatureA", false, evalCtx)
},
}

Expand All @@ -85,10 +87,10 @@ var GreetingMessage = struct {
}{
Stringer: stringer("greetingMessage"),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) string {
return client.String(ctx, "greetingMessage", "Hello there!", evalCtx)
return Client.String(ctx, "greetingMessage", "Hello there!", evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[string], error) {
return client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx)
return Client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx)
},
}

Expand All @@ -107,10 +109,10 @@ var ThemeCustomization = struct {
}{
Stringer: stringer("themeCustomization"),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) any {
return client.Object(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
return Client.Object(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[any], error) {
return client.ObjectValueDetails(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
return Client.ObjectValueDetails(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
},
}

Expand All @@ -129,9 +131,9 @@ var UsernameMaxLength = struct {
}{
Stringer: stringer("usernameMaxLength"),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) int64 {
return client.Int(ctx, "usernameMaxLength", 50, evalCtx)
return Client.Int(ctx, "usernameMaxLength", 50, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[int64], error) {
return client.IntValueDetails(ctx, "usernameMaxLength", 50, evalCtx)
return Client.IntValueDetails(ctx, "usernameMaxLength", 50, evalCtx)
},
}
11 changes: 11 additions & 0 deletions internal/cmd/testdata/success_java.golden
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ public final class OpenFeature {
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<Integer> usernameMaxLengthDetails(EvaluationContext ctx);

/**
* Returns the underlying OpenFeature client for ad-hoc flag evaluations.
* @return The OpenFeature Client instance
*/
Client getOpenFeatureClient();
}

private static final class OpenFeatureGeneratedClient implements GeneratedClient {
Expand Down Expand Up @@ -180,6 +186,11 @@ public final class OpenFeature {
public FlagEvaluationDetails<Integer> usernameMaxLengthDetails(EvaluationContext ctx) {
return client.getIntegerDetails("usernameMaxLength", 50, ctx);
}

@Override
public Client getOpenFeatureClient() {
return client;
}
}

public static GeneratedClient getClient() {
Expand Down
4 changes: 4 additions & 0 deletions internal/cmd/testdata/success_nodejs.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
JsonValue,
} from "@openfeature/server-sdk";
import type {
Client,
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
Expand All @@ -26,6 +27,8 @@ export const FlagKeys = {
} as const;

export interface GeneratedClient {
/** The underlying OpenFeature client for ad-hoc flag evaluations. */
readonly client: Client;
/**
* Discount percentage applied to purchases.
*
Expand Down Expand Up @@ -206,6 +209,7 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
const client = domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context)

return {
client,
discountPercentage: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number> => {
return client.getNumberValue("discountPercentage", 0.15, context, options);
},
Expand Down
7 changes: 7 additions & 0 deletions internal/cmd/testdata/success_react.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type FlagQuery,
useFlag,
useSuspenseFlag,
useOpenFeatureClient,
JsonValue
} from "@openfeature/react-sdk";

Expand Down Expand Up @@ -158,3 +159,9 @@ export const useUsernameMaxLength = (options?: ReactFlagEvaluationOptions): Flag
export const useSuspenseUsernameMaxLength = (options?: ReactFlagEvaluationNoSuspenseOptions): FlagQuery<number> => {
return useSuspenseFlag("usernameMaxLength", 50, options);
};

/**
* Re-exported hook for accessing the underlying OpenFeature client
* for ad-hoc flag evaluations beyond what's defined in the manifest.
*/
export { useOpenFeatureClient };
24 changes: 23 additions & 1 deletion internal/generators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,26 @@ To add a new generator, follow these steps:
6. Write tests for your generator to ensure it works as expected.
7. Update the documentation to include information about your new generator.

We appreciate your contributions and look forward to seeing your new generators!
We appreciate your contributions and look forward to seeing your new generators!

## Reserved Keywords

Each generator reserves certain symbol names that it exports in the generated output. If a flag key transforms to a reserved name, that flag will be **excluded** from the generated output and a warning will be printed.

| Generator | Reserved names | Transform applied |
|-----------|------------------------|------------------------------|
| Go | `Client` | `ToPascal` |
| Node.js | `client` | `ToCamel` |
| React | `useOpenFeatureClient` | `use` + `ToPascal` |

A generator only reserves a name when a flag key could actually transform into a symbol it already emits. For example, a flag key `"client"` in a Go manifest transforms to `Client` (via `ToPascal`), colliding with the exported `var Client` that the Go generator places in every generated file. Likewise, a flag key `"openFeatureClient"` for the React generator produces the hook `useOpenFeatureClient`, colliding with the re-exported hook of the same name. The flag is skipped and a warning is emitted:

```
Flag "client" transforms to "Client" which is a reserved symbol in the Go generator. This flag will be excluded from the generated output.
```
Comment thread
kriscoleman marked this conversation as resolved.

To avoid this, rename any flags whose transformed name matches a reserved symbol.

Generators whose exposed client symbol cannot collide with a flag-generated member do not reserve a name. For instance, C# exposes a `Client` property while flags generate `<Pascal>Async`/`<Pascal>DetailsAsync` methods, and Java exposes a no-arg `getOpenFeatureClient()` that a flag method would only ever overload — so neither needs an entry above.

When adding a new generator, use the shared `generators.FilterReservedFlags` helper in the generator's `Generate()` method, and add a row here only if a flag key can genuinely transform into a symbol the generator exports.
8 changes: 8 additions & 0 deletions internal/generators/angular/angular.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export type FlagKey = (typeof FlagKeys)[keyof typeof FlagKeys];
export class GeneratedFeatureFlagService {
private readonly flagService = inject(FeatureFlagService);

/**
* The underlying FeatureFlagService for ad-hoc flag evaluations
* beyond what's defined in the manifest.
*/
get client(): FeatureFlagService {
return this.flagService;
}

{{ range .Flagset.Flags }}
/**
* Get evaluation details for the `{{ .Key }}` flag.
Expand Down
5 changes: 5 additions & 0 deletions internal/generators/csharp/csharp.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
_client = client ?? throw new ArgumentNullException(nameof(client));
}

/// <summary>
/// The underlying OpenFeature client for ad-hoc flag evaluations.
/// </summary>
public IFeatureClient Client => _client;

{{- range .Flagset.Flags }}
/// <summary>
/// {{ if .Description }}{{ .Description }}{{ else }}Feature flag{{ end }}
Expand Down
10 changes: 10 additions & 0 deletions internal/generators/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"text/template"

"github.com/iancoleman/strcase"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"golang.org/x/tools/imports"
Expand Down Expand Up @@ -128,7 +129,16 @@ func formatNestedValue(value any) string {
}
}

// reservedNames are symbols exported by the Go generator itself. Flag keys
// that transform (via ToPascal) to one of these names will be excluded from
// the generated output and a warning will be emitted.
var reservedNames = map[string]bool{
"Client": true,
}

func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
g.Flagset = generators.FilterReservedFlags(g.Flagset, "Go", reservedNames, strcase.ToCamel)

funcs := template.FuncMap{
"SupportImports": supportImports,
"OpenFeatureType": openFeatureType,
Expand Down
8 changes: 5 additions & 3 deletions internal/generators/golang/golang.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ type (
evaluationDetails[T any] func(context.Context, openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[T], error)
)

var client = openfeature.NewDefaultClient()
// Client is the underlying OpenFeature client used for flag evaluations.
// It can be used directly for ad-hoc flag evaluations beyond what is defined in the manifest.
var Client = openfeature.NewDefaultClient()

{{- range .Flagset.Flags }}
// {{ .Key | ToPascal }} returns the value of the "{{ .Key }}" feature flag.
Expand All @@ -41,10 +43,10 @@ var {{ .Key | ToPascal }} = struct {
}{
Stringer: stringer({{ .Key | Quote }}),
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) {{- if eq (.Type | OpenFeatureType) "Object"}}any{{- else}}{{ .Type | TypeString }}{{- end}} {
return client.{{ .Type | OpenFeatureType }}(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
return Client.{{ .Type | OpenFeatureType }}(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.GenericEvaluationDetails[{{- if eq (.Type | OpenFeatureType) "Object"}}any{{- else}}{{ .Type | TypeString }}{{- end}}], error){
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
return Client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
},
}
{{- end}}
11 changes: 11 additions & 0 deletions internal/generators/java/java.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public final class OpenFeature {
FlagEvaluationDetails<{{ .Type | OpenFeatureType }}> {{ .Key | ToCamel }}Details(EvaluationContext ctx);

{{- end }}

/**
* Returns the underlying OpenFeature client for ad-hoc flag evaluations.
* @return The OpenFeature Client instance
*/
Client getOpenFeatureClient();
}

private static final class OpenFeatureGeneratedClient implements GeneratedClient {
Expand All @@ -65,6 +71,11 @@ public final class OpenFeature {
}

{{- end }}

@Override
public Client getOpenFeatureClient() {
return client;
}
}

public static GeneratedClient getClient() {
Expand Down
10 changes: 10 additions & 0 deletions internal/generators/nodejs/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"text/template"

"github.com/iancoleman/strcase"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
Expand Down Expand Up @@ -43,7 +44,16 @@ func toJSONString(value any) string {
return string(bytes)
}

// reservedNames are symbols exported by the Node.js generator itself. Flag
// keys that transform (via ToCamel) to one of these names will be excluded
// from the generated output and a warning will be emitted.
var reservedNames = map[string]bool{
"client": true,
}

func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
g.Flagset = generators.FilterReservedFlags(g.Flagset, "Node.js", reservedNames, strcase.ToLowerCamel)

funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
Expand Down
4 changes: 4 additions & 0 deletions internal/generators/nodejs/nodejs.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
JsonValue,
} from "@openfeature/server-sdk";
import type {
Client,
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
Expand All @@ -20,6 +21,8 @@ export const FlagKeys = {
} as const;

export interface GeneratedClient {
/** The underlying OpenFeature client for ad-hoc flag evaluations. */
readonly client: Client;
{{- range .Flagset.Flags }}
/**
* {{ if .Description }}{{ .Description }}{{ else }}Feature flag{{ end }}
Expand Down Expand Up @@ -82,6 +85,7 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
const client = domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context)

return {
client,
{{- range .Flagset.Flags }}
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> => {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
Expand Down
12 changes: 12 additions & 0 deletions internal/generators/react/react.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"text/template"

"github.com/iancoleman/strcase"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
Expand Down Expand Up @@ -43,7 +44,18 @@ func toJSONString(value any) string {
return string(bytes)
}

// reservedNames are symbols exported by the React generator itself. Flag keys
// whose generated hook name (use + ToPascal) matches one of these will be
// excluded from the generated output and a warning will be emitted.
var reservedNames = map[string]bool{
"useOpenFeatureClient": true,
}

func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
g.Flagset = generators.FilterReservedFlags(g.Flagset, "React", reservedNames, func(key string) string {
return "use" + strcase.ToCamel(key)
})

funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
Expand Down
8 changes: 7 additions & 1 deletion internal/generators/react/react.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type FlagQuery,
useFlag,
useSuspenseFlag,
useOpenFeatureClient,
JsonValue
} from "@openfeature/react-sdk";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -44,4 +45,9 @@ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions):
export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions): FlagQuery<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> => {
return useSuspenseFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
};
{{ end}}
{{ end}}
/**
* Re-exported hook for accessing the underlying OpenFeature client
* for ad-hoc flag evaluations beyond what's defined in the manifest.
*/
export { useOpenFeatureClient };
Loading
Loading