Skip to content

Commit a88708c

Browse files
committed
WIP
Signed-off-by: Michael Beemer <[email protected]>
1 parent d08f41d commit a88708c

File tree

16 files changed

+368
-283
lines changed

16 files changed

+368
-283
lines changed

.gitmodules

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[submodule "libs/providers/flagd/schemas"]
22
path = libs/providers/flagd/schemas
3-
url = https://github.com/open-feature/schemas.git
3+
url = https://github.com/open-feature/flagd-schemas.git
44
[submodule "libs/providers/flagd-web/schemas"]
55
path = libs/providers/flagd-web/schemas
6-
url = https://github.com/open-feature/schemas
6+
url = https://github.com/open-feature/flagd-schemas.git
77
[submodule "libs/providers/flagd/spec"]
88
path = libs/providers/flagd/spec
99
url = https://github.com/open-feature/spec.git
Submodule flagd-schemas updated 45 files
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
{
22
"name": "@openfeature/flagd-core",
33
"version": "0.2.5",
4+
"license": "Apache-2.0",
45
"scripts": {
56
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
67
"current-version": "echo $npm_package_version"
78
},
89
"peerDependencies": {
9-
"@openfeature/core": ">=0.0.16"
10+
"@openfeature/core": ">=1.6.0"
1011
},
1112
"dependencies": {
1213
"ajv": "^8.12.0",
1314
"tslib": "^2.3.0"
1415
}
15-
}
16+
}

libs/shared/flagd-core/src/lib/feature-flag.spec.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
import type { Logger } from '@openfeature/core';
12
import { FeatureFlag, Flag } from './feature-flag';
23

4+
const logger: Logger = {
5+
error: jest.fn(),
6+
warn: jest.fn(),
7+
info: jest.fn(),
8+
debug: jest.fn(),
9+
};
10+
311
describe('Flagd flag structure', () => {
412
it('should be constructed with valid input - boolean', () => {
513
const input: Flag = {
@@ -12,12 +20,11 @@ describe('Flagd flag structure', () => {
1220
targeting: '',
1321
};
1422

15-
const ff = new FeatureFlag(input);
23+
const ff = new FeatureFlag('test', input, logger);
1624

1725
expect(ff).toBeTruthy();
1826
expect(ff.state).toBe('ENABLED');
1927
expect(ff.defaultVariant).toBe('off');
20-
expect(ff.targeting).toBe('');
2128
expect(ff.variants.get('on')).toBeTruthy();
2229
expect(ff.variants.get('off')).toBeFalsy();
2330
});
@@ -33,12 +40,11 @@ describe('Flagd flag structure', () => {
3340
targeting: '',
3441
};
3542

36-
const ff = new FeatureFlag(input);
43+
const ff = new FeatureFlag('test', input, logger);
3744

3845
expect(ff).toBeTruthy();
3946
expect(ff.state).toBe('ENABLED');
4047
expect(ff.defaultVariant).toBe('one');
41-
expect(ff.targeting).toBe('');
4248
expect(ff.variants.get('one')).toBe(1.0);
4349
expect(ff.variants.get('two')).toBe(2.0);
4450
});
@@ -60,12 +66,11 @@ describe('Flagd flag structure', () => {
6066
targeting: '',
6167
};
6268

63-
const ff = new FeatureFlag(input);
69+
const ff = new FeatureFlag('test', input, logger);
6470

6571
expect(ff).toBeTruthy();
6672
expect(ff.state).toBe('ENABLED');
6773
expect(ff.defaultVariant).toBe('pi2');
68-
expect(ff.targeting).toBe('');
6974
expect(ff.variants.get('pi2')).toStrictEqual({ value: 3.14, accuracy: 2 });
7075
expect(ff.variants.get('pi5')).toStrictEqual({ value: 3.14159, accuracy: 5 });
7176
});

libs/shared/flagd-core/src/lib/feature-flag.ts

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { FlagValue, ParseError } from '@openfeature/core';
1+
import type {
2+
FlagValue,
3+
FlagMetadata,
4+
ResolutionDetails,
5+
JsonValue,
6+
Logger,
7+
ResolutionReason,
8+
EvaluationContext,
9+
} from '@openfeature/core';
10+
import { ParseError, StandardResolutionReasons, GeneralError, TypeMismatchError } from '@openfeature/core';
211
import { sha1 } from 'object-hash';
12+
import { Targeting } from './targeting/targeting';
313

414
/**
515
* Flagd flag configuration structure mapping to schema definition.
@@ -9,27 +19,38 @@ export interface Flag {
919
defaultVariant: string;
1020
variants: { [key: string]: FlagValue };
1121
targeting?: string;
22+
metadata?: FlagMetadata;
1223
}
1324

1425
/**
1526
* Flagd flag configuration structure for internal reference.
1627
*/
1728
export class FeatureFlag {
29+
private readonly _key: string;
1830
private readonly _state: 'ENABLED' | 'DISABLED';
1931
private readonly _defaultVariant: string;
2032
private readonly _variants: Map<string, FlagValue>;
21-
private readonly _targeting: unknown;
2233
private readonly _hash: string;
34+
private readonly _metadata: FlagMetadata;
35+
private readonly _targeting?: Targeting;
2336

24-
constructor(flag: Flag) {
37+
constructor(key: string, flag: Flag, logger: Logger) {
38+
this._key = key;
2539
this._state = flag['state'];
2640
this._defaultVariant = flag['defaultVariant'];
2741
this._variants = new Map<string, FlagValue>(Object.entries(flag['variants']));
28-
this._targeting = flag['targeting'];
42+
this._metadata = flag['metadata'] ?? {};
43+
this._targeting =
44+
flag.targeting && Object.keys(flag.targeting).length > 0 ? new Targeting(flag.targeting, logger) : undefined;
2945
this._hash = sha1(flag);
46+
3047
this.validateStructure();
3148
}
3249

50+
get key(): string {
51+
return this._key;
52+
}
53+
3354
get hash(): string {
3455
return this._hash;
3556
}
@@ -42,14 +63,58 @@ export class FeatureFlag {
4263
return this._defaultVariant;
4364
}
4465

45-
get targeting(): unknown {
46-
return this._targeting;
47-
}
48-
4966
get variants(): Map<string, FlagValue> {
5067
return this._variants;
5168
}
5269

70+
get metadata(): FlagMetadata {
71+
return this._metadata;
72+
}
73+
74+
evaluate(evalCtx: EvaluationContext): ResolutionDetails<JsonValue> {
75+
let variant: string;
76+
let reason: ResolutionReason;
77+
78+
if (!this._targeting) {
79+
variant = this._defaultVariant;
80+
reason = StandardResolutionReasons.STATIC;
81+
} else {
82+
let targetingResolution: JsonValue;
83+
try {
84+
targetingResolution = this._targeting.evaluate(this._key, evalCtx);
85+
} catch (e) {
86+
console.log(e);
87+
throw new GeneralError(`Error evaluating targeting rule for flag '${this._key}'`, { cause: e });
88+
}
89+
90+
// Return default variant if targeting resolution is null or undefined
91+
if (targetingResolution == null) {
92+
variant = this._defaultVariant;
93+
reason = StandardResolutionReasons.DEFAULT;
94+
} else {
95+
// Obtain resolution in string. This is useful for short-circuiting json logic
96+
variant = targetingResolution.toString();
97+
reason = StandardResolutionReasons.TARGETING_MATCH;
98+
}
99+
}
100+
101+
if (typeof variant !== 'string') {
102+
throw new TypeMismatchError(`Variant must be a string, but found '${typeof variant}'`);
103+
}
104+
105+
const resolvedVariant = this._variants.get(variant);
106+
if (resolvedVariant === undefined) {
107+
throw new GeneralError(`Variant '${variant}' not found in flag with key '${this._key}'`);
108+
}
109+
110+
return {
111+
value: resolvedVariant,
112+
reason,
113+
variant,
114+
flagMetadata: this.metadata,
115+
};
116+
}
117+
53118
validateStructure() {
54119
// basic validation, ideally this sort of thing is caught by IDEs and other schema validation before we get here
55120
// consistent with Java/Go and other implementations, we only warn for schema validation, but we fail for this sort of basic structural errors

0 commit comments

Comments
 (0)