1
1
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'
5
4
import { mergician } from 'mergician'
6
5
7
6
import { APIDefinition , OpenAPIOverlay } from '../definition.js'
@@ -24,64 +23,107 @@ export class Overlay {
24
23
// If you make any changes here, PLEASE ALSO MAKE THEM UPSTREAM.
25
24
public run ( spec : APIDefinition , overlay : OpenAPIOverlay ) : APIDefinition {
26
25
// Use jsonpath.apply to do the changes
27
- if ( overlay . actions && overlay . actions . length > 0 )
26
+ if ( overlay . actions && overlay . actions . length > 0 ) {
28
27
for ( const a of overlay . actions ) {
29
28
const action = a as JSONSchema4Object
30
29
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` )
32
31
continue
33
32
}
34
33
35
34
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
+ }
74
45
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 )
82
48
}
83
49
}
50
+ } else {
51
+ process . stderr . write ( 'WARNING: No actions found in your overlay\n' )
52
+ }
84
53
85
54
return spec
86
55
}
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
+ }
87
129
}
0 commit comments