Skip to content

Commit 8dbf86f

Browse files
authored
Define type-specific getters for SSRC (#2519)
RC's existing SDKs define type-specific getters, like getBoolean. These aren't idiomatic for TS/JS, but have a couple advantages: 1. RC param names, values and types are mutable remotely, so a simple object can’t guarantee a strict type for application logic. A formal schema would address this, but feels excessive for the common case. Type-specific methods are consistent with RC's current SDKs and ensure appropriate types for application logic. 2. RC Android and iOS SDKs log events when personalized values are used. A method interface facilitates such additional functionality
1 parent 0aca056 commit 8dbf86f

File tree

8 files changed

+428
-99
lines changed

8 files changed

+428
-99
lines changed

etc/firebase-admin.remote-config.api.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export interface AndCondition {
1313
conditions?: Array<OneOfCondition>;
1414
}
1515

16+
// @public
17+
export type DefaultConfig = {
18+
[key: string]: string | number | boolean;
19+
};
20+
1621
// @public
1722
export type EvaluationContext = {
1823
randomizationId?: string;
@@ -30,7 +35,7 @@ export function getRemoteConfig(app?: App): RemoteConfig;
3035

3136
// @public
3237
export interface GetServerTemplateOptions {
33-
defaultConfig?: ServerConfig;
38+
defaultConfig?: DefaultConfig;
3439
}
3540

3641
// @public
@@ -169,9 +174,12 @@ export interface RemoteConfigUser {
169174
}
170175

171176
// @public
172-
export type ServerConfig = {
173-
[key: string]: string | boolean | number;
174-
};
177+
export interface ServerConfig {
178+
getBoolean(key: string): boolean;
179+
getNumber(key: string): number;
180+
getString(key: string): string;
181+
getValue(key: string): Value;
182+
}
175183

176184
// @public
177185
export interface ServerTemplate {
@@ -193,6 +201,17 @@ export interface ServerTemplateData {
193201
// @public
194202
export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL';
195203

204+
// @public
205+
export interface Value {
206+
asBoolean(): boolean;
207+
asNumber(): number;
208+
asString(): string;
209+
getSource(): ValueSource;
210+
}
211+
212+
// @public
213+
export type ValueSource = 'static' | 'default' | 'remote';
214+
196215
// @public
197216
export interface Version {
198217
description?: string;

src/remote-config/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { RemoteConfig } from './remote-config';
2626

2727
export {
2828
AndCondition,
29+
DefaultConfig,
2930
EvaluationContext,
3031
ExplicitParameterValue,
3132
GetServerTemplateOptions,
@@ -50,6 +51,8 @@ export {
5051
ServerTemplate,
5152
ServerTemplateData,
5253
TagColor,
54+
Value,
55+
ValueSource,
5356
Version,
5457
} from './remote-config-api';
5558
export { RemoteConfig } from './remote-config';
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Copyright 2024 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
import {
20+
Value,
21+
ValueSource,
22+
} from '../remote-config-api';
23+
24+
/**
25+
* Implements type-safe getters for parameter values.
26+
*
27+
* Visible for testing.
28+
*
29+
* @internal
30+
*/
31+
export class ValueImpl implements Value {
32+
public static readonly DEFAULT_VALUE_FOR_BOOLEAN = false;
33+
public static readonly DEFAULT_VALUE_FOR_STRING = '';
34+
public static readonly DEFAULT_VALUE_FOR_NUMBER = 0;
35+
public static readonly BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
36+
constructor(
37+
private readonly source: ValueSource,
38+
private readonly value = ValueImpl.DEFAULT_VALUE_FOR_STRING) { }
39+
asString(): string {
40+
return this.value;
41+
}
42+
asBoolean(): boolean {
43+
if (this.source === 'static') {
44+
return ValueImpl.DEFAULT_VALUE_FOR_BOOLEAN;
45+
}
46+
return ValueImpl.BOOLEAN_TRUTHY_VALUES.indexOf(this.value.toLowerCase()) >= 0;
47+
}
48+
asNumber(): number {
49+
if (this.source === 'static') {
50+
return ValueImpl.DEFAULT_VALUE_FOR_NUMBER;
51+
}
52+
const num = Number(this.value);
53+
if (isNaN(num)) {
54+
return ValueImpl.DEFAULT_VALUE_FOR_NUMBER;
55+
}
56+
return num;
57+
}
58+
getSource(): ValueSource {
59+
return this.source;
60+
}
61+
}

src/remote-config/remote-config-api.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ export interface GetServerTemplateOptions {
361361
* intended before it connects to the Remote Config backend, and so that
362362
* default values are available if none are set on the backend.
363363
*/
364-
defaultConfig?: ServerConfig,
364+
defaultConfig?: DefaultConfig;
365365
}
366366

367367
/**
@@ -541,4 +541,98 @@ export interface ListVersionsOptions {
541541
/**
542542
* Represents the configuration produced by evaluating a server template.
543543
*/
544-
export type ServerConfig = { [key: string]: string | boolean | number }
544+
export interface ServerConfig {
545+
546+
/**
547+
* Gets the value for the given key as a boolean.
548+
*
549+
* Convenience method for calling <code>serverConfig.getValue(key).asBoolean()</code>.
550+
*
551+
* @param key - The name of the parameter.
552+
*
553+
* @returns The value for the given key as a boolean.
554+
*/
555+
getBoolean(key: string): boolean;
556+
557+
/**
558+
* Gets the value for the given key as a number.
559+
*
560+
* Convenience method for calling <code>serverConfig.getValue(key).asNumber()</code>.
561+
*
562+
* @param key - The name of the parameter.
563+
*
564+
* @returns The value for the given key as a number.
565+
*/
566+
getNumber(key: string): number;
567+
568+
/**
569+
* Gets the value for the given key as a string.
570+
* Convenience method for calling <code>serverConfig.getValue(key).asString()</code>.
571+
*
572+
* @param key - The name of the parameter.
573+
*
574+
* @returns The value for the given key as a string.
575+
*/
576+
getString(key: string): string;
577+
578+
/**
579+
* Gets the {@link Value} for the given key.
580+
*
581+
* Ensures application logic will always have a type-safe reference,
582+
* even if the parameter is removed remotely.
583+
*
584+
* @param key - The name of the parameter.
585+
*
586+
* @returns The value for the given key.
587+
*/
588+
getValue(key: string): Value;
589+
}
590+
591+
/**
592+
* Wraps a parameter value with metadata and type-safe getters.
593+
*
594+
* Type-safe getters insulate application logic from remote
595+
* changes to parameter names and types.
596+
*/
597+
export interface Value {
598+
599+
/**
600+
* Gets the value as a boolean.
601+
*
602+
* The following values (case insensitive) are interpreted as true:
603+
* "1", "true", "t", "yes", "y", "on". Other values are interpreted as false.
604+
*/
605+
asBoolean(): boolean;
606+
607+
/**
608+
* Gets the value as a number. Comparable to calling <code>Number(value) || 0</code>.
609+
*/
610+
asNumber(): number;
611+
612+
/**
613+
* Gets the value as a string.
614+
*/
615+
asString(): string;
616+
617+
/**
618+
* Gets the {@link ValueSource} for the given key.
619+
*/
620+
getSource(): ValueSource;
621+
}
622+
623+
/**
624+
* Indicates the source of a value.
625+
*
626+
* <ul>
627+
* <li>"static" indicates the value was defined by a static constant.</li>
628+
* <li>"default" indicates the value was defined by default config.</li>
629+
* <li>"remote" indicates the value was defined by config produced by
630+
* evaluating a template.</li>
631+
* </ul>
632+
*/
633+
export type ValueSource = 'static' | 'default' | 'remote';
634+
635+
/**
636+
* Defines the format for in-app default parameter values.
637+
*/
638+
export type DefaultConfig = { [key: string]: string | number | boolean };

src/remote-config/remote-config.ts

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { App } from '../app';
1818
import * as validator from '../utils/validator';
1919
import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal';
2020
import { ConditionEvaluator } from './condition-evaluator-internal';
21+
import { ValueImpl } from './internal/value-impl';
2122
import {
2223
ListVersionsOptions,
2324
ListVersionsResult,
@@ -30,12 +31,13 @@ import {
3031
Version,
3132
ExplicitParameterValue,
3233
InAppDefaultValue,
33-
ParameterValueType,
3434
ServerConfig,
3535
RemoteConfigParameterValue,
3636
EvaluationContext,
3737
ServerTemplateData,
3838
NamedCondition,
39+
Value,
40+
DefaultConfig,
3941
GetServerTemplateOptions,
4042
InitServerTemplateOptions,
4143
} from './remote-config-api';
@@ -306,12 +308,20 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
306308
*/
307309
class ServerTemplateImpl implements ServerTemplate {
308310
public cache: ServerTemplateData;
311+
private stringifiedDefaultConfig: {[key: string]: string} = {};
309312

310313
constructor(
311314
private readonly apiClient: RemoteConfigApiClient,
312315
private readonly conditionEvaluator: ConditionEvaluator,
313-
private readonly defaultConfig: ServerConfig = {}
314-
) { }
316+
public readonly defaultConfig: DefaultConfig = {}
317+
) {
318+
// RC stores all remote values as string, but it's more intuitive
319+
// to declare default values with specific types, so this converts
320+
// the external declaration to an internal string representation.
321+
for (const key in defaultConfig) {
322+
this.stringifiedDefaultConfig[key] = String(defaultConfig[key]);
323+
}
324+
}
315325

316326
/**
317327
* Fetches and caches the current active version of the project's {@link ServerTemplate}.
@@ -340,10 +350,16 @@ class ServerTemplateImpl implements ServerTemplate {
340350
const evaluatedConditions = this.conditionEvaluator.evaluateConditions(
341351
this.cache.conditions, context);
342352

343-
const evaluatedConfig: ServerConfig = {};
353+
const configValues: { [key: string]: Value } = {};
344354

355+
// Initializes config Value objects with default values.
356+
for (const key in this.stringifiedDefaultConfig) {
357+
configValues[key] = new ValueImpl('default', this.stringifiedDefaultConfig[key]);
358+
}
359+
360+
// Overlays config Value objects derived by evaluating the template.
345361
for (const [key, parameter] of Object.entries(this.cache.parameters)) {
346-
const { conditionalValues, defaultValue, valueType } = parameter;
362+
const { conditionalValues, defaultValue } = parameter;
347363

348364
// Supports parameters with no conditional values.
349365
const normalizedConditionalValues = conditionalValues || {};
@@ -366,7 +382,7 @@ class ServerTemplateImpl implements ServerTemplate {
366382

367383
if (parameterValueWrapper) {
368384
const parameterValue = (parameterValueWrapper as ExplicitParameterValue).value;
369-
evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterValue);
385+
configValues[key] = new ValueImpl('remote', parameterValue);
370386
continue;
371387
}
372388

@@ -381,46 +397,28 @@ class ServerTemplateImpl implements ServerTemplate {
381397
}
382398

383399
const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value;
384-
evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
400+
configValues[key] = new ValueImpl('remote', parameterDefaultValue);
385401
}
386402

387-
const mergedConfig = {};
388-
389-
// Merges default config and rendered config, prioritizing the latter.
390-
Object.assign(mergedConfig, this.defaultConfig, evaluatedConfig);
391-
392-
// Enables config to be a convenient object, but with the ability to perform additional
393-
// functionality when a value is retrieved.
394-
const proxyHandler = {
395-
get(target: ServerConfig, prop: string) {
396-
return target[prop];
397-
}
398-
};
399-
400-
return new Proxy(mergedConfig, proxyHandler);
403+
return new ServerConfigImpl(configValues);
401404
}
405+
}
402406

403-
/**
404-
* Private helper method that coerces a parameter value string to the {@link ParameterValueType}.
405-
*/
406-
private parseRemoteConfigParameterValue(parameterType: ParameterValueType | undefined,
407-
parameterValue: string): string | number | boolean {
408-
const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
409-
const DEFAULT_VALUE_FOR_NUMBER = 0;
410-
const DEFAULT_VALUE_FOR_STRING = '';
411-
412-
if (parameterType === 'BOOLEAN') {
413-
return BOOLEAN_TRUTHY_VALUES.indexOf(parameterValue) >= 0;
414-
} else if (parameterType === 'NUMBER') {
415-
const num = Number(parameterValue);
416-
if (isNaN(num)) {
417-
return DEFAULT_VALUE_FOR_NUMBER;
418-
}
419-
return num;
420-
} else {
421-
// Treat everything else as string
422-
return parameterValue || DEFAULT_VALUE_FOR_STRING;
423-
}
407+
class ServerConfigImpl implements ServerConfig {
408+
constructor(
409+
private readonly configValues: { [key: string]: Value },
410+
){}
411+
getBoolean(key: string): boolean {
412+
return this.getValue(key).asBoolean();
413+
}
414+
getNumber(key: string): number {
415+
return this.getValue(key).asNumber();
416+
}
417+
getString(key: string): string {
418+
return this.getValue(key).asString();
419+
}
420+
getValue(key: string): Value {
421+
return this.configValues[key] || new ValueImpl('static');
424422
}
425423
}
426424

test/unit/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import './remote-config/index.spec';
9898
import './remote-config/remote-config.spec';
9999
import './remote-config/remote-config-api-client.spec';
100100
import './remote-config/condition-evaluator.spec';
101+
import './remote-config/internal/value-impl.spec';
101102

102103
// AppCheck
103104
import './app-check/app-check.spec';

0 commit comments

Comments
 (0)