Skip to content

Commit b5e9baf

Browse files
committed
overlay: replace jsonpath by the more recent (and typed) jsonpathly
The currently used `jsonpath` lib is pretty old and unmaintained (last commits 4 years ago), and even more problematic it causes issues to [upgrade our github action](bump-sh/github-action#508) as that lib has an inconsistant use of 'require' and doesn't seem to work well with the packaging step (rollup) of the github action. Hopefully this upgrade (and change of lib) should let us a clean upgrade of the GH action to an ESM version 🤞
1 parent 2061959 commit b5e9baf

File tree

3 files changed

+113
-179
lines changed

3 files changed

+113
-179
lines changed

package-lock.json

+20-128
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"axios": "^1.7.7",
102102
"chalk": "^5.3.0",
103103
"debug": "^4.3.7",
104-
"jsonpath": "^1.1.1",
104+
"jsonpathly": "^2.0.2",
105105
"mergician": "^2.0.2",
106106
"oas-schemas": "git+https://git@github.com/OAI/OpenAPI-Specification.git#882d1caedb0bff825a1fd10728e7e3dc43912d37",
107107
"open": "^10.1.0"

src/core/overlay.ts

+92-50
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import debug from 'debug'
2-
import {JSONSchema4Object} from 'json-schema'
3-
/* eslint-disable-next-line import/default */
4-
import jsonpath from 'jsonpath'
2+
import {JSONSchema4Object, JSONSchema4Type} from 'json-schema'
3+
import * as jsonpath from 'jsonpathly'
54
import {mergician} from 'mergician'
65

76
import {APIDefinition, OpenAPIOverlay} from '../definition.js'
@@ -24,64 +23,107 @@ export class Overlay {
2423
// If you make any changes here, PLEASE ALSO MAKE THEM UPSTREAM.
2524
public run(spec: APIDefinition, overlay: OpenAPIOverlay): APIDefinition {
2625
// Use jsonpath.apply to do the changes
27-
if (overlay.actions && overlay.actions.length > 0)
26+
if (overlay.actions && overlay.actions.length > 0) {
2827
for (const a of overlay.actions) {
2928
const action = a as JSONSchema4Object
3029
if (!action.target) {
31-
process.stderr.write('Action with a missing target\n')
30+
process.stderr.write(`WARNING: ${this.humanName(action)} has an empty target\n`)
3231
continue
3332
}
3433

3534
const target = action.target as string
36-
// Is it a remove?
37-
if (Object.hasOwn(action, 'remove')) {
38-
/* eslint-disable-next-line no-constant-condition */
39-
while (true) {
40-
const path = jsonpath.paths(spec, target)
41-
if (path.length === 0) {
42-
break
43-
}
44-
45-
const parent = jsonpath.parent(spec, target)
46-
const thingToRemove = path[0].at(-1)
47-
if (thingToRemove !== undefined) {
48-
if (Array.isArray(parent)) {
49-
parent.splice(thingToRemove as number, 1)
50-
} else {
51-
delete parent[thingToRemove]
52-
}
53-
}
54-
}
55-
} else {
56-
try {
57-
// It must be an update
58-
// Deep merge objects using a module (built-in spread operator is only shallow)
59-
const merger = mergician({appendArrays: true})
60-
if (target === '$') {
61-
// You can't actually merge an update on a root object
62-
// target with the jsonpath lib, this is just us merging
63-
// the given update with the whole spec.
64-
spec = merger(spec, action.update)
65-
} else {
66-
jsonpath.apply(spec, target, (chunk) => {
67-
if (typeof chunk === 'object' && typeof action.update === 'object') {
68-
if (Array.isArray(chunk) && Array.isArray(action.update)) {
69-
return [...chunk, ...action.update]
70-
}
71-
72-
return merger(chunk, action.update)
73-
}
35+
// jsonpathly's paths are strings. They represent a “Path
36+
// expression” which represents the absolute path in the objet
37+
// tree reached by our `target`.
38+
//
39+
// E.g. '$["store"]["book"][0]["price"]'
40+
const paths = jsonpath.paths(spec, target)
41+
if (paths.length === 0) {
42+
process.stderr.write(`WARNING: Action target '${target}' has no matching elements\n`)
43+
continue
44+
}
7445

75-
return action.update
76-
})
77-
}
78-
} catch (error) {
79-
process.stderr.write(`Error applying overlay: ${(error as Error).message}\n`)
80-
// return chunk
81-
}
46+
for (const path of paths) {
47+
this.executeAction(spec, action, path)
8248
}
8349
}
50+
} else {
51+
process.stderr.write('WARNING: No actions found in your overlay\n')
52+
}
8453

8554
return spec
8655
}
56+
57+
private executeAction(spec: APIDefinition, action: JSONSchema4Object, path: string): void {
58+
const explodedPath: string[] = path.split(/(?:]\[|\$\[)+/)
59+
// Remove root
60+
explodedPath.shift()
61+
62+
// Take last element from path (which is the thing to act
63+
// upon)
64+
let thingToActUpon: number | string | undefined = explodedPath.pop()
65+
// The last element (e.g. '"price"]' or '0]') contains a final ']'
66+
// so we need to remove it AND we need to parse the element to
67+
// transform the string in either a string or a number
68+
thingToActUpon =
69+
thingToActUpon === undefined ? '$' : (thingToActUpon = JSON.parse(thingToActUpon.slice(0, -1)) as number | string)
70+
71+
// Reconstruct the stringified path expression targeting the parent
72+
const parentPath: string = explodedPath.join('][')
73+
const parent: JSONSchema4Object =
74+
parentPath.length > 0 ? (jsonpath.query(spec, `$[${parentPath}]`) as JSONSchema4Object) : spec
75+
76+
// Do the overlay action
77+
// Is it a remove?
78+
if (Object.hasOwn(action, 'remove')) {
79+
this.remove(parent, thingToActUpon)
80+
} else if (Object.hasOwn(action, 'update')) {
81+
this.update(spec, parent, action.update, thingToActUpon)
82+
} else {
83+
process.stderr.write(`WARNING: ${this.humanName(action)} needs either a 'remove' or an 'update' property\n`)
84+
}
85+
}
86+
87+
private humanName(action: JSONSchema4Object): string {
88+
return action.description ? `Action '${action.description}'` : 'Action'
89+
}
90+
91+
private remove(parent: JSONSchema4Object, property_or_index: number | string): void {
92+
if (Array.isArray(parent)) {
93+
parent.splice(property_or_index as number, 1)
94+
} else {
95+
delete parent[property_or_index]
96+
}
97+
}
98+
99+
private update(
100+
spec: APIDefinition,
101+
parent: JSONSchema4Object,
102+
update: JSONSchema4Type,
103+
property_or_index: number | string,
104+
): void {
105+
try {
106+
// Deep merge objects using a module (built-in spread operator is only shallow)
107+
const merger = mergician({appendArrays: true})
108+
if (property_or_index === '$') {
109+
// You can't actually merge an update on a root object
110+
// target with the jsonpath lib, this is just us merging
111+
// the given update with the whole spec.
112+
spec = merger(spec, update)
113+
} else if (property_or_index) {
114+
const targetObject = parent[property_or_index]
115+
116+
if (typeof targetObject === 'object' && typeof update === 'object') {
117+
parent[property_or_index] =
118+
Array.isArray(targetObject) && Array.isArray(update)
119+
? [...targetObject, ...update]
120+
: merger(targetObject, update)
121+
} else {
122+
parent[property_or_index] = update
123+
}
124+
}
125+
} catch (error) {
126+
process.stderr.write(`Error applying overlay: ${(error as Error).message}\n`)
127+
}
128+
}
87129
}

0 commit comments

Comments
 (0)