From 1c02980637d98a632fe6122b5291dcf60ad36467 Mon Sep 17 00:00:00 2001 From: Bill Lynch Date: Thu, 12 Oct 2023 14:34:29 +0100 Subject: [PATCH 1/2] refactor(client-http,web,server): use a serializable and more simple configuration object --- .../src/client/ConfidenceClient.test.ts | 17 +- .../src/client/ConfidenceClient.ts | 71 ++++++++- .../src/client/ConfidenceFlag.test.ts | 137 ----------------- .../client-http/src/client/ConfidenceFlag.ts | 113 -------------- .../src/client/Configuration.test.ts | 145 ++++-------------- .../client-http/src/client/Configuration.ts | 68 +++++++- packages/client-http/src/client/index.ts | 1 - .../src/ConfidenceServerProvider.test.ts | 64 +++----- .../src/ConfidenceServerProvider.ts | 58 ++++--- .../src/ConfidenceWebProvider.test.ts | 64 +++----- .../src/ConfidenceWebProvider.ts | 50 +++--- .../openfeature-web-provider/src/factory.ts | 5 +- 12 files changed, 276 insertions(+), 517 deletions(-) delete mode 100644 packages/client-http/src/client/ConfidenceFlag.test.ts delete mode 100644 packages/client-http/src/client/ConfidenceFlag.ts diff --git a/packages/client-http/src/client/ConfidenceClient.test.ts b/packages/client-http/src/client/ConfidenceClient.test.ts index 0b7ded3a..5f12f8ca 100644 --- a/packages/client-http/src/client/ConfidenceClient.test.ts +++ b/packages/client-http/src/client/ConfidenceClient.test.ts @@ -1,6 +1,5 @@ import { AppliedFlag, ConfidenceClient } from './ConfidenceClient'; import { Configuration, ResolveContext } from './Configuration'; -import { ConfidenceFlag } from './ConfidenceFlag'; describe('ConfidenceClient', () => { const mockFetch = jest.fn(); @@ -75,7 +74,7 @@ describe('ConfidenceClient', () => { it('should return a valid configuration with the flags resolved', async () => { const fakeFlag = { - flag: 'test-flag', + flag: 'flags/test-flag', variant: 'test', value: { str: 'test', @@ -97,7 +96,19 @@ describe('ConfidenceClient', () => { const config = await instanceUnderTest.resolve(context); expect(config).toEqual({ - flags: { ['test-flag']: new ConfidenceFlag(fakeFlag) }, + flags: { + ['test-flag']: { + name: 'test-flag', + schema: { + str: 'string', + }, + value: { + str: 'test', + }, + reason: Configuration.ResolveReason.Match, + variant: 'test', + }, + }, resolveToken: 'resolve-token', context, }); diff --git a/packages/client-http/src/client/ConfidenceClient.ts b/packages/client-http/src/client/ConfidenceClient.ts index e83d5f1d..8c8dd7fe 100644 --- a/packages/client-http/src/client/ConfidenceClient.ts +++ b/packages/client-http/src/client/ConfidenceClient.ts @@ -1,5 +1,4 @@ import { Configuration, ResolveContext } from './Configuration'; -import { ConfidenceFlag, ResolvedFlag } from './ConfidenceFlag'; type ApplyRequest = { clientSecret: string; @@ -13,6 +12,13 @@ type ResolveRequest = { apply?: boolean; flags?: string[]; }; +export type ResolvedFlag = { + flag: string; + variant: string; + value?: T; + flagSchema?: ConfidenceFlagSchema; + reason: Configuration.ResolveReason; +}; type ResolveResponse = { resolvedFlags: ResolvedFlag[]; resolveToken: string; @@ -29,6 +35,12 @@ export type AppliedFlag = { flag: string; applyTime: string; }; +type ConfidenceSimpleTypes = { boolSchema: {} } | { doubleSchema: {} } | { intSchema: {} } | { stringSchema: {} }; +type ConfidenceFlagSchema = { + schema: { + [key: string]: ConfidenceSimpleTypes | { structSchema: ConfidenceFlagSchema }; + }; +}; export class ConfidenceClient { private readonly backendApplyEnabled: boolean; @@ -58,10 +70,14 @@ export class ConfidenceClient { body: JSON.stringify(payload), }); const responseBody: ResolveResponse = await response.json(); + return { - flags: responseBody.resolvedFlags.reduce((acc, flag) => { - return { ...acc, [flag.flag]: new ConfidenceFlag(flag) }; - }, {}), + flags: responseBody.resolvedFlags + .filter(({ flag }) => flag.startsWith('flags/')) + .map(({ flag, ...rest }) => ({ flag: flag.slice('flags/'.length), ...rest })) + .reduce((acc, flag) => { + return { ...acc, [flag.flag]: resolvedFlagToFlag(flag) }; + }, {}), resolveToken: responseBody.resolveToken, context, }; @@ -80,3 +96,50 @@ export class ConfidenceClient { }); } } + +function resolvedFlagToFlag(flag: ResolvedFlag): Configuration.Flag { + return { + name: flag.flag.replace(/$flag\//, ''), + reason: flag.reason, + variant: flag.variant, + value: flag.value, + schema: parseSchema(flag.flagSchema), + }; +} + +function parseBaseType(obj: ConfidenceSimpleTypes): Configuration.FlagSchema { + if ('boolSchema' in obj) { + return 'boolean'; + } + if ('doubleSchema' in obj) { + return 'number'; + } + if ('intSchema' in obj) { + return 'number'; + } + if ('stringSchema' in obj) { + return 'string'; + } + + throw new Error(`Confidence: cannot parse schema. unknown schema: ${JSON.stringify(obj)}`); +} + +function parseSchema(schema: ConfidenceFlagSchema | undefined): Configuration.FlagSchema { + if (!schema) { + return {}; + } + + return Object.keys(schema.schema).reduce((acc: Record, key) => { + const obj = schema.schema[key]; + if ('structSchema' in obj) { + return { + ...acc, + [key]: parseSchema(obj.structSchema), + }; + } + return { + ...acc, + [key]: parseBaseType(obj), + }; + }, {}); +} diff --git a/packages/client-http/src/client/ConfidenceFlag.test.ts b/packages/client-http/src/client/ConfidenceFlag.test.ts deleted file mode 100644 index 4faa7bfe..00000000 --- a/packages/client-http/src/client/ConfidenceFlag.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ConfidenceFlag, ResolvedFlag } from './ConfidenceFlag'; -import { Configuration } from './Configuration'; -import ResolveReason = Configuration.ResolveReason; - -const resolvedFlag: ResolvedFlag = { - flag: 'test', - value: { - str: 'testVal', - }, - variant: 'treatment', - reason: ResolveReason.Match, - flagSchema: { - schema: { - str: { - stringSchema: {}, - }, - }, - }, -}; - -describe('ConfidenceFlag', () => { - it('should construct correctly', () => { - const flag = new ConfidenceFlag(resolvedFlag); - - expect(flag.value).toEqual(resolvedFlag.value); - expect(flag.reason).toEqual(resolvedFlag.reason); - expect(flag.variant).toEqual(resolvedFlag.variant); - expect(flag.flagName).toEqual(resolvedFlag.flag); - expect(flag.schema).toEqual({ str: 'string' }); - }); - - describe('parsing schema', () => { - it('should parse', () => { - const flag = new ConfidenceFlag({ - flag: 'test', - variant: 'treatment', - reason: ResolveReason.Match, - flagSchema: { - schema: { - str: { - stringSchema: {}, - }, - struct: { - structSchema: { - schema: { - str: { - stringSchema: {}, - }, - int: { - intSchema: {}, - }, - double: { - doubleSchema: {}, - }, - bool: { - boolSchema: {}, - }, - structStruct: { - structSchema: { - schema: { - bool: { - boolSchema: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }); - - expect(flag.schema).toEqual({ - str: 'string', - struct: { - str: 'string', - int: 'number', - double: 'number', - bool: 'boolean', - structStruct: { - bool: 'boolean', - }, - }, - }); - }); - }); - - describe('getValue', () => { - it('should return null value if the flag is not a match', () => { - const flag = new ConfidenceFlag({ ...resolvedFlag, reason: ResolveReason.Archived }); - - const val = flag.getValue('str'); - - expect(val!.value).toBeNull(); - expect(val!.match('')).toBeFalsy(); - }); - - it('should return null value if the flag no schema', () => { - const flag = new ConfidenceFlag({ ...resolvedFlag, flagSchema: undefined }); - - const val = flag.getValue('str'); - - expect(val).toBeNull(); - }); - - it('should return null value if the flag path is not found in the flag', () => { - const flag = new ConfidenceFlag({ ...resolvedFlag, flagSchema: undefined }); - - const val = flag.getValue('unknown'); - - expect(val).toBeNull(); - }); - - it('should return the correct value', () => { - const flag = new ConfidenceFlag(resolvedFlag); - - const val = flag.getValue('str'); - - expect(val!.value).toEqual('testVal'); - expect(val!.match('asdf')).toBeTruthy(); - }); - - it('should only match against the correct type', () => { - const flag = new ConfidenceFlag(resolvedFlag); - - const val = flag.getValue('str'); - - expect(val!.match('asdf')).toBeTruthy(); - expect(val!.match(false)).toBeFalsy(); - expect(val!.match(0)).toBeFalsy(); - expect(val!.match(null)).toBeFalsy(); - expect(val!.match(undefined)).toBeFalsy(); - expect(val!.match({})).toBeFalsy(); - }); - }); -}); diff --git a/packages/client-http/src/client/ConfidenceFlag.ts b/packages/client-http/src/client/ConfidenceFlag.ts deleted file mode 100644 index 7fc395a1..00000000 --- a/packages/client-http/src/client/ConfidenceFlag.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Configuration } from './Configuration'; - -type FlagSchema = - | 'number' - | 'boolean' - | 'string' - | { - [step: string]: FlagSchema; - }; -type ConfidenceBaseTypes = { boolSchema: {} } | { doubleSchema: {} } | { intSchema: {} } | { stringSchema: {} }; -type ConfidenceFlagSchema = { - schema: { - [key: string]: ConfidenceBaseTypes | { structSchema: ConfidenceFlagSchema }; - }; -}; - -function valueMatchesSchema(value: any, schema: FlagSchema): boolean { - if (value === null) { - return false; - } - - if (typeof schema !== 'object') { - return typeof value === schema; - } - - return Object.keys(value).every(key => valueMatchesSchema(value[key], schema[key])); -} -function parseBaseType(obj: ConfidenceBaseTypes): FlagSchema { - if ('boolSchema' in obj) { - return 'boolean'; - } - if ('doubleSchema' in obj) { - return 'number'; - } - if ('intSchema' in obj) { - return 'number'; - } - if ('stringSchema' in obj) { - return 'string'; - } - - throw new Error(`Confidence: cannot parse schema. unknown schema: ${JSON.stringify(obj)}`); -} -function parseSchema(schema: ConfidenceFlagSchema | undefined): FlagSchema { - if (!schema) { - return {}; - } - - return Object.keys(schema.schema).reduce((acc: Record, key) => { - const obj = schema.schema[key]; - if ('structSchema' in obj) { - return { - ...acc, - [key]: parseSchema(obj.structSchema), - }; - } - return { - ...acc, - [key]: parseBaseType(obj), - }; - }, {}); -} - -export type ResolvedFlag = { - flag: string; - variant: string; - value?: T; - flagSchema?: ConfidenceFlagSchema; - reason: Configuration.ResolveReason; -}; - -export class ConfidenceFlag implements Configuration.Flag { - readonly flagName: string; - readonly variant: string; - readonly value: unknown; - readonly schema: FlagSchema; - readonly reason: Configuration.ResolveReason; - - constructor(flag: ResolvedFlag) { - this.flagName = flag.flag; - this.reason = flag.reason; - this.variant = flag.variant; - this.value = flag.value; - this.schema = parseSchema(flag.flagSchema); - } - - getValue(...path: string[]): Configuration.FlagValue | null { - if (this.reason !== Configuration.ResolveReason.Match) { - return { - value: null, - match: () => false, - }; - } - - let value: any = this.value; - let schema: FlagSchema = this.schema; - for (const part of path) { - if (typeof schema !== 'object') { - return null; - } - value = value[part]; - schema = schema[part]; - if (schema === undefined) { - return null; - } - } - - return { - value, - match: (val): boolean => valueMatchesSchema(val, schema), - }; - } -} diff --git a/packages/client-http/src/client/Configuration.test.ts b/packages/client-http/src/client/Configuration.test.ts index 01139e41..4af2a280 100644 --- a/packages/client-http/src/client/Configuration.test.ts +++ b/packages/client-http/src/client/Configuration.test.ts @@ -1,123 +1,46 @@ import { Configuration } from './Configuration'; -import { ConfidenceFlag } from './ConfidenceFlag'; -const fakeConfiguration: Configuration = { - flags: { - test: new ConfidenceFlag({ - flag: 'test', - variant: 'test', - value: { - bool: true, - str: 'base string', - double: 1.1, - int: 1, - obj: { - bool: true, - str: 'obj string', - double: 2.1, - int: 2, - obj: { - bool: true, - str: 'obj obj string', - double: 3.1, - int: 3, - }, - }, - }, - flagSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - double: { - doubleSchema: {}, - }, - int: { - intSchema: {}, +describe('Configuration', () => { + describe('Configuration.Flag.getFlagDetails', () => { + it('should get the value and the schema', () => { + const result: Configuration.FlagValue = Configuration.FlagValue.traverse( + { + schema: { + a: { + b: 'string', + }, }, - obj: { - structSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - double: { - doubleSchema: {}, - }, - int: { - intSchema: {}, - }, - obj: { - structSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - double: { - doubleSchema: {}, - }, - int: { - intSchema: {}, - }, - }, - }, - }, - }, + value: { + a: { + b: 'hello world', }, }, }, - }, - reason: Configuration.ResolveReason.Match, - }), - emptyFlag: new ConfidenceFlag({ - flag: 'emptyFlag', - variant: '', - reason: Configuration.ResolveReason.NoSegmentMatch, - }), - }, - resolveToken: 'test-token', - context: {}, -}; + 'a.b', + ); -describe('Configuration', () => { - it.each` - path | expectedValue - ${['bool']} | ${true} - ${['str']} | ${'base string'} - ${['double']} | ${1.1} - ${['int']} | ${1} - ${['obj', 'bool']} | ${true} - ${['obj', 'str']} | ${'obj string'} - ${['obj', 'double']} | ${2.1} - ${['obj', 'int']} | ${2} - ${['obj', 'obj', 'bool']} | ${true} - ${['obj', 'obj', 'str']} | ${'obj obj string'} - ${['obj', 'obj', 'double']} | ${3.1} - ${['obj', 'obj', 'int']} | ${3} - ${['obj', 'obj']} | ${{ bool: true, str: 'obj obj string', double: 3.1, int: 3 }} - ${['obj']} | ${{ bool: true, str: 'obj string', double: 2.1, int: 2, obj: { bool: true, str: 'obj obj string', double: 3.1, int: 3 } }} - `('should get "$expectedValue" for path $path', ({ path, expectedValue }) => { - expect(fakeConfiguration.flags.test.getValue(...path)?.value).toEqual(expectedValue); - }); - - describe('emptyFlag', () => { - it('should return a value of null', () => { - expect(fakeConfiguration.flags.emptyFlag.getValue('nothing')?.value).toEqual(null); + expect(result.value).toEqual('hello world'); + expect(result.schema).toEqual('string'); }); - }); - describe('parseError', () => { - it('should return null info', () => { - expect(fakeConfiguration.flags.test.getValue('404')).toEqual(null); + it('should throw an error when the path not traversable for the value and schema', () => { + expect(() => + Configuration.FlagValue.traverse( + { + schema: { + a: { + b: 'string', + }, + }, + value: { + a: { + b: 'hello world', + }, + }, + }, + 'a.b.c', + ), + ).toThrowError(); }); }); }); diff --git a/packages/client-http/src/client/Configuration.ts b/packages/client-http/src/client/Configuration.ts index 7fad6478..28d87f17 100644 --- a/packages/client-http/src/client/Configuration.ts +++ b/packages/client-http/src/client/Configuration.ts @@ -8,18 +8,72 @@ export namespace Configuration { NoTreatmentMatch = 'RESOLVE_REASON_NO_TREATMENT_MATCH', Archived = 'RESOLVE_REASON_FLAG_ARCHIVED', } + + export type FlagSchema = + | 'number' + | 'boolean' + | 'string' + | { + [step: string]: FlagSchema; + }; + export interface FlagValue { - readonly value: T; - match(obj: S): this is FlagValue; + value: T; + schema: FlagSchema; + } + export interface Flag extends FlagValue { + name: string; + reason: ResolveReason; + variant: string; + value: T; + schema: FlagSchema; + } + + export namespace FlagValue { + export function matches({ schema }: FlagValue, value: any): value is T { + return valueMatchesSchema(value, schema); + } + + export type Traversed = S extends `${infer STEP}.${infer REST}` + ? STEP extends keyof T + ? Traversed + : unknown + : S extends keyof T + ? T[S] + : never; + + export function traverse(flag: FlagValue, path: S): FlagValue> { + let value: any = flag.value; + let schema: FlagSchema = flag.schema; + + for (const part of path.split('.')) { + if (typeof schema !== 'object') { + throw new Error(`Parse Error. Cannot find path: ${path}. In flag: ${JSON.stringify(flag)}`); + } + value = value[part]; + schema = schema[part]; + if (schema === undefined) { + throw new Error(`Parse Error. Cannot find path: ${path}. In flag: ${JSON.stringify(flag)}`); + } + } + + return { value, schema }; + } } +} - export interface Flag { - readonly flagName: string; - readonly variant: string; - readonly reason: ResolveReason; - getValue(...path: string[]): FlagValue | null; +function valueMatchesSchema(value: any, schema: Configuration.FlagSchema): boolean { + if (value === null || schema === null) { + return false; } + + if (typeof schema !== 'object') { + return typeof value === schema; + } + + return Object.keys(value).every(key => valueMatchesSchema(value[key], schema[key])); } + export interface Configuration { flags: Readonly<{ [name: string]: Configuration.Flag; diff --git a/packages/client-http/src/client/index.ts b/packages/client-http/src/client/index.ts index 2890f3e4..3cde1adc 100644 --- a/packages/client-http/src/client/index.ts +++ b/packages/client-http/src/client/index.ts @@ -1,3 +1,2 @@ export * from './Configuration'; export * from './ConfidenceClient'; -export * from './ConfidenceFlag'; diff --git a/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts b/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts index bab72661..6a228891 100644 --- a/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts +++ b/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts @@ -1,5 +1,5 @@ import { ErrorCode, Logger, ProviderStatus } from '@openfeature/web-sdk'; -import { ConfidenceClient, Configuration, ConfidenceFlag } from '@spotify-confidence/client-http'; +import { ConfidenceClient, Configuration } from '@spotify-confidence/client-http'; import { ConfidenceServerProvider } from './ConfidenceServerProvider'; const mockApply = jest.fn(); @@ -20,8 +20,8 @@ const mockClient = { const dummyConfiguration: Configuration = { flags: { - ['flags/testFlag']: new ConfidenceFlag({ - flag: 'flags/testFlag', + ['testFlag']: { + name: 'testFlag', variant: 'control', value: { bool: true, @@ -36,56 +36,28 @@ const dummyConfiguration: Configuration = { }, }, reason: Configuration.ResolveReason.Match, - flagSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - int: { - intSchema: {}, - }, - dub: { - doubleSchema: {}, - }, - obj: { - structSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - int: { - intSchema: {}, - }, - dub: { - doubleSchema: {}, - }, - }, - }, - }, + schema: { + bool: 'boolean', + str: 'string', + int: 'number', + dub: 'number', + obj: { + bool: 'boolean', + str: 'string', + int: 'number', + dub: 'number', }, }, - }), - ['flags/anotherFlag']: new ConfidenceFlag({ - flag: 'flags/anotherFlag', + }, + ['anotherFlag']: { + name: 'anotherFlag', variant: 'control', value: { bool: true, }, reason: Configuration.ResolveReason.Match, - flagSchema: { - schema: { - bool: { - boolSchema: {}, - }, - }, - }, - }), + schema: { bool: 'boolean' }, + }, }, resolveToken: 'before-each', context: {}, diff --git a/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts b/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts index 42af8276..90657952 100644 --- a/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts +++ b/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts @@ -1,15 +1,16 @@ import { - ProviderMetadata, - ProviderStatus, + ErrorCode, EvaluationContext, - Logger, - ResolutionDetails, JsonValue, - ErrorCode, + Logger, Provider, + ProviderMetadata, + ProviderStatus, + ResolutionDetails, + ResolutionReason, } from '@openfeature/js-sdk'; -import { ConfidenceClient, ResolveContext, ApplyManager, Configuration } from '@spotify-confidence/client-http'; +import { ApplyManager, ConfidenceClient, Configuration, ResolveContext } from '@spotify-confidence/client-http'; interface ConfidenceServerProviderOptions { apply?: { @@ -57,7 +58,7 @@ export class ConfidenceServerProvider implements Provider { const [flagName, ...pathParts] = flagKey.split('.'); try { - const flag = configuration.flags[`flags/${flagName}`]; + const flag = configuration.flags[flagName]; if (!flag) { return { @@ -67,40 +68,34 @@ export class ConfidenceServerProvider implements Provider { }; } - if (flag.reason !== Configuration.ResolveReason.Match) { - return { - errorCode: ErrorCode.GENERAL, - value: defaultValue, - reason: flag.reason, - }; - } - - const flagValue = flag.getValue(...pathParts); - if (flagValue === null) { + let flagValue: Configuration.FlagValue; + try { + flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); + } catch (e) { return { errorCode: 'PARSE_ERROR' as ErrorCode, value: defaultValue, reason: 'ERROR', }; } - - if (!flagValue.match(defaultValue)) { + if (flagValue.value === null) { return { - errorCode: 'TYPE_MISMATCH' as ErrorCode, value: defaultValue, - reason: 'ERROR', + reason: mapConfidenceReason(flag.reason), }; } - if (flagValue.value === null) { + if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { return { + errorCode: 'TYPE_MISMATCH' as ErrorCode, value: defaultValue, - reason: flag.reason, + reason: 'ERROR', }; } + this.applyManager.apply(configuration.resolveToken, flagName); return { - value: flagValue.value, - reason: 'TARGETING_MATCH', + value: flagValue.value as T, + reason: mapConfidenceReason(flag.reason), variant: flag.variant, flagMetadata: { resolveToken: configuration.resolveToken || '', @@ -166,3 +161,16 @@ export class ConfidenceServerProvider implements Provider { return this.fetchFlag(flagKey, defaultValue, context, logger); } } + +function mapConfidenceReason(reason: Configuration.ResolveReason): ResolutionReason { + switch (reason) { + case Configuration.ResolveReason.Archived: + return 'DISABLED'; + case Configuration.ResolveReason.Unspecified: + return 'UNKNOWN'; + case Configuration.ResolveReason.Match: + return 'TARGETING_MATCH'; + default: + return 'DEFAULT'; + } +} diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts index 3cacddc8..0291bb20 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts @@ -7,7 +7,7 @@ import { ProviderStatus, } from '@openfeature/web-sdk'; import { ConfidenceWebProvider } from './ConfidenceWebProvider'; -import { ConfidenceClient, ConfidenceFlag, Configuration, ResolveContext } from '@spotify-confidence/client-http'; +import { ConfidenceClient, Configuration, ResolveContext } from '@spotify-confidence/client-http'; const mockApply = jest.fn(); jest.mock('@spotify-confidence/client-http', () => { @@ -30,8 +30,8 @@ const dummyEvaluationContext: EvaluationContext = { targetingKey: 'test' }; const dummyConfiguration: Configuration = { flags: { - ['flags/testFlag']: new ConfidenceFlag({ - flag: 'flags/testFlag', + ['testFlag']: { + name: 'testFlag', variant: 'control', value: { bool: true, @@ -46,56 +46,30 @@ const dummyConfiguration: Configuration = { }, }, reason: Configuration.ResolveReason.Match, - flagSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - int: { - intSchema: {}, - }, - dub: { - doubleSchema: {}, - }, - obj: { - structSchema: { - schema: { - bool: { - boolSchema: {}, - }, - str: { - stringSchema: {}, - }, - int: { - intSchema: {}, - }, - dub: { - doubleSchema: {}, - }, - }, - }, - }, + schema: { + bool: 'boolean', + str: 'string', + int: 'number', + dub: 'number', + obj: { + bool: 'boolean', + str: 'string', + int: 'number', + dub: 'number', }, }, - }), - ['flags/anotherFlag']: new ConfidenceFlag({ - flag: 'flags/anotherFlag', + }, + ['anotherFlag']: { + name: 'anotherFlag', variant: 'control', value: { bool: true, }, reason: Configuration.ResolveReason.Match, - flagSchema: { - schema: { - bool: { - boolSchema: {}, - }, - }, + schema: { + bool: 'boolean', }, - }), + }, }, resolveToken: 'before-each', context: dummyContext, diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts index bf61fc5a..926aa00a 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts @@ -9,6 +9,7 @@ import { ProviderMetadata, ProviderStatus, ResolutionDetails, + ResolutionReason, } from '@openfeature/web-sdk'; import equal from 'fast-deep-equal'; @@ -102,7 +103,7 @@ export class ConfidenceWebProvider implements Provider { const [flagName, ...pathParts] = flagKey.split('.'); try { - const flag = this.configuration.flags[`flags/${flagName}`]; + const flag = this.configuration.flags[flagName]; if (!flag) { logger.warn('Flag "%s" was not found', flagName); @@ -113,17 +114,10 @@ export class ConfidenceWebProvider implements Provider { }; } - if (flag.reason !== Configuration.ResolveReason.Match) { - logger.info('No variant match for flag "%s"', flagName); - return { - errorCode: ErrorCode.GENERAL, - value: defaultValue, - reason: flag.reason, - }; - } - - const flagValue = flag.getValue(...pathParts); - if (flagValue === null) { + let flagValue: Configuration.FlagValue; + try { + flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); + } catch (e) { logger.warn('Value with path "%s" was not found in flag "%s"', pathParts.join('.'), flagName); return { errorCode: ErrorCode.PARSE_ERROR, @@ -131,27 +125,26 @@ export class ConfidenceWebProvider implements Provider { reason: 'ERROR', }; } - - if (!flagValue.match(defaultValue)) { - logger.warn('Value for "%s" is of incorrect type', flagKey); + if (flagValue.value === null) { return { - errorCode: ErrorCode.TYPE_MISMATCH, value: defaultValue, - reason: 'ERROR', + reason: mapConfidenceReason(flag.reason), }; } - if (flagValue.value === null) { - logger.info('Value for "%s" is default', flagKey); + if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { + logger.warn('Value for "%s" is of incorrect type', flagKey); return { + errorCode: ErrorCode.TYPE_MISMATCH, value: defaultValue, - reason: flag.reason, + reason: 'ERROR', }; } + this.applyManager.apply(this.configuration.resolveToken, flagName); logger.info('Value for "%s" successfully evaluated', flagKey); return { - value: flagValue.value, - reason: 'TARGETING_MATCH', + value: flagValue.value as T, + reason: mapConfidenceReason(flag.reason), variant: flag.variant, flagMetadata: { resolveToken: this.configuration.resolveToken, @@ -203,3 +196,16 @@ export class ConfidenceWebProvider implements Provider { return this.getFlag(flagKey, defaultValue, context, logger); } } + +function mapConfidenceReason(reason: Configuration.ResolveReason): ResolutionReason { + switch (reason) { + case Configuration.ResolveReason.Archived: + return 'DISABLED'; + case Configuration.ResolveReason.Unspecified: + return 'UNKNOWN'; + case Configuration.ResolveReason.Match: + return 'TARGETING_MATCH'; + default: + return 'DEFAULT'; + } +} diff --git a/packages/openfeature-web-provider/src/factory.ts b/packages/openfeature-web-provider/src/factory.ts index 9989e0f2..8f4a00d6 100644 --- a/packages/openfeature-web-provider/src/factory.ts +++ b/packages/openfeature-web-provider/src/factory.ts @@ -1,8 +1,7 @@ -import { Provider } from '@openfeature/web-sdk'; import { ConfidenceWebProvider } from './ConfidenceWebProvider'; import { ConfidenceClient } from '@spotify-confidence/client-http'; -type ConfidenceWebProviderFactoryOptions = { +export type ConfidenceWebProviderFactoryOptions = { region: 'eu' | 'us'; fetchImplementation: typeof fetch; clientSecret: string; @@ -12,7 +11,7 @@ type ConfidenceWebProviderFactoryOptions = { }; }; -export function createConfidenceWebProvider(options: ConfidenceWebProviderFactoryOptions): Provider { +export function createConfidenceWebProvider(options: ConfidenceWebProviderFactoryOptions): ConfidenceWebProvider { const confidenceClient = new ConfidenceClient({ ...options, apply: !options.apply, From b011c533747511ec121a9e87c8d7fc4cd271f301 Mon Sep 17 00:00:00 2001 From: Bill Lynch Date: Mon, 23 Oct 2023 11:18:11 +0100 Subject: [PATCH 2/2] style(web,client-http,examples): tidy imports and formatting --- .../src/client/ConfidenceClient.ts | 22 +++++++++---------- .../src/ConfidenceWebProvider.ts | 4 ++++ .../openfeature-web-provider/src/factory.ts | 5 +++-- yarn.lock | 13 ----------- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/packages/client-http/src/client/ConfidenceClient.ts b/packages/client-http/src/client/ConfidenceClient.ts index 8c8dd7fe..8b2becef 100644 --- a/packages/client-http/src/client/ConfidenceClient.ts +++ b/packages/client-http/src/client/ConfidenceClient.ts @@ -12,6 +12,17 @@ type ResolveRequest = { apply?: boolean; flags?: string[]; }; +type ResolveResponse = { + resolvedFlags: ResolvedFlag[]; + resolveToken: string; +}; +type ConfidenceSimpleTypes = { boolSchema: {} } | { doubleSchema: {} } | { intSchema: {} } | { stringSchema: {} }; +type ConfidenceFlagSchema = { + schema: { + [key: string]: ConfidenceSimpleTypes | { structSchema: ConfidenceFlagSchema }; + }; +}; + export type ResolvedFlag = { flag: string; variant: string; @@ -19,11 +30,6 @@ export type ResolvedFlag = { flagSchema?: ConfidenceFlagSchema; reason: Configuration.ResolveReason; }; -type ResolveResponse = { - resolvedFlags: ResolvedFlag[]; - resolveToken: string; -}; - export type ConfidenceClientOptions = { fetchImplementation: typeof fetch; clientSecret: string; @@ -35,12 +41,6 @@ export type AppliedFlag = { flag: string; applyTime: string; }; -type ConfidenceSimpleTypes = { boolSchema: {} } | { doubleSchema: {} } | { intSchema: {} } | { stringSchema: {} }; -type ConfidenceFlagSchema = { - schema: { - [key: string]: ConfidenceSimpleTypes | { structSchema: ConfidenceFlagSchema }; - }; -}; export class ConfidenceClient { private readonly backendApplyEnabled: boolean; diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts index 926aa00a..661c1905 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts @@ -102,6 +102,7 @@ export class ConfidenceWebProvider implements Provider { } const [flagName, ...pathParts] = flagKey.split('.'); + try { const flag = this.configuration.flags[flagName]; @@ -125,12 +126,14 @@ export class ConfidenceWebProvider implements Provider { reason: 'ERROR', }; } + if (flagValue.value === null) { return { value: defaultValue, reason: mapConfidenceReason(flag.reason), }; } + if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { logger.warn('Value for "%s" is of incorrect type', flagKey); return { @@ -141,6 +144,7 @@ export class ConfidenceWebProvider implements Provider { } this.applyManager.apply(this.configuration.resolveToken, flagName); + logger.info('Value for "%s" successfully evaluated', flagKey); return { value: flagValue.value as T, diff --git a/packages/openfeature-web-provider/src/factory.ts b/packages/openfeature-web-provider/src/factory.ts index 8f4a00d6..9989e0f2 100644 --- a/packages/openfeature-web-provider/src/factory.ts +++ b/packages/openfeature-web-provider/src/factory.ts @@ -1,7 +1,8 @@ +import { Provider } from '@openfeature/web-sdk'; import { ConfidenceWebProvider } from './ConfidenceWebProvider'; import { ConfidenceClient } from '@spotify-confidence/client-http'; -export type ConfidenceWebProviderFactoryOptions = { +type ConfidenceWebProviderFactoryOptions = { region: 'eu' | 'us'; fetchImplementation: typeof fetch; clientSecret: string; @@ -11,7 +12,7 @@ export type ConfidenceWebProviderFactoryOptions = { }; }; -export function createConfidenceWebProvider(options: ConfidenceWebProviderFactoryOptions): ConfidenceWebProvider { +export function createConfidenceWebProvider(options: ConfidenceWebProviderFactoryOptions): Provider { const confidenceClient = new ConfidenceClient({ ...options, apply: !options.apply, diff --git a/yarn.lock b/yarn.lock index b23dbec5..01eb0d66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2787,19 +2787,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@spotify-confidence/integration-react@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@spotify-confidence/integration-react/-/integration-react-0.0.4.tgz#383afc78c2db608b1bf75cb41c53d17512b6a77d" - integrity sha512-4NPbuMTweAz8sjsiEdFNBcB7im5XeETDc7ysyoLWw3vSN407fww+e3XDDhon+advI47TYvlUdvlEN75mOGnW4g== - -"@spotify-confidence/openfeature-server-provider@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@spotify-confidence/openfeature-server-provider/-/openfeature-server-provider-0.0.3.tgz#e424b2626f291037c00133171a01c4915981b6d4" - integrity sha512-GaOyWe8yOC//LYHe+K3pn2wXUph88doXoLOz/6nup+J/xDc2F7kQIsFkA+TE+fI27CH8iaPUbO8ledTdKcYFjw== - dependencies: - "@spotify-confidence/client-http" "^0.0.2" - fast-deep-equal "^3.1.3" - "@spotify/eslint-config-base@^15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@spotify/eslint-config-base/-/eslint-config-base-15.0.0.tgz#fa8a003e656b1c14694528a487bb9e974e013e4d"