├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── demo ├── README.md ├── backwards.json ├── bundle.js ├── example-simple-issue.json ├── github-arthropod-complete.lens.yml ├── github-arthropod.lens.yml ├── github-issue.json ├── github-to-arthropod-original.json ├── index.html ├── new-github-issue.json ├── simple-issue.json └── web-components │ ├── cambria-demo.js │ ├── cambria-document.js │ ├── cambria-lens.js │ └── cambria-schema.js ├── dist ├── cli.d.ts ├── cli.js ├── cli.js.map ├── defaults.d.ts ├── defaults.js ├── defaults.js.map ├── doc.d.ts ├── doc.js ├── doc.js.map ├── helpers.d.ts ├── helpers.js ├── helpers.js.map ├── index.d.ts ├── index.js ├── index.js.map ├── json-schema.d.ts ├── json-schema.js ├── json-schema.js.map ├── lens-graph.d.ts ├── lens-graph.js ├── lens-graph.js.map ├── lens-loader.d.ts ├── lens-loader.js ├── lens-loader.js.map ├── lens-ops.d.ts ├── lens-ops.js ├── lens-ops.js.map ├── patch.d.ts ├── patch.js ├── patch.js.map ├── reverse.d.ts ├── reverse.js └── reverse.js.map ├── package.json ├── src ├── cambria-lens-schema.json ├── cli.ts ├── defaults.ts ├── doc.ts ├── helpers.ts ├── index.ts ├── json-schema.ts ├── lens-graph.ts ├── lens-loader.ts ├── lens-ops.ts ├── patch.ts └── reverse.ts ├── test ├── github-arthropod.ts ├── github-issue.json ├── json-schema.ts ├── lens-graph.ts └── patch.ts ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "globals": { 8 | "__static": true 9 | }, 10 | "parser": "@typescript-eslint/parser", 11 | "plugins": ["@typescript-eslint"], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/eslint-recommended", 16 | "prettier/@typescript-eslint", 17 | "plugin:prettier/recommended" 18 | ], 19 | "settings": { 20 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"] 21 | }, 22 | "rules": { 23 | // we agreed we don't like semicolons 24 | "semi": ["error", "never"], 25 | 26 | // we agreed class methods don't have to use "this". 27 | "class-methods-use-this": "off", 28 | 29 | // we like dangling commas 30 | "comma-dangle": "off", 31 | 32 | // we agreed we don't require default cases 33 | "default-case": "off", 34 | 35 | // doesn't really make sense when targeting electron 36 | "no-restricted-syntax": "off", 37 | 38 | // we agreed that we're okay with idiomatic short-circuits 39 | "no-unused-expressions": [ 40 | "error", 41 | { 42 | "allowShortCircuit": true 43 | } 44 | ], 45 | 46 | // we agreed this feels unnecessarily opinionated 47 | "lines-between-class-members": "off", 48 | 49 | // arguably we should not do this, but we do, 18 times 50 | "no-shadow": "off", 51 | // arguably we should not do this, but there are 70 cases where we do 52 | "no-param-reassign": "off", 53 | 54 | // third-party libs often use this 55 | "no-underscore-dangle": "off", 56 | 57 | // pushpin was inherently visual, so we've disabled quite a few accessibility rules 58 | // it would be reasonable to re-enable these but would take some work, and might be 59 | // a good idea for an app like arthropod 60 | "jsx-a11y/no-static-element-interactions": "off", 61 | "jsx-a11y/anchor-is-valid": "off", 62 | "jsx-a11y/interactive-supports-focus": "off", 63 | "jsx-a11y/no-noninteractive-tabindex": "off", 64 | "jsx-a11y/click-events-have-key-events": "off", 65 | "jsx-a11y/no-autofocus": "off", 66 | "jsx-a11y/media-has-caption": "off", // randomly sourced audio doesn't come captioned 67 | 68 | // This isn't really useful 69 | "@typescript-eslint/no-empty-interface": "off", 70 | 71 | // we might want to do this, but there are 97 cases where we don't 72 | "@typescript-eslint/explicit-member-accessibility": "off", 73 | 74 | // we might want to this, but there are 424 places we don't 75 | "@typescript-eslint/explicit-function-return-type": "off", 76 | 77 | // we agreed this rule is gratuitious 78 | "@typescript-eslint/no-use-before-define": "off", 79 | 80 | // someday, we should turn this back on, but we use it 44 times 81 | "@typescript-eslint/no-explicit-any": "off", 82 | 83 | // sometimes third-party libs are typed incorrectly 84 | "@typescript-eslint/no-non-null-assertion": "off", 85 | 86 | // we agreed unused arguments should be left in-place and not removed 87 | "@typescript-eslint/no-unused-vars": [ 88 | "error", 89 | { 90 | "args": "none", 91 | "ignoreRestSiblings": true 92 | } 93 | ], 94 | 95 | // import-specific rulings 96 | // we probably want to enable this, and it's violated 23 times 97 | "import/no-extraneous-dependencies": "off", 98 | 99 | // we probably don't like this rule, but only Content violates it, so we could have it 100 | "import/no-named-as-default-member": "off", 101 | 102 | // we agreed it's better to be consistent in how you export than follow this rule 103 | "import/prefer-default-export": "off", 104 | 105 | // tsc handles this better, and allows for multiple typed exports of the same name 106 | "import/export": "off", 107 | 108 | // we agreed we don't really care about this rule 109 | "import/no-cycle": "off" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "arrowParens": "always", 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "semi": false, 7 | "singleQuote": true, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\src\\electron.js" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Mocha All", 17 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 18 | "args": [ 19 | "--timeout", 20 | "999999", 21 | "--colors", 22 | "-r", 23 | "ts-node/register", 24 | "${workspaceFolder}/test/**.ts" 25 | ], 26 | "console": "integratedTerminal" 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Mocha Current File", 32 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 33 | "args": ["--timeout", "999999", "-r", "ts-node/register", "--colors", "${file}"], 34 | "console": "integratedTerminal", 35 | "internalConsoleOptions": "neverOpen" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "editor.rulers": [100], 5 | "eslint.enable": true, 6 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true 9 | }, 10 | "typescript.tsdk": "node_modules/typescript/lib", 11 | "debug.node.autoAttach": "on", 12 | "json.schemas": [ 13 | { 14 | "url": "./src/cambria-lens-schema.json", 15 | "fileMatch": ["*/lenses/*.json"] 16 | } 17 | ], 18 | "yaml.schemas": { 19 | "./src/cambria-lens-schema.json": ["*/lenses/*.yml", "**/*.lens.yml"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "test", 7 | "group": { 8 | "kind": "test", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: test", 13 | "detail": "mocha -r ts-node/register test/**.ts" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ink & Switch LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cambria 2 | 3 | Cambria is a Javascript/Typescript library for converting JSON data between related schemas. 4 | 5 | You specify (in YAML or JSON) a _lens_, which specifies a data transformation. Cambria lets you use this lens to convert: 6 | 7 | - a whole document, in JSON 8 | - an edit to a document, in [JSON Patch](http://jsonpatch.com/) 9 | - a schema description, in [JSON Schema](https://json-schema.org/) 10 | 11 | Lenses are bidirectional. Once you've converted a document from schema A to schema B, you can edit the document in schema B and propagate those edits _backwards through the same lens_ to schema A. 12 | 13 | **For more background on why Cambria exists and what it can do, see the [research essay](https://www.inkandswitch.com/cambria.html).** 14 | 15 | ⚠ Cambria is still immature software, and isn't yet ready for production use 16 | 17 | ## Use cases 18 | 19 | - Manage backwards compatibility in a JSON API 20 | - Manage database migrations for JSON data 21 | - Transform a JSON document into a different shape on the command line 22 | - Combine with [cambria-automerge](https://github.com/inkandswitch/cambria-automerge) to collaborate on documents across multiple versions of [local-first software](https://www.inkandswitch.com/local-first.html) 23 | 24 | ## CLI Usage 25 | 26 | Cambria includes a simple CLI tool for converting JSON from the command line. 27 | 28 | (You'll want to run `yarn build` to compile the latest code.) 29 | 30 | Covert the github issue into a an arthropod-style issue: 31 | 32 | `cat ./demo/github-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml` 33 | 34 | To get a live updating pipeline using `entr`: 35 | 36 | `echo ./demo/github-arthropod.lens.yml | entr bash -c "cat ./demo/github-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml > ./demo/simple-issue.json"` 37 | 38 | Compile back from an updated "simple issue" to a new github issue file: 39 | 40 | `cat ./demo/simple-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml -r -b ./demo/github-issue.json` 41 | 42 | Live updating pipeline backwards: 43 | 44 | `echo ./demo/simple-issue.json | entr bash -c "cat ./demo/simple-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml -r -b ./demo/github-issue.json > ./demo/new-github-issue.json"` 45 | 46 | ## API Usage 47 | 48 | Cambria is mostly intended to be used as a Typescript / Javascript library. Here's a simple example of converting an entire document. 49 | 50 | ```js 51 | // read doc from stdin if no input specified 52 | const input = readFileSync(program.input || 0, 'utf-8') 53 | const doc = JSON.parse(input) 54 | 55 | // we can (optionally) apply the contents of the changed document to a target document 56 | const targetDoc = program.base ? JSON.parse(readFileSync(program.base, 'utf-8')) : {} 57 | 58 | // now load a (yaml) lens definition 59 | const lensData = readFileSync(program.lens, 'utf-8') 60 | let lens = loadYamlLens(lensData) 61 | 62 | // should we reverse this lens? 63 | if (program.reverse) { 64 | lens = reverseLens(lens) 65 | } 66 | 67 | // finally, apply the lens to the document, with the schema, onto the target document! 68 | const newDoc = applyLensToDoc(lens, doc, program.schema, targetDoc) 69 | console.log(JSON.stringify(newDoc, null, 4)) 70 | ``` 71 | 72 | ## Install 73 | 74 | If you're using npm, run `npm install cambria`. If you're using yarn, run `yarn add cambria`. Then you can import it with `require('cambria')` as in the examples (or `import * as Cambria from 'cambria'` if using ES2015 or TypeScript). 75 | 76 | ## Tests 77 | 78 | `npm run test` 79 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # 7/16 Demo 2 | 3 | this is just a demo we ran in workshop on 7/16. 4 | 5 | ## Running the demo 6 | 7 | Run `yarn build` to compile the latest code 8 | 9 | Compile the github issue into a "simple issue": 10 | 11 | `cat ./demo/github-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml` 12 | 13 | To get a live updating pipeline using `entr`: 14 | 15 | `echo ./demo/github-arthropod.lens.yml | entr bash -c "cat ./demo/github-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml > ./demo/simple-issue.json"` 16 | 17 | Compile back from an updated "simple issue" to a new github issue file: 18 | 19 | `cat ./demo/simple-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml -r -b ./demo/github-issue.json` 20 | 21 | Live updating pipeline backwards: 22 | 23 | `echo ./demo/simple-issue.json | entr bash -c "cat ./demo/simple-issue.json | node ./dist/cli.js -l ./demo/github-arthropod.lens.yml -r -b ./demo/github-issue.json > ./demo/new-github-issue.json"` 24 | -------------------------------------------------------------------------------- /demo/backwards.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "node_id": "MDU6SXNzdWUx", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 5 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 6 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 8 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 9 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 10 | "number": 1347, 11 | "state": "closed", 12 | "title": "Found a difficult bug", 13 | "body": "I know what's going on!", 14 | "user": { 15 | "login": "octocat", 16 | "id": 1, 17 | "node_id": "MDQ6VXNlcjE=", 18 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/octocat", 21 | "html_url": "https://github.com/octocat", 22 | "followers_url": "https://api.github.com/users/octocat/followers", 23 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 27 | "organizations_url": "https://api.github.com/users/octocat/orgs", 28 | "repos_url": "https://api.github.com/users/octocat/repos", 29 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/octocat/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | { 36 | "id": 208045946, 37 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 38 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 39 | "name": "bug", 40 | "description": "Something isn't working", 41 | "color": "f29513", 42 | "default": true 43 | } 44 | ], 45 | "assignee": { 46 | "login": "octocat", 47 | "id": 1, 48 | "node_id": "MDQ6VXNlcjE=", 49 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 50 | "gravatar_id": "", 51 | "url": "https://api.github.com/users/octocat", 52 | "html_url": "https://github.com/octocat", 53 | "followers_url": "https://api.github.com/users/octocat/followers", 54 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 55 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 56 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 57 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 58 | "organizations_url": "https://api.github.com/users/octocat/orgs", 59 | "repos_url": "https://api.github.com/users/octocat/repos", 60 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 61 | "received_events_url": "https://api.github.com/users/octocat/received_events", 62 | "type": "User", 63 | "site_admin": false 64 | }, 65 | "assignees": [ 66 | { 67 | "login": "octocat", 68 | "id": 1, 69 | "node_id": "MDQ6VXNlcjE=", 70 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 71 | "gravatar_id": "", 72 | "url": "https://api.github.com/users/octocat", 73 | "html_url": "https://github.com/octocat", 74 | "followers_url": "https://api.github.com/users/octocat/followers", 75 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 76 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 77 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 78 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 79 | "organizations_url": "https://api.github.com/users/octocat/orgs", 80 | "repos_url": "https://api.github.com/users/octocat/repos", 81 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 82 | "received_events_url": "https://api.github.com/users/octocat/received_events", 83 | "type": "User", 84 | "site_admin": false 85 | } 86 | ], 87 | "milestone": { 88 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 89 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 90 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 91 | "id": 1002604, 92 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 93 | "number": 1, 94 | "state": "open", 95 | "title": "v1.0", 96 | "description": "Tracking milestone for version 1.0", 97 | "creator": { 98 | "login": "octocat", 99 | "id": 1, 100 | "node_id": "MDQ6VXNlcjE=", 101 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 102 | "gravatar_id": "", 103 | "url": "https://api.github.com/users/octocat", 104 | "html_url": "https://github.com/octocat", 105 | "followers_url": "https://api.github.com/users/octocat/followers", 106 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 107 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 108 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 109 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 110 | "organizations_url": "https://api.github.com/users/octocat/orgs", 111 | "repos_url": "https://api.github.com/users/octocat/repos", 112 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 113 | "received_events_url": "https://api.github.com/users/octocat/received_events", 114 | "type": "User", 115 | "site_admin": false 116 | }, 117 | "open_issues": 4, 118 | "closed_issues": 8, 119 | "created_at": "2011-04-10T20:09:31Z", 120 | "updated_at": "2014-03-03T18:58:10Z", 121 | "closed_at": "2013-02-12T13:22:01Z", 122 | "due_on": "2012-10-09T23:39:01Z" 123 | }, 124 | "locked": true, 125 | "active_lock_reason": "too heated", 126 | "comments": 0, 127 | "pull_request": { 128 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 129 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 130 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 131 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 132 | }, 133 | "closed_at": null, 134 | "created_at": "2011-04-22T13:33:48Z", 135 | "updated_at": "2011-04-22T13:33:48Z", 136 | "closed_by": { 137 | "login": "octocat", 138 | "id": 1, 139 | "node_id": "MDQ6VXNlcjE=", 140 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 141 | "gravatar_id": "", 142 | "url": "https://api.github.com/users/octocat", 143 | "html_url": "https://github.com/octocat", 144 | "followers_url": "https://api.github.com/users/octocat/followers", 145 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 146 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 147 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 148 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 149 | "organizations_url": "https://api.github.com/users/octocat/orgs", 150 | "repos_url": "https://api.github.com/users/octocat/repos", 151 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 152 | "received_events_url": "https://api.github.com/users/octocat/received_events", 153 | "type": "User", 154 | "site_admin": false 155 | }, 156 | "hello": "world" 157 | } 158 | -------------------------------------------------------------------------------- /demo/example-simple-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "status": "todo", 4 | "name": "Found a bug", 5 | "description": "I'm having a problem with this.", 6 | "category": "bug", 7 | "metadata": { 8 | "created_at": "2011-04-22T13:33:48Z", 9 | "updated_at": "2011-04-22T13:33:48Z" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/github-arthropod-complete.lens.yml: -------------------------------------------------------------------------------- 1 | # Convert a Github Issue into the Arthropod format 2 | schemaName: Issue 3 | 4 | lens: 5 | # remove unnecessary fields 6 | - remove: { name: milestone } 7 | - remove: { name: pull_request } 8 | - remove: { name: closed_by } 9 | - remove: { name: repository_url } 10 | - remove: { name: number } 11 | - remove: { name: assignees } 12 | - remove: { name: user } 13 | - remove: { name: url } 14 | - remove: { name: comments_url } 15 | - remove: { name: events_url } 16 | - remove: { name: html_url } 17 | - remove: { name: locked } 18 | - remove: { name: active_lock_reason } 19 | - remove: { name: comments } 20 | - remove: { name: node_id } 21 | - remove: { name: closed_at } 22 | 23 | # Some simple renames 24 | - rename: 25 | source: title 26 | destination: name 27 | - rename: 28 | source: body 29 | destination: description 30 | 31 | # Convert github's state field to our status field 32 | - rename: 33 | source: state 34 | destination: status 35 | - convert: 36 | name: status 37 | mapping: 38 | - open: todo 39 | closed: done 40 | - todo: open 41 | doing: open 42 | done: closed 43 | 44 | # pull the creator up to the top level 45 | - in: 46 | name: user 47 | lens: 48 | - rename: 49 | source: login 50 | destination: created_by 51 | - hoist: 52 | name: created_by 53 | host: user 54 | - remove: { name: user } 55 | 56 | # pull first label up to category 57 | - head: 58 | name: labels 59 | - in: 60 | name: labels 61 | lens: 62 | - rename: 63 | source: name 64 | destination: category 65 | - hoist: 66 | name: category 67 | host: labels 68 | - remove: 69 | name: labels 70 | 71 | # push created_at and updated_at inside a metadata object 72 | - add: 73 | name: metadata 74 | type: object 75 | - plunge: 76 | name: created_at 77 | host: metadata 78 | - plunge: 79 | name: updated_at 80 | host: metadata 81 | -------------------------------------------------------------------------------- /demo/github-arthropod.lens.yml: -------------------------------------------------------------------------------- 1 | # Convert a Github Issue into the Arthropod format 2 | schemaName: Issue 3 | 4 | lens: 5 | # remove unnecessary fields 6 | - remove: { name: milestone } 7 | - remove: { name: pull_request } 8 | - remove: { name: closed_by } 9 | - remove: { name: repository_url } 10 | - remove: { name: number } 11 | - remove: { name: assignees } 12 | - remove: { name: user } 13 | - remove: { name: url } 14 | - remove: { name: comments_url } 15 | - remove: { name: events_url } 16 | - remove: { name: html_url } 17 | - remove: { name: locked } 18 | - remove: { name: active_lock_reason } 19 | - remove: { name: comments } 20 | - remove: { name: node_id } 21 | - remove: { name: closed_at } 22 | 23 | - rename: 24 | source: title 25 | destination: name 26 | 27 | - head: 28 | name: labels 29 | 30 | - in: 31 | name: labels 32 | lens: 33 | - rename: 34 | source: name 35 | destination: category 36 | 37 | - hoist: 38 | host: labels 39 | name: category 40 | 41 | - remove: 42 | name: labels 43 | -------------------------------------------------------------------------------- /demo/github-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "node_id": "MDU6SXNzdWUx", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 5 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 6 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 8 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 9 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 10 | "number": 1347, 11 | "state": "open", 12 | "title": "Found a bug", 13 | "body": "I'm having a problem with this.", 14 | "user": { 15 | "login": "octocat", 16 | "id": 1, 17 | "node_id": "MDQ6VXNlcjE=", 18 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/octocat", 21 | "html_url": "https://github.com/octocat", 22 | "followers_url": "https://api.github.com/users/octocat/followers", 23 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 27 | "organizations_url": "https://api.github.com/users/octocat/orgs", 28 | "repos_url": "https://api.github.com/users/octocat/repos", 29 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/octocat/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | { 36 | "id": 208045946, 37 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 38 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 39 | "name": "bug", 40 | "description": "Something isn't working", 41 | "color": "f29513", 42 | "default": true 43 | } 44 | ], 45 | "assignees": [ 46 | { 47 | "login": "octocat", 48 | "id": 1, 49 | "node_id": "MDQ6VXNlcjE=", 50 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 51 | "gravatar_id": "", 52 | "url": "https://api.github.com/users/octocat", 53 | "html_url": "https://github.com/octocat", 54 | "followers_url": "https://api.github.com/users/octocat/followers", 55 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 56 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 57 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 58 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 59 | "organizations_url": "https://api.github.com/users/octocat/orgs", 60 | "repos_url": "https://api.github.com/users/octocat/repos", 61 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 62 | "received_events_url": "https://api.github.com/users/octocat/received_events", 63 | "type": "User", 64 | "site_admin": false 65 | } 66 | ], 67 | "milestone": { 68 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 69 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 70 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 71 | "id": 1002604, 72 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 73 | "number": 1, 74 | "state": "open", 75 | "title": "v1.0", 76 | "description": "Tracking milestone for version 1.0", 77 | "creator": { 78 | "login": "octocat", 79 | "id": 1, 80 | "node_id": "MDQ6VXNlcjE=", 81 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 82 | "gravatar_id": "", 83 | "url": "https://api.github.com/users/octocat", 84 | "html_url": "https://github.com/octocat", 85 | "followers_url": "https://api.github.com/users/octocat/followers", 86 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 87 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 88 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 89 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 90 | "organizations_url": "https://api.github.com/users/octocat/orgs", 91 | "repos_url": "https://api.github.com/users/octocat/repos", 92 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 93 | "received_events_url": "https://api.github.com/users/octocat/received_events", 94 | "type": "User", 95 | "site_admin": false 96 | }, 97 | "open_issues": 4, 98 | "closed_issues": 8, 99 | "created_at": "2011-04-10T20:09:31Z", 100 | "updated_at": "2014-03-03T18:58:10Z", 101 | "closed_at": "2013-02-12T13:22:01Z", 102 | "due_on": "2012-10-09T23:39:01Z" 103 | }, 104 | "locked": true, 105 | "active_lock_reason": "too heated", 106 | "comments": 0, 107 | "pull_request": { 108 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 109 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 110 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 111 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 112 | }, 113 | "closed_at": null, 114 | "created_at": "2011-04-22T13:33:48Z", 115 | "updated_at": "2011-04-22T13:33:48Z", 116 | "closed_by": { 117 | "login": "octocat", 118 | "id": 1, 119 | "node_id": "MDQ6VXNlcjE=", 120 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/octocat", 123 | "html_url": "https://github.com/octocat", 124 | "followers_url": "https://api.github.com/users/octocat/followers", 125 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 129 | "organizations_url": "https://api.github.com/users/octocat/orgs", 130 | "repos_url": "https://api.github.com/users/octocat/repos", 131 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/octocat/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /demo/github-to-arthropod-original.json: -------------------------------------------------------------------------------- 1 | { 2 | "lens": [ 3 | { "tag": "lensOp/renameProperty", "from": "title", "to": "name" }, 4 | { "tag": "lensOp/renameProperty", "from": "body", "to": "description" }, 5 | { "tag": "lensOp/renameProperty", "from": "state", "to": "status" }, 6 | { 7 | "tag": "lensOp/convertValue", 8 | "target": "status", 9 | "mapping": { 10 | "right": { "open": "todo", "closed": "done" }, 11 | "left": { "todo": "open", "doing": "open", "done": "closed" } 12 | } 13 | }, 14 | { 15 | "tag": "lensOp/hoistProperty", 16 | "target": "login", 17 | "host": "assignee" 18 | }, 19 | { "tag": "lensOp/removeProperty", "property": { "name": "assignee", "type": "object" } }, 20 | { "tag": "lensOp/renameProperty", "from": "login", "to": "assignee" }, 21 | { "tag": "lensOp/removeProperty", "property": { "name": "locked", "type": "object" } }, 22 | { 23 | "tag": "lensOp/removeProperty", 24 | "property": { "name": "active_lock_reason", "type": "object" } 25 | }, 26 | 27 | { "tag": "lensOp/removeProperty", "property": { "name": "milestone", "type": "object" } }, 28 | { "tag": "lensOp/removeProperty", "property": { "name": "closed_by", "type": "object" } }, 29 | { "tag": "lensOp/removeProperty", "property": { "name": "pull_request", "type": "object" } }, 30 | { "tag": "lensOp/removeProperty", "property": { "name": "node_id", "type": "string" } }, 31 | { "tag": "lensOp/removeProperty", "property": { "name": "assignees", "type": "array" } }, 32 | { "tag": "lensOp/removeProperty", "property": { "name": "user", "type": "object" } }, 33 | { "tag": "lensOp/removeProperty", "property": { "name": "repository_url", "type": "string" } }, 34 | { "tag": "lensOp/removeProperty", "property": { "name": "labels_url", "type": "string" } }, 35 | { "tag": "lensOp/removeProperty", "property": { "name": "comments_url", "type": "string" } }, 36 | { "tag": "lensOp/removeProperty", "property": { "name": "events_url", "type": "string" } }, 37 | { "tag": "lensOp/removeProperty", "property": { "name": "html_url", "type": "string" } }, 38 | { "tag": "lensOp/removeProperty", "property": { "name": "number", "type": "string" } } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cambria Demos 6 | 7 | 8 | 9 |

Cambria component demo

10 | 11 | 12 | 13 | { "name": "The Fifth Element" } 14 | 15 |
 16 | - rename: 
 17 |     source: name
 18 |     destination: title
 19 | - add:
 20 |     name: nullable
 21 |     type: [string, "null"]
 22 |       
23 | {} 24 |
25 | 26 | 27 | 28 | { "assignee": "Peter" } 29 | 30 |
 31 | - wrap: 
 32 |     name: assignee
 33 |       
34 | {} 35 |
36 | 37 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /demo/new-github-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | { 4 | "id": 208045946, 5 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 6 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 7 | "name": "feature", 8 | "description": "Something isn't working", 9 | "color": "f29513", 10 | "default": true 11 | } 12 | ], 13 | "closed_at": null, 14 | "node_id": "MDU6SXNzdWUx", 15 | "comments": 0, 16 | "active_lock_reason": "too heated", 17 | "locked": true, 18 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 19 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 20 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 21 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 22 | "user": { 23 | "login": "octocat", 24 | "id": 1, 25 | "node_id": "MDQ6VXNlcjE=", 26 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 27 | "gravatar_id": "", 28 | "url": "https://api.github.com/users/octocat", 29 | "html_url": "https://github.com/octocat", 30 | "followers_url": "https://api.github.com/users/octocat/followers", 31 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 32 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 33 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 34 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 35 | "organizations_url": "https://api.github.com/users/octocat/orgs", 36 | "repos_url": "https://api.github.com/users/octocat/repos", 37 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 38 | "received_events_url": "https://api.github.com/users/octocat/received_events", 39 | "type": "User", 40 | "site_admin": false 41 | }, 42 | "assignees": [ 43 | { 44 | "login": "octocat", 45 | "id": 1, 46 | "node_id": "MDQ6VXNlcjE=", 47 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 48 | "gravatar_id": "", 49 | "url": "https://api.github.com/users/octocat", 50 | "html_url": "https://github.com/octocat", 51 | "followers_url": "https://api.github.com/users/octocat/followers", 52 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 53 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 54 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 55 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 56 | "organizations_url": "https://api.github.com/users/octocat/orgs", 57 | "repos_url": "https://api.github.com/users/octocat/repos", 58 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 59 | "received_events_url": "https://api.github.com/users/octocat/received_events", 60 | "type": "User", 61 | "site_admin": false 62 | } 63 | ], 64 | "number": 1347, 65 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 66 | "closed_by": { 67 | "login": "octocat", 68 | "id": 1, 69 | "node_id": "MDQ6VXNlcjE=", 70 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 71 | "gravatar_id": "", 72 | "url": "https://api.github.com/users/octocat", 73 | "html_url": "https://github.com/octocat", 74 | "followers_url": "https://api.github.com/users/octocat/followers", 75 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 76 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 77 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 78 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 79 | "organizations_url": "https://api.github.com/users/octocat/orgs", 80 | "repos_url": "https://api.github.com/users/octocat/repos", 81 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 82 | "received_events_url": "https://api.github.com/users/octocat/received_events", 83 | "type": "User", 84 | "site_admin": false 85 | }, 86 | "pull_request": { 87 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 88 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 89 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 90 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 91 | }, 92 | "milestone": { 93 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 94 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 95 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 96 | "id": 1002604, 97 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 98 | "number": 1, 99 | "state": "open", 100 | "title": "v1.0", 101 | "description": "Tracking milestone for version 1.0", 102 | "creator": { 103 | "login": "octocat", 104 | "id": 1, 105 | "node_id": "MDQ6VXNlcjE=", 106 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 107 | "gravatar_id": "", 108 | "url": "https://api.github.com/users/octocat", 109 | "html_url": "https://github.com/octocat", 110 | "followers_url": "https://api.github.com/users/octocat/followers", 111 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 112 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 113 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 114 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 115 | "organizations_url": "https://api.github.com/users/octocat/orgs", 116 | "repos_url": "https://api.github.com/users/octocat/repos", 117 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 118 | "received_events_url": "https://api.github.com/users/octocat/received_events", 119 | "type": "User", 120 | "site_admin": false 121 | }, 122 | "open_issues": 4, 123 | "closed_issues": 8, 124 | "created_at": "2011-04-10T20:09:31Z", 125 | "updated_at": "2014-03-03T18:58:10Z", 126 | "closed_at": "2013-02-12T13:22:01Z", 127 | "due_on": "2012-10-09T23:39:01Z" 128 | }, 129 | "id": 1, 130 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 131 | "state": "open", 132 | "title": "Found a bug!!!", 133 | "body": "I'm having a problem with this.", 134 | "created_at": "2011-04-22T13:33:48Z", 135 | "updated_at": "2011-04-22T13:33:48Z" 136 | } 137 | -------------------------------------------------------------------------------- /demo/simple-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "state": "open", 4 | "name": "Found a bug!!!", 5 | "body": "I'm having a problem with this.", 6 | "category": "feature", 7 | "created_at": "2011-04-22T13:33:48Z", 8 | "updated_at": "2011-04-22T13:33:48Z" 9 | } 10 | -------------------------------------------------------------------------------- /demo/web-components/cambria-demo.js: -------------------------------------------------------------------------------- 1 | require('./cambria-document') 2 | require('./cambria-lens') 3 | require('./cambria-schema') 4 | 5 | class CambriaDemo extends HTMLElement { 6 | template = document.createElement('template') 7 | 8 | constructor() { 9 | super() 10 | 11 | this.template.innerHTML = ` 12 | 65 |
66 |
Left Document
67 | 68 |
Left Schema
69 | 70 |
Patch
71 |
72 |
73 |
Lens
74 |
75 | 76 |
77 |
Right Document
78 | 79 |
Right Schema
80 | 81 |
82 | 83 |
84 |
Last Patch
85 |
... no activity ...
86 |
87 | 88 |
89 |
Last Error
90 | ... no errors yet ... 91 |
` 92 | 93 | // Create a shadow root 94 | const shadow = this.attachShadow({ mode: 'open' }) 95 | 96 | const result = this.template.content.cloneNode(true) 97 | shadow.appendChild(result) 98 | 99 | this.error = shadow.querySelector('.error .content') 100 | this.patch = shadow.querySelector('.patch .content') 101 | 102 | const slots = {} 103 | shadow.querySelectorAll('slot').forEach((slot) => { 104 | slots[slot.name] = slot.assignedElements()[0] || slot.firstElementChild 105 | }) 106 | 107 | this.left = slots.left 108 | 109 | this.leftSchema = shadow.querySelector('.left .schema') 110 | this.rightSchema = shadow.querySelector('.right .schema') 111 | 112 | this.right = slots.right 113 | this.lens = slots.lens 114 | 115 | slots.lens.addEventListener('lens-changed', (e) => { 116 | // trigger a re-processing of the document 117 | this.right.clear() 118 | this.left.importDoc() 119 | }) 120 | 121 | this.left.addEventListener('doc-change', (e) => { 122 | this.leftSchema.setSchema(e.detail.schema) 123 | const { patch, schema } = this.lens.translateChange(e.detail) 124 | this.rightSchema.setSchema(schema) 125 | this.right.applyChange({ patch, schema }) 126 | console.log('doc-change', e) 127 | }) 128 | 129 | this.left.addEventListener('doc-patch', (e) => { 130 | const { patch } = this.lens.translatePatch(e.detail) 131 | this.right.applyPatch({ patch }) 132 | console.log('doc-patch', e) 133 | }) 134 | 135 | this.right.addEventListener('doc-change', (e) => { 136 | const { patch, schema } = this.lens.translateChange({ ...e.detail, reverse: true }) 137 | this.left.applyChange({ patch, schema }) 138 | console.log('doc-change from right', e) 139 | }) 140 | 141 | this.right.addEventListener('doc-patch', (e) => { 142 | const { patch } = this.lens.translatePatch({ ...e.detail, reverse: true }) 143 | this.left.applyPatch({ patch }) 144 | console.log('doc-patchfrom right', e) 145 | }) 146 | 147 | /* 148 | // ehhhhhh 149 | slots.left.addEventListener('doc-change', (e) => { 150 | this.renderSchema(this.leftSchema, e.detail.schema) 151 | slots.lens.dispatchEvent( 152 | new CustomEvent('doc-change', { detail: { ...e.detail, destination: slots.right } }) 153 | ) 154 | }) 155 | 156 | slots.right.addEventListener('doc-change', (e) => { 157 | this.renderSchema(this.rightSchema, e.detail.schema) 158 | 159 | slots.lens.dispatchEvent( 160 | new CustomEvent('doc-change', { 161 | detail: { ...e.detail, reverse: true, destination: slots.left }, 162 | }) 163 | ) 164 | }) 165 | 166 | slots.left.addEventListener('doc-patch', (e) => { 167 | slots.lens.dispatchEvent( 168 | new CustomEvent('doc-patch', { detail: { ...e.detail, destination: slots.right } }) 169 | ) 170 | }) 171 | 172 | slots.right.addEventListener('doc-patch', (e) => { 173 | slots.lens.dispatchEvent( 174 | new CustomEvent('doc-patch', { 175 | detail: { ...e.detail, reverse: true, destination: slots.left }, 176 | }) 177 | ) 178 | }) 179 | 180 | // hack 181 | Object.values(slots).forEach((slot) => 182 | slot.addEventListener('cloudina-error', (e) => { 183 | this.error.innerText = `${e.detail.topic}: ${e.detail.message}` 184 | }) 185 | ) 186 | 187 | slots.lens.addEventListener('doc-change', (e) => { 188 | debugger 189 | const { detail } = e 190 | const { destination } = e.detail 191 | 192 | destination.dispatchEvent( 193 | new CustomEvent('doc-change', { bubbles: false, detail: { ...detail, origin: 'LENS' } }) 194 | ) 195 | }) 196 | 197 | slots.lens.addEventListener('doc-patch', (e) => { 198 | debugger 199 | const { detail } = e 200 | const { patch, destination } = e.detail 201 | this.patch.innerText = JSON.stringify(patch, null, 2) 202 | 203 | if (destination === slots.left) { 204 | this.renderSchema(this.leftSchema, e.detail.schema) 205 | } else if (destination === slots.right) { 206 | this.renderSchema(this.rightSchema, e.detail.schema) 207 | } 208 | destination.dispatchEvent(new CustomEvent('doc-patch', { detail })) 209 | }) */ 210 | 211 | this.left.importDoc() 212 | } 213 | 214 | renderSchema(target, schema) { 215 | target.innerText = JSON.stringify(schema, null, 2) 216 | } 217 | } 218 | 219 | // Define the new element 220 | customElements.define('cambria-demo', CambriaDemo) 221 | -------------------------------------------------------------------------------- /demo/web-components/cambria-document.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const jsonpatch = require('fast-json-patch') 3 | const { JSONEditor } = require('@json-editor/json-editor') 4 | 5 | const Cambria = require('../../dist') 6 | 7 | class CambriaDocument extends HTMLElement { 8 | lastJSON = {} 9 | 10 | constructor() { 11 | super() 12 | 13 | const shadow = this.attachShadow({ mode: 'open' }) 14 | this.editorHost = document.createElement('div') 15 | shadow.appendChild(this.editorHost) 16 | 17 | this.addEventListener('doc-patch', (e) => this.handlePatch(e)) 18 | this.addEventListener('doc-change', (e) => this.handleChange(e)) 19 | } 20 | 21 | /** import a new JSON doc into the system 22 | * this also triggers "downstream" editors to regenerate schemas 23 | * we also run this when the lens changes to reset state 24 | */ 25 | importDoc() { 26 | const rawText = this.firstChild.wholeText 27 | const rawJSON = JSON.parse(rawText) 28 | const [schema, patch] = Cambria.importDoc(rawJSON) 29 | this.lastJSON = rawJSON 30 | this.schema = schema 31 | 32 | // This bit here is rather dubious. 33 | const initializationPatch = [{ op: 'add', path: '', value: {} }] 34 | this.dispatchEvent( 35 | new CustomEvent('doc-change', { 36 | bubbles: true, 37 | composed: true, 38 | detail: { schema, patch: initializationPatch }, 39 | }) 40 | ) 41 | 42 | this.dispatchEvent( 43 | new CustomEvent('doc-patch', { 44 | bubbles: true, 45 | composed: true, 46 | detail: { patch }, 47 | }) 48 | ) 49 | } 50 | 51 | clear() { 52 | if (!this.editor) { 53 | throw new Error("can't clear without an editor initialized") 54 | } 55 | this.editor.setValue({}) 56 | } 57 | 58 | handleEdit() { 59 | try { 60 | const validation = this.editor.validate() 61 | if (validation.valid === false && validation.errors.length > 0) { 62 | throw new Error(validation.errors[0].message) 63 | } 64 | const newJSON = this.editor.getValue() 65 | const patch = jsonpatch.compare(this.lastJSON, newJSON) 66 | this.lastJSON = newJSON 67 | 68 | if (patch.length > 0) { 69 | this.dispatchEvent( 70 | new CustomEvent('doc-patch', { 71 | bubbles: true, 72 | composed: true, 73 | detail: { patch }, 74 | }) 75 | ) 76 | } 77 | 78 | // clear the error status 79 | this.dispatchEvent( 80 | new CustomEvent('cloudina-error', { 81 | detail: { topic: 'document edit', message: '' }, 82 | }) 83 | ) 84 | } catch (e) { 85 | this.dispatchEvent( 86 | new CustomEvent('cloudina-error', { 87 | detail: { topic: 'document edit', message: e.message }, 88 | }) 89 | ) 90 | } 91 | } 92 | 93 | applyChange({ patch, schema }) { 94 | if (this.editor) { 95 | this.editor.destroy() 96 | } 97 | this.editor = new JSONEditor(this.editorHost, { schema }) 98 | this.applyPatch({ patch }) 99 | this.editor.on('change', (e) => this.handleEdit(e)) 100 | } 101 | 102 | applyPatch({ patch }) { 103 | if (!this.editor) { 104 | throw new Error('received a patch before editor initialized') 105 | } 106 | 107 | const { lastJSON } = this 108 | const newJSON = jsonpatch.applyPatch(lastJSON, patch).newDocument 109 | this.editor.setValue(newJSON) 110 | this.lastJSON = newJSON 111 | } 112 | 113 | /** receive a new schema, 114 | * make a new editor, clear the old state */ 115 | handleChange(event) { 116 | const { schema } = event.detail 117 | if (this.editor) { 118 | this.editor.destroy() 119 | } 120 | this.editor = new JSONEditor(this.editorHost, { schema }) 121 | this.editor.on('change', (e) => this.handleEdit(e)) 122 | 123 | // let handlePatch take care of filling in the data 124 | this.handlePatch(event) 125 | } 126 | 127 | handlePatch(event) { 128 | const { patch } = event.detail 129 | 130 | if (!this.editor) { 131 | throw new Error('received a patch before editor initialized') 132 | } 133 | 134 | const { lastJSON } = this 135 | const newJSON = jsonpatch.applyPatch(lastJSON, patch).newDocument 136 | this.editor.setValue(newJSON) 137 | this.lastJSON = newJSON 138 | } 139 | 140 | connectedCallback() { 141 | this.dispatchEvent(new Event('input')) 142 | } 143 | } 144 | 145 | customElements.define('cambria-document', CambriaDocument) 146 | -------------------------------------------------------------------------------- /demo/web-components/cambria-lens.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { safeLoad, safeDump } = require('js-yaml') 3 | const Cambria = require('../../dist') 4 | 5 | // Sends `lens-compiled` events when it gets a new, good lens. 6 | // Receives `doc-change` events and emits `doc-patch` ones in response. 7 | class CambriaLens extends HTMLPreElement { 8 | constructor() { 9 | super() 10 | 11 | this.addEventListener('input', (e) => this.handleInput(e.target.innerText)) 12 | this.handleInput(this.innerText) 13 | 14 | this.addEventListener('doc-patch', (e) => this.handleDocPatch(e, this.compiledLens)) 15 | this.addEventListener('doc-change', (e) => this.handleDocChange(e, this.compiledLens)) 16 | } 17 | 18 | translateChange(detail) { 19 | const { schema, patch, reverse } = detail 20 | const lens = this.compiledLens 21 | 22 | const outSchema = Cambria.updateSchema(schema, reverse ? Cambria.reverseLens(lens) : lens) 23 | 24 | this.schema = schema 25 | this.outSchema = outSchema 26 | 27 | const outPatch = Cambria.applyLensToPatch( 28 | reverse ? Cambria.reverseLens(lens) : lens, 29 | patch, 30 | this.schema 31 | ) 32 | 33 | return { schema: outSchema, patch: outPatch, reverse } 34 | } 35 | 36 | translatePatch(detail) { 37 | const { patch, reverse } = detail 38 | const lens = this.compiledLens 39 | 40 | const outPatch = Cambria.applyLensToPatch( 41 | reverse ? Cambria.reverseLens(lens) : lens, 42 | patch, 43 | reverse ? this.outSchema : this.schema 44 | ) 45 | 46 | return { patch: outPatch, reverse } 47 | } 48 | 49 | handleDocChange(event, lens) { 50 | try { 51 | const { schema, patch, reverse, destination } = event.detail 52 | 53 | const outSchema = Cambria.updateSchema(schema, reverse ? Cambria.reverseLens(lens) : lens) 54 | 55 | this.schema = schema 56 | this.outSchema = outSchema 57 | 58 | const convertedPatch = Cambria.applyLensToPatch( 59 | reverse ? Cambria.reverseLens(lens) : lens, 60 | patch, 61 | this.schema 62 | ) 63 | 64 | destination.dispatchEvent( 65 | new CustomEvent('doc-change', { 66 | bubbles: false, 67 | detail: { schema: outSchema, patch: convertedPatch, destination }, 68 | }) 69 | ) 70 | } catch (e) { 71 | this.dispatchEvent( 72 | new CustomEvent('cloudina-error', { 73 | detail: { topic: 'doc conversion', message: e.message }, 74 | }) 75 | ) 76 | } 77 | } 78 | 79 | handleDocPatch(event, lens) { 80 | try { 81 | const { patch, reverse, destination } = event.detail 82 | 83 | if (!this.schema) { 84 | throw new Error('Tried to convert a patch before receiving the schema.') 85 | } 86 | 87 | const convertedPatch = Cambria.applyLensToPatch( 88 | reverse ? Cambria.reverseLens(lens) : lens, 89 | patch, 90 | this.schema 91 | ) 92 | 93 | destination.dispatchEvent( 94 | new CustomEvent('doc-patch', { 95 | bubbles: false, 96 | detail: { patch: convertedPatch, destination }, 97 | }) 98 | ) 99 | } catch (e) { 100 | this.dispatchEvent( 101 | new CustomEvent('cloudina-error', { 102 | detail: { topic: 'doc conversion', message: e.message }, 103 | }) 104 | ) 105 | } 106 | } 107 | handleInput(value) { 108 | try { 109 | let hackYaml = safeLoad(value) 110 | if (!hackYaml.lens) { 111 | hackYaml = { lens: hackYaml } 112 | } 113 | value = safeDump(hackYaml) 114 | 115 | this.compiledLens = Cambria.loadYamlLens(value) 116 | this.dispatchEvent(new CustomEvent('lens-changed', { bubbles: true })) 117 | } catch (e) { 118 | this.dispatchEvent( 119 | new CustomEvent('cloudina-error', { 120 | detail: { topic: 'lens compilation', message: e.message }, 121 | }) 122 | ) 123 | } 124 | } 125 | } 126 | 127 | customElements.define('cambria-lens', CambriaLens, { extends: 'pre' }) 128 | -------------------------------------------------------------------------------- /demo/web-components/cambria-schema.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const JSONSchemaView = require('json-schema-view-js') 3 | 4 | class CambriaSchema extends HTMLElement { 5 | css = `.json-schema-view, 6 | json-schema-view { 7 | font-family: monospace; 8 | font-size: 0; 9 | } 10 | .json-schema-view > *, 11 | json-schema-view > * { 12 | font-size: 14px; 13 | } 14 | .json-schema-view .toggle-handle, 15 | json-schema-view .toggle-handle { 16 | cursor: pointer; 17 | margin: auto .3em; 18 | font-size: 10px; 19 | display: inline-block; 20 | transform-origin: 50% 40%; 21 | transition: transform 150ms ease-in; 22 | } 23 | .json-schema-view .toggle-handle:after, 24 | json-schema-view .toggle-handle:after { 25 | content: "▼"; 26 | } 27 | .json-schema-view .toggle-handle, 28 | json-schema-view .toggle-handle, 29 | .json-schema-view .toggle-handle:hover, 30 | json-schema-view .toggle-handle:hover { 31 | text-decoration: none; 32 | color: #333; 33 | } 34 | .json-schema-view .description, 35 | json-schema-view .description { 36 | color: gray; 37 | font-style: italic; 38 | } 39 | .json-schema-view .title, 40 | json-schema-view .title { 41 | font-weight: bold; 42 | cursor: pointer; 43 | } 44 | .json-schema-view .title, 45 | json-schema-view .title, 46 | .json-schema-view .title:hover, 47 | json-schema-view .title:hover { 48 | text-decoration: none; 49 | color: #333; 50 | } 51 | .json-schema-view .title, 52 | json-schema-view .title, 53 | .json-schema-view .brace, 54 | json-schema-view .brace, 55 | .json-schema-view .bracket, 56 | json-schema-view .bracket { 57 | color: #333; 58 | } 59 | .json-schema-view .property, 60 | json-schema-view .property { 61 | font-size: 0; 62 | display: table-row; 63 | } 64 | .json-schema-view .property > *, 65 | json-schema-view .property > * { 66 | font-size: 14px; 67 | padding: .2em; 68 | } 69 | .json-schema-view .name, 70 | json-schema-view .name { 71 | color: blue; 72 | display: table-cell; 73 | vertical-align: top; 74 | } 75 | .json-schema-view .type, 76 | json-schema-view .type { 77 | color: green; 78 | } 79 | .json-schema-view .type-any, 80 | json-schema-view .type-any { 81 | color: #3333ff; 82 | } 83 | .json-schema-view .required, 84 | json-schema-view .required { 85 | color: #F00; 86 | } 87 | .json-schema-view .format, 88 | json-schema-view .format, 89 | .json-schema-view .enums, 90 | json-schema-view .enums, 91 | .json-schema-view .pattern, 92 | json-schema-view .pattern { 93 | color: #000; 94 | } 95 | .json-schema-view .inner, 96 | json-schema-view .inner { 97 | padding-left: 18px; 98 | } 99 | .json-schema-view.collapsed .description, 100 | json-schema-view.collapsed .description { 101 | display: none; 102 | } 103 | .json-schema-view.collapsed .property, 104 | json-schema-view.collapsed .property { 105 | display: none; 106 | } 107 | .json-schema-view.collapsed .closeing.brace, 108 | json-schema-view.collapsed .closeing.brace { 109 | display: inline-block; 110 | } 111 | .json-schema-view.collapsed .toggle-handle, 112 | json-schema-view.collapsed .toggle-handle { 113 | transform: rotate(-90deg); 114 | } 115 | ` 116 | constructor() { 117 | super() 118 | this.shadow = this.attachShadow({ mode: 'open' }) 119 | this.shadow.innerHTML = `
` 120 | } 121 | 122 | setSchema(schema) { 123 | const view = new JSONSchemaView.default(schema, 1) 124 | 125 | const attach = this.shadow.querySelector('.attach') 126 | attach.innerHTML = '' 127 | attach.appendChild(view.render()) 128 | } 129 | } 130 | 131 | // Define the new element 132 | customElements.define('cambria-schema', CambriaSchema) 133 | -------------------------------------------------------------------------------- /dist/cli.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const commander_1 = require("commander"); 4 | const fs_1 = require("fs"); 5 | const reverse_1 = require("./reverse"); 6 | const doc_1 = require("./doc"); 7 | const lens_loader_1 = require("./lens-loader"); 8 | commander_1.program 9 | .description('// A CLI document conversion tool for cambria') 10 | .requiredOption('-l, --lens ', 'lens source as yaml') 11 | .option('-i, --input ', 'input document filename') 12 | .option('-s, --schema ', 'json schema for input document') 13 | .option('-b, --base ', 'base document filename') 14 | .option('-r, --reverse', 'run the lens in reverse'); 15 | commander_1.program.parse(process.argv); 16 | // read doc from stdin if no input specified 17 | const input = fs_1.readFileSync(commander_1.program.input || 0, 'utf-8'); 18 | const baseDoc = commander_1.program.base ? JSON.parse(fs_1.readFileSync(commander_1.program.base, 'utf-8')) : {}; 19 | const doc = JSON.parse(input); 20 | const lensData = fs_1.readFileSync(commander_1.program.lens, 'utf-8'); 21 | let lens = lens_loader_1.loadYamlLens(lensData); 22 | if (commander_1.program.reverse) { 23 | lens = reverse_1.reverseLens(lens); 24 | } 25 | const newDoc = doc_1.applyLensToDoc(lens, doc, commander_1.program.schema, baseDoc); 26 | console.log(JSON.stringify(newDoc, null, 4)); 27 | //# sourceMappingURL=cli.js.map -------------------------------------------------------------------------------- /dist/cli.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;AAAA,yCAAmC;AACnC,2BAAiC;AAEjC,uCAAuC;AACvC,+BAAsC;AACtC,+CAA4C;AAE5C,mBAAO;KACJ,WAAW,CAAC,+CAA+C,CAAC;KAC5D,cAAc,CAAC,uBAAuB,EAAE,qBAAqB,CAAC;KAC9D,MAAM,CAAC,wBAAwB,EAAE,yBAAyB,CAAC;KAC3D,MAAM,CAAC,uBAAuB,EAAE,gCAAgC,CAAC;KACjE,MAAM,CAAC,uBAAuB,EAAE,wBAAwB,CAAC;KACzD,MAAM,CAAC,eAAe,EAAE,yBAAyB,CAAC,CAAA;AAErD,mBAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAE3B,4CAA4C;AAC5C,MAAM,KAAK,GAAG,iBAAY,CAAC,mBAAO,CAAC,KAAK,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;AACvD,MAAM,OAAO,GAAG,mBAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,iBAAY,CAAC,mBAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACnF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAC7B,MAAM,QAAQ,GAAG,iBAAY,CAAC,mBAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAEpD,IAAI,IAAI,GAAG,0BAAY,CAAC,QAAQ,CAAC,CAAA;AAEjC,IAAI,mBAAO,CAAC,OAAO,EAAE;IACnB,IAAI,GAAG,qBAAW,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,MAAM,GAAG,oBAAc,CAAC,IAAI,EAAE,GAAG,EAAE,mBAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEjE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA"} -------------------------------------------------------------------------------- /dist/defaults.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; 2 | import { Patch } from './patch'; 3 | export declare function defaultValuesByType(type: JSONSchema7TypeName | JSONSchema7TypeName[]): JSONSchema7['default']; 4 | export declare function defaultObjectForSchema(schema: JSONSchema7): JSONSchema7; 5 | export declare function addDefaultValues(patch: Patch, schema: JSONSchema7): Patch; 6 | -------------------------------------------------------------------------------- /dist/defaults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.addDefaultValues = exports.defaultObjectForSchema = exports.defaultValuesByType = void 0; 4 | /* eslint-disable no-use-before-define */ 5 | const fast_json_patch_1 = require("fast-json-patch"); 6 | /** 7 | * behaviour: 8 | * - if we have an array of types where null is an option, that's our default 9 | * - otherwise use the first type in the array to pick a default from the table 10 | * - otherwise just use the value to lookup in the table 11 | */ 12 | const defaultValuesForType = { 13 | string: '', 14 | number: 0, 15 | boolean: false, 16 | array: [], 17 | object: {}, 18 | }; 19 | function defaultValuesByType(type) { 20 | if (Array.isArray(type)) { 21 | if (type.includes('null')) { 22 | return null; 23 | } 24 | return defaultValuesForType[type[0]]; 25 | } 26 | return defaultValuesForType[type]; 27 | } 28 | exports.defaultValuesByType = defaultValuesByType; 29 | // Return a recursively filled-in default object for a given schema 30 | function defaultObjectForSchema(schema) { 31 | // By setting the root to empty object, 32 | // we kick off a recursive process that fills in the entire thing 33 | const initializeRootPatch = [ 34 | { 35 | op: 'add', 36 | path: '', 37 | value: {}, 38 | }, 39 | ]; 40 | const defaultsPatch = addDefaultValues(initializeRootPatch, schema); 41 | return fast_json_patch_1.applyPatch({}, defaultsPatch).newDocument; 42 | } 43 | exports.defaultObjectForSchema = defaultObjectForSchema; 44 | function addDefaultValues(patch, schema) { 45 | return patch 46 | .map((op) => { 47 | const isMakeMap = (op.op === 'add' || op.op === 'replace') && 48 | op.value !== null && 49 | typeof op.value === 'object' && 50 | Object.entries(op.value).length === 0; 51 | if (!isMakeMap) 52 | return op; 53 | const objectProperties = getPropertiesForPath(schema, op.path); 54 | return [ 55 | op, 56 | // fill in default values for each property on the object 57 | ...Object.entries(objectProperties).map(([propName, propSchema]) => { 58 | if (typeof propSchema !== 'object') 59 | throw new Error(`Missing property ${propName}`); 60 | const path = `${op.path}/${propName}`; 61 | // Fill in a default iff: 62 | // 1) it's an object or array: init to empty 63 | // 2) it's another type and there's a default value set. 64 | // TODO: is this right? 65 | // Should we allow defaulting containers to non-empty? seems like no. 66 | // Should we fill in "default defaults" like empty string? 67 | // I think better to let the json schema explicitly define defaults 68 | let defaultValue; 69 | if (propSchema.type === 'object') { 70 | defaultValue = {}; 71 | } 72 | else if (propSchema.type === 'array') { 73 | defaultValue = []; 74 | } 75 | else if ('default' in propSchema) { 76 | defaultValue = propSchema.default; 77 | } 78 | else if (Array.isArray(propSchema.type) && propSchema.type.includes('null')) { 79 | defaultValue = null; 80 | } 81 | if (defaultValue !== undefined) { 82 | // todo: this is a TS hint, see if we can remove 83 | if (op.op !== 'add' && op.op !== 'replace') 84 | throw new Error(''); 85 | return addDefaultValues([Object.assign(Object.assign({}, op), { path, value: defaultValue })], schema); 86 | } 87 | return []; 88 | }), 89 | ].flat(Infinity); 90 | }) 91 | .flat(Infinity); 92 | } 93 | exports.addDefaultValues = addDefaultValues; 94 | // given a json schema and a json path to an object field somewhere in that schema, 95 | // return the json schema for the object being pointed to 96 | function getPropertiesForPath(schema, path) { 97 | const pathComponents = path.split('/').slice(1); 98 | const { properties } = pathComponents.reduce((schema, pathSegment) => { 99 | const types = Array.isArray(schema.type) ? schema.type : [schema.type]; 100 | if (types.includes('object')) { 101 | const schemaForProperty = schema.properties && schema.properties[pathSegment]; 102 | if (typeof schemaForProperty !== 'object') 103 | throw new Error('Expected object'); 104 | return schemaForProperty; 105 | } 106 | if (types.includes('array')) { 107 | // throw away the array index, just return the schema for array items 108 | if (!schema.items || typeof schema.items !== 'object') 109 | throw new Error('Expected array items to have types'); 110 | // todo: revisit this "as", was a huge pain to get this past TS 111 | return schema.items; 112 | } 113 | throw new Error('Expected object or array in schema based on JSON Pointer'); 114 | }, schema); 115 | if (properties === undefined) 116 | return {}; 117 | return properties; 118 | } 119 | //# sourceMappingURL=defaults.js.map -------------------------------------------------------------------------------- /dist/defaults.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/doc.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { Patch } from './patch'; 3 | import { LensSource } from './lens-ops'; 4 | /** 5 | * importDoc - convert any Plain Old Javascript Object into an implied JSON Schema and 6 | * a JSON Patch that sets every value in that document. 7 | * @param inputDoc a document to convert into a big JSON patch describing its full contents 8 | */ 9 | export declare function importDoc(inputDoc: any): [JSONSchema7, Patch]; 10 | /** 11 | * applyLensToDoc - converts a full document through a lens. 12 | * Under the hood, we convert your input doc into a big patch and the apply it to the targetDoc. 13 | * This allows merging data back and forth with other omitted values. 14 | * @property lensSource: the lens specification to apply to the document 15 | * @property inputDoc: the Plain Old Javascript Object to convert 16 | * @property inputSchema: (default: inferred from inputDoc) a JSON schema defining the input 17 | * @property targetDoc: (default: {}) a document to apply the contents of this document to as a patch 18 | */ 19 | export declare function applyLensToDoc(lensSource: LensSource, inputDoc: any, inputSchema?: JSONSchema7, targetDoc?: any): any; 20 | -------------------------------------------------------------------------------- /dist/doc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.applyLensToDoc = exports.importDoc = void 0; 7 | const fast_json_patch_1 = require("fast-json-patch"); 8 | const to_json_schema_1 = __importDefault(require("to-json-schema")); 9 | const defaults_1 = require("./defaults"); 10 | const patch_1 = require("./patch"); 11 | const json_schema_1 = require("./json-schema"); 12 | /** 13 | * importDoc - convert any Plain Old Javascript Object into an implied JSON Schema and 14 | * a JSON Patch that sets every value in that document. 15 | * @param inputDoc a document to convert into a big JSON patch describing its full contents 16 | */ 17 | function importDoc(inputDoc) { 18 | const options = { 19 | postProcessFnc: (type, schema, obj, defaultFnc) => (Object.assign(Object.assign({}, defaultFnc(type, schema, obj)), { type: [type, 'null'] })), 20 | objects: { 21 | postProcessFnc: (schema, obj, defaultFnc) => (Object.assign(Object.assign({}, defaultFnc(schema, obj)), { required: Object.getOwnPropertyNames(obj) })), 22 | }, 23 | }; 24 | const schema = to_json_schema_1.default(inputDoc, options); 25 | const patch = fast_json_patch_1.compare({}, inputDoc); 26 | return [schema, patch]; 27 | } 28 | exports.importDoc = importDoc; 29 | /** 30 | * applyLensToDoc - converts a full document through a lens. 31 | * Under the hood, we convert your input doc into a big patch and the apply it to the targetDoc. 32 | * This allows merging data back and forth with other omitted values. 33 | * @property lensSource: the lens specification to apply to the document 34 | * @property inputDoc: the Plain Old Javascript Object to convert 35 | * @property inputSchema: (default: inferred from inputDoc) a JSON schema defining the input 36 | * @property targetDoc: (default: {}) a document to apply the contents of this document to as a patch 37 | */ 38 | function applyLensToDoc(lensSource, 39 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 40 | inputDoc, inputSchema, 41 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 42 | targetDoc) { 43 | const [impliedSchema, patchForOriginalDoc] = importDoc(inputDoc); 44 | if (inputSchema === undefined || inputSchema === null) { 45 | inputSchema = impliedSchema; 46 | } 47 | // construct the "base" upon which we will apply the patches from doc. 48 | // We start with the default object for the output schema, 49 | // then we add in any existing fields on the target doc. 50 | // TODO: I think we need to deep merge here, can't just shallow merge? 51 | const outputSchema = json_schema_1.updateSchema(inputSchema, lensSource); 52 | const base = Object.assign(defaults_1.defaultObjectForSchema(outputSchema), targetDoc || {}); 53 | // return a doc based on the converted patch. 54 | // (start with either a specified baseDoc, or just empty doc) 55 | // convert the patch through the lens 56 | const outputPatch = patch_1.applyLensToPatch(lensSource, patchForOriginalDoc, inputSchema); 57 | return fast_json_patch_1.applyPatch(base, outputPatch).newDocument; 58 | } 59 | exports.applyLensToDoc = applyLensToDoc; 60 | //# sourceMappingURL=doc.js.map -------------------------------------------------------------------------------- /dist/doc.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"doc.js","sourceRoot":"","sources":["../src/doc.ts"],"names":[],"mappings":";;;;;;AAEA,qDAAqD;AACrD,oEAAyC;AAEzC,yCAAmD;AACnD,mCAAiD;AAEjD,+CAA4C;AAE5C;;;;GAIG;AACH,SAAgB,SAAS,CAAC,QAAa;IACrC,MAAM,OAAO,GAAG;QACd,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,CAAC,iCAC9C,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,KAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,IACpB;QACF,OAAO,EAAE;YACP,cAAc,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,CAAC,iCACxC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,KAC1B,QAAQ,EAAE,MAAM,CAAC,mBAAmB,CAAC,GAAG,CAAC,IACzC;SACH;KACF,CAAA;IAED,MAAM,MAAM,GAAG,wBAAY,CAAC,QAAQ,EAAE,OAAO,CAAgB,CAAA;IAC7D,MAAM,KAAK,GAAG,yBAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAEnC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AACxB,CAAC;AAlBD,8BAkBC;AAED;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAC5B,UAAsB;AACtB,6EAA6E;AAC7E,QAAa,EACb,WAAyB;AACzB,6EAA6E;AAC7E,SAAe;IAEf,MAAM,CAAC,aAAa,EAAE,mBAAmB,CAAC,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;IAEhE,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,KAAK,IAAI,EAAE;QACrD,WAAW,GAAG,aAAa,CAAA;KAC5B;IAED,sEAAsE;IACtE,0DAA0D;IAC1D,wDAAwD;IACxD,sEAAsE;IACtE,MAAM,YAAY,GAAG,0BAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,iCAAsB,CAAC,YAAY,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC,CAAA;IAEjF,6CAA6C;IAC7C,6DAA6D;IAC7D,qCAAqC;IACrC,MAAM,WAAW,GAAG,wBAAgB,CAAC,UAAU,EAAE,mBAAmB,EAAE,WAAW,CAAC,CAAA;IAClF,OAAO,4BAAU,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,WAAW,CAAA;AAClD,CAAC;AA1BD,wCA0BC"} -------------------------------------------------------------------------------- /dist/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7TypeName } from 'json-schema'; 2 | import { LensSource, LensMap, LensIn, Property, AddProperty, RemoveProperty, RenameProperty, HoistProperty, PlungeProperty, WrapProperty, HeadProperty, ValueMapping, ConvertValue } from './lens-ops'; 3 | export declare function addProperty(property: Property): AddProperty; 4 | export declare function removeProperty(property: Property): RemoveProperty; 5 | export declare function renameProperty(source: string, destination: string): RenameProperty; 6 | export declare function hoistProperty(host: string, name: string): HoistProperty; 7 | export declare function plungeProperty(host: string, name: string): PlungeProperty; 8 | export declare function wrapProperty(name: string): WrapProperty; 9 | export declare function headProperty(name: string): HeadProperty; 10 | export declare function inside(name: string, lens: LensSource): LensIn; 11 | export declare function map(lens: LensSource): LensMap; 12 | export declare function convertValue(name: string, mapping: ValueMapping, sourceType?: JSONSchema7TypeName, destinationType?: JSONSchema7TypeName): ConvertValue; 13 | -------------------------------------------------------------------------------- /dist/helpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // helper functions for nicer syntax 3 | // (we might write our own parser later, but at least for now 4 | // this avoids seeing the raw json...) 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.convertValue = exports.map = exports.inside = exports.headProperty = exports.wrapProperty = exports.plungeProperty = exports.hoistProperty = exports.renameProperty = exports.removeProperty = exports.addProperty = void 0; 7 | function addProperty(property) { 8 | return Object.assign({ op: 'add' }, property); 9 | } 10 | exports.addProperty = addProperty; 11 | function removeProperty(property) { 12 | return Object.assign({ op: 'remove' }, property); 13 | } 14 | exports.removeProperty = removeProperty; 15 | function renameProperty(source, destination) { 16 | return { 17 | op: 'rename', 18 | source, 19 | destination, 20 | }; 21 | } 22 | exports.renameProperty = renameProperty; 23 | function hoistProperty(host, name) { 24 | return { 25 | op: 'hoist', 26 | host, 27 | name, 28 | }; 29 | } 30 | exports.hoistProperty = hoistProperty; 31 | function plungeProperty(host, name) { 32 | return { 33 | op: 'plunge', 34 | host, 35 | name, 36 | }; 37 | } 38 | exports.plungeProperty = plungeProperty; 39 | function wrapProperty(name) { 40 | return { 41 | op: 'wrap', 42 | name, 43 | }; 44 | } 45 | exports.wrapProperty = wrapProperty; 46 | function headProperty(name) { 47 | return { 48 | op: 'head', 49 | name, 50 | }; 51 | } 52 | exports.headProperty = headProperty; 53 | function inside(name, lens) { 54 | return { 55 | op: 'in', 56 | name, 57 | lens, 58 | }; 59 | } 60 | exports.inside = inside; 61 | function map(lens) { 62 | return { 63 | op: 'map', 64 | lens, 65 | }; 66 | } 67 | exports.map = map; 68 | function convertValue(name, mapping, sourceType, destinationType) { 69 | return { 70 | op: 'convert', 71 | name, 72 | mapping, 73 | sourceType, 74 | destinationType, 75 | }; 76 | } 77 | exports.convertValue = convertValue; 78 | //# sourceMappingURL=helpers.js.map -------------------------------------------------------------------------------- /dist/helpers.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":";AAAA,oCAAoC;AACpC,6DAA6D;AAC7D,sCAAsC;;;AAmBtC,SAAgB,WAAW,CAAC,QAAkB;IAC5C,uBACE,EAAE,EAAE,KAAK,IACN,QAAQ,EACZ;AACH,CAAC;AALD,kCAKC;AAED,SAAgB,cAAc,CAAC,QAAkB;IAC/C,uBACE,EAAE,EAAE,QAAQ,IACT,QAAQ,EACZ;AACH,CAAC;AALD,wCAKC;AAED,SAAgB,cAAc,CAAC,MAAc,EAAE,WAAmB;IAChE,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,MAAM;QACN,WAAW;KACZ,CAAA;AACH,CAAC;AAND,wCAMC;AAED,SAAgB,aAAa,CAAC,IAAY,EAAE,IAAY;IACtD,OAAO;QACL,EAAE,EAAE,OAAO;QACX,IAAI;QACJ,IAAI;KACL,CAAA;AACH,CAAC;AAND,sCAMC;AAED,SAAgB,cAAc,CAAC,IAAY,EAAE,IAAY;IACvD,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,IAAI;QACJ,IAAI;KACL,CAAA;AACH,CAAC;AAND,wCAMC;AAED,SAAgB,YAAY,CAAC,IAAY;IACvC,OAAO;QACL,EAAE,EAAE,MAAM;QACV,IAAI;KACL,CAAA;AACH,CAAC;AALD,oCAKC;AAED,SAAgB,YAAY,CAAC,IAAY;IACvC,OAAO;QACL,EAAE,EAAE,MAAM;QACV,IAAI;KACL,CAAA;AACH,CAAC;AALD,oCAKC;AAED,SAAgB,MAAM,CAAC,IAAY,EAAE,IAAgB;IACnD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,IAAI;QACJ,IAAI;KACL,CAAA;AACH,CAAC;AAND,wBAMC;AAED,SAAgB,GAAG,CAAC,IAAgB;IAClC,OAAO;QACL,EAAE,EAAE,KAAK;QACT,IAAI;KACL,CAAA;AACH,CAAC;AALD,kBAKC;AAED,SAAgB,YAAY,CAC1B,IAAY,EACZ,OAAqB,EACrB,UAAgC,EAChC,eAAqC;IAErC,OAAO;QACL,EAAE,EAAE,SAAS;QACb,IAAI;QACJ,OAAO;QACP,UAAU;QACV,eAAe;KAChB,CAAA;AACH,CAAC;AAbD,oCAaC"} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { updateSchema, schemaForLens } from './json-schema'; 2 | export { compile, applyLensToPatch, Patch, CompiledLens } from './patch'; 3 | export { applyLensToDoc, importDoc } from './doc'; 4 | export { LensSource, LensOp, Property } from './lens-ops'; 5 | export { defaultObjectForSchema } from './defaults'; 6 | export { reverseLens } from './reverse'; 7 | export { LensGraph, initLensGraph, registerLens, lensGraphSchema, lensFromTo } from './lens-graph'; 8 | export { addProperty, removeProperty, renameProperty, hoistProperty, plungeProperty, wrapProperty, headProperty, inside, map, convertValue, } from './helpers'; 9 | export { loadYamlLens } from './lens-loader'; 10 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // TODO: The exported surface is fairly large right now, 3 | // See how much we can narrow this. 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | var json_schema_1 = require("./json-schema"); 6 | Object.defineProperty(exports, "updateSchema", { enumerable: true, get: function () { return json_schema_1.updateSchema; } }); 7 | Object.defineProperty(exports, "schemaForLens", { enumerable: true, get: function () { return json_schema_1.schemaForLens; } }); 8 | var patch_1 = require("./patch"); 9 | Object.defineProperty(exports, "compile", { enumerable: true, get: function () { return patch_1.compile; } }); 10 | Object.defineProperty(exports, "applyLensToPatch", { enumerable: true, get: function () { return patch_1.applyLensToPatch; } }); 11 | var doc_1 = require("./doc"); 12 | Object.defineProperty(exports, "applyLensToDoc", { enumerable: true, get: function () { return doc_1.applyLensToDoc; } }); 13 | Object.defineProperty(exports, "importDoc", { enumerable: true, get: function () { return doc_1.importDoc; } }); 14 | var defaults_1 = require("./defaults"); 15 | Object.defineProperty(exports, "defaultObjectForSchema", { enumerable: true, get: function () { return defaults_1.defaultObjectForSchema; } }); 16 | var reverse_1 = require("./reverse"); 17 | Object.defineProperty(exports, "reverseLens", { enumerable: true, get: function () { return reverse_1.reverseLens; } }); 18 | var lens_graph_1 = require("./lens-graph"); 19 | Object.defineProperty(exports, "initLensGraph", { enumerable: true, get: function () { return lens_graph_1.initLensGraph; } }); 20 | Object.defineProperty(exports, "registerLens", { enumerable: true, get: function () { return lens_graph_1.registerLens; } }); 21 | Object.defineProperty(exports, "lensGraphSchema", { enumerable: true, get: function () { return lens_graph_1.lensGraphSchema; } }); 22 | Object.defineProperty(exports, "lensFromTo", { enumerable: true, get: function () { return lens_graph_1.lensFromTo; } }); 23 | var helpers_1 = require("./helpers"); 24 | Object.defineProperty(exports, "addProperty", { enumerable: true, get: function () { return helpers_1.addProperty; } }); 25 | Object.defineProperty(exports, "removeProperty", { enumerable: true, get: function () { return helpers_1.removeProperty; } }); 26 | Object.defineProperty(exports, "renameProperty", { enumerable: true, get: function () { return helpers_1.renameProperty; } }); 27 | Object.defineProperty(exports, "hoistProperty", { enumerable: true, get: function () { return helpers_1.hoistProperty; } }); 28 | Object.defineProperty(exports, "plungeProperty", { enumerable: true, get: function () { return helpers_1.plungeProperty; } }); 29 | Object.defineProperty(exports, "wrapProperty", { enumerable: true, get: function () { return helpers_1.wrapProperty; } }); 30 | Object.defineProperty(exports, "headProperty", { enumerable: true, get: function () { return helpers_1.headProperty; } }); 31 | Object.defineProperty(exports, "inside", { enumerable: true, get: function () { return helpers_1.inside; } }); 32 | Object.defineProperty(exports, "map", { enumerable: true, get: function () { return helpers_1.map; } }); 33 | Object.defineProperty(exports, "convertValue", { enumerable: true, get: function () { return helpers_1.convertValue; } }); 34 | var lens_loader_1 = require("./lens-loader"); 35 | Object.defineProperty(exports, "loadYamlLens", { enumerable: true, get: function () { return lens_loader_1.loadYamlLens; } }); 36 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,wDAAwD;AACxD,mCAAmC;;AAEnC,6CAA2D;AAAlD,2GAAA,YAAY,OAAA;AAAE,4GAAA,aAAa,OAAA;AACpC,iCAAwE;AAA/D,gGAAA,OAAO,OAAA;AAAE,yGAAA,gBAAgB,OAAA;AAClC,6BAAiD;AAAxC,qGAAA,cAAc,OAAA;AAAE,gGAAA,SAAS,OAAA;AAElC,uCAAmD;AAA1C,kHAAA,sBAAsB,OAAA;AAC/B,qCAAuC;AAA9B,sGAAA,WAAW,OAAA;AACpB,2CAAkG;AAA9E,2GAAA,aAAa,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,6GAAA,eAAe,OAAA;AAAE,wGAAA,UAAU,OAAA;AAE5E,qCAWkB;AAVhB,sGAAA,WAAW,OAAA;AACX,yGAAA,cAAc,OAAA;AACd,yGAAA,cAAc,OAAA;AACd,wGAAA,aAAa,OAAA;AACb,yGAAA,cAAc,OAAA;AACd,uGAAA,YAAY,OAAA;AACZ,uGAAA,YAAY,OAAA;AACZ,iGAAA,MAAM,OAAA;AACN,8FAAA,GAAG,OAAA;AACH,uGAAA,YAAY,OAAA;AAGd,6CAA4C;AAAnC,2GAAA,YAAY,OAAA"} -------------------------------------------------------------------------------- /dist/json-schema.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { LensSource } from './lens-ops'; 3 | export declare const emptySchema: { 4 | $schema: string; 5 | type: "object"; 6 | additionalProperties: boolean; 7 | }; 8 | export declare function updateSchema(schema: JSONSchema7, lens: LensSource): JSONSchema7; 9 | export declare function schemaForLens(lens: LensSource): JSONSchema7; 10 | -------------------------------------------------------------------------------- /dist/json-schema.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"json-schema.js","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/lens-graph.d.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from 'graphlib'; 2 | import { LensSource } from '.'; 3 | import { JSONSchema7 } from 'json-schema'; 4 | export interface LensGraph { 5 | graph: Graph; 6 | } 7 | export declare function initLensGraph(): LensGraph; 8 | export declare function registerLens({ graph }: LensGraph, from: string, to: string, lenses: LensSource): LensGraph; 9 | export declare function lensGraphSchemas({ graph }: LensGraph): string[]; 10 | export declare function lensGraphSchema({ graph }: LensGraph, schema: string): JSONSchema7; 11 | export declare function lensFromTo({ graph }: LensGraph, from: string, to: string): LensSource; 12 | -------------------------------------------------------------------------------- /dist/lens-graph.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.lensFromTo = exports.lensGraphSchema = exports.lensGraphSchemas = exports.registerLens = exports.initLensGraph = void 0; 4 | const graphlib_1 = require("graphlib"); 5 | const _1 = require("."); 6 | const json_schema_1 = require("./json-schema"); 7 | function initLensGraph() { 8 | const lensGraph = { graph: new graphlib_1.Graph() }; 9 | lensGraph.graph.setNode('mu', json_schema_1.emptySchema); 10 | return lensGraph; 11 | } 12 | exports.initLensGraph = initLensGraph; 13 | // Add a new lens to the schema graph. 14 | // If the "to" schema doesn't exist yet, registers the schema too. 15 | // Returns a copy of the graph with the new contents. 16 | function registerLens({ graph }, from, to, lenses) { 17 | // clone the graph to ensure this is a pure function 18 | graph = graphlib_1.json.read(graphlib_1.json.write(graph)); // (these are graphlib's jsons) 19 | if (!graph.node(from)) { 20 | throw new RangeError(`unknown schema ${from}`); 21 | } 22 | const existingLens = graph.edge({ v: from, w: to }); 23 | if (existingLens) { 24 | // we could assert this? assert.deepEqual(existingLens, lenses) 25 | // we've already registered a lens on this edge, hope it's the same one! 26 | // todo: maybe warn here? seems dangerous to silently return... 27 | return { graph }; 28 | } 29 | if (!graph.node(to)) { 30 | graph.setNode(to, _1.updateSchema(graph.node(from), lenses)); 31 | } 32 | graph.setEdge(from, to, lenses); 33 | graph.setEdge(to, from, _1.reverseLens(lenses)); 34 | return { graph }; 35 | } 36 | exports.registerLens = registerLens; 37 | function lensGraphSchemas({ graph }) { 38 | return graph.nodes(); 39 | } 40 | exports.lensGraphSchemas = lensGraphSchemas; 41 | function lensGraphSchema({ graph }, schema) { 42 | return graph.node(schema); 43 | } 44 | exports.lensGraphSchema = lensGraphSchema; 45 | function lensFromTo({ graph }, from, to) { 46 | if (!graph.hasNode(from)) { 47 | throw new Error(`couldn't find schema in graph: ${from}`); 48 | } 49 | if (!graph.hasNode(to)) { 50 | throw new Error(`couldn't find schema in graph: ${to}`); 51 | } 52 | const migrationPaths = graphlib_1.alg.dijkstra(graph, to); 53 | const lenses = []; 54 | if (migrationPaths[from].distance == Infinity) { 55 | throw new Error(`no path found from ${from} to ${to}`); 56 | } 57 | if (migrationPaths[from].distance == 0) { 58 | return []; 59 | } 60 | for (let v = from; v != to; v = migrationPaths[v].predecessor) { 61 | const w = migrationPaths[v].predecessor; 62 | const edge = graph.edge({ v, w }); 63 | lenses.push(...edge); 64 | } 65 | return lenses; 66 | } 67 | exports.lensFromTo = lensFromTo; 68 | //# sourceMappingURL=lens-graph.js.map -------------------------------------------------------------------------------- /dist/lens-graph.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"lens-graph.js","sourceRoot":"","sources":["../src/lens-graph.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/lens-loader.d.ts: -------------------------------------------------------------------------------- 1 | import { LensSource } from './lens-ops'; 2 | interface YAMLLens { 3 | lens: LensSource; 4 | } 5 | export declare function loadLens(rawLens: YAMLLens): LensSource; 6 | export declare function loadYamlLens(lensData: string): LensSource; 7 | export {}; 8 | -------------------------------------------------------------------------------- /dist/lens-loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.loadYamlLens = exports.loadLens = void 0; 7 | const js_yaml_1 = __importDefault(require("js-yaml")); 8 | const foldInOp = (lensOpJson) => { 9 | const opName = Object.keys(lensOpJson)[0]; 10 | // the json format is 11 | // {"": {opArgs}} 12 | // and the internal format is 13 | // {op: , ...opArgs} 14 | const data = lensOpJson[opName]; 15 | if (['in', 'map'].includes(opName)) { 16 | data.lens = data.lens.map((lensOp) => foldInOp(lensOp)); 17 | } 18 | const op = Object.assign({ op: opName }, data); 19 | return op; 20 | }; 21 | function loadLens(rawLens) { 22 | return rawLens.lens 23 | .filter((o) => o !== null) 24 | .map((lensOpJson) => foldInOp(lensOpJson)); 25 | } 26 | exports.loadLens = loadLens; 27 | function loadYamlLens(lensData) { 28 | const rawLens = js_yaml_1.default.safeLoad(lensData); 29 | if (!rawLens || typeof rawLens !== 'object') 30 | throw new Error('Error loading lens'); 31 | if (!('lens' in rawLens)) 32 | throw new Error(`Expected top-level key 'lens' in YAML lens file`); 33 | // we could have a root op to make this consistent... 34 | return loadLens(rawLens); 35 | } 36 | exports.loadYamlLens = loadYamlLens; 37 | //# sourceMappingURL=lens-loader.js.map -------------------------------------------------------------------------------- /dist/lens-loader.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"lens-loader.js","sourceRoot":"","sources":["../src/lens-loader.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA0B;AAO1B,MAAM,QAAQ,GAAG,CAAC,UAAU,EAAU,EAAE;IACtC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAEzC,qBAAqB;IACrB,yBAAyB;IACzB,6BAA6B;IAC7B,4BAA4B;IAC5B,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;KACxD;IAED,MAAM,EAAE,mBAAK,EAAE,EAAE,MAAM,IAAK,IAAI,CAAE,CAAA;IAClC,OAAO,EAAE,CAAA;AACX,CAAC,CAAA;AAED,SAAgB,QAAQ,CAAC,OAAiB;IACxC,OAAQ,OAAO,CAAC,IAAmB;SAChC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;SACzB,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAA;AAC9C,CAAC;AAJD,4BAIC;AAED,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,OAAO,GAAG,iBAAI,CAAC,QAAQ,CAAC,QAAQ,CAAa,CAAA;IACnD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAA;IAClF,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IAE5F,qDAAqD;IACrD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAPD,oCAOC"} -------------------------------------------------------------------------------- /dist/lens-ops.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7TypeName } from 'json-schema'; 2 | export interface Property { 3 | name?: string; 4 | type: JSONSchema7TypeName | JSONSchema7TypeName[]; 5 | default?: any; 6 | required?: boolean; 7 | items?: Property; 8 | } 9 | export interface AddProperty extends Property { 10 | op: 'add'; 11 | } 12 | export interface RemoveProperty extends Property { 13 | op: 'remove'; 14 | } 15 | export interface RenameProperty { 16 | op: 'rename'; 17 | source: string; 18 | destination: string; 19 | } 20 | export interface HoistProperty { 21 | op: 'hoist'; 22 | name: string; 23 | host: string; 24 | } 25 | export interface PlungeProperty { 26 | op: 'plunge'; 27 | name: string; 28 | host: string; 29 | } 30 | export interface WrapProperty { 31 | op: 'wrap'; 32 | name: string; 33 | } 34 | export interface HeadProperty { 35 | op: 'head'; 36 | name: string; 37 | } 38 | export interface LensIn { 39 | op: 'in'; 40 | name: string; 41 | lens: LensSource; 42 | } 43 | export interface LensMap { 44 | op: 'map'; 45 | lens: LensSource; 46 | } 47 | export declare type ValueMapping = { 48 | [key: string]: any; 49 | }[]; 50 | export interface ConvertValue { 51 | op: 'convert'; 52 | name: string; 53 | mapping: ValueMapping; 54 | sourceType?: JSONSchema7TypeName; 55 | destinationType?: JSONSchema7TypeName; 56 | } 57 | export declare type LensOp = AddProperty | RemoveProperty | RenameProperty | HoistProperty | WrapProperty | HeadProperty | PlungeProperty | LensIn | LensMap | ConvertValue; 58 | export declare type LensSource = LensOp[]; 59 | -------------------------------------------------------------------------------- /dist/lens-ops.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=lens-ops.js.map -------------------------------------------------------------------------------- /dist/lens-ops.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"lens-ops.js","sourceRoot":"","sources":["../src/lens-ops.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/patch.d.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from 'fast-json-patch'; 2 | import { JSONSchema7 } from 'json-schema'; 3 | import { LensSource } from './lens-ops'; 4 | export declare type PatchOp = Operation; 5 | declare type MaybePatchOp = PatchOp | null; 6 | export declare type Patch = Operation[]; 7 | export declare type CompiledLens = (patch: Patch, targetDoc: any) => Patch; 8 | export declare function compile(lensSource: LensSource): { 9 | right: CompiledLens; 10 | left: CompiledLens; 11 | }; 12 | export declare function applyLensToPatch(lensSource: LensSource, patch: Patch, patchSchema: JSONSchema7): Patch; 13 | export declare function applyLensToPatchOp(lensSource: LensSource, patchOp: MaybePatchOp): MaybePatchOp; 14 | export declare function expandPatch(patchOp: PatchOp): PatchOp[]; 15 | export {}; 16 | -------------------------------------------------------------------------------- /dist/patch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.expandPatch = exports.applyLensToPatchOp = exports.applyLensToPatch = exports.compile = void 0; 4 | const reverse_1 = require("./reverse"); 5 | const defaults_1 = require("./defaults"); 6 | const json_schema_1 = require("./json-schema"); 7 | function assertNever(x) { 8 | throw new Error(`Unexpected object: ${x}`); 9 | } 10 | function noNulls(items) { 11 | return items.filter((x) => x !== null); 12 | } 13 | // Provide curried functions that incorporate the lenses internally; 14 | // this is useful for exposing a pre-baked converter function to developers 15 | // without them needing to access the lens themselves 16 | // TODO: the public interface could just be runLens and reverseLens 17 | // ... maybe also composeLens? 18 | function compile(lensSource) { 19 | return { 20 | right: (patch, targetDoc) => applyLensToPatch(lensSource, patch, targetDoc), 21 | left: (patch, targetDoc) => applyLensToPatch(reverse_1.reverseLens(lensSource), patch, targetDoc), 22 | }; 23 | } 24 | exports.compile = compile; 25 | // given a patch, returns a new patch that has had the lens applied to it. 26 | function applyLensToPatch(lensSource, patch, patchSchema // the json schema for the doc the patch was operating on 27 | ) { 28 | // expand patches that set nested objects into scalar patches 29 | const expandedPatch = patch.map((op) => expandPatch(op)).flat(); 30 | // send everything through the lens 31 | const lensedPatch = noNulls(expandedPatch.map((patchOp) => applyLensToPatchOp(lensSource, patchOp))); 32 | // add in default values needed (based on the new schema after lensing) 33 | const readerSchema = json_schema_1.updateSchema(patchSchema, lensSource); 34 | const lensedPatchWithDefaults = defaults_1.addDefaultValues(lensedPatch, readerSchema); 35 | return lensedPatchWithDefaults; 36 | } 37 | exports.applyLensToPatch = applyLensToPatch; 38 | // todo: remove destinationDoc entirely 39 | function applyLensToPatchOp(lensSource, patchOp) { 40 | return lensSource.reduce((prevPatch, lensOp) => { 41 | return runLensOp(lensOp, prevPatch); 42 | }, patchOp); 43 | } 44 | exports.applyLensToPatchOp = applyLensToPatchOp; 45 | function runLensOp(lensOp, patchOp) { 46 | if (patchOp === null) { 47 | return null; 48 | } 49 | switch (lensOp.op) { 50 | case 'rename': 51 | if ( 52 | // TODO: what about other JSON patch op types? 53 | // (consider other parts of JSON patch: move / copy / test / remove ?) 54 | (patchOp.op === 'replace' || patchOp.op === 'add') && 55 | patchOp.path.split('/')[1] === lensOp.source) { 56 | const path = patchOp.path.replace(lensOp.source, lensOp.destination); 57 | return Object.assign(Object.assign({}, patchOp), { path }); 58 | } 59 | break; 60 | case 'hoist': { 61 | // leading slash needs trimming 62 | const pathElements = patchOp.path.substr(1).split('/'); 63 | const [possibleSource, possibleDestination, ...rest] = pathElements; 64 | if (possibleSource === lensOp.host && possibleDestination === lensOp.name) { 65 | const path = ['', lensOp.name, ...rest].join('/'); 66 | return Object.assign(Object.assign({}, patchOp), { path }); 67 | } 68 | break; 69 | } 70 | case 'plunge': { 71 | const pathElements = patchOp.path.substr(1).split('/'); 72 | const [head] = pathElements; 73 | if (head === lensOp.name) { 74 | const path = ['', lensOp.host, pathElements].join('/'); 75 | return Object.assign(Object.assign({}, patchOp), { path }); 76 | } 77 | break; 78 | } 79 | case 'wrap': { 80 | const pathComponent = new RegExp(`^/(${lensOp.name})(.*)`); 81 | const match = patchOp.path.match(pathComponent); 82 | if (match) { 83 | const path = `/${match[1]}/0${match[2]}`; 84 | if ((patchOp.op === 'add' || patchOp.op === 'replace') && 85 | patchOp.value === null && 86 | match[2] === '') { 87 | return { op: 'remove', path }; 88 | } 89 | return Object.assign(Object.assign({}, patchOp), { path }); 90 | } 91 | break; 92 | } 93 | case 'head': { 94 | // break early if we're not handling a write to the array handled by this lens 95 | const arrayMatch = patchOp.path.split('/')[1] === lensOp.name; 96 | if (!arrayMatch) 97 | break; 98 | // We only care about writes to the head element, nothing else matters 99 | const headMatch = patchOp.path.match(new RegExp(`^/${lensOp.name}/0(.*)`)); 100 | if (!headMatch) 101 | return null; 102 | if (patchOp.op === 'add' || patchOp.op === 'replace') { 103 | // If the write is to the first array element, write to the scalar 104 | return { 105 | op: patchOp.op, 106 | path: `/${lensOp.name}${headMatch[1] || ''}`, 107 | value: patchOp.value, 108 | }; 109 | } 110 | if (patchOp.op === 'remove') { 111 | if (headMatch[1] === '') { 112 | return { 113 | op: 'replace', 114 | path: `/${lensOp.name}${headMatch[1] || ''}`, 115 | value: null, 116 | }; 117 | } 118 | else { 119 | return Object.assign(Object.assign({}, patchOp), { path: `/${lensOp.name}${headMatch[1] || ''}` }); 120 | } 121 | } 122 | break; 123 | } 124 | case 'add': 125 | // hmm, what do we do here? perhaps write the default value if there's nothing 126 | // already written into the doc there? 127 | // (could be a good use case for destinationDoc) 128 | break; 129 | case 'remove': 130 | if (patchOp.path.split('/')[1] === lensOp.name) 131 | return null; 132 | break; 133 | case 'in': { 134 | // Run the inner body in a context where the path has been narrowed down... 135 | const pathComponent = new RegExp(`^/${lensOp.name}`); 136 | if (patchOp.path.match(pathComponent)) { 137 | const childPatch = applyLensToPatchOp(lensOp.lens, Object.assign(Object.assign({}, patchOp), { path: patchOp.path.replace(pathComponent, '') })); 138 | if (childPatch) { 139 | return Object.assign(Object.assign({}, childPatch), { path: `/${lensOp.name}${childPatch.path}` }); 140 | } 141 | else { 142 | return null; 143 | } 144 | } 145 | break; 146 | } 147 | case 'map': { 148 | const arrayIndexMatch = patchOp.path.match(/\/([0-9]+)\//); 149 | if (!arrayIndexMatch) 150 | break; 151 | const arrayIndex = arrayIndexMatch[1]; 152 | const itemPatch = applyLensToPatchOp(lensOp.lens, Object.assign(Object.assign({}, patchOp), { path: patchOp.path.replace(/\/[0-9]+\//, '/') })); 153 | if (itemPatch) { 154 | return Object.assign(Object.assign({}, itemPatch), { path: `/${arrayIndex}${itemPatch.path}` }); 155 | } 156 | return null; 157 | } 158 | case 'convert': { 159 | if (patchOp.op !== 'add' && patchOp.op !== 'replace') 160 | break; 161 | if (`/${lensOp.name}` !== patchOp.path) 162 | break; 163 | const stringifiedValue = String(patchOp.value); 164 | // todo: should we add in support for fallback/default conversions 165 | if (!Object.keys(lensOp.mapping[0]).includes(stringifiedValue)) { 166 | throw new Error(`No mapping for value: ${stringifiedValue}`); 167 | } 168 | return Object.assign(Object.assign({}, patchOp), { value: lensOp.mapping[0][stringifiedValue] }); 169 | } 170 | default: 171 | assertNever(lensOp); // exhaustiveness check 172 | } 173 | return patchOp; 174 | } 175 | function expandPatch(patchOp) { 176 | // this only applies for add and replace ops; no expansion to do otherwise 177 | // todo: check the whole list of json patch verbs 178 | if (patchOp.op !== 'add' && patchOp.op !== 'replace') 179 | return [patchOp]; 180 | if (patchOp.value && typeof patchOp.value === 'object') { 181 | let result = [ 182 | { 183 | op: patchOp.op, 184 | path: patchOp.path, 185 | value: Array.isArray(patchOp.value) ? [] : {}, 186 | }, 187 | ]; 188 | result = result.concat(Object.entries(patchOp.value).map(([key, value]) => { 189 | return expandPatch({ 190 | op: patchOp.op, 191 | path: `${patchOp.path}/${key}`, 192 | value, 193 | }); 194 | })); 195 | return result.flat(Infinity); 196 | } 197 | return [patchOp]; 198 | } 199 | exports.expandPatch = expandPatch; 200 | //# sourceMappingURL=patch.js.map -------------------------------------------------------------------------------- /dist/patch.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"patch.js","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/reverse.d.ts: -------------------------------------------------------------------------------- 1 | import { LensSource } from './lens-ops'; 2 | export declare function reverseLens(lens: LensSource): LensSource; 3 | -------------------------------------------------------------------------------- /dist/reverse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.reverseLens = void 0; 4 | function assertNever(x) { 5 | throw new Error(`Unexpected object: ${x}`); 6 | } 7 | function reverseLens(lens) { 8 | return lens 9 | .slice() 10 | .reverse() 11 | .map((l) => reverseLensOp(l)); 12 | } 13 | exports.reverseLens = reverseLens; 14 | function reverseLensOp(lensOp) { 15 | switch (lensOp.op) { 16 | case 'rename': 17 | return Object.assign(Object.assign({}, lensOp), { source: lensOp.destination, destination: lensOp.source }); 18 | case 'add': { 19 | return Object.assign(Object.assign({}, lensOp), { op: 'remove' }); 20 | } 21 | case 'remove': 22 | return Object.assign(Object.assign({}, lensOp), { op: 'add' }); 23 | case 'wrap': 24 | return Object.assign(Object.assign({}, lensOp), { op: 'head' }); 25 | case 'head': 26 | return Object.assign(Object.assign({}, lensOp), { op: 'wrap' }); 27 | case 'in': 28 | case 'map': 29 | return Object.assign(Object.assign({}, lensOp), { lens: reverseLens(lensOp.lens) }); 30 | case 'hoist': 31 | return Object.assign(Object.assign({}, lensOp), { op: 'plunge' }); 32 | case 'plunge': 33 | return Object.assign(Object.assign({}, lensOp), { op: 'hoist' }); 34 | case 'convert': { 35 | const mapping = [lensOp.mapping[1], lensOp.mapping[0]]; 36 | const reversed = Object.assign(Object.assign({}, lensOp), { mapping, sourceType: lensOp.destinationType, destinationType: lensOp.sourceType }); 37 | return reversed; 38 | } 39 | default: 40 | return assertNever(lensOp); // exhaustiveness check 41 | } 42 | } 43 | //# sourceMappingURL=reverse.js.map -------------------------------------------------------------------------------- /dist/reverse.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"reverse.js","sourceRoot":"","sources":["../src/reverse.ts"],"names":[],"mappings":";;;AAEA,SAAS,WAAW,CAAC,CAAQ;IAC3B,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,SAAgB,WAAW,CAAC,IAAgB;IAC1C,OAAO,IAAI;SACR,KAAK,EAAE;SACP,OAAO,EAAE;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;AACjC,CAAC;AALD,kCAKC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,QAAQ,MAAM,CAAC,EAAE,EAAE;QACjB,KAAK,QAAQ;YACX,uCACK,MAAM,KACT,MAAM,EAAE,MAAM,CAAC,WAAW,EAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,IAC3B;QAEH,KAAK,KAAK,CAAC,CAAC;YACV,uCACK,MAAM,KACT,EAAE,EAAE,QAAQ,IACb;SACF;QAED,KAAK,QAAQ;YACX,uCACK,MAAM,KACT,EAAE,EAAE,KAAK,IACV;QAEH,KAAK,MAAM;YACT,uCACK,MAAM,KACT,EAAE,EAAE,MAAM,IACX;QACH,KAAK,MAAM;YACT,uCACK,MAAM,KACT,EAAE,EAAE,MAAM,IACX;QAEH,KAAK,IAAI,CAAC;QACV,KAAK,KAAK;YACR,uCAAY,MAAM,KAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAE;QAEtD,KAAK,OAAO;YACV,uCACK,MAAM,KACT,EAAE,EAAE,QAAQ,IACb;QACH,KAAK,QAAQ;YACX,uCACK,MAAM,KACT,EAAE,EAAE,OAAO,IACZ;QACH,KAAK,SAAS,CAAC,CAAC;YACd,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;YACtD,MAAM,QAAQ,mCACT,MAAM,KACT,OAAO,EACP,UAAU,EAAE,MAAM,CAAC,eAAe,EAClC,eAAe,EAAE,MAAM,CAAC,UAAU,GACnC,CAAA;YAED,OAAO,QAAQ,CAAA;SAChB;QAED;YACE,OAAO,WAAW,CAAC,MAAM,CAAC,CAAA,CAAC,uBAAuB;KACrD;AACH,CAAC"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cambria", 3 | "version": "0.1.2", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc --outDir ./dist --project ./tsconfig.json", 9 | "test": "mocha -r ts-node/register test/**.ts", 10 | "prepare": "yarn run build", 11 | "build-demo": "browserify ./demo/web-components/cambria-demo.js -o ./demo/bundle.js" 12 | }, 13 | "dependencies": { 14 | "commander": "^5.1.0", 15 | "fast-json-patch": "^3.0.0-1", 16 | "graphlib": "^2.1.8", 17 | "js-yaml": "^3.14.0", 18 | "json-schema": "^0.2.5", 19 | "to-json-schema": "^0.2.5" 20 | }, 21 | "devDependencies": { 22 | "@json-editor/json-editor": "^2.3.0", 23 | "@types/graphlib": "^2.1.6", 24 | "@types/mocha": "^8.0.0", 25 | "@types/node": "^14.0.23", 26 | "@typescript-eslint/eslint-plugin": "^3.6.1", 27 | "@typescript-eslint/parser": "^3.6.1", 28 | "ajv": "^6.12.4", 29 | "assert": "^2.0.0", 30 | "browserify": "^16.5.2", 31 | "eslint": "^7.4.0", 32 | "eslint-config-airbnb-base": "^14.2.0", 33 | "eslint-config-prettier": "^6.11.0", 34 | "eslint-plugin-import": "^2.22.0", 35 | "eslint-plugin-jsx-a11y": "^6.3.1", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "json-schema-view-js": "^2.0.1", 38 | "mocha": "^8.0.1", 39 | "prettier": "^2.0.5", 40 | "ts-node": "^8.10.2", 41 | "typescript": "^3.9.6" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cambria-lens-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "lens", 4 | "title": "Cambria Lens", 5 | "type": "object", 6 | "properties": { 7 | "schemaName": { 8 | "type": "string", 9 | "title": "Schema name", 10 | "description": "The name of the schema being extended" 11 | }, 12 | "lens": { "$ref": "#/definitions/lens" } 13 | }, 14 | "required": ["schemaName", "lens"], 15 | "definitions": { 16 | "jsonSchemaType": { 17 | "type": "string", 18 | "enum": ["string", "boolean", "null", "object", "array", "number"] 19 | }, 20 | "fields": { 21 | "dataType": { 22 | "allOf": [ 23 | { "$ref": "#/definitions/jsonSchemaType" }, 24 | { 25 | "title": "Type", 26 | "description": "The type of the new property" 27 | } 28 | ] 29 | }, 30 | "name": { 31 | "type": "string", 32 | "title": "Name", 33 | "description": "The name of the property to operate on" 34 | }, 35 | "default": { 36 | "title": "Default", 37 | "description": "The default value used when none can be found." 38 | }, 39 | "req": { 40 | "title": "Required", 41 | "description": "Is the value required?", 42 | "optional": true 43 | } 44 | }, 45 | "valueMapping": { 46 | "type": "array", 47 | "items": [ 48 | { 49 | "type": "object", 50 | "title": "Old-to-new Map", 51 | "description": "A lookup table where keys will be translated to values when running the lens forward." 52 | }, 53 | { 54 | "type": "object", 55 | "title": "New-to-old Map", 56 | "description": "A lookup table where keys will be translated to values when running the lens backwards." 57 | } 58 | ] 59 | }, 60 | "lens": { 61 | "type": "array", 62 | "title": "Lens", 63 | "description": "A lens to apply inside of the given context", 64 | "items": { "$ref": "#/definitions/lensOp" } 65 | }, 66 | "lensOp": { 67 | "title": "Lens Operation", 68 | "description": "One step in a lens conversion", 69 | "oneOf": [ 70 | { 71 | "type": "object", 72 | "additionalProperties": false, 73 | "properties": { 74 | "add": { 75 | "type": "object", 76 | "additionalProperties": false, 77 | "properties": { 78 | "name": { "$ref": "#/definitions/fields/name" }, 79 | "type": { "$ref": "#/definitions/fields/dataType" }, 80 | "default": { "$ref": "#/definitions/fields/default" }, 81 | "required": { "$ref": "#/definitions/fields/req" } 82 | } 83 | } 84 | } 85 | }, 86 | { 87 | "type": "object", 88 | "additionalProperties": false, 89 | "properties": { 90 | "remove": { 91 | "type": "object", 92 | "additionalProperties": false, 93 | "properties": { 94 | "name": { "$ref": "#/definitions/fields/name" }, 95 | "type": { "$ref": "#/definitions/fields/dataType" }, 96 | "default": { "$ref": "#/definitions/fields/default" } 97 | } 98 | } 99 | } 100 | }, 101 | { 102 | "type": "object", 103 | "additionalProperties": false, 104 | "properties": { 105 | "rename": { 106 | "type": "object", 107 | "additionalProperties": false, 108 | "properties": { 109 | "source": { 110 | "type": "string", 111 | "title": "Source", 112 | "description": "The old name for the property" 113 | }, 114 | "destination": { 115 | "type": "string", 116 | "title": "Destination", 117 | "description": "The new name for the property" 118 | } 119 | } 120 | } 121 | } 122 | }, 123 | { 124 | "type": "object", 125 | "additionalProperties": false, 126 | "properties": { 127 | "hoist": { 128 | "type": "object", 129 | "additionalProperties": false, 130 | "properties": { 131 | "name": { "$ref": "#/definitions/fields/name" }, 132 | "host": { 133 | "type": "string", 134 | "title": "Host", 135 | "description": "The property name of the containing object to hoist out of" 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | { 142 | "type": "object", 143 | "additionalProperties": false, 144 | "properties": { 145 | "plunge": { 146 | "type": "object", 147 | "additionalProperties": false, 148 | "properties": { 149 | "name": { "$ref": "#/definitions/fields/name" }, 150 | "host": { 151 | "type": "string", 152 | "title": "Host", 153 | "description": "The property name of the containing object to plunge into" 154 | } 155 | } 156 | } 157 | } 158 | }, 159 | { 160 | "type": "object", 161 | "additionalProperties": false, 162 | "properties": { 163 | "wrap": { 164 | "type": "object", 165 | "additionalProperties": false, 166 | "properties": { 167 | "name": { "$ref": "#/definitions/fields/name" } 168 | } 169 | } 170 | } 171 | }, 172 | { 173 | "type": "object", 174 | "additionalProperties": false, 175 | "properties": { 176 | "head": { 177 | "type": "object", 178 | "additionalProperties": false, 179 | "properties": { 180 | "name": { "$ref": "#/definitions/fields/name" } 181 | } 182 | } 183 | } 184 | }, 185 | { 186 | "type": "object", 187 | "additionalProperties": false, 188 | "properties": { 189 | "in": { 190 | "type": "object", 191 | "additionalProperties": false, 192 | "properties": { 193 | "name": { 194 | "type": "string", 195 | "title": "Name", 196 | "description": "The property name in which the sub-lens will be run" 197 | }, 198 | "lens": { "$ref": "#/definitions/lens" } 199 | } 200 | } 201 | } 202 | }, 203 | { 204 | "type": "object", 205 | "additionalProperties": false, 206 | "properties": { 207 | "map": { 208 | "type": "object", 209 | "additionalProperties": false, 210 | "properties": { 211 | "lens": { "$ref": "#/definitions/lens" } 212 | } 213 | } 214 | } 215 | }, 216 | { 217 | "type": "object", 218 | "additionalProperties": false, 219 | "properties": { 220 | "convert": { 221 | "type": "object", 222 | "additionalProperties": false, 223 | "properties": { 224 | "name": { "$ref": "#/definitions/fields/name" }, 225 | "sourceType": { "$ref": "#/definitions/fields/dataType" }, 226 | "destinationType": { "$ref": "#/definitions/fields/dataType" }, 227 | "mapping": { "$ref": "#/definitions/valueMapping" } 228 | } 229 | } 230 | } 231 | } 232 | ] 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { program } from 'commander' 2 | import { readFileSync } from 'fs' 3 | 4 | import { reverseLens } from './reverse' 5 | import { applyLensToDoc } from './doc' 6 | import { loadYamlLens } from './lens-loader' 7 | 8 | program 9 | .description('// A CLI document conversion tool for cambria') 10 | .requiredOption('-l, --lens ', 'lens source as yaml') 11 | .option('-i, --input ', 'input document filename') 12 | .option('-s, --schema ', 'json schema for input document') 13 | .option('-b, --base ', 'base document filename') 14 | .option('-r, --reverse', 'run the lens in reverse') 15 | 16 | program.parse(process.argv) 17 | 18 | // read doc from stdin if no input specified 19 | const input = readFileSync(program.input || 0, 'utf-8') 20 | const baseDoc = program.base ? JSON.parse(readFileSync(program.base, 'utf-8')) : {} 21 | const doc = JSON.parse(input) 22 | const lensData = readFileSync(program.lens, 'utf-8') 23 | 24 | let lens = loadYamlLens(lensData) 25 | 26 | if (program.reverse) { 27 | lens = reverseLens(lens) 28 | } 29 | 30 | const newDoc = applyLensToDoc(lens, doc, program.schema, baseDoc) 31 | 32 | console.log(JSON.stringify(newDoc, null, 4)) 33 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import { applyPatch } from 'fast-json-patch' 3 | import { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from 'json-schema' 4 | import { Patch } from './patch' 5 | 6 | /** 7 | * behaviour: 8 | * - if we have an array of types where null is an option, that's our default 9 | * - otherwise use the first type in the array to pick a default from the table 10 | * - otherwise just use the value to lookup in the table 11 | */ 12 | const defaultValuesForType = { 13 | string: '', 14 | number: 0, 15 | boolean: false, 16 | array: [], 17 | object: {}, 18 | } 19 | export function defaultValuesByType( 20 | type: JSONSchema7TypeName | JSONSchema7TypeName[] 21 | ): JSONSchema7['default'] { 22 | if (Array.isArray(type)) { 23 | if (type.includes('null')) { 24 | return null 25 | } 26 | return defaultValuesForType[type[0]] 27 | } 28 | return defaultValuesForType[type] 29 | } 30 | 31 | // Return a recursively filled-in default object for a given schema 32 | export function defaultObjectForSchema(schema: JSONSchema7): JSONSchema7 { 33 | // By setting the root to empty object, 34 | // we kick off a recursive process that fills in the entire thing 35 | const initializeRootPatch = [ 36 | { 37 | op: 'add' as const, 38 | path: '', 39 | value: {}, 40 | }, 41 | ] 42 | const defaultsPatch = addDefaultValues(initializeRootPatch, schema) 43 | 44 | return applyPatch({}, defaultsPatch).newDocument 45 | } 46 | 47 | export function addDefaultValues(patch: Patch, schema: JSONSchema7): Patch { 48 | return patch 49 | .map((op) => { 50 | const isMakeMap = 51 | (op.op === 'add' || op.op === 'replace') && 52 | op.value !== null && 53 | typeof op.value === 'object' && 54 | Object.entries(op.value).length === 0 55 | 56 | if (!isMakeMap) return op 57 | 58 | const objectProperties = getPropertiesForPath(schema, op.path) 59 | 60 | return [ 61 | op, 62 | // fill in default values for each property on the object 63 | ...Object.entries(objectProperties).map(([propName, propSchema]) => { 64 | if (typeof propSchema !== 'object') throw new Error(`Missing property ${propName}`) 65 | const path = `${op.path}/${propName}` 66 | 67 | // Fill in a default iff: 68 | // 1) it's an object or array: init to empty 69 | // 2) it's another type and there's a default value set. 70 | // TODO: is this right? 71 | // Should we allow defaulting containers to non-empty? seems like no. 72 | // Should we fill in "default defaults" like empty string? 73 | // I think better to let the json schema explicitly define defaults 74 | let defaultValue 75 | if (propSchema.type === 'object') { 76 | defaultValue = {} 77 | } else if (propSchema.type === 'array') { 78 | defaultValue = [] 79 | } else if ('default' in propSchema) { 80 | defaultValue = propSchema.default 81 | } else if (Array.isArray(propSchema.type) && propSchema.type.includes('null')) { 82 | defaultValue = null 83 | } 84 | 85 | if (defaultValue !== undefined) { 86 | // todo: this is a TS hint, see if we can remove 87 | if (op.op !== 'add' && op.op !== 'replace') throw new Error('') 88 | return addDefaultValues([{ ...op, path, value: defaultValue }], schema) 89 | } 90 | return [] 91 | }), 92 | ].flat(Infinity) 93 | }) 94 | .flat(Infinity) as Patch 95 | } 96 | 97 | // given a json schema and a json path to an object field somewhere in that schema, 98 | // return the json schema for the object being pointed to 99 | function getPropertiesForPath( 100 | schema: JSONSchema7, 101 | path: string 102 | ): { [key: string]: JSONSchema7Definition } { 103 | const pathComponents = path.split('/').slice(1) 104 | const { properties } = pathComponents.reduce((schema: JSONSchema7, pathSegment: string) => { 105 | const types = Array.isArray(schema.type) ? schema.type : [schema.type] 106 | if (types.includes('object')) { 107 | const schemaForProperty = schema.properties && schema.properties[pathSegment] 108 | if (typeof schemaForProperty !== 'object') throw new Error('Expected object') 109 | return schemaForProperty 110 | } 111 | if (types.includes('array')) { 112 | // throw away the array index, just return the schema for array items 113 | if (!schema.items || typeof schema.items !== 'object') 114 | throw new Error('Expected array items to have types') 115 | 116 | // todo: revisit this "as", was a huge pain to get this past TS 117 | return schema.items as JSONSchema7 118 | } 119 | throw new Error('Expected object or array in schema based on JSON Pointer') 120 | }, schema) 121 | 122 | if (properties === undefined) return {} 123 | return properties 124 | } 125 | -------------------------------------------------------------------------------- /src/doc.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | import { JSONSchema7 } from 'json-schema' 3 | import { compare, applyPatch } from 'fast-json-patch' 4 | import toJSONSchema from 'to-json-schema' 5 | 6 | import { defaultObjectForSchema } from './defaults' 7 | import { Patch, applyLensToPatch } from './patch' 8 | import { LensSource } from './lens-ops' 9 | import { updateSchema } from './json-schema' 10 | 11 | /** 12 | * importDoc - convert any Plain Old Javascript Object into an implied JSON Schema and 13 | * a JSON Patch that sets every value in that document. 14 | * @param inputDoc a document to convert into a big JSON patch describing its full contents 15 | */ 16 | export function importDoc(inputDoc: any): [JSONSchema7, Patch] { 17 | const options = { 18 | postProcessFnc: (type, schema, obj, defaultFnc) => ({ 19 | ...defaultFnc(type, schema, obj), 20 | type: [type, 'null'], 21 | }), 22 | objects: { 23 | postProcessFnc: (schema, obj, defaultFnc) => ({ 24 | ...defaultFnc(schema, obj), 25 | required: Object.getOwnPropertyNames(obj), 26 | }), 27 | }, 28 | } 29 | 30 | const schema = toJSONSchema(inputDoc, options) as JSONSchema7 31 | const patch = compare({}, inputDoc) 32 | 33 | return [schema, patch] 34 | } 35 | 36 | /** 37 | * applyLensToDoc - converts a full document through a lens. 38 | * Under the hood, we convert your input doc into a big patch and the apply it to the targetDoc. 39 | * This allows merging data back and forth with other omitted values. 40 | * @property lensSource: the lens specification to apply to the document 41 | * @property inputDoc: the Plain Old Javascript Object to convert 42 | * @property inputSchema: (default: inferred from inputDoc) a JSON schema defining the input 43 | * @property targetDoc: (default: {}) a document to apply the contents of this document to as a patch 44 | */ 45 | export function applyLensToDoc( 46 | lensSource: LensSource, 47 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 48 | inputDoc: any, 49 | inputSchema?: JSONSchema7, 50 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 51 | targetDoc?: any 52 | ): any { 53 | const [impliedSchema, patchForOriginalDoc] = importDoc(inputDoc) 54 | 55 | if (inputSchema === undefined || inputSchema === null) { 56 | inputSchema = impliedSchema 57 | } 58 | 59 | // construct the "base" upon which we will apply the patches from doc. 60 | // We start with the default object for the output schema, 61 | // then we add in any existing fields on the target doc. 62 | // TODO: I think we need to deep merge here, can't just shallow merge? 63 | const outputSchema = updateSchema(inputSchema, lensSource) 64 | const base = Object.assign(defaultObjectForSchema(outputSchema), targetDoc || {}) 65 | 66 | // return a doc based on the converted patch. 67 | // (start with either a specified baseDoc, or just empty doc) 68 | // convert the patch through the lens 69 | const outputPatch = applyLensToPatch(lensSource, patchForOriginalDoc, inputSchema) 70 | return applyPatch(base, outputPatch).newDocument 71 | } 72 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | // helper functions for nicer syntax 2 | // (we might write our own parser later, but at least for now 3 | // this avoids seeing the raw json...) 4 | 5 | import { JSONSchema7TypeName } from 'json-schema' 6 | import { 7 | LensSource, 8 | LensMap, 9 | LensIn, 10 | Property, 11 | AddProperty, 12 | RemoveProperty, 13 | RenameProperty, 14 | HoistProperty, 15 | PlungeProperty, 16 | WrapProperty, 17 | HeadProperty, 18 | ValueMapping, 19 | ConvertValue, 20 | } from './lens-ops' 21 | 22 | export function addProperty(property: Property): AddProperty { 23 | return { 24 | op: 'add', 25 | ...property, 26 | } 27 | } 28 | 29 | export function removeProperty(property: Property): RemoveProperty { 30 | return { 31 | op: 'remove', 32 | ...property, 33 | } 34 | } 35 | 36 | export function renameProperty(source: string, destination: string): RenameProperty { 37 | return { 38 | op: 'rename', 39 | source, 40 | destination, 41 | } 42 | } 43 | 44 | export function hoistProperty(host: string, name: string): HoistProperty { 45 | return { 46 | op: 'hoist', 47 | host, 48 | name, 49 | } 50 | } 51 | 52 | export function plungeProperty(host: string, name: string): PlungeProperty { 53 | return { 54 | op: 'plunge', 55 | host, 56 | name, 57 | } 58 | } 59 | 60 | export function wrapProperty(name: string): WrapProperty { 61 | return { 62 | op: 'wrap', 63 | name, 64 | } 65 | } 66 | 67 | export function headProperty(name: string): HeadProperty { 68 | return { 69 | op: 'head', 70 | name, 71 | } 72 | } 73 | 74 | export function inside(name: string, lens: LensSource): LensIn { 75 | return { 76 | op: 'in', 77 | name, 78 | lens, 79 | } 80 | } 81 | 82 | export function map(lens: LensSource): LensMap { 83 | return { 84 | op: 'map', 85 | lens, 86 | } 87 | } 88 | 89 | export function convertValue( 90 | name: string, 91 | mapping: ValueMapping, 92 | sourceType?: JSONSchema7TypeName, 93 | destinationType?: JSONSchema7TypeName 94 | ): ConvertValue { 95 | return { 96 | op: 'convert', 97 | name, 98 | mapping, 99 | sourceType, 100 | destinationType, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: The exported surface is fairly large right now, 2 | // See how much we can narrow this. 3 | 4 | export { updateSchema, schemaForLens } from './json-schema' 5 | export { compile, applyLensToPatch, Patch, CompiledLens } from './patch' 6 | export { applyLensToDoc, importDoc } from './doc' 7 | export { LensSource, LensOp, Property } from './lens-ops' 8 | export { defaultObjectForSchema } from './defaults' 9 | export { reverseLens } from './reverse' 10 | export { LensGraph, initLensGraph, registerLens, lensGraphSchema, lensFromTo } from './lens-graph' 11 | 12 | export { 13 | addProperty, 14 | removeProperty, 15 | renameProperty, 16 | hoistProperty, 17 | plungeProperty, 18 | wrapProperty, 19 | headProperty, 20 | inside, 21 | map, 22 | convertValue, 23 | } from './helpers' 24 | 25 | export { loadYamlLens } from './lens-loader' 26 | -------------------------------------------------------------------------------- /src/json-schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from 'json-schema' 2 | import { inspect } from 'util' 3 | import { defaultValuesByType } from './defaults' 4 | import { 5 | Property, 6 | LensSource, 7 | ConvertValue, 8 | LensOp, 9 | HeadProperty, 10 | WrapProperty, 11 | LensIn, 12 | } from './lens-ops' 13 | 14 | export const emptySchema = { 15 | $schema: 'http://json-schema.org/draft-07/schema', 16 | type: 'object' as const, 17 | additionalProperties: false, 18 | } 19 | 20 | function deepInspect(object: any) { 21 | return inspect(object, false, null, true) 22 | } 23 | 24 | // add a property to a schema 25 | // note: property names are in json pointer with leading / 26 | // (because that's how our Property types work for now) 27 | 28 | // mutates the schema that is passed in 29 | // (should switch to a more functional style) 30 | function addProperty(schema: JSONSchema7, property: Property): JSONSchema7 { 31 | const { properties: origProperties = {}, required: origRequired = [] } = schema 32 | const { name, items, required: isPropertyRequired } = property 33 | let { type } = property 34 | 35 | if (!name || !type) { 36 | throw new Error(`Missing property name in addProperty.\nFound:\n${JSON.stringify(property)}`) 37 | } 38 | 39 | if (Array.isArray(type)) { 40 | type = type.map((t) => (t === null ? 'null' : t)) 41 | } 42 | 43 | const arraylessPropertyDefinition = { 44 | type, 45 | default: property.default || defaultValuesByType(type), // default is a reserved keyword 46 | } 47 | // this is kludgey but you should see the crazy syntax for the alternative 48 | const propertyDefinition = 49 | type === 'array' && items 50 | ? { 51 | ...arraylessPropertyDefinition, 52 | items: { type: items.type, default: items.default || defaultValuesByType(items.type) }, 53 | } 54 | : arraylessPropertyDefinition 55 | 56 | const properties = { ...origProperties, [name]: propertyDefinition } 57 | const shouldAdd = isPropertyRequired !== false && !origRequired.includes(name) 58 | const required = [...origRequired, ...(shouldAdd ? [name] : [])] 59 | return { 60 | ...schema, 61 | properties, 62 | required, 63 | } 64 | } 65 | 66 | function withNullable(schema: JSONSchema7, fn: (s: JSONSchema7) => JSONSchema7): JSONSchema7 { 67 | if (schema.anyOf) { 68 | if (schema.anyOf.length !== 2) { 69 | throw new Error('We only support this operation on schemas with one type or a nullable type') 70 | } 71 | return { ...schema, anyOf: schema.anyOf.map(db).map((s) => (s.type === 'null' ? s : fn(s))) } 72 | } else { 73 | return fn(schema) 74 | } 75 | } 76 | 77 | function renameProperty(_schema: JSONSchema7, from: string, to: string): JSONSchema7 { 78 | return withNullable(_schema, (schema) => { 79 | if (typeof schema !== 'object' || typeof schema.properties !== 'object') { 80 | throw new Error(`expected schema object, got ${JSON.stringify(schema)}`) 81 | } 82 | if (!from) { 83 | throw new Error("Rename property requires a 'source' to rename.") 84 | } 85 | if (!schema.properties[from]) { 86 | throw new Error( 87 | `Cannot rename property '${from}' because it does not exist among ${Object.keys( 88 | schema.properties 89 | )}.` 90 | ) 91 | } 92 | if (!to) { 93 | throw new Error(`Need a 'destination' to rename ${from} to.`) 94 | } 95 | 96 | const { properties = {}, required = [] } = schema // extract properties with default of empty 97 | const { [from]: propDetails, ...rest } = properties // pull out the old value 98 | 99 | if (propDetails === undefined) { 100 | throw new Error(`Rename error: missing expected property ${from}`) 101 | } 102 | 103 | return { 104 | ...schema, 105 | properties: { [to]: propDetails, ...rest }, 106 | required: [...required.filter((r) => r !== from), to], 107 | } // assign it to the new one 108 | }) 109 | } 110 | 111 | // remove a property from a schema 112 | // property name is _not_ in JSON Pointer, no leading slash here. 113 | // (yes, that's inconsistent with addPropertyToSchema, which is bad) 114 | function removeProperty(schema: JSONSchema7, removedPointer: string): JSONSchema7 { 115 | const { properties = {}, required = [] } = schema 116 | const removed = removedPointer 117 | // we don't care about the `discarded` variable... 118 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 119 | 120 | if (!(removed in properties)) { 121 | throw new Error(`Attempting to remove nonexistent property: ${removed}`) 122 | } 123 | 124 | // no way to discard the 125 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 126 | const { [removed]: discarded, ...rest } = properties 127 | 128 | return { 129 | ...schema, 130 | properties: rest, 131 | required: required.filter((e) => e !== removed), 132 | } 133 | } 134 | 135 | function schemaSupportsType( 136 | typeValue: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined, 137 | type: JSONSchema7TypeName 138 | ): boolean { 139 | if (!typeValue) { 140 | return false 141 | } 142 | if (!Array.isArray(typeValue)) { 143 | typeValue = [typeValue] 144 | } 145 | 146 | return typeValue.includes(type) 147 | } 148 | 149 | /** db 150 | * removes the horrible, obnoxious, and annoying case where JSON schemas can just be 151 | * "true" or "false" meaning the below definitions and screwing up my type checker 152 | */ 153 | function db(s: JSONSchema7Definition): JSONSchema7 { 154 | if (s === true) { 155 | return {} 156 | } 157 | if (s === false) { 158 | return { not: {} } 159 | } 160 | return s 161 | } 162 | 163 | function supportsNull(schema: JSONSchema7): boolean { 164 | return ( 165 | schemaSupportsType(schema.type, 'null') || 166 | !!schema.anyOf?.some((subSchema) => schemaSupportsType(db(subSchema).type, 'null')) 167 | ) 168 | } 169 | 170 | function findHost(schema: JSONSchema7, name: string): JSONSchema7 { 171 | if (schema.anyOf) { 172 | const maybeSchema = schema.anyOf?.find((t) => typeof t === 'object' && t.properties) 173 | if (typeof maybeSchema === 'object' && typeof maybeSchema.properties === 'object') { 174 | const maybeHost = maybeSchema.properties[name] 175 | if (maybeHost !== false && maybeHost !== true) { 176 | return maybeHost 177 | } 178 | } 179 | } else if (schema.properties && schema.properties[name]) { 180 | const maybeHost = schema.properties[name] 181 | if (maybeHost !== false && maybeHost !== true) { 182 | return maybeHost 183 | } 184 | } 185 | throw new Error("Coudln't find the host for this data.") 186 | } 187 | 188 | function inSchema(schema: JSONSchema7, op: LensIn): JSONSchema7 { 189 | const properties: JSONSchema7 = schema.properties 190 | ? schema.properties 191 | : (schema.anyOf?.find((t) => typeof t === 'object' && t.properties) as any).properties 192 | 193 | if (!properties) { 194 | throw new Error("Cannot look 'in' an object that doesn't have properties.") 195 | } 196 | 197 | const { name, lens } = op 198 | 199 | if (!name) { 200 | throw new Error(`Expected to find property ${name} in ${Object.keys(op || {})}`) 201 | } 202 | 203 | const host = findHost(schema, name) 204 | 205 | if (host === undefined) { 206 | throw new Error(`Expected to find property ${name} in ${Object.keys(properties || {})}`) 207 | } 208 | 209 | const newProperties: JSONSchema7 = { 210 | ...properties, 211 | [name]: updateSchema(host, lens), 212 | } 213 | 214 | return { 215 | ...schema, 216 | properties: newProperties, 217 | } as JSONSchema7 218 | } 219 | 220 | type JSONSchema7Items = boolean | JSONSchema7 | JSONSchema7Definition[] | undefined 221 | function validateSchemaItems(items: JSONSchema7Items) { 222 | if (Array.isArray(items)) { 223 | throw new Error('Cambria only supports consistent types for arrays.') 224 | } 225 | if (!items || items === true) { 226 | throw new Error(`Cambria requires a specific items definition, found ${items}.`) 227 | } 228 | return items 229 | } 230 | 231 | function mapSchema(schema: JSONSchema7, lens: LensSource) { 232 | if (!lens) { 233 | throw new Error('Map requires a `lens` to map over the array.') 234 | } 235 | if (!schema.items) { 236 | throw new Error(`Map requires a schema with items to map over, ${deepInspect(schema)}`) 237 | } 238 | return { ...schema, items: updateSchema(validateSchemaItems(schema.items), lens) } 239 | } 240 | 241 | function filterScalarOrArray(v: T | T[], cb: (t: T) => boolean) { 242 | if (!Array.isArray(v)) { 243 | v = [v] 244 | } 245 | v = v.filter(cb) 246 | if (v.length === 1) { 247 | return v[0] 248 | } 249 | return v 250 | } 251 | 252 | // XXX: THIS SHOULD REMOVE DEFAULT: NULL 253 | function removeNullSupport(prop: JSONSchema7): JSONSchema7 | null { 254 | if (!supportsNull(prop)) { 255 | return prop 256 | } 257 | if (prop.type) { 258 | if (prop.type === 'null') { 259 | return null 260 | } 261 | 262 | prop = { ...prop, type: filterScalarOrArray(prop.type, (t) => t !== 'null') } 263 | 264 | if (prop.default === null) { 265 | prop.default = defaultValuesByType(prop.type!) // the above always assigns a legal type 266 | } 267 | } 268 | 269 | if (prop.anyOf) { 270 | const newAnyOf = prop.anyOf.reduce((acc: JSONSchema7[], s) => { 271 | const clean = removeNullSupport(db(s)) 272 | return clean ? [...acc, clean] : acc 273 | }, []) 274 | if (newAnyOf.length === 1) { 275 | return newAnyOf[0] 276 | } 277 | prop = { ...prop, anyOf: newAnyOf } 278 | } 279 | return prop 280 | } 281 | 282 | function wrapProperty(schema: JSONSchema7, op: WrapProperty): JSONSchema7 { 283 | if (!op.name) { 284 | throw new Error('Wrap property requires a `name` to identify what to wrap.') 285 | } 286 | 287 | if (!schema.properties) { 288 | throw new Error('Cannot wrap a property here. There are no properties.') 289 | } 290 | 291 | const prop = db(schema.properties[op.name]) 292 | if (!prop) { 293 | throw new Error(`Cannot wrap property '${op.name}' because it does not exist.`) 294 | } 295 | 296 | if (!supportsNull(prop)) { 297 | throw new Error( 298 | `Cannot wrap property '${op.name}' because it does not allow nulls, found ${deepInspect( 299 | schema 300 | )}` 301 | ) 302 | } 303 | 304 | return { 305 | ...schema, 306 | properties: { 307 | ...schema.properties, 308 | [op.name]: { 309 | type: 'array', 310 | default: [], 311 | items: removeNullSupport(prop) || { not: {} }, 312 | }, 313 | }, 314 | } 315 | } 316 | 317 | function headProperty(schema, op: HeadProperty) { 318 | if (!op.name) { 319 | throw new Error('Head requires a `name` to identify what to take head from.') 320 | } 321 | if (!schema.properties[op.name]) { 322 | throw new Error(`Cannot head property '${op.name}' because it does not exist.`) 323 | } 324 | 325 | return { 326 | ...schema, 327 | properties: { 328 | ...schema.properties, 329 | [op.name]: { anyOf: [{ type: 'null' }, schema.properties[op.name].items] }, 330 | }, 331 | } 332 | } 333 | 334 | function hoistProperty(_schema: JSONSchema7, host: string, name: string): JSONSchema7 { 335 | return withNullable(_schema, (schema) => { 336 | if (schema.properties === undefined) { 337 | throw new Error(`Can't hoist when root schema isn't an object`) 338 | } 339 | if (!host) { 340 | throw new Error(`Need a \`host\` property to hoist from.`) 341 | } 342 | if (!name) { 343 | throw new Error(`Need to provide a \`name\` to hoist up`) 344 | } 345 | 346 | const { properties } = schema 347 | if (!(host in properties)) { 348 | throw new Error( 349 | `Can't hoist anything from ${host}, it does not exist here. (Found properties ${Object.keys( 350 | properties 351 | )})` 352 | ) 353 | } 354 | 355 | const hoistedPropertySchema = withNullable(db(properties[host]), (hostSchema) => { 356 | const hostProperties = hostSchema.properties 357 | const hostRequired = hostSchema.required || [] 358 | if (!hostProperties) { 359 | throw new Error( 360 | `There are no properties to hoist out of ${host}, found ${Object.keys(hostSchema)}` 361 | ) 362 | } 363 | if (!(name in hostProperties)) { 364 | throw new Error( 365 | `Can't hoist anything from ${host}, it does not exist here. (Found properties ${Object.keys( 366 | properties 367 | )})` 368 | ) 369 | } 370 | const { [name]: target, ...remainingProperties } = hostProperties 371 | return { 372 | ...hostSchema, 373 | properties: remainingProperties, 374 | required: hostRequired.filter((e) => e !== name), 375 | } 376 | }) 377 | const childObject = withNullable(db(properties[host]), (hostSchema) => { 378 | const hostProperties = hostSchema.properties! 379 | const { [name]: target } = hostProperties 380 | return db(target) 381 | }) 382 | 383 | return { 384 | ...schema, 385 | properties: { 386 | ...schema.properties, 387 | [host]: hoistedPropertySchema, 388 | [name]: childObject, 389 | }, 390 | required: [...(schema.required || []), name], 391 | } 392 | }) 393 | } 394 | 395 | function plungeProperty(schema: JSONSchema7, host: string, name: string) { 396 | // XXXX what should we do for missing child properties? error? 397 | const { properties = {} } = schema 398 | 399 | if (!host) { 400 | throw new Error(`Need a \`host\` property to plunge into`) 401 | } 402 | 403 | if (!name) { 404 | throw new Error(`Need to provide a \`name\` to plunge`) 405 | } 406 | 407 | const destinationTypeProperties = properties[name] 408 | 409 | if (!destinationTypeProperties) { 410 | throw new Error(`Could not find a property called ${name} among ${Object.keys(properties)}`) 411 | } 412 | 413 | // we can throw an error here if things are missing? 414 | if (destinationTypeProperties === true) { 415 | // errrr... complain? 416 | return schema 417 | } 418 | 419 | // add the property to the root schema 420 | schema = inSchema(schema, { 421 | op: 'in', 422 | name: host, 423 | lens: [ 424 | { 425 | op: 'add', 426 | ...(destinationTypeProperties as Property), 427 | name, 428 | }, 429 | ], 430 | }) 431 | 432 | // remove it from its current parent 433 | // PS: ugh 434 | schema = removeProperty(schema, name) 435 | 436 | return schema 437 | } 438 | 439 | function convertValue(schema: JSONSchema7, lensOp: ConvertValue) { 440 | const { name, destinationType, mapping } = lensOp 441 | if (!destinationType) { 442 | return schema 443 | } 444 | if (!name) { 445 | throw new Error(`Missing property name in 'convert'.\nFound:\n${JSON.stringify(lensOp)}`) 446 | } 447 | if (!mapping) { 448 | throw new Error(`Missing mapping for 'convert'.\nFound:\n${JSON.stringify(lensOp)}`) 449 | } 450 | 451 | return { 452 | ...schema, 453 | properties: { 454 | ...schema.properties, 455 | [name]: { 456 | type: destinationType, 457 | default: defaultValuesByType(destinationType), 458 | }, 459 | }, 460 | } 461 | } 462 | 463 | function assertNever(x: never): never { 464 | throw new Error(`Unexpected object: ${x}`) 465 | } 466 | 467 | function applyLensOperation(schema: JSONSchema7, op: LensOp) { 468 | switch (op.op) { 469 | case 'add': 470 | return addProperty(schema, op) 471 | case 'remove': 472 | return removeProperty(schema, op.name || '') 473 | case 'rename': 474 | return renameProperty(schema, op.source, op.destination) 475 | case 'in': 476 | return inSchema(schema, op) 477 | case 'map': 478 | return mapSchema(schema, op.lens) 479 | case 'wrap': 480 | return wrapProperty(schema, op) 481 | case 'head': 482 | return headProperty(schema, op) 483 | case 'hoist': 484 | return hoistProperty(schema, op.host, op.name) 485 | case 'plunge': 486 | return plungeProperty(schema, op.host, op.name) 487 | case 'convert': 488 | return convertValue(schema, op) 489 | 490 | default: 491 | assertNever(op) // exhaustiveness check 492 | return null 493 | } 494 | } 495 | export function updateSchema(schema: JSONSchema7, lens: LensSource): JSONSchema7 { 496 | return lens.reduce((schema: JSONSchema7, op: LensOp) => { 497 | if (schema === undefined) throw new Error("Can't update undefined schema") 498 | return applyLensOperation(schema, op) 499 | }, schema as JSONSchema7) 500 | } 501 | 502 | export function schemaForLens(lens: LensSource): JSONSchema7 { 503 | const emptySchema = { 504 | $schema: 'http://json-schema.org/draft-07/schema', 505 | type: 'object' as const, 506 | additionalProperties: false, 507 | } 508 | 509 | return updateSchema(emptySchema, lens) 510 | } 511 | -------------------------------------------------------------------------------- /src/lens-graph.ts: -------------------------------------------------------------------------------- 1 | import { Graph, alg, json } from 'graphlib' 2 | import { LensSource, LensOp, updateSchema, reverseLens } from '.' 3 | import { emptySchema } from './json-schema' 4 | import { JSONSchema7 } from 'json-schema' 5 | 6 | export interface LensGraph { 7 | graph: Graph 8 | } 9 | 10 | export function initLensGraph(): LensGraph { 11 | const lensGraph: LensGraph = { graph: new Graph() } 12 | 13 | lensGraph.graph.setNode('mu', emptySchema) 14 | return lensGraph 15 | } 16 | 17 | // Add a new lens to the schema graph. 18 | // If the "to" schema doesn't exist yet, registers the schema too. 19 | // Returns a copy of the graph with the new contents. 20 | export function registerLens( 21 | { graph }: LensGraph, 22 | from: string, 23 | to: string, 24 | lenses: LensSource 25 | ): LensGraph { 26 | // clone the graph to ensure this is a pure function 27 | graph = json.read(json.write(graph)) // (these are graphlib's jsons) 28 | 29 | if (!graph.node(from)) { 30 | throw new RangeError(`unknown schema ${from}`) 31 | } 32 | 33 | const existingLens = graph.edge({ v: from, w: to }) 34 | if (existingLens) { 35 | // we could assert this? assert.deepEqual(existingLens, lenses) 36 | // we've already registered a lens on this edge, hope it's the same one! 37 | // todo: maybe warn here? seems dangerous to silently return... 38 | return { graph } 39 | } 40 | 41 | if (!graph.node(to)) { 42 | graph.setNode(to, updateSchema(graph.node(from), lenses)) 43 | } 44 | 45 | graph.setEdge(from, to, lenses) 46 | graph.setEdge(to, from, reverseLens(lenses)) 47 | 48 | return { graph } 49 | } 50 | 51 | export function lensGraphSchemas({ graph }: LensGraph): string[] { 52 | return graph.nodes() 53 | } 54 | 55 | export function lensGraphSchema({ graph }: LensGraph, schema: string): JSONSchema7 { 56 | return graph.node(schema) 57 | } 58 | 59 | export function lensFromTo({ graph }: LensGraph, from: string, to: string): LensSource { 60 | if (!graph.hasNode(from)) { 61 | throw new Error(`couldn't find schema in graph: ${from}`) 62 | } 63 | 64 | if (!graph.hasNode(to)) { 65 | throw new Error(`couldn't find schema in graph: ${to}`) 66 | } 67 | 68 | const migrationPaths = alg.dijkstra(graph, to) 69 | const lenses: LensOp[] = [] 70 | if (migrationPaths[from].distance == Infinity) { 71 | throw new Error(`no path found from ${from} to ${to}`) 72 | } 73 | if (migrationPaths[from].distance == 0) { 74 | return [] 75 | } 76 | for (let v = from; v != to; v = migrationPaths[v].predecessor) { 77 | const w = migrationPaths[v].predecessor 78 | const edge = graph.edge({ v, w }) 79 | lenses.push(...edge) 80 | } 81 | return lenses 82 | } 83 | -------------------------------------------------------------------------------- /src/lens-loader.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'js-yaml' 2 | import { LensSource, LensOp } from './lens-ops' 3 | 4 | interface YAMLLens { 5 | lens: LensSource 6 | } 7 | 8 | const foldInOp = (lensOpJson): LensOp => { 9 | const opName = Object.keys(lensOpJson)[0] 10 | 11 | // the json format is 12 | // {"": {opArgs}} 13 | // and the internal format is 14 | // {op: , ...opArgs} 15 | const data = lensOpJson[opName] 16 | if (['in', 'map'].includes(opName)) { 17 | data.lens = data.lens.map((lensOp) => foldInOp(lensOp)) 18 | } 19 | 20 | const op = { op: opName, ...data } 21 | return op 22 | } 23 | 24 | export function loadLens(rawLens: YAMLLens): LensSource { 25 | return (rawLens.lens as LensSource) 26 | .filter((o) => o !== null) 27 | .map((lensOpJson) => foldInOp(lensOpJson)) 28 | } 29 | 30 | export function loadYamlLens(lensData: string): LensSource { 31 | const rawLens = YAML.safeLoad(lensData) as YAMLLens 32 | if (!rawLens || typeof rawLens !== 'object') throw new Error('Error loading lens') 33 | if (!('lens' in rawLens)) throw new Error(`Expected top-level key 'lens' in YAML lens file`) 34 | 35 | // we could have a root op to make this consistent... 36 | return loadLens(rawLens) 37 | } 38 | -------------------------------------------------------------------------------- /src/lens-ops.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7TypeName } from 'json-schema' 2 | 3 | export interface Property { 4 | name?: string 5 | type: JSONSchema7TypeName | JSONSchema7TypeName[] 6 | default?: any 7 | required?: boolean 8 | items?: Property 9 | } 10 | 11 | export interface AddProperty extends Property { 12 | op: 'add' 13 | } 14 | 15 | export interface RemoveProperty extends Property { 16 | op: 'remove' 17 | } 18 | 19 | export interface RenameProperty { 20 | op: 'rename' 21 | source: string 22 | destination: string 23 | } 24 | 25 | export interface HoistProperty { 26 | op: 'hoist' 27 | name: string 28 | host: string 29 | } 30 | 31 | export interface PlungeProperty { 32 | op: 'plunge' 33 | name: string 34 | host: string 35 | } 36 | export interface WrapProperty { 37 | op: 'wrap' 38 | name: string 39 | } 40 | 41 | export interface HeadProperty { 42 | op: 'head' 43 | name: string 44 | } 45 | 46 | export interface LensIn { 47 | op: 'in' 48 | name: string 49 | lens: LensSource 50 | } 51 | 52 | export interface LensMap { 53 | op: 'map' 54 | lens: LensSource 55 | } 56 | 57 | // ideally this would be a tuple, but the typechecker 58 | // wouldn't let me assign a flipped array in the reverse lens op 59 | export type ValueMapping = { [key: string]: any }[] 60 | 61 | // Notes on value conversion: 62 | // - Types are optional, only needed if the type is actually changing 63 | // - We only support hardcoded mappings for the time being; 64 | // can consider further conversions later 65 | export interface ConvertValue { 66 | op: 'convert' 67 | name: string 68 | mapping: ValueMapping 69 | sourceType?: JSONSchema7TypeName 70 | destinationType?: JSONSchema7TypeName 71 | } 72 | 73 | export type LensOp = 74 | | AddProperty 75 | | RemoveProperty 76 | | RenameProperty 77 | | HoistProperty 78 | | WrapProperty 79 | | HeadProperty 80 | | PlungeProperty 81 | | LensIn 82 | | LensMap 83 | | ConvertValue 84 | 85 | export type LensSource = LensOp[] 86 | -------------------------------------------------------------------------------- /src/patch.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from 'fast-json-patch' 2 | import { JSONSchema7 } from 'json-schema' 3 | import { LensSource, LensOp } from './lens-ops' 4 | import { reverseLens } from './reverse' 5 | import { addDefaultValues } from './defaults' 6 | import { updateSchema } from './json-schema' 7 | 8 | // todo: we're throwing away the type param right now so it doesn't actually do anything. 9 | // can we actually find a way to keep it around and typecheck patches against a type? 10 | export type PatchOp = Operation 11 | type MaybePatchOp = PatchOp | null 12 | export type Patch = Operation[] 13 | export type CompiledLens = (patch: Patch, targetDoc: any) => Patch 14 | 15 | function assertNever(x: never): never { 16 | throw new Error(`Unexpected object: ${x}`) 17 | } 18 | 19 | function noNulls(items: (T | null)[]) { 20 | return items.filter((x): x is T => x !== null) 21 | } 22 | 23 | // Provide curried functions that incorporate the lenses internally; 24 | // this is useful for exposing a pre-baked converter function to developers 25 | // without them needing to access the lens themselves 26 | // TODO: the public interface could just be runLens and reverseLens 27 | // ... maybe also composeLens? 28 | export function compile(lensSource: LensSource): { right: CompiledLens; left: CompiledLens } { 29 | return { 30 | right: (patch: Patch, targetDoc: any) => applyLensToPatch(lensSource, patch, targetDoc), 31 | left: (patch: Patch, targetDoc: any) => 32 | applyLensToPatch(reverseLens(lensSource), patch, targetDoc), 33 | } 34 | } 35 | 36 | // given a patch, returns a new patch that has had the lens applied to it. 37 | export function applyLensToPatch( 38 | lensSource: LensSource, 39 | patch: Patch, 40 | patchSchema: JSONSchema7 // the json schema for the doc the patch was operating on 41 | ): Patch { 42 | // expand patches that set nested objects into scalar patches 43 | const expandedPatch: Patch = patch.map((op) => expandPatch(op)).flat() 44 | 45 | // send everything through the lens 46 | const lensedPatch = noNulls( 47 | expandedPatch.map((patchOp) => applyLensToPatchOp(lensSource, patchOp)) 48 | ) 49 | 50 | // add in default values needed (based on the new schema after lensing) 51 | const readerSchema = updateSchema(patchSchema, lensSource) 52 | const lensedPatchWithDefaults = addDefaultValues(lensedPatch, readerSchema) 53 | 54 | return lensedPatchWithDefaults 55 | } 56 | 57 | // todo: remove destinationDoc entirely 58 | export function applyLensToPatchOp(lensSource: LensSource, patchOp: MaybePatchOp): MaybePatchOp { 59 | return lensSource.reduce((prevPatch: MaybePatchOp, lensOp: LensOp) => { 60 | return runLensOp(lensOp, prevPatch) 61 | }, patchOp) 62 | } 63 | 64 | function runLensOp(lensOp: LensOp, patchOp: MaybePatchOp): MaybePatchOp { 65 | if (patchOp === null) { 66 | return null 67 | } 68 | 69 | switch (lensOp.op) { 70 | case 'rename': 71 | if ( 72 | // TODO: what about other JSON patch op types? 73 | // (consider other parts of JSON patch: move / copy / test / remove ?) 74 | (patchOp.op === 'replace' || patchOp.op === 'add') && 75 | patchOp.path.split('/')[1] === lensOp.source 76 | ) { 77 | const path = patchOp.path.replace(lensOp.source, lensOp.destination) 78 | return { ...patchOp, path } 79 | } 80 | 81 | break 82 | 83 | case 'hoist': { 84 | // leading slash needs trimming 85 | const pathElements = patchOp.path.substr(1).split('/') 86 | const [possibleSource, possibleDestination, ...rest] = pathElements 87 | if (possibleSource === lensOp.host && possibleDestination === lensOp.name) { 88 | const path = ['', lensOp.name, ...rest].join('/') 89 | return { ...patchOp, path } 90 | } 91 | break 92 | } 93 | 94 | case 'plunge': { 95 | const pathElements = patchOp.path.substr(1).split('/') 96 | const [head] = pathElements 97 | if (head === lensOp.name) { 98 | const path = ['', lensOp.host, pathElements].join('/') 99 | return { ...patchOp, path } 100 | } 101 | break 102 | } 103 | 104 | case 'wrap': { 105 | const pathComponent = new RegExp(`^/(${lensOp.name})(.*)`) 106 | const match = patchOp.path.match(pathComponent) 107 | if (match) { 108 | const path = `/${match[1]}/0${match[2]}` 109 | if ( 110 | (patchOp.op === 'add' || patchOp.op === 'replace') && 111 | patchOp.value === null && 112 | match[2] === '' 113 | ) { 114 | return { op: 'remove', path } 115 | } 116 | return { ...patchOp, path } 117 | } 118 | break 119 | } 120 | 121 | case 'head': { 122 | // break early if we're not handling a write to the array handled by this lens 123 | const arrayMatch = patchOp.path.split('/')[1] === lensOp.name 124 | if (!arrayMatch) break 125 | 126 | // We only care about writes to the head element, nothing else matters 127 | const headMatch = patchOp.path.match(new RegExp(`^/${lensOp.name}/0(.*)`)) 128 | if (!headMatch) return null 129 | 130 | if (patchOp.op === 'add' || patchOp.op === 'replace') { 131 | // If the write is to the first array element, write to the scalar 132 | return { 133 | op: patchOp.op, 134 | path: `/${lensOp.name}${headMatch[1] || ''}`, 135 | value: patchOp.value, 136 | } 137 | } 138 | 139 | if (patchOp.op === 'remove') { 140 | if (headMatch[1] === '') { 141 | return { 142 | op: 'replace' as const, 143 | path: `/${lensOp.name}${headMatch[1] || ''}`, 144 | value: null, 145 | } 146 | } else { 147 | return { ...patchOp, path: `/${lensOp.name}${headMatch[1] || ''}` } 148 | } 149 | } 150 | 151 | break 152 | } 153 | 154 | case 'add': 155 | // hmm, what do we do here? perhaps write the default value if there's nothing 156 | // already written into the doc there? 157 | // (could be a good use case for destinationDoc) 158 | break 159 | 160 | case 'remove': 161 | if (patchOp.path.split('/')[1] === lensOp.name) return null 162 | break 163 | 164 | case 'in': { 165 | // Run the inner body in a context where the path has been narrowed down... 166 | const pathComponent = new RegExp(`^/${lensOp.name}`) 167 | if (patchOp.path.match(pathComponent)) { 168 | const childPatch = applyLensToPatchOp(lensOp.lens, { 169 | ...patchOp, 170 | path: patchOp.path.replace(pathComponent, ''), 171 | }) 172 | 173 | if (childPatch) { 174 | return { ...childPatch, path: `/${lensOp.name}${childPatch.path}` } 175 | } else { 176 | return null 177 | } 178 | } 179 | break 180 | } 181 | 182 | case 'map': { 183 | const arrayIndexMatch = patchOp.path.match(/\/([0-9]+)\//) 184 | if (!arrayIndexMatch) break 185 | const arrayIndex = arrayIndexMatch[1] 186 | const itemPatch = applyLensToPatchOp( 187 | lensOp.lens, 188 | { ...patchOp, path: patchOp.path.replace(/\/[0-9]+\//, '/') } 189 | // Then add the parent path back to the beginning of the results 190 | ) 191 | 192 | if (itemPatch) { 193 | return { ...itemPatch, path: `/${arrayIndex}${itemPatch.path}` } 194 | } 195 | return null 196 | } 197 | 198 | case 'convert': { 199 | if (patchOp.op !== 'add' && patchOp.op !== 'replace') break 200 | if (`/${lensOp.name}` !== patchOp.path) break 201 | const stringifiedValue = String(patchOp.value) 202 | 203 | // todo: should we add in support for fallback/default conversions 204 | if (!Object.keys(lensOp.mapping[0]).includes(stringifiedValue)) { 205 | throw new Error(`No mapping for value: ${stringifiedValue}`) 206 | } 207 | 208 | return { ...patchOp, value: lensOp.mapping[0][stringifiedValue] } 209 | } 210 | 211 | default: 212 | assertNever(lensOp) // exhaustiveness check 213 | } 214 | 215 | return patchOp 216 | } 217 | 218 | export function expandPatch(patchOp: PatchOp): PatchOp[] { 219 | // this only applies for add and replace ops; no expansion to do otherwise 220 | // todo: check the whole list of json patch verbs 221 | if (patchOp.op !== 'add' && patchOp.op !== 'replace') return [patchOp] 222 | 223 | if (patchOp.value && typeof patchOp.value === 'object') { 224 | let result: any[] = [ 225 | { 226 | op: patchOp.op, 227 | path: patchOp.path, 228 | value: Array.isArray(patchOp.value) ? [] : {}, 229 | }, 230 | ] 231 | 232 | result = result.concat( 233 | Object.entries(patchOp.value).map(([key, value]) => { 234 | return expandPatch({ 235 | op: patchOp.op, 236 | path: `${patchOp.path}/${key}`, 237 | value, 238 | }) 239 | }) 240 | ) 241 | 242 | return result.flat(Infinity) 243 | } 244 | return [patchOp] 245 | } 246 | -------------------------------------------------------------------------------- /src/reverse.ts: -------------------------------------------------------------------------------- 1 | import { LensSource, LensOp } from './lens-ops' 2 | 3 | function assertNever(x: never): never { 4 | throw new Error(`Unexpected object: ${x}`) 5 | } 6 | 7 | export function reverseLens(lens: LensSource): LensSource { 8 | return lens 9 | .slice() 10 | .reverse() 11 | .map((l) => reverseLensOp(l)) 12 | } 13 | 14 | function reverseLensOp(lensOp: LensOp): LensOp { 15 | switch (lensOp.op) { 16 | case 'rename': 17 | return { 18 | ...lensOp, 19 | source: lensOp.destination, 20 | destination: lensOp.source, 21 | } 22 | 23 | case 'add': { 24 | return { 25 | ...lensOp, 26 | op: 'remove', 27 | } 28 | } 29 | 30 | case 'remove': 31 | return { 32 | ...lensOp, 33 | op: 'add', 34 | } 35 | 36 | case 'wrap': 37 | return { 38 | ...lensOp, 39 | op: 'head', 40 | } 41 | case 'head': 42 | return { 43 | ...lensOp, 44 | op: 'wrap', 45 | } 46 | 47 | case 'in': 48 | case 'map': 49 | return { ...lensOp, lens: reverseLens(lensOp.lens) } 50 | 51 | case 'hoist': 52 | return { 53 | ...lensOp, 54 | op: 'plunge', 55 | } 56 | case 'plunge': 57 | return { 58 | ...lensOp, 59 | op: 'hoist', 60 | } 61 | case 'convert': { 62 | const mapping = [lensOp.mapping[1], lensOp.mapping[0]] 63 | const reversed = { 64 | ...lensOp, 65 | mapping, 66 | sourceType: lensOp.destinationType, 67 | destinationType: lensOp.sourceType, 68 | } 69 | 70 | return reversed 71 | } 72 | 73 | default: 74 | return assertNever(lensOp) // exhaustiveness check 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/github-arthropod.ts: -------------------------------------------------------------------------------- 1 | // a quasi-integration test, converting a github doc to an arthropod doc-- 2 | // testing a complex doc + lens 3 | 4 | import assert from 'assert' 5 | import githubIssue from './github-issue.json' 6 | import { applyLensToDoc } from '../src/doc' 7 | import { reverseLens } from '../src/reverse' 8 | 9 | describe('renaming title, and hoisting label name to category', () => { 10 | const lens = [ 11 | { op: 'rename' as const, source: 'title', destination: 'name' }, 12 | { op: 'head' as const, name: 'labels' }, 13 | { 14 | op: 'in' as const, 15 | name: 'labels', 16 | lens: [{ op: 'rename' as const, source: 'name', destination: 'category' }], 17 | }, 18 | { op: 'hoist' as const, host: 'labels', name: 'category' }, 19 | { 20 | op: 'remove' as const, 21 | name: 'labels', 22 | type: ['object' as const, 'null' as const], 23 | }, 24 | ] 25 | 26 | it('converts the doc forwards', () => { 27 | const { title: _title, labels: _labels, ...rest } = githubIssue 28 | assert.deepEqual(applyLensToDoc(lens, githubIssue), { 29 | ...rest, 30 | name: githubIssue.title, 31 | category: githubIssue.labels[0].name, 32 | }) 33 | }) 34 | 35 | it('converts the doc backwards, merging with the original doc', () => { 36 | const newArthropod = { 37 | name: 'Changed the name', 38 | category: 'Bug', 39 | } 40 | 41 | const newGithub = applyLensToDoc(reverseLens(lens), newArthropod, undefined, githubIssue) 42 | 43 | assert.deepEqual(newGithub, { 44 | ...githubIssue, 45 | title: 'Changed the name', 46 | labels: [ 47 | { 48 | ...githubIssue.labels[0], 49 | name: 'Bug', 50 | }, 51 | ], 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/github-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "node_id": "MDU6SXNzdWUx", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 5 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 6 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 8 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 9 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 10 | "number": 1347, 11 | "state": "open", 12 | "title": "Found a bug", 13 | "body": "I'm having a problem with this.", 14 | "user": { 15 | "login": "octocat", 16 | "id": 1, 17 | "node_id": "MDQ6VXNlcjE=", 18 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/octocat", 21 | "html_url": "https://github.com/octocat", 22 | "followers_url": "https://api.github.com/users/octocat/followers", 23 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 27 | "organizations_url": "https://api.github.com/users/octocat/orgs", 28 | "repos_url": "https://api.github.com/users/octocat/repos", 29 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/octocat/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | { 36 | "id": 208045946, 37 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 38 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 39 | "name": "bug", 40 | "description": "Something isn't working", 41 | "color": "f29513", 42 | "default": true 43 | } 44 | ], 45 | "assignees": [ 46 | { 47 | "login": "octocat", 48 | "id": 1, 49 | "node_id": "MDQ6VXNlcjE=", 50 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 51 | "gravatar_id": "", 52 | "url": "https://api.github.com/users/octocat", 53 | "html_url": "https://github.com/octocat", 54 | "followers_url": "https://api.github.com/users/octocat/followers", 55 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 56 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 57 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 58 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 59 | "organizations_url": "https://api.github.com/users/octocat/orgs", 60 | "repos_url": "https://api.github.com/users/octocat/repos", 61 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 62 | "received_events_url": "https://api.github.com/users/octocat/received_events", 63 | "type": "User", 64 | "site_admin": false 65 | } 66 | ], 67 | "milestone": { 68 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 69 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 70 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 71 | "id": 1002604, 72 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 73 | "number": 1, 74 | "state": "open", 75 | "title": "v1.0", 76 | "description": "Tracking milestone for version 1.0", 77 | "creator": { 78 | "login": "octocat", 79 | "id": 1, 80 | "node_id": "MDQ6VXNlcjE=", 81 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 82 | "gravatar_id": "", 83 | "url": "https://api.github.com/users/octocat", 84 | "html_url": "https://github.com/octocat", 85 | "followers_url": "https://api.github.com/users/octocat/followers", 86 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 87 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 88 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 89 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 90 | "organizations_url": "https://api.github.com/users/octocat/orgs", 91 | "repos_url": "https://api.github.com/users/octocat/repos", 92 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 93 | "received_events_url": "https://api.github.com/users/octocat/received_events", 94 | "type": "User", 95 | "site_admin": false 96 | }, 97 | "open_issues": 4, 98 | "closed_issues": 8, 99 | "created_at": "2011-04-10T20:09:31Z", 100 | "updated_at": "2014-03-03T18:58:10Z", 101 | "closed_at": "2013-02-12T13:22:01Z", 102 | "due_on": "2012-10-09T23:39:01Z" 103 | }, 104 | "locked": true, 105 | "active_lock_reason": "too heated", 106 | "comments": 0, 107 | "pull_request": { 108 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 109 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 110 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 111 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 112 | }, 113 | "closed_at": null, 114 | "created_at": "2011-04-22T13:33:48Z", 115 | "updated_at": "2011-04-22T13:33:48Z", 116 | "closed_by": { 117 | "login": "octocat", 118 | "id": 1, 119 | "node_id": "MDQ6VXNlcjE=", 120 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/octocat", 123 | "html_url": "https://github.com/octocat", 124 | "followers_url": "https://api.github.com/users/octocat/followers", 125 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 129 | "organizations_url": "https://api.github.com/users/octocat/orgs", 130 | "repos_url": "https://api.github.com/users/octocat/repos", 131 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/octocat/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/json-schema.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { JSONSchema7 } from 'json-schema' 3 | import { updateSchema } from '../src/json-schema' 4 | import { 5 | addProperty, 6 | inside, 7 | map, 8 | headProperty, 9 | wrapProperty, 10 | hoistProperty, 11 | plungeProperty, 12 | renameProperty, 13 | convertValue, 14 | } from '../src/helpers' 15 | 16 | describe('transforming a json schema', () => { 17 | const v1Schema = { 18 | $schema: 'http://json-schema.org/draft-07/schema', 19 | type: 'object', 20 | title: 'ProjectDoc', 21 | description: 'An Arthropod project with some tasks', 22 | additionalProperties: false, 23 | $id: 'ProjectV1', 24 | properties: { 25 | name: { 26 | type: 'string', 27 | default: '', 28 | }, 29 | summary: { 30 | type: 'string', 31 | default: '', 32 | }, 33 | }, 34 | required: ['name', 'summary'], 35 | } as JSONSchema7 // need to convince typescript this is valid json schema 36 | 37 | describe('addProperty', () => { 38 | it('adds the property', () => { 39 | const newSchema = updateSchema(v1Schema, [ 40 | addProperty({ name: 'description', type: 'string' }), 41 | ]) 42 | 43 | assert.deepEqual(newSchema.properties, { 44 | ...v1Schema.properties, 45 | description: { type: 'string', default: '' }, 46 | }) 47 | }) 48 | 49 | it('supports nullable fields', () => { 50 | const newSchema = updateSchema(v1Schema, [ 51 | addProperty({ name: 'description', type: ['string', 'null'] }), 52 | ]) 53 | 54 | assert.deepEqual(newSchema.properties, { 55 | ...v1Schema.properties, 56 | description: { type: ['string', 'null'], default: null }, 57 | }) 58 | }) 59 | 60 | it('uses default value if provided', () => { 61 | const newSchema = updateSchema(v1Schema, [ 62 | addProperty({ name: 'description', type: 'string', default: 'hi' }), 63 | ]) 64 | 65 | assert.deepEqual(newSchema.properties, { 66 | ...v1Schema.properties, 67 | description: { type: 'string', default: 'hi' }, 68 | }) 69 | }) 70 | 71 | it('sets field as required', () => { 72 | const newSchema = updateSchema(v1Schema, [ 73 | addProperty({ name: 'description', type: 'string', required: true }), 74 | ]) 75 | 76 | assert.deepEqual(newSchema.properties, { 77 | ...v1Schema.properties, 78 | description: { type: 'string', default: '' }, 79 | }) 80 | 81 | assert.deepEqual(newSchema.required, [...(v1Schema.required || []), 'description']) 82 | }) 83 | 84 | it('fails when presented with invalid data', () => { 85 | const badData: any = { garbage: 'input' } 86 | assert.throws(() => { 87 | updateSchema(v1Schema, [addProperty(badData)]) 88 | }, `Missing property name in addProperty.\nFound:\n${JSON.stringify(badData)}`) 89 | }) 90 | }) 91 | 92 | describe('renameProperty', () => { 93 | const newSchema = updateSchema(v1Schema, [renameProperty('name', 'title')]) 94 | 95 | it('adds a new property and removes the old property', () => { 96 | assert.deepEqual(newSchema.properties, { 97 | title: { 98 | type: 'string', 99 | default: '', 100 | }, 101 | summary: { 102 | type: 'string', 103 | default: '', 104 | }, 105 | }) 106 | }) 107 | 108 | it('removes the old property from required array', () => { 109 | assert.equal(newSchema.required?.indexOf('name'), -1) 110 | }) 111 | }) 112 | 113 | describe('convertValue', () => { 114 | it('changes the type on the existing property', () => { 115 | const newSchema = updateSchema(v1Schema, [ 116 | convertValue( 117 | 'summary', 118 | [ 119 | { todo: false, inProgress: false, done: true }, 120 | { false: 'todo', true: 'done' }, 121 | ], 122 | 'string', 123 | 'boolean' 124 | ), 125 | ]) 126 | 127 | assert.deepEqual(newSchema.properties, { 128 | name: { 129 | type: 'string', 130 | default: '', 131 | }, 132 | summary: { 133 | type: 'boolean', 134 | default: false, 135 | }, 136 | }) 137 | }) 138 | 139 | it("doesn't update the schema when there's no type change", () => { 140 | const newSchema = updateSchema(v1Schema, [ 141 | convertValue('summary', [{ something: 'another' }, { another: 'something' }]), 142 | ]) 143 | 144 | assert.deepEqual(newSchema, v1Schema) 145 | }) 146 | 147 | it('fails when presented with invalid data', () => { 148 | const badData: any = { garbage: 'input' } 149 | assert.throws(() => { 150 | updateSchema(v1Schema, [addProperty(badData)]) 151 | }, `Missing property destinationType in 'convert'.\nFound:\n${JSON.stringify(badData)}`) 152 | }) 153 | }) 154 | 155 | describe('inside', () => { 156 | it('adds new properties inside a key', () => { 157 | const newSchema = updateSchema(v1Schema, [ 158 | addProperty({ name: 'metadata', type: 'object' }), 159 | inside('metadata', [ 160 | addProperty({ name: 'createdAt', type: 'number' }), 161 | addProperty({ name: 'updatedAt', type: 'number' }), 162 | ]), 163 | ]) 164 | 165 | assert.deepEqual(newSchema.properties, { 166 | ...v1Schema.properties, 167 | metadata: { 168 | type: 'object', 169 | default: {}, 170 | properties: { 171 | createdAt: { 172 | type: 'number', 173 | default: 0, 174 | }, 175 | updatedAt: { 176 | type: 'number', 177 | default: 0, 178 | }, 179 | }, 180 | required: ['createdAt', 'updatedAt'], 181 | }, 182 | }) 183 | }) 184 | 185 | it('renames properties inside a key', () => { 186 | const newSchema = updateSchema(v1Schema, [ 187 | addProperty({ name: 'metadata', type: 'object' }), 188 | inside('metadata', [ 189 | addProperty({ name: 'createdAt', type: 'number' }), 190 | renameProperty('createdAt', 'created'), 191 | ]), 192 | ]) 193 | 194 | assert.deepEqual(newSchema.properties, { 195 | name: { 196 | type: 'string', 197 | default: '', 198 | }, 199 | summary: { 200 | type: 'string', 201 | default: '', 202 | }, 203 | metadata: { 204 | type: 'object', 205 | default: {}, 206 | properties: { 207 | created: { 208 | type: 'number', 209 | default: 0, 210 | }, 211 | }, 212 | required: ['created'], 213 | }, 214 | }) 215 | }) 216 | }) 217 | 218 | describe('map', () => { 219 | it('adds new properties inside an array', () => { 220 | const newSchema = updateSchema(v1Schema, [ 221 | addProperty({ name: 'tasks', type: 'array', items: { type: 'object' as const } }), 222 | inside('tasks', [ 223 | map([ 224 | addProperty({ name: 'name', type: 'string' }), 225 | addProperty({ name: 'description', type: 'string' }), 226 | ]), 227 | ]), 228 | ]) 229 | 230 | assert.deepEqual(newSchema.properties, { 231 | ...v1Schema.properties, 232 | tasks: { 233 | type: 'array', 234 | default: [], 235 | items: { 236 | type: 'object', 237 | default: {}, 238 | properties: { 239 | name: { 240 | type: 'string', 241 | default: '', 242 | }, 243 | description: { 244 | type: 'string', 245 | default: '', 246 | }, 247 | }, 248 | required: ['name', 'description'], 249 | }, 250 | }, 251 | }) 252 | }) 253 | 254 | it('renames properties inside an array', () => { 255 | const newSchema = updateSchema(v1Schema, [ 256 | addProperty({ name: 'tasks', type: 'array', items: { type: 'object' as const } }), 257 | inside('tasks', [ 258 | map([addProperty({ name: 'name', type: 'string' }), renameProperty('name', 'title')]), 259 | ]), 260 | ]) 261 | 262 | assert.deepEqual(newSchema.properties, { 263 | ...v1Schema.properties, 264 | tasks: { 265 | type: 'array', 266 | default: [], 267 | items: { 268 | type: 'object', 269 | default: {}, 270 | properties: { 271 | title: { 272 | type: 'string', 273 | default: '', 274 | }, 275 | }, 276 | required: ['title'], 277 | }, 278 | }, 279 | }) 280 | }) 281 | }) 282 | 283 | describe('headProperty', () => { 284 | it('can turn an array into a scalar', () => { 285 | const newSchema = updateSchema(v1Schema, [ 286 | addProperty({ name: 'assignees', type: 'array', items: { type: 'string' as const } }), 287 | headProperty('assignees'), 288 | ]) 289 | 290 | // Really, the correct result would be: 291 | // { { type: 'null', type: 'string' }, default: 'Joe' } } 292 | // the behaviour you see below here doesn't really work with at least AJV 293 | // https://github.com/ajv-validator/ajv/issues/276 294 | assert.deepEqual(newSchema.properties, { 295 | ...v1Schema.properties, 296 | assignees: { anyOf: [{ type: 'null' }, { type: 'string', default: '' }] }, 297 | }) 298 | }) 299 | 300 | it('can preserve schema information for an array of objects becoming a single object', () => { 301 | const newSchema = updateSchema(v1Schema, [ 302 | addProperty({ name: 'assignees', type: 'array', items: { type: 'object' as const } }), 303 | inside('assignees', [map([addProperty({ name: 'name', type: 'string' })])]), 304 | headProperty('assignees'), 305 | ]) 306 | 307 | const expectedSchema = { 308 | ...v1Schema.properties, 309 | assignees: { 310 | anyOf: [ 311 | { type: 'null' }, 312 | { 313 | type: 'object', 314 | default: {}, 315 | properties: { 316 | name: { type: 'string', default: '' }, 317 | }, 318 | required: ['name'], 319 | }, 320 | ], 321 | }, 322 | } 323 | 324 | assert.deepEqual(newSchema.properties, expectedSchema) 325 | }) 326 | }) 327 | 328 | describe('wrapProperty', () => { 329 | it('can wrap a scalar into an array', () => { 330 | const newSchema = updateSchema(v1Schema, [ 331 | addProperty({ name: 'assignee', type: ['string', 'null'] }), 332 | wrapProperty('assignee'), 333 | ]) 334 | 335 | assert.deepEqual(newSchema.properties, { 336 | ...v1Schema.properties, 337 | assignee: { 338 | type: 'array', 339 | default: [], 340 | items: { 341 | type: 'string' as const, 342 | default: '', 343 | }, 344 | }, 345 | }) 346 | }) 347 | 348 | it.skip('can wrap an object into an array', () => { 349 | const newSchema = updateSchema(v1Schema, [ 350 | addProperty({ name: 'assignee', type: ['object', 'null'] }), 351 | inside('assignee', [ 352 | addProperty({ name: 'id', type: 'string' }), 353 | addProperty({ name: 'name', type: 'string' }), 354 | ]), 355 | wrapProperty('assignee'), 356 | ]) 357 | 358 | assert.deepEqual(newSchema.properties, { 359 | ...v1Schema.properties, 360 | assignee: { 361 | type: 'array', 362 | default: [], 363 | items: { 364 | type: 'object' as const, 365 | properties: { 366 | name: { type: 'string', default: '' }, 367 | id: { type: 'string', default: '' }, 368 | }, 369 | }, 370 | }, 371 | }) 372 | }) 373 | }) 374 | 375 | describe('hoistProperty', () => { 376 | it('hoists the property up in the schema', () => { 377 | const newSchema = updateSchema(v1Schema, [ 378 | addProperty({ name: 'metadata', type: 'object' }), 379 | inside('metadata', [ 380 | addProperty({ name: 'createdAt', type: 'number' }), 381 | addProperty({ name: 'editedAt', type: 'number' }), 382 | ]), 383 | hoistProperty('metadata', 'createdAt'), 384 | ]) 385 | 386 | assert.deepEqual(newSchema.properties, { 387 | ...v1Schema.properties, 388 | metadata: { 389 | type: 'object', 390 | default: {}, 391 | properties: { 392 | editedAt: { 393 | type: 'number', 394 | default: 0, 395 | }, 396 | }, 397 | required: ['editedAt'], 398 | }, 399 | createdAt: { 400 | type: 'number', 401 | default: 0, 402 | }, 403 | }) 404 | }) 405 | 406 | it('hoists up an object with child properties', () => { 407 | // hoist up a details object out of metadata 408 | const newSchema = updateSchema(v1Schema, [ 409 | addProperty({ name: 'metadata', type: 'object' }), 410 | inside('metadata', [ 411 | addProperty({ name: 'details', type: 'object' }), 412 | inside('details', [addProperty({ name: 'title', type: 'string' })]), 413 | ]), 414 | hoistProperty('metadata', 'details'), 415 | ]) 416 | 417 | assert.deepEqual(newSchema.properties, { 418 | ...v1Schema.properties, 419 | metadata: { 420 | type: 'object', 421 | default: {}, 422 | properties: {}, 423 | required: [], 424 | }, 425 | details: { 426 | type: 'object', 427 | default: {}, 428 | properties: { 429 | title: { type: 'string', default: '' }, 430 | }, 431 | required: ['title'], 432 | }, 433 | }) 434 | }) 435 | }) 436 | 437 | describe('plungeProperty', () => { 438 | it('plunges the property down in the schema', () => { 439 | // move the existing summary down into a metadata object 440 | const newSchema = updateSchema(v1Schema, [ 441 | addProperty({ name: 'metadata', type: 'object' }), 442 | inside('metadata', [ 443 | addProperty({ name: 'createdAt', type: 'number' }), 444 | addProperty({ name: 'editedAt', type: 'number' }), 445 | ]), 446 | plungeProperty('metadata', 'summary'), 447 | ]) 448 | 449 | assert.deepEqual(newSchema.properties, { 450 | name: v1Schema.properties?.name, 451 | metadata: { 452 | type: 'object', 453 | default: {}, 454 | properties: { 455 | createdAt: { 456 | type: 'number', 457 | default: 0, 458 | }, 459 | editedAt: { 460 | type: 'number', 461 | default: 0, 462 | }, 463 | summary: { 464 | type: 'string', 465 | default: '', 466 | }, 467 | }, 468 | required: ['createdAt', 'editedAt', 'summary'], 469 | }, 470 | }) 471 | }) 472 | 473 | it('fails when presented with invalid data', () => { 474 | assert.throws(() => { 475 | updateSchema(v1Schema, [plungeProperty('metadata', 'nosaj-thing')]) 476 | }, /Could not find a property called nosaj-thing among/) 477 | }) 478 | 479 | it.skip('plunges an object down with its child properties', () => { 480 | // plunge metadata object into a container object 481 | const newSchema = updateSchema(v1Schema, [ 482 | addProperty({ name: 'container', type: 'object' }), 483 | addProperty({ name: 'metadata', type: 'object' }), 484 | inside('metadata', [ 485 | addProperty({ name: 'createdAt', type: 'number' }), 486 | addProperty({ name: 'editedAt', type: 'number' }), 487 | ]), 488 | plungeProperty('container', 'metadata'), 489 | ]) 490 | 491 | assert.deepEqual(newSchema.properties, { 492 | ...v1Schema.properties, 493 | container: { 494 | type: 'object', 495 | default: {}, 496 | required: ['metadata'], 497 | properties: { 498 | metadata: { 499 | type: 'object', 500 | default: {}, 501 | properties: { 502 | createdAt: { 503 | type: 'number', 504 | default: 0, 505 | }, 506 | editedAt: { 507 | type: 'number', 508 | default: 0, 509 | }, 510 | }, 511 | required: ['createdAt', 'editedAt', 'summary'], 512 | }, 513 | }, 514 | }, 515 | }) 516 | }) 517 | }) 518 | }) 519 | -------------------------------------------------------------------------------- /test/lens-graph.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { JSONSchema7 } from 'json-schema' 3 | import { updateSchema } from '../src/json-schema' 4 | import { 5 | addProperty, 6 | inside, 7 | map, 8 | headProperty, 9 | wrapProperty, 10 | hoistProperty, 11 | plungeProperty, 12 | renameProperty, 13 | convertValue, 14 | removeProperty, 15 | } from '../src/helpers' 16 | 17 | import { 18 | LensGraph, 19 | initLensGraph, 20 | registerLens, 21 | lensGraphSchemas, 22 | lensFromTo, 23 | } from '../src/lens-graph' 24 | 25 | const LensMutoV1 = [addProperty({ name: 'title', type: 'string' })] 26 | const LensV1toV2 = [ 27 | addProperty({ name: 'metadata', type: 'object' }), 28 | inside('metadata', [ 29 | addProperty({ name: 'createdAt', type: 'number' }), 30 | addProperty({ name: 'updatedAt', type: 'number' }), 31 | ]), 32 | ] 33 | const LensV2toV3 = [ 34 | hoistProperty('metadata', 'createdAt'), 35 | addProperty({ name: 'metadata', type: 'object' }), 36 | ] 37 | 38 | const Lenses = [ 39 | { from: 'mu', to: 'V1', lens: LensMutoV1 }, 40 | { from: 'V1', to: 'V2', lens: LensV1toV2 }, 41 | { from: 'V2', to: 'V3', lens: LensV2toV3 }, 42 | ] 43 | 44 | describe('registering lenses', () => { 45 | it('should be able to create a graph', () => { 46 | const graph = initLensGraph() 47 | assert.deepEqual(lensGraphSchemas(graph), ['mu']) 48 | }) 49 | 50 | it('should be able to register some lenses', () => { 51 | const graph = Lenses.reduce((graph, { from, to, lens }) => { 52 | return registerLens(graph, from, to, lens) 53 | }, initLensGraph()) 54 | assert.deepEqual(lensGraphSchemas(graph), ['mu', 'V1', 'V2', 'V3']) 55 | }) 56 | 57 | it('should compose a lens from a path', () => { 58 | const graph = Lenses.reduce( 59 | (graph, { from, to, lens }) => registerLens(graph, from, to, lens), 60 | initLensGraph() 61 | ) 62 | 63 | const lens = lensFromTo(graph, 'V1', 'V3') 64 | assert.deepEqual(lens, [...LensV1toV2, ...LensV2toV3]) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "skipLibCheck": true, 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "lib": ["es2015", "es2017", "es2019", "dom", "webworker"], 10 | "target": "es2016", 11 | "module": "CommonJS", 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "jsx": "react", 15 | "pretty": true, 16 | "experimentalDecorators": true, 17 | "resolveJsonModule": true 18 | }, 19 | "parserOptions": { 20 | "tsconfigRootDir": "." 21 | }, 22 | "include": ["src/**/*.ts"] 23 | } 24 | --------------------------------------------------------------------------------