Skip to content

Commit

Permalink
feat: resolve externalValue
Browse files Browse the repository at this point in the history
  • Loading branch information
mathis-m committed Apr 5, 2021
1 parent c5afce4 commit 235f3f3
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 1 deletion.
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
Expand Up @@ -3,6 +3,7 @@ import noop from 'lodash/noop';

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 @@ -396,6 +397,7 @@ export default function mapSpec(opts) {

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

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

const externalValuesCache = {};

/**
* 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) => {
const parent = fullPath.slice(0, -1);

if (shouldSkipResolution(fullPath)) {
return undefined;
}

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

try {
let externalValueOrPromise = getExternalValue(externalValue, fullPath);
if (typeof externalValueOrPromise === 'undefined') {
externalValueOrPromise = new ExternalValueError(
`Could not resolve externalValue: ${externalValue}`,
{
externalValue,
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,
});
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];
}
49 changes: 49 additions & 0 deletions test/specmap/external-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import xmock from 'xmock';

import mapSpec, { plugins } from '../../src/specmap';

const { externalValue } = plugins;

describe('externalValue', () => {
let xapp;

beforeAll(() => {
xapp = xmock();
});

afterAll(() => {
xapp.restore();
});

beforeEach(() => {
externalValue.clearCache()
});

describe('ExternalValueError', () => {
test('should contain the externalValue error details', () => {
try {
throw new externalValue.ExternalValueError('Probe', {
externalValue: 'http://test.com/probe',
fullPath: "probe",
});
} catch (e) {
expect(e.toString()).toEqual('ExternalValueError: Probe');
expect(e.externalValue).toEqual('http://test.com/probe');
expect(e.fullPath).toEqual("probe");
}
});
test('.wrapError should wrap an error in ExternalValueError', () => {
try {
throw externalValue.wrapError(new Error('hi'), {
externalValue: 'http://test.com/probe',
fullPath: "probe",
});
} catch (e) {
expect(e.message).toMatch(/externalValue/);
expect(e.message).toMatch(/hi/);
expect(e.externalValue).toEqual('http://test.com/probe');
expect(e.fullPath).toEqual("probe");
}
});
});
});

0 comments on commit 235f3f3

Please sign in to comment.