Skip to content

Commit

Permalink
feat: allow listen for changes made on parameters and bag objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcachi committed Jun 12, 2023
1 parent b5a5525 commit 2efd0ca
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 53 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,20 +598,33 @@ klient.parameters.get('request.headers');


//
// Set a new parameter
// Set a custom parameter
//
klient.parameters.set('env', 'dev');


//
// Overwrite a parameter dynamically
// Overwrite an existant parameter
//
klient.parameters.set('request.headers', {
'Content-Type': 'application/json',
'Authorization': 'Test',
});


//
// Listen for changes by watching specific property
// Will execute callback when target property value has been changed
//
klient.parameters.watch(
'request.headers', // target path
(next, prev) => { // Runnable callback
// ...
},
false // Execute if a nested prop changed (deep option)
);


//
// Recursively merge existant parameters with new ones
//
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"dependencies": {
"axios": "^0.27.0",
"deep-diff": "^1.0.2",
"deepmerge": "^3.0.0",
"object-path": "^0.11.8"
},
Expand All @@ -56,6 +57,7 @@
"@klient/open-stack-cli": "latest",
"@klient/testing": "~1.1.0",
"@release-it/conventional-changelog": "~5.1.1",
"@types/deep-diff": "^1.0.2",
"@types/jest": "~27.4.1",
"@types/node": "~17.0.43",
"@types/object-path": "~0.11.1",
Expand Down
112 changes: 112 additions & 0 deletions src/__tests__/bag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Klient from '..';

test('watch', () => {
const unclonable = new Klient();
const { parameters } = new Klient({
request: {
headers: {
'Content-Type': 'application/json'
}
},
arr: [{ prop: true }, unclonable]
});

const innerParams = parameters as any;
const clonedParams = parameters.all() as any;

expect(JSON.stringify(Object.keys(clonedParams))).toBe(
JSON.stringify(['url', 'extensions', 'request', 'debug', 'arr'])
);
expect(clonedParams.arr === innerParams).toBeFalsy();
expect(clonedParams.arr[0] === innerParams.arr[0]).toBeFalsy();
expect(clonedParams.arr[1] === innerParams.arr[1]).toBeTruthy();

const urlWatchSpy = jest.fn();
const requestWatchSpy = jest.fn();

parameters.watch('url', urlWatchSpy);
// Check duplication
parameters.watch('url', urlWatchSpy);
parameters.watch('request.headers.Content-Type', requestWatchSpy);

parameters.set('url', undefined);
parameters.set('url', 'http://localhost');

expect(urlWatchSpy).toBeCalledTimes(1);
expect(urlWatchSpy).toBeCalledWith('http://localhost', undefined);

parameters.set('request.headers', {});

expect(requestWatchSpy).toBeCalledWith(undefined, 'application/json');

parameters.merge({
request: {
headers: {
'Content-Type': 'application/xml'
}
}
});

expect(requestWatchSpy).toBeCalledWith('application/xml', undefined);
});

test('unwatch', () => {
const { parameters } = new Klient();

const testWatchSpy = jest.fn();

parameters.unwatch('test', () => undefined);
parameters.watch('test', testWatchSpy);
parameters.unwatch('test', testWatchSpy);

parameters.set('test', true);

expect(testWatchSpy).not.toBeCalled();
});

test('watch:deep', () => {
const { parameters } = new Klient({
example: {
nested: {
test: true
}
}
});

const exampleDeepSpy = jest.fn();
const exampleSpy = jest.fn();
const exampleNestedSpy = jest.fn();
const exampleNestedDeepSpy = jest.fn();
const exampleNestedTestSpy = jest.fn();

parameters.watch('example', exampleSpy);
parameters.watch('example', exampleDeepSpy, true);
parameters.watch('example.nested', exampleNestedSpy);
parameters.watch('example.nested', exampleNestedDeepSpy, true);
parameters.watch('example.nested.test', exampleNestedTestSpy);

expect(parameters.watchers['example'].length).toBe(2);
expect(parameters.watchers['example.nested'].length).toBe(2);
expect(parameters.watchers['example.nested.test'].length).toBe(1);

parameters.set('example.nested.test', false);

expect(exampleSpy).not.toBeCalled();
expect(exampleNestedSpy).not.toBeCalled();

expect(exampleDeepSpy).toBeCalledWith(
{
nested: {
test: false
}
},
{
nested: {
test: true
}
}
);

expect(exampleNestedDeepSpy).toBeCalledWith({ test: false }, { test: true });
expect(exampleNestedTestSpy).toBeCalledWith(false, true);
});
2 changes: 1 addition & 1 deletion src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SourceExtensions from '../extensions';
import SourceBag from '../services/bag';
import SourceBag from '../services/bag/bag';
import SourceDispatcher from '../services/dispatcher/dispatcher';
import SourceRequestFactory from '../services/request/factory';
import SourceRequest from '../services/request/request';
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Klient from './klient';
export { AxiosError } from 'axios';

export { default as Extensions } from './extensions';
export { default as Bag } from './services/bag';
export { default as Bag } from './services/bag/bag';
export { default as Dispatcher } from './services/dispatcher/dispatcher';
export { default as RequestFactory } from './services/request/factory';
export { default as Request } from './services/request/request';
Expand Down
2 changes: 1 addition & 1 deletion src/klient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Bag from './services/bag';
import Bag from './services/bag/bag';
import Dispatcher from './services/dispatcher/dispatcher';
import RequestFactory from './services/request/factory';
import Extensions from './extensions';
Expand Down
48 changes: 0 additions & 48 deletions src/services/bag.ts

This file was deleted.

59 changes: 59 additions & 0 deletions src/services/bag/bag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as deepmerge from 'deepmerge';
import * as objectPath from 'object-path';

import { isPlainObject, isPlainArray, softClone } from '../../toolbox/object';
import { watch, unwatch, invokeWatchers, getWatchers, Watchable, WatchCallback } from './watch';

type BagItems = Record<string, unknown>;

export default class Bag implements BagItems, Watchable {
[x: string]: unknown;

constructor(items: BagItems = {}) {
Object.keys(items).forEach((key) => {
this[key] = items[key];
});
}

get watchers() {
return getWatchers(this);
}

get(path: string) {
return objectPath.get(this.all(), path);
}

all() {
return softClone(this);
}

set(path: string, value: unknown): this {
const prevState = this.all();

objectPath.set(this, path, value);

return invokeWatchers(this, this.all(), prevState);
}

merge(...items: BagItems[]): this {
const prevState = this.all();
const nextState = deepmerge.all<BagItems>([this.all(), ...items], {
arrayMerge: (_destinationArray: unknown[], sourceArray: unknown[]) => sourceArray,
isMergeableObject: (o: object) => isPlainArray(o) || isPlainObject(o)
});

Object.keys(nextState).forEach((key) => {
this[key] = nextState[key];
});

return invokeWatchers(this, nextState, prevState);
}

watch(path: string, onChange: WatchCallback, deep = false): this {
return watch(this, path, onChange, deep);
}

unwatch(path: string, onChange: WatchCallback): this {
return unwatch(this, path, onChange);
}
}

0 comments on commit 2efd0ca

Please sign in to comment.