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 };
21 changes: 20 additions & 1 deletion internal/generators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,23 @@ 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` |

For example, a flag key `"client"` in a Go manifest would transform to `Client` (via `ToPascal`), colliding with the exported `var Client` that the Go generator places in every generated file. The flag will be skipped and the following warning emitted:

```
Flag "client" transforms to "Client" which is a reserved symbol in the Go generator. This flag will be excluded from the generated output.
```

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

When adding a new generator, document its reserved names in the table above and enforce them in the generator's `Generate()` method using the same pattern.
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
23 changes: 23 additions & 0 deletions internal/generators/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"strings"
"text/template"

"github.com/iancoleman/strcase"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"github.com/open-feature/cli/internal/logger"
"golang.org/x/tools/imports"
)

Expand Down Expand Up @@ -128,7 +130,28 @@ 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 {
filtered := &flagset.Flagset{}
for _, flag := range g.Flagset.Flags {
transformed := strcase.ToCamel(flag.Key)
if reservedNames[transformed] {
logger.Default.Warning(fmt.Sprintf(
"Flag %q transforms to %q which is a reserved symbol in the Go generator. This flag will be excluded from the generated output.",
flag.Key, transformed,
))
continue
}
filtered.Flags = append(filtered.Flags, flag)
}
g.Flagset = filtered

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
24 changes: 24 additions & 0 deletions internal/generators/nodejs/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package nodejs
import (
_ "embed"
"encoding/json"
"fmt"
"text/template"

"github.com/iancoleman/strcase"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"github.com/open-feature/cli/internal/logger"
)

type NodejsGenerator struct {
Expand Down Expand Up @@ -43,7 +46,28 @@ 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 {
filtered := &flagset.Flagset{}
for _, flag := range g.Flagset.Flags {
transformed := strcase.ToLowerCamel(flag.Key)
if reservedNames[transformed] {
logger.Default.Warning(fmt.Sprintf(
"Flag %q transforms to %q which is a reserved symbol in the Node.js generator. This flag will be excluded from the generated output.",
flag.Key, transformed,
))
continue
}
filtered.Flags = append(filtered.Flags, flag)
}
g.Flagset = filtered

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
Loading
Loading