Skip to content

Commit

Permalink
feat: support booleanish schemas & represent additional{Items,Propert…
Browse files Browse the repository at this point in the history
…ies} (#31)

BREAKING CHANGE: additionalProperties/additionalItems keywords are now processed as schemas
BREAKING CHANGE: true/false schemas are now represented
  • Loading branch information
P0lip authored Jan 23, 2024
1 parent 44abda7 commit 8300b12
Show file tree
Hide file tree
Showing 28 changed files with 317 additions and 59 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
rootDir: process.cwd(),
testEnvironment: 'node',
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['./setupTests.ts'],
testMatch: ['<rootDir>/src/**/__tests__/*.(ts|js)?(x)'],
transform: {
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/arrays/additional-empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "array",
"additionalItems": {}
}
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/arrays/additional-false.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "array",
"additionalItems": false
}
11 changes: 11 additions & 0 deletions src/__tests__/__fixtures__/arrays/additional-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "array",
"additionalItems": {
"type": "object",
"properties": {
"baz": {
"type": "number"
}
}
}
}
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/arrays/additional-true.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "array",
"additionalItems": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
"type": "string"
}
},
"required": [
"code",
"msg"
]
"required": ["code", "msg"]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
"type": "string"
}
},
"required": [
"code",
"msg"
]
"required": ["code", "msg"]
}
]
}
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/objects/additional-empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "object",
"additionalProperties": {}
}
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/objects/additional-false.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "object",
"additionalProperties": false
}
11 changes: 11 additions & 0 deletions src/__tests__/__fixtures__/objects/additional-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"baz": {
"type": "number"
}
}
}
}
4 changes: 4 additions & 0 deletions src/__tests__/__fixtures__/objects/additional-true.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "object",
"additionalProperties": true
}
121 changes: 120 additions & 1 deletion src/__tests__/__snapshots__/tree.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,61 @@ exports[`SchemaTree output compound keywords given oneOf combiner placed next to
"
`;

exports[`SchemaTree output should generate valid tree for arrays/additional-empty.json 1`] = `
"└─ #
├─ types
│ └─ 0: array
├─ primaryType: array
└─ children
└─ 0
└─ #/additionalItems
"
`;

exports[`SchemaTree output should generate valid tree for arrays/additional-false.json 1`] = `
"└─ #
├─ types
│ └─ 0: array
├─ primaryType: array
└─ children
└─ 0
└─ #/additionalItems
└─ value: false
"
`;

exports[`SchemaTree output should generate valid tree for arrays/additional-schema.json 1`] = `
"└─ #
├─ types
│ └─ 0: array
├─ primaryType: array
└─ children
└─ 0
└─ #/additionalItems
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalItems/properties/baz
├─ types
│ └─ 0: number
└─ primaryType: number
"
`;

exports[`SchemaTree output should generate valid tree for arrays/additional-true.json 1`] = `
"└─ #
├─ types
│ └─ 0: array
├─ primaryType: array
└─ children
└─ 0
└─ #/additionalItems
└─ value: true
"
`;

exports[`SchemaTree output should generate valid tree for arrays/of-allofs.json 1`] = `
"└─ #
├─ types
Expand Down Expand Up @@ -823,7 +878,16 @@ exports[`SchemaTree output should generate valid tree for combiners/allOfs/neste
│ └─ #/properties/order
│ ├─ types
│ │ └─ 0: object
│ └─ primaryType: object
│ ├─ primaryType: object
│ └─ children
│ └─ 0
│ └─ #/properties/order/additionalProperties
│ ├─ types
│ │ └─ 0: string
│ ├─ primaryType: string
│ └─ enum
│ ├─ 0: ASC
│ └─ 1: DESC
└─ 7
└─ #/properties/nextToken
├─ types
Expand Down Expand Up @@ -1246,6 +1310,61 @@ exports[`SchemaTree output should generate valid tree for formats-schema.json 1`
"
`;

exports[`SchemaTree output should generate valid tree for objects/additional-empty.json 1`] = `
"└─ #
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalProperties
"
`;

exports[`SchemaTree output should generate valid tree for objects/additional-false.json 1`] = `
"└─ #
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalProperties
└─ value: false
"
`;

exports[`SchemaTree output should generate valid tree for objects/additional-schema.json 1`] = `
"└─ #
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalProperties
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalProperties/properties/baz
├─ types
│ └─ 0: number
└─ primaryType: number
"
`;

exports[`SchemaTree output should generate valid tree for objects/additional-true.json 1`] = `
"└─ #
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
└─ 0
└─ #/additionalProperties
└─ value: true
"
`;

exports[`SchemaTree output should generate valid tree for references/base.json 1`] = `
"└─ #
├─ types
Expand Down
28 changes: 28 additions & 0 deletions src/__tests__/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,34 @@ describe('SchemaTree', () => {
tree.root.children[0].children[1].annotations.description,
).toEqual('_Everyone_ ~hates~ loves caves');
});

it('should render true/false schemas', () => {
const schema = {
type: 'object',
properties: {
bear: true,
cave: false,
},
};

const tree = new SchemaTree(schema);
tree.populate();

expect(printTree(schema)).toMatchInlineSnapshot(`
"└─ #
├─ types
│ └─ 0: object
├─ primaryType: object
└─ children
├─ 0
│ └─ #/properties/bear
│ └─ value: true
└─ 1
└─ #/properties/cave
└─ value: false
"
`);
});
});

describe('position', () => {
Expand Down
29 changes: 19 additions & 10 deletions src/__tests__/utils/printTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { pathToPointer } from '@stoplight/json';
import type { Dictionary } from '@stoplight/types';
import * as treeify from 'treeify';

import { isMirroredNode, isReferenceNode, isRegularNode } from '../../guards';
import { isBooleanishNode, isMirroredNode, isReferenceNode, isRegularNode, isRootNode } from '../../guards';
import type { MirroredSchemaNode, ReferenceNode, RegularNode, SchemaNode } from '../../nodes';
import type { BooleanishNode } from '../../nodes/BooleanishNode';
import type { SchemaTreeOptions } from '../../tree';
import { SchemaTree } from '../../tree';
import type { SchemaFragment } from '../../types';
Expand Down Expand Up @@ -41,22 +42,30 @@ function printReferenceNode(node: ReferenceNode) {
};
}

function printBooleanishNode(node: BooleanishNode) {
return {
value: node.fragment,
};
}

function printMirrorNode(node: MirroredSchemaNode): any {
return {
mirrors: pathToPointer(node.mirroredNode.path as string[]),
};
}

function printNode(node: SchemaNode) {
return isMirroredNode(node)
? printMirrorNode(node)
: isRegularNode(node)
? printRegularNode(node)
: isReferenceNode(node)
? printReferenceNode(node)
: {
kind: 'unknown node',
};
if (isMirroredNode(node)) {
return printMirrorNode(node);
} else if (isRegularNode(node)) {
return printRegularNode(node);
} else if (isReferenceNode(node)) {
return printReferenceNode(node);
} else if (isBooleanishNode(node)) {
return printBooleanishNode(node);
} else if (isRootNode(node)) {
return {};
}
}

function prepareTree(node: SchemaNode) {
Expand Down
4 changes: 2 additions & 2 deletions src/accessors/getValidations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const VALIDATION_TYPES: Partial<Dictionary<(keyof SchemaFragment)[], SchemaNodeK
get integer() {
return this.number;
},
object: ['additionalProperties', 'minProperties', 'maxProperties'],
array: ['additionalItems', 'minItems', 'maxItems', 'uniqueItems'],
object: ['minProperties', 'maxProperties'],
array: ['minItems', 'maxItems', 'uniqueItems'],
};

function getTypeValidations(types: SchemaNodeKind[]): (keyof SchemaFragment)[] | null {
Expand Down
5 changes: 5 additions & 0 deletions src/guards/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RootNode,
SchemaNode,
} from '../nodes';
import type { BooleanishNode } from '../nodes/BooleanishNode';

export function isSchemaNode(node: unknown): node is SchemaNode {
const name = Object.getPrototypeOf(node).constructor.name;
Expand Down Expand Up @@ -34,3 +35,7 @@ export function isMirroredNode(node: SchemaNode): node is MirroredSchemaNode {
export function isReferenceNode(node: SchemaNode): node is ReferenceNode {
return 'external' in node && 'value' in node;
}

export function isBooleanishNode(node: SchemaNode): node is BooleanishNode {
return typeof node.fragment === 'boolean';
}
3 changes: 1 addition & 2 deletions src/nodes/BaseNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { SchemaFragment } from '../types';
import type { MirroredRegularNode } from './mirrored';
import type { RegularNode } from './RegularNode';
import type { RootNode } from './RootNode';
Expand Down Expand Up @@ -35,7 +34,7 @@ export abstract class BaseNode {
return this.pos === this.parentChildren.length - 1;
}

protected constructor(public readonly fragment: SchemaFragment) {
protected constructor() {
this.id = String(SEED++);
this.subpath = [];
}
Expand Down
7 changes: 7 additions & 0 deletions src/nodes/BooleanishNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseNode } from './BaseNode';

export class BooleanishNode extends BaseNode {
constructor(public readonly fragment: boolean) {
super();
}
}
4 changes: 2 additions & 2 deletions src/nodes/ReferenceNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { BaseNode } from './BaseNode';
export class ReferenceNode extends BaseNode {
public readonly value: string | null;

constructor(fragment: SchemaFragment, public readonly error: string | null) {
super(fragment);
constructor(public readonly fragment: SchemaFragment, public readonly error: string | null) {
super();

this.value = unwrapStringOrNull(fragment.$ref);
}
Expand Down
5 changes: 3 additions & 2 deletions src/nodes/RegularNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isDeprecated } from '../accessors/isDeprecated';
import { unwrapArrayOrNull, unwrapStringOrNull } from '../accessors/unwrap';
import type { SchemaFragment } from '../types';
import { BaseNode } from './BaseNode';
import type { BooleanishNode } from './BooleanishNode';
import type { ReferenceNode } from './ReferenceNode';
import { MirroredSchemaNode, SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from './types';

Expand All @@ -25,14 +26,14 @@ export class RegularNode extends BaseNode {
public readonly title: string | null;
public readonly deprecated: boolean;

public children: (RegularNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined;
public children: (RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined;

public readonly annotations: Readonly<Partial<Dictionary<unknown, SchemaAnnotations>>>;
public readonly validations: Readonly<Dictionary<unknown>>;
public readonly originalFragment: SchemaFragment;

constructor(public readonly fragment: SchemaFragment, context?: { originalFragment?: SchemaFragment }) {
super(fragment);
super();

this.$id = unwrapStringOrNull('id' in fragment ? fragment.id : fragment.$id);
this.types = getTypes(fragment);
Expand Down
Loading

0 comments on commit 8300b12

Please sign in to comment.