Skip to content

Commit 4374ca1

Browse files
committed
feat: resolve externalValue
1 parent c5afce4 commit 4374ca1

File tree

3 files changed

+224
-1
lines changed

3 files changed

+224
-1
lines changed

src/resolver.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ export function clearCache() {
2525
plugins.refs.clearCache();
2626
}
2727

28+
export function makeFetchRaw(http, opts = {}) {
29+
const { requestInterceptor, responseInterceptor } = opts;
30+
// Set credentials with 'http.withCredentials' value
31+
const credentials = http.withCredentials ? 'include' : 'same-origin';
32+
return (docPath) =>
33+
http({
34+
url: docPath,
35+
loadSpec: true,
36+
requestInterceptor,
37+
responseInterceptor,
38+
credentials,
39+
}).then((res) => res.text);
40+
}
41+
2842
export default function resolve(obj) {
2943
const {
3044
fetch,
@@ -66,8 +80,13 @@ export default function resolve(obj) {
6680

6781
// Build a json-fetcher ( ie: give it a URL and get json out )
6882
plugins.refs.fetchJSON = makeFetchJSON(http, { requestInterceptor, responseInterceptor });
83+
// Build a raw-fetcher ( ie: give it a URL and get raw text out )
84+
plugins.externalValue.fetchRaw = makeFetchRaw(http, {
85+
requestInterceptor,
86+
responseInterceptor,
87+
});
6988

70-
const plugs = [plugins.refs];
89+
const plugs = [plugins.refs, plugins.externalValue];
7190

7291
if (typeof parameterMacro === 'function') {
7392
plugs.push(plugins.parameters);

src/specmap/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import noop from 'lodash/noop';
33

44
import lib from './lib';
55
import refs from './lib/refs';
6+
import externalValue from './lib/external-value';
67
import allOf from './lib/all-of';
78
import parameters from './lib/parameters';
89
import properties from './lib/properties';
@@ -396,6 +397,7 @@ export default function mapSpec(opts) {
396397

397398
const plugins = {
398399
refs,
400+
externalValue,
399401
allOf,
400402
parameters,
401403
properties,

src/specmap/lib/external-value.js

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { fetch } from 'cross-fetch';
2+
3+
import createError from './create-error';
4+
import lib from '.';
5+
6+
const externalValuesCache = {};
7+
8+
/**
9+
* Clears all external value caches.
10+
* @param {String} url (optional) the original externalValue value of the cache item to be cleared.
11+
* @api public
12+
*/
13+
function clearCache(url) {
14+
if (typeof url !== 'undefined') {
15+
delete externalValuesCache[url];
16+
} else {
17+
Object.keys(externalValuesCache).forEach((key) => {
18+
delete externalValuesCache[key];
19+
});
20+
}
21+
}
22+
23+
/**
24+
* Fetches a document.
25+
* @param {String} docPath the absolute URL of the document.
26+
* @return {Promise} a promise of the document content.
27+
* @api public
28+
*/
29+
const fetchRaw = (url) => fetch(url).then((res) => res.text);
30+
31+
const shouldResolveTestFn = [
32+
// OAS 3.0 Response Media Type Examples externalValue
33+
(path) =>
34+
// ["paths", *, *, "responses", *, "content", *, "examples", *, "externalValue"]
35+
path[0] === 'paths' &&
36+
path[3] === 'responses' &&
37+
path[5] === 'content' &&
38+
path[7] === 'examples' &&
39+
path[9] === 'externalValue',
40+
41+
// OAS 3.0 Request Body Media Type Examples externalValue
42+
(path) =>
43+
// ["paths", *, *, "requestBody", "content", *, "examples", *, "externalValue"]
44+
path[0] === 'paths' &&
45+
path[3] === 'requestBody' &&
46+
path[4] === 'content' &&
47+
path[6] === 'examples' &&
48+
path[8] === 'externalValue',
49+
50+
// OAS 3.0 Parameter Examples externalValue
51+
(path) =>
52+
// ["paths", *, "parameters", *, "examples", *, "externalValue"]
53+
path[0] === 'paths' &&
54+
path[2] === 'parameters' &&
55+
path[4] === 'examples' &&
56+
path[6] === 'externalValue',
57+
(path) =>
58+
// ["paths", *, *, "parameters", *, "examples", *, "externalValue"]
59+
path[0] === 'paths' &&
60+
path[3] === 'parameters' &&
61+
path[5] === 'examples' &&
62+
path[7] === 'externalValue',
63+
(path) =>
64+
// ["paths", *, "parameters", *, "content", *, "examples", *, "externalValue"]
65+
path[0] === 'paths' &&
66+
path[2] === 'parameters' &&
67+
path[4] === 'content' &&
68+
path[6] === 'examples' &&
69+
path[8] === 'externalValue',
70+
(path) =>
71+
// ["paths", *, *, "parameters", *, "content", *, "examples", *, "externalValue"]
72+
path[0] === 'paths' &&
73+
path[3] === 'parameters' &&
74+
path[5] === 'content' &&
75+
path[7] === 'examples' &&
76+
path[9] === 'externalValue',
77+
];
78+
79+
const shouldSkipResolution = (path) => !shouldResolveTestFn.some((fn) => fn(path));
80+
81+
const ExternalValueError = createError('ExternalValueError', function cb(message, extra, oriError) {
82+
this.originalError = oriError;
83+
Object.assign(this, extra || {});
84+
});
85+
86+
/**
87+
* This plugin resolves externalValue keys.
88+
* In order to do so it will use a cache in case the url was already requested.
89+
* It will use the fetchRaw method in order get the raw content hosted on specified url.
90+
* If successful retrieved it will replace the url with the actual value
91+
*/
92+
const plugin = {
93+
key: 'externalValue',
94+
plugin: (externalValue, _, fullPath) => {
95+
const parent = fullPath.slice(0, -1);
96+
97+
if (shouldSkipResolution(fullPath)) {
98+
return undefined;
99+
}
100+
101+
if (typeof externalValue !== 'string') {
102+
return new ExternalValueError('externalValue: must be a string', {
103+
externalValue,
104+
fullPath,
105+
});
106+
}
107+
108+
try {
109+
let externalValueOrPromise = getExternalValue(externalValue, fullPath);
110+
if (typeof externalValueOrPromise === 'undefined') {
111+
externalValueOrPromise = new ExternalValueError(
112+
`Could not resolve externalValue: ${externalValue}`,
113+
{
114+
externalValue,
115+
fullPath,
116+
}
117+
);
118+
}
119+
// eslint-disable-next-line no-underscore-dangle
120+
if (externalValueOrPromise.__value != null) {
121+
// eslint-disable-next-line no-underscore-dangle
122+
externalValueOrPromise = externalValueOrPromise.__value;
123+
} else {
124+
externalValueOrPromise = externalValueOrPromise.catch((e) => {
125+
throw wrapError(e, {
126+
externalValue,
127+
fullPath,
128+
});
129+
});
130+
}
131+
132+
if (externalValueOrPromise instanceof Error) {
133+
return [lib.remove(fullPath), externalValueOrPromise];
134+
}
135+
136+
const backupOriginalValuePatch = lib.add([...parent, '$externalValue'], externalValue)
137+
const valuePatch = lib.replace([...parent, 'value'], externalValueOrPromise);
138+
const cleanUpPatch = lib.remove(fullPath)
139+
return [
140+
backupOriginalValuePatch,
141+
valuePatch,
142+
cleanUpPatch
143+
];
144+
} catch (err) {
145+
return [
146+
lib.remove(fullPath),
147+
wrapError(err, {
148+
externalValue,
149+
fullPath,
150+
}),
151+
];
152+
}
153+
},
154+
};
155+
const mod = Object.assign(plugin, {
156+
wrapError,
157+
clearCache,
158+
ExternalValueError,
159+
fetchRaw,
160+
getExternalValue,
161+
});
162+
export default mod;
163+
164+
/**
165+
* Wraps an error as ExternalValueError.
166+
* @param {Error} e the error.
167+
* @param {Object} extra (optional) optional data.
168+
* @return {Error} an instance of ExternalValueError.
169+
* @api public
170+
*/
171+
function wrapError(e, extra) {
172+
let message;
173+
174+
if (e && e.response && e.response.body) {
175+
message = `${e.response.body.code} ${e.response.body.message}`;
176+
} else {
177+
message = e.message;
178+
}
179+
180+
return new ExternalValueError(`Could not resolve externalValue: ${message}`, extra, e);
181+
}
182+
183+
/**
184+
* Fetches and caches a ExternalValue.
185+
* @param {String} docPath the absolute URL of the document.
186+
* @return {Promise} a promise of the document content.
187+
* @api public
188+
*/
189+
function getExternalValue(url) {
190+
const val = externalValuesCache[url];
191+
if (val) {
192+
return lib.isPromise(val) ? val : Promise.resolve(val);
193+
}
194+
195+
// NOTE: we need to use `mod.fetchRaw` in order to be able to overwrite it.
196+
// Any tips on how to make this cleaner, please ping!
197+
externalValuesCache[url] = mod.fetchRaw(url).then((raw) => {
198+
externalValuesCache[url] = raw;
199+
return raw;
200+
});
201+
return externalValuesCache[url];
202+
}

0 commit comments

Comments
 (0)