Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions libs/providers/ofrep-web/src/lib/model/in-memory-cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { FlagMetadata, FlagValue, ResolutionDetails } from '@openfeature/web-sdk';
import type { ResolutionError } from './resolution-error';
import type { FlagMetadata } from '@openfeature/web-sdk';
import type { EvaluationResponse } from '@openfeature/ofrep-core';

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

/**
* Cache of metadata from bulk evaluation.
Expand Down
22 changes: 20 additions & 2 deletions libs/providers/ofrep-web/src/lib/ofrep-web-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OFREPWebProvider } from './ofrep-web-provider';
import TestLogger from '../../test/test-logger';
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { server } from '../../../../shared/ofrep-core/src/test/mock-service-worker';
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { TEST_FLAG_METADATA, TEST_FLAG_SET_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';

Expand Down Expand Up @@ -159,6 +159,24 @@ describe('OFREPWebProvider', () => {
expect(flag.flagMetadata).toEqual(TEST_FLAG_SET_METADATA);
});

it('should return default value if API does not return a value', async () => {
const flagKey = 'flag-without-value';
const providerName = expect.getState().currentTestName || 'test-provider';
const provider = new OFREPWebProvider({ baseUrl: endpointBaseURL }, new TestLogger());
await OpenFeature.setContext(defaultContext);
await OpenFeature.setProviderAndWait(providerName, provider);
const client = OpenFeature.getClient(providerName);

const flag = client.getNumberDetails(flagKey, 42);
expect(flag).toEqual({
flagKey,
value: 42,
variant: 'emptyVariant',
flagMetadata: TEST_FLAG_METADATA,
reason: 'DISABLED',
});
});

it('should return EvaluationDetails if the flag exists', async () => {
const flagKey = 'bool-flag';
const providerName = expect.getState().currentTestName || 'test-provider';
Expand Down Expand Up @@ -190,7 +208,7 @@ describe('OFREPWebProvider', () => {
flagKey,
value: false,
errorCode: 'PARSE_ERROR',
errorMessage: 'Flag or flag configuration could not be parsed',
errorMessage: 'custom error details',
reason: 'ERROR',
flagMetadata: {},
});
Expand Down
108 changes: 37 additions & 71 deletions libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { EvaluationRequest, EvaluationResponse } from '@openfeature/ofrep-core';
import { ErrorMessageMap } from '@openfeature/ofrep-core';
import {
type EvaluationFlagValue,
handleEvaluationError,
isEvaluationFailureResponse,
OFREPApi,
OFREPApiFetchError,
OFREPApiTooManyRequestsError,
OFREPApiUnauthorizedError,
OFREPForbiddenError,
isEvaluationFailureResponse,
isEvaluationSuccessResponse,
toFlagMetadata,
toResolutionDetails,
} from '@openfeature/ofrep-core';
import type {
EvaluationContext,
Expand All @@ -30,18 +34,6 @@ import type { EvaluateFlagsResponse } from './model/evaluate-flags-response';
import { BulkEvaluationStatus } from './model/evaluate-flags-response';
import type { FlagCache, MetadataCache } from './model/in-memory-cache';
import type { OFREPWebProviderOptions } from './model/ofrep-web-provider-options';
import { isResolutionError } from './model/resolution-error';

const ErrorMessageMap: { [key in ErrorCode]: string } = {
[ErrorCode.FLAG_NOT_FOUND]: 'Flag was not found',
[ErrorCode.GENERAL]: 'General error',
[ErrorCode.INVALID_CONTEXT]: 'Context is invalid or could be parsed',
[ErrorCode.PARSE_ERROR]: 'Flag or flag configuration could not be parsed',
[ErrorCode.PROVIDER_FATAL]: 'Provider is in a fatal error state',
[ErrorCode.PROVIDER_NOT_READY]: 'Provider is not yet ready',
[ErrorCode.TARGETING_KEY_MISSING]: 'Targeting key is missing',
[ErrorCode.TYPE_MISMATCH]: 'Flag is not of expected type',
};

export class OFREPWebProvider implements Provider {
DEFAULT_POLL_INTERVAL = 30000;
Expand All @@ -62,7 +54,7 @@ export class OFREPWebProvider implements Provider {
private _pollingInterval: number;
private _retryPollingAfter: Date | undefined;
private _flagCache: FlagCache = {};
private _flagSetMetadataCache: MetadataCache = {};
private _flagSetMetadataCache?: MetadataCache = {};
private _context: EvaluationContext | undefined;
private _pollingIntervalId?: number;

Expand Down Expand Up @@ -109,28 +101,28 @@ export class OFREPWebProvider implements Provider {
defaultValue: boolean,
context: EvaluationContext,
): ResolutionDetails<boolean> {
return this._resolve(flagKey, 'boolean', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
): ResolutionDetails<string> {
return this._resolve(flagKey, 'string', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
): ResolutionDetails<number> {
return this._resolve(flagKey, 'number', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: T,
context: EvaluationContext,
): ResolutionDetails<T> {
return this._resolve(flagKey, 'object', defaultValue);
return this._resolve(flagKey, defaultValue);
}

/**
Expand Down Expand Up @@ -204,36 +196,24 @@ export class OFREPWebProvider implements Provider {
}

const bulkSuccessResp = response.value;
const newCache: FlagCache = {};

if ('flags' in bulkSuccessResp && Array.isArray(bulkSuccessResp.flags)) {
bulkSuccessResp.flags.forEach((evalResp: EvaluationResponse) => {
if (isEvaluationFailureResponse(evalResp)) {
newCache[evalResp.key] = {
reason: StandardResolutionReasons.ERROR,
flagMetadata: evalResp.metadata,
errorCode: evalResp.errorCode,
errorDetails: evalResp.errorDetails,
};
}

if (isEvaluationSuccessResponse(evalResp) && evalResp.key) {
newCache[evalResp.key] = {
value: evalResp.value,
variant: evalResp.variant,
reason: evalResp.reason,
flagMetadata: evalResp.metadata,
};
}
});
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
this._flagCache = newCache;
this._etag = response.httpResponse?.headers.get('etag');
this._flagSetMetadataCache = typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {};
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
} else {
if (!('flags' in bulkSuccessResp) || !Array.isArray(bulkSuccessResp.flags)) {
throw new Error('No flags in OFREP bulk evaluation response');
}

const newCache = bulkSuccessResp.flags.reduce<FlagCache>((currentCache, currentResponse) => {
if (currentResponse.key) {
currentCache[currentResponse.key] = currentResponse;
}
return currentCache;
}, {});

const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
this._flagCache = newCache;
this._etag = response.httpResponse?.headers.get('etag');
this._flagSetMetadataCache = toFlagMetadata(
typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {},
);
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
} catch (error) {
if (error instanceof OFREPApiTooManyRequestsError && error.retryAfterDate !== null) {
this._retryPollingAfter = error.retryAfterDate;
Expand Down Expand Up @@ -278,7 +258,7 @@ export class OFREPWebProvider implements Provider {
* @param defaultValue - default value
* @private
*/
private _resolve<T extends FlagValue>(flagKey: string, type: string, defaultValue: T): ResolutionDetails<T> {
private _resolve<T extends FlagValue>(flagKey: string, defaultValue: T): ResolutionDetails<T> {
const resolved = this._flagCache[flagKey];

if (!resolved) {
Expand All @@ -291,32 +271,18 @@ export class OFREPWebProvider implements Provider {
};
}

if (isResolutionError(resolved)) {
return {
...resolved,
value: defaultValue,
errorMessage: ErrorMessageMap[resolved.errorCode],
};
}
return this.responseToResolutionDetails(resolved, defaultValue);
}

if (typeof resolved.value !== type) {
return {
value: defaultValue,
flagMetadata: resolved.flagMetadata,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.TYPE_MISMATCH,
errorMessage: ErrorMessageMap[ErrorCode.TYPE_MISMATCH],
};
private responseToResolutionDetails<T extends EvaluationFlagValue>(
response: EvaluationResponse,
defaultValue: T,
): ResolutionDetails<T> {
if (isEvaluationFailureResponse(response)) {
return handleEvaluationError(response, defaultValue);
}

return {
variant: resolved.variant,
value: resolved.value as T,
flagMetadata: resolved.flagMetadata,
errorCode: resolved.errorCode,
errorMessage: resolved.errorMessage,
reason: resolved.reason,
};
return toResolutionDetails(response, defaultValue);
}

/**
Expand Down
14 changes: 13 additions & 1 deletion libs/providers/ofrep/src/lib/ofrep-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
OFREPApiUnexpectedResponseError,
OFREPForbiddenError,
} from '@openfeature/ofrep-core';
import { ErrorCode, GeneralError, TypeMismatchError } from '@openfeature/server-sdk';
import { ErrorCode, GeneralError } from '@openfeature/server-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { TEST_FLAG_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';
// eslint-disable-next-line @nx/enforce-module-boundaries
Expand Down Expand Up @@ -166,6 +166,18 @@ describe('OFREPProvider should', () => {
expect(flag.value).toEqual(true);
});

it('should return default value if API does not return a value', async () => {
const flag = await provider.resolveNumberEvaluation('flag-without-value', 42, {
errors: { disabled: true },
});
expect(flag).toEqual({
value: 42,
variant: 'emptyVariant',
flagMetadata: TEST_FLAG_METADATA,
reason: 'DISABLED',
});
});

it('run successful evaluation of basic boolean flag', async () => {
const flag = await provider.resolveBooleanEvaluation('my-flag', false, {});
expect(flag.value).toEqual(true);
Expand Down
17 changes: 6 additions & 11 deletions libs/providers/ofrep/src/lib/ofrep-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { EvaluationFlagValue, OFREPApiEvaluationResult, OFREPProviderBaseOptions } from '@openfeature/ofrep-core';
import { isEvaluationFailureResponse } from '@openfeature/ofrep-core';
import {
OFREPApi,
OFREPApiTooManyRequestsError,
handleEvaluationError,
toResolutionDetails,
} from '@openfeature/ofrep-core';
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails } from '@openfeature/server-sdk';
import { ErrorCode, GeneralError, StandardResolutionReasons } from '@openfeature/server-sdk';
import { GeneralError } from '@openfeature/server-sdk';

export type OFREPProviderOptions = OFREPProviderBaseOptions;

Expand Down Expand Up @@ -91,19 +92,13 @@ export class OFREPProvider implements Provider {
defaultValue: T,
): ResolutionDetails<T> {
if (result.httpStatus !== 200) {
return handleEvaluationError(result, defaultValue);
return handleEvaluationError(result.value, defaultValue);
}

if (typeof result.value.value !== typeof defaultValue) {
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
flagMetadata: result.value.metadata,
errorCode: ErrorCode.TYPE_MISMATCH,
errorMessage: 'Flag is not of expected type',
};
if (isEvaluationFailureResponse(result)) {
return handleEvaluationError(result, defaultValue);
}

return toResolutionDetails(result.value);
return toResolutionDetails(result.value, defaultValue);
}
}
20 changes: 20 additions & 0 deletions libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ describe('OFREPApi', () => {
},
},
},
{
key: 'flag-without-value',
metadata: {
booleanKey: true,
numberKey: 1,
stringKey: 'string',
},
reason: 'DISABLED',
variant: 'emptyVariant',
},
],
} satisfies BulkEvaluationSuccessResponse);
});
Expand Down Expand Up @@ -367,6 +377,16 @@ describe('OFREPApi', () => {
},
},
},
{
key: 'flag-without-value',
metadata: {
booleanKey: true,
numberKey: 1,
stringKey: 'string',
},
reason: 'DISABLED',
variant: 'emptyVariant',
},
],
});
});
Expand Down
Loading