Skip to content

Commit f2efa55

Browse files
authored
feat: support no default variant (#1354)
Signed-off-by: Todd Baert <[email protected]>
1 parent 91ba360 commit f2efa55

File tree

7 files changed

+33
-36
lines changed

7 files changed

+33
-36
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"checkObsoleteDependencies": true,
6565
"checkVersionMismatches": true,
6666
"ignoredDependencies": ["jest-cucumber", "jest"],
67-
"ignoredFiles": ["**/test/**", "**/tests/*", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/jest.*"]
67+
"ignoredFiles": ["**/test/**", "**/tests/**", "**/e2e/**", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/jest.*"]
6868
}
6969
]
7070
}

libs/providers/flagd-web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"@openfeature/web-sdk": "^1.0.0"
1111
},
1212
"dependencies": {
13-
"@openfeature/flagd-core": "1.1.0",
1413
"@connectrpc/connect": "^1.4.0",
1514
"@connectrpc/connect-web": "^1.4.0",
1615
"@bufbuild/protobuf": "^1.2.0"

libs/providers/flagd/src/e2e/step-definitions/flagSteps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ErrorCode } from '@openfeature/core';
12
import type { State, Steps } from './state';
23
import { mapValueToType } from './utils';
34

@@ -63,6 +64,10 @@ export const flagSteps: Steps =
6364
expect(state.details?.reason).toBe(expectedReason);
6465
});
6566

67+
then(/^the error-code should be "(.*)"$/, (expectedError: keyof typeof ErrorCode) => {
68+
expect(state.details?.errorCode).toBe(ErrorCode[expectedError]);
69+
});
70+
6671
then(/^the variant should be "(.*)"$/, (expectedVariant) => {
6772
expect(state.details?.variant).toBe(expectedVariant);
6873
});

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

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
EvaluationContext,
88
ResolutionReason,
99
} from '@openfeature/core';
10-
import { ParseError, StandardResolutionReasons, ErrorCode } from '@openfeature/core';
10+
import { StandardResolutionReasons, ErrorCode, GeneralError } from '@openfeature/core';
1111
import { sha1 } from 'object-hash';
1212
import { Targeting } from './targeting/targeting';
1313

@@ -45,7 +45,7 @@ type RequiredResolutionDetails<T> = Omit<ResolutionDetails<T>, 'value'> & {
4545
export class FeatureFlag {
4646
private readonly _key: string;
4747
private readonly _state: 'ENABLED' | 'DISABLED';
48-
private readonly _defaultVariant: string;
48+
private readonly _defaultVariant: string | undefined;
4949
private readonly _variants: Map<string, FlagValue>;
5050
private readonly _hash: string;
5151
private readonly _metadata: FlagMetadata;
@@ -59,7 +59,7 @@ export class FeatureFlag {
5959
) {
6060
this._key = key;
6161
this._state = flag['state'];
62-
this._defaultVariant = flag['defaultVariant'];
62+
this._defaultVariant = flag['defaultVariant'] || undefined;
6363
this._variants = new Map<string, FlagValue>(Object.entries(flag['variants']));
6464
this._metadata = flag['metadata'] ?? {};
6565

@@ -89,7 +89,7 @@ export class FeatureFlag {
8989
return this._state;
9090
}
9191

92-
get defaultVariant(): string {
92+
get defaultVariant(): string | undefined {
9393
return this._defaultVariant;
9494
}
9595

@@ -102,7 +102,7 @@ export class FeatureFlag {
102102
}
103103

104104
evaluate(evalCtx: EvaluationContext, logger: Logger = this.logger): RequiredResolutionDetails<JsonValue> {
105-
let variant: string;
105+
let variant: string | undefined;
106106
let reason: ResolutionReason;
107107

108108
if (this._targetingParseErrorMessage) {
@@ -142,7 +142,21 @@ export class FeatureFlag {
142142
}
143143
}
144144

145-
const resolvedValue = this._variants.get(variant);
145+
if (
146+
(variant === undefined || variant === null) &&
147+
(this.defaultVariant === null || this.defaultVariant === undefined)
148+
) {
149+
return {
150+
reason: StandardResolutionReasons.ERROR,
151+
errorCode: ErrorCode.FLAG_NOT_FOUND,
152+
errorMessage: `Flag '${this._key}' has no default variant defined, will use code default`,
153+
flagMetadata: this.metadata,
154+
};
155+
}
156+
157+
const resolvedVariant = variant as string;
158+
159+
const resolvedValue = this._variants.get(resolvedVariant);
146160
if (resolvedValue === undefined) {
147161
return {
148162
reason: StandardResolutionReasons.ERROR,
@@ -155,7 +169,7 @@ export class FeatureFlag {
155169
return {
156170
value: resolvedValue,
157171
reason,
158-
variant,
172+
variant: resolvedVariant,
159173
flagMetadata: this.metadata,
160174
};
161175
}
@@ -164,14 +178,10 @@ export class FeatureFlag {
164178
// basic validation, ideally this sort of thing is caught by IDEs and other schema validation before we get here
165179
// consistent with Java/Go and other implementations, we only warn for schema validation, but we fail for this sort of basic structural errors
166180
if (this._state !== 'ENABLED' && this._state !== 'DISABLED') {
167-
throw new ParseError(`Invalid flag state: ${JSON.stringify(this._state, undefined, 2)}`);
168-
}
169-
if (this._defaultVariant === undefined) {
170-
// this can be falsy, and int, etc...
171-
throw new ParseError(`Invalid flag defaultVariant: ${JSON.stringify(this._defaultVariant, undefined, 2)}`);
181+
throw new GeneralError(`Invalid flag state: ${JSON.stringify(this._state, undefined, 2)}`);
172182
}
173-
if (!this._variants.has(this._defaultVariant)) {
174-
throw new ParseError(
183+
if (this._defaultVariant && !this._variants.has(this._defaultVariant)) {
184+
throw new GeneralError(
175185
`Default variant ${this._defaultVariant} missing from variants ${JSON.stringify(this._variants, undefined, 2)}`,
176186
);
177187
}

libs/shared/flagd-core/src/lib/parser.spec.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,14 @@ describe('Flag configurations', () => {
3131

3232
it('should parse valid configurations - long', () => {
3333
const longFlag =
34-
'{"flags":{"myBoolFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myStringFlag":{"state":"ENABLED","variants":{"key1":"val1","key2":"val2"},"defaultVariant":"key1"},"myFloatFlag":{"state":"ENABLED","variants":{"one":1.23,"two":2.34},"defaultVariant":"one"},"myIntFlag":{"state":"ENABLED","variants":{"one":1,"two":2},"defaultVariant":"one"},"myObjectFlag":{"state":"ENABLED","variants":{"object1":{"key":"val"},"object2":{"key":true}},"defaultVariant":"object1"},"fibAlgo":{"variants":{"recursive":"recursive","memo":"memo","loop":"loop","binet":"binet"},"defaultVariant":"recursive","state":"ENABLED","targeting":{"if":[{"$ref":"emailWithFaas"},"binet",null]}},"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",{"in":["Chrome",{"var":"userAgent"}]},"third",null]}}},"$evaluators":{"emailWithFaas":{"in":["@faas.com",{"var":["email"]}]}}}';
34+
'{"flags":{"myBoolFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myStringFlag":{"state":"ENABLED","variants":{"key1":"val1","key2":"val2"},"defaultVariant":"key1"},"myFloatFlag":{"state":"ENABLED","variants":{"one":1.23,"two":2.34},"defaultVariant":"one"},"myIntFlag":{"state":"ENABLED","variants":{"one":1,"two":2},"defaultVariant":"one"},"myObjectFlag":{"state":"ENABLED","variants":{"object1":{"key":"val"},"object2":{"key":true}},"defaultVariant":"object1"},"fibAlgo":{"variants":{"recursive":"recursive","memo":"memo","loop":"loop","binet":"binet"},"defaultVariant":"recursive","state":"ENABLED","targeting":{"if":[{"$ref":"emailWithFaas"},"binet",null]}},"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",{"in":["Chrome",{"var":"userAgent"}]},"third",null]}},"undefinedDefaultFlag":{"state":"ENABLED","variants":{"on":true,"off":false}},"nullDefaultFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":null}},"$evaluators":{"emailWithFaas":{"in":["@faas.com",{"var":["email"]}]}}}';
3535

3636
const { flags } = parse(longFlag, false, logger);
3737
expect(flags).toBeTruthy();
3838
expect(flags.get('myBoolFlag')).toBeTruthy();
3939
expect(flags.get('myStringFlag')).toBeTruthy();
4040
});
4141

42-
it('should fail if invalid - missing default value', () => {
43-
const invalidFlag =
44-
'{\n' +
45-
' "flags": {\n' +
46-
' "myBoolFlag": {\n' +
47-
' "state": "ENABLED",\n' +
48-
' "variants": {\n' +
49-
' "on": true,\n' +
50-
' "off": false\n' +
51-
' }\n' +
52-
' }\n' +
53-
' }\n' +
54-
'}';
55-
56-
expect(() => parse(invalidFlag, false, logger)).toThrow();
57-
});
58-
5942
it('should parse flag configurations with references', () => {
6043
const flagWithRef =
6144
'{"flags":{"fibAlgo":{"variants":{"recursive":"recursive","binet":"binet"},"defaultVariant":"recursive","state":"ENABLED","targeting":{"if":[{"$ref":"emailSuffixFaas"},"binet",null]}}},"$evaluators":{"emailSuffixFaas":{"in":["@faas.com",{"var":["email"]}]}}}';

0 commit comments

Comments
 (0)