diff --git a/Makefile b/Makefile index cbe9c598..baad711e 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ help: Makefile testDataDir := test/data/ tempDir := ${testDataDir}temp/ gitDataDir := ${tempDir}sdk-test-data/ -branchName := tp/bootstrap-config +branchName := main githubRepoLink := https://github.com/Eppo-exp/sdk-test-data.git repoName := sdk-test-data .PHONY: test-data diff --git a/src/client/eppo-client-with-bandits.spec.ts b/src/client/eppo-client-with-bandits.spec.ts index eb2ba9d5..cfba4c85 100644 --- a/src/client/eppo-client-with-bandits.spec.ts +++ b/src/client/eppo-client-with-bandits.spec.ts @@ -8,7 +8,7 @@ import { BanditTestCase, BANDIT_TEST_DATA_DIR, readMockConfigurationWireResponse, - SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE, + BANDITS_WIRE_FILE, } from '../../test/testHelpers'; import ApiEndpoints from '../api-endpoints'; import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger'; @@ -21,7 +21,7 @@ import { IConfigurationWire, IPrecomputedConfiguration, IObfuscatedPrecomputedConfigurationResponse, - ConfigurationWireV1, + configurationFromString, } from '../configuration-wire/configuration-wire-types'; import { Evaluator, FlagEvaluation } from '../evaluator'; import { @@ -137,8 +137,8 @@ describe('EppoClient Bandits E2E test', () => { } describe('bootstrapped client', () => { - const banditFlagsConfig = ConfigurationWireV1.fromString( - readMockConfigurationWireResponse(SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE), + const banditFlagsConfig = configurationFromString( + readMockConfigurationWireResponse(BANDITS_WIRE_FILE), ); let client: EppoClient; diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts index 14a4df73..4d02f06d 100644 --- a/src/client/eppo-client.spec.ts +++ b/src/client/eppo-client.spec.ts @@ -10,8 +10,8 @@ import { OBFUSCATED_MOCK_UFC_RESPONSE_FILE, readMockConfigurationWireResponse, readMockUFCResponse, - SHARED_BOOTSTRAP_FLAGS_FILE, - SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE, + FLAGS_WIRE_FILE, + OBFUSCATED_FLAGS_WIRE_FILE, SubjectTestCase, testCasesByFileName, validateTestAssignments, @@ -21,7 +21,7 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache'; import { IConfigurationStore } from '../configuration-store/configuration-store'; import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; import { - ConfigurationWireV1, + configurationFromString, IConfigurationWire, IObfuscatedPrecomputedConfigurationResponse, ObfuscatedPrecomputedConfigurationResponse, @@ -321,8 +321,6 @@ describe('EppoClient E2E test', () => { }); }); - const testCases = testCasesByFileName(ASSIGNMENT_TEST_DATA_DIR); - function testCasesAgainstClient(client: EppoClient, testCase: IAssignmentTestCase) { const { flag, variationType, defaultValue, subjects } = testCase; @@ -358,11 +356,11 @@ describe('EppoClient E2E test', () => { const testCases = testCasesByFileName(ASSIGNMENT_TEST_DATA_DIR); describe('boostrapped client', () => { - const bootstrapFlagsConfig = ConfigurationWireV1.fromString( - readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_FILE), + const bootstrapFlagsConfig = configurationFromString( + readMockConfigurationWireResponse(FLAGS_WIRE_FILE), ); - const bootstrapFlagsObfuscatedConfig = ConfigurationWireV1.fromString( - readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE), + const bootstrapFlagsObfuscatedConfig = configurationFromString( + readMockConfigurationWireResponse(OBFUSCATED_FLAGS_WIRE_FILE), ); describe('Not obfuscated', () => { diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 9637714d..64c64798 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -25,10 +25,10 @@ import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.stor import { ConfigurationWireV1, IConfigurationWire, - inflateResponse, IPrecomputedConfiguration, PrecomputedConfiguration, } from '../configuration-wire/configuration-wire-types'; +import { inflateJsonObject } from '../configuration-wire/json-util'; import { DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES, DEFAULT_POLL_CONFIG_REQUEST_RETRIES, @@ -348,11 +348,11 @@ export default class EppoClient { if (!configuration.config) { throw new Error('Flag configuration not provided'); } - const flagConfigResponse: IUniversalFlagConfigResponse = inflateResponse( + const flagConfigResponse: IUniversalFlagConfigResponse = inflateJsonObject( configuration.config.response, ); const banditParamResponse: IBanditParametersResponse | undefined = configuration.bandits - ? inflateResponse(configuration.bandits.response) + ? inflateJsonObject(configuration.bandits.response) : undefined; // We need to run this method sync, but, because the configuration stores potentially have an async write at the end diff --git a/src/configuration-wire/configuration-wire-types.spec.ts b/src/configuration-wire/configuration-wire-types.spec.ts index c90e8acc..694b6a0a 100644 --- a/src/configuration-wire/configuration-wire-types.spec.ts +++ b/src/configuration-wire/configuration-wire-types.spec.ts @@ -6,7 +6,8 @@ import { import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client'; import { FormatEnum } from '../interfaces'; -import { ConfigurationWireV1, deflateResponse, inflateResponse } from './configuration-wire-types'; +import { ConfigurationWireV1 } from './configuration-wire-types'; +import { deflateJsonObject, inflateJsonObject } from './json-util'; describe('Response String Type Safety', () => { const mockFlagConfig: IUniversalFlagConfigResponse = readMockUFCResponse( @@ -18,22 +19,22 @@ describe('Response String Type Safety', () => { describe('deflateResponse and inflateResponse', () => { it('should correctly serialize and deserialize flag config', () => { - const serialized = deflateResponse(mockFlagConfig); - const deserialized = inflateResponse(serialized); + const serialized = deflateJsonObject(mockFlagConfig); + const deserialized = inflateJsonObject(serialized); expect(deserialized).toEqual(mockFlagConfig); }); it('should correctly serialize and deserialize bandit config', () => { - const serialized = deflateResponse(mockBanditConfig); - const deserialized = inflateResponse(serialized); + const serialized = deflateJsonObject(mockBanditConfig); + const deserialized = inflateJsonObject(serialized); expect(deserialized).toEqual(mockBanditConfig); }); it('should maintain type information through serialization', () => { - const serialized = deflateResponse(mockFlagConfig); - const deserialized = inflateResponse(serialized); + const serialized = deflateJsonObject(mockFlagConfig); + const deserialized = inflateJsonObject(serialized); // TypeScript compilation check: these should work expect(deserialized.format).toBe(FormatEnum.SERVER); @@ -54,7 +55,7 @@ describe('Response String Type Safety', () => { if (!wirePacket.config) { fail('Flag config not present in ConfigurationWire'); } - const deserializedConfig = inflateResponse(wirePacket.config.response); + const deserializedConfig = inflateJsonObject(wirePacket.config.response); expect(deserializedConfig).toEqual(mockFlagConfig); }); @@ -80,8 +81,8 @@ describe('Response String Type Safety', () => { expect(wirePacket.bandits.etag).toBe('bandit-etag'); // Verify we can deserialize both responses - const deserializedConfig = inflateResponse(wirePacket.config.response); - const deserializedBandits = inflateResponse(wirePacket.bandits.response); + const deserializedConfig = inflateJsonObject(wirePacket.config.response); + const deserializedBandits = inflateJsonObject(wirePacket.bandits.response); expect(deserializedConfig).toEqual(mockFlagConfig); expect(deserializedBandits).toEqual(mockBanditConfig); diff --git a/src/configuration-wire/configuration-wire-types.ts b/src/configuration-wire/configuration-wire-types.ts index d7734d77..cad726b1 100644 --- a/src/configuration-wire/configuration-wire-types.ts +++ b/src/configuration-wire/configuration-wire-types.ts @@ -1,4 +1,4 @@ -import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client'; +import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client'; import { Environment, FormatEnum, @@ -9,6 +9,20 @@ import { import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from '../obfuscation'; import { ContextAttributes, FlagKey, HashedFlagKey } from '../types'; +import { deflateJsonObject, inflateJsonObject, JsonString } from './json-util'; + +/** + * Builds an `IConfigurationWire` instance from the payload string. + * To generate the payload string, see `ConfigurationWireHelper.fetchBootstrapConfiguration`. + * + * @param payloadString + */ +export function configurationFromString( + payloadString: string | JsonString, +): IConfigurationWire { + return inflateJsonObject(payloadString as JsonString); +} + // Base interface for all configuration responses interface IBasePrecomputedConfigurationResponse { readonly format: FormatEnum.PRECOMPUTED; @@ -169,28 +183,17 @@ export interface IConfigurationWire { type UfcResponseType = IUniversalFlagConfigResponse | IBanditParametersResponse; // The UFC responses are JSON-encoded strings so we can treat them as opaque blobs, but we also want to enforce type safety. -type ResponseString = string & { - readonly __brand: unique symbol; - readonly __type: T; -}; +type UFCResponseString = JsonString; /** * A wrapper around a server response that includes the response, etag, and fetchedAt timestamp. */ interface IConfigResponse { - readonly response: ResponseString; // JSON-encoded server response + readonly response: UFCResponseString; // JSON-encoded server response readonly etag?: string; // Entity Tag - denotes a snapshot or version of the config. readonly fetchedAt?: string; // ISO timestamp for when this config was fetched } -export function inflateResponse(response: ResponseString): T { - return JSON.parse(response) as T; -} - -export function deflateResponse(value: T): ResponseString { - return JSON.stringify(value) as ResponseString; -} - export class ConfigurationWireV1 implements IConfigurationWire { public readonly version = 1; @@ -200,10 +203,6 @@ export class ConfigurationWireV1 implements IConfigurationWire { readonly bandits?: IConfigResponse, ) {} - public static fromString(stringifiedPayload: string): IConfigurationWire { - return JSON.parse(stringifiedPayload) as IConfigurationWire; - } - public static fromResponses( flagConfig: IUniversalFlagConfigResponse, banditConfig?: IBanditParametersResponse, @@ -213,13 +212,13 @@ export class ConfigurationWireV1 implements IConfigurationWire { return new ConfigurationWireV1( undefined, { - response: deflateResponse(flagConfig), + response: deflateJsonObject(flagConfig), fetchedAt: new Date().toISOString(), etag: flagConfigEtag, }, banditConfig ? { - response: deflateResponse(banditConfig), + response: deflateJsonObject(banditConfig), fetchedAt: new Date().toISOString(), etag: banditConfigEtag, } diff --git a/src/configuration-wire/json-util.ts b/src/configuration-wire/json-util.ts new file mode 100644 index 00000000..f7a84ac2 --- /dev/null +++ b/src/configuration-wire/json-util.ts @@ -0,0 +1,12 @@ +export type JsonString = string & { + readonly __brand: unique symbol; + readonly __type: T; +}; + +export function inflateJsonObject(response: JsonString): T { + return JSON.parse(response) as T; +} + +export function deflateJsonObject(value: T): JsonString { + return JSON.stringify(value) as JsonString; +} diff --git a/src/index.ts b/src/index.ts index 9a0f65e7..58ef7f13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ import { ConfigurationStoreBundle } from './configuration-store/i-configuration- import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store'; import { ConfigurationWireHelper } from './configuration-wire/configuration-wire-helper'; import { + configurationFromString, IConfigurationWire, IObfuscatedPrecomputedConfigurationResponse, IPrecomputedConfigurationResponse, @@ -159,6 +160,7 @@ export { PrecomputedFlag, FlagKey, ConfigurationWireHelper, + configurationFromString, // Test helpers decodePrecomputedFlag, diff --git a/test/testHelpers.ts b/test/testHelpers.ts index 8b8be06d..44d34309 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -19,9 +19,9 @@ const TEST_CONFIGURATION_WIRE_DATA_DIR = './test/data/configuration-wire/'; const MOCK_PRECOMPUTED_FILENAME = 'precomputed-v1'; export const MOCK_PRECOMPUTED_WIRE_FILE = `${MOCK_PRECOMPUTED_FILENAME}.json`; export const MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE = `${MOCK_PRECOMPUTED_FILENAME}-deobfuscated.json`; -export const SHARED_BOOTSTRAP_FLAGS_FILE = 'bootstrap-flags-v1.json'; -export const SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE = 'bootstrap-flags-obfuscated-v1.json'; -export const SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE = 'bootstrap-bandit-flags-v1.json'; +export const FLAGS_WIRE_FILE = 'flags-v1.json'; +export const OBFUSCATED_FLAGS_WIRE_FILE = 'flags-v1-obfuscated.json'; +export const BANDITS_WIRE_FILE = 'bandit-flags-v1.json'; export interface SubjectTestCase { subjectKey: string;