| 
 | 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