Skip to content

Commit 56a65a0

Browse files
committed
feat: implement optional value for OFREP Api
Signed-off-by: Lukas Reining <[email protected]>11 Signed-off-by: Lukas Reining <[email protected]>
1 parent 3b052ea commit 56a65a0

File tree

11 files changed

+196
-121
lines changed

11 files changed

+196
-121
lines changed

libs/providers/ofrep-web/src/lib/model/in-memory-cache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { FlagMetadata, FlagValue, ResolutionDetails } from '@openfeature/web-sdk';
2-
import type { ResolutionError } from './resolution-error';
1+
import type { FlagMetadata } from '@openfeature/web-sdk';
2+
import type { EvaluationResponse } from '@openfeature/ofrep-core';
33

44
/**
55
* Cache of flag values from bulk evaluation.
66
*/
7-
export type FlagCache = { [key: string]: ResolutionDetails<FlagValue> | ResolutionError };
7+
export type FlagCache = { [key: string]: EvaluationResponse };
88

99
/**
1010
* Cache of metadata from bulk evaluation.

libs/providers/ofrep-web/src/lib/ofrep-web-provider.spec.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { OFREPWebProvider } from './ofrep-web-provider';
22
import TestLogger from '../../test/test-logger';
3+
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
34
// eslint-disable-next-line @nx/enforce-module-boundaries
45
import { server } from '../../../../shared/ofrep-core/src/test/mock-service-worker';
5-
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
66
// eslint-disable-next-line @nx/enforce-module-boundaries
77
import { TEST_FLAG_METADATA, TEST_FLAG_SET_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';
88

@@ -159,6 +159,24 @@ describe('OFREPWebProvider', () => {
159159
expect(flag.flagMetadata).toEqual(TEST_FLAG_SET_METADATA);
160160
});
161161

162+
it('should return default value if API does not return a value', async () => {
163+
const flagKey = 'flag-without-value';
164+
const providerName = expect.getState().currentTestName || 'test-provider';
165+
const provider = new OFREPWebProvider({ baseUrl: endpointBaseURL }, new TestLogger());
166+
await OpenFeature.setContext(defaultContext);
167+
await OpenFeature.setProviderAndWait(providerName, provider);
168+
const client = OpenFeature.getClient(providerName);
169+
170+
const flag = client.getNumberDetails(flagKey, 42);
171+
expect(flag).toEqual({
172+
flagKey,
173+
value: 42,
174+
variant: 'emptyVariant',
175+
flagMetadata: TEST_FLAG_METADATA,
176+
reason: 'DISABLED',
177+
});
178+
});
179+
162180
it('should return EvaluationDetails if the flag exists', async () => {
163181
const flagKey = 'bool-flag';
164182
const providerName = expect.getState().currentTestName || 'test-provider';
@@ -190,7 +208,7 @@ describe('OFREPWebProvider', () => {
190208
flagKey,
191209
value: false,
192210
errorCode: 'PARSE_ERROR',
193-
errorMessage: 'Flag or flag configuration could not be parsed',
211+
errorMessage: 'custom error details',
194212
reason: 'ERROR',
195213
flagMetadata: {},
196214
});

libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts

Lines changed: 37 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import type { EvaluationRequest, EvaluationResponse } from '@openfeature/ofrep-core';
2+
import { ErrorMessageMap } from '@openfeature/ofrep-core';
23
import {
4+
type EvaluationFlagValue,
5+
handleEvaluationError,
6+
isEvaluationFailureResponse,
37
OFREPApi,
48
OFREPApiFetchError,
59
OFREPApiTooManyRequestsError,
610
OFREPApiUnauthorizedError,
711
OFREPForbiddenError,
8-
isEvaluationFailureResponse,
9-
isEvaluationSuccessResponse,
12+
toFlagMetadata,
13+
toResolutionDetails,
1014
} from '@openfeature/ofrep-core';
1115
import type {
1216
EvaluationContext,
@@ -30,18 +34,6 @@ import type { EvaluateFlagsResponse } from './model/evaluate-flags-response';
3034
import { BulkEvaluationStatus } from './model/evaluate-flags-response';
3135
import type { FlagCache, MetadataCache } from './model/in-memory-cache';
3236
import type { OFREPWebProviderOptions } from './model/ofrep-web-provider-options';
33-
import { isResolutionError } from './model/resolution-error';
34-
35-
const ErrorMessageMap: { [key in ErrorCode]: string } = {
36-
[ErrorCode.FLAG_NOT_FOUND]: 'Flag was not found',
37-
[ErrorCode.GENERAL]: 'General error',
38-
[ErrorCode.INVALID_CONTEXT]: 'Context is invalid or could be parsed',
39-
[ErrorCode.PARSE_ERROR]: 'Flag or flag configuration could not be parsed',
40-
[ErrorCode.PROVIDER_FATAL]: 'Provider is in a fatal error state',
41-
[ErrorCode.PROVIDER_NOT_READY]: 'Provider is not yet ready',
42-
[ErrorCode.TARGETING_KEY_MISSING]: 'Targeting key is missing',
43-
[ErrorCode.TYPE_MISMATCH]: 'Flag is not of expected type',
44-
};
4537

4638
export class OFREPWebProvider implements Provider {
4739
DEFAULT_POLL_INTERVAL = 30000;
@@ -62,7 +54,7 @@ export class OFREPWebProvider implements Provider {
6254
private _pollingInterval: number;
6355
private _retryPollingAfter: Date | undefined;
6456
private _flagCache: FlagCache = {};
65-
private _flagSetMetadataCache: MetadataCache = {};
57+
private _flagSetMetadataCache?: MetadataCache = {};
6658
private _context: EvaluationContext | undefined;
6759
private _pollingIntervalId?: number;
6860

@@ -109,28 +101,28 @@ export class OFREPWebProvider implements Provider {
109101
defaultValue: boolean,
110102
context: EvaluationContext,
111103
): ResolutionDetails<boolean> {
112-
return this._resolve(flagKey, 'boolean', defaultValue);
104+
return this._resolve(flagKey, defaultValue);
113105
}
114106
resolveStringEvaluation(
115107
flagKey: string,
116108
defaultValue: string,
117109
context: EvaluationContext,
118110
): ResolutionDetails<string> {
119-
return this._resolve(flagKey, 'string', defaultValue);
111+
return this._resolve(flagKey, defaultValue);
120112
}
121113
resolveNumberEvaluation(
122114
flagKey: string,
123115
defaultValue: number,
124116
context: EvaluationContext,
125117
): ResolutionDetails<number> {
126-
return this._resolve(flagKey, 'number', defaultValue);
118+
return this._resolve(flagKey, defaultValue);
127119
}
128120
resolveObjectEvaluation<T extends JsonValue>(
129121
flagKey: string,
130122
defaultValue: T,
131123
context: EvaluationContext,
132124
): ResolutionDetails<T> {
133-
return this._resolve(flagKey, 'object', defaultValue);
125+
return this._resolve(flagKey, defaultValue);
134126
}
135127

136128
/**
@@ -204,36 +196,24 @@ export class OFREPWebProvider implements Provider {
204196
}
205197

206198
const bulkSuccessResp = response.value;
207-
const newCache: FlagCache = {};
208-
209-
if ('flags' in bulkSuccessResp && Array.isArray(bulkSuccessResp.flags)) {
210-
bulkSuccessResp.flags.forEach((evalResp: EvaluationResponse) => {
211-
if (isEvaluationFailureResponse(evalResp)) {
212-
newCache[evalResp.key] = {
213-
reason: StandardResolutionReasons.ERROR,
214-
flagMetadata: evalResp.metadata,
215-
errorCode: evalResp.errorCode,
216-
errorDetails: evalResp.errorDetails,
217-
};
218-
}
219-
220-
if (isEvaluationSuccessResponse(evalResp) && evalResp.key) {
221-
newCache[evalResp.key] = {
222-
value: evalResp.value,
223-
variant: evalResp.variant,
224-
reason: evalResp.reason,
225-
flagMetadata: evalResp.metadata,
226-
};
227-
}
228-
});
229-
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
230-
this._flagCache = newCache;
231-
this._etag = response.httpResponse?.headers.get('etag');
232-
this._flagSetMetadataCache = typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {};
233-
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
234-
} else {
199+
if (!('flags' in bulkSuccessResp) || !Array.isArray(bulkSuccessResp.flags)) {
235200
throw new Error('No flags in OFREP bulk evaluation response');
236201
}
202+
203+
const newCache = bulkSuccessResp.flags.reduce<FlagCache>((currentCache, currentResponse) => {
204+
if (currentResponse.key) {
205+
currentCache[currentResponse.key] = currentResponse;
206+
}
207+
return currentCache;
208+
}, {});
209+
210+
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
211+
this._flagCache = newCache;
212+
this._etag = response.httpResponse?.headers.get('etag');
213+
this._flagSetMetadataCache = toFlagMetadata(
214+
typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {},
215+
);
216+
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
237217
} catch (error) {
238218
if (error instanceof OFREPApiTooManyRequestsError && error.retryAfterDate !== null) {
239219
this._retryPollingAfter = error.retryAfterDate;
@@ -278,7 +258,7 @@ export class OFREPWebProvider implements Provider {
278258
* @param defaultValue - default value
279259
* @private
280260
*/
281-
private _resolve<T extends FlagValue>(flagKey: string, type: string, defaultValue: T): ResolutionDetails<T> {
261+
private _resolve<T extends FlagValue>(flagKey: string, defaultValue: T): ResolutionDetails<T> {
282262
const resolved = this._flagCache[flagKey];
283263

284264
if (!resolved) {
@@ -291,32 +271,18 @@ export class OFREPWebProvider implements Provider {
291271
};
292272
}
293273

294-
if (isResolutionError(resolved)) {
295-
return {
296-
...resolved,
297-
value: defaultValue,
298-
errorMessage: ErrorMessageMap[resolved.errorCode],
299-
};
300-
}
274+
return this.responseToResolutionDetails(resolved, defaultValue);
275+
}
301276

302-
if (typeof resolved.value !== type) {
303-
return {
304-
value: defaultValue,
305-
flagMetadata: resolved.flagMetadata,
306-
reason: StandardResolutionReasons.ERROR,
307-
errorCode: ErrorCode.TYPE_MISMATCH,
308-
errorMessage: ErrorMessageMap[ErrorCode.TYPE_MISMATCH],
309-
};
277+
private responseToResolutionDetails<T extends EvaluationFlagValue>(
278+
response: EvaluationResponse,
279+
defaultValue: T,
280+
): ResolutionDetails<T> {
281+
if (isEvaluationFailureResponse(response)) {
282+
return handleEvaluationError(response, defaultValue);
310283
}
311284

312-
return {
313-
variant: resolved.variant,
314-
value: resolved.value as T,
315-
flagMetadata: resolved.flagMetadata,
316-
errorCode: resolved.errorCode,
317-
errorMessage: resolved.errorMessage,
318-
reason: resolved.reason,
319-
};
285+
return toResolutionDetails(response, defaultValue);
320286
}
321287

322288
/**

libs/providers/ofrep/src/lib/ofrep-provider.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
OFREPApiUnexpectedResponseError,
77
OFREPForbiddenError,
88
} from '@openfeature/ofrep-core';
9-
import { ErrorCode, GeneralError, TypeMismatchError } from '@openfeature/server-sdk';
9+
import { ErrorCode, GeneralError } from '@openfeature/server-sdk';
1010
// eslint-disable-next-line @nx/enforce-module-boundaries
1111
import { TEST_FLAG_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';
1212
// eslint-disable-next-line @nx/enforce-module-boundaries
@@ -166,6 +166,18 @@ describe('OFREPProvider should', () => {
166166
expect(flag.value).toEqual(true);
167167
});
168168

169+
it('should return default value if API does not return a value', async () => {
170+
const flag = await provider.resolveNumberEvaluation('flag-without-value', 42, {
171+
errors: { disabled: true },
172+
});
173+
expect(flag).toEqual({
174+
value: 42,
175+
variant: 'emptyVariant',
176+
flagMetadata: TEST_FLAG_METADATA,
177+
reason: 'DISABLED',
178+
});
179+
});
180+
169181
it('run successful evaluation of basic boolean flag', async () => {
170182
const flag = await provider.resolveBooleanEvaluation('my-flag', false, {});
171183
expect(flag.value).toEqual(true);

libs/providers/ofrep/src/lib/ofrep-provider.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { EvaluationFlagValue, OFREPApiEvaluationResult, OFREPProviderBaseOptions } from '@openfeature/ofrep-core';
2+
import { isEvaluationFailureResponse } from '@openfeature/ofrep-core';
23
import {
34
OFREPApi,
45
OFREPApiTooManyRequestsError,
56
handleEvaluationError,
67
toResolutionDetails,
78
} from '@openfeature/ofrep-core';
89
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails } from '@openfeature/server-sdk';
9-
import { ErrorCode, GeneralError, StandardResolutionReasons } from '@openfeature/server-sdk';
10+
import { GeneralError } from '@openfeature/server-sdk';
1011

1112
export type OFREPProviderOptions = OFREPProviderBaseOptions;
1213

@@ -91,19 +92,13 @@ export class OFREPProvider implements Provider {
9192
defaultValue: T,
9293
): ResolutionDetails<T> {
9394
if (result.httpStatus !== 200) {
94-
return handleEvaluationError(result, defaultValue);
95+
return handleEvaluationError(result.value, defaultValue);
9596
}
9697

97-
if (typeof result.value.value !== typeof defaultValue) {
98-
return {
99-
value: defaultValue,
100-
reason: StandardResolutionReasons.ERROR,
101-
flagMetadata: result.value.metadata,
102-
errorCode: ErrorCode.TYPE_MISMATCH,
103-
errorMessage: 'Flag is not of expected type',
104-
};
98+
if (isEvaluationFailureResponse(result)) {
99+
return handleEvaluationError(result, defaultValue);
105100
}
106101

107-
return toResolutionDetails(result.value);
102+
return toResolutionDetails(result.value, defaultValue);
108103
}
109104
}

libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,16 @@ describe('OFREPApi', () => {
288288
},
289289
},
290290
},
291+
{
292+
key: 'flag-without-value',
293+
metadata: {
294+
booleanKey: true,
295+
numberKey: 1,
296+
stringKey: 'string',
297+
},
298+
reason: 'DISABLED',
299+
variant: 'emptyVariant',
300+
},
291301
],
292302
} satisfies BulkEvaluationSuccessResponse);
293303
});
@@ -367,6 +377,16 @@ describe('OFREPApi', () => {
367377
},
368378
},
369379
},
380+
{
381+
key: 'flag-without-value',
382+
metadata: {
383+
booleanKey: true,
384+
numberKey: 1,
385+
stringKey: 'string',
386+
},
387+
reason: 'DISABLED',
388+
variant: 'emptyVariant',
389+
},
370390
],
371391
});
372392
});

0 commit comments

Comments
 (0)