Skip to content

Commit

Permalink
Improved isItOn API - added fluent detail feature (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
petruki authored Apr 13, 2024
1 parent c8c7264 commit cc1437f
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 66 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ Invoking the API can be done by instantiating the switcher and calling *isItOn*

```ts
const switcher = Switcher.factory();
await switcher.isItOn('FEATURE01');
await switcher.isItOn('FEATURE01') as boolean;
// or
const { result, reason, metadata } = await switcher.detail().isItOn('FEATURE01') as ResultDetail;
```

2. **Promise**
Expand Down Expand Up @@ -162,7 +164,7 @@ Switcher.forget('FEATURE01');
switcher.isItOn('FEATURE01'); // Now, it's going to return the result retrieved from the API or the Snaopshot file

Switcher.assume('FEATURE01').false().withMetadata({ message: 'Feature is disabled' }); // Include metadata to emulate Relay response
const response = await switcher.isItOn('FEATURE01', [], true) as ResultDetail; // false
const response = await switcher.detail().isItOn('FEATURE01') as ResultDetail; // false
console.log(response.metadata.message); // Feature is disabled
```
Expand Down
3 changes: 2 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"test": "deno test --allow-read --allow-net --allow-write --coverage=coverage",
"lcov": "deno coverage coverage --lcov --output=coverage/report.lcov",
"clean": "rm -rf ./npm ./coverage ./generated-snapshots",
"cover": "deno task clean && deno task test && deno task lcov && genhtml -o coverage/html coverage/report.lcov"
"cover": "deno task clean && deno task test && deno task lcov && genhtml -o coverage/html coverage/report.lcov",
"play": "deno run -A test/playground/index.ts"
},
"lock": false,
"test": {
Expand Down
7 changes: 0 additions & 7 deletions snapshot/local.json

This file was deleted.

14 changes: 10 additions & 4 deletions src/lib/remote.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { AuthError, CheckSwitcherError, CriteriaError, SnapshotServiceError } from './exceptions/index.ts';
import type { Entry, SwitcherContext } from '../types/index.d.ts';
import type {
AuthResponse,
CheckSnapshotVersionResponse,
Entry,
ResultDetail,
SwitcherContext,
} from '../types/index.d.ts';

let httpClient: Deno.HttpClient;

Expand Down Expand Up @@ -67,7 +73,7 @@ export const checkCriteria = async (
);

if (response.status == 200) {
return response.json();
return response.json() as Promise<ResultDetail>;
}

throw new Error(`[checkCriteria] failed with status ${response.status}`);
Expand Down Expand Up @@ -95,7 +101,7 @@ export const auth = async (context: SwitcherContext) => {
});

if (response.status == 200) {
return response.json();
return response.json() as Promise<AuthResponse>;
}

throw new Error(`[auth] failed with status ${response.status}`);
Expand Down Expand Up @@ -149,7 +155,7 @@ export const checkSnapshotVersion = async (
});

if (response.status == 200) {
return response.json();
return response.json() as Promise<CheckSnapshotVersionResponse>;
}

throw new Error(`[checkSnapshotVersion] failed with status ${response.status}`);
Expand Down
6 changes: 3 additions & 3 deletions src/lib/utils/executionLogger.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Criteria } from '../../types/index.d.ts';
import type { ResultDetail } from '../../types/index.d.ts';

const logger: ExecutionLogger[] = [];

export default class ExecutionLogger {
key?: string;
input?: string[][];
response: Criteria = { result: false };
response: ResultDetail = { result: false };

/**
* Add new execution result
Expand All @@ -15,7 +15,7 @@ export default class ExecutionLogger {
* @param response
*/
static add(
response: Criteria,
response: ResultDetail,
key: string,
input?: string[][],
): void {
Expand Down
61 changes: 30 additions & 31 deletions src/switcher-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class Switcher {
private _input?: string[][];
private _key = '';
private _forceRemote = false;
private _showDetail = false;

/**
* Create the necessary configuration to communicate with the API
Expand Down Expand Up @@ -442,34 +443,33 @@ export class Switcher {
*
* @param key
* @param input
* @param showDetail Display details (ResultDetail)
*/
async isItOn(key?: string, input?: string[][], showDetail = false): Promise<boolean | ResultDetail> {
async isItOn(key?: string, input?: string[][]): Promise<boolean | ResultDetail> {
let result: boolean | ResultDetail;
this._validateArgs(key, input);

// verify if query from Bypasser
const bypassKey = Bypasser.searchBypassed(this._key);
if (bypassKey) {
const response = bypassKey.getResponse();
return showDetail ? response : response.result;
return this._showDetail ? response : response.result;
}

// verify if query from snapshot
if (Switcher._options.local && !this._forceRemote) {
result = await this._executeLocalCriteria(showDetail);
result = await this._executeLocalCriteria();
} else {
try {
await this.validate();
if (Switcher._context.token === 'SILENT') {
result = await this._executeLocalCriteria(showDetail);
result = await this._executeLocalCriteria();
} else {
result = await this._executeRemoteCriteria(showDetail);
result = await this._executeRemoteCriteria();
}
} catch (err) {
if (Switcher._options.silentMode) {
Switcher._updateSilentToken();
return this._executeLocalCriteria(showDetail);
return this._executeLocalCriteria();
}

throw err;
Expand Down Expand Up @@ -509,30 +509,33 @@ export class Switcher {
return this;
}

async _executeRemoteCriteria(showDetail: boolean) {
if (!this._useSync()) {
return this._executeAsyncRemoteCriteria(showDetail);
}
detail(showDetail = true) {
this._showDetail = showDetail;
return this;
}

const responseCriteria = await services.checkCriteria(
Switcher._context,
this._key,
this._input,
showDetail,
);
async _executeRemoteCriteria(): Promise<boolean | ResultDetail> {
let responseCriteria: ResultDetail;

if (Switcher._options.logger && this._key) {
ExecutionLogger.add(responseCriteria, this._key, this._input);
}
if (this._useSync()) {
responseCriteria = await services.checkCriteria(
Switcher._context,
this._key,
this._input,
this._showDetail,
);

if (showDetail) {
return responseCriteria;
if (Switcher._options.logger && this._key) {
ExecutionLogger.add(responseCriteria, this._key, this._input);
}
} else {
responseCriteria = this._executeAsyncRemoteCriteria(this._showDetail);
}

return responseCriteria.result;
return this._showDetail ? responseCriteria : responseCriteria.result;
}

_executeAsyncRemoteCriteria(showDetail: boolean) {
_executeAsyncRemoteCriteria(showDetail: boolean): ResultDetail {
if (this._nextRun < Date.now()) {
this._nextRun = Date.now() + this._delay;
services.checkCriteria(
Expand All @@ -544,11 +547,7 @@ export class Switcher {
}

const executionLog = ExecutionLogger.getExecution(this._key, this._input);
if (showDetail) {
return executionLog.response;
}

return executionLog.response.result;
return executionLog.response;
}

async _executeApiValidation() {
Expand All @@ -562,7 +561,7 @@ export class Switcher {
}
}

async _executeLocalCriteria(showDetail: boolean) {
async _executeLocalCriteria() {
const response = await checkCriteriaLocal(
Switcher._snapshot,
this._key || '',
Expand All @@ -573,7 +572,7 @@ export class Switcher {
ExecutionLogger.add(response, this._key, this._input);
}

if (showDetail) {
if (this._showDetail) {
return response;
}

Expand Down
20 changes: 15 additions & 5 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ declare global {
};
}

// Client SDK settings types

export type SwitcherContext = {
url?: string;
apiKey?: string;
Expand Down Expand Up @@ -39,6 +41,19 @@ export type RetryOptions = {
retryDurationIn: string;
};

// Remote API types

export type AuthResponse = {
token: string;
exp: number;
};

export type CheckSnapshotVersionResponse = {
status: boolean;
};

// Switcher API domain types

export type Snapshot = {
data: SnapshotData;
};
Expand Down Expand Up @@ -78,11 +93,6 @@ export type Entry = {
input: string;
};

export type Criteria = {
result: boolean;
reason?: string;
};

export type ResultDetail = {
result: boolean;
reason?: string;
Expand Down
15 changes: 8 additions & 7 deletions test/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const domain = 'Playground';
const component = 'switcher-playground';
const environment = 'default';
const url = 'https://api.switcherapi.com';
const snapshotLocation = './snapshot/';
const snapshotLocation = './test/playground/snapshot/';

let switcher: Switcher;

Expand All @@ -31,7 +31,7 @@ const _testLocal = async () => {
domain: 'Local Playground',
environment: 'local'
}, {
snapshotLocation: './snapshot/',
snapshotLocation: './test/playground/snapshot/',
local: true
});

Expand All @@ -43,8 +43,8 @@ const _testLocal = async () => {

setInterval(async () => {
const time = Date.now();
const result = await switcher.isItOn(SWITCHER_KEY) as boolean;
console.log(`- ${Date.now() - time} ms - ${result}`);
const result = await switcher.detail().isItOn(SWITCHER_KEY);
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);
};

Expand All @@ -60,8 +60,8 @@ const _testSimpleAPICall = async (local: boolean) => {

setInterval(async () => {
const time = Date.now();
const result = await switcher.isItOn(SWITCHER_KEY) as boolean;
console.log(`- ${Date.now() - time} ms - ${result}`);
const result = await switcher.isItOn(SWITCHER_KEY);
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);
};

Expand All @@ -75,7 +75,8 @@ const _testThrottledAPICall = async () => {
switcher.throttle(1000);

for (let index = 0; index < 10; index++) {
console.log(`Call #${index} - ${await switcher.isItOn(SWITCHER_KEY, [checkNumeric('1')]) as boolean}}`);
const result = await switcher.isItOn(SWITCHER_KEY, [checkNumeric('1')]);
console.log(`Call #${index} - ${JSON.stringify(result)}}`);
}

Switcher.unloadSnapshot();
Expand Down
8 changes: 4 additions & 4 deletions test/switcher-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ describe('E2E test - Switcher local:', function () {
});

it('should be valid - isItOn - with detail', testSettings, async function () {
const response = await switcher.isItOn('FF2FOR2020', [
const response = await switcher.detail().isItOn('FF2FOR2020', [
checkValue('Japan'),
checkNetwork('10.0.0.3')
], true) as ResultDetail;
]) as ResultDetail;

assertTrue(response.result);
assertEquals(response.reason, 'Success');
Expand Down Expand Up @@ -173,15 +173,15 @@ describe('E2E test - Switcher local:', function () {

it('should be valid assuming key to be false - with details', async function () {
Switcher.assume('FF2FOR2020').false();
const { result, reason } = await switcher.isItOn('FF2FOR2020', [], true) as ResultDetail;
const { result, reason } = await switcher.detail().isItOn('FF2FOR2020', []) as ResultDetail;

assertFalse(result);
assertEquals(reason, 'Forced to false');
});

it('should be valid assuming key to be false - with metadata', async function () {
Switcher.assume('FF2FOR2020').false().withMetadata({ value: 'something' });
const { result, reason, metadata } = await switcher.isItOn('FF2FOR2020', [], true) as ResultDetail;
const { result, reason, metadata } = await switcher.detail(true).isItOn('FF2FOR2020', []) as ResultDetail;

assertFalse(result);
assertEquals(reason, 'Forced to false');
Expand Down
4 changes: 2 additions & 2 deletions test/switcher-integrated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('Integrated test - Switcher:', function () {
await switcher.isItOn('FLAG_2');

// first async API call
const response = await switcher.isItOn('FLAG_2', undefined, true) as ResultDetail;
const response = await switcher.detail().isItOn('FLAG_2') as ResultDetail;
assertTrue(response.result);
});
});
Expand Down Expand Up @@ -182,7 +182,7 @@ describe('Integrated test - Switcher:', function () {
Switcher.buildContext(contextSettings);

const switcher = Switcher.factory();
const detailedResult = await switcher.isItOn('FF2FOR2030', undefined, true) as ResultDetail;
const detailedResult = await switcher.detail().isItOn('FF2FOR2030') as ResultDetail;
assertTrue(detailedResult.result);
assertEquals(detailedResult.reason, 'Success');
assertEquals(detailedResult.metadata, { user: 'user1' });
Expand Down

0 comments on commit cc1437f

Please sign in to comment.