Skip to content

Commit

Permalink
Added Client.assume:when criteria to bypass Switcher results (#80)
Browse files Browse the repository at this point in the history
* Added Client.assume:when criteria to bypass Switcher results

* chore: improved test summary
  • Loading branch information
petruki authored Nov 5, 2024
1 parent 25c672b commit 4d1dfc3
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 79 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ await switcher.remote().isItOn('FEATURE01');
```

## Built-in mock feature
You can also bypass your switcher configuration by invoking 'Client.assume'. This is perfect for your test code where you want to test both scenarios when the switcher is true and false.
You can also bypass your switcher configuration by invoking 'Client.assume'. This is perfect for your test code where you want to validate both scenarios when the switcher is true and false.

```ts
Client.assume('FEATURE01').true();
Expand All @@ -175,6 +175,12 @@ switcher.isItOn('FEATURE01'); // Now, it's going to return the result retrieved
Client.assume('FEATURE01').false().withMetadata({ message: 'Feature is disabled' }); // Include metadata to emulate Relay response
const response = await switcher.detail().isItOn('FEATURE01') as ResultDetail; // false
console.log(response.metadata.message); // Feature is disabled

Client.assume('FEATURE01').true().when(StrategiesType.VALUE, 'USER_1');
switcher.checkValue('USER_1').isItOn('FEATURE01'); // true when the value is 'USER_1'

Client.assume('FEATURE01').true().when(StrategiesType.NETWORK, ['USER_2', 'USER_3']);
switcher.checkValue('USER_1').isItOn('FEATURE01'); // false as the value is not in the list
```

**Enabling Test Mode**
Expand Down
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@switcherapi/switcher-client-deno",
"version": "2.0.3",
"version": "2.1.0",
"description": "Switcher4Deno is a Feature Flag Deno Client SDK for Switcher API",
"tasks": {
"cache-reload": "deno cache --reload --lock=deno.lock mod.ts",
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Switcher } from './src/switcher.ts';
export { Client } from './src/client.ts';
export { StrategiesType } from './src/lib/snapshot.ts';
export type { ResultDetail, SwitcherContext, SwitcherOptions } from './src/types/index.d.ts';
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sonar.projectKey=switcherapi_switcher-client-deno
sonar.projectName=switcher-client-deno
sonar.organization=switcherapi
sonar.projectVersion=2.0.3
sonar.projectVersion=2.1.0

sonar.javascript.lcov.reportPaths=coverage/report.lcov

Expand Down
25 changes: 25 additions & 0 deletions src/lib/bypasser/criteria.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { StrategiesType } from '../snapshot.ts';

export default class Criteria {
private readonly when: Map<string, string[]>;

constructor(strategy: string, input: string | string[]) {
this.when = new Map();
this.when.set(strategy, Array.isArray(input) ? input : [input]);
}

/**
* Add a new strategy/input to the criteria
*/
and(strategy: string, input: string | string[]): this {
if (Object.values(StrategiesType).filter((s) => s === strategy).length) {
this.when.set(strategy, Array.isArray(input) ? input : [input]);
}

return this;
}

getWhen(): Map<string, string[]> {
return this.when;
}
}
2 changes: 1 addition & 1 deletion src/lib/bypasser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default class Bypasser {
*/
static searchBypassed(key: string): Key | undefined {
for (const bypassed of bypassedKeys) {
if (bypassed.key === key) {
if (bypassed.getKey() === key) {
return bypassed;
}
}
Expand Down
40 changes: 34 additions & 6 deletions src/lib/bypasser/key.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Criteria from './criteria.ts';

/**
* Key record used to store key response when bypassing criteria execution
*/
export default class Key {
key: string;
result: boolean;
reaason?: string;
metadata?: object;
private readonly key: string;
private result: boolean;
private reaason?: string;
private metadata?: object;
private criteria?: Criteria;

constructor(key: string) {
this.key = key;
Expand Down Expand Up @@ -38,6 +41,14 @@ export default class Key {
return this;
}

/**
* Conditionally set result based on strategy
*/
when(strategy: string, input: string | string[]): Criteria {
this.criteria = new Criteria(strategy, input);
return this.criteria;
}

/**
* Return selected switcher name
*/
Expand All @@ -48,15 +59,32 @@ export default class Key {
/**
* Return current value
*/
getResponse(): {
getResponse(input?: string[][]): {
result: boolean;
reason: string | undefined;
metadata: object | undefined;
} {
let result = this.result;
if (this.criteria && input) {
result = this.getResultBasedOnCriteria(this.criteria, input);
}

return {
result: this.result,
result,
reason: this.reaason,
metadata: this.metadata,
};
}

private getResultBasedOnCriteria(criteria: Criteria, input: string[][]): boolean {
for (const [strategyWhen, inputWhen] of criteria.getWhen()) {
const entry = input.filter((e) => e[0] === strategyWhen);
if (entry.length && !inputWhen.includes(entry[0][1])) {
this.reaason = `Forced to ${!this.result} when: [${inputWhen}] - input: ${entry[0][1]}`;
return !this.result;
}
}

return this.result;
}
}
63 changes: 41 additions & 22 deletions src/lib/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,47 @@ import { CheckSwitcherError } from './exceptions/index.ts';
import { checkSnapshotVersion, resolveSnapshot } from './remote.ts';
import type { Snapshot } from '../types/index.d.ts';

/**
* Strategy types that can be used to validate the switcher
*/
type StrategyKeys = 'NETWORK' | 'VALUE' | 'NUMERIC' | 'TIME' | 'DATE' | 'REGEX' | 'PAYLOAD';

export const StrategiesType: Readonly<Record<StrategyKeys, string>> = {
NETWORK: 'NETWORK_VALIDATION',
VALUE: 'VALUE_VALIDATION',
NUMERIC: 'NUMERIC_VALIDATION',
TIME: 'TIME_VALIDATION',
DATE: 'DATE_VALIDATION',
REGEX: 'REGEX_VALIDATION',
PAYLOAD: 'PAYLOAD_VALIDATION',
};

/**
* Operations that can be used to validate the switcher
*/
type OperationsKeys =
| 'EQUAL'
| 'NOT_EQUAL'
| 'EXIST'
| 'NOT_EXIST'
| 'GREATER'
| 'LOWER'
| 'BETWEEN'
| 'HAS_ONE'
| 'HAS_ALL';

export const OperationsType: Readonly<Record<OperationsKeys, string>> = {
EQUAL: 'EQUAL',
NOT_EQUAL: 'NOT_EQUAL',
EXIST: 'EXIST',
NOT_EXIST: 'NOT_EXIST',
GREATER: 'GREATER',
LOWER: 'LOWER',
BETWEEN: 'BETWEEN',
HAS_ONE: 'HAS_ONE',
HAS_ALL: 'HAS_ALL',
};

export const loadDomain = (snapshotLocation: string, environment: string) => {
let dataJSON;
try {
Expand Down Expand Up @@ -81,28 +122,6 @@ export const checkSwitchersLocal = (snapshot: Snapshot, switcherKeys: string[])
}
};

export const StrategiesType = Object.freeze({
NETWORK: 'NETWORK_VALIDATION',
VALUE: 'VALUE_VALIDATION',
NUMERIC: 'NUMERIC_VALIDATION',
TIME: 'TIME_VALIDATION',
DATE: 'DATE_VALIDATION',
REGEX: 'REGEX_VALIDATION',
PAYLOAD: 'PAYLOAD_VALIDATION',
});

export const OperationsType = Object.freeze({
EQUAL: 'EQUAL',
NOT_EQUAL: 'NOT_EQUAL',
EXIST: 'EXIST',
NOT_EXIST: 'NOT_EXIST',
GREATER: 'GREATER',
LOWER: 'LOWER',
BETWEEN: 'BETWEEN',
HAS_ONE: 'HAS_ONE',
HAS_ALL: 'HAS_ALL',
});

export const processOperation = async (
strategy: string,
operation: string,
Expand Down
2 changes: 1 addition & 1 deletion src/switcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class Switcher {
// verify if query from Bypasser
const bypassKey = Bypasser.searchBypassed(this._key);
if (bypassKey) {
const response = bypassKey.getResponse();
const response = bypassKey.getResponse(util.get(this._input, []));
return this._showDetail ? response : response.result;
}

Expand Down
Loading

0 comments on commit 4d1dfc3

Please sign in to comment.