Skip to content

Commit

Permalink
Merge 33c6bcf into dde00f0
Browse files Browse the repository at this point in the history
  • Loading branch information
akmjenkins committed Dec 22, 2021
2 parents dde00f0 + 33c6bcf commit f3d4615
Show file tree
Hide file tree
Showing 20 changed files with 1,074 additions and 506 deletions.
522 changes: 334 additions & 188 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module.exports = {
collectCoverageFrom: ['./src/*.js'],
coverageThreshold: {
global: {
branches: 75,
branches: 70,
functions: 95,
lines: 90,
statements: 90,
statements: 85,
},
},
};
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"name": "json-modifiable",
"version": "1.2.0",
"description": "A rules engine that dynamically modifies your objects using JSON standards",
"main": "build/index.js",
"browser": "build/bundle.min.js",
"main": "build/bundle.min.js",
"module": "build/index.js",
"types": "build/index.d.ts",
"author": "Adam Jenkins",
"sideEffects": false,
"license": "MIT",
"repository": {
"type": "git",
Expand Down Expand Up @@ -40,7 +41,6 @@
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.4",
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.172",
"@types/node": "^16.7.10",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
Expand All @@ -57,7 +57,11 @@
"eslint-plugin-prettier": "^4.0.0",
"fast-json-patch": "^3.1.0",
"jest": "^27.1.0",
"json-pointer": "^0.6.1",
"json-ptr": "^3.0.0",
"jsonpointer": "^5.0.0",
"prettier": "^2.3.2",
"property-expr": "^2.0.4",
"rimraf": "^3.0.2",
"rollup": "^2.56.3",
"rollup-plugin-bundle-size": "^1.0.3",
Expand All @@ -67,6 +71,6 @@
"typescript": "^4.4.2"
},
"dependencies": {
"interpolatable": "^1.2.0"
"interpolatable": "^1.3.2"
}
}
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default {
sourcemap: true,
file: 'build/bundle.min.js',
format: 'iife',
name: 'createJSONModifiable',
name: 'jsonModifiable',
plugins: [bundlesize(), terser()],
},
],
Expand Down
54 changes: 54 additions & 0 deletions src/engine.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Options as InterpolatableOptions } from 'interpolatable';

type Unsubscribe = () => void;
type Subscriber<T> = (arg: T) => void;

export type Validator<Schema = unknown> = (
schema: Schema,
subject: any,
) => boolean;
export type Resolver<Context = Record<string, unknown>> = (
object: Context,
path: string,
) => any;

type ErrorEvent = {
type: 'ValidationError' | 'PatchError';
err: Error;
};

export interface JSONModifiable<Descriptor, Context> {
get: () => Descriptor;
setContext: (context: Context) => void;
subscribe: (subscriber: Subscriber<Descriptor>) => Unsubscribe;
on: (event: 'modified', subscriber: Subscriber<Descriptor>) => Unsubscribe;
on: (event: 'error', subscriber: Subscriber<ErrorEvent>) => Unsubscribe;
}

export type Condition<Schema> = Record<string, Schema>;

export type Rule<Operation, Schema = unknown, Context = unknown> = {
when: Condition<Schema>[];
then?: Operation;
otherwise?: Operation;
options?: InterpolatableOptions<Context>;
};

export type Options<Descriptor, Operation, Context> = {
context?: Context;
pattern?: InterpolatableOptions<Context>['pattern'];
resolver?: InterpolatableOptions<Context>['resolver'];
patch?: (descriptor: Descriptor, operation: Operation) => Descriptor;
};

export function engine<
Descriptor = Record<string, unknown>,
Schema = unknown,
Operation = Partial<Descriptor>,
Context = unknown,
>(
descriptor: Descriptor,
validator: Validator<Schema>,
rules: Rule<Operation, Schema, Context>[],
options?: Options<Descriptor, Operation, Context>,
): JSONModifiable<Descriptor, Context>;
66 changes: 66 additions & 0 deletions src/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createStatefulRules } from './rule';
import { compareSets } from './utils';

const resolver = (context, key) => context[key];
const patch = (...args) => Object.assign({}, ...args);

export const engine = (
descriptor,
validator,
rules = [],
{ context = {}, ...opts } = {},
subscribers = new Map(),
modified,
) => {
opts = { resolver, patch, ...opts };

if (!validator) throw new Error(`A validator is required`);
if (!opts.patch) throw new Error(`A patch function is required`);
if (!opts.resolver) throw new Error(`A resolver function is required`);

rules = createStatefulRules(rules, { ...opts, validator });
modified = descriptor;

const emit = (eventType, thing) => (
subscribers.get(eventType)?.forEach((s) => s(thing)), thing
);

const on = (eventType, subscriber) => {
const set = subscribers.get(eventType) || new Set();
subscribers.set(eventType, set);
set.add(subscriber);
return () => set.delete(subscriber);
};

const evaluate = (ops) =>
ops.reduce((acc, op) => {
try {
return opts.patch(acc, op);
} catch (err) {
emit('error', { type: 'PatchError', err });
return acc;
}
}, descriptor);

const cache = new Map();
const getCached = (ops) => {
for (const [key, value] of cache) if (compareSets(key, ops)) return value;
};

const get = () => modified;

const setContext = (ctx, force) => {
if (!force && ctx === context) return;
const rulesToApply = rules((context = ctx));
const ops = new Set(rulesToApply);
const next = getCached(ops) || evaluate(rulesToApply);
modified === next || cache.set(ops, emit('modified', (modified = next)));
};

const subscribe = (s) => on('modified', s);

// run immediately
setContext(context, true);

return { on, subscribe, get, setContext };
};
54 changes: 2 additions & 52 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,2 @@
import { JSONPatchOperation } from './patch';

type Unsubscribe = () => void;
type Subscriber<T> = (arg: T) => void;

type Validator = (schema: any, subject: any) => boolean;
type Resolver = (object: Record<string, unknown>, path: string) => any;

type ErrorEvent = {
type: 'ValidationError' | 'PatchError';
err: Error;
};

interface JSONModifiable<T, C, Op> {
get: () => T;
set: (descriptor: T) => void;
setRules: (rules: Rule<Op>[]) => void;
setContext: (context: C) => void;
subscribe: (subscriber: Subscriber<T>) => Unsubscribe;
on: (event: 'modified', subscriber: Subscriber<T>) => Unsubscribe;
on: (event: 'error', subscriber: Subscriber<ErrorEvent>) => Unsubscribe;
}

type Condition = {
[key: string]: Record<string, unknown>;
};

type Operation = unknown;

type Rule<Op> = {
when: Condition[];
then?: Op[];
otherwise?: Op[];
};

type Options<T, C, Op> = {
validator: Validator;
context?: C;
pattern?: RegExp | null;
resolver?: Resolver;
patch?: (operations: Op[], record: T) => T;
};

export default function createJSONModifiable<
T,
C = Record<string, unknown>,
Op = JSONPatchOperation,
>(
descriptor: T,
rules: Rule<Op>[],
options: Options<T, C, Op>,
): JSONModifiable<T, C, Op>;
export * from './engine';
export * from './json';
74 changes: 3 additions & 71 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,4 @@
import defaults from './options';
import { createStatefulRules } from './rule';
import { compareSets } from './utils';
import { engine } from './engine';
import { jsonEngine } from './json';

export default (
descriptor,
rules = [],
{ context, ...opts } = {},
subscribers = new Map(),
modified,
) => {
opts = { ...defaults, ...opts };

if (!opts.validator) throw new Error(`A validator is required`);

rules = createStatefulRules(rules, opts);

modified = descriptor;
const cache = new Map();
const emit = (eventType, thing) => {
const set = subscribers.get(eventType);
set && set.forEach((s) => s(thing));
};

const evaluate = (ops) =>
ops.reduce((acc, ops) => {
try {
return opts.patch(acc, ops);
} catch (err) {
emit('error', { type: 'PatchError', err });
return acc;
}
}, descriptor);

const getCached = (ops) => {
for (const [key, value] of cache) if (compareSets(key, ops)) return value;
};

const notify = (next, key) => {
if (modified === next) return;
cache.set(key, next);
modified = next;
emit('modified', modified);
};

const run = () => {
const rulesToApply = rules(context, opts);
const ops = new Set(rulesToApply);
notify(getCached(ops) || evaluate(rulesToApply), ops);
};

// run immediately
run();

const on = (eventType, subscriber) => {
let m = subscribers.get(eventType);
m
? m.add(subscriber)
: subscribers.set(eventType, (m = new Set([subscriber])));

return () => subscribers.get(eventType).delete(subscriber);
};

return {
on,
subscribe: (subscriber) => on('modified', subscriber),
get: () => modified,
set: (d) => descriptor === d || run((descriptor = d), cache.clear()),
setRules: (r) => run((rules = createStatefulRules(rules, opts))),
setContext: (ctx) => run((context = ctx)),
};
};
export { engine, jsonEngine };
18 changes: 18 additions & 0 deletions src/json.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JSONPatchOperation } from './patch';
import { Validator, Rule, JSONModifiable, Options } from './engine';

export { JSONPatchOperation } from './patch';

type JSONOptions = Omit<Options, 'resolver' | 'patch'>;

export type JSONPatchRule<Schema = unknown> = Rule<
JSONPatchOperation[],
Schema
>;

export function jsonEngine<Descriptor, Schema = unknown, Context = unknown>(
descriptor: Descriptor,
validator: Validator<Schema>,
rules: JSONPatchRule[],
options?: JSONOptions<Descriptor, JSONPatchOperation, Context>,
): JSONModifiable<Descriptor, Schema, JSONPatchOperation, Context>;
6 changes: 6 additions & 0 deletions src/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { patch } from './patch';
import { get } from './pointer';
import { engine } from './engine';

export const jsonEngine = (descriptor, validator, rules, opts = {}) =>
engine(descriptor, validator, rules, { ...opts, patch, resolver: get });
6 changes: 0 additions & 6 deletions src/options.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/pointer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const replacer = (m) => (m === '~1' ? '/' : '~');
const decodePointer = (pointer) =>
tilde.test(pointer) ? pointer.replace(tilde, replacer) : pointer;

const compile = (pointer) => {
export const compile = (pointer) => {
try {
return decodePointer(pointer).split('/').slice(1);
return pointer.split('/').map(decodePointer).slice(1);
} catch {
throw new Error(`Invalid JSON Pointer ${pointer}`);
}
Expand Down

0 comments on commit f3d4615

Please sign in to comment.