Skip to content

Commit 467364b

Browse files
authored
chore:refactor JSON utils, add configurationFromString method for exporting (#255)
* refactor JSON utils, add configurationFromString method for exporting * catch up to test data files
1 parent e256730 commit 467364b

File tree

8 files changed

+61
-47
lines changed

8 files changed

+61
-47
lines changed

src/client/eppo-client-with-bandits.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
BanditTestCase,
99
BANDIT_TEST_DATA_DIR,
1010
readMockConfigurationWireResponse,
11-
SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE,
11+
BANDITS_WIRE_FILE,
1212
} from '../../test/testHelpers';
1313
import ApiEndpoints from '../api-endpoints';
1414
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
@@ -21,7 +21,7 @@ import {
2121
IConfigurationWire,
2222
IPrecomputedConfiguration,
2323
IObfuscatedPrecomputedConfigurationResponse,
24-
ConfigurationWireV1,
24+
configurationFromString,
2525
} from '../configuration-wire/configuration-wire-types';
2626
import { Evaluator, FlagEvaluation } from '../evaluator';
2727
import {
@@ -137,8 +137,8 @@ describe('EppoClient Bandits E2E test', () => {
137137
}
138138

139139
describe('bootstrapped client', () => {
140-
const banditFlagsConfig = ConfigurationWireV1.fromString(
141-
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE),
140+
const banditFlagsConfig = configurationFromString(
141+
readMockConfigurationWireResponse(BANDITS_WIRE_FILE),
142142
);
143143

144144
let client: EppoClient;

src/client/eppo-client.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
1111
readMockConfigurationWireResponse,
1212
readMockUFCResponse,
13-
SHARED_BOOTSTRAP_FLAGS_FILE,
14-
SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE,
13+
FLAGS_WIRE_FILE,
14+
OBFUSCATED_FLAGS_WIRE_FILE,
1515
SubjectTestCase,
1616
testCasesByFileName,
1717
validateTestAssignments,
@@ -21,7 +21,7 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache';
2121
import { IConfigurationStore } from '../configuration-store/configuration-store';
2222
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
2323
import {
24-
ConfigurationWireV1,
24+
configurationFromString,
2525
IConfigurationWire,
2626
IObfuscatedPrecomputedConfigurationResponse,
2727
ObfuscatedPrecomputedConfigurationResponse,
@@ -356,11 +356,11 @@ describe('EppoClient E2E test', () => {
356356
const testCases = testCasesByFileName<IAssignmentTestCase>(ASSIGNMENT_TEST_DATA_DIR);
357357

358358
describe('boostrapped client', () => {
359-
const bootstrapFlagsConfig = ConfigurationWireV1.fromString(
360-
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_FILE),
359+
const bootstrapFlagsConfig = configurationFromString(
360+
readMockConfigurationWireResponse(FLAGS_WIRE_FILE),
361361
);
362-
const bootstrapFlagsObfuscatedConfig = ConfigurationWireV1.fromString(
363-
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE),
362+
const bootstrapFlagsObfuscatedConfig = configurationFromString(
363+
readMockConfigurationWireResponse(OBFUSCATED_FLAGS_WIRE_FILE),
364364
);
365365

366366
describe('Not obfuscated', () => {

src/client/eppo-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.stor
2121
import {
2222
ConfigurationWireV1,
2323
IConfigurationWire,
24-
inflateResponse,
2524
IPrecomputedConfiguration,
2625
PrecomputedConfiguration,
2726
} from '../configuration-wire/configuration-wire-types';
27+
import { inflateJsonObject } from '../configuration-wire/json-util';
2828
import {
2929
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
3030
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
@@ -330,11 +330,11 @@ export default class EppoClient {
330330
if (!configuration.config) {
331331
throw new Error('Flag configuration not provided');
332332
}
333-
const flagConfigResponse: IUniversalFlagConfigResponse = inflateResponse(
333+
const flagConfigResponse: IUniversalFlagConfigResponse = inflateJsonObject(
334334
configuration.config.response,
335335
);
336336
const banditParamResponse: IBanditParametersResponse | undefined = configuration.bandits
337-
? inflateResponse(configuration.bandits.response)
337+
? inflateJsonObject(configuration.bandits.response)
338338
: undefined;
339339

340340
// This method runs async because the configuration stores potentially have an async write at the end of updating

src/configuration-wire/configuration-wire-types.spec.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
77
import { FormatEnum } from '../interfaces';
88

9-
import { ConfigurationWireV1, deflateResponse, inflateResponse } from './configuration-wire-types';
9+
import { ConfigurationWireV1 } from './configuration-wire-types';
10+
import { deflateJsonObject, inflateJsonObject } from './json-util';
1011

1112
describe('Response String Type Safety', () => {
1213
const mockFlagConfig: IUniversalFlagConfigResponse = readMockUFCResponse(
@@ -18,22 +19,22 @@ describe('Response String Type Safety', () => {
1819

1920
describe('deflateResponse and inflateResponse', () => {
2021
it('should correctly serialize and deserialize flag config', () => {
21-
const serialized = deflateResponse(mockFlagConfig);
22-
const deserialized = inflateResponse(serialized);
22+
const serialized = deflateJsonObject(mockFlagConfig);
23+
const deserialized = inflateJsonObject(serialized);
2324

2425
expect(deserialized).toEqual(mockFlagConfig);
2526
});
2627

2728
it('should correctly serialize and deserialize bandit config', () => {
28-
const serialized = deflateResponse(mockBanditConfig);
29-
const deserialized = inflateResponse(serialized);
29+
const serialized = deflateJsonObject(mockBanditConfig);
30+
const deserialized = inflateJsonObject(serialized);
3031

3132
expect(deserialized).toEqual(mockBanditConfig);
3233
});
3334

3435
it('should maintain type information through serialization', () => {
35-
const serialized = deflateResponse(mockFlagConfig);
36-
const deserialized = inflateResponse(serialized);
36+
const serialized = deflateJsonObject(mockFlagConfig);
37+
const deserialized = inflateJsonObject(serialized);
3738

3839
// TypeScript compilation check: these should work
3940
expect(deserialized.format).toBe(FormatEnum.SERVER);
@@ -54,7 +55,7 @@ describe('Response String Type Safety', () => {
5455
if (!wirePacket.config) {
5556
fail('Flag config not present in ConfigurationWire');
5657
}
57-
const deserializedConfig = inflateResponse(wirePacket.config.response);
58+
const deserializedConfig = inflateJsonObject(wirePacket.config.response);
5859
expect(deserializedConfig).toEqual(mockFlagConfig);
5960
});
6061

@@ -80,8 +81,8 @@ describe('Response String Type Safety', () => {
8081
expect(wirePacket.bandits.etag).toBe('bandit-etag');
8182

8283
// Verify we can deserialize both responses
83-
const deserializedConfig = inflateResponse(wirePacket.config.response);
84-
const deserializedBandits = inflateResponse(wirePacket.bandits.response);
84+
const deserializedConfig = inflateJsonObject(wirePacket.config.response);
85+
const deserializedBandits = inflateJsonObject(wirePacket.bandits.response);
8586

8687
expect(deserializedConfig).toEqual(mockFlagConfig);
8788
expect(deserializedBandits).toEqual(mockBanditConfig);

src/configuration-wire/configuration-wire-types.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
1+
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client';
22
import {
33
Environment,
44
FormatEnum,
@@ -9,6 +9,20 @@ import {
99
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from '../obfuscation';
1010
import { ContextAttributes, FlagKey, HashedFlagKey } from '../types';
1111

12+
import { deflateJsonObject, inflateJsonObject, JsonString } from './json-util';
13+
14+
/**
15+
* Builds an `IConfigurationWire` instance from the payload string.
16+
* To generate the payload string, see `ConfigurationWireHelper.fetchBootstrapConfiguration`.
17+
*
18+
* @param payloadString
19+
*/
20+
export function configurationFromString(
21+
payloadString: string | JsonString<IConfigurationWire>,
22+
): IConfigurationWire {
23+
return inflateJsonObject(payloadString as JsonString<IConfigurationWire>);
24+
}
25+
1226
// Base interface for all configuration responses
1327
interface IBasePrecomputedConfigurationResponse {
1428
readonly format: FormatEnum.PRECOMPUTED;
@@ -169,28 +183,17 @@ export interface IConfigurationWire {
169183
type UfcResponseType = IUniversalFlagConfigResponse | IBanditParametersResponse;
170184

171185
// The UFC responses are JSON-encoded strings so we can treat them as opaque blobs, but we also want to enforce type safety.
172-
type ResponseString<T extends UfcResponseType> = string & {
173-
readonly __brand: unique symbol;
174-
readonly __type: T;
175-
};
186+
type UFCResponseString<T extends UfcResponseType> = JsonString<T>;
176187

177188
/**
178189
* A wrapper around a server response that includes the response, etag, and fetchedAt timestamp.
179190
*/
180191
interface IConfigResponse<T extends UfcResponseType> {
181-
readonly response: ResponseString<T>; // JSON-encoded server response
192+
readonly response: UFCResponseString<T>; // JSON-encoded server response
182193
readonly etag?: string; // Entity Tag - denotes a snapshot or version of the config.
183194
readonly fetchedAt?: string; // ISO timestamp for when this config was fetched
184195
}
185196

186-
export function inflateResponse<T extends UfcResponseType>(response: ResponseString<T>): T {
187-
return JSON.parse(response) as T;
188-
}
189-
190-
export function deflateResponse<T extends UfcResponseType>(value: T): ResponseString<T> {
191-
return JSON.stringify(value) as ResponseString<T>;
192-
}
193-
194197
export class ConfigurationWireV1 implements IConfigurationWire {
195198
public readonly version = 1;
196199

@@ -200,10 +203,6 @@ export class ConfigurationWireV1 implements IConfigurationWire {
200203
readonly bandits?: IConfigResponse<IBanditParametersResponse>,
201204
) {}
202205

203-
public static fromString(stringifiedPayload: string): IConfigurationWire {
204-
return JSON.parse(stringifiedPayload) as IConfigurationWire;
205-
}
206-
207206
public static fromResponses(
208207
flagConfig: IUniversalFlagConfigResponse,
209208
banditConfig?: IBanditParametersResponse,
@@ -213,13 +212,13 @@ export class ConfigurationWireV1 implements IConfigurationWire {
213212
return new ConfigurationWireV1(
214213
undefined,
215214
{
216-
response: deflateResponse(flagConfig),
215+
response: deflateJsonObject(flagConfig),
217216
fetchedAt: new Date().toISOString(),
218217
etag: flagConfigEtag,
219218
},
220219
banditConfig
221220
? {
222-
response: deflateResponse(banditConfig),
221+
response: deflateJsonObject(banditConfig),
223222
fetchedAt: new Date().toISOString(),
224223
etag: banditConfigEtag,
225224
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type JsonString<T> = string & {
2+
readonly __brand: unique symbol;
3+
readonly __type: T;
4+
};
5+
6+
export function inflateJsonObject<T>(response: JsonString<T>): T {
7+
return JSON.parse(response) as T;
8+
}
9+
10+
export function deflateJsonObject<T>(value: T): JsonString<T> {
11+
return JSON.stringify(value) as JsonString<T>;
12+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { ConfigurationStoreBundle } from './configuration-store/i-configuration-
3636
import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store';
3737
import { ConfigurationWireHelper } from './configuration-wire/configuration-wire-helper';
3838
import {
39+
configurationFromString,
3940
IConfigurationWire,
4041
IObfuscatedPrecomputedConfigurationResponse,
4142
IPrecomputedConfigurationResponse,
@@ -159,6 +160,7 @@ export {
159160
PrecomputedFlag,
160161
FlagKey,
161162
ConfigurationWireHelper,
163+
configurationFromString,
162164

163165
// Test helpers
164166
decodePrecomputedFlag,

test/testHelpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ const TEST_CONFIGURATION_WIRE_DATA_DIR = './test/data/configuration-wire/';
1919
const MOCK_PRECOMPUTED_FILENAME = 'precomputed-v1';
2020
export const MOCK_PRECOMPUTED_WIRE_FILE = `${MOCK_PRECOMPUTED_FILENAME}.json`;
2121
export const MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE = `${MOCK_PRECOMPUTED_FILENAME}-deobfuscated.json`;
22-
export const SHARED_BOOTSTRAP_FLAGS_FILE = 'bootstrap-flags-v1.json';
23-
export const SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE = 'bootstrap-flags-obfuscated-v1.json';
24-
export const SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE = 'bootstrap-bandit-flags-v1.json';
22+
export const FLAGS_WIRE_FILE = 'flags-v1.json';
23+
export const OBFUSCATED_FLAGS_WIRE_FILE = 'flags-v1-obfuscated.json';
24+
export const BANDITS_WIRE_FILE = 'bandit-flags-v1.json';
2525

2626
export interface SubjectTestCase {
2727
subjectKey: string;

0 commit comments

Comments
 (0)