Skip to content

Commit e344f0f

Browse files
committed
examples
1 parent 650a1ad commit e344f0f

9 files changed

+751
-132
lines changed

README.md

+103-132
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,46 @@
11
# JYBID
2-
**j**son **y**aml **b**undle **i**nherit **d**ereference ***
2+
**j**son **y**aml **b**undle **i**nherit **d**ereference **
33

44
This lib allows to bundle/dereference `json` and `yaml` documents and extend a document with [JSON-Patch](http://jsonpatch.com/)
55

6+
[bundle](#bundle)
7+
[dereference](#dereference)
8+
[selectors](#selectors)
9+
[examples](#examples)
10+
611
## Purpose
7-
When I was writing some API docs in [OpenAPI](https://www.openapis.org/) format I found some troubles:
12+
* Split big documents
13+
* Not only reference but inherit one documents from another
14+
* Have differences between documents in form of [JSON-Patch](http://jsonpatch.com/)
15+
16+
Why does somebody want that?
17+
Well, when I was writing some API docs in [OpenAPI](https://www.openapis.org/) format I had troubles:
818
1. one doc for whole API is too big
9-
2. docs sometimes logically extend one another.
10-
3. when I have several API versions, how to get a list of changes between them? It would be great to have it in form of [JSON-Patch](http://jsonpatch.com/) and visualize [somehow](https://github.com/benjamine/jsondiffpatch)
19+
2. docs sometimes logically extend one another
20+
3. how to get diff between versions of API? It would be great to have [JSON-Patch](http://jsonpatch.com/) and visualize it [somehow](https://github.com/benjamine/jsondiffpatch)
1121

1222
So I wanted to:
1323
1. Use `json` and `yaml` config files simultaneously, being able to split big files into several smaller
14-
2. Have some inheritance technique
15-
16-
First part is fairly easy, it can be solved by [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser)
17-
Second part also has a solution like [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch)
18-
But all together it doesn't work out of box, so I made a little patch to [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser) that enables inheritance based on JSON-Patch syntax.
19-
Also I extended JSON-Pointer in case when we point to something in array and it is called [array selector](#selectors).
20-
21-
## Example of how can JYBID be used
22-
Suppose we have a weather forecast database and want to build a readonly service for it. We have data only 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 to our service. Of course this is not the best idea, but just for example..
23-
So, we have a sevice API
24-
25-
```
26-
openapi: 3.0.1
27-
info:
28-
title: Readonly API for weather forecast service
29-
description: >-
30-
Multiline
31-
description of service.
32-
version: 1.0.0
33-
servers:
34-
- url: 'https://weather.forecast/v1'
35-
paths:
36-
/city:
37-
get:
38-
summary: Get forecast for city by name
39-
operationId: getForecastInCity
40-
parameters:
41-
- name: names
42-
in: query
43-
description: Latin city names
44-
required: true
45-
schema:
46-
type: array
47-
items:
48-
type: string
49-
default: Moscow
50-
responses:
51-
'200':
52-
description: successful operation
53-
content:
54-
application/json:
55-
schema:
56-
type: array
57-
items:
58-
$ref: '#/components/schemas/Forecast'
59-
'400':
60-
description: any error
61-
content: {}
62-
components:
63-
schemas:
64-
Forecast:
65-
$ref: ./forecast.json
66-
```
67-
68-
Next month we add forecasts for Belarus and we have troubles now: for example a town named 'Kamenka' exists in Russia and in Belarus.
69-
Of course we need to add parameter `country` but we can't add it to v1 because some people already use our API in their app and it works ok in Russia and in Finland. If we add `country` with some default value many requests will fail. Well, we could make some workarounds based on city name being checked against list of all cities in our 3 countries but.. it is just better to make next version of API correct and ask our clients to use it instead of incorrect v1.
70-
So we need to replace unclear parameter 'names' with 'cities' and add 'country', this is how we could do it with [jybid](#jybid)
71-
72-
```
73-
$inherit:
74-
source:
75-
$ref: ./api.v1.yaml
76-
with:
77-
- op: 'replace'
78-
path: '/info/version'
79-
value: 2.0.0
80-
- op: 'replace'
81-
path: '/servers/0/url'
82-
value: 'https://weather.forecast/v2'
83-
- op: 'remove'
84-
path: '/paths/~1city/get/parameters/[name=names]'
85-
- op: 'add'
86-
path: '/paths/~1city/get/parameters/-'
87-
value:
88-
name: cities
89-
in: query
90-
description: Latin city names
91-
required: true
92-
schema:
93-
type: array
94-
items:
95-
type: string
96-
default: Moscow
97-
- op: 'add'
98-
path: '/paths/~1city/get/parameters/-'
99-
value:
100-
name: country
101-
in: query
102-
description: Latin country name
103-
required: true
104-
schema:
105-
type: string
106-
default: Russia
107-
```
108-
109-
Referencing, bundling and inheritance is in keywords
110-
111-
$inherit, source, with
112-
113-
Array selector is in line
24+
2. Have some inheritance technique based on JSON-Patch
11425

115-
path: '/paths/~1city/get/parameters/[name=names]'
26+
1 comes easy, it can be solved by [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser)
27+
2 also has a solution like [ajv-merge-patch](https://github.com/epoberezkin/ajv-merge-patch)
28+
But all together it doesn't work out of box.
29+
So I made a little patch to [json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser) that enables inheritance based on JSON-Patch syntax.
30+
Also I extended JSON-Pointer for array indexes [array selector](#selectors).
11631

117-
## Methods: Bundle & Dereference
118-
To bundle a file
119-
**bundle(filepath, options) returns Promise**
32+
## Methods
33+
### bundle
34+
**bundle(filepath, options) returns Promise** - to bundle a file
12035

121-
To bundle a file and resolve even internal references
122-
**dereference(filepath, options) returns Promise**
36+
### dereference
37+
**dereference(filepath, options) returns Promise** - to bundle a file and resolve even internal references
12338

12439
For both
125-
1. if `options.inherit==true` then json-patches will be compiled
126-
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)
40+
* if `options.inherit==true` then json-patches will be compiled
41+
* 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)
12742

43+
Example:
12844
```
12945
const { bundle, dereference } = require('./index')
13046
const fs = require('fs')
@@ -133,30 +49,61 @@ fs.writeFileSync('/tmp/a.json', JSON.stringify({
13349
c: {$ref: '#/d'},
13450
d: 4
13551
}), {encoding: 'utf8'})
52+
13653
fs.writeFileSync('/tmp/b.json', JSON.stringify({
13754
$inherit: {
13855
source: {$ref: '/tmp/a.json'},
13956
with: [{op: 'add', path: '/b', value: 2}]
14057
}
14158
}), {encoding: 'utf8'})
59+
14260
bundle('/tmp/a.json').then((doc) => {console.log(JSON.stringify(doc));})
14361
{"a":1,"c":{"$ref":"#/d"},"d":4}
14462
14563
bundle('/tmp/b.json', {inherit: true}).then((doc) => {console.log(JSON.stringify(doc));})
14664
{"a":1,"c":{"$ref":"#/d"},"d":4,"b":2}
65+
14766
dereference('/tmp/b.json', {inherit: true}).then((doc) => {console.log(JSON.stringify(doc));})
14867
{"a":1,"c":4,"d":4,"b":2}
14968
```
15069

70+
### compilePatchOps
71+
**compilePatchOps(source, patch) returns Array**
72+
73+
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 there is a method
74+
75+
When source document is bundled we check every patch operation:
76+
1. if it contains selector in **path**
77+
2. corresponding object in **source** is array
78+
79+
then we replace it with equal operation with [JSON-Pointer](https://tools.ietf.org/html/rfc6901) path, for example:
80+
81+
```
82+
const { compilePatchOps } = require('./index')
83+
84+
compilePatchOps(
85+
{arr: [{a: 1}, {c: 2}, {c: {$ref: '#/d'}}, {d: 4}]},
86+
[{op: 'replace', path: '/arr/[c=]', value: 2}]
87+
);
88+
[ { op: 'replace', path: '/arr/1', value: 2 },
89+
{ op: 'replace', path: '/arr/2', value: 2 } ]
90+
>
91+
```
92+
15193
## Selectors
152-
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'`. At what element do we point? You never know until you see **object** for which we apply the patch
94+
[by property value](#by_property=value_pairs)
95+
[with property](#with_property)
96+
[with value](#with_value)
97+
98+
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: what means `path: '/2'` ?. At what element do we point? You never know until you see **object** to which you apply the patch
15399
```
154100
object = [1,2,234]
155101
patch = [{op: 'remove', path: '/2'}]
156102
```
157-
So, we could 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
103+
Now we know, we wanted to remove `234`.
104+
Lets select element by its properties or value like in jquery
158105
```
159-
// patch with "selector" in path
106+
// patch with selector by value in path
160107
patch = [{op: 'remove', path: '/[=234]'}]
161108
162109
// compiling patch
@@ -165,8 +112,9 @@ compilePatchOps(object, patch)
165112
// patch with JSON-Pointer path
166113
[{op: 'remove', path: '/2'}]
167114
```
115+
So you can have easier to understand JSON-Patch operations with arrays and compile them to valid when needed.
168116

169-
Note that inside selector **[\*]** quotation is used:
117+
Note that inside selectors quotation is used:
170118
* `\"` is `"`
171119
* `\\` is `\`
172120
* `/` is `/`
@@ -195,27 +143,50 @@ Value is treated as string if possible and as number if it can't be string
195143
It works like AND
196144
`[prop=name][date=]` == `{prop: 'name', date: 'anything'}`
197145

198-
## Method: compilePatchOps
199-
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 there is a method
200-
201-
**compilePatchOps(source, patch) returns Array**
146+
## Examples
147+
### Long story short
148+
Weather service API v1 file [api_v1](./examples/api_1.0.yaml)
149+
```
150+
node index.js bundle --file examples/api_v1.yaml
151+
```
152+
And you have [bundled](./examples/api_v1.bundled.json) API file for version 1.
202153

203-
When source document is bundled we check every patch operation:
204-
1. if it contains selector in **path**
205-
2. corresponding object in **source** is array
154+
Next you have version 2 [api_v2](./examples/api_2.0.yaml), look how short it is. Compile it to have correct service API file for version 2
155+
```
156+
node index.js bundle --file examples/api_v2.yaml
157+
```
158+
Without JYBID you'd have to write it manually: [bundled](./examples/api_v2.bundled.json)
206159

207-
then we replace it with equal operation with [JSON-Pointer](https://tools.ietf.org/html/rfc6901) path, for example:
160+
### Long story long
161+
Suppose we have a weather forecast database and want to build a readonly service for it. We have data only 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 to our service. Of course this is not the best idea, but just for example..
162+
So, we have a sevice API [api_v1](./examples/api_1.0.yaml).
163+
Also there are [bundled](./examples/api_v1.bundled.json) and [dereferenced](./examples/api_v1.dereferenced.json) documents.
164+
You can try bundling or dereferencing like this
165+
```
166+
node index.js bundle --file examples/api_v1.yaml
167+
```
168+
169+
Ok, next month we add forecasts for Belarus and we have troubles now: for example a town named 'Kamenka' exists in Russia and in Belarus.
170+
Of course we need to add parameter `country` but we can't add it to v1 because some people already use our API in their app and it works ok in Russia and in Finland. If we add `country` with some default value many requests will fail. Well, we could make some workarounds based on city name being checked against list of all cities in our 3 countries but.. it is just better to make next version of API correct and ask our clients to use it instead of incorrect v1.
171+
So we need to replace unclear parameter 'names' with 'cities' and add 'country', this [api_v2](./examples/api_2.0.yaml) is how we could do it with [jybid](#jybid)
172+
Without inheritance you would have to make one of these documents manually [bundled](./examples/api_v2.bundled.json) [dereferenced](./examples/api_v2.dereferenced.json)
208173

174+
In [api_v2](./examples/api_2.0.yaml) you can find examples of inheritance
175+
```
176+
$inherit:
177+
source:
178+
$ref: ./api_v1.yaml
179+
with:
209180
```
210-
const { compilePatchOps } = require('./index')
211181

212-
compilePatchOps(
213-
{arr: [{a: 1}, {c: 2}, {c: {$ref: '#/d'}}, {d: 4}]},
214-
[{op: 'replace', path: '/arr/[c=]', value: 2}]
215-
);
216-
[ { op: 'replace', path: '/arr/1', value: 2 },
217-
{ op: 'replace', path: '/arr/2', value: 2 } ]
218-
>
182+
Codewords are
183+
* $inherit
184+
* source
185+
* with
186+
187+
Also there is an example of array selector
188+
```
189+
path: '/paths/~1city/get/parameters/[name=names]'
219190
```
220191

221192
## Thanks

0 commit comments

Comments
 (0)