Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore:refactor JSON utils, add configurationFromString method for exporting #255

Open
wants to merge 1 commit into
base: tp/config-mgr
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/client/eppo-client-with-bandits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
IConfigurationWire,
IPrecomputedConfiguration,
IObfuscatedPrecomputedConfigurationResponse,
ConfigurationWireV1,
ConfigurationWireV1, configurationFromString,

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Insert `⏎·`
} from '../configuration-wire/configuration-wire-types';
import { Evaluator, FlagEvaluation } from '../evaluator';
import {
Expand Down Expand Up @@ -137,7 +137,7 @@
}

describe('bootstrapped client', () => {
const banditFlagsConfig = ConfigurationWireV1.fromString(
const banditFlagsConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE),
);

Expand Down
8 changes: 3 additions & 5 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -321,8 +321,6 @@ describe('EppoClient E2E test', () => {
});
});

const testCases = testCasesByFileName<IAssignmentTestCase>(ASSIGNMENT_TEST_DATA_DIR);

function testCasesAgainstClient(client: EppoClient, testCase: IAssignmentTestCase) {
const { flag, variationType, defaultValue, subjects } = testCase;

Expand Down Expand Up @@ -358,10 +356,10 @@ describe('EppoClient E2E test', () => {
const testCases = testCasesByFileName<IAssignmentTestCase>(ASSIGNMENT_TEST_DATA_DIR);

describe('boostrapped client', () => {
const bootstrapFlagsConfig = ConfigurationWireV1.fromString(
const bootstrapFlagsConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_FILE),
);
const bootstrapFlagsObfuscatedConfig = ConfigurationWireV1.fromString(
const bootstrapFlagsObfuscatedConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE),
);

Expand Down
6 changes: 3 additions & 3 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
21 changes: 11 additions & 10 deletions src/configuration-wire/configuration-wire-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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);
});

Expand All @@ -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);
Expand Down
39 changes: 19 additions & 20 deletions src/configuration-wire/configuration-wire-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client';
import {
Environment,
FormatEnum,
Expand All @@ -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>,
): IConfigurationWire {
return inflateJsonObject(payloadString as JsonString<IConfigurationWire>);
}

// Base interface for all configuration responses
interface IBasePrecomputedConfigurationResponse {
readonly format: FormatEnum.PRECOMPUTED;
Expand Down Expand Up @@ -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<T extends UfcResponseType> = string & {
readonly __brand: unique symbol;
readonly __type: T;
};
type UFCResponseString<T extends UfcResponseType> = JsonString<T>;

/**
* A wrapper around a server response that includes the response, etag, and fetchedAt timestamp.
*/
interface IConfigResponse<T extends UfcResponseType> {
readonly response: ResponseString<T>; // JSON-encoded server response
readonly response: UFCResponseString<T>; // 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<T extends UfcResponseType>(response: ResponseString<T>): T {
return JSON.parse(response) as T;
}

export function deflateResponse<T extends UfcResponseType>(value: T): ResponseString<T> {
return JSON.stringify(value) as ResponseString<T>;
}

export class ConfigurationWireV1 implements IConfigurationWire {
public readonly version = 1;

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

public static fromString(stringifiedPayload: string): IConfigurationWire {
return JSON.parse(stringifiedPayload) as IConfigurationWire;
}

public static fromResponses(
flagConfig: IUniversalFlagConfigResponse,
banditConfig?: IBanditParametersResponse,
Expand All @@ -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,
}
Expand Down
12 changes: 12 additions & 0 deletions src/configuration-wire/json-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type JsonString<T> = string & {
readonly __brand: unique symbol;
readonly __type: T;
};

export function inflateJsonObject<T>(response: JsonString<T>): T {
return JSON.parse(response) as T;
}

export function deflateJsonObject<T>(value: T): JsonString<T> {
return JSON.stringify(value) as JsonString<T>;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -159,6 +160,7 @@ export {
PrecomputedFlag,
FlagKey,
ConfigurationWireHelper,
configurationFromString,

// Test helpers
decodePrecomputedFlag,
Expand Down
Loading