diff --git a/go.sum b/go.sum index 88c7307..438351b 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -dagger.io/dagger v0.18.9 h1:IXZhlGm893LuqYFpo6VHtaCAEP6Qz0VjMhLvyKQVl1Y= -dagger.io/dagger v0.18.9/go.mod h1:e6Y+sAPWh04pHvBf4s3sSiOe1QMoCEcccmMv898RnZA= dagger.io/dagger v0.18.10 h1:Ibyz5LqxjjEHfLMlaU9PJ3xt3ju7p29RWy0lVfvSNU0= dagger.io/dagger v0.18.10/go.mod h1:VSj+2HMd/EnaCVt7gTY70p8LBW+oQDYjA1XTadr8vBE= -github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gEsrg= -github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg= github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo= github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10= github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= @@ -108,8 +104,6 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= -github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -210,15 +204,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -246,8 +236,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -259,8 +247,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1: google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/internal/cmd/testdata/success_csharp.golden b/internal/cmd/testdata/success_csharp.golden index 28b6de1..352a1a7 100644 --- a/internal/cmd/testdata/success_csharp.golden +++ b/internal/cmd/testdata/success_csharp.golden @@ -153,6 +153,38 @@ namespace TestNamespace return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options); } + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The flag value + public async Task ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } + + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The evaluation details containing the flag value and metadata + public async Task> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } + /// /// Maximum allowed length for usernames. /// diff --git a/internal/cmd/testdata/success_go.golden b/internal/cmd/testdata/success_go.golden index 5c02d02..9293c21 100644 --- a/internal/cmd/testdata/success_go.golden +++ b/internal/cmd/testdata/success_go.golden @@ -14,6 +14,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error) type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error) type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error) +type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error) +type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error) var client openfeature.IClient = nil // Discount percentage applied to purchases. @@ -67,6 +69,23 @@ var GreetingMessage = struct { return client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx) }, } +// Allows customization of theme colors. +var ThemeCustomization = struct { + // Value returns the value of the flag ThemeCustomization, + // as well as the evaluation error, if present. + Value ObjectProvider + + // ValueWithDetails returns the value of the flag ThemeCustomization, + // the evaluation error, if any, and the evaluation details. + ValueWithDetails ObjectProviderDetails +}{ + Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error) { + return client.ObjectValue(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx) + }, + ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error){ + return client.ObjectValueDetails(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx) + }, +} // Maximum allowed length for usernames. var UsernameMaxLength = struct { // Value returns the value of the flag UsernameMaxLength, diff --git a/internal/cmd/testdata/success_java.golden b/internal/cmd/testdata/success_java.golden index 7385d66..e8ce927 100644 --- a/internal/cmd/testdata/success_java.golden +++ b/internal/cmd/testdata/success_java.golden @@ -71,7 +71,27 @@ public final class OpenFeature { * Returns the evaluation details containing the flag value and metadata */ FlagEvaluationDetails greetingMessageDetails(EvaluationContext ctx); + + /** + * Allows customization of theme colors. + * Details: + * - Flag key: themeCustomization + * - Type: Object + * - Default value: Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d") + * Returns the flag value + */ + Object themeCustomization(EvaluationContext ctx); + /** + * Allows customization of theme colors. + * Details: + * - Flag key: themeCustomization + * - Type: Object + * - Default value: Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d") + * Returns the evaluation details containing the flag value and metadata + */ + FlagEvaluationDetails themeCustomizationDetails(EvaluationContext ctx); + /** * Maximum allowed length for usernames. * Details: @@ -131,7 +151,17 @@ public final class OpenFeature { public FlagEvaluationDetails greetingMessageDetails(EvaluationContext ctx) { return client.getStringDetails("greetingMessage", "Hello there!", ctx); } + + @Override + public Object themeCustomization(EvaluationContext ctx) { + return client.getObjectValue("themeCustomization", Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d"), ctx); + } + @Override + public FlagEvaluationDetails themeCustomizationDetails(EvaluationContext ctx) { + return client.getObjectDetails("themeCustomization", Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d"), ctx); + } + @Override public Integer usernameMaxLength(EvaluationContext ctx) { return client.getIntegerValue("usernameMaxLength", 50, ctx); diff --git a/internal/cmd/testdata/success_nestjs.golden b/internal/cmd/testdata/success_nestjs.golden index ff482ec..1a4b4ad 100644 --- a/internal/cmd/testdata/success_nestjs.golden +++ b/internal/cmd/testdata/success_nestjs.golden @@ -8,8 +8,9 @@ import type { EvaluationContext, EvaluationDetails, OpenFeatureModuleOptions, + JsonValue } from "@openfeature/nestjs-sdk"; -import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag } from "@openfeature/nestjs-sdk"; +import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk"; import type { GeneratedClient } from "./openfeature"; import { getGeneratedClient } from "./openfeature"; @@ -133,7 +134,7 @@ export function DiscountPercentage(props?: TypedFeatureProps): ParameterDecorato * @Get("/") * public async handleRequest( * @EnableFeatureA() - * enableFeatureA: Observable>, + * enableFeatureA: Observable>, * ) * ``` * @param {TypedFeatureProps} props The options for injecting the feature flag. @@ -158,7 +159,7 @@ export function EnableFeatureA(props?: TypedFeatureProps): ParameterDecorator { * @Get("/") * public async handleRequest( * @GreetingMessage() - * greetingMessage: Observable>, + * greetingMessage: Observable>, * ) * ``` * @param {TypedFeatureProps} props The options for injecting the feature flag. @@ -168,6 +169,31 @@ export function GreetingMessage(props?: TypedFeatureProps): ParameterDecorator { return StringFeatureFlag({ flagKey: "greetingMessage", defaultValue: "Hello there!", ...props }); } +/** + * Gets the {@link EvaluationDetails} for `themeCustomization` from a domain scoped or the default OpenFeature + * client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}. + * + * **Details:** + * - flag key: `themeCustomization` + * - description: `Allows customization of theme colors.` + * - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` + * - type: `JsonValue` + * + * Usage: + * ```typescript + * @Get("/") + * public async handleRequest( + * @ThemeCustomization() + * themeCustomization: Observable>, + * ) + * ``` + * @param {TypedFeatureProps} props The options for injecting the feature flag. + * @returns {ParameterDecorator} The decorator function. + */ +export function ThemeCustomization(props?: TypedFeatureProps): ParameterDecorator { + return ObjectFeatureFlag({ flagKey: "themeCustomization", defaultValue: {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, ...props }); +} + /** * Gets the {@link EvaluationDetails} for `usernameMaxLength` from a domain scoped or the default OpenFeature * client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}. diff --git a/internal/cmd/testdata/success_nodejs.golden b/internal/cmd/testdata/success_nodejs.golden index 0082564..66e7716 100644 --- a/internal/cmd/testdata/success_nodejs.golden +++ b/internal/cmd/testdata/success_nodejs.golden @@ -3,6 +3,7 @@ import { OpenFeature, stringOrUndefined, objectOrUndefined, + JsonValue, } from "@openfeature/server-sdk"; import type { EvaluationContext, @@ -101,6 +102,36 @@ export interface GeneratedClient { */ greetingMessageDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise>; + /** + * Allows customization of theme colors. + * + * **Details:** + * - flag key: `themeCustomization` + * - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` + * - type: `JsonValue` + * + * Performs a flag evaluation that returns a object. + * @param {EvaluationContext} context The evaluation context used on an individual flag evaluation + * @param {FlagEvaluationOptions} options Additional flag evaluation options + * @returns {Promise} Flag evaluation response + */ + themeCustomization(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise; + + /** + * Allows customization of theme colors. + * + * **Details:** + * - flag key: `themeCustomization` + * - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` + * - type: `JsonValue` + * + * Performs a flag evaluation that a returns an evaluation details object. + * @param {EvaluationContext} context The evaluation context used on an individual flag evaluation + * @param {FlagEvaluationOptions} options Additional flag evaluation options + * @returns {Promise>} Flag evaluation details response + */ + themeCustomizationDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise>; + /** * Maximum allowed length for usernames. * @@ -185,6 +216,14 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext, return client.getStringDetails("greetingMessage", "Hello there!", context, options); }, + themeCustomization: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise => { + return client.getObjectValue("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options); + }, + + themeCustomizationDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise> => { + return client.getObjectDetails("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options); + }, + usernameMaxLength: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise => { return client.getNumberValue("usernameMaxLength", 50, context, options); }, diff --git a/internal/cmd/testdata/success_python.golden b/internal/cmd/testdata/success_python.golden index 93c8d68..3b2e436 100644 --- a/internal/cmd/testdata/success_python.golden +++ b/internal/cmd/testdata/success_python.golden @@ -278,6 +278,94 @@ class GeneratedClient: flag_evaluation_options=flag_evaluation_options, ) + def theme_customization( + self, + evaluation_context: Optional[EvaluationContext] = None, + flag_evaluation_options: Optional[FlagEvaluationOptions] = None, + ) -> object: + """ + Allows customization of theme colors. + + **Details:** + - flag key: `themeCustomization` + - default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}` + - type: `object` + + Performs a flag evaluation that returns a `object`. + """ + return self.client.get_object_value( + flag_key="themeCustomization", + default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, + evaluation_context=evaluation_context, + flag_evaluation_options=flag_evaluation_options, + ) + + def theme_customization_details( + self, + evaluation_context: Optional[EvaluationContext] = None, + flag_evaluation_options: Optional[FlagEvaluationOptions] = None, + ) -> FlagEvaluationDetails: + """ + Allows customization of theme colors. + + **Details:** + - flag key: `themeCustomization` + - default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}` + - type: `object` + + Performs a flag evaluation that returns a `FlagEvaluationDetails` instance. + """ + return self.client.get_object_details( + flag_key="themeCustomization", + default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, + evaluation_context=evaluation_context, + flag_evaluation_options=flag_evaluation_options, + ) + + async def theme_customization_async( + self, + evaluation_context: Optional[EvaluationContext] = None, + flag_evaluation_options: Optional[FlagEvaluationOptions] = None, + ) -> object: + """ + Allows customization of theme colors. + + **Details:** + - flag key: `themeCustomization` + - default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}` + - type: `object` + + Performs a flag evaluation asynchronously and returns a `object`. + """ + return await self.client.get_object_value_async( + flag_key="themeCustomization", + default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, + evaluation_context=evaluation_context, + flag_evaluation_options=flag_evaluation_options, + ) + + async def theme_customization_details_async( + self, + evaluation_context: Optional[EvaluationContext] = None, + flag_evaluation_options: Optional[FlagEvaluationOptions] = None, + ) -> FlagEvaluationDetails: + """ + Allows customization of theme colors. + + **Details:** + - flag key: `themeCustomization` + - default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}` + - type: `object` + + Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance. + """ + return await self.client.get_object_details_async( + flag_key="themeCustomization", + default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, + evaluation_context=evaluation_context, + flag_evaluation_options=flag_evaluation_options, + ) + def username_max_length( self, evaluation_context: Optional[EvaluationContext] = None, diff --git a/internal/cmd/testdata/success_react.golden b/internal/cmd/testdata/success_react.golden index c00172f..6210231 100644 --- a/internal/cmd/testdata/success_react.golden +++ b/internal/cmd/testdata/success_react.golden @@ -5,6 +5,7 @@ import { type ReactFlagEvaluationNoSuspenseOptions, useFlag, useSuspenseFlag, + JsonValue } from "@openfeature/react-sdk"; /** @@ -88,6 +89,33 @@ export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspen return useSuspenseFlag("greetingMessage", "Hello there!", options); }; +/** +* Allows customization of theme colors. +* +* **Details:** +* - flag key: `themeCustomization` +* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` +* - type: `JsonValue` +*/ +export const useThemeCustomization = (options?: ReactFlagEvaluationOptions) => { + return useFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options); +}; + +/** +* Allows customization of theme colors. +* +* **Details:** +* - flag key: `themeCustomization` +* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` +* - type: `JsonValue` +* +* Equivalent to useFlag with options: `{ suspend: true }` +* @experimental — Suspense is an experimental feature subject to change in future versions. +*/ +export const useSuspenseThemeCustomization = (options?: ReactFlagEvaluationNoSuspenseOptions) => { + return useSuspenseFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options); +}; + /** * Maximum allowed length for usernames. * diff --git a/internal/generators/csharp/csharp.go b/internal/generators/csharp/csharp.go index 3c79d55..5db7bf2 100644 --- a/internal/generators/csharp/csharp.go +++ b/internal/generators/csharp/csharp.go @@ -3,6 +3,9 @@ package csharp import ( _ "embed" "fmt" + "maps" + "slices" + "strings" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -31,6 +34,8 @@ func openFeatureType(t flagset.FlagType) string { return "bool" case flagset.StringType: return "string" + case flagset.ObjectType: + return "object" default: return "" } @@ -50,10 +55,77 @@ func formatDefaultValue(flag flagset.Flag) string { } } +func toCSharpDict(value any) string { + assertedMap, ok := value.(map[string]any) + if !ok { + return "null" + } + + keys := slices.Sorted(maps.Keys(assertedMap)) + + var builder strings.Builder + builder.WriteString("new Value(Structure.Builder()") + + for _, key := range keys { + val := assertedMap[key] + + builder.WriteString(fmt.Sprintf(".Set(%q, %s)", key, formatNestedValue(val))) + } + builder.WriteString(".Build())") + + return builder.String() +} + +func formatNestedValue(value any) string { + switch val := value.(type) { + case string: + flag := flagset.Flag{ + Type: flagset.StringType, + DefaultValue: val, + } + return formatDefaultValue(flag) + case bool: + flag := flagset.Flag{ + Type: flagset.BoolType, + DefaultValue: val, + } + return formatDefaultValue(flag) + case int, int64: + flag := flagset.Flag{ + Type: flagset.IntType, + DefaultValue: val, + } + return formatDefaultValue(flag) + case float64: + flag := flagset.Flag{ + Type: flagset.FloatType, + DefaultValue: val, + } + return formatDefaultValue(flag) + case map[string]any: + return toCSharpDict(val) + case []any: + var sliceBuilder strings.Builder + sliceBuilder.WriteString("new Value(new List{") + for index, elem := range val { + if index > 0 { + sliceBuilder.WriteString(", ") + } + + sliceBuilder.WriteString(formatNestedValue(elem)) + } + sliceBuilder.WriteString("})") + return sliceBuilder.String() + default: + return fmt.Sprintf("new Value(%s)", val) + } +} + func (g *CsharpGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, "FormatDefaultValue": formatDefaultValue, + "ToCSharpDict": toCSharpDict, } newParams := &generators.Params[any]{ @@ -67,8 +139,6 @@ func (g *CsharpGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for C#. func NewGenerator(fs *flagset.Flagset) *CsharpGenerator { return &CsharpGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/csharp/csharp.tmpl b/internal/generators/csharp/csharp.tmpl index 39d620d..68ff342 100644 --- a/internal/generators/csharp/csharp.tmpl +++ b/internal/generators/csharp/csharp.tmpl @@ -64,13 +64,13 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else /// /// /// Flag key: {{ .Key }} - /// Default value: {{ .DefaultValue }} + /// Default value: {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ .DefaultValue }}{{ end }} /// Type: {{ .Type | OpenFeatureType }} /// /// Optional context for the flag evaluation /// Options for flag evaluation /// The flag value - public async Task<{{ .Type | OpenFeatureType }}> {{ .Key | ToPascal }}Async(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + public async Task<{{ if eq (.Type | OpenFeatureType) "object" }}Value{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> {{ .Key | ToPascal }}Async(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) { {{- if eq .Type 1 }} return await _client.GetIntegerValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); @@ -80,6 +80,8 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else return await _client.GetBooleanValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); {{- else if eq .Type 4 }} return await _client.GetStringValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); + {{- else if eq .Type 5 }} + return await _client.GetObjectValueAsync("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, evaluationContext, options); {{- else }} throw new NotSupportedException("Unsupported flag type"); {{- end }} @@ -90,13 +92,13 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else /// /// /// Flag key: {{ .Key }} - /// Default value: {{ .DefaultValue }} + /// Default value: {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ .DefaultValue }}{{ end }} /// Type: {{ .Type | OpenFeatureType }} /// /// Optional context for the flag evaluation /// Options for flag evaluation /// The evaluation details containing the flag value and metadata - public async Task> {{ .Key | ToPascal }}DetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + public async Task> {{ .Key | ToPascal }}DetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) { {{- if eq .Type 1 }} return await _client.GetIntegerDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); @@ -106,6 +108,8 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else return await _client.GetBooleanDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); {{- else if eq .Type 4 }} return await _client.GetStringDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options); + {{- else if eq .Type 5 }} + return await _client.GetObjectDetailsAsync("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, evaluationContext, options); {{- else }} throw new NotSupportedException("Unsupported flag type"); {{- end }} diff --git a/internal/generators/golang/golang.go b/internal/generators/golang/golang.go index 1e13dd3..2700394 100644 --- a/internal/generators/golang/golang.go +++ b/internal/generators/golang/golang.go @@ -2,7 +2,12 @@ package golang import ( _ "embed" + "encoding/json" + "fmt" + "maps" + "slices" "sort" + "strings" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -30,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string { return "Boolean" case flagset.StringType: return "String" + case flagset.ObjectType: + return "Object" default: return "" } @@ -45,6 +52,8 @@ func typeString(flagType flagset.FlagType) string { return "bool" case flagset.FloatType: return "float64" + case flagset.ObjectType: + return "map[string]any" default: return "" } @@ -60,11 +69,68 @@ func supportImports(flags []flagset.Flag) []string { return res } +func toMapLiteral(value any) string { + assertedMap, ok := value.(map[string]any) + if !ok { + return "nil" + } + + // To have a determined order of the object for comparison + keys := slices.Sorted(maps.Keys(assertedMap)) + + var builder strings.Builder + builder.WriteString("map[string]any{") + + for index, key := range keys { + if index > 0 { + builder.WriteString(", ") + } + val := assertedMap[key] + + builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val))) + } + + builder.WriteString("}") + return builder.String() +} + +func formatNestedValue(value any) string { + switch val := value.(type) { + case string: + return fmt.Sprintf("%q", val) + case bool: + return fmt.Sprintf("%t", val) + case int, int64, float64: + return fmt.Sprintf("%v", val) + case map[string]any: + return toMapLiteral(val) + case []any: + var sliceBuilder strings.Builder + sliceBuilder.WriteString("[]any{") + for index, elem := range val { + if index > 0 { + sliceBuilder.WriteString(", ") + } + + sliceBuilder.WriteString(formatNestedValue(elem)) + } + sliceBuilder.WriteString("}") + return sliceBuilder.String() + default: + jsonBytes, err := json.Marshal(val) + if err != nil { + return "nil" + } + return fmt.Sprintf("%q", string(jsonBytes)) + } +} + func (g *GolangGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "SupportImports": supportImports, "OpenFeatureType": openFeatureType, "TypeString": typeString, + "ToMapLiteral": toMapLiteral, } newParams := &generators.Params[any]{ @@ -80,8 +146,6 @@ func (g *GolangGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for Go. func NewGenerator(fs *flagset.Flagset) *GolangGenerator { return &GolangGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/golang/golang.tmpl b/internal/generators/golang/golang.tmpl index d297d76..4ec2635 100644 --- a/internal/generators/golang/golang.tmpl +++ b/internal/generators/golang/golang.tmpl @@ -15,6 +15,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error) type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error) type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error) +type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error) +type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error) var client openfeature.IClient = nil @@ -29,11 +31,11 @@ var {{ .Key | ToPascal }} = struct { // the evaluation error, if any, and the evaluation details. ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails }{ - Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{ .Type | TypeString }}, error) { - return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx) + Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{- if eq (.Type | OpenFeatureType) "Object"}}any{{- else}}{{ .Type | TypeString }}{{- end}}, error) { + return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx) }, - ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{ .Type | OpenFeatureType }}EvaluationDetails, error){ - return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx) + ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{- if eq (.Type | OpenFeatureType) "Object"}}Interface{{- else }}{{ .Type | OpenFeatureType }}{{- end}}EvaluationDetails, error){ + 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.go b/internal/generators/java/java.go index 23ee8a5..dd04e29 100644 --- a/internal/generators/java/java.go +++ b/internal/generators/java/java.go @@ -2,7 +2,11 @@ package java import ( _ "embed" + "encoding/json" "fmt" + "maps" + "slices" + "strings" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -31,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string { return "Boolean" case flagset.StringType: return "String" + case flagset.ObjectType: + return "Object" default: return "" } @@ -50,10 +56,84 @@ func formatDefaultValueForJava(flag flagset.Flag) string { } } +func toMapLiteral(value any) string { + assertedMap, ok := value.(map[string]any) + if !ok { + return "null" + } + + keys := slices.Sorted(maps.Keys(assertedMap)) + + var builder strings.Builder + builder.WriteString("Map.of(") + + for index, key := range keys { + if index > 0 { + builder.WriteString(", ") + } + val := assertedMap[key] + + builder.WriteString(fmt.Sprintf("%q, %s", key, formatNestedValue(val))) + } + builder.WriteString(")") + + return builder.String() +} + +func formatNestedValue(value any) string { + switch val := value.(type) { + case string: + flag := flagset.Flag{ + Type: flagset.StringType, + DefaultValue: val, + } + return formatDefaultValueForJava(flag) + case bool: + flag := flagset.Flag{ + Type: flagset.BoolType, + DefaultValue: val, + } + return formatDefaultValueForJava(flag) + case int, int64: + flag := flagset.Flag{ + Type: flagset.IntType, + DefaultValue: val, + } + return formatDefaultValueForJava(flag) + case float64: + flag := flagset.Flag{ + Type: flagset.FloatType, + DefaultValue: val, + } + return formatDefaultValueForJava(flag) + case map[string]any: + return toMapLiteral(val) + case []any: + var sliceBuilder strings.Builder + sliceBuilder.WriteString("List.of(") + for index, elem := range val { + if index > 0 { + sliceBuilder.WriteString(", ") + } + + sliceBuilder.WriteString(formatNestedValue(elem)) + } + sliceBuilder.WriteString(")") + return sliceBuilder.String() + default: + jsonBytes, err := json.Marshal(val) + if err != nil { + return "null" + } + return fmt.Sprintf("%q", string(jsonBytes)) + } +} + func (g *JavaGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, "FormatDefaultValue": formatDefaultValueForJava, + "ToMapLiteral": toMapLiteral, } newParams := &generators.Params[any]{ @@ -67,8 +147,6 @@ func (g *JavaGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for Java. func NewGenerator(fs *flagset.Flagset) *JavaGenerator { return &JavaGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/java/java.tmpl b/internal/generators/java/java.tmpl index 2f0ce7b..8eb52fd 100644 --- a/internal/generators/java/java.tmpl +++ b/internal/generators/java/java.tmpl @@ -17,7 +17,7 @@ public final class OpenFeature { * Details: * - Flag key: {{ .Key }} * - Type: {{ .Type | OpenFeatureType }} - * - Default value: {{ .DefaultValue }} + * - Default value: {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ .DefaultValue }}{{ end }} * Returns the flag value */ {{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx); @@ -27,7 +27,7 @@ public final class OpenFeature { * Details: * - Flag key: {{ .Key }} * - Type: {{ .Type | OpenFeatureType }} - * - Default value: {{ .DefaultValue }} + * - Default value: {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ .DefaultValue }}{{ end }} * Returns the evaluation details containing the flag value and metadata */ FlagEvaluationDetails<{{ .Type | OpenFeatureType }}> {{ .Key | ToCamel }}Details(EvaluationContext ctx); @@ -44,12 +44,12 @@ public final class OpenFeature { {{ range .Flagset.Flags }} @Override public {{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx) { - return client.get{{ .Type | OpenFeatureType | ToPascal }}Value("{{ .Key }}", {{ . | FormatDefaultValue }}, ctx); + return client.get{{ .Type | OpenFeatureType | ToPascal }}Value("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, ctx); } @Override public FlagEvaluationDetails<{{ .Type | OpenFeatureType }}> {{ .Key | ToCamel }}Details(EvaluationContext ctx) { - return client.get{{ .Type | OpenFeatureType | ToPascal }}Details("{{ .Key }}", {{ . | FormatDefaultValue }}, ctx); + return client.get{{ .Type | OpenFeatureType | ToPascal }}Details("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, ctx); } {{ end }} } diff --git a/internal/generators/nestjs/nestjs.go b/internal/generators/nestjs/nestjs.go index 74b3227..9ff86a8 100644 --- a/internal/generators/nestjs/nestjs.go +++ b/internal/generators/nestjs/nestjs.go @@ -2,6 +2,7 @@ package nestjs import ( _ "embed" + "encoding/json" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string { return "boolean" case flagset.StringType: return "string" + case flagset.ObjectType: + return "object" default: return "" } } +func toJSONString(value any) string { + bytes, err := json.Marshal(value) + if err != nil { + return "{}" + } + return string(bytes) +} + func (g *NestJsGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, + "ToJSONString": toJSONString, } newParams := &generators.Params[any]{ @@ -49,8 +61,6 @@ func (g *NestJsGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for NestJS. func NewGenerator(fs *flagset.Flagset) *NestJsGenerator { return &NestJsGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/nestjs/nestjs.tmpl b/internal/generators/nestjs/nestjs.tmpl index 971a74e..a22727a 100644 --- a/internal/generators/nestjs/nestjs.tmpl +++ b/internal/generators/nestjs/nestjs.tmpl @@ -8,8 +8,9 @@ import type { EvaluationContext, EvaluationDetails, OpenFeatureModuleOptions, + JsonValue } from "@openfeature/nestjs-sdk"; -import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag } from "@openfeature/nestjs-sdk"; +import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk"; import type { GeneratedClient } from "./openfeature"; import { getGeneratedClient } from "./openfeature"; @@ -100,21 +101,21 @@ interface TypedFeatureProps { * **Details:** * - flag key: `{{ .Key }}` * - description: `{{ .Description }}` - * - default value: `{{ .DefaultValue }}` - * - type: `{{ .Type | OpenFeatureType }}` + * - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` + * - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}` * * Usage: * ```typescript * @Get("/") * public async handleRequest( * @{{ .Key | ToPascal }}() - * {{ .Key | ToCamel }}: Observable>, + * {{ .Key | ToCamel }}: Observable>, * ) * ``` * @param {TypedFeatureProps} props The options for injecting the feature flag. * @returns {ParameterDecorator} The decorator function. */ export function {{ .Key | ToPascal }}(props?: TypedFeatureProps): ParameterDecorator { - return {{ .Type | OpenFeatureType | ToPascal }}FeatureFlag({ flagKey: {{ .Key | Quote }}, defaultValue: {{ .DefaultValue | QuoteString }}, ...props }); + return {{ .Type | OpenFeatureType | ToPascal }}FeatureFlag({ flagKey: {{ .Key | Quote }}, defaultValue: {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, ...props }); } {{ end -}} diff --git a/internal/generators/nodejs/nodejs.go b/internal/generators/nodejs/nodejs.go index e5b646a..4a97ce4 100644 --- a/internal/generators/nodejs/nodejs.go +++ b/internal/generators/nodejs/nodejs.go @@ -2,6 +2,7 @@ package nodejs import ( _ "embed" + "encoding/json" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string { return "boolean" case flagset.StringType: return "string" + case flagset.ObjectType: + return "object" default: return "" } } +func toJSONString(value any) string { + bytes, err := json.Marshal(value) + if err != nil { + return "{}" + } + return string(bytes) +} + func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, + "ToJSONString": toJSONString, } newParams := &generators.Params[any]{ @@ -49,8 +61,6 @@ func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for NodeJS. func NewGenerator(fs *flagset.Flagset) *NodejsGenerator { return &NodejsGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/nodejs/nodejs.tmpl b/internal/generators/nodejs/nodejs.tmpl index e1c54c8..5f74b42 100644 --- a/internal/generators/nodejs/nodejs.tmpl +++ b/internal/generators/nodejs/nodejs.tmpl @@ -3,6 +3,7 @@ import { OpenFeature, stringOrUndefined, objectOrUndefined, + JsonValue, } from "@openfeature/server-sdk"; import type { EvaluationContext, @@ -17,30 +18,30 @@ export interface GeneratedClient { * * **Details:** * - flag key: `{{ .Key }}` - * - default value: `{{ .DefaultValue }}` - * - type: `{{ .Type | OpenFeatureType }}` + * - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` + * - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}` * * Performs a flag evaluation that returns a {{ .Type | OpenFeatureType }}. * @param {EvaluationContext} context The evaluation context used on an individual flag evaluation * @param {FlagEvaluationOptions} options Additional flag evaluation options - * @returns {Promise<{{ .Type | OpenFeatureType }}>} Flag evaluation response + * @returns {Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>} Flag evaluation response */ - {{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}>; + {{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>; /** * {{ .Description }} * * **Details:** * - flag key: `{{ .Key }}` - * - default value: `{{ .DefaultValue }}` - * - type: `{{ .Type | OpenFeatureType }}` + * - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` + * - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}` * * Performs a flag evaluation that a returns an evaluation details object. * @param {EvaluationContext} context The evaluation context used on an individual flag evaluation * @param {FlagEvaluationOptions} options Additional flag evaluation options - * @returns {Promise>} Flag evaluation details response + * @returns {Promise>} Flag evaluation details response */ - {{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise>; + {{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise>; {{ end -}} } @@ -74,12 +75,12 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext, return { {{- range .Flagset.Flags }} - {{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}> => { - return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options); + {{ .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); }, - {{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise> => { - return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options); + {{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise> => { + return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options); }, {{ end -}} {{ printf " " }}} diff --git a/internal/generators/python/python.go b/internal/generators/python/python.go index 4ded095..a24e6f6 100644 --- a/internal/generators/python/python.go +++ b/internal/generators/python/python.go @@ -2,6 +2,11 @@ package python import ( _ "embed" + "encoding/json" + "fmt" + "maps" + "slices" + "strings" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -43,6 +48,8 @@ func methodType(flagType flagset.FlagType) string { return "boolean" case flagset.FloatType: return "float" + case flagset.ObjectType: + return "object" default: panic("unsupported flag type") } @@ -64,7 +71,7 @@ func typedDetailsMethodAsync(flagType flagset.FlagType) string { return "get_" + methodType(flagType) + "_details_async" } -func pythonBoolLiteral(value interface{}) interface{} { +func pythonBoolLiteral(value any) any { if v, ok := value.(bool); ok { if v { return "True" @@ -74,6 +81,63 @@ func pythonBoolLiteral(value interface{}) interface{} { return value } +func toPythonDict(value any) string { + assertedMap, ok := value.(map[string]any) + if !ok { + return "None" + } + + // To have a determined order of the object for comparison + keys := slices.Sorted(maps.Keys(assertedMap)) + + var builder strings.Builder + builder.WriteString("{") + + for index, key := range keys { + if index != 0 { + builder.WriteString(", ") + } + val := assertedMap[key] + + builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val))) + } + + builder.WriteString("}") + return builder.String() +} + +func formatNestedValue(value any) string { + switch val := value.(type) { + case string: + return fmt.Sprintf("%q", val) + case bool: + return fmt.Sprintf(pythonBoolLiteral(val).(string)) + case int, int64, float64: + return fmt.Sprintf("%v", val) + case map[string]any: + return toPythonDict(val) + case []any: + var sliceBuilder strings.Builder + sliceBuilder.WriteString("[") + for index, elem := range val { + if index > 0 { + sliceBuilder.WriteString(", ") + } + + sliceBuilder.WriteString(formatNestedValue(elem)) + } + sliceBuilder.WriteString("]") + return sliceBuilder.String() + default: + jsonBytes, err := json.Marshal(val) + if err != nil { + return "None" + } + return strings.ReplaceAll(string(jsonBytes), "null", "None") + } + +} + func (g *PythonGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, @@ -82,6 +146,7 @@ func (g *PythonGenerator) Generate(params *generators.Params[Params]) error { "TypedDetailsMethodSync": typedDetailsMethodSync, "TypedDetailsMethodAsync": typedDetailsMethodAsync, "PythonBoolLiteral": pythonBoolLiteral, + "ToPythonDict": toPythonDict, } newParams := &generators.Params[any]{ @@ -95,8 +160,6 @@ func (g *PythonGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for Python. func NewGenerator(fs *flagset.Flagset) *PythonGenerator { return &PythonGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/python/python.tmpl b/internal/generators/python/python.tmpl index e6fbdd8..696488f 100644 --- a/internal/generators/python/python.tmpl +++ b/internal/generators/python/python.tmpl @@ -25,14 +25,14 @@ class GeneratedClient: **Details:** - flag key: `{{ .Key }}` - - default value: `{{ .DefaultValue | PythonBoolLiteral }}` + - default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}` - type: `{{ .Type | OpenFeatureType }}` Performs a flag evaluation that returns a `{{ .Type | OpenFeatureType }}`. """ return self.client.{{ .Type | TypedGetMethodSync }}( flag_key={{ .Key | Quote }}, - default_value={{ .DefaultValue | QuoteString | PythonBoolLiteral }}, + default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }}, evaluation_context=evaluation_context, flag_evaluation_options=flag_evaluation_options, ) @@ -47,14 +47,14 @@ class GeneratedClient: **Details:** - flag key: `{{ .Key }}` - - default value: `{{ .DefaultValue | PythonBoolLiteral }}` + - default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}` - type: `{{ .Type | OpenFeatureType }}` Performs a flag evaluation that returns a `FlagEvaluationDetails` instance. """ return self.client.{{ .Type | TypedDetailsMethodSync }}( flag_key={{ .Key | Quote }}, - default_value={{ .DefaultValue | QuoteString | PythonBoolLiteral }}, + default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }}, evaluation_context=evaluation_context, flag_evaluation_options=flag_evaluation_options, ) @@ -69,14 +69,14 @@ class GeneratedClient: **Details:** - flag key: `{{ .Key }}` - - default value: `{{ .DefaultValue | PythonBoolLiteral }}` + - default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}` - type: `{{ .Type | OpenFeatureType }}` Performs a flag evaluation asynchronously and returns a `{{ .Type | OpenFeatureType }}`. """ return await self.client.{{ .Type | TypedGetMethodAsync }}( flag_key={{ .Key | Quote }}, - default_value={{ .DefaultValue | QuoteString | PythonBoolLiteral }}, + default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }}, evaluation_context=evaluation_context, flag_evaluation_options=flag_evaluation_options, ) @@ -91,14 +91,14 @@ class GeneratedClient: **Details:** - flag key: `{{ .Key }}` - - default value: `{{ .DefaultValue | PythonBoolLiteral }}` + - default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}` - type: `{{ .Type | OpenFeatureType }}` Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance. """ return await self.client.{{ .Type | TypedDetailsMethodAsync }}( flag_key={{ .Key | Quote }}, - default_value={{ .DefaultValue | QuoteString | PythonBoolLiteral }}, + default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }}, evaluation_context=evaluation_context, flag_evaluation_options=flag_evaluation_options, ) diff --git a/internal/generators/react/react.go b/internal/generators/react/react.go index 0f8c3cc..fba37e4 100644 --- a/internal/generators/react/react.go +++ b/internal/generators/react/react.go @@ -2,6 +2,7 @@ package react import ( _ "embed" + "encoding/json" "text/template" "github.com/open-feature/cli/internal/flagset" @@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string { return "boolean" case flagset.StringType: return "string" + case flagset.ObjectType: + return "object" default: return "" } } +func toJSONString(value any) string { + bytes, err := json.Marshal(value) + if err != nil { + return "{}" + } + return string(bytes) +} + func (g *ReactGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, + "ToJSONString": toJSONString, } newParams := &generators.Params[any]{ @@ -49,8 +61,6 @@ func (g *ReactGenerator) Generate(params *generators.Params[Params]) error { // NewGenerator creates a generator for React. func NewGenerator(fs *flagset.Flagset) *ReactGenerator { return &ReactGenerator{ - CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{ - flagset.ObjectType: true, - }), + CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}), } } diff --git a/internal/generators/react/react.tmpl b/internal/generators/react/react.tmpl index e1c40cf..47b64fa 100644 --- a/internal/generators/react/react.tmpl +++ b/internal/generators/react/react.tmpl @@ -5,6 +5,7 @@ import { type ReactFlagEvaluationNoSuspenseOptions, useFlag, useSuspenseFlag, + JsonValue } from "@openfeature/react-sdk"; {{ range .Flagset.Flags }} /** @@ -12,11 +13,11 @@ import { * * **Details:** * - flag key: `{{ .Key }}` -* - default value: `{{ .DefaultValue }}` -* - type: `{{ .Type | OpenFeatureType }}` +* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` +* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}` */ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => { - return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options); + return useFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options); }; /** @@ -24,13 +25,13 @@ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) = * * **Details:** * - flag key: `{{ .Key }}` -* - default value: `{{ .DefaultValue }}` -* - type: `{{ .Type | OpenFeatureType }}` +* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` +* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}` * * Equivalent to useFlag with options: `{ suspend: true }` * @experimental — Suspense is an experimental feature subject to change in future versions. */ export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions) => { - return useSuspenseFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options); + return useSuspenseFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options); }; {{ end}} \ No newline at end of file diff --git a/test/csharp-integration/OpenFeature.cs b/test/csharp-integration/OpenFeature.cs index 80603d7..159b902 100644 --- a/test/csharp-integration/OpenFeature.cs +++ b/test/csharp-integration/OpenFeature.cs @@ -111,6 +111,38 @@ public async Task> GreetingMessageDetailsAsync(Evaluat { return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext); } + + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The flag value + public async Task ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } + + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The evaluation details containing the flag value and metadata + public async Task> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } /// /// Maximum allowed length for usernames. diff --git a/test/csharp-integration/expected/OpenFeature.cs b/test/csharp-integration/expected/OpenFeature.cs index b20990a..c24f009 100644 --- a/test/csharp-integration/expected/OpenFeature.cs +++ b/test/csharp-integration/expected/OpenFeature.cs @@ -159,15 +159,15 @@ public async Task> GreetingMessageDetailsAsync(Eva /// /// /// Flag key: themeCustomization - /// Default value: map[primaryColor:#007bff secondaryColor:#6c757d] - /// Type: Value + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object /// /// Optional context for the flag evaluation /// Options for flag evaluation /// The flag value public async Task ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) { - throw new NotSupportedException("Unsupported flag type"); + return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); } /// @@ -175,15 +175,15 @@ public async Task ThemeCustomizationAsync(EvaluationContext? evaluationCo /// /// /// Flag key: themeCustomization - /// Default value: map[primaryColor:#007bff secondaryColor:#6c757d] - /// Type: Value + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object /// /// Optional context for the flag evaluation /// Options for flag evaluation /// The evaluation details containing the flag value and metadata public async Task> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) { - throw new NotSupportedException("Unsupported flag type"); + return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); } /// diff --git a/test/csharp-integration/expected/OpenFeature.g.cs b/test/csharp-integration/expected/OpenFeature.g.cs index 28b6de1..a1a3b09 100644 --- a/test/csharp-integration/expected/OpenFeature.g.cs +++ b/test/csharp-integration/expected/OpenFeature.g.cs @@ -152,6 +152,38 @@ public async Task> GreetingMessageDetailsAsync(Eva { return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options); } + + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The flag value + public async Task ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } + + /// + /// Allows customization of theme colors. + /// + /// + /// Flag key: themeCustomization + /// Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()) + /// Type: object + /// + /// Optional context for the flag evaluation + /// Options for flag evaluation + /// The evaluation details containing the flag value and metadata + public async Task> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null) + { + return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options); + } /// /// Maximum allowed length for usernames. diff --git a/test/go-integration/test.go b/test/go-integration/test.go index 10fba10..3d98509 100644 --- a/test/go-integration/test.go +++ b/test/go-integration/test.go @@ -48,6 +48,16 @@ func run() error { "default": 50, }, }, + "themeCustomization": { + State: memprovider.Enabled, + DefaultVariant: "default", + Variants: map[string]any{ + "default": map[string]any{ + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + }, + }, + }, }) // Set the provider and wait for it to be ready @@ -84,7 +94,13 @@ func run() error { } fmt.Printf("usernameMaxLength: %v\n", usernameMaxLength) - fmt.Println("Generated Go code compiles successfully!") + themeCustomization, err := generated.ThemeCustomization.Value(ctx, evalCtx) + if err != nil { + return fmt.Errorf("Error evaluating int flag: %v\n", err) + } + fmt.Printf("themeCustomization: %v\n", themeCustomization) + fmt.Println("Generated Go code compiles successfully!") + return nil } diff --git a/test/integration/cmd/go/run.go b/test/integration/cmd/go/run.go index 93a5cda..d1670c9 100644 --- a/test/integration/cmd/go/run.go +++ b/test/integration/cmd/go/run.go @@ -36,7 +36,7 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe // Build the CLI cli := client.Container(). - From("golang:1.23-alpine"). + From("golang:1.23-alpine"). WithExec([]string{"apk", "add", "--no-cache", "git"}). WithDirectory("/src", source). WithWorkdir("/src"). diff --git a/test/nodejs-integration/test.ts b/test/nodejs-integration/test.ts index 03e82cc..735b57a 100644 --- a/test/nodejs-integration/test.ts +++ b/test/nodejs-integration/test.ts @@ -1,4 +1,4 @@ -import { OpenFeature } from "@openfeature/server-sdk"; +import { OpenFeature, JsonValue } from "@openfeature/server-sdk"; import { existsSync, readdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -23,8 +23,8 @@ class TestProvider { const values: Record = { usernameMaxLength: 100, discountPercentage: 0.25 }; return { value: values[flagKey] ?? defaultValue, reason: 'STATIC' }; } - async resolveObjectEvaluation(flagKey: string, defaultValue: T) { - const values: Record = { + async resolveObjectEvaluation(flagKey: string, defaultValue: JsonValue) { + const values: Record = { themeCustomization: { primaryColor: '#ff0000', secondaryColor: '#00ff00' @@ -68,7 +68,8 @@ async function main() { { name: 'enableFeatureA', expected: 'boolean' }, { name: 'greetingMessage', expected: 'string' }, { name: 'usernameMaxLength', expected: 'number' }, - { name: 'discountPercentage', expected: 'number' } + { name: 'discountPercentage', expected: 'number' }, + { name: 'themeCustomization', expected: 'object' } ]; for (const test of tests) {