Skip to content

Commit 48dde2e

Browse files
committed
feat: support booleanish schemas & additionalProperties
1 parent 44abda7 commit 48dde2e

22 files changed

+169
-31
lines changed

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
rootDir: process.cwd(),
33
testEnvironment: 'node',
4+
roots: ['<rootDir>/src'],
45
setupFilesAfterEnv: ['./setupTests.ts'],
56
testMatch: ['<rootDir>/src/**/__tests__/*.(ts|js)?(x)'],
67
transform: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type: array
2+
additionalItems: {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type: array
2+
additionalItems: false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: array
2+
additionalItems:
3+
type: object
4+
properties:
5+
baz:
6+
type: number
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type: array
2+
additionalItems: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "object",
3+
"additionalProperties": {}
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "object",
3+
"additionalProperties": false
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "object",
3+
"additionalProperties": {
4+
"type": "object",
5+
"properties": {
6+
"baz": {
7+
"type": "number"
8+
}
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "object",
3+
"additionalProperties": true
4+
}

src/__tests__/__snapshots__/tree.spec.ts.snap

+57-1
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,16 @@ exports[`SchemaTree output should generate valid tree for combiners/allOfs/neste
823823
│ └─ #/properties/order
824824
│ ├─ types
825825
│ │ └─ 0: object
826-
│ └─ primaryType: object
826+
│ ├─ primaryType: object
827+
│ └─ children
828+
│ └─ 0
829+
│ └─ #/properties/order/additionalProperties
830+
│ ├─ types
831+
│ │ └─ 0: string
832+
│ ├─ primaryType: string
833+
│ └─ enum
834+
│ ├─ 0: ASC
835+
│ └─ 1: DESC
827836
└─ 7
828837
└─ #/properties/nextToken
829838
├─ types
@@ -1246,6 +1255,53 @@ exports[`SchemaTree output should generate valid tree for formats-schema.json 1`
12461255
"
12471256
`;
12481257

1258+
exports[`SchemaTree output should generate valid tree for objects/additional-empty.json 1`] = `
1259+
"└─ #
1260+
├─ types
1261+
│ └─ 0: object
1262+
├─ primaryType: object
1263+
└─ children
1264+
└─ 0
1265+
└─ #/additionalProperties
1266+
"
1267+
`;
1268+
1269+
exports[`SchemaTree output should generate valid tree for objects/additional-false.json 1`] = `
1270+
"└─ #
1271+
├─ types
1272+
│ └─ 0: object
1273+
└─ primaryType: object
1274+
"
1275+
`;
1276+
1277+
exports[`SchemaTree output should generate valid tree for objects/additional-schema.json 1`] = `
1278+
"└─ #
1279+
├─ types
1280+
│ └─ 0: object
1281+
├─ primaryType: object
1282+
└─ children
1283+
└─ 0
1284+
└─ #/additionalProperties
1285+
├─ types
1286+
│ └─ 0: object
1287+
├─ primaryType: object
1288+
└─ children
1289+
└─ 0
1290+
└─ #/additionalProperties/properties/baz
1291+
├─ types
1292+
│ └─ 0: number
1293+
└─ primaryType: number
1294+
"
1295+
`;
1296+
1297+
exports[`SchemaTree output should generate valid tree for objects/additional-true.json 1`] = `
1298+
"└─ #
1299+
├─ types
1300+
│ └─ 0: object
1301+
└─ primaryType: object
1302+
"
1303+
`;
1304+
12491305
exports[`SchemaTree output should generate valid tree for references/base.json 1`] = `
12501306
"└─ #
12511307
├─ types

src/accessors/getValidations.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const VALIDATION_TYPES: Partial<Dictionary<(keyof SchemaFragment)[], SchemaNodeK
1212
get integer() {
1313
return this.number;
1414
},
15-
object: ['additionalProperties', 'minProperties', 'maxProperties'],
16-
array: ['additionalItems', 'minItems', 'maxItems', 'uniqueItems'],
15+
object: ['minProperties', 'maxProperties'],
16+
array: ['minItems', 'maxItems', 'uniqueItems'],
1717
};
1818

1919
function getTypeValidations(types: SchemaNodeKind[]): (keyof SchemaFragment)[] | null {

src/nodes/BaseNode.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { SchemaFragment } from '../types';
21
import type { MirroredRegularNode } from './mirrored';
32
import type { RegularNode } from './RegularNode';
43
import type { RootNode } from './RootNode';
@@ -35,7 +34,7 @@ export abstract class BaseNode {
3534
return this.pos === this.parentChildren.length - 1;
3635
}
3736

38-
protected constructor(public readonly fragment: SchemaFragment) {
37+
protected constructor() {
3938
this.id = String(SEED++);
4039
this.subpath = [];
4140
}

src/nodes/BooleanishNode.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { BaseNode } from './BaseNode';
2+
3+
export class BooleanishNode extends BaseNode {
4+
constructor(public readonly fragment: boolean) {
5+
super();
6+
}
7+
}

src/nodes/ReferenceNode.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { BaseNode } from './BaseNode';
77
export class ReferenceNode extends BaseNode {
88
public readonly value: string | null;
99

10-
constructor(fragment: SchemaFragment, public readonly error: string | null) {
11-
super(fragment);
10+
constructor(public readonly fragment: SchemaFragment, public readonly error: string | null) {
11+
super();
1212

1313
this.value = unwrapStringOrNull(fragment.$ref);
1414
}

src/nodes/RegularNode.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isDeprecated } from '../accessors/isDeprecated';
1010
import { unwrapArrayOrNull, unwrapStringOrNull } from '../accessors/unwrap';
1111
import type { SchemaFragment } from '../types';
1212
import { BaseNode } from './BaseNode';
13+
import type { BooleanishNode } from './BooleanishNode';
1314
import type { ReferenceNode } from './ReferenceNode';
1415
import { MirroredSchemaNode, SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from './types';
1516

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

28-
public children: (RegularNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined;
29+
public children: (RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined;
2930

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

3435
constructor(public readonly fragment: SchemaFragment, context?: { originalFragment?: SchemaFragment }) {
35-
super(fragment);
36+
super();
3637

3738
this.$id = unwrapStringOrNull('id' in fragment ? fragment.id : fragment.$id);
3839
this.types = getTypes(fragment);

src/nodes/RootNode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class RootNode extends BaseNode {
77
public readonly children: SchemaNode[];
88

99
constructor(public readonly fragment: SchemaFragment) {
10-
super(fragment);
10+
super();
1111
this.children = [];
1212
}
1313
}

src/nodes/mirrored/MirroredReferenceNode.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import type { SchemaFragment } from '../../types';
12
import { BaseNode } from '../BaseNode';
23
import type { ReferenceNode } from '../ReferenceNode';
34

45
export class MirroredReferenceNode extends BaseNode implements ReferenceNode {
6+
public readonly fragment: SchemaFragment;
7+
58
constructor(public readonly mirroredNode: ReferenceNode) {
6-
super(mirroredNode.fragment);
9+
super();
10+
this.fragment = mirroredNode.fragment;
711
}
812

913
get error() {

src/nodes/mirrored/MirroredRegularNode.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import type { Dictionary } from '@stoplight/types';
22

3-
import { isRegularNode } from '../../guards';
3+
import { isReferenceNode, isRegularNode } from '../../guards';
44
import type { SchemaFragment } from '../../types';
55
import { isNonNullable } from '../../utils';
66
import { BaseNode } from '../BaseNode';
7+
import { BooleanishNode } from '../BooleanishNode';
78
import type { ReferenceNode } from '../ReferenceNode';
89
import type { RegularNode } from '../RegularNode';
910
import type { SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from '../types';
1011
import { MirroredReferenceNode } from './MirroredReferenceNode';
1112

1213
export class MirroredRegularNode extends BaseNode implements RegularNode {
14+
public readonly fragment: SchemaFragment;
1315
public readonly $id!: string | null;
1416
public readonly types!: SchemaNodeKind[] | null;
1517
public readonly primaryType!: SchemaNodeKind | null;
@@ -28,10 +30,14 @@ export class MirroredRegularNode extends BaseNode implements RegularNode {
2830
public readonly simple!: boolean;
2931
public readonly unknown!: boolean;
3032

31-
private readonly cache: WeakMap<RegularNode | ReferenceNode, MirroredRegularNode | MirroredReferenceNode>;
33+
private readonly cache: WeakMap<
34+
RegularNode | BooleanishNode | ReferenceNode,
35+
MirroredRegularNode | BooleanishNode | MirroredReferenceNode
36+
>;
3237

3338
constructor(public readonly mirroredNode: RegularNode, context?: { originalFragment?: SchemaFragment }) {
34-
super(mirroredNode.fragment);
39+
super();
40+
this.fragment = mirroredNode.fragment;
3541
this.originalFragment = context?.originalFragment ?? mirroredNode.originalFragment;
3642

3743
this.cache = new WeakMap();
@@ -59,9 +65,9 @@ export class MirroredRegularNode extends BaseNode implements RegularNode {
5965

6066
private readonly _this: MirroredRegularNode;
6167

62-
private _children?: (MirroredRegularNode | MirroredReferenceNode)[];
68+
private _children?: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[];
6369

64-
public get children(): (MirroredRegularNode | MirroredReferenceNode)[] | null | undefined {
70+
public get children(): (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] | null | undefined {
6571
const referencedChildren = this.mirroredNode.children;
6672

6773
if (!isNonNullable(referencedChildren)) {
@@ -74,7 +80,7 @@ export class MirroredRegularNode extends BaseNode implements RegularNode {
7480
this._children.length = 0;
7581
}
7682

77-
const children: (MirroredRegularNode | MirroredReferenceNode)[] = this._children;
83+
const children: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] = this._children;
7884
for (const child of referencedChildren) {
7985
// this is to avoid pointing at nested mirroring
8086
const cached = this.cache.get(child);
@@ -84,7 +90,11 @@ export class MirroredRegularNode extends BaseNode implements RegularNode {
8490
continue;
8591
}
8692

87-
const mirroredChild = isRegularNode(child) ? new MirroredRegularNode(child) : new MirroredReferenceNode(child);
93+
const mirroredChild = isRegularNode(child)
94+
? new MirroredRegularNode(child)
95+
: isReferenceNode(child)
96+
? new MirroredReferenceNode(child)
97+
: new BooleanishNode(child.fragment);
8898

8999
mirroredChild.parent = this._this;
90100
mirroredChild.subpath = child.subpath;

src/nodes/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { BooleanishNode } from './BooleanishNode';
12
import type { MirroredReferenceNode } from './mirrored/MirroredReferenceNode';
23
import type { MirroredRegularNode } from './mirrored/MirroredRegularNode';
34
import type { ReferenceNode } from './ReferenceNode';
@@ -6,7 +7,7 @@ import type { RootNode } from './RootNode';
67

78
export type MirroredSchemaNode = MirroredRegularNode | MirroredReferenceNode;
89

9-
export type SchemaNode = RootNode | RegularNode | ReferenceNode | MirroredSchemaNode;
10+
export type SchemaNode = RootNode | RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode;
1011

1112
export enum SchemaNodeKind {
1213
Any = 'any',

src/utils/guards.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Dictionary } from '@stoplight/types';
22

3+
import type { SchemaFragment } from '../types';
4+
35
export function isStringOrNumber(value: unknown): value is number | string {
46
return typeof value === 'string' || typeof value === 'number';
57
}
@@ -23,3 +25,7 @@ export function isObjectLiteral(maybeObj: unknown): maybeObj is Dictionary<unkno
2325
export function isNonNullable<T = unknown>(maybeNullable: T): maybeNullable is NonNullable<T> {
2426
return maybeNullable !== void 0 && maybeNullable !== null;
2527
}
28+
29+
export function isValidSchemaFragment(maybeSchemaFragment: unknown): maybeSchemaFragment is SchemaFragment {
30+
return typeof maybeSchemaFragment === 'boolean' || isObjectLiteral(maybeSchemaFragment);
31+
}

src/walker/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type WalkingOptions = {
1313
};
1414

1515
export type WalkerSnapshot = {
16-
readonly fragment: SchemaFragment;
16+
readonly fragment: SchemaFragment | boolean;
1717
readonly depth: number;
1818
readonly schemaNode: RegularNode | RootNode;
1919
readonly path: string[];
@@ -23,7 +23,7 @@ export type WalkerHookAction = 'filter' | 'stepIn';
2323
export type WalkerHookHandler = (node: SchemaNode) => boolean;
2424

2525
export type WalkerNodeEventHandler = (node: SchemaNode) => void;
26-
export type WalkerFragmentEventHandler = (node: SchemaFragment) => void;
26+
export type WalkerFragmentEventHandler = (node: SchemaFragment | boolean) => void;
2727
export type WalkerErrorEventHandler = (ex: Error) => void;
2828

2929
export type WalkerEmitter = {

0 commit comments

Comments
 (0)