Add api, client
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
This commit is contained in:
parent
440cb60787
commit
3b2d04527b
|
|
@ -14,7 +14,8 @@
|
||||||
"sourceType":"module"
|
"sourceType":"module"
|
||||||
},
|
},
|
||||||
"plugins":[
|
"plugins":[
|
||||||
"@typescript-eslint"
|
"@typescript-eslint",
|
||||||
|
"check-file"
|
||||||
],
|
],
|
||||||
"rules":{
|
"rules":{
|
||||||
"linebreak-style":[
|
"linebreak-style":[
|
||||||
|
|
@ -28,6 +29,14 @@
|
||||||
"semi":[
|
"semi":[
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"always"
|
||||||
]
|
],
|
||||||
|
"check-file/filename-naming-convention":[
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"*.spec.{js,ts}":"*",
|
||||||
|
"**/jest.config.ts":"*",
|
||||||
|
"*.{js,ts}":"KEBAB_CASE"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"singleQuote": true
|
"singleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1811,6 +1811,17 @@
|
||||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"eslint-plugin-check-file": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-check-file/-/eslint-plugin-check-file-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-OFLBvqFIDVR0F5gI497UhK/swBZRGq9oJLpChfF3wsh+SxmPP5yzkjOS5gQKj2OupiUMpBrPDcq29zH2j873Nw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"micromatch": "^4.0.4",
|
||||||
|
"requireindex": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eslint-plugin-jest": {
|
"eslint-plugin-jest": {
|
||||||
"version": "26.1.5",
|
"version": "26.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.5.tgz",
|
||||||
|
|
@ -3533,6 +3544,12 @@
|
||||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"requireindex": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.22.0",
|
"version": "1.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"main": "./dist/esm/index.js",
|
"main": "./dist/esm/index.js",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest --verbose",
|
||||||
"lint": "eslint ./",
|
"lint": "eslint ./",
|
||||||
"postbuild": "cp ./package.esm.json ./dist/esm/package.json",
|
"postbuild": "cp ./package.esm.json ./dist/esm/package.json",
|
||||||
"build": "rm -f -R ./dist && tsc --project tsconfig.json && tsc --project tsconfig.cjs.json"
|
"build": "rm -f -R ./dist && tsc --project tsconfig.json && tsc --project tsconfig.cjs.json"
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"@typescript-eslint/parser": "^5.23.0",
|
"@typescript-eslint/parser": "^5.23.0",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-check-file": "^1.1.0",
|
||||||
"eslint-plugin-jest": "^26.1.5",
|
"eslint-plugin-jest": "^26.1.5",
|
||||||
"jest": "^28.1.0",
|
"jest": "^28.1.0",
|
||||||
"jest-junit": "^13.2.0",
|
"jest-junit": "^13.2.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import { ERROR_REASON, GENERAL_ERROR } from './constants';
|
||||||
|
import { OpenFeature } from './open-feature';
|
||||||
|
import {
|
||||||
|
Client,
|
||||||
|
EvaluationContext,
|
||||||
|
EvaluationDetails,
|
||||||
|
FlagEvaluationOptions,
|
||||||
|
FlagValue,
|
||||||
|
Hook,
|
||||||
|
ResolutionDetails,
|
||||||
|
TransformingProvider,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
type OpenFeatureClientOptions = {
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OpenFeatureClient implements Client {
|
||||||
|
name?: string | undefined;
|
||||||
|
version?: string | undefined;
|
||||||
|
readonly context: EvaluationContext;
|
||||||
|
|
||||||
|
constructor(private readonly api: OpenFeature, options: OpenFeatureClientOptions, context: EvaluationContext = {}) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.version = options.version;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
addHooks(...hooks: Hook<FlagValue>[]): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
get hooks(): Hook<FlagValue>[] {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBooleanValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return (await this.getBooleanDetails(flagKey, defaultValue, context, options)).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBooleanDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<boolean>> {
|
||||||
|
return this.evaluate<boolean>(flagKey, this.provider.resolveBooleanEvaluation, defaultValue, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStringValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: string,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<string> {
|
||||||
|
return (await this.getStringDetails(flagKey, defaultValue, context, options)).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStringDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: string,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<string>> {
|
||||||
|
return this.evaluate<string>(flagKey, this.provider.resolveStringEvaluation, defaultValue, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNumberValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: number,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<number> {
|
||||||
|
return (await this.getNumberDetails(flagKey, defaultValue, context, options)).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNumberDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: number,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<number>> {
|
||||||
|
return this.evaluate<number>(flagKey, this.provider.resolveNumberEvaluation, defaultValue, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectValue<T extends object>(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: T,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<T> {
|
||||||
|
return (await this.getObjectDetails(flagKey, defaultValue, context, options)).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectDetails<T extends object>(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: T,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<T>> {
|
||||||
|
return this.evaluate<T>(flagKey, this.provider.resolveObjectEvaluation, defaultValue, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async evaluate<T extends FlagValue>(
|
||||||
|
flagKey: string,
|
||||||
|
resolver: (
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: T,
|
||||||
|
transformedContext: unknown,
|
||||||
|
options: FlagEvaluationOptions | undefined
|
||||||
|
) => Promise<ResolutionDetails<T>>,
|
||||||
|
defaultValue: T,
|
||||||
|
context: EvaluationContext = {},
|
||||||
|
options: FlagEvaluationOptions = {}
|
||||||
|
): Promise<EvaluationDetails<T>> {
|
||||||
|
// merge global, client, and evaluation context
|
||||||
|
const mergedContext = {
|
||||||
|
...this.api.context,
|
||||||
|
...this.context,
|
||||||
|
...context,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// if a transformer is defined, run it to prepare the context.
|
||||||
|
const transformedContext =
|
||||||
|
typeof this.provider.contextTransformer === 'function'
|
||||||
|
? await this.provider.contextTransformer(mergedContext)
|
||||||
|
: mergedContext;
|
||||||
|
|
||||||
|
// run the referenced resolver, binding the provider.
|
||||||
|
const resolution = await resolver.call(this.provider, flagKey, defaultValue, transformedContext, options);
|
||||||
|
return {
|
||||||
|
...resolution,
|
||||||
|
flagKey,
|
||||||
|
};
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorCode = (!!err && (err as { code: string }).code) || GENERAL_ERROR;
|
||||||
|
return {
|
||||||
|
errorCode,
|
||||||
|
value: defaultValue,
|
||||||
|
reason: ERROR_REASON,
|
||||||
|
flagKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get provider() {
|
||||||
|
return OpenFeature.instance.provider as TransformingProvider<unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// reasons
|
||||||
|
export const ERROR_REASON = 'ERROR';
|
||||||
|
|
||||||
|
// error-codes
|
||||||
|
export const GENERAL_ERROR = 'GENERAL_ERROR';
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// real code will go here, just scaffolding the project for now.
|
export * from './open-feature';
|
||||||
export const greet = (greeting: string): string => {
|
export * from './client';
|
||||||
const message = `${greeting}, OpenFeature`;
|
export * from './types';
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Provider, ResolutionDetails } from './types';
|
||||||
|
|
||||||
|
const REASON_NO_OP = 'No-op';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The No-op provider is set by default, and simply always returns the default value.
|
||||||
|
*/
|
||||||
|
class NoopFeatureProvider implements Provider {
|
||||||
|
readonly name = 'No-op Provider';
|
||||||
|
|
||||||
|
resolveBooleanEvaluation(_: string, defaultValue: boolean): Promise<ResolutionDetails<boolean>> {
|
||||||
|
return this.noOp(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveStringEvaluation(_: string, defaultValue: string): Promise<ResolutionDetails<string>> {
|
||||||
|
return this.noOp(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveNumberEvaluation(_: string, defaultValue: number): Promise<ResolutionDetails<number>> {
|
||||||
|
return this.noOp(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveObjectEvaluation<T extends object>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
|
||||||
|
return this.noOp<T>(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private noOp<T>(defaultValue: T) {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: defaultValue,
|
||||||
|
reason: REASON_NO_OP,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOOP_PROVIDER = new NoopFeatureProvider();
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { OpenFeatureClient } from './client';
|
||||||
|
import { NOOP_PROVIDER } from './no-op-provider';
|
||||||
|
import { Client, EvaluationContext, EvaluationLifeCycle, FlagValue, Hook, Provider } from './types';
|
||||||
|
|
||||||
|
// use a symbol as a key for the global singleton
|
||||||
|
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js.api');
|
||||||
|
|
||||||
|
type OpenFeatureGlobal = {
|
||||||
|
[GLOBAL_OPENFEATURE_API_KEY]?: OpenFeature;
|
||||||
|
};
|
||||||
|
const _global = global as OpenFeatureGlobal;
|
||||||
|
|
||||||
|
export class OpenFeature implements EvaluationLifeCycle {
|
||||||
|
private _provider: Provider = NOOP_PROVIDER;
|
||||||
|
private _context: EvaluationContext = {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static get instance(): OpenFeature {
|
||||||
|
const globalApi = _global[GLOBAL_OPENFEATURE_API_KEY];
|
||||||
|
if (globalApi) {
|
||||||
|
return globalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new OpenFeature();
|
||||||
|
_global[GLOBAL_OPENFEATURE_API_KEY] = instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(name?: string, version?: string, context?: EvaluationContext): Client {
|
||||||
|
return new OpenFeatureClient(this, { name, version }, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
addHooks(...hooks: Hook<FlagValue>[]): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
get hooks(): Hook<FlagValue>[] {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
set provider(provider: Provider) {
|
||||||
|
this._provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
get provider(): Provider {
|
||||||
|
return this._provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
set context(context: EvaluationContext) {
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
get context(): EvaluationContext {
|
||||||
|
return this._context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
export type EvaluationContext = {
|
||||||
|
/**
|
||||||
|
* A string uniquely identifying the subject (end-user, or client service) of a flag evaluation.
|
||||||
|
* Providers may require this field for fractional flag evaluation, rules, or overrides targeting specific users. Such providers may behave unpredictably if a targeting key is not specified at flag resolution.
|
||||||
|
*/
|
||||||
|
targetingKey?: string;
|
||||||
|
} & Record<string, string | number | boolean | Date>;
|
||||||
|
|
||||||
|
export type FlagValue = boolean | string | number | object;
|
||||||
|
|
||||||
|
export interface FlagEvaluationOptions {
|
||||||
|
hooks?: Hook[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Features {
|
||||||
|
/**
|
||||||
|
* Get a boolean flag value.
|
||||||
|
*/
|
||||||
|
getBooleanValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a boolean flag with additional details.
|
||||||
|
*/
|
||||||
|
getBooleanDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<boolean>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string flag value.
|
||||||
|
*/
|
||||||
|
getStringValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: string,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string flag with additional details.
|
||||||
|
*/
|
||||||
|
getStringDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: string,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<string>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a number flag value.
|
||||||
|
*/
|
||||||
|
getNumberValue(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: number,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a number flag with additional details.
|
||||||
|
*/
|
||||||
|
getNumberDetails(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: number,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<number>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object (JSON) flag value.
|
||||||
|
*/
|
||||||
|
getObjectValue<T extends object>(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: T,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object (JSON) flag with additional details.
|
||||||
|
*/
|
||||||
|
getObjectDetails<T extends object>(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: T,
|
||||||
|
context?: EvaluationContext,
|
||||||
|
options?: FlagEvaluationOptions
|
||||||
|
): Promise<EvaluationDetails<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function which transforms the EvaluationContext to a type useful for the provider.
|
||||||
|
*/
|
||||||
|
export type ContextTransformer<T = unknown> = (context: EvaluationContext) => T;
|
||||||
|
|
||||||
|
interface GenericProvider<T> {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a boolean flag and it's evaluation details.
|
||||||
|
*/
|
||||||
|
resolveBooleanEvaluation(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
transformedContext: T,
|
||||||
|
options: FlagEvaluationOptions | undefined
|
||||||
|
): Promise<ResolutionDetails<boolean>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a string flag and it's evaluation details.
|
||||||
|
*/
|
||||||
|
resolveStringEvaluation(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: string,
|
||||||
|
transformedContext: T,
|
||||||
|
options: FlagEvaluationOptions | undefined
|
||||||
|
): Promise<ResolutionDetails<string>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a numeric flag and it's evaluation details.
|
||||||
|
*/
|
||||||
|
resolveNumberEvaluation(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: number,
|
||||||
|
transformedContext: T,
|
||||||
|
options: FlagEvaluationOptions | undefined
|
||||||
|
): Promise<ResolutionDetails<number>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and parse an object flag and it's evaluation details.
|
||||||
|
*/
|
||||||
|
resolveObjectEvaluation<U extends object>(
|
||||||
|
flagKey: string,
|
||||||
|
defaultValue: U,
|
||||||
|
transformedContext: T,
|
||||||
|
options: FlagEvaluationOptions | undefined
|
||||||
|
): Promise<ResolutionDetails<U>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NonTransformingProvider = GenericProvider<EvaluationContext>;
|
||||||
|
|
||||||
|
export interface TransformingProvider<T> extends GenericProvider<T> {
|
||||||
|
contextTransformer: ContextTransformer<Promise<T> | T> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that providers must implement to resolve flag values for their particular
|
||||||
|
* backend or vendor.
|
||||||
|
*
|
||||||
|
* Implementation for resolving all the required flag types must be defined.
|
||||||
|
*
|
||||||
|
* Additionally, a ContextTransformer function that transforms the OpenFeature context to the requisite user/context/attribute representation (typeof T)
|
||||||
|
* may also be implemented. This function will run immediately before the flag value resolver functions, appropriately transforming the context.
|
||||||
|
*/
|
||||||
|
export type Provider<T extends EvaluationContext | unknown = EvaluationContext> = T extends EvaluationContext
|
||||||
|
? NonTransformingProvider
|
||||||
|
: TransformingProvider<T>;
|
||||||
|
|
||||||
|
export interface EvaluationLifeCycle {
|
||||||
|
addHooks(...hooks: Hook[]): void;
|
||||||
|
get hooks(): Hook[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderOptions<T = unknown> {
|
||||||
|
contextTransformer?: ContextTransformer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolutionDetails<U> = {
|
||||||
|
value: U;
|
||||||
|
variant?: string;
|
||||||
|
reason?: string;
|
||||||
|
errorCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EvaluationDetails<T extends FlagValue> = {
|
||||||
|
flagKey: string;
|
||||||
|
} & ResolutionDetails<T>;
|
||||||
|
|
||||||
|
export interface Client extends EvaluationLifeCycle, Features {
|
||||||
|
readonly name?: string;
|
||||||
|
readonly version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HookContext = {
|
||||||
|
// TODO: implement with hooks
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface Hook<T extends FlagValue = FlagValue> {
|
||||||
|
// TODO: implement with hooks
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
import { OpenFeatureClient } from '../src/client';
|
||||||
|
import { ERROR_REASON, GENERAL_ERROR } from '../src/constants';
|
||||||
|
import { OpenFeature } from '../src/open-feature';
|
||||||
|
import {
|
||||||
|
Client,
|
||||||
|
EvaluationContext,
|
||||||
|
EvaluationDetails,
|
||||||
|
NonTransformingProvider,
|
||||||
|
Provider,
|
||||||
|
ResolutionDetails,
|
||||||
|
TransformingProvider,
|
||||||
|
} from '../src/types';
|
||||||
|
|
||||||
|
const BOOLEAN_VALUE = true;
|
||||||
|
const STRING_VALUE = 'val';
|
||||||
|
const NUMBER_VALUE = 2034;
|
||||||
|
const OBJECT_VALUE = {
|
||||||
|
key: 'value',
|
||||||
|
};
|
||||||
|
const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
|
||||||
|
const STRING_VARIANT = `${STRING_VALUE}-variant`;
|
||||||
|
const NUMBER_VARIANT = NUMBER_VALUE.toString();
|
||||||
|
const OBJECT_VARIANT = OBJECT_VALUE.key;
|
||||||
|
const REASON = 'mocked-value';
|
||||||
|
|
||||||
|
// a mock provider with some jest spies
|
||||||
|
const MOCK_PROVIDER: Provider = {
|
||||||
|
name: 'mock',
|
||||||
|
|
||||||
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: BOOLEAN_VALUE,
|
||||||
|
variant: BOOLEAN_VARIANT,
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
resolveStringEvaluation: jest.fn((): Promise<ResolutionDetails<string>> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: STRING_VALUE,
|
||||||
|
variant: STRING_VARIANT,
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
resolveNumberEvaluation: jest.fn((): Promise<ResolutionDetails<number>> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: NUMBER_VALUE,
|
||||||
|
variant: NUMBER_VARIANT,
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
resolveObjectEvaluation: jest.fn(<U extends object>(): Promise<ResolutionDetails<U>> => {
|
||||||
|
const details = Promise.resolve<ResolutionDetails<U>>({
|
||||||
|
value: OBJECT_VALUE as unknown as U,
|
||||||
|
variant: OBJECT_VARIANT,
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
return details as Promise<ResolutionDetails<U>>;
|
||||||
|
}) as <U extends object>() => Promise<ResolutionDetails<U>>,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.name, () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
OpenFeature.instance.provider = MOCK_PROVIDER;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.6', () => {
|
||||||
|
it('should allow addition of hooks', () => {
|
||||||
|
// TODO: implement with hooks
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.7, 1.8', () => {
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = OpenFeature.instance.getClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flag evaluation', () => {
|
||||||
|
describe(` ${OpenFeatureClient.prototype.getBooleanValue.name}`, () => {
|
||||||
|
it('should return boolean, and call boolean resolver', async () => {
|
||||||
|
const booleanFlag = 'my-boolean-flag';
|
||||||
|
const defaultBooleanValue = false;
|
||||||
|
const value = await client.getBooleanValue(booleanFlag, defaultBooleanValue);
|
||||||
|
|
||||||
|
expect(value).toEqual(BOOLEAN_VALUE);
|
||||||
|
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(booleanFlag, defaultBooleanValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getStringValue.name, () => {
|
||||||
|
it('should return string, and call string resolver', async () => {
|
||||||
|
const stringFlag = 'my-string-flag';
|
||||||
|
const defaultStringValue = 'default-value';
|
||||||
|
const value = await client.getStringValue(stringFlag, defaultStringValue);
|
||||||
|
|
||||||
|
expect(value).toEqual(STRING_VALUE);
|
||||||
|
expect(MOCK_PROVIDER.resolveStringEvaluation).toHaveBeenCalledWith(stringFlag, defaultStringValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getNumberValue.name, () => {
|
||||||
|
it('should return number, and call number resolver', async () => {
|
||||||
|
const numberFlag = 'my-number-flag';
|
||||||
|
const defaultNumberValue = 1970;
|
||||||
|
const value = await client.getNumberValue(numberFlag, defaultNumberValue);
|
||||||
|
|
||||||
|
expect(value).toEqual(NUMBER_VALUE);
|
||||||
|
expect(MOCK_PROVIDER.resolveNumberEvaluation).toHaveBeenCalledWith(numberFlag, defaultNumberValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getObjectValue.name, () => {
|
||||||
|
it('should return object, and call object resolver', async () => {
|
||||||
|
const objectFlag = 'my-object-flag';
|
||||||
|
const defaultObjectFlag = {};
|
||||||
|
const value = await client.getObjectValue(objectFlag, {});
|
||||||
|
|
||||||
|
expect(value).toEqual(OBJECT_VALUE);
|
||||||
|
expect(MOCK_PROVIDER.resolveObjectEvaluation).toHaveBeenCalledWith(objectFlag, defaultObjectFlag, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.9, 1.10', () => {
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = OpenFeature.instance.getClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detailed flag evaluation', () => {
|
||||||
|
describe(` ${OpenFeatureClient.prototype.getBooleanDetails.name}`, () => {
|
||||||
|
it('should return boolean details, and call boolean resolver', async () => {
|
||||||
|
const booleanFlag = 'my-boolean-flag';
|
||||||
|
const defaultBooleanValue = false;
|
||||||
|
const booleanDetails = await client.getBooleanDetails(booleanFlag, defaultBooleanValue);
|
||||||
|
|
||||||
|
expect(booleanDetails.value).toEqual(BOOLEAN_VALUE);
|
||||||
|
expect(booleanDetails.variant).toEqual(BOOLEAN_VARIANT);
|
||||||
|
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(booleanFlag, defaultBooleanValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getStringDetails.name, () => {
|
||||||
|
it('should return string details, and call string resolver', async () => {
|
||||||
|
const stringFlag = 'my-string-flag';
|
||||||
|
const defaultStringValue = 'default-value';
|
||||||
|
const stringDetails = await client.getStringDetails(stringFlag, defaultStringValue);
|
||||||
|
|
||||||
|
expect(stringDetails.value).toEqual(STRING_VALUE);
|
||||||
|
expect(stringDetails.variant).toEqual(STRING_VARIANT);
|
||||||
|
expect(MOCK_PROVIDER.resolveStringEvaluation).toHaveBeenCalledWith(stringFlag, defaultStringValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getNumberDetails.name, () => {
|
||||||
|
it('should return number details, and call number resolver', async () => {
|
||||||
|
const numberFlag = 'my-number-flag';
|
||||||
|
const defaultNumberValue = 1970;
|
||||||
|
const numberDetails = await client.getNumberDetails(numberFlag, defaultNumberValue);
|
||||||
|
|
||||||
|
expect(numberDetails.value).toEqual(NUMBER_VALUE);
|
||||||
|
expect(numberDetails.variant).toEqual(NUMBER_VARIANT);
|
||||||
|
expect(MOCK_PROVIDER.resolveNumberEvaluation).toHaveBeenCalledWith(numberFlag, defaultNumberValue, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(OpenFeatureClient.prototype.getObjectDetails.name, () => {
|
||||||
|
it('should return object details, and call object resolver', async () => {
|
||||||
|
const objectFlag = 'my-object-flag';
|
||||||
|
const defaultObjectFlag = {};
|
||||||
|
const objectDetails = await client.getObjectDetails(objectFlag, defaultObjectFlag);
|
||||||
|
|
||||||
|
expect(objectDetails.value).toEqual(OBJECT_VALUE);
|
||||||
|
expect(objectDetails.variant).toEqual(OBJECT_VARIANT);
|
||||||
|
expect(MOCK_PROVIDER.resolveObjectEvaluation).toHaveBeenCalledWith(objectFlag, defaultObjectFlag, {}, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.11', () => {
|
||||||
|
describe('generic support', () => {
|
||||||
|
it('should support generic', async () => {
|
||||||
|
// No generic information exists at runtime, but this test has some value in ensuring the generic args still exist in the typings.
|
||||||
|
type MyType = { key: string };
|
||||||
|
const client = OpenFeature.instance.getClient();
|
||||||
|
const details: ResolutionDetails<MyType> = await client.getObjectDetails<MyType>('flag', { key: 'value' });
|
||||||
|
|
||||||
|
expect(details).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Evaluation details structure', () => {
|
||||||
|
const flagKey = 'number-details';
|
||||||
|
const defaultValue = 1970;
|
||||||
|
let details: EvaluationDetails<number>;
|
||||||
|
|
||||||
|
describe('Normal execution', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const client = OpenFeature.instance.getClient();
|
||||||
|
details = await client.getNumberDetails(flagKey, defaultValue);
|
||||||
|
|
||||||
|
expect(details).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.10, 1.11', () => {
|
||||||
|
it('should contain flag value', () => {
|
||||||
|
expect(details.value).toEqual(NUMBER_VALUE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.12', () => {
|
||||||
|
it('should contain flag key', () => {
|
||||||
|
expect(details.flagKey).toEqual(flagKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.13', () => {
|
||||||
|
it('should contain flag variant', () => {
|
||||||
|
expect(details.variant).toEqual(NUMBER_VARIANT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.14', () => {
|
||||||
|
it('should contain reason', () => {
|
||||||
|
expect(details.reason).toEqual(REASON);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Abnormal execution', () => {
|
||||||
|
let details: EvaluationDetails<number>;
|
||||||
|
let client: Client;
|
||||||
|
const errorProvider = {
|
||||||
|
name: 'error-mock',
|
||||||
|
|
||||||
|
resolveNumberEvaluation: jest.fn((): Promise<ResolutionDetails<number>> => {
|
||||||
|
throw new Error('Fake error!');
|
||||||
|
}),
|
||||||
|
} as unknown as Provider;
|
||||||
|
const defaultValue = 123;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
OpenFeature.instance.provider = errorProvider;
|
||||||
|
client = OpenFeature.instance.getClient();
|
||||||
|
details = await client.getNumberDetails('some-flag', defaultValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.18', () => {
|
||||||
|
it('must not throw, must return default', async () => {
|
||||||
|
details = await client.getNumberDetails('some-flag', defaultValue);
|
||||||
|
|
||||||
|
expect(details).toBeTruthy();
|
||||||
|
expect(details.value).toEqual(defaultValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.15', () => {
|
||||||
|
it('should contain error', () => {
|
||||||
|
expect(details.errorCode).toBeTruthy();
|
||||||
|
expect(details.errorCode).toEqual(GENERAL_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.15', () => {
|
||||||
|
it('should contain "error" reason', () => {
|
||||||
|
expect(details.reason).toEqual(ERROR_REASON);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.21', () => {
|
||||||
|
describe('Transforming provider', () => {
|
||||||
|
const transformingProvider = {
|
||||||
|
name: 'transforming',
|
||||||
|
// a simple context transformer that just adds a property (transformed: true)
|
||||||
|
contextTransformer: jest.fn((context: EvaluationContext) => {
|
||||||
|
return { ...context, transformed: true };
|
||||||
|
}),
|
||||||
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
} as unknown as TransformingProvider<EvaluationContext>;
|
||||||
|
it('should run context transformer, and pass transformed context to resolver', async () => {
|
||||||
|
const flagKey = 'some-flag';
|
||||||
|
const defaultValue = false;
|
||||||
|
const context = {};
|
||||||
|
OpenFeature.instance.provider = transformingProvider;
|
||||||
|
const client = OpenFeature.instance.getClient();
|
||||||
|
await client.getBooleanValue(flagKey, defaultValue, context);
|
||||||
|
|
||||||
|
// expect transformer was called with context
|
||||||
|
expect(transformingProvider.contextTransformer).toHaveBeenCalledWith(context);
|
||||||
|
// expect transformed context was passed to resolver.
|
||||||
|
expect(transformingProvider.resolveBooleanEvaluation).toHaveBeenCalledWith(
|
||||||
|
flagKey,
|
||||||
|
defaultValue,
|
||||||
|
expect.objectContaining({ transformed: true }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Non-transforming provider', () => {
|
||||||
|
const nonTransformingProvider = {
|
||||||
|
name: 'non-transforming',
|
||||||
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
} as unknown as NonTransformingProvider;
|
||||||
|
it('should pass context to resolver', async () => {
|
||||||
|
const flagKey = 'some-other-flag';
|
||||||
|
const defaultValue = false;
|
||||||
|
const context = { transformed: false };
|
||||||
|
OpenFeature.instance.provider = nonTransformingProvider;
|
||||||
|
const client = OpenFeature.instance.getClient();
|
||||||
|
await client.getBooleanValue(flagKey, defaultValue, context);
|
||||||
|
|
||||||
|
// expect context was passed to resolver.
|
||||||
|
expect(nonTransformingProvider.resolveBooleanEvaluation).toHaveBeenCalledWith(
|
||||||
|
flagKey,
|
||||||
|
defaultValue,
|
||||||
|
expect.objectContaining({ transformed: false }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { greet } from '../src/index';
|
|
||||||
|
|
||||||
describe('greet', () => {
|
|
||||||
it('should return greeting', () => {
|
|
||||||
const result = greet('hi');
|
|
||||||
expect(result).toContain('hi');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { OpenFeatureClient } from '../src/client';
|
||||||
|
import { OpenFeature } from '../src/open-feature';
|
||||||
|
import { Provider } from '../src/types';
|
||||||
|
|
||||||
|
describe(OpenFeature.name, () => {
|
||||||
|
describe('Requirement 1.1', () => {
|
||||||
|
it('should be global singleton', () => {
|
||||||
|
expect(OpenFeature.instance.provider === OpenFeature.instance.provider).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.2', () => {
|
||||||
|
it('should be set provider', () => {
|
||||||
|
const fakeProvider = {} as Provider;
|
||||||
|
OpenFeature.instance.provider = fakeProvider;
|
||||||
|
expect(OpenFeature.instance.provider === fakeProvider).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.3', () => {
|
||||||
|
it('should allow addition of hooks', () => {
|
||||||
|
// TODO: implement with hooks
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.4', () => {
|
||||||
|
it('should implement a hook accessor', () => {
|
||||||
|
expect(OpenFeature.instance.provider).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement 1.5', () => {
|
||||||
|
it('should implement a client factory', () => {
|
||||||
|
expect(OpenFeature.instance.getClient).toBeDefined();
|
||||||
|
expect(OpenFeature.instance.getClient()).toBeInstanceOf(OpenFeatureClient);
|
||||||
|
|
||||||
|
const name = 'my-client';
|
||||||
|
const namedClient = OpenFeature.instance.getClient(name);
|
||||||
|
|
||||||
|
// check that using a named configuration also works as expected.
|
||||||
|
expect(namedClient).toBeInstanceOf(OpenFeatureClient);
|
||||||
|
expect(namedClient.name).toEqual(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue