Skip to content

Commit

Permalink
fixup! refactor(client-http,web,server): use a serializable and more …
Browse files Browse the repository at this point in the history
…simple configuration object
  • Loading branch information
andreas-karlsson authored and Billlynch committed Oct 23, 2023
1 parent ce6e5da commit 83e3731
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 69 deletions.
4 changes: 2 additions & 2 deletions packages/client-http/src/client/ConfidenceClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,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',
Expand All @@ -98,7 +98,7 @@ describe('ConfidenceClient', () => {
expect(config).toEqual({
flags: {
['test-flag']: {
flagName: 'test-flag',
name: 'test-flag',
schema: {
str: 'string',
},
Expand Down
11 changes: 7 additions & 4 deletions packages/client-http/src/client/ConfidenceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@ export class ConfidenceClient {
const responseBody: ResolveResponse = await response.json();

return {
flags: responseBody.resolvedFlags.reduce((acc, flag) => {
return { ...acc, [flag.flag]: resolvedFlagToFlag(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,
};
Expand All @@ -96,7 +99,7 @@ export class ConfidenceClient {

function resolvedFlagToFlag(flag: ResolvedFlag): Configuration.Flag {
return {
flagName: flag.flag,
name: flag.flag.replace(/$flag\//, ''),
reason: flag.reason,
variant: flag.variant,
value: flag.value,
Expand Down
17 changes: 4 additions & 13 deletions packages/client-http/src/client/Configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@ import { Configuration } from './Configuration';
describe('Configuration', () => {
describe('Configuration.Flag.getFlagDetails', () => {
it('should get the value and the schema', () => {
const result = Configuration.Flag.getFlagDetails(
const result = Configuration.FlagValue.traverse(
{
flagName: 'test',
schema: {
a: {
b: 'string',
},
},
reason: Configuration.ResolveReason.Match,
value: {
a: {
b: 'hello world',
},
},
variant: 'control',
},
'a',
'b',
'a.b',
);

expect(result.value).toEqual('hello world');
Expand All @@ -29,25 +25,20 @@ describe('Configuration', () => {

it('should throw an error when the path not traversable for the value and schema', () => {
expect(() =>
Configuration.Flag.getFlagDetails(
Configuration.FlagValue.traverse(
{
flagName: 'test',
schema: {
a: {
b: 'string',
},
},
reason: Configuration.ResolveReason.Match,
value: {
a: {
b: 'hello world',
},
},
variant: 'control',
},
'a',
'b',
'c',
'a.b.c',
),
).toThrowError();
});
Expand Down
34 changes: 25 additions & 9 deletions packages/client-http/src/client/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ export namespace Configuration {
[step: string]: FlagSchema;
};

export interface Flag<T = unknown> {
flagName: string;
export interface FlagValue<T = unknown> {
value: T;
schema: FlagSchema;
}
export interface Flag<T = unknown> extends FlagValue<T> {
name: string;
reason: ResolveReason;
variant: string;
value: T;
schema: FlagSchema;
}

export namespace Flag {
export type Details<T = unknown> = { value: T; schema: FlagSchema };
export function valueMatchesSchema(value: any, schema: FlagSchema | null): boolean {
export namespace FlagValue {
function valueMatchesSchema(value: any, schema: FlagSchema): boolean {
if (value === null || schema === null) {
return false;
}
Expand All @@ -38,18 +41,31 @@ export namespace Configuration {

return Object.keys(value).every(key => valueMatchesSchema(value[key], schema[key]));
}
export function getFlagDetails<T>(flag: Flag<T>, ...path: string[]): Details<T> {

export function matches<T>({ schema }: FlagValue<T>, value: any): value is T {
return valueMatchesSchema(value, schema);
}

export type Traversed<T, S extends string> = S extends `${infer STEP}.${infer REST}`
? STEP extends keyof T
? Traversed<T[STEP], REST>
: never
: S extends keyof T
? T[S]
: never;

export function traverse<T, S extends string>(flag: FlagValue<T>, path: S): FlagValue<Traversed<T, S>> {
let value: any = flag.value;
let schema: FlagSchema = flag.schema;

for (const part of path) {
for (const part of path.split('.')) {
if (typeof schema !== 'object') {
throw new Error(`Parse Error. Cannot find path: ${path.join(',')}. In flag: ${JSON.stringify(flag)}`);
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.join(',')}. In flag: ${JSON.stringify(flag)}`);
throw new Error(`Parse Error. Cannot find path: ${path}. In flag: ${JSON.stringify(flag)}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const mockClient = {

const dummyConfiguration: Configuration = {
flags: {
['flags/testFlag']: {
flagName: 'flags/testFlag',
['testFlag']: {
name: 'testFlag',
variant: 'control',
value: {
bool: true,
Expand Down Expand Up @@ -49,8 +49,8 @@ const dummyConfiguration: Configuration = {
},
},
},
['flags/anotherFlag']: {
flagName: 'flags/anotherFlag',
['anotherFlag']: {
name: 'anotherFlag',
variant: 'control',
value: {
bool: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ProviderMetadata,
ProviderStatus,
ResolutionDetails,
ResolutionReason,
} from '@openfeature/js-sdk';

import { ApplyManager, ConfidenceClient, Configuration, ResolveContext } from '@spotify-confidence/client-http';
Expand Down Expand Up @@ -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 {
Expand All @@ -67,31 +68,23 @@ export class ConfidenceServerProvider implements Provider {
};
}

if (flag.reason !== Configuration.ResolveReason.Match) {
return {
errorCode: ErrorCode.GENERAL,
value: defaultValue,
reason: flag.reason,
};
}

let flagDetails: Configuration.Flag.Details;
let flagValue: Configuration.FlagValue;
try {
flagDetails = Configuration.Flag.getFlagDetails(flag, ...pathParts);
flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.'));
} catch (e) {
return {
errorCode: 'PARSE_ERROR' as ErrorCode,
value: defaultValue,
reason: 'ERROR',
};
}
if (flagDetails.value === null) {
if (flagValue.value === null) {
return {
value: defaultValue,
reason: flag.reason,
reason: mapConfidenceReason(flag.reason),
};
}
if (!Configuration.Flag.valueMatchesSchema(defaultValue, flagDetails.schema)) {
if (!Configuration.FlagValue.matches(flagValue, defaultValue)) {
return {
errorCode: 'TYPE_MISMATCH' as ErrorCode,
value: defaultValue,
Expand All @@ -101,8 +94,8 @@ export class ConfidenceServerProvider implements Provider {

this.applyManager.apply(configuration.resolveToken, flagName);
return {
value: flagDetails.value as T,
reason: 'TARGETING_MATCH',
value: flagValue.value as T,
reason: mapConfidenceReason(flag.reason),
variant: flag.variant,
flagMetadata: {
resolveToken: configuration.resolveToken || '',
Expand Down Expand Up @@ -168,3 +161,15 @@ 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';
}
return 'DEFAULT';
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const dummyEvaluationContext: EvaluationContext = { targetingKey: 'test' };

const dummyConfiguration: Configuration = {
flags: {
['flags/testFlag']: {
flagName: 'flags/testFlag',
['testFlag']: {
name: 'testFlag',
variant: 'control',
value: {
bool: true,
Expand Down Expand Up @@ -59,8 +59,8 @@ const dummyConfiguration: Configuration = {
},
},
},
['flags/anotherFlag']: {
flagName: 'flags/anotherFlag',
['anotherFlag']: {
name: 'anotherFlag',
variant: 'control',
value: {
bool: true,
Expand Down
38 changes: 21 additions & 17 deletions packages/openfeature-web-provider/src/ConfidenceWebProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ProviderMetadata,
ProviderStatus,
ResolutionDetails,
ResolutionReason,
} from '@openfeature/web-sdk';
import equal from 'fast-deep-equal';

Expand Down Expand Up @@ -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);
Expand All @@ -113,18 +114,9 @@ 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,
};
}

let flagDetails: Configuration.Flag.Details;
let flagValue: Configuration.FlagValue;
try {
flagDetails = Configuration.Flag.getFlagDetails(flag, ...pathParts);
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 {
Expand All @@ -133,13 +125,13 @@ export class ConfidenceWebProvider implements Provider {
reason: 'ERROR',
};
}
if (flagDetails.value === null) {
if (flagValue.value === null) {
return {
value: defaultValue,
reason: flag.reason,
reason: mapConfidenceReason(flag.reason),
};
}
if (!Configuration.Flag.valueMatchesSchema(defaultValue, flagDetails.schema)) {
if (!Configuration.FlagValue.matches(flagValue, defaultValue)) {
logger.warn('Value for "%s" is of incorrect type', flagKey);
return {
errorCode: ErrorCode.TYPE_MISMATCH,
Expand All @@ -151,8 +143,8 @@ export class ConfidenceWebProvider implements Provider {
this.applyManager.apply(this.configuration.resolveToken, flagName);
logger.info('Value for "%s" successfully evaluated', flagKey);
return {
value: flagDetails.value as T,
reason: 'TARGETING_MATCH',
value: flagValue.value as T,
reason: mapConfidenceReason(flag.reason),
variant: flag.variant,
flagMetadata: {
resolveToken: this.configuration.resolveToken,
Expand Down Expand Up @@ -204,3 +196,15 @@ 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';
}
return 'DEFAULT';
}

0 comments on commit 83e3731

Please sign in to comment.