@json-schema-tools/traverse
Advanced tools
Comparing version 1.0.0 to 1.0.1
@@ -1,10 +0,10 @@ | ||
import { JSONSchema } from "@open-rpc/meta-schema"; | ||
import { CoreSchemaMetaSchema as JSONMetaSchema } from "@json-schema-tools/meta-schema"; | ||
/** | ||
* Signature of the mutation method passed to traverse. | ||
*/ | ||
export declare type MutationFunction = (schema: JSONSchema) => JSONSchema; | ||
export declare type MutationFunction = (schema: JSONMetaSchema) => JSONMetaSchema; | ||
/** | ||
* The options you can use when traversing. | ||
*/ | ||
export interface ITraverseOptions { | ||
export interface TraverseOptions { | ||
/** | ||
@@ -15,3 +15,3 @@ * Set this to true if you don't want to call the mutator function on the root schema. | ||
} | ||
export declare const defaultOptions: ITraverseOptions; | ||
export declare const defaultOptions: TraverseOptions; | ||
/** | ||
@@ -28,2 +28,2 @@ * Traverse all subschema of a schema, calling the mutator function with each. | ||
*/ | ||
export default function traverse(schema: JSONSchema, mutation: MutationFunction, traverseOptions?: ITraverseOptions, depth?: number, recursiveStack?: JSONSchema[], prePostMap?: Array<[JSONSchema, JSONSchema]>): JSONSchema; | ||
export default function traverse(schema: JSONMetaSchema, mutation: MutationFunction, traverseOptions?: TraverseOptions, depth?: number, recursiveStack?: JSONMetaSchema[], prePostMap?: Array<[JSONMetaSchema, JSONMetaSchema]>): Boolean | JSONMetaSchema; |
@@ -17,2 +17,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.defaultOptions = void 0; | ||
var lodash_merge_1 = __importDefault(require("lodash.merge")); | ||
@@ -45,2 +46,14 @@ exports.defaultOptions = { | ||
if (prePostMap === void 0) { prePostMap = []; } | ||
// booleans are a bit messed. Since all other schemas are objects (non-primitive type | ||
// which gets a new address in mem) for each new JS refer to one of 2 memory addrs, and | ||
// thus adding it to the recursive stack will prevent it from being explored if the | ||
// boolean is seen in a further nested schema. | ||
if (typeof schema === "boolean" || schema instanceof Boolean) { | ||
if (traverseOptions.skipFirstMutation === true && depth === 0) { | ||
return schema; | ||
} | ||
else { | ||
return mutation(schema); | ||
} | ||
} | ||
var mutableSchema = __assign({}, schema); | ||
@@ -69,31 +82,38 @@ recursiveStack.push(schema); | ||
} | ||
else if (schema.items) { | ||
if (schema.items instanceof Array) { | ||
mutableSchema.items = schema.items.map(rec); | ||
} | ||
else if (schema.items === true) { | ||
mutableSchema.items = mutation(schema.items); | ||
} | ||
else { | ||
var foundCycle_1 = isCycle(schema.items, recursiveStack); | ||
if (foundCycle_1) { | ||
var _a = prePostMap.find(function (_a) { | ||
var orig = _a[0]; | ||
return foundCycle_1 === orig; | ||
}), cycledMutableSchema = _a[1]; | ||
mutableSchema.items = cycledMutableSchema; | ||
else { | ||
var itemsIsSingleSchema = false; | ||
if (schema.items) { | ||
if (schema.items instanceof Array) { | ||
mutableSchema.items = schema.items.map(rec); | ||
} | ||
else { | ||
mutableSchema.items = traverse(schema.items, mutation, traverseOptions, depth + 1, recursiveStack, prePostMap); | ||
var foundCycle_1 = isCycle(schema.items, recursiveStack); | ||
if (foundCycle_1) { | ||
var _a = prePostMap.find(function (_a) { | ||
var orig = _a[0]; | ||
return foundCycle_1 === orig; | ||
}), cycledMutableSchema = _a[1]; | ||
mutableSchema.items = cycledMutableSchema; | ||
} | ||
else { | ||
itemsIsSingleSchema = true; | ||
mutableSchema.items = traverse(schema.items, mutation, traverseOptions, depth + 1, recursiveStack, prePostMap); | ||
} | ||
} | ||
} | ||
if (!!schema.additionalItems === true && !itemsIsSingleSchema) { | ||
mutableSchema.additionalItems = rec(schema.additionalItems); | ||
} | ||
if (schema.properties) { | ||
var sProps_1 = schema.properties; | ||
mutableSchema.properties = Object.keys(sProps_1) | ||
.reduce(function (r, v) { | ||
var _a; | ||
return (__assign(__assign({}, r), (_a = {}, _a[v] = rec(sProps_1[v]), _a))); | ||
}, {}); | ||
} | ||
if (!!schema.additionalProperties === true) { | ||
mutableSchema.additionalProperties = rec(schema.additionalProperties); | ||
} | ||
} | ||
else if (schema.properties) { | ||
var sProps_1 = schema.properties; | ||
mutableSchema.properties = Object.keys(sProps_1) | ||
.reduce(function (r, v) { | ||
var _a; | ||
return (__assign(__assign({}, r), (_a = {}, _a[v] = rec(sProps_1[v]), _a))); | ||
}, {}); | ||
} | ||
if (traverseOptions.skipFirstMutation === true && depth === 0) { | ||
@@ -100,0 +120,0 @@ return mutableSchema; |
@@ -28,8 +28,69 @@ "use strict"; | ||
} | ||
return mockMutation; | ||
}; | ||
["anyOf", "oneOf", "allOf"].forEach(function (prop) { | ||
it("traverses " + prop, function () { return test(prop); }); | ||
it("traverses " + prop, function () { | ||
test(prop); | ||
}); | ||
}); | ||
it("traverses items when items is ordered list", function () { return test("items"); }); | ||
it("traverses items when items constrained to single schema", function () { return test("items", { a: {}, b: {} }); }); | ||
it("traverses items when items is ordered list", function () { | ||
test("items"); | ||
}); | ||
it("traverses items when items constrained to single schema", function () { | ||
test("items", { a: {}, b: {} }); | ||
}); | ||
it("accepts boolean as a valid schema", function () { | ||
var testSchema = true; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(1); | ||
}); | ||
it("accepts boolean as valid schema in a nested schema", function () { | ||
var schema = { type: "object", properties: { a: true, b: false } }; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(3); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, false); | ||
expect(mockMutation).toHaveBeenNthCalledWith(3, schema); | ||
}); | ||
it("allows booleans that are created via boolean class and new", function () { | ||
var a = new Boolean(true); | ||
var b = new Boolean(false); | ||
var schema = { type: "object", properties: { a: a, b: b } }; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(3); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, a); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, b); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, false); | ||
expect(mockMutation).toHaveBeenNthCalledWith(3, schema); | ||
}); | ||
it("when items is a boolean works fine", function () { | ||
var schema = { type: "array", items: true }; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, schema); | ||
}); | ||
it("doesnt skip boolean schemas that it has not seen", function () { | ||
var schema = { | ||
type: "object", | ||
properties: { | ||
a: true, | ||
b: { | ||
properties: { | ||
c: true, | ||
d: { properties: { e: false } } | ||
} | ||
} | ||
} | ||
}; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(6); | ||
}); | ||
it("traverses properties", function () { | ||
@@ -49,2 +110,56 @@ var testSchema = { | ||
}); | ||
it("traverses additionalProperties as boolean", function () { | ||
var testSchema = { | ||
additionalProperties: true | ||
}; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("traverses additionalProperties as schema", function () { | ||
var testSchema = { | ||
additionalProperties: { | ||
properties: { | ||
c: {}, | ||
d: {}, | ||
}, | ||
}, | ||
}; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties.properties.c); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties.properties.d); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
it("traverses additionalItems as boolean", function () { | ||
var testSchema = { | ||
additionalItems: true | ||
}; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("traverses additionalItems as schema", function () { | ||
var testSchema = { | ||
additionalItems: { | ||
properties: { | ||
c: {}, | ||
d: {}, | ||
}, | ||
}, | ||
}; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems.properties.c); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems.properties.d); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
it("skips the first schema when the option skipFirstMutation is true", function () { | ||
@@ -57,3 +172,32 @@ var testSchema = { anyOf: [{}, {}] }; | ||
}); | ||
it("skips first mutation when schema is a bool", function () { | ||
var testSchema = true; | ||
var mockMutation = jest.fn(function (mockS) { return mockS; }); | ||
_1.default(testSchema, mockMutation, { skipFirstMutation: true }); | ||
expect(mockMutation).not.toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
describe("schema.type being an array", function () { | ||
it("allows type to be an array", function () { | ||
var schema = { type: ["boolean", "string"], title: "gotimebucko" }; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(1); | ||
}); | ||
it("array and or object", function () { | ||
var schema = { | ||
type: ["object", "array"], | ||
title: "gotimebucko", | ||
properties: { | ||
a: { type: "string" }, | ||
b: { type: "integer" } | ||
}, | ||
items: { type: "string" } | ||
}; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
}); | ||
describe("cycle detection", function () { | ||
@@ -67,2 +211,8 @@ it("handles basic cycles", function () { | ||
}); | ||
it("does not follow $refs", function () { | ||
var schema = { type: "object", properties: { foo: { $ref: "#" } } }; | ||
var mockMutation = jest.fn(function (s) { return s; }); | ||
_1.default(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("handles chained cycles", function () { | ||
@@ -69,0 +219,0 @@ var schema = { |
@@ -0,1 +1,14 @@ | ||
## [1.0.1](https://github.com/json-schema-tools/traverse/compare/1.0.0...1.0.1) (2020-06-19) | ||
### Bug Fixes | ||
* actually build the docs in ci ([94fdd63](https://github.com/json-schema-tools/traverse/commit/94fdd63230d6082db13d7b173bb391305a87b4ef)) | ||
* add $ref test ([#5](https://github.com/json-schema-tools/traverse/issues/5)) ([ccaed64](https://github.com/json-schema-tools/traverse/commit/ccaed643a8fbcd150c7c9b34677e3a031bdd4050)) | ||
* docs working now ([#45](https://github.com/json-schema-tools/traverse/issues/45)) ([c1b68db](https://github.com/json-schema-tools/traverse/commit/c1b68db66718ff4248180e65b730203f88c83d50)) | ||
* missing comma ([0cc337e](https://github.com/json-schema-tools/traverse/commit/0cc337e49bcf46e1a81916171bb4d0e0457cc382)) | ||
* move typedoc config to proper spot ([40a2f6f](https://github.com/json-schema-tools/traverse/commit/40a2f6f95b3bdeb2da3a4c5ee3b96509959d028f)) | ||
* update readme ([#46](https://github.com/json-schema-tools/traverse/issues/46)) ([8c370cb](https://github.com/json-schema-tools/traverse/commit/8c370cb802692a18c3f02c39d523ff5dea61a959)) | ||
* update to metaschema ([#3](https://github.com/json-schema-tools/traverse/issues/3)) ([95ba547](https://github.com/json-schema-tools/traverse/commit/95ba5477fdf0a3c8e05df819eadd1f4a8baa51d3)) | ||
# 1.0.0 (2020-01-28) | ||
@@ -2,0 +15,0 @@ |
{ | ||
"name": "@json-schema-tools/traverse", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "", | ||
@@ -10,5 +10,5 @@ "main": "build/index.js", | ||
"scripts": { | ||
"build": "tsc && typedoc --out docs && touch docs/.nojekyll ", | ||
"lint": "tslint --fix -p .", | ||
"test": "npm run lint && jest --coverage" | ||
"build": "tsc", | ||
"build:docs": "typedoc", | ||
"test": "jest --coverage" | ||
}, | ||
@@ -18,41 +18,15 @@ "author": "", | ||
"devDependencies": { | ||
"@open-rpc/meta-schema": "^1.6.0", | ||
"@types/jest": "^24.0.13", | ||
"@json-schema-tools/meta-schema": "^1.0.9", | ||
"@types/jest": "^26.0.0", | ||
"@types/lodash.merge": "^4.6.6", | ||
"@types/node": "^12.12.17", | ||
"jest": "^24.8.0", | ||
"ts-jest": "^24.1.0", | ||
"tslint": "^5.20.1", | ||
"typedoc": "^0.15.5", | ||
"typescript": "^3.7.3" | ||
"@types/node": "^14.0.13", | ||
"jest": "^24.9.0", | ||
"ts-jest": "^24.3.0", | ||
"typedoc": "^0.17.7", | ||
"typescript": "^3.9.5" | ||
}, | ||
"dependencies": { | ||
"@json-schema-tools/meta-schema": "^1.0.3", | ||
"lodash.merge": "^4.6.2" | ||
}, | ||
"typedocOptions": { | ||
"name": "@json-schema-tools/traverse", | ||
"mode": "file", | ||
"src": [ | ||
"./src/index.ts" | ||
], | ||
"out": "docs", | ||
"includeDeclarations": true, | ||
"excludeNotExported": true, | ||
"excludePrivate": true, | ||
"excludeProtected": true, | ||
"hideGenerator": true, | ||
"listInvalidSymbolLinks": true, | ||
"readme": "./README.md", | ||
"exclude": [ | ||
"node_modules", | ||
"lib", | ||
"docs", | ||
"build", | ||
"jest.config.js", | ||
"src/*.test.ts" | ||
], | ||
"toc": [ | ||
"traverse" | ||
] | ||
} | ||
} |
@@ -1,53 +0,54 @@ | ||
# Pristine Typescript | ||
# JSON Schema Traverse | ||
A typescript open source repository in its original condition. | ||
## Features | ||
Pristine Typescript a fork of [Pristine](https://github.com/etclabscore/pristine). | ||
- circular reference detection & handling | ||
- synchronous - doesn't touch the filesystem or make network requests. | ||
- easily perform schema mutations while traversing | ||
- | ||
There are a lack of repositories to start from to build community driven open source projects. Pristine Typescript is a complete starting point, it follows a Documentation Driven Development approach, and can be used as a resource to augment existing documentation. | ||
## Getting Started | ||
## How to use Pristine in your project | ||
```sh | ||
npm install @json-schema-tools/traverse | ||
``` | ||
There are 2 options for using pristine with your project. | ||
1. Fork this repo as the start of your own, OR | ||
2. [follow these instructions](https://thoughts.t37.net/merging-2-different-git-repositories-without-losing-your-history-de7a06bba804) to use it on an existing repository. | ||
```js | ||
const traverse = require("@json-schema-tools/traverse").default; | ||
//import traverse from "@json-schema-tools/traverse" | ||
## Documentation Driven Development | ||
const mySchema = { | ||
title: "baz", | ||
type: "object", | ||
properties: { | ||
foo: { | ||
title: "foo", | ||
type: "array", | ||
items: { type: "string" } | ||
}, | ||
bar: { | ||
title: "bar", | ||
anyOf: [ | ||
{ title: "stringerific", type: "string" }, | ||
{ title: "numberoo", type: "number" } | ||
] | ||
} | ||
} | ||
}; | ||
There are many ways to drive open source development. Documenting the problem in the README gives a middle ground between technical and non-technical specifications. This allows organizing solutions to this challenge around community and documentation. | ||
traverse(mySchema, (schemaOrSubschema) => { | ||
console.log(schemaOrSubschema.title); | ||
}); | ||
``` | ||
> [...] a beautifully crafted library with no documentation is also damn near worthless. If your software solves the wrong problem or nobody can figure out how to use it, there’s something very bad going on. | ||
## Features Todo | ||
- [Readme Driven Development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) by Tom Preson-Werner | ||
- unevaluatedItems (https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.3.1.3) | ||
- unevaluatedProperties (https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.3.2.4) | ||
- contains (https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.3.1.4) | ||
- propertyNames (https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.3.2.5) | ||
- | ||
### Conventions and Specifications | ||
This repository has some strong opinions built in like circleci, semantic-release, npm. So feel free to fork and change it at your own discretion. It is only meant to be a starting point. That being said: | ||
Using conventions, documentation and specifications make it easier to: | ||
- communicate the problem you are solving | ||
- ease onboarding | ||
- build and use composable tools | ||
- promote open source contribution and engagement | ||
- promote issue and feature discussion on Github itself | ||
#### Resources | ||
- [Pristine](https://github.com/etclabscore/pristine) | ||
- [opensource.guide](https://opensource.guide/) | ||
- [Github community profiles for public repositories](https://help.github.com/articles/about-community-profiles-for-public-repositories/) | ||
- [Readme Driven Development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) | ||
- [pengwynn/flint](https://github.com/pengwynn/flint) | ||
- [Working Backwards](https://www.allthingsdistributed.com/2006/11/working_backwards.html) | ||
- [Literate programming](https://en.wikipedia.org/wiki/Literate_programming) | ||
- [Hammock Driven Development](https://www.youtube.com/watch?v=f84n5oFoZBc) | ||
- [Inversion and The Power of Avoiding Stupidity](https://fs.blog/2013/10/inversion/) | ||
- [choosealicense.com](http://choosealicense.com) | ||
## Getting Started | ||
To get started, [fork](https://help.github.com/articles/fork-a-repo/) or [duplicate](https://help.github.com/articles/duplicating-a-repository/) the repository. Then edit this file and delete everything above this line. | ||
### Contributing | ||
How to contribute, build and release are outlined in [CONTRIBUTING.md](CONTRIBUTING.md), [BUILDING.md](BUILDING.md) and [RELEASING.md](RELEASING.md) respectively. Commits in this repository follow the [CONVENTIONAL_COMMITS.md](CONVENTIONAL_COMMITS.md) specification. |
import traverse from "./"; | ||
import { JSONSchema } from "@open-rpc/meta-schema"; | ||
import { CoreSchemaMetaSchema as JSONSchema, CoreSchemaMetaSchema } from "@json-schema-tools/meta-schema"; | ||
@@ -29,10 +29,84 @@ describe("traverse", () => { | ||
} | ||
return mockMutation; | ||
}; | ||
["anyOf", "oneOf", "allOf"].forEach((prop) => { | ||
it(`traverses ${prop}`, () => test(prop)); | ||
it(`traverses ${prop}`, () => { | ||
test(prop); | ||
}); | ||
}); | ||
it("traverses items when items is ordered list", () => test("items")); | ||
it("traverses items when items constrained to single schema", () => test("items", { a: {}, b: {} })); | ||
it("traverses items when items is ordered list", () => { | ||
test("items"); | ||
}); | ||
it("traverses items when items constrained to single schema", () => { | ||
test("items", { a: {}, b: {} }); | ||
}); | ||
it("accepts boolean as a valid schema", () => { | ||
const testSchema: any = true; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(1); | ||
}); | ||
it("accepts boolean as valid schema in a nested schema", () => { | ||
const schema = { type: "object", properties: { a: true, b: false } }; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(3); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, false); | ||
expect(mockMutation).toHaveBeenNthCalledWith(3, schema); | ||
}); | ||
it("allows booleans that are created via boolean class and new", () => { | ||
const a = new Boolean(true); | ||
const b = new Boolean(false); | ||
const schema = { type: "object", properties: { a, b } }; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(3); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, a); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, b); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, false); | ||
expect(mockMutation).toHaveBeenNthCalledWith(3, schema); | ||
}); | ||
it("when items is a boolean works fine", () => { | ||
const schema = { type: "array", items: true }; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
expect(mockMutation).toHaveBeenNthCalledWith(1, true); | ||
expect(mockMutation).toHaveBeenNthCalledWith(2, schema); | ||
}); | ||
it("doesnt skip boolean schemas that it has not seen", () => { | ||
const schema = { | ||
type: "object", | ||
properties: { | ||
a: true, | ||
b: { | ||
properties: { | ||
c: true, | ||
d: { properties: { e: false } } | ||
} | ||
} | ||
} | ||
}; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(6); | ||
}); | ||
it("traverses properties", () => { | ||
@@ -55,2 +129,68 @@ const testSchema: any = { | ||
it("traverses additionalProperties as boolean", () => { | ||
const testSchema: any = { | ||
additionalProperties: true | ||
}; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("traverses additionalProperties as schema", () => { | ||
const testSchema: any = { | ||
additionalProperties: { | ||
properties: { | ||
c: {}, | ||
d: {}, | ||
}, | ||
}, | ||
}; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties.properties.c); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalProperties.properties.d); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
it("traverses additionalItems as boolean", () => { | ||
const testSchema: any = { | ||
additionalItems: true | ||
}; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("traverses additionalItems as schema", () => { | ||
const testSchema: any = { | ||
additionalItems: { | ||
properties: { | ||
c: {}, | ||
d: {}, | ||
}, | ||
}, | ||
}; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems.properties.c); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema.additionalItems.properties.d); | ||
expect(mockMutation).toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
it("skips the first schema when the option skipFirstMutation is true", () => { | ||
@@ -65,4 +205,39 @@ const testSchema: any = { anyOf: [{}, {}] }; | ||
}); | ||
it("skips first mutation when schema is a bool", () => { | ||
const testSchema: any = true; | ||
const mockMutation = jest.fn((mockS) => mockS); | ||
traverse(testSchema, mockMutation, { skipFirstMutation: true }); | ||
expect(mockMutation).not.toHaveBeenCalledWith(testSchema); | ||
expect(mockMutation).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
describe("schema.type being an array", () => { | ||
it("allows type to be an array", () => { | ||
const schema = { type: ["boolean", "string"], title: "gotimebucko" }; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(1); | ||
}); | ||
it("array and or object", () => { | ||
const schema = { | ||
type: ["object", "array"], | ||
title: "gotimebucko", | ||
properties: { | ||
a: { type: "string" }, | ||
b: { type: "integer" } | ||
}, | ||
items: { type: "string" } | ||
}; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(4); | ||
}); | ||
}); | ||
describe("cycle detection", () => { | ||
@@ -77,2 +252,9 @@ it("handles basic cycles", () => { | ||
it("does not follow $refs", () => { | ||
const schema = { type: "object", properties: { foo: { $ref: "#" } } }; | ||
const mockMutation = jest.fn((s) => s); | ||
traverse(schema, mockMutation); | ||
expect(mockMutation).toHaveBeenCalledTimes(2); | ||
}); | ||
it("handles chained cycles", () => { | ||
@@ -221,3 +403,3 @@ const schema = { | ||
let i = 0; | ||
const result = traverse(schema, (s: JSONSchema) => { | ||
const result: CoreSchemaMetaSchema = traverse(schema, (s: JSONSchema) => { | ||
s.i = i; | ||
@@ -224,0 +406,0 @@ i += 1; |
103
src/index.ts
import merge from "lodash.merge"; | ||
import { JSONSchema } from "@open-rpc/meta-schema"; | ||
import { CoreSchemaMetaSchema as JSONMetaSchema } from "@json-schema-tools/meta-schema"; | ||
@@ -7,3 +7,3 @@ /** | ||
*/ | ||
export type MutationFunction = (schema: JSONSchema) => JSONSchema; | ||
export type MutationFunction = (schema: JSONMetaSchema) => JSONMetaSchema; | ||
@@ -13,3 +13,3 @@ /** | ||
*/ | ||
export interface ITraverseOptions { | ||
export interface TraverseOptions { | ||
/** | ||
@@ -21,7 +21,7 @@ * Set this to true if you don't want to call the mutator function on the root schema. | ||
export const defaultOptions: ITraverseOptions = { | ||
export const defaultOptions: TraverseOptions = { | ||
skipFirstMutation: false, | ||
}; | ||
const isCycle = (s: JSONSchema, recursiveStack: JSONSchema[]) => { | ||
const isCycle = (s: JSONMetaSchema, recursiveStack: JSONMetaSchema[]) => { | ||
const foundInRecursiveStack = recursiveStack.find((recSchema) => recSchema === s); | ||
@@ -46,10 +46,23 @@ if (foundInRecursiveStack) { | ||
export default function traverse( | ||
schema: JSONSchema, | ||
schema: JSONMetaSchema, | ||
mutation: MutationFunction, | ||
traverseOptions = defaultOptions, | ||
depth = 0, | ||
recursiveStack: JSONSchema[] = [], | ||
prePostMap: Array<[JSONSchema, JSONSchema]> = [], | ||
recursiveStack: JSONMetaSchema[] = [], | ||
prePostMap: Array<[JSONMetaSchema, JSONMetaSchema]> = [], | ||
) { | ||
const mutableSchema: JSONSchema = { ...schema }; | ||
// booleans are a bit messed. Since all other schemas are objects (non-primitive type | ||
// which gets a new address in mem) for each new JS refer to one of 2 memory addrs, and | ||
// thus adding it to the recursive stack will prevent it from being explored if the | ||
// boolean is seen in a further nested schema. | ||
if (typeof schema === "boolean" || schema instanceof Boolean) { | ||
if (traverseOptions.skipFirstMutation === true && depth === 0) { | ||
return schema; | ||
} else { | ||
return mutation(schema); | ||
} | ||
} | ||
const mutableSchema: JSONMetaSchema = { ...schema }; | ||
recursiveStack.push(schema); | ||
@@ -59,3 +72,3 @@ | ||
const rec = (s: JSONSchema) => { | ||
const rec = (s: JSONMetaSchema) => { | ||
const foundCycle = isCycle(s, recursiveStack); | ||
@@ -65,3 +78,3 @@ if (foundCycle) { | ||
([orig]) => foundCycle === orig, | ||
) as [JSONSchema, JSONSchema]; | ||
) as [JSONMetaSchema, JSONMetaSchema]; | ||
return cycledMutableSchema; | ||
@@ -80,2 +93,3 @@ } | ||
if (schema.anyOf) { | ||
@@ -87,32 +101,45 @@ mutableSchema.anyOf = schema.anyOf.map(rec); | ||
mutableSchema.oneOf = schema.oneOf.map(rec); | ||
} else if (schema.items) { | ||
if (schema.items instanceof Array) { | ||
mutableSchema.items = schema.items.map(rec); | ||
} else if (schema.items as any === true) { | ||
mutableSchema.items = mutation(schema.items); | ||
} else { | ||
const foundCycle = isCycle(schema.items, recursiveStack); | ||
if (foundCycle) { | ||
const [, cycledMutableSchema] = prePostMap.find( | ||
([orig]) => foundCycle === orig, | ||
) as [JSONSchema, JSONSchema]; | ||
mutableSchema.items = cycledMutableSchema; | ||
} else { | ||
let itemsIsSingleSchema = false; | ||
if (schema.items) { | ||
if (schema.items instanceof Array) { | ||
mutableSchema.items = schema.items.map(rec); | ||
} else { | ||
mutableSchema.items = traverse( | ||
schema.items, | ||
mutation, | ||
traverseOptions, | ||
depth + 1, | ||
recursiveStack, | ||
prePostMap, | ||
); | ||
const foundCycle = isCycle(schema.items, recursiveStack); | ||
if (foundCycle) { | ||
const [, cycledMutableSchema] = prePostMap.find( | ||
([orig]) => foundCycle === orig, | ||
) as [JSONMetaSchema, JSONMetaSchema]; | ||
mutableSchema.items = cycledMutableSchema; | ||
} else { | ||
itemsIsSingleSchema = true; | ||
mutableSchema.items = traverse( | ||
schema.items, | ||
mutation, | ||
traverseOptions, | ||
depth + 1, | ||
recursiveStack, | ||
prePostMap, | ||
); | ||
} | ||
} | ||
} | ||
} else if (schema.properties) { | ||
const sProps: { [key: string]: JSONSchema } = schema.properties; | ||
mutableSchema.properties = Object.keys(sProps) | ||
.reduce( | ||
(r: JSONSchema, v: string) => ({ ...r, ...{ [v]: rec(sProps[v]) } }), | ||
{}, | ||
); | ||
if (!!schema.additionalItems === true && !itemsIsSingleSchema) { | ||
mutableSchema.additionalItems = rec(schema.additionalItems); | ||
} | ||
if (schema.properties) { | ||
const sProps: { [key: string]: JSONMetaSchema } = schema.properties; | ||
mutableSchema.properties = Object.keys(sProps) | ||
.reduce( | ||
(r: JSONMetaSchema, v: string) => ({ ...r, ...{ [v]: rec(sProps[v]) } }), | ||
{}, | ||
); | ||
} | ||
if (!!schema.additionalProperties === true) { | ||
mutableSchema.additionalProperties = rec(schema.additionalProperties); | ||
} | ||
} | ||
@@ -119,0 +146,0 @@ |
@@ -12,4 +12,5 @@ { | ||
"strict": true, | ||
"esModuleInterop": true | ||
"esModuleInterop": true, | ||
"resolveJsonModule": true | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
83845
8
27
1082
55
2
+ Added@json-schema-tools/meta-schema@1.7.5(transitive)