Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: resolve external value #2013

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/webpack/browser.config.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const browserMin = {
devtool: 'source-map',
performance: {
hints: 'error',
maxEntrypointSize: 270000,
maxEntrypointSize: 280000,
maxAssetSize: 1300000,
},
output: {
Expand Down
21 changes: 20 additions & 1 deletion src/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ export function clearCache() {
plugins.refs.clearCache();
}

export function makeFetchRaw(http, opts = {}) {
const { requestInterceptor, responseInterceptor } = opts;
// Set credentials with 'http.withCredentials' value
const credentials = http.withCredentials ? 'include' : 'same-origin';
return (docPath) =>
http({
url: docPath,
loadSpec: true,
requestInterceptor,
responseInterceptor,
credentials,
}).then((res) => res.text);
}

export default function resolve(obj) {
const {
fetch,
Expand Down Expand Up @@ -66,8 +80,13 @@ export default function resolve(obj) {

// Build a json-fetcher ( ie: give it a URL and get json out )
plugins.refs.fetchJSON = makeFetchJSON(http, { requestInterceptor, responseInterceptor });
// Build a raw-fetcher ( ie: give it a URL and get raw text out )
plugins.externalValue.fetchRaw = makeFetchRaw(http, {
requestInterceptor,
responseInterceptor,
});

const plugs = [plugins.refs];
const plugs = [plugins.refs, plugins.externalValue];

if (typeof parameterMacro === 'function') {
plugs.push(plugins.parameters);
Expand Down
2 changes: 2 additions & 0 deletions src/specmap/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import lib from './lib';
import refs from './lib/refs';
import externalValue from './lib/external-value';
import allOf from './lib/all-of';
import parameters from './lib/parameters';
import properties from './lib/properties';
Expand Down Expand Up @@ -393,6 +394,7 @@ export default function mapSpec(opts) {

const plugins = {
refs,
externalValue,
allOf,
parameters,
properties,
Expand Down
242 changes: 242 additions & 0 deletions src/specmap/lib/external-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { fetch } from 'cross-fetch';

import createError from './create-error';
import lib from '.';
import url from 'url';

const externalValuesCache = {};


/**
* Resolves a path(optional absolute) and its base to an abolute URL.
* @api public
*/
function absoluteify(path, basePath) {
if (!ABSOLUTE_URL_REGEXP.test(path)) {
if (!basePath) {
throw new JSONRefError(
mathis-m marked this conversation as resolved.
Show resolved Hide resolved
`Tried to resolve a relative URL, without having a basePath. path: '${path}' basePath: '${basePath}'`
);
}
return url.resolve(basePath, path);
}
return path;
}

/**
* Clears all external value caches.
* @param {String} url (optional) the original externalValue value of the cache item to be cleared.
* @api public
*/
function clearCache(url) {
if (typeof url !== 'undefined') {
delete externalValuesCache[url];
} else {
Object.keys(externalValuesCache).forEach((key) => {
delete externalValuesCache[key];
});
}
}

/**
* Fetches a document.
* @param {String} docPath the absolute URL of the document.
* @return {Promise} a promise of the document content.
* @api public
*/
const fetchRaw = (url) => fetch(url).then((res) => res.text);

const shouldResolveTestFn = [
// OAS 3.0 Response Media Type Examples externalValue
(path) =>
// ["paths", *, *, "responses", *, "content", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[3] === 'responses' &&
path[5] === 'content' &&
path[7] === 'examples' &&
path[9] === 'externalValue',

// OAS 3.0 Request Body Media Type Examples externalValue
(path) =>
// ["paths", *, *, "requestBody", "content", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[3] === 'requestBody' &&
path[4] === 'content' &&
path[6] === 'examples' &&
path[8] === 'externalValue',

// OAS 3.0 Parameter Examples externalValue
(path) =>
// ["paths", *, "parameters", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[2] === 'parameters' &&
path[4] === 'examples' &&
path[6] === 'externalValue',
(path) =>
// ["paths", *, *, "parameters", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[3] === 'parameters' &&
path[5] === 'examples' &&
path[7] === 'externalValue',
(path) =>
// ["paths", *, "parameters", *, "content", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[2] === 'parameters' &&
path[4] === 'content' &&
path[6] === 'examples' &&
path[8] === 'externalValue',
(path) =>
// ["paths", *, *, "parameters", *, "content", *, "examples", *, "externalValue"]
path[0] === 'paths' &&
path[3] === 'parameters' &&
path[5] === 'content' &&
path[7] === 'examples' &&
path[9] === 'externalValue',
];

const shouldSkipResolution = (path) => !shouldResolveTestFn.some((fn) => fn(path));

const ExternalValueError = createError('ExternalValueError', function cb(message, extra, oriError) {
this.originalError = oriError;
Object.assign(this, extra || {});
});

/**
* This plugin resolves externalValue keys.
* In order to do so it will use a cache in case the url was already requested.
* It will use the fetchRaw method in order get the raw content hosted on specified url.
* If successful retrieved it will replace the url with the actual value
*/
const plugin = {
key: 'externalValue',
plugin: (externalValue, _, fullPath, specmap, patch) => {
const parent = fullPath.slice(0, -1);
const parentObj = lib.getIn(patch.value, parent);

if (parentObj.value !== undefined) {
return undefined;
}

if (shouldSkipResolution(fullPath)) {
return undefined;
}
const { baseDoc } = specmap.getContext(fullPath);

if (typeof externalValue !== 'string') {
return new ExternalValueError('externalValue: must be a string', {
externalValue,
baseDoc,
fullPath,
});
}

const pathFragmentSplit = externalValue.split('#');
const externalValuePath = pathFragmentSplit[0];

let basePath;
try {
basePath = baseDoc || externalValuePath ? absoluteify(externalValuePath, baseDoc) : null;
} catch (e) {
return new ExternalValueError(
`Could not absoluteify externalValue: ${externalValue}`,
{
externalValue,
baseDoc,
fullPath,
}
);
}

try {
let externalValueOrPromise = getExternalValue(externalValue, fullPath);
if (typeof externalValueOrPromise === 'undefined') {
externalValueOrPromise = new ExternalValueError(
`Could not resolve externalValue: ${externalValue}`,
{
externalValue,
baseDoc,
fullPath,
}
);
}
// eslint-disable-next-line no-underscore-dangle
if (externalValueOrPromise.__value != null) {
// eslint-disable-next-line no-underscore-dangle
externalValueOrPromise = externalValueOrPromise.__value;
} else {
externalValueOrPromise = externalValueOrPromise.catch((e) => {
throw wrapError(e, {
externalValue,
fullPath,
});
});
}

if (externalValueOrPromise instanceof Error) {
return [lib.remove(fullPath), externalValueOrPromise];
}

const backupOriginalValuePatch = lib.add([...parent, '$externalValue'], externalValue);
const valuePatch = lib.replace([...parent, 'value'], externalValueOrPromise);
const cleanUpPatch = lib.remove(fullPath);
return [backupOriginalValuePatch, valuePatch, cleanUpPatch];
} catch (err) {
return [
lib.remove(fullPath),
wrapError(err, {
externalValue,
fullPath,
}),
];
}
},
};
const mod = Object.assign(plugin, {
wrapError,
clearCache,
ExternalValueError,
fetchRaw,
getExternalValue,
absoluteify
});
export default mod;

/**
* Wraps an error as ExternalValueError.
* @param {Error} e the error.
* @param {Object} extra (optional) optional data.
* @return {Error} an instance of ExternalValueError.
* @api public
*/
function wrapError(e, extra) {
let message;

if (e && e.response && e.response.body) {
message = `${e.response.body.code} ${e.response.body.message}`;
} else {
message = e.message;
}

return new ExternalValueError(`Could not resolve externalValue: ${message}`, extra, e);
}

/**
* Fetches and caches a ExternalValue.
* @param {String} docPath the absolute URL of the document.
* @return {Promise} a promise of the document content.
* @api public
*/
function getExternalValue(url) {
const val = externalValuesCache[url];
if (val) {
return lib.isPromise(val) ? val : Promise.resolve(val);
}

// NOTE: we need to use `mod.fetchRaw` in order to be able to overwrite it.
// Any tips on how to make this cleaner, please ping!
externalValuesCache[url] = mod.fetchRaw(url).then((raw) => {
externalValuesCache[url] = raw;
return raw;
});
return externalValuesCache[url];
}
Loading