Skip to content

Commit

Permalink
Merge branch 'main' into release-please--branches--main--components--…
Browse files Browse the repository at this point in the history
…core
  • Loading branch information
toddbaert authored Nov 21, 2023
2 parents 3c82517 + 6051dfd commit 5e7c5d6
Show file tree
Hide file tree
Showing 6 changed files with 834 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Don't export types from this file publicly.
* It might cause confusion since these types are not a part of the general API,
* but just for the in-memory provider.
*/
import { EvaluationContext, JsonValue } from '@openfeature/core';

type Variants<T> = Record<string, T>;

/**
* A Feature Flag definition, containing it's specification
*/
export type Flag = {
/**
* An object containing all possible flags mappings (variant -> flag value)
*/
variants: Variants<boolean> | Variants<string> | Variants<number> | Variants<JsonValue>;
/**
* The variant it will resolve to in STATIC evaluation
*/
defaultVariant: string;
/**
* Determines if flag evaluation is enabled or not for this flag.
* If false, falls back to the default value provided to the client
*/
disabled: boolean;
/**
* Function used in order to evaluate a flag to a specific value given the provided context.
* It should return a variant key.
* If it does not return a valid variant it falls back to the default value provided to the client
* @param EvaluationContext
*/
contextEvaluator?: (ctx: EvaluationContext) => string;
};

export type FlagConfiguration = Record<string, Flag>;
179 changes: 179 additions & 0 deletions packages/client/src/provider/in-memory-provider/in-memory-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
EvaluationContext,
FlagNotFoundError,
FlagValueType,
GeneralError,
JsonValue,
Logger,
OpenFeatureError,
ProviderEvents,
ResolutionDetails,
StandardResolutionReasons,
TypeMismatchError,
ProviderStatus,
} from '@openfeature/core';
import { Provider } from '../provider';
import { OpenFeatureEventEmitter } from '../../events';
import { FlagConfiguration, Flag } from './flag-configuration';
import { VariantNotFoundError } from './variant-not-found-error';

/**
* A simple OpenFeature provider intended for demos and as a test stub.
*/
export class InMemoryProvider implements Provider {
public readonly events = new OpenFeatureEventEmitter();
public readonly runsOn = 'client';
status: ProviderStatus = ProviderStatus.NOT_READY;
readonly metadata = {
name: 'in-memory',
} as const;
private _flagConfiguration: FlagConfiguration;
private _context: EvaluationContext | undefined;

constructor(flagConfiguration: FlagConfiguration = {}) {
this._flagConfiguration = { ...flagConfiguration };
}

async initialize(context?: EvaluationContext | undefined): Promise<void> {
try {

for (const key in this._flagConfiguration) {
this.resolveFlagWithReason(key, context);
}

this._context = context;
// set the provider's state, but don't emit events manually;
// the SDK does this based on the resolution/rejection of the init promise
this.status = ProviderStatus.READY;
} catch (error) {
this.status = ProviderStatus.ERROR;
throw error;
}
}

/**
* Overwrites the configured flags.
* @param { FlagConfiguration } flagConfiguration new flag configuration
*/
async putConfiguration(flagConfiguration: FlagConfiguration) {
const flagsChanged = Object.entries(flagConfiguration)
.filter(([key, value]) => this._flagConfiguration[key] !== value)
.map(([key]) => key);

this.status = ProviderStatus.STALE;
this.events.emit(ProviderEvents.Stale);

this._flagConfiguration = { ...flagConfiguration };
this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged });

try {
await this.initialize(this._context);
// we need to emit our own events in this case, since it's not part of the init flow.
this.events.emit(ProviderEvents.Ready);
} catch (err) {
this.events.emit(ProviderEvents.Error);
throw err;
}
}

resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<boolean> {
return this.resolveAndCheckFlag<boolean>(flagKey, defaultValue, context || this._context, logger);
}

resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<number> {
return this.resolveAndCheckFlag<number>(flagKey, defaultValue, context || this._context, logger);
}

resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<string> {
return this.resolveAndCheckFlag<string>(flagKey, defaultValue, context || this._context, logger);
}

resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<T> {
return this.resolveAndCheckFlag<T>(flagKey, defaultValue, context || this._context, logger);
}

private resolveAndCheckFlag<T extends JsonValue | FlagValueType>(flagKey: string,
defaultValue: T, context?: EvaluationContext, logger?: Logger): ResolutionDetails<T> {
if (!(flagKey in this._flagConfiguration)) {
const message = `no flag found with key ${flagKey}`;
logger?.debug(message);
throw new FlagNotFoundError(message);
}

if (this._flagConfiguration[flagKey].disabled) {
return { value: defaultValue, reason: StandardResolutionReasons.DISABLED };
}

const resolvedFlag = this.resolveFlagWithReason(flagKey, context) as ResolutionDetails<T>;

if (resolvedFlag.value === undefined) {
const message = `no value associated with variant provided for ${flagKey} found`;
logger?.error(message);
throw new VariantNotFoundError(message);
}

if (typeof resolvedFlag.value != typeof defaultValue) {
throw new TypeMismatchError();
}

return resolvedFlag;
}

private resolveFlagWithReason<T extends JsonValue | FlagValueType>(
flagKey: string,
ctx?: EvaluationContext,
): ResolutionDetails<T> {
try {
const resolutionResult = this.lookupFlagValue<T>(flagKey, ctx);

return resolutionResult;
} catch (error: unknown) {
if (!(error instanceof OpenFeatureError)) {
throw new GeneralError((error as Error)?.message || 'unknown error');
}
throw error;
}
}

private lookupFlagValue<T extends JsonValue | FlagValueType>(
flagKey: string,
ctx?: EvaluationContext,
): ResolutionDetails<T> {
const flagSpec: Flag = this._flagConfiguration[flagKey];

const isContextEval = ctx && flagSpec?.contextEvaluator;
const variant = isContextEval ? flagSpec.contextEvaluator?.(ctx) : flagSpec.defaultVariant;

const value = variant && flagSpec?.variants[variant];

const evalReason = isContextEval ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC;

const reason = this.status === ProviderStatus.STALE ? StandardResolutionReasons.CACHED : evalReason;

return {
value: value as T,
...(variant && { variant }),
reason,
};
}
}
1 change: 1 addition & 0 deletions packages/client/src/provider/in-memory-provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './in-memory-provider';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ErrorCode, OpenFeatureError } from '@openfeature/core';

/**
* A custom error for the in-memory provider.
* Indicates the resolved or default variant doesn't exist.
*/
export class VariantNotFoundError extends OpenFeatureError {
code: ErrorCode;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, VariantNotFoundError.prototype);
this.name = 'VariantNotFoundError';
this.code = ErrorCode.GENERAL;
}
}
1 change: 1 addition & 0 deletions packages/client/src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './provider';
export * from './no-op-provider';
export * from './in-memory-provider';
Loading

0 comments on commit 5e7c5d6

Please sign in to comment.