Skip to content

Commit 0483fb4

Browse files
authored
feat: implement reparentBundleTarget function (#96)
* feat: implement reparentBundleTarget function * chore: dedupe yarn.lock * chore: add a comment * chore: export from main entrypoint
1 parent 574422b commit 0483fb4

File tree

5 files changed

+317
-15
lines changed

5 files changed

+317
-15
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
"test.watch": "yarn test --watch"
3939
},
4040
"dependencies": {
41-
"@stoplight/ordered-object-literal": "^1.0.1",
42-
"@stoplight/types": "^12.2.0",
41+
"@stoplight/ordered-object-literal": "^1.0.2",
42+
"@stoplight/types": "^12.3.0",
4343
"jsonc-parser": "~2.2.1",
44-
"lodash": "^4.17.15",
44+
"lodash": "^4.17.21",
4545
"safe-stable-stringify": "^1.1"
4646
},
4747
"devDependencies": {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { reparentBundleTarget } from '../reparentBundleTarget';
2+
3+
describe('reparentBundleTarget', () => {
4+
it.each<[string, string]>([['#', '#/components'], ['#/components', '#'], ['#/components/schemas', '#/components']])(
5+
'given %p paths, should throw',
6+
(from, to) => {
7+
expect(reparentBundleTarget.bind(null, {}, from, to)).toThrow();
8+
},
9+
);
10+
11+
it('should reparent refs', () => {
12+
const document = {
13+
properties: {
14+
user: {
15+
$ref: '#/definitions/User',
16+
},
17+
},
18+
definitions: {
19+
Name: {
20+
type: 'string',
21+
},
22+
Admin: {
23+
properties: {
24+
name: {
25+
$ref: '#/definitions/Name',
26+
},
27+
},
28+
},
29+
Editor: {
30+
properties: {
31+
name: {
32+
$ref: '#/definitions/Name',
33+
},
34+
},
35+
},
36+
Users: {
37+
oneOf: [
38+
{
39+
$ref: '#/definitions/Admin',
40+
},
41+
{
42+
$ref: '#/definitions/Editor',
43+
},
44+
],
45+
},
46+
},
47+
};
48+
49+
reparentBundleTarget(document, '#/definitions', '#/$defs');
50+
51+
expect(document).toStrictEqual({
52+
properties: {
53+
user: {
54+
$ref: '#/$defs/User',
55+
},
56+
},
57+
$defs: {
58+
Name: {
59+
type: 'string',
60+
},
61+
Admin: {
62+
properties: {
63+
name: {
64+
$ref: '#/$defs/Name',
65+
},
66+
},
67+
},
68+
Editor: {
69+
properties: {
70+
name: {
71+
$ref: '#/$defs/Name',
72+
},
73+
},
74+
},
75+
Users: {
76+
oneOf: [
77+
{
78+
$ref: '#/$defs/Admin',
79+
},
80+
{
81+
$ref: '#/$defs/Editor',
82+
},
83+
],
84+
},
85+
},
86+
});
87+
});
88+
89+
it('given missing source, should do nothing', () => {
90+
const document = {
91+
properties: {
92+
user: {
93+
$ref: '#/definitions/User',
94+
},
95+
},
96+
definitions: {},
97+
};
98+
99+
reparentBundleTarget(document, '#/components/schemas', '#/$defs');
100+
101+
expect(document).toStrictEqual({
102+
properties: {
103+
user: {
104+
$ref: '#/definitions/User',
105+
},
106+
},
107+
definitions: {},
108+
});
109+
});
110+
111+
it('given invalid source, should do nothing', () => {
112+
let document: Record<string, unknown> = {
113+
properties: {
114+
user: {
115+
$ref: '#/definitions/User',
116+
},
117+
},
118+
components: null,
119+
};
120+
121+
reparentBundleTarget(document, '#/components/schemas', '#/$defs');
122+
123+
expect(document).toStrictEqual({
124+
properties: {
125+
user: {
126+
$ref: '#/definitions/User',
127+
},
128+
},
129+
components: null,
130+
});
131+
132+
document = {
133+
properties: {
134+
user: {
135+
$ref: '#/definitions/User',
136+
},
137+
},
138+
components: {
139+
schemas: null,
140+
},
141+
};
142+
143+
reparentBundleTarget(document, '#/components/schemas', '#/$defs');
144+
145+
expect(document).toStrictEqual({
146+
properties: {
147+
user: {
148+
$ref: '#/definitions/User',
149+
},
150+
},
151+
components: {
152+
schemas: null,
153+
},
154+
});
155+
});
156+
157+
it('given existing target, should do nothing', () => {
158+
const document = {
159+
properties: {
160+
user: {
161+
$ref: '#/definitions/User',
162+
},
163+
},
164+
$defs: {},
165+
definitions: {
166+
Name: {
167+
type: 'string',
168+
},
169+
Admin: {
170+
properties: {
171+
name: {
172+
$ref: '#/definitions/Name',
173+
},
174+
},
175+
},
176+
},
177+
};
178+
179+
reparentBundleTarget(document, '#/definitions', '#/$defs');
180+
181+
expect(document).toStrictEqual({
182+
properties: {
183+
user: {
184+
$ref: '#/definitions/User',
185+
},
186+
},
187+
$defs: {},
188+
definitions: {
189+
Name: {
190+
type: 'string',
191+
},
192+
Admin: {
193+
properties: {
194+
name: {
195+
$ref: '#/definitions/Name',
196+
},
197+
},
198+
},
199+
},
200+
});
201+
});
202+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './parseWithPointers';
1717
export * from './pathToPointer';
1818
export * from './pointerToPath';
1919
export * from './renameObjectKey';
20+
export * from './reparentBundleTarget';
2021
export * from './resolveInlineRef';
2122
export * from './safeParse';
2223
export * from './safeStringify';

src/reparentBundleTarget.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { isLocalRef } from './isLocalRef';
2+
import { isPlainObject } from './isPlainObject';
3+
import { pointerToPath } from './pointerToPath';
4+
5+
function isObject(maybeObj: unknown): maybeObj is Record<string, unknown> | unknown[] {
6+
return isPlainObject(maybeObj) || Array.isArray(maybeObj);
7+
}
8+
9+
/**
10+
* reparentBundleTarget - the function provides a way to change the main root of all $refs.
11+
* To illustrate the example, let's say you have a JSON Schema Draft 7 model that uses "definitions" and you'd like to move all these $refs to "$defs"
12+
* {
13+
* "type": "object",
14+
* "properties": {
15+
* "user": {
16+
* "$ref": "#/definitions/User"
17+
* }
18+
* },
19+
* "definitions": {
20+
* "User": {
21+
* "type": "object"
22+
* }
23+
* }
24+
* }
25+
* reparentBundleTarget(document, '#/definitions', '#/$defs'); // this **MUTATES** the data, so make sure to make a copy of it if you don't want your data to be lost
26+
* {
27+
* "type": "object"
28+
* "properties": {
29+
* "user": {
30+
* "$ref": "#/$defs/User"
31+
* }
32+
* },
33+
* "$defs": {
34+
* "User": {
35+
* "type": "object"
36+
* }
37+
* }
38+
*
39+
* @param document - the input document, i.e. a JSON Schema model, or a OAS document
40+
* @param from - the root to move from
41+
* @param to - the root to migrate to
42+
*/
43+
export function reparentBundleTarget(document: Record<string, unknown>, from: string, to: string): void {
44+
if (to.length <= 1 || from.length <= 1) {
45+
throw Error('Source/target path must not be empty and point at root');
46+
}
47+
48+
if (from.indexOf(to) === 0) {
49+
throw Error('Target path cannot be contained within source');
50+
}
51+
52+
const sourcePath = pointerToPath(from);
53+
let value: unknown = document;
54+
for (const segment of sourcePath) {
55+
if (!isObject(value)) {
56+
return;
57+
}
58+
59+
value = value[segment];
60+
}
61+
62+
if (!isObject(value)) {
63+
return;
64+
}
65+
66+
const targetPath = pointerToPath(to);
67+
let newTarget: unknown = document;
68+
for (const [i, segment] of targetPath.entries()) {
69+
if (!isObject(newTarget) || segment in newTarget) {
70+
return;
71+
}
72+
73+
const newValue = i === targetPath.length - 1 ? value : {};
74+
newTarget[segment] = newValue;
75+
newTarget = newValue;
76+
}
77+
78+
delete document[sourcePath[0]];
79+
_reparentBundleTarget(document, from, to);
80+
}
81+
82+
function _reparentBundleTarget(document: Record<string, unknown> | unknown[], from: string, to: string): void {
83+
for (const key of Object.keys(document)) {
84+
const value = document[key];
85+
86+
if (key === '$ref') {
87+
if (typeof value !== 'string' || !isLocalRef(value)) continue;
88+
if (value.indexOf(from) === 0) {
89+
document[key] = value.replace(from, to);
90+
}
91+
92+
continue;
93+
}
94+
95+
if (isObject(value)) {
96+
_reparentBundleTarget(value, from, to);
97+
}
98+
}
99+
}

yarn.lock

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -792,10 +792,10 @@
792792
lodash "^4.17.4"
793793
read-pkg-up "^7.0.0"
794794

795-
"@stoplight/ordered-object-literal@^1.0.1":
796-
version "1.0.1"
797-
resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.1.tgz#01ece81ba5dda199ca3dc5ec7464691efa5d5b76"
798-
integrity sha512-kDcBIKwzAXZTkgzaiPXH2I0JXavBkOb3jFzYNFS5cBuvZS3s/K+knpk2wLVt0n8XrnRQsSffzN6XG9HqUhfq6Q==
795+
"@stoplight/ordered-object-literal@^1.0.2":
796+
version "1.0.2"
797+
resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz#2a88a5ebc8b68b54837ac9a9ae7b779cdd862062"
798+
integrity sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==
799799

800800
"@stoplight/scripts@^7.0.4":
801801
version "7.0.4"
@@ -827,10 +827,10 @@
827827
shelljs "0.8.x"
828828
tslib "1.9.3"
829829

830-
"@stoplight/types@^12.2.0":
831-
version "12.2.0"
832-
resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-12.2.0.tgz#258c9f1bbc6682aa5ca1a4161d5e75979ffe4a3f"
833-
integrity sha512-k1VPgSIpP+wLzGJ14lxhwtSyzYG5az+qby7fzYpc2fGWYFe5apmDm26vf/6N4mMQi2UQ7KxgGfNRSjdu6Msslg==
830+
"@stoplight/types@^12.3.0":
831+
version "12.3.0"
832+
resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-12.3.0.tgz#ac71d295319f26abb279e3d89d1c1774857d20b4"
833+
integrity sha512-hgzUR1z5BlYvIzUeFK5pjs5JXSvEutA9Pww31+dVicBlunsG1iXopDx/cvfBY7rHOrgtZDuvyeK4seqkwAZ6Cg==
834834
dependencies:
835835
"@types/json-schema" "^7.0.4"
836836
utility-types "^3.10.0"
@@ -4973,10 +4973,10 @@ lodash@4.17.11:
49734973
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
49744974
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
49754975

4976-
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1:
4977-
version "4.17.19"
4978-
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
4979-
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
4976+
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.1:
4977+
version "4.17.21"
4978+
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
4979+
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
49804980

49814981
log-symbols@^1.0.2:
49824982
version "1.0.2"

0 commit comments

Comments
 (0)