-
Notifications
You must be signed in to change notification settings - Fork 761
feat: resolve external value #2013
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
Open
mathis-m
wants to merge
7
commits into
swagger-api:master
Choose a base branch
from
mathis-m:ft/external_value
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
174802e
feat: resolve externalValue to value
mathis-m d74f72f
test(external-value): test ExternalValueError
mathis-m 421a481
test(external-value): value should skip resolution
mathis-m 346779d
fix(external-value): skip if value is present
mathis-m 9aaec9f
chore: increase entrypoint size by 9.765625 kibibyte
mathis-m 17efe8b
feat: absolutify externalValue
mathis-m ef4f74f
Fix copy pasta
mathis-m File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
`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]; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.