Skip to content

Commit 28eec21

Browse files
committed
feat: resolve externalValue
1 parent c5afce4 commit 28eec21

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-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/externalValue';
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/externalValue.js

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

0 commit comments

Comments
 (0)