Skip to content

Commit 5dec8fb

Browse files
committedJul 15, 2018
Initial commit
Allez les bleus ! 🇫🇷
0 parents  commit 5dec8fb

File tree

14 files changed

+886
-0
lines changed

14 files changed

+886
-0
lines changed
 

‎.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/openapi-preprocessor
2+
/*.yaml
3+
/*.yml
4+
/*.json
5+
/*.pl

‎LICENSE

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
Apache License
3+
Version 2.0, January 2004
4+
http://www.apache.org/licenses/
5+
6+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+
1. Definitions.
9+
10+
"License" shall mean the terms and conditions for use, reproduction,
11+
and distribution as defined by Sections 1 through 9 of this document.
12+
13+
"Licensor" shall mean the copyright owner or entity authorized by
14+
the copyright owner that is granting the License.
15+
16+
"Legal Entity" shall mean the union of the acting entity and all
17+
other entities that control, are controlled by, or are under common
18+
control with that entity. For the purposes of this definition,
19+
"control" means (i) the power, direct or indirect, to cause the
20+
direction or management of such entity, whether by contract or
21+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+
outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+
"You" (or "Your") shall mean an individual or Legal Entity
25+
exercising permissions granted by this License.
26+
27+
"Source" form shall mean the preferred form for making modifications,
28+
including but not limited to software source code, documentation
29+
source, and configuration files.
30+
31+
"Object" form shall mean any form resulting from mechanical
32+
transformation or translation of a Source form, including but
33+
not limited to compiled object code, generated documentation,
34+
and conversions to other media types.
35+
36+
"Work" shall mean the work of authorship, whether in Source or
37+
Object form, made available under the License, as indicated by a
38+
copyright notice that is included in or attached to the work
39+
(an example is provided in the Appendix below).
40+
41+
"Derivative Works" shall mean any work, whether in Source or Object
42+
form, that is based on (or derived from) the Work and for which the
43+
editorial revisions, annotations, elaborations, or other modifications
44+
represent, as a whole, an original work of authorship. For the purposes
45+
of this License, Derivative Works shall not include works that remain
46+
separable from, or merely link (or bind by name) to the interfaces of,
47+
the Work and Derivative Works thereof.
48+
49+
"Contribution" shall mean any work of authorship, including
50+
the original version of the Work and any modifications or additions
51+
to that Work or Derivative Works thereof, that is intentionally
52+
submitted to Licensor for inclusion in the Work by the copyright owner
53+
or by an individual or Legal Entity authorized to submit on behalf of
54+
the copyright owner. For the purposes of this definition, "submitted"
55+
means any form of electronic, verbal, or written communication sent
56+
to the Licensor or its representatives, including but not limited to
57+
communication on electronic mailing lists, source code control systems,
58+
and issue tracking systems that are managed by, or on behalf of, the
59+
Licensor for the purpose of discussing and improving the Work, but
60+
excluding communication that is conspicuously marked or otherwise
61+
designated in writing by the copyright owner as "Not a Contribution."
62+
63+
"Contributor" shall mean Licensor and any individual or Legal Entity
64+
on behalf of whom a Contribution has been received by Licensor and
65+
subsequently incorporated within the Work.
66+
67+
2. Grant of Copyright License. Subject to the terms and conditions of
68+
this License, each Contributor hereby grants to You a perpetual,
69+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+
copyright license to reproduce, prepare Derivative Works of,
71+
publicly display, publicly perform, sublicense, and distribute the
72+
Work and such Derivative Works in Source or Object form.
73+
74+
3. Grant of Patent License. Subject to the terms and conditions of
75+
this License, each Contributor hereby grants to You a perpetual,
76+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+
(except as stated in this section) patent license to make, have made,
78+
use, offer to sell, sell, import, and otherwise transfer the Work,
79+
where such license applies only to those patent claims licensable
80+
by such Contributor that are necessarily infringed by their
81+
Contribution(s) alone or by combination of their Contribution(s)
82+
with the Work to which such Contribution(s) was submitted. If You
83+
institute patent litigation against any entity (including a
84+
cross-claim or counterclaim in a lawsuit) alleging that the Work
85+
or a Contribution incorporated within the Work constitutes direct
86+
or contributory patent infringement, then any patent licenses
87+
granted to You under this License for that Work shall terminate
88+
as of the date such litigation is filed.
89+
90+
4. Redistribution. You may reproduce and distribute copies of the
91+
Work or Derivative Works thereof in any medium, with or without
92+
modifications, and in Source or Object form, provided that You
93+
meet the following conditions:
94+
95+
(a) You must give any other recipients of the Work or
96+
Derivative Works a copy of this License; and
97+
98+
(b) You must cause any modified files to carry prominent notices
99+
stating that You changed the files; and
100+
101+
(c) You must retain, in the Source form of any Derivative Works
102+
that You distribute, all copyright, patent, trademark, and
103+
attribution notices from the Source form of the Work,
104+
excluding those notices that do not pertain to any part of
105+
the Derivative Works; and
106+
107+
(d) If the Work includes a "NOTICE" text file as part of its
108+
distribution, then any Derivative Works that You distribute must
109+
include a readable copy of the attribution notices contained
110+
within such NOTICE file, excluding those notices that do not
111+
pertain to any part of the Derivative Works, in at least one
112+
of the following places: within a NOTICE text file distributed
113+
as part of the Derivative Works; within the Source form or
114+
documentation, if provided along with the Derivative Works; or,
115+
within a display generated by the Derivative Works, if and
116+
wherever such third-party notices normally appear. The contents
117+
of the NOTICE file are for informational purposes only and
118+
do not modify the License. You may add Your own attribution
119+
notices within Derivative Works that You distribute, alongside
120+
or as an addendum to the NOTICE text from the Work, provided
121+
that such additional attribution notices cannot be construed
122+
as modifying the License.
123+
124+
You may add Your own copyright statement to Your modifications and
125+
may provide additional or different license terms and conditions
126+
for use, reproduction, or distribution of Your modifications, or
127+
for any such Derivative Works as a whole, provided Your use,
128+
reproduction, and distribution of the Work otherwise complies with
129+
the conditions stated in this License.
130+
131+
5. Submission of Contributions. Unless You explicitly state otherwise,
132+
any Contribution intentionally submitted for inclusion in the Work
133+
by You to the Licensor shall be under the terms and conditions of
134+
this License, without any additional terms or conditions.
135+
Notwithstanding the above, nothing herein shall supersede or modify
136+
the terms of any separate license agreement you may have executed
137+
with Licensor regarding such Contributions.
138+
139+
6. Trademarks. This License does not grant permission to use the trade
140+
names, trademarks, service marks, or product names of the Licensor,
141+
except as required for reasonable and customary use in describing the
142+
origin of the Work and reproducing the content of the NOTICE file.
143+
144+
7. Disclaimer of Warranty. Unless required by applicable law or
145+
agreed to in writing, Licensor provides the Work (and each
146+
Contributor provides its Contributions) on an "AS IS" BASIS,
147+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+
implied, including, without limitation, any warranties or conditions
149+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+
PARTICULAR PURPOSE. You are solely responsible for determining the
151+
appropriateness of using or redistributing the Work and assume any
152+
risks associated with Your exercise of permissions under this License.
153+
154+
8. Limitation of Liability. In no event and under no legal theory,
155+
whether in tort (including negligence), contract, or otherwise,
156+
unless required by applicable law (such as deliberate and grossly
157+
negligent acts) or agreed to in writing, shall any Contributor be
158+
liable to You for damages, including any direct, indirect, special,
159+
incidental, or consequential damages of any character arising as a
160+
result of this License or out of the use or inability to use the
161+
Work (including but not limited to damages for loss of goodwill,
162+
work stoppage, computer failure or malfunction, or any and all
163+
other commercial damages or losses), even if such Contributor
164+
has been advised of the possibility of such damages.
165+
166+
9. Accepting Warranty or Additional Liability. While redistributing
167+
the Work or Derivative Works thereof, You may choose to offer,
168+
and charge a fee for, acceptance of support, warranty, indemnity,
169+
or other liability obligations and/or rights consistent with this
170+
License. However, in accepting such obligations, You may act only
171+
on Your own behalf and on Your sole responsibility, not on behalf
172+
of any other Contributor, and only if You agree to indemnify,
173+
defend, and hold each Contributor harmless for any liability
174+
incurred by, or claims asserted against, such Contributor by reason
175+
of your accepting any such warranty or additional liability.
176+
177+
END OF TERMS AND CONDITIONS
178+
179+
APPENDIX: How to apply the Apache License to your work.
180+
181+
To apply the Apache License to your work, attach the following
182+
boilerplate notice, with the fields enclosed by brackets "[]"
183+
replaced with your own identifying information. (Don't include
184+
the brackets!) The text should be enclosed in the appropriate
185+
comment syntax for the file format. We also recommend that a
186+
file or class name and description of purpose be included on the
187+
same "printed page" as the copyright notice for easier
188+
identification within third-party archives.
189+
190+
Copyright [yyyy] [name of copyright owner]
191+
192+
Licensed under the Apache License, Version 2.0 (the "License");
193+
you may not use this file except in compliance with the License.
194+
You may obtain a copy of the License at
195+
196+
http://www.apache.org/licenses/LICENSE-2.0
197+
198+
Unless required by applicable law or agreed to in writing, software
199+
distributed under the License is distributed on an "AS IS" BASIS,
200+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+
See the License for the specific language governing permissions and
202+
limitations under the License.

‎io.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"net/url"
9+
"os"
10+
"path/filepath"
11+
12+
yaml "gopkg.in/yaml.v2"
13+
)
14+
15+
func loadURL(u *url.URL) (map[string]interface{}, error) {
16+
if u.Scheme != "file" {
17+
return nil, fmt.Errorf("unsupported %q URL scheme", u.Scheme)
18+
}
19+
20+
return loadFile(filepath.FromSlash(u.Path))
21+
}
22+
23+
func loadFile(pth string) (map[string]interface{}, error) {
24+
f, err := os.Open(pth)
25+
if err != nil {
26+
return nil, err
27+
}
28+
var load func(io.Reader) (map[string]interface{}, error)
29+
switch filepath.Ext(pth) {
30+
case ".json":
31+
load = loadJSON
32+
case ".yaml", ".yml":
33+
load = loadYAML
34+
default:
35+
return nil, errors.New("unsupported file extension")
36+
}
37+
return load(f)
38+
}
39+
40+
func loadYAML(r io.Reader) (map[string]interface{}, error) {
41+
data, err := loadAny(yaml.NewDecoder(r))
42+
if err != nil {
43+
return nil, err
44+
}
45+
return fixMaps(data).(map[string]interface{}), err
46+
}
47+
48+
func fixMaps(v interface{}) interface{} {
49+
switch v := v.(type) {
50+
case nil, bool, string, int, int64, float64:
51+
case []interface{}:
52+
for i, item := range v {
53+
v[i] = fixMaps(item)
54+
}
55+
case map[interface{}]interface{}:
56+
m := make(map[string]interface{}, len(v))
57+
for key, val := range v {
58+
m[fmt.Sprint(key)] = fixMaps(val)
59+
}
60+
return m
61+
case map[string]interface{}:
62+
for key, value := range v {
63+
v[key] = fixMaps(value)
64+
}
65+
}
66+
return v
67+
}
68+
69+
func loadJSON(r io.Reader) (map[string]interface{}, error) {
70+
return loadAny(json.NewDecoder(r))
71+
}
72+
73+
func loadAny(decoder interface{ Decode(interface{}) error }) (map[string]interface{}, error) {
74+
var data map[string]interface{}
75+
err := decoder.Decode(&data)
76+
if err != nil {
77+
return nil, err
78+
}
79+
if data == nil {
80+
return nil, errors.New("unexpected empty object")
81+
}
82+
return data, err
83+
}

‎main.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/url"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
func main() {
13+
code, err := _main()
14+
if err != nil {
15+
fmt.Fprintln(os.Stderr, err)
16+
if code == 0 {
17+
code = 1
18+
}
19+
}
20+
os.Exit(code)
21+
}
22+
23+
func _main() (int, error) {
24+
log.SetPrefix("")
25+
log.SetFlags(0)
26+
27+
if len(os.Args) < 2 {
28+
return 1, fmt.Errorf("usage: %s <file>", os.Args[0])
29+
}
30+
31+
enc := json.NewEncoder(os.Stdout)
32+
enc.SetIndent("", " ")
33+
34+
return 0, processFile(os.Args[1], enc.Encode)
35+
}
36+
37+
func processFile(pth string, encode func(interface{}) error) error {
38+
pth, err := filepath.Abs(pth)
39+
if err != nil {
40+
return err
41+
}
42+
43+
spec, err := loadFile(pth)
44+
if err != nil {
45+
return err
46+
}
47+
48+
var tmp interface{}
49+
tmp = spec
50+
51+
err = ExpandRefs(&tmp, &url.URL{
52+
Scheme: "file",
53+
Path: filepath.ToSlash(pth),
54+
})
55+
if err != nil {
56+
return err
57+
}
58+
59+
return encode(tmp)
60+
}

‎refs.go

+376
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/mohae/deepcopy"
10+
11+
"github.com/dolmen-go/jsonptr"
12+
)
13+
14+
type setter func(interface{})
15+
16+
// visitRefs visits $ref and allows to change them.
17+
func visitRefs(root interface{}, ptr jsonptr.Pointer, visitor func(jsonptr.Pointer, string) (string, error)) (err error) {
18+
switch root := root.(type) {
19+
case map[string]interface{}:
20+
ptr = ptr.Copy()
21+
for k, v := range root {
22+
ptr.Property(k)
23+
if k == "$ref" {
24+
if str, isString := v.(string); isString {
25+
root[k], err = visitor(ptr, str)
26+
if err != nil {
27+
return
28+
}
29+
}
30+
}
31+
ptr.Up()
32+
}
33+
case []string:
34+
ptr = ptr.Copy()
35+
for i, v := range root {
36+
ptr.Index(i)
37+
visitRefs(v, ptr, visitor)
38+
ptr.Up()
39+
}
40+
}
41+
return
42+
}
43+
44+
type refResolver struct {
45+
rootURL string
46+
docs map[string]*interface{}
47+
}
48+
49+
func (resolver *refResolver) resolve(link string, relativeTo *url.URL) (interface{}, setter, *url.URL, error) {
50+
u, err := url.Parse(link)
51+
if err != nil {
52+
return nil, nil, nil, err
53+
}
54+
55+
if !u.IsAbs() {
56+
u = relativeTo.ResolveReference(u)
57+
}
58+
59+
if *u == *relativeTo {
60+
return nil, nil, nil, errors.New("circular link")
61+
}
62+
63+
rootURL := *u
64+
rootURL.Fragment = ""
65+
rootURLStr := rootURL.String()
66+
67+
rdoc, loaded := resolver.docs[rootURLStr]
68+
if !loaded {
69+
//log.Println("Loading", &rootURL)
70+
doc, err := loadURL(&rootURL)
71+
if err != nil {
72+
return nil, nil, nil, err
73+
}
74+
var itf interface{}
75+
itf = doc
76+
rdoc = &itf
77+
resolver.docs[rootURLStr] = rdoc
78+
}
79+
80+
if u.Fragment == "" {
81+
return *rdoc, func(newDoc interface{}) {
82+
*rdoc = newDoc
83+
}, &rootURL, nil
84+
}
85+
86+
ptr, err := jsonptr.Parse(u.Fragment)
87+
if err != nil {
88+
return nil, nil, nil, err
89+
}
90+
91+
// FIXME we could reduce the number of evals of JSON pointers...
92+
93+
frag, err := ptr.In(*rdoc)
94+
if err != nil {
95+
// If the can't be immediately resolved, this may be because
96+
// of a $inline in the way
97+
98+
p := ptr[:1]
99+
for {
100+
doc, err := p.In(*rdoc)
101+
if err == nil {
102+
break
103+
}
104+
if obj, isMap := doc.(map[string]interface{}); isMap {
105+
if _, isInline := obj["$inline"]; isInline {
106+
u2 := rootURL
107+
u2.Fragment = p.String()
108+
var target interface{}
109+
110+
err := resolver.expand(doc, func(newDoc interface{}) {
111+
target = newDoc
112+
}, &u2, nil, map[string]bool{})
113+
if err != nil {
114+
return nil, nil, nil, err
115+
}
116+
117+
p.Set(rdoc, target)
118+
}
119+
}
120+
if len(p) == len(ptr) {
121+
// Failed to resolve the fragment
122+
return nil, nil, nil, err
123+
}
124+
}
125+
126+
frag, _ = ptr.In(*rdoc)
127+
}
128+
129+
return frag, func(newDoc interface{}) {
130+
ptr.Set(rdoc, newDoc)
131+
}, &rootURL, nil
132+
}
133+
134+
func (resolver *refResolver) expand(doc interface{}, set setter, docURL *url.URL, imp map[string]string, visited map[string]bool) error {
135+
u := docURL.String()
136+
//log.Println(u, docURL.Fragment)
137+
if visited[u] {
138+
return nil
139+
}
140+
visited[u] = true
141+
142+
if doc, isSlice := doc.([]interface{}); isSlice {
143+
u2 := *docURL
144+
for i, v := range doc {
145+
switch v.(type) {
146+
case []interface{}, map[string]interface{}:
147+
u2.Fragment = fmt.Sprintf("%s/%d", docURL.Fragment, i)
148+
err := resolver.expand(v, func(newDoc interface{}) {
149+
doc[i] = newDoc
150+
}, &u2, imp, visited)
151+
if err != nil {
152+
return err
153+
}
154+
}
155+
}
156+
return nil
157+
}
158+
obj, isObject := doc.(map[string]interface{})
159+
if !isObject || obj == nil {
160+
return nil
161+
}
162+
163+
if ref, isRef := obj["$ref"]; isRef {
164+
//log.Printf("$ref: %s => %s", docURL, ref)
165+
link, isString := ref.(string)
166+
if !isString {
167+
return fmt.Errorf("%s: $ref must be a string", docURL)
168+
}
169+
if len(obj) > 1 {
170+
return fmt.Errorf("%s: $ref must be alone (use $merge instead)", docURL)
171+
}
172+
173+
doc, set, u, err := resolver.resolve(link, docURL)
174+
if err != nil {
175+
return err
176+
}
177+
//log.Printf("%#v", doc)
178+
resolver.expand(doc, func(newDoc interface{}) {
179+
doc = newDoc
180+
set(newDoc)
181+
}, u, imp, visited)
182+
if err != nil {
183+
return err
184+
}
185+
186+
if imp != nil {
187+
fragment := docURL.Fragment
188+
u2 := *u
189+
u2.Fragment = ""
190+
u2str := u2.String()
191+
if u2str != resolver.rootURL {
192+
if src := imp[fragment]; src != "" && src != u2str {
193+
return fmt.Errorf("import fragment %s from both %s and %s", link, src, u2str)
194+
}
195+
imp[fragment] = u2str
196+
}
197+
}
198+
199+
return nil
200+
}
201+
202+
// An extension to build an object from mixed local data and
203+
// imported data
204+
if link, isMerge := obj["$merge"]; isMerge {
205+
var links []string
206+
switch link := link.(type) {
207+
case string:
208+
if len(obj) == 1 {
209+
return fmt.Errorf("%s: merging with nothing?", docURL)
210+
}
211+
links = []string{link}
212+
case []interface{}:
213+
links = make([]string, len(link))
214+
for i, v := range link {
215+
l, isString := v.(string)
216+
if !isString {
217+
return fmt.Errorf("%s/%d: must be a string", docURL, i)
218+
}
219+
links[i] = l
220+
}
221+
default:
222+
return fmt.Errorf("%s: must be a string or array of strings", docURL)
223+
}
224+
delete(obj, "$merge")
225+
226+
s := docURL.String()
227+
delete(visited, s)
228+
err := resolver.expand(doc, func(newDoc interface{}) {
229+
doc = newDoc
230+
set(newDoc)
231+
}, docURL, imp, visited)
232+
visited[s] = true
233+
if err != nil {
234+
return err
235+
}
236+
237+
for i, link := range links {
238+
target, set, u, err := resolver.resolve(link, docURL)
239+
if err != nil {
240+
return err
241+
}
242+
err = resolver.expand(doc, func(newDoc interface{}) {
243+
target = newDoc
244+
set(newDoc)
245+
}, u, imp, visited)
246+
if err != nil {
247+
return err
248+
}
249+
250+
objTarget, isObj := target.(map[string]interface{})
251+
if !isObj {
252+
if len(links) == 1 {
253+
return fmt.Errorf("%s/$merge: link must point to object", docURL)
254+
}
255+
return fmt.Errorf("%s/$merge/%d: link must point to object", docURL, i)
256+
}
257+
for k, v := range objTarget {
258+
if _, exists := obj[k]; exists {
259+
// TODO warn
260+
continue
261+
}
262+
obj[k] = v
263+
}
264+
}
265+
266+
return nil
267+
}
268+
269+
if link, isInline := obj["$inline"]; isInline {
270+
271+
target, setTarget, u, err := resolver.resolve(link.(string), docURL)
272+
if err != nil {
273+
return err
274+
}
275+
err = resolver.expand(doc, func(newDoc interface{}) {
276+
target = newDoc
277+
setTarget(newDoc)
278+
}, u, imp, visited)
279+
if err != nil {
280+
return err
281+
}
282+
283+
if len(obj) == 1 {
284+
set(target)
285+
return nil
286+
}
287+
288+
delete(obj, "$inline")
289+
target = deepcopy.Copy(target)
290+
291+
switch target.(type) {
292+
case map[string]interface{}:
293+
for _, k := range sortedKeys(obj) {
294+
v := obj[k]
295+
//log.Println(k)
296+
u := *docURL
297+
u.Fragment = u.Fragment + "/" + jsonptr.EscapeString(k)
298+
resolver.expand(v, func(newDoc interface{}) {
299+
v = newDoc
300+
}, &u, imp, visited)
301+
if err := jsonptr.Set(&target, "/"+k, v); err != nil {
302+
return fmt.Errorf("%s/%s: %v", docURL, k, err)
303+
}
304+
}
305+
case []interface{}:
306+
// TODO
307+
return fmt.Errorf("%s: inlining of array not yet implemented", docURL)
308+
default:
309+
return fmt.Errorf("%s: inlined scalar value can't be patched", docURL)
310+
}
311+
312+
set(target)
313+
// doc = target
314+
return nil
315+
}
316+
317+
for _, k := range sortedKeys(obj) {
318+
//log.Println("Key:", k)
319+
u := *docURL
320+
u.Fragment = u.Fragment + "/" + jsonptr.EscapeString(k)
321+
resolver.expand(obj[k], func(newDoc interface{}) {
322+
obj[k] = newDoc
323+
}, &u, imp, visited)
324+
}
325+
326+
return nil
327+
}
328+
329+
func ExpandRefs(rdoc *interface{}, docURL *url.URL) error {
330+
if len(docURL.Fragment) > 0 {
331+
panic("URL fragment unexpected for initial document")
332+
}
333+
334+
docURLstr := docURL.String()
335+
resolver := refResolver{
336+
rootURL: docURL.String(),
337+
docs: map[string]*interface{}{
338+
docURLstr: rdoc,
339+
},
340+
}
341+
342+
toInject := make(map[string]string)
343+
344+
err := resolver.expand(*rdoc, func(newDoc interface{}) {
345+
*rdoc = newDoc
346+
}, docURL, toInject, make(map[string]bool))
347+
348+
if err != nil {
349+
return err
350+
}
351+
352+
// Inject content in external documents pointed by $ref.
353+
// The inject path is the same as the path in the source doc.
354+
for ptr, u := range toInject {
355+
// log.Println(ptr, u)
356+
357+
if _, err := jsonptr.Get(*rdoc, ptr); err != nil {
358+
return fmt.Errorf("%s: content replaced from %s", ptr, u)
359+
}
360+
target, _ := jsonptr.Get(*resolver.docs[u], ptr)
361+
_ = jsonptr.Set(rdoc, ptr, target)
362+
}
363+
364+
// As some $ref pointed to external documents we have to fix them
365+
if len(resolver.docs) > 1 {
366+
_ = visitRefs(*rdoc, nil, func(ptr jsonptr.Pointer, ref string) (string, error) {
367+
i := strings.IndexByte(ref, '#')
368+
if i > 0 {
369+
ref = ref[i:]
370+
}
371+
return ref, nil
372+
})
373+
}
374+
375+
return err
376+
}

‎refs_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"path/filepath"
7+
"reflect"
8+
"sort"
9+
"testing"
10+
)
11+
12+
func TestExpandRefs(t *testing.T) {
13+
dir, err := os.Open("testdata")
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
all, err := dir.Readdir(-1)
18+
dir.Close()
19+
if err != nil {
20+
log.Fatal(err)
21+
}
22+
23+
sort.SliceStable(all, func(i, j int) bool {
24+
return all[i].Name() < all[j].Name()
25+
})
26+
27+
for _, f := range all {
28+
if !f.IsDir() {
29+
continue
30+
}
31+
name := f.Name()
32+
if name[0] < '0' || name[0] > '9' {
33+
continue
34+
}
35+
t.Run(name, func(t *testing.T) {
36+
testExpandRefs(t, "testdata/"+name)
37+
})
38+
}
39+
}
40+
41+
func testExpandRefs(t *testing.T, path string) {
42+
var inputPath string
43+
for _, ext := range []string{".yml", ".yaml", ".json"} {
44+
p := path + "/input" + ext
45+
t.Log(p)
46+
_, err := os.Stat(p)
47+
if err == nil {
48+
inputPath = p
49+
break
50+
}
51+
if os.IsNotExist(err) {
52+
continue
53+
}
54+
log.Fatalf("%s: %v", p, err)
55+
}
56+
if inputPath == "" {
57+
t.Fatal("no input file")
58+
}
59+
60+
expected, err := loadFile(filepath.Join(filepath.FromSlash(path), "result.json"))
61+
if err != nil {
62+
t.Fatalf("%s/result.json: %v", path, err)
63+
}
64+
65+
var out interface{}
66+
err = processFile(inputPath, func(result interface{}) error {
67+
out = result
68+
return nil
69+
})
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
if !reflect.DeepEqual(out, expected) {
75+
t.Errorf("output doesn't match")
76+
}
77+
}

‎testdata/10-ref-ext/input.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
swagger: "2.0"
3+
info:
4+
$ref: ../common/info.yml#/info
5+
paths:
6+
/:
7+
get:
8+
responses:
9+
404:
10+
description: Not found.

‎testdata/10-ref-ext/result.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"info": {
3+
"title": "Test",
4+
"version": "0.0.0"
5+
},
6+
"paths": {
7+
"/": {
8+
"get": {
9+
"responses": {
10+
"404": {
11+
"description": "Not found."
12+
}
13+
}
14+
}
15+
}
16+
},
17+
"swagger": "2.0"
18+
}

‎testdata/40-inline/indirect.yml

Whitespace-only changes.

‎testdata/40-inline/input.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
swagger: "2.0"
3+
info:
4+
$inline: indirect.yml#/info
5+
version: "0.0.1"
6+
paths:
7+
/:
8+
get:
9+
responses:
10+
404:
11+
description: Not found.

‎testdata/40-inline/result.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"info": {
3+
"title": "Test",
4+
"version": "0.0.1"
5+
},
6+
"paths": {
7+
"/": {
8+
"get": {
9+
"responses": {
10+
"404": {
11+
"description": "Not found."
12+
}
13+
}
14+
}
15+
}
16+
},
17+
"swagger": "2.0"
18+
}

‎testdata/41-inline-indirect/input.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
swagger: "2.0"
3+
info:
4+
$inline: ../common/info.yml#/info
5+
version: "0.0.1"
6+
paths:
7+
/:
8+
get:
9+
responses:
10+
404:
11+
description: Not found.

‎testdata/common/info.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
info:
2+
title: "Test"
3+
version: "0.0.0"

‎util.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package main
2+
3+
import "sort"
4+
5+
func sortedKeys(obj map[string]interface{}) (keys []string) {
6+
keys = make([]string, 0, len(obj))
7+
for k := range obj {
8+
keys = append(keys, k)
9+
}
10+
sort.Strings(keys)
11+
return
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.