Skip to content

Commit 58a32ed

Browse files
author
oomilekh
committed
closes #1
1 parent 5a6fd59 commit 58a32ed

5 files changed

+220
-69
lines changed

.editorconfig

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# EditorConfig is awesome: https://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
# Unix-style newlines with a newline ending every file
7+
[*]
8+
end_of_line = lf
9+
insert_final_newline = true
10+
charset = utf-8
11+
12+
# Tab indentation (no size specified)
13+
[Makefile]
14+
indent_style = tab
15+
16+
[*.js]
17+
indent_style = space
18+
indent_size = 4
19+
20+
[*.{json,yml,yaml,sh,md}]
21+
indent_style = space
22+
indent_size = 2

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules
2+
.tmp/*

README.md

+168-40
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,134 @@
1-
# JYBID - json-yaml-bundle-inherit-dereference
2-
Bundling json and yaml documents + extending with JSON-Patch
1+
# JYBID
2+
**j**son **y**aml **b**undle **i**nherit **d**ereference ***
33

4-
## Bundle & Dereference
4+
This lib allows to bundle/dereference `json` and `yaml` documents and extend a document with [JSON-Patch](http://jsonpatch.com/)
5+
6+
## Purpose
7+
I was making some API docs in [OpenAPI](https://www.openapis.org/) format and met 2 troubles:
8+
1. one doc for whole API is too big
9+
2. docs sometimes extend one another: add some, remove some, change some. For example, when API version changed I added some URL request parameters, renamed some, and removed others
10+
3. when I have versions of API, how to get a list of changes?
11+
12+
So I wanted to:
13+
1. Use `json` and `yaml` config files simultaneously
14+
2. Split big files into pieces and being able to glue them back on reading
15+
3. Have some inheritance technique for files (single and splitted) that allows describe changes and get a resulting document
16+
17+
Needs 1 and 2 are solved by [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser)
18+
Need 3 could be solved by something like [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch)
19+
But all together it didn't work out of box, so I made a little patch to [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser) and added JSON-Patch compiler.
20+
And extended JSON-Patch selector for array elements
21+
22+
## Example
23+
We have weather forecast database for Russia and Finland and we supposed that cities in these countries always have different names so we dont need to specify country in request. Of course it is wrong idea, but just for example..
24+
```
25+
openapi: 3.0.1
26+
info:
27+
title: Readonly API for weather forecast
28+
description: >-
29+
Multiline
30+
description of service.
31+
version: 1.0.0
32+
servers:
33+
- url: 'https://weather.forecast/v1'
34+
paths:
35+
/city:
36+
get:
37+
summary: Get forecast for city by it name
38+
operationId: getForecastInCity
39+
parameters:
40+
- name: names
41+
in: query
42+
description: Latin city names
43+
required: true
44+
schema:
45+
type: array
46+
items:
47+
type: string
48+
default: Moscow
49+
responses:
50+
'200':
51+
description: successful operation
52+
content:
53+
application/json:
54+
schema:
55+
type: array
56+
items:
57+
$ref: '#/components/schemas/Forecast'
58+
'400':
59+
description: any error
60+
content: {}
61+
components:
62+
schemas:
63+
Forecast:
64+
$ref: ./forecast.json
65+
```
66+
Next month we add forecasts for Belarus and we have troubles now: for example a town named 'Kamenka' exists in Russia and in Belarus
67+
But we cant add parameter to v1 because some good people use our API in android app and it works ok in Russia and in Finland, and if we set 'default' country many requests will fail. Well, we could make some workarounds but.. it is just better to make correct next version of API.
68+
So we need to replace unclear parameter 'names' with 'cities' and add 'country', this is how we could do it with [jybid](#jybid)
69+
```
70+
$inherit:
71+
source:
72+
$ref: ./api.v1.yaml
73+
with:
74+
- op: 'replace'
75+
path: '/info/version'
76+
value: 2.0.0
77+
- op: 'replace'
78+
path: '/servers/0/url'
79+
value: 'https://weather.forecast/v2'
80+
- op: 'remove'
81+
path: '/paths/~1city/get/parameters/[name=names]'
82+
- op: 'add'
83+
path: '/paths/~1city/get/parameters/-'
84+
value:
85+
name: cities
86+
in: query
87+
description: Latin city names
88+
required: true
89+
schema:
90+
type: array
91+
items:
92+
type: string
93+
default: Moscow
94+
- op: 'add'
95+
path: '/paths/~1city/get/parameters/-'
96+
value:
97+
name: country
98+
in: query
99+
description: Latin country name
100+
required: true
101+
schema:
102+
type: string
103+
default: Russia
104+
105+
```
106+
107+
Referencing, bundling and inheritance is in keywords
108+
109+
$inherit, source, with
110+
111+
Array selector is in line
112+
113+
path: '/paths/~1city/get/parameters/[name=names]'
114+
115+
## Methods: Bundle & Dereference
116+
To bundle file's references
5117
**bundle(filepath, options) returns Promise**
118+
119+
To bundle and eliminate internal references
6120
**dereference(filepath, options) returns Promise**
7-
Both if `options.inherit` is passed compile json-patches, for example:
121+
122+
For both
123+
1. if `options.inherit==true` then json-patches will be compiled
124+
2. if `options.inherit` is a string then it will set a keyword instead of `"$inherit"` and if it is `"$patch"` then document syntax complies to [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch)
125+
8126
```
9127
const { bundle, dereference } = require('./index')
10128
const fs = require('fs')
11129
fs.writeFileSync('/tmp/a.json', JSON.stringify({
12-
a: 1,
13-
c: {$ref: '#/d'},
130+
a: 1,
131+
c: {$ref: '#/d'},
14132
d: 4
15133
}), {encoding: 'utf8'})
16134
fs.writeFileSync('/tmp/b.json', JSON.stringify({
@@ -21,72 +139,82 @@ fs.writeFileSync('/tmp/b.json', JSON.stringify({
21139
}), {encoding: 'utf8'})
22140
bundle('/tmp/a.json').then((doc) => {console.log(JSON.stringify(doc));})
23141
{"a":1,"c":{"$ref":"#/d"},"d":4}
142+
24143
bundle('/tmp/b.json', {inherit: true}).then((doc) => {console.log(JSON.stringify(doc));})
25144
{"a":1,"c":{"$ref":"#/d"},"d":4,"b":2}
26145
dereference('/tmp/b.json', {inherit: true}).then((doc) => {console.log(JSON.stringify(doc));})
27146
{"a":1,"c":4,"d":4,"b":2}
28147
```
29148

30-
## Better JSON-Patch - [*] path parts for array index
31-
In JSON-Patch **path** referencing array elements does not look good, which element do we remove here, you never know until you see **object**?
149+
## Selectors
150+
In [JSON-Patch](http://jsonpatch.com/) **path** must be a [JSON-Pointer](https://tools.ietf.org/html/rfc6901), but referencing array elements does not look good: `path: '/2'`. Which element do we remove here? You never know until you see **object**
32151
```
33152
object = [1,2,234]
34153
patch = [{op: 'remove', path: '/2'}]
35154
```
36-
We can make **path** better if we select element by its properties, not index, so we'd like to replace numbers with **selectors**, like in jquery
155+
We can make **path** look better if we select element by its properties or value instead of index in array, so we'd like to replace numbers with **selectors**, like in jquery
37156
```
157+
// patch with "selector" in path
38158
patch = [{op: 'remove', path: '/[=234]'}]
39-
/* we compile patch to receive standart */
159+
160+
// compiling patch
40161
compilePatchOps(object, patch)
162+
163+
// patch with JSON-Pointer path
41164
[{op: 'remove', path: '/2'}]
42165
```
43-
Inside `[*]` quotation is used:
44-
* `\"` to get `"`
45-
* `\\` to get `\`
46-
`/` is `/`, `~` is `~`, not `~1` and `~0` like in JSON-Pointer
47166

48-
### Selectors
49-
#### by property value pairs
167+
Note that inside selector **[\*]** quotation is used:
168+
* `\"` is `"`
169+
* `\\` is `\`
170+
* `/` is `/`
171+
* `~` is `~`, not `~1` and `~0` like in [JSON-Pointer](https://tools.ietf.org/html/rfc6901)
172+
173+
so, selectors are:
174+
175+
### by property=value pairs
50176
Value is treated as string if possible and as number if it can't be string
51-
`[prop=value]` == `{prop: 'value'}`
52-
`[prop="13"]` == `{prop: '13'}`
53-
`[prop=42]` == `{prop: 42}`
54-
To reference compicated property name
55-
`["string attr name"=name]` == `{'string attr name': 'name'}`
56-
57-
#### with property
58-
`[prop=]` == `{prop: 1}` or `{prop: 'a'}` or `{prop: null`
59-
`["string attr name"=]` for quoted property name
60-
but `[prop=null]` == `{prop: null}` and `[prop="null"]` == `{prop: "null"}`
61-
62-
#### values
63-
`[="name"]` == `'name'`
64-
`[=13]` == `13`
65-
66-
#### multiple conditions
177+
* `[prop=value]` == `{prop: 'value'}`
178+
* `[prop="13"]` == `{prop: '13'}`
179+
* `[prop=42]` == `{prop: 42}`
180+
* `["string attr name"=name]` == `{'string attr name': 'name'}` To reference compicated property name use double quotes
181+
182+
### with property
183+
* `[prop=]` == `{prop: 1}` or `{prop: 'a'}` or `{prop: null}`
184+
* `["string attr name"=]` for quoted property name
185+
* `[prop=null]` == `{prop: null}`
186+
* `[prop="null"]` == `{prop: "null"}`
187+
188+
### with value
189+
* `[="name"]` == `'name'`
190+
* `[=13]` == `13`
191+
192+
### multiple conditions
193+
It works like AND
67194
`[prop=name][date=]` == `{prop: 'name', date: 'anything'}`
68195

69-
### Compilation of [*] pathes to JSON-Pointer
196+
## Method: compilePatchOps
197+
To compile [JSON-Patch](http://jsonpatch.com/) with [selectors](#selectors) in pathes to [JSON-Patch](http://jsonpatch.com/) with [JSON-Pointer](https://tools.ietf.org/html/rfc6901) pathes
70198
**compilePatchOps(source, patch) returns Array**
199+
71200
When source document is bundled we check every patch operation:
72-
1. if it contains our `[*]` in **path**
73-
2. corresponding object in source is array
74-
then we replace it with equal operations with JSON-Pointer pathes, for example:
201+
1. if it contains selector in **path**
202+
2. corresponding object in **source** is array
203+
204+
then we replace it with equal operation with [JSON-Pointer](https://tools.ietf.org/html/rfc6901) path, for example:
205+
75206
```
76207
const { compilePatchOps } = require('./index')
77208
78209
compilePatchOps(
79-
{arr: [{a: 1}, {c: 2}, {c: {$ref: '#/d'}}, {d: 4}]},
210+
{arr: [{a: 1}, {c: 2}, {c: {$ref: '#/d'}}, {d: 4}]},
80211
[{op: 'replace', path: '/arr/[c=]', value: 2}]
81212
);
82213
[ { op: 'replace', path: '/arr/1', value: 2 },
83214
{ op: 'replace', path: '/arr/2', value: 2 } ]
84215
>
85216
```
86217

87-
## Notes
88-
If `options.inherit` is set to **$patch** document syntax complies to [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch)
89-
90218
## Thanks
91219
[json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser)
92220
[rfc6902](https://github.com/chbrown/rfc6902)

test/test_bundle.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const date = new Date();
88
const casesBundle = {
99
'bundle': {
1010
prepare: {
11-
'/tmp/a.json': {a: 1, b: {$ref: './b.json#/value'}},
12-
'/tmp/b.json': {value: 2, a: {$ref: './a.json'}}
11+
'.tmp/a.json': {a: 1, b: {$ref: './b.json#/value'}},
12+
'.tmp/b.json': {value: 2, a: {$ref: './a.json'}}
1313
},
14-
arguments: ['/tmp/b.json'],
14+
arguments: ['.tmp/b.json'],
1515
result: {
1616
"value": 2,
1717
"a": {
@@ -24,21 +24,21 @@ const casesBundle = {
2424
},
2525
'bundle + inherit': {
2626
prepare: {
27-
'/tmp/a.json': {a: 1, b: {$ref: '#/c'}, c: 3},
28-
'/tmp/b.json': {$inherit: {source: {$ref: './a.json'}}}
27+
'.tmp/a.json': {a: 1, b: {$ref: '#/c'}, c: 3},
28+
'.tmp/b.json': {$inherit: {source: {$ref: './a.json'}}}
2929
},
30-
arguments: ['/tmp/b.json', {inherit: true}],
30+
arguments: ['.tmp/b.json', {inherit: true}],
3131
result: {
3232
a: 1, b: {$ref: '#/c'}, c: 3
3333
}
3434
},
3535
'bundle + 2 * inherit': {
3636
prepare: {
37-
'/tmp/a.json': {a: 1, b: {$ref: '#/c'}, c: 3},
38-
'/tmp/b.json': {b: {$inherit: {source: {$ref: './a.json'}}}},
39-
'/tmp/c.json': {$inherit: {source: {$ref: './b.json'}}},
37+
'.tmp/a.json': {a: 1, b: {$ref: '#/c'}, c: 3},
38+
'.tmp/b.json': {b: {$inherit: {source: {$ref: './a.json'}}}},
39+
'.tmp/c.json': {$inherit: {source: {$ref: './b.json'}}},
4040
},
41-
arguments: ['/tmp/c.json', {inherit: true}],
41+
arguments: ['.tmp/c.json', {inherit: true}],
4242
result: {
4343
b: {a: 1, b: {$ref: '#/b/c'}, c: 3}
4444
}

0 commit comments

Comments
 (0)