{"id":22645509,"url":"https://github.com/akmjenkins/json-modifiable","last_synced_at":"2025-04-12T00:33:52.243Z","repository":{"id":45308479,"uuid":"405986278","full_name":"akmjenkins/json-modifiable","owner":"akmjenkins","description":"A rules engine that dynamically modifies your objects using JSON standards","archived":false,"fork":false,"pushed_at":"2022-03-07T01:16:06.000Z","size":524,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T11:18:12.530Z","etag":null,"topics":["descriptors","json","json-patch","json-pointer","json-schema","rules-engine"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/akmjenkins.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-13T13:32:17.000Z","updated_at":"2022-09-28T15:21:25.000Z","dependencies_parsed_at":"2022-09-07T12:12:05.052Z","dependency_job_id":null,"html_url":"https://github.com/akmjenkins/json-modifiable","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-modifiable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-modifiable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-modifiable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-modifiable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akmjenkins","download_url":"https://codeload.github.com/akmjenkins/json-modifiable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248501696,"owners_count":21114676,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["descriptors","json","json-patch","json-pointer","json-schema","rules-engine"],"created_at":"2024-12-09T06:06:11.643Z","updated_at":"2025-04-12T00:33:52.217Z","avatar_url":"https://github.com/akmjenkins.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# json-modifiable\n\n[![npm version](https://img.shields.io/npm/v/json-modifiable)](https://npmjs.org/package/json-modifiable)\n[![Coverage Status](https://coveralls.io/repos/github/akmjenkins/json-modifiable/badge.svg)](https://coveralls.io/github/akmjenkins/json-modifiable)\n![Build Status](https://github.com/akmjenkins/json-modifiable/actions/workflows/test.yaml/badge.svg)\n[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/json-modifiable)](https://bundlephobia.com/result?p=json-modifiable)\n\n## What is this?\n\nAn incredibly tiny and configurable rules engine for applying arbitrary modifications to a [descriptor](#descriptor) based on context. It's **highly configurable**, although you might find it easiest to write the [rules](#rules) using JSON standards -[json pointer](https://datatracker.ietf.org/doc/html/rfc6901), [json patch](http://jsonpatch.com/), and [json schema](https://json-schema.org/)\n\n## Why?\n\nSerializable logic that can be easily stored in a database and shared amongst multiple components of your application.\n\n## Features\n\n- Highly configurable - define your own JSON structures. This doc encourages your rules to be written using [json schema](https://json-schema.org/) but the [validator](#validator) allows you to write them however you choose.\n- Configurable and highly performant interpolations (using  uses [interpolatable](https://github.com/akmjenkins/interpolatable)) to make highly reusable rules\n- Extremely lightweight (under 2kb minzipped)\n- Runs everywhere JavaScript does - Deno/Node/browsers\n\n\n## Installation\n\n```bash\nnpm install json-modifiable\n## or\nyarn add json-modifiable\n```\n\nOr directly via the browser:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/json-modifiable\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const descriptor = jsonModifiable.engine(...)\n  \n  // or see JSON Engine\n  const descriptor = jsonModifiable.jsonEngine(...)\n\u003c/script\u003e\n```\n\n## Concepts\n\n- [Descriptor](#descriptor)\n- [Context](#context)\n- [Validator](#validator)\n- [Rules](#rules)\n  - [Conditions](#condition)\n  - [Operation](#operation)\n- [Resolver](#resolver)  \n- [Patch](#patch)\n- [Interpolation](#interpolation)\n\n### Descriptor\n\n```ts\ntype Descriptor = Record\u003cstring,unknown\u003e\n```\n\nA descriptor is a plain-old JavaScript object (POJO) that should be \"modified\" - contain different properties/structures - in various [contexts](#context). The modifications are defined by [rules](#rules).\n\n```js\n{\n    fieldId: 'lastName',\n    path: 'user.lastName',\n    label: 'Last Name',\n    readOnly: false,\n    placeholder: 'Enter Your First Name',\n    type: 'text',\n    hidden: true,\n    validations: [],\n}\n```\n\n### Context\n\n```ts\ntype Context = Record\u003cstring,unknown\u003e\n```\n\nContext is also a plain object. The context is used by the [validator](#validator) to evaluate the [conditions](#condition) of [rules](#rules)\n\n```js\n{\n  formData: {\n    firstName: 'Joe',\n    lastName: 'Smith'\n  }\n}\n```\n\n### Validator\n\n```ts\ntype Validator = (schema: any, subject: any) =\u003e boolean;\n```\n\nA validator is the only dependency that must be user supplied. It accepts a [condition](#condition) and an subject to evaluate and it must **synchronously** return a boolean. Because of the extensive performance optimizations going on inside the engine to keep it blazing fast **it's important to note the validator MUST BE A PURE FUNCTION**\n\nHere's a great one, and the one used in all our tests:\n\n```js\nimport { engine } from 'json-modifiable';\nimport Ajv from 'ajv';\n\nconst ajv = new Ajv();\nconst validator = ajv.validate.bind(ajv);\n\nconst modifiable = engine(myDescriptor, validator, rules);\n```\n\nYou should be able to see that by supplying a different validator, you can write rules however you want, not just using JSON Schema.\n\n## Rules\n\n```ts\nexport type Rule\u003cOperation\u003e = {\n  when: Condition[];\n  then?: Operation;\n  otherwise?: Operation;\n};\n```\n\nA rule is an object whose `when` property is an array of [conditions](#condition) and contains a `then` and/or `otherwise` clause. The [validator](#validator) will evaluate the conditions and, if any of them are true, will apply the `then` operation (if supplied) via the [patch function](#patch). If none of them are true, the `otherwise` operation (if supplied) will be applied.\n\n#### Condition\n\n```ts\ntype Condition\u003cSchema\u003e = Record\u003cstring, Schema\u003e;\n```\n\nA condition is a plain object whose keys are resolved to values (by means of the [resolver](#resolver) function) and whose values are passed to the [validator](#validator) function with the resolved values.\n\nThe default resolver maps the keys of conditions to the values of context directly:\n\n```js\n// context\n{\n  firstName: 'joe'\n}\n\n// condition\n{\n  firstName: {\n    type: 'string',\n    pattern: '^j'\n  }\n}\n```\n\nis given to the validator like this:\n\n```js\nvalidator({ type: 'string', pattern: '^j'}, 'joe');\n```\n\n#### Operation\n\nAn operation is simply the value encoded in the `then` or `otherwise` of a [rule](#rule). After the engine has run, the modified descriptor is computed by reducing all collected operations via the [patch](#patch) function in the order they were supplied in rules. They can be absolutely anything that the patch function can understand. The default patch function (literally `Object.assign`) expects the operation to be `Partial\u003cDescriptor\u003e` like this:\n\n```js\nconst descriptor = {\n  fieldName: 'lastName',\n  label: 'Last Name',\n}\n\nconst rule = {\n  when: {\n    firstName: {\n      type: 'string',\n      minLength: 1\n    }\n  },\n  then: {\n    validations: ['required']\n  }\n}\n\n// resultant descriptor when firstName is not empty\n{\n  fieldName: 'lastName',\n  label: 'Last Name',\n  validations: ['required']\n}\n```\n\n### Resolver\n\n```ts\nexport type Resolver\u003cContext\u003e = (object: Context, path: string) =\u003e any;\n```\n\nA resolver resolves the keys of [conditions](#condition) to values. It is passed the key and the context being evaluated. The resultant value can be any value that will subsequently get passed to the [validator](#validator). The default resolver simply maps the key of the condition to the key in context:\n\n```js\nconst resolver = (context, ket) =\u003e context[key]\n```\n\nBut the resolver function can be anything you choose. Some other ideas are:\n\n- [json pointer](https://datatracker.ietf.org/doc/html/rfc6901)\n\n```js\n// context\n{\n  formData: {\n    address: {\n      street: '123 Fake Street'\n    }\n  }\n}\n\n// condition\n{\n  '/formData/address/street': {\n    type: 'string'\n  }\n}\n\n// for example:\nimport { get } from 'json-pointer';\nconst resolver = get;\n```\n\n- [lodash get](https://lodash.com/docs/#get)\n\n```js\n// context\n{\n  formData: {\n    address: {\n      street: '123 Fake Street'\n    }\n  }\n}\n\n// condition\n{\n  'formData.address.street': {\n    type: 'string'\n  }\n}\n\n// for example:\nimport { get } from 'lodash';\nconst resolver = get;\n```\n\n### Patch\n\nThe patch function does the work of applying the instructions encoded in the [operations](#operations) to the descriptor to end up with the final \"modified\" descriptor for any given context. \n\n**NOTE:** The descriptor itself **should never be mutated**. `json-modifiable` leaves it up to the user to ensure the patch function is non-mutating. The default patch function is a simple shallow-clone `Object.assign`:\n\n```js\nconst patch = Object.assign\n```\n\n\n### Interpolation\n\n`json-modifiable` uses [interpolatable](https://github.com/akmjenkins/interpolatable) to offer allow interpolation of values into rules/patches. See the [docs](https://github.com/akmjenkins/interpolatable) for how it works. The resolver function passed to `json-modifiable` will be the same one passed to interpolatable. By default it's just an accessor, but you could also use a resolver that works with [json pointer](https://datatracker.ietf.org/doc/html/rfc6901):\n\nGiven the rule and the following context:\n\n```js\nconst rule = {\n  when: [\n    {\n      type: 'object',\n      properties: '{{/fields/from/context}}',\n      required: '{{/fields/required}}',\n    },\n  ];\n}\n\nconst context = {\n  fields: {\n    from: {\n      context: {\n        a: {\n          type: \"strng\"\n        },\n        b: {\n          type: \"number\"\n        }\n      }\n    }\n  },\n  required: [\"a\"]\n}\n```\n\nYou'll end up with the following interpolated rule:\n\n```js\n{\n  when: [\n    {\n      type: 'object',\n      properties: {\n        a: {\n          type: \"strng\"\n        },\n        b: {\n          type: \"number\"\n        }\n      }\n      required: [\"a\"]\n    },\n  ];\n}\n```\n\nInterpolations are very powerful and keep your rules serializable.\n\n#### About interpolation performance\n\n**TLDR** in performance critical environments where you aren't using interpolation, pass `null` for the `pattern` option:\n\n```js\nconst modifiable = engine(\n  myDescriptor, \n  rules, \n  { \n    validator,\n    pattern: null\n  }\n);\n```\n\n## Basic Usage\n\n`json-modifiable` relies on a [validator](#validator) function that evaluates the [condition](#condition) of [rules](#rules) and applies patches\n\n\n```js\nimport { engine } from 'json-modifiable';\n\nconst descriptor = engine(\n  {\n    fieldId: 'lastName',\n    path: 'user.lastName',\n    label: 'Last Name',\n    readOnly: false,\n    placeholder: 'Enter Your First Name',\n    type: 'text',\n    hidden: true,\n    validations: [],\n  },\n  validator,\n  [\n    {\n      when: [\n        {\n          'firstName': {\n            type: 'string',\n            minLength: 1\n          }\n        },\n      ],\n      then: {\n        validations: ['required']\n      }\n    },\n    // ... more rules\n  ],\n);\n\ndescriptor.get().validations.find((v) =\u003e v === 'required'); // not found\ndescriptor.setContext({ formData: { firstName: 'fred' } });\ndescriptor.get().validations.find((v) =\u003e v === 'required'); // found!\n```\n\n## What in the heck is this good for?\n\nDefinining easy to read and easy to apply business logic to things that need to behave differently in different contexts. One use case I've used this for is to quickly and easily perform complicated modifications to form field descriptors based on the state of the form (or some other current application context).\n\n```js\nconst descriptor = {\n  fieldId: 'lastName',\n  path: 'user.lastName',\n  label: 'Last Name',\n  readOnly: false,\n  placeholder: 'Enter Your First Name',\n  type: 'text',\n  hidden: true,\n  validations: [],\n};\n\nconst rules = [\n  {\n    when: [\n      {\n        '/formData/firstName': {\n          type: 'string',\n          minLength: 1,\n        },\n      },\n    ],\n    then: {\n      validations: ['required'],\n      hidden: false\n    }\n  },\n];\n```\n\n\n### JSON Engine\n\nThis library also exports a function `jsonEngine` which is a thin wrapper over the engine using [json patch](http://jsonpatch.com/) as the patch function and [json pointer](https://datatracker.ietf.org/doc/html/rfc6901) as the default resolver. You can then write modifiable rules like this:\n\n```ts\nconst myRule: JSONPatchRule\u003cSomeJSONSchema\u003e = {\n  when: [\n    {\n      '/contextPath': {\n        type: 'string',\n        const: '1',\n      },\n    },\n  ],\n  then: [\n    {\n      op: 'remove',\n      path: '/validations/0',\n    },\n    {\n      op: 'replace',\n      path: '/someNewKey',\n      value: { newThing: 'fred' },\n    },\n  ],\n  otherwise: [\n    {\n      op: 'remove',\n      path: '/validations',\n    },\n  ],\n};\n```\n\n\nThis library internally has tiny, (largely) spec compliant implementations of [json patch](http://jsonpatch.com/) and [json pointer](https://datatracker.ietf.org/doc/html/rfc6901) that it uses as the default options for [json engine](#json-engine).\n\nThe very important difference with the embedded json-patch utility is that it **only patches the parts of the descriptor that are actually modified** - i.e. no `cloneDeep`. This allows it to work beautifully with libraries that rely on (or make heavy use of) referential integrity/memoization (like React).\n\n```js\nconst DynamicFormField = ({ context }) =\u003e {\n\n  const refDescriptor = useRef(engine(descriptor, rules, { context }))\n  const [currentDescriptor, setCurrentDescriptor] = useState(descriptor.current.get());\n  const [context,setContext] = useState({})  \n\n  useEffect(() =\u003e {\n    return refDescriptor.current.subscribe(setCurrentDescriptor)\n  },[])\n\n  useEffect(() =\u003e {\n    refDescriptor.current.setContext(context);\n  },[context])\n\n  return (/* some JSX */)\n}\n```\n\nThink outside the box here, what if you didn't have rules for individual field descriptors, but what if you entire form was just modifiable descriptors and the rules governing the entire form were encoded as a bunch of JSON patch operations? Because of the referential integrity of the patches, `memo`-ed components still work and things are still lightening fast.\n\n```js\nconst myForm = {\n  firstName: {\n    label: 'First Name',\n    placeholder: 'Enter your first name',\n  },\n};\n\nconst formRules = [\n  {\n    when: {\n      '/formData/firstName': {\n        type: 'string',\n        pattern: '^A',\n      },\n    },\n    then: [\n      {\n        op: 'replace',\n        path: '/firstName/placeholder',\n        value: 'Hey {{/formData/firstName}}, my first name starts with A too!',\n      },\n    ],\n  },\n];\n```\n\n## Other Cool Stuff\n\nCheck out [json-schema-rules-engine](https://github.com/akmjenkins/json-schema-rules-engine) for a different type of rules engine.\n\n## License\n\n[MIT](./LICENSE)\n\n## Contributing\n\nPRs welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakmjenkins%2Fjson-modifiable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakmjenkins%2Fjson-modifiable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakmjenkins%2Fjson-modifiable/lists"}