Skip to content

Commit 3b2d045

Browse files
toddbaertbeeme1mr
andcommitted
Add api, client
Co-authored-by: Michael Beemer <[email protected]>
1 parent 440cb60 commit 3b2d045

13 files changed

+871
-17
lines changed

.eslintrc.json

+11-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"sourceType":"module"
1515
},
1616
"plugins":[
17-
"@typescript-eslint"
17+
"@typescript-eslint",
18+
"check-file"
1819
],
1920
"rules":{
2021
"linebreak-style":[
@@ -28,6 +29,14 @@
2829
"semi":[
2930
"error",
3031
"always"
31-
]
32+
],
33+
"check-file/filename-naming-convention":[
34+
"error",
35+
{
36+
"*.spec.{js,ts}":"*",
37+
"**/jest.config.ts":"*",
38+
"*.{js,ts}":"KEBAB_CASE"
39+
}
40+
]
3241
}
3342
}

.prettierrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"singleQuote": true
2+
"singleQuote": true,
3+
"printWidth": 120
34
}

package-lock.json

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "./dist/esm/index.js",
66
"types": "./dist/types/index.d.ts",
77
"scripts": {
8-
"test": "jest",
8+
"test": "jest --verbose",
99
"lint": "eslint ./",
1010
"postbuild": "cp ./package.esm.json ./dist/esm/package.json",
1111
"build": "rm -f -R ./dist && tsc --project tsconfig.json && tsc --project tsconfig.cjs.json"
@@ -43,6 +43,7 @@
4343
"@typescript-eslint/parser": "^5.23.0",
4444
"eslint": "^8.14.0",
4545
"eslint-config-prettier": "^8.5.0",
46+
"eslint-plugin-check-file": "^1.1.0",
4647
"eslint-plugin-jest": "^26.1.5",
4748
"jest": "^28.1.0",
4849
"jest-junit": "^13.2.0",

src/client.ts

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { ERROR_REASON, GENERAL_ERROR } from './constants';
2+
import { OpenFeature } from './open-feature';
3+
import {
4+
Client,
5+
EvaluationContext,
6+
EvaluationDetails,
7+
FlagEvaluationOptions,
8+
FlagValue,
9+
Hook,
10+
ResolutionDetails,
11+
TransformingProvider,
12+
} from './types';
13+
14+
type OpenFeatureClientOptions = {
15+
name?: string;
16+
version?: string;
17+
};
18+
19+
export class OpenFeatureClient implements Client {
20+
name?: string | undefined;
21+
version?: string | undefined;
22+
readonly context: EvaluationContext;
23+
24+
constructor(private readonly api: OpenFeature, options: OpenFeatureClientOptions, context: EvaluationContext = {}) {
25+
this.name = options.name;
26+
this.version = options.version;
27+
this.context = context;
28+
}
29+
30+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
31+
addHooks(...hooks: Hook<FlagValue>[]): void {
32+
throw new Error('Method not implemented.');
33+
}
34+
35+
get hooks(): Hook<FlagValue>[] {
36+
throw new Error('Method not implemented.');
37+
}
38+
39+
async getBooleanValue(
40+
flagKey: string,
41+
defaultValue: boolean,
42+
context?: EvaluationContext,
43+
options?: FlagEvaluationOptions
44+
): Promise<boolean> {
45+
return (await this.getBooleanDetails(flagKey, defaultValue, context, options)).value;
46+
}
47+
48+
getBooleanDetails(
49+
flagKey: string,
50+
defaultValue: boolean,
51+
context?: EvaluationContext,
52+
options?: FlagEvaluationOptions
53+
): Promise<EvaluationDetails<boolean>> {
54+
return this.evaluate<boolean>(flagKey, this.provider.resolveBooleanEvaluation, defaultValue, context, options);
55+
}
56+
57+
async getStringValue(
58+
flagKey: string,
59+
defaultValue: string,
60+
context?: EvaluationContext,
61+
options?: FlagEvaluationOptions
62+
): Promise<string> {
63+
return (await this.getStringDetails(flagKey, defaultValue, context, options)).value;
64+
}
65+
66+
getStringDetails(
67+
flagKey: string,
68+
defaultValue: string,
69+
context?: EvaluationContext,
70+
options?: FlagEvaluationOptions
71+
): Promise<EvaluationDetails<string>> {
72+
return this.evaluate<string>(flagKey, this.provider.resolveStringEvaluation, defaultValue, context, options);
73+
}
74+
75+
async getNumberValue(
76+
flagKey: string,
77+
defaultValue: number,
78+
context?: EvaluationContext,
79+
options?: FlagEvaluationOptions
80+
): Promise<number> {
81+
return (await this.getNumberDetails(flagKey, defaultValue, context, options)).value;
82+
}
83+
84+
getNumberDetails(
85+
flagKey: string,
86+
defaultValue: number,
87+
context?: EvaluationContext,
88+
options?: FlagEvaluationOptions
89+
): Promise<EvaluationDetails<number>> {
90+
return this.evaluate<number>(flagKey, this.provider.resolveNumberEvaluation, defaultValue, context, options);
91+
}
92+
93+
async getObjectValue<T extends object>(
94+
flagKey: string,
95+
defaultValue: T,
96+
context?: EvaluationContext,
97+
options?: FlagEvaluationOptions
98+
): Promise<T> {
99+
return (await this.getObjectDetails(flagKey, defaultValue, context, options)).value;
100+
}
101+
102+
getObjectDetails<T extends object>(
103+
flagKey: string,
104+
defaultValue: T,
105+
context?: EvaluationContext,
106+
options?: FlagEvaluationOptions
107+
): Promise<EvaluationDetails<T>> {
108+
return this.evaluate<T>(flagKey, this.provider.resolveObjectEvaluation, defaultValue, context, options);
109+
}
110+
111+
private async evaluate<T extends FlagValue>(
112+
flagKey: string,
113+
resolver: (
114+
flagKey: string,
115+
defaultValue: T,
116+
transformedContext: unknown,
117+
options: FlagEvaluationOptions | undefined
118+
) => Promise<ResolutionDetails<T>>,
119+
defaultValue: T,
120+
context: EvaluationContext = {},
121+
options: FlagEvaluationOptions = {}
122+
): Promise<EvaluationDetails<T>> {
123+
// merge global, client, and evaluation context
124+
const mergedContext = {
125+
...this.api.context,
126+
...this.context,
127+
...context,
128+
};
129+
130+
try {
131+
// if a transformer is defined, run it to prepare the context.
132+
const transformedContext =
133+
typeof this.provider.contextTransformer === 'function'
134+
? await this.provider.contextTransformer(mergedContext)
135+
: mergedContext;
136+
137+
// run the referenced resolver, binding the provider.
138+
const resolution = await resolver.call(this.provider, flagKey, defaultValue, transformedContext, options);
139+
return {
140+
...resolution,
141+
flagKey,
142+
};
143+
} catch (err: unknown) {
144+
const errorCode = (!!err && (err as { code: string }).code) || GENERAL_ERROR;
145+
return {
146+
errorCode,
147+
value: defaultValue,
148+
reason: ERROR_REASON,
149+
flagKey,
150+
};
151+
}
152+
}
153+
154+
private get provider() {
155+
return OpenFeature.instance.provider as TransformingProvider<unknown>;
156+
}
157+
}

src/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// reasons
2+
export const ERROR_REASON = 'ERROR';
3+
4+
// error-codes
5+
export const GENERAL_ERROR = 'GENERAL_ERROR';

src/index.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// real code will go here, just scaffolding the project for now.
2-
export const greet = (greeting: string): string => {
3-
const message = `${greeting}, OpenFeature`;
4-
return message;
5-
};
1+
export * from './open-feature';
2+
export * from './client';
3+
export * from './types';

src/no-op-provider.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Provider, ResolutionDetails } from './types';
2+
3+
const REASON_NO_OP = 'No-op';
4+
5+
/**
6+
* The No-op provider is set by default, and simply always returns the default value.
7+
*/
8+
class NoopFeatureProvider implements Provider {
9+
readonly name = 'No-op Provider';
10+
11+
resolveBooleanEvaluation(_: string, defaultValue: boolean): Promise<ResolutionDetails<boolean>> {
12+
return this.noOp(defaultValue);
13+
}
14+
15+
resolveStringEvaluation(_: string, defaultValue: string): Promise<ResolutionDetails<string>> {
16+
return this.noOp(defaultValue);
17+
}
18+
19+
resolveNumberEvaluation(_: string, defaultValue: number): Promise<ResolutionDetails<number>> {
20+
return this.noOp(defaultValue);
21+
}
22+
23+
resolveObjectEvaluation<T extends object>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
24+
return this.noOp<T>(defaultValue);
25+
}
26+
27+
private noOp<T>(defaultValue: T) {
28+
return Promise.resolve({
29+
value: defaultValue,
30+
reason: REASON_NO_OP,
31+
});
32+
}
33+
}
34+
35+
export const NOOP_PROVIDER = new NoopFeatureProvider();

src/open-feature.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { OpenFeatureClient } from './client';
2+
import { NOOP_PROVIDER } from './no-op-provider';
3+
import { Client, EvaluationContext, EvaluationLifeCycle, FlagValue, Hook, Provider } from './types';
4+
5+
// use a symbol as a key for the global singleton
6+
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js.api');
7+
8+
type OpenFeatureGlobal = {
9+
[GLOBAL_OPENFEATURE_API_KEY]?: OpenFeature;
10+
};
11+
const _global = global as OpenFeatureGlobal;
12+
13+
export class OpenFeature implements EvaluationLifeCycle {
14+
private _provider: Provider = NOOP_PROVIDER;
15+
private _context: EvaluationContext = {};
16+
17+
// eslint-disable-next-line @typescript-eslint/no-empty-function
18+
private constructor() {}
19+
20+
static get instance(): OpenFeature {
21+
const globalApi = _global[GLOBAL_OPENFEATURE_API_KEY];
22+
if (globalApi) {
23+
return globalApi;
24+
}
25+
26+
const instance = new OpenFeature();
27+
_global[GLOBAL_OPENFEATURE_API_KEY] = instance;
28+
return instance;
29+
}
30+
31+
getClient(name?: string, version?: string, context?: EvaluationContext): Client {
32+
return new OpenFeatureClient(this, { name, version }, context);
33+
}
34+
35+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36+
addHooks(...hooks: Hook<FlagValue>[]): void {
37+
throw new Error('Method not implemented.');
38+
}
39+
40+
get hooks(): Hook<FlagValue>[] {
41+
throw new Error('Method not implemented.');
42+
}
43+
44+
set provider(provider: Provider) {
45+
this._provider = provider;
46+
}
47+
48+
get provider(): Provider {
49+
return this._provider;
50+
}
51+
52+
set context(context: EvaluationContext) {
53+
this._context = context;
54+
}
55+
56+
get context(): EvaluationContext {
57+
return this._context;
58+
}
59+
}

0 commit comments

Comments
 (0)