diff --git a/internal/cmd/testdata/success_angular.golden b/internal/cmd/testdata/success_angular.golden
index 3c9b471c..71d5ea62 100644
--- a/internal/cmd/testdata/success_angular.golden
+++ b/internal/cmd/testdata/success_angular.golden
@@ -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.
diff --git a/internal/cmd/testdata/success_csharp.golden b/internal/cmd/testdata/success_csharp.golden
index a77ce093..a3214005 100644
--- a/internal/cmd/testdata/success_csharp.golden
+++ b/internal/cmd/testdata/success_csharp.golden
@@ -75,6 +75,11 @@ namespace TestNamespace
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
+
+ ///
+ /// The underlying OpenFeature client for ad-hoc flag evaluations.
+ ///
+ public IFeatureClient Client => _client;
///
/// Discount percentage applied to purchases.
///
diff --git a/internal/cmd/testdata/success_go.golden b/internal/cmd/testdata/success_go.golden
index 64861abe..92da0ba2 100644
--- a/internal/cmd/testdata/success_go.golden
+++ b/internal/cmd/testdata/success_go.golden
@@ -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.
@@ -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)
},
}
@@ -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)
},
}
@@ -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)
},
}
@@ -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)
},
}
@@ -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)
},
}
diff --git a/internal/cmd/testdata/success_java.golden b/internal/cmd/testdata/success_java.golden
index de285270..8749455f 100644
--- a/internal/cmd/testdata/success_java.golden
+++ b/internal/cmd/testdata/success_java.golden
@@ -125,6 +125,12 @@ public final class OpenFeature {
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails 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 {
@@ -180,6 +186,11 @@ public final class OpenFeature {
public FlagEvaluationDetails usernameMaxLengthDetails(EvaluationContext ctx) {
return client.getIntegerDetails("usernameMaxLength", 50, ctx);
}
+
+ @Override
+ public Client getOpenFeatureClient() {
+ return client;
+ }
}
public static GeneratedClient getClient() {
diff --git a/internal/cmd/testdata/success_nodejs.golden b/internal/cmd/testdata/success_nodejs.golden
index f1f7d4a6..6107ca4f 100644
--- a/internal/cmd/testdata/success_nodejs.golden
+++ b/internal/cmd/testdata/success_nodejs.golden
@@ -6,6 +6,7 @@ import {
JsonValue,
} from "@openfeature/server-sdk";
import type {
+ Client,
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
@@ -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.
*
@@ -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 => {
return client.getNumberValue("discountPercentage", 0.15, context, options);
},
diff --git a/internal/cmd/testdata/success_react.golden b/internal/cmd/testdata/success_react.golden
index 17a219ac..444040f2 100644
--- a/internal/cmd/testdata/success_react.golden
+++ b/internal/cmd/testdata/success_react.golden
@@ -6,6 +6,7 @@ import {
type FlagQuery,
useFlag,
useSuspenseFlag,
+ useOpenFeatureClient,
JsonValue
} from "@openfeature/react-sdk";
@@ -158,3 +159,9 @@ export const useUsernameMaxLength = (options?: ReactFlagEvaluationOptions): Flag
export const useSuspenseUsernameMaxLength = (options?: ReactFlagEvaluationNoSuspenseOptions): FlagQuery => {
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 };
diff --git a/internal/generators/README.md b/internal/generators/README.md
index a3491976..f12e832c 100644
--- a/internal/generators/README.md
+++ b/internal/generators/README.md
@@ -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!
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/internal/generators/angular/angular.tmpl b/internal/generators/angular/angular.tmpl
index 6db1c753..30a274ae 100644
--- a/internal/generators/angular/angular.tmpl
+++ b/internal/generators/angular/angular.tmpl
@@ -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.
diff --git a/internal/generators/csharp/csharp.tmpl b/internal/generators/csharp/csharp.tmpl
index f09bf644..1e492c5b 100644
--- a/internal/generators/csharp/csharp.tmpl
+++ b/internal/generators/csharp/csharp.tmpl
@@ -70,6 +70,11 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
_client = client ?? throw new ArgumentNullException(nameof(client));
}
+ ///
+ /// The underlying OpenFeature client for ad-hoc flag evaluations.
+ ///
+ public IFeatureClient Client => _client;
+
{{- range .Flagset.Flags }}
///
/// {{ if .Description }}{{ .Description }}{{ else }}Feature flag{{ end }}
diff --git a/internal/generators/golang/golang.go b/internal/generators/golang/golang.go
index c45d0a81..898d9812 100644
--- a/internal/generators/golang/golang.go
+++ b/internal/generators/golang/golang.go
@@ -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"
)
@@ -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,
diff --git a/internal/generators/golang/golang.tmpl b/internal/generators/golang/golang.tmpl
index c8898bc3..48653ce6 100644
--- a/internal/generators/golang/golang.tmpl
+++ b/internal/generators/golang/golang.tmpl
@@ -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.
@@ -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}}
diff --git a/internal/generators/java/java.tmpl b/internal/generators/java/java.tmpl
index 3e3002cc..4b024575 100644
--- a/internal/generators/java/java.tmpl
+++ b/internal/generators/java/java.tmpl
@@ -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 {
@@ -65,6 +71,11 @@ public final class OpenFeature {
}
{{- end }}
+
+ @Override
+ public Client getOpenFeatureClient() {
+ return client;
+ }
}
public static GeneratedClient getClient() {
diff --git a/internal/generators/nodejs/nodejs.go b/internal/generators/nodejs/nodejs.go
index dc6469af..3ba049e5 100644
--- a/internal/generators/nodejs/nodejs.go
+++ b/internal/generators/nodejs/nodejs.go
@@ -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 {
@@ -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,
diff --git a/internal/generators/nodejs/nodejs.tmpl b/internal/generators/nodejs/nodejs.tmpl
index dd4d3c79..5c4ef83b 100644
--- a/internal/generators/nodejs/nodejs.tmpl
+++ b/internal/generators/nodejs/nodejs.tmpl
@@ -6,6 +6,7 @@ import {
JsonValue,
} from "@openfeature/server-sdk";
import type {
+ Client,
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
@@ -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 }}
@@ -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);
diff --git a/internal/generators/react/react.tmpl b/internal/generators/react/react.tmpl
index 95401a02..6b6aaf50 100644
--- a/internal/generators/react/react.tmpl
+++ b/internal/generators/react/react.tmpl
@@ -6,6 +6,7 @@ import {
type FlagQuery,
useFlag,
useSuspenseFlag,
+ useOpenFeatureClient,
JsonValue
} from "@openfeature/react-sdk";
@@ -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}}
\ No newline at end of file
+{{ end}}
+/**
+ * Re-exported hook for accessing the underlying OpenFeature client
+ * for ad-hoc flag evaluations beyond what's defined in the manifest.
+ */
+export { useOpenFeatureClient };
diff --git a/test/csharp-integration/Program.cs b/test/csharp-integration/Program.cs
index 3c3230b6..3c86f9f8 100644
--- a/test/csharp-integration/Program.cs
+++ b/test/csharp-integration/Program.cs
@@ -25,6 +25,9 @@ static void Main(string[] args)
// Test client retrieval from DI
var client = serviceProvider.GetRequiredService();
+ // Verify the underlying client is accessible
+ var underlyingClient = client.Client;
+
// Also test the traditional factory method
var clientFromFactory = GeneratedClient.CreateClient();
diff --git a/test/go-integration/test.go b/test/go-integration/test.go
index 0a8ebeac..455361a1 100644
--- a/test/go-integration/test.go
+++ b/test/go-integration/test.go
@@ -105,6 +105,9 @@ func run() error {
}
fmt.Printf("themeCustomization: %v\n", themeCustomization)
+ // Verify the underlying client is accessible for ad-hoc evaluations
+ _ = generated.Client
+
// Test the String() method functionality for all flags
fmt.Printf("enableFeatureA flag key: %s\n", generated.EnableFeatureA.String())
fmt.Printf("discountPercentage flag key: %s\n", generated.DiscountPercentage.String())
diff --git a/test/nodejs-integration/test.ts b/test/nodejs-integration/test.ts
index 735b57a3..31c628e2 100644
--- a/test/nodejs-integration/test.ts
+++ b/test/nodejs-integration/test.ts
@@ -61,6 +61,12 @@ async function main() {
const { getGeneratedClient } = await import(clientPath);
const client = getGeneratedClient();
+ // Verify the underlying client is accessible
+ if (!client.client) {
+ throw new Error('Underlying OpenFeature client not exposed');
+ }
+ console.log('✅ Underlying client accessible');
+
console.log('🧪 Testing flags...');
// Test each flag