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 };
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
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()
Comment on lines +26 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Client as the name for the exported package-level variable may cause a naming collision if a feature flag in the manifest is also named client. When transformed via ToPascal, the flag variable would also be named Client. Consider using a more distinct name like OpenFeatureClient to avoid this risk.

// OpenFeatureClient 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 OpenFeatureClient = 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update this reference to match the suggested name change for the exported client variable.

	     return OpenFeatureClient.{{ .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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update this reference to match the suggested name change for the exported client variable.

	     return OpenFeatureClient.{{ .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
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;
Comment on lines +24 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding a client property to the GeneratedClient interface introduces a potential naming collision with any feature flag named client. Since flags are exposed as methods on this interface (e.g., client(...)), having a property and a method with the same name will result in a TypeScript compilation error. Consider using a more specific name such as openFeatureClient.

  /** The underlying OpenFeature client for ad-hoc flag evaluations. */
  readonly openFeatureClient: 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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the property name to match the suggested change in the GeneratedClient interface.

    openFeatureClient: 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
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";

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 };
3 changes: 3 additions & 0 deletions test/csharp-integration/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ static void Main(string[] args)
// Test client retrieval from DI
var client = serviceProvider.GetRequiredService<GeneratedClient>();

// Verify the underlying client is accessible
var underlyingClient = client.Client;

// Also test the traditional factory method
var clientFromFactory = GeneratedClient.CreateClient();

Expand Down
3 changes: 3 additions & 0 deletions test/go-integration/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the test to use the suggested OpenFeatureClient name.

Suggested change
_ = generated.Client
_ = generated.OpenFeatureClient


// 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())
Expand Down
6 changes: 6 additions & 0 deletions test/nodejs-integration/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Comment on lines +65 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the test to use the suggested openFeatureClient property name.

Suggested change
if (!client.client) {
throw new Error('Underlying OpenFeature client not exposed');
}
if (!client.openFeatureClient) {
throw new Error('Underlying OpenFeature client not exposed');
}

console.log('✅ Underlying client accessible');

console.log('🧪 Testing flags...');

// Test each flag
Expand Down
Loading