Skip to content
Next Next commit
Update SDK to use type-specific getters
  • Loading branch information
erikeldridge committed Mar 30, 2024
commit 5732ea9a4df1d5e66e85ade0fc5d2e78b790c48d
91 changes: 88 additions & 3 deletions src/remote-config/remote-config-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export interface ServerTemplateOptions {
* intended before it connects to the Remote Config backend, and so that
* default values are available if none are set on the backend.
*/
defaultConfig?: ServerConfig,
defaultConfig?: { [key: string]: string | number | boolean };

/**
* Enables integrations to use template data loaded independently. For
Expand All @@ -385,7 +385,7 @@ export interface ServerTemplate {
/**
* A {@link ServerConfig} that contains default Config values.
*/
defaultConfig: ServerConfig;
defaultConfig: { [key: string]: string | number | boolean };

/**
* Evaluates the current template to produce a {@link ServerConfig}.
Expand Down Expand Up @@ -537,4 +537,89 @@ export interface ListVersionsOptions {
/**
* Represents the configuration produced by evaluating a server template.
*/
export type ServerConfig = { [key: string]: string | boolean | number }
export interface ServerConfig {

/**
* Gets the value for the given key as a boolean.
*
* Convenience method for calling <code>serverConfig.getValue(key).asBoolean()</code>.
*
* @param key - The name of the parameter.
*
* @returns The value for the given key as a boolean.
*/
getBoolean(key: string): boolean;

/**
* Gets the value for the given key as a number.
*
* Convenience method for calling <code>serverConfig.getValue(key).asNumber()</code>.
*
* @param key - The name of the parameter.
*
* @returns The value for the given key as a number.
*/
getNumber(key: string): number;

/**
* Gets the value for the given key as a string.
* Convenience method for calling <code>serverConfig.getValue(key).asString()</code>.
*
* @param key - The name of the parameter.
*
* @returns The value for the given key as a string.
*/
getString(key: string): string;

/**
* Gets the {@link Value} for the given key.
*
* @param key - The name of the parameter.
*
* @returns The value for the given key.
*/
getValue(key: string): Value;
}

/**
* Wraps a parameter value with metadata and type-safe getters.
*
* Type-safe getters insulate application logic from remote
* changes to parameter names and types.
*/
export interface Value {

/**
* Gets the value as a boolean.
*
* The following values (case insensitive) are interpreted as true:
* "1", "true", "t", "yes", "y", "on". Other values are interpreted as false.
*/
asBoolean(): boolean;

/**
* Gets the value as a number. Comparable to calling <code>Number(value) || 0</code>.
*/
asNumber(): number;

/**
* Gets the value as a string.
*/
asString(): string;

/**
* Gets the {@link ValueSource} for the given key.
*/
getSource(): ValueSource;
}

/**
* Indicates the source of a value.
*
* <ul>
* <li>"static" indicates the value was defined by a static constant.</li>
* <li>"default" indicates the value was defined by default config.</li>
* <li>"remote" indicates the value was defined by fetched config.</li>
* </ul>
*/
export type ValueSource = 'static' | 'default' | 'remote';
94 changes: 55 additions & 39 deletions src/remote-config/remote-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import {
Version,
ExplicitParameterValue,
InAppDefaultValue,
ParameterValueType,
ServerConfig,
RemoteConfigParameterValue,
EvaluationContext,
ServerTemplateData,
ServerTemplateOptions,
NamedCondition,
Value,
ValueSource,
} from './remote-config-api';

/**
Expand Down Expand Up @@ -296,7 +297,7 @@ class ServerTemplateImpl implements ServerTemplate {
constructor(
private readonly apiClient: RemoteConfigApiClient,
private readonly conditionEvaluator: ConditionEvaluator,
public readonly defaultConfig: ServerConfig = {}
public readonly defaultConfig: { [key: string]: string | number | boolean } = {}
) { }

/**
Expand Down Expand Up @@ -326,10 +327,10 @@ class ServerTemplateImpl implements ServerTemplate {
const evaluatedConditions = this.conditionEvaluator.evaluateConditions(
this.cache.conditions, context);

const evaluatedConfig: ServerConfig = {};
const evaluatedConfig: { [key: string]: string } = {};

for (const [key, parameter] of Object.entries(this.cache.parameters)) {
const { conditionalValues, defaultValue, valueType } = parameter;
const { conditionalValues, defaultValue } = parameter;

// Supports parameters with no conditional values.
const normalizedConditionalValues = conditionalValues || {};
Expand All @@ -352,7 +353,7 @@ class ServerTemplateImpl implements ServerTemplate {

if (parameterValueWrapper) {
const parameterValue = (parameterValueWrapper as ExplicitParameterValue).value;
evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterValue);
evaluatedConfig[key] = parameterValue;
continue;
}

Expand All @@ -367,47 +368,62 @@ class ServerTemplateImpl implements ServerTemplate {
}

const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value;
evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
evaluatedConfig[key] = parameterDefaultValue;
}

const mergedConfig = {};

// Merges default config and rendered config, prioritizing the latter.
Object.assign(mergedConfig, this.defaultConfig, evaluatedConfig);

// Enables config to be a convenient object, but with the ability to perform additional
// functionality when a value is retrieved.
const proxyHandler = {
get(target: ServerConfig, prop: string) {
return target[prop];
}
};

return new Proxy(mergedConfig, proxyHandler);
return new ServerConfigImpl(evaluatedConfig, this.defaultConfig);
}
}

/**
* Private helper method that coerces a parameter value string to the {@link ParameterValueType}.
*/
private parseRemoteConfigParameterValue(parameterType: ParameterValueType | undefined,
parameterValue: string): string | number | boolean {
const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
const DEFAULT_VALUE_FOR_NUMBER = 0;
const DEFAULT_VALUE_FOR_STRING = '';

if (parameterType === 'BOOLEAN') {
return BOOLEAN_TRUTHY_VALUES.indexOf(parameterValue) >= 0;
} else if (parameterType === 'NUMBER') {
const num = Number(parameterValue);
if (isNaN(num)) {
return DEFAULT_VALUE_FOR_NUMBER;
}
return num;
class ServerConfigImpl implements ServerConfig {
constructor(
private readonly evaluatedConfig: { [key: string]: string },
private readonly defaultConfig: { [key: string]: string | number | boolean }
){}
getBoolean(key: string): boolean {
return this.getValue(key).asBoolean();
}
getNumber(key: string): number {
return this.getValue(key).asNumber();
}
getString(key: string): string {
return this.getValue(key).asString();
}
getValue(key: string): Value {
if (key in this.evaluatedConfig) {
return new ValueImpl('remote', this.evaluatedConfig[key]);
} else if (key in this.defaultConfig) {
return new ValueImpl('default', String(this.defaultConfig[key]));
} else {
// Treat everything else as string
return parameterValue || DEFAULT_VALUE_FOR_STRING;
return new ValueImpl('static');
}
}
}

class ValueImpl implements Value {
static BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
static DEFAULT_VALUE_FOR_NUMBER = 0;
static DEFAULT_VALUE_FOR_STRING = '';
constructor(
private readonly source: ValueSource,
private readonly value = ValueImpl.DEFAULT_VALUE_FOR_STRING){}
asBoolean(): boolean {
return ValueImpl.BOOLEAN_TRUTHY_VALUES.indexOf(this.value) >= 0;
}
asNumber(): number {
const num = Number(this.value);
if (isNaN(num)) {
return ValueImpl.DEFAULT_VALUE_FOR_NUMBER;
}
return num;
}
asString(): string {
return this.value;
}
getSource(): ValueSource {
return this.source;
}

}

/**
Expand Down
25 changes: 13 additions & 12 deletions test/unit/remote-config/remote-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -921,10 +921,9 @@ describe('RemoteConfig', () => {
return remoteConfig.getServerTemplate()
.then((template: ServerTemplate) => {
const config = template.evaluate!();
expect(config.dog_type).to.equal('corgi');
expect(config.dog_type_enabled).to.equal(true);
expect(config.dog_age).to.equal(22);
expect(config.dog_jsonified).to.equal('{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}');
expect(config.getString('dog_type')).to.equal('corgi');
expect(config.getBoolean('dog_type_enabled')).to.equal(true);
expect(config.getNumber('dog_age')).to.equal(22);
});
});

Expand Down Expand Up @@ -963,7 +962,7 @@ describe('RemoteConfig', () => {
}
});
const config = template.evaluate();
expect(config.is_enabled).to.be.true;
expect(config.getBoolean('is_enabled')).to.be.true;
});

it('honors condition order', () => {
Expand Down Expand Up @@ -1025,7 +1024,7 @@ describe('RemoteConfig', () => {
}
});
const config = template.evaluate();
expect(config.dog_type).to.eq('corgi');
expect(config.getString('dog_type')).to.eq('corgi');
});

it('uses local default if parameter not in template', () => {
Expand All @@ -1040,7 +1039,7 @@ describe('RemoteConfig', () => {
})
.then((template: ServerTemplate) => {
const config = template.evaluate!();
expect(config.dog_coat).to.equal(template.defaultConfig.dog_coat);
expect(config.getString('dog_coat')).to.equal(template.defaultConfig.dog_coat);
});
});

Expand All @@ -1056,7 +1055,8 @@ describe('RemoteConfig', () => {
})
.then((template: ServerTemplate) => {
const config = template.evaluate!();
expect(config.dog_no_remote_default_value).to.equal(template.defaultConfig.dog_no_remote_default_value);
expect(config.getString('dog_no_remote_default_value')).to.equal(
template.defaultConfig.dog_no_remote_default_value);
});
});

Expand All @@ -1072,7 +1072,8 @@ describe('RemoteConfig', () => {
})
.then((template: ServerTemplate) => {
const config = template.evaluate!();
expect(config.dog_use_inapp_default).to.equal(template.defaultConfig.dog_use_inapp_default);
expect(config.getString('dog_use_inapp_default')).to.equal(
template.defaultConfig.dog_use_inapp_default);
});
});

Expand Down Expand Up @@ -1102,7 +1103,7 @@ describe('RemoteConfig', () => {

let config = template.evaluate();

expect(config.dog_type).to.equal('pug');
expect(config.getString('dog_type')).to.equal('pug');

response.parameters = {
dog_type: {
Expand All @@ -1117,7 +1118,7 @@ describe('RemoteConfig', () => {

config = template.evaluate();

expect(config.dog_type).to.equal('corgi');
expect(config.getString('dog_type')).to.equal('corgi');
});

it('overrides local default when remote value exists', () => {
Expand Down Expand Up @@ -1146,7 +1147,7 @@ describe('RemoteConfig', () => {
.then((template: ServerTemplate) => {
const config = template.evaluate();
// Asserts remote value overrides local default.
expect(config.dog_type_enabled).to.be.true;
expect(config.getBoolean('dog_type_enabled')).to.be.true;
});
});
});
Expand Down