api 4.5.1 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -162
  3. package/bin/api +2 -0
  4. package/dist/bin.d.ts +1 -0
  5. package/dist/bin.js +91 -0
  6. package/dist/cache.d.ts +30 -0
  7. package/dist/cache.js +217 -0
  8. package/dist/cli/codegen/index.d.ts +4 -0
  9. package/dist/cli/codegen/index.js +23 -0
  10. package/dist/cli/codegen/language.d.ts +27 -0
  11. package/dist/cli/codegen/language.js +19 -0
  12. package/dist/cli/codegen/languages/typescript.d.ts +99 -0
  13. package/dist/cli/codegen/languages/typescript.js +769 -0
  14. package/dist/cli/commands/index.d.ts +4 -0
  15. package/dist/cli/commands/index.js +9 -0
  16. package/dist/cli/commands/install.d.ts +3 -0
  17. package/dist/cli/commands/install.js +230 -0
  18. package/dist/cli/lib/prompt.d.ts +9 -0
  19. package/dist/cli/lib/prompt.js +81 -0
  20. package/dist/cli/logger.d.ts +1 -0
  21. package/dist/cli/logger.js +16 -0
  22. package/dist/cli/storage.d.ts +105 -0
  23. package/dist/cli/storage.js +264 -0
  24. package/dist/core/getJSONSchemaDefaults.d.ts +15 -0
  25. package/dist/core/getJSONSchemaDefaults.js +62 -0
  26. package/dist/core/index.d.ts +32 -0
  27. package/dist/core/index.js +143 -0
  28. package/dist/core/parseResponse.d.ts +1 -0
  29. package/dist/core/parseResponse.js +65 -0
  30. package/dist/core/prepareAuth.d.ts +5 -0
  31. package/dist/core/prepareAuth.js +55 -0
  32. package/dist/core/prepareParams.d.ts +24 -0
  33. package/dist/core/prepareParams.js +351 -0
  34. package/dist/core/prepareServer.d.ts +13 -0
  35. package/dist/core/prepareServer.js +50 -0
  36. package/dist/fetcher.d.ts +54 -0
  37. package/dist/fetcher.js +165 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +276 -0
  40. package/dist/packageInfo.d.ts +2 -0
  41. package/dist/packageInfo.js +6 -0
  42. package/package.json +67 -28
  43. package/src/.sink.d.ts +1 -0
  44. package/src/bin.ts +20 -0
  45. package/src/cache.ts +212 -0
  46. package/src/cli/codegen/index.ts +31 -0
  47. package/src/cli/codegen/language.ts +47 -0
  48. package/src/cli/codegen/languages/typescript.ts +807 -0
  49. package/src/cli/commands/index.ts +5 -0
  50. package/src/cli/commands/install.ts +196 -0
  51. package/src/cli/lib/prompt.ts +29 -0
  52. package/src/cli/logger.ts +10 -0
  53. package/src/cli/storage.ts +297 -0
  54. package/src/core/getJSONSchemaDefaults.ts +74 -0
  55. package/src/core/index.ts +108 -0
  56. package/src/{lib/parseResponse.js → core/parseResponse.ts} +5 -7
  57. package/src/core/prepareAuth.ts +85 -0
  58. package/src/core/prepareParams.ts +338 -0
  59. package/src/{lib/prepareServer.js → core/prepareServer.ts} +13 -12
  60. package/src/fetcher.ts +141 -0
  61. package/src/index.ts +212 -0
  62. package/src/packageInfo.ts +3 -0
  63. package/src/typings.d.ts +3 -0
  64. package/tsconfig.json +24 -0
  65. package/src/cache.js +0 -225
  66. package/src/index.js +0 -177
  67. package/src/lib/getSchema.js +0 -34
  68. package/src/lib/index.js +0 -11
  69. package/src/lib/prepareAuth.js +0 -69
  70. package/src/lib/prepareParams.js +0 -198
package/src/fetcher.ts ADDED
@@ -0,0 +1,141 @@
1
+ import type { OASDocument } from 'oas/@types/rmoas.types';
2
+
3
+ import 'isomorphic-fetch';
4
+ import OpenAPIParser from '@readme/openapi-parser';
5
+ import yaml from 'js-yaml';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ export default class Fetcher {
10
+ uri: string | OASDocument;
11
+
12
+ /**
13
+ * @example @petstore/v1.0#n6kvf10vakpemvplx
14
+ * @example @petstore#n6kvf10vakpemvplx
15
+ */
16
+ // eslint-disable-next-line unicorn/no-unsafe-regex
17
+ static registryUUIDRegex = /^@(?<project>[a-zA-Z0-9-_]+)(\/?(?<version>.+))?#(?<uuid>[a-z0-9]+)$/;
18
+
19
+ constructor(uri: string | OASDocument) {
20
+ if (typeof uri === 'string') {
21
+ if (Fetcher.isAPIRegistryUUID(uri)) {
22
+ // Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry.
23
+ this.uri = uri.replace(Fetcher.registryUUIDRegex, 'https://dash.readme.com/api/v1/api-registry/$4');
24
+ } else if (Fetcher.isGitHubBlobURL(uri)) {
25
+ /**
26
+ * People may try to use a public repository URL to the source viewer on GitHub not knowing
27
+ * that this page actually serves HTML. In this case we want to rewrite these to the "raw"
28
+ * version of this page that'll allow us to access the API definition.
29
+ *
30
+ * @example https://github.com/readmeio/oas-examples/blob/main/3.1/json/petstore.json
31
+ */
32
+ this.uri = uri.replace(/\/\/github.com/, '//raw.githubusercontent.com').replace(/\/blob\//, '/');
33
+ } else {
34
+ this.uri = uri;
35
+ }
36
+ } else {
37
+ this.uri = uri;
38
+ }
39
+ }
40
+
41
+ static isAPIRegistryUUID(uri: string) {
42
+ return Fetcher.registryUUIDRegex.test(uri);
43
+ }
44
+
45
+ static isGitHubBlobURL(uri: string) {
46
+ return /\/\/github.com\/[-_a-zA-Z0-9]+\/[-_a-zA-Z0-9]+\/blob\/(.*).(yaml|json|yml)/.test(uri);
47
+ }
48
+
49
+ static getProjectPrefixFromRegistryUUID(uri: string) {
50
+ const matches = uri.match(Fetcher.registryUUIDRegex);
51
+ if (!matches) {
52
+ return undefined;
53
+ }
54
+
55
+ return matches.groups.project;
56
+ }
57
+
58
+ async load() {
59
+ if (typeof this.uri !== 'string') {
60
+ throw new TypeError(
61
+ "Something disastrous occurred and a non-string URI was supplied to the Fetcher library. This shouldn't have happened!"
62
+ );
63
+ }
64
+
65
+ return Promise.resolve(this.uri)
66
+ .then(uri => {
67
+ let url;
68
+ try {
69
+ url = new URL(uri);
70
+ } catch (err) {
71
+ // If that try fails for whatever reason than the URI that we have isn't a real URL and
72
+ // we can safely attempt to look for it on the filesystem.
73
+ return Fetcher.getFile(uri);
74
+ }
75
+
76
+ return Fetcher.getURL(url.href);
77
+ })
78
+ .then(res => Fetcher.validate(res))
79
+ .then(res => res as OASDocument);
80
+ }
81
+
82
+ static getURL(url: string) {
83
+ // @todo maybe include our user-agent here to identify our request
84
+ return fetch(url).then(res => {
85
+ if (!res.ok) {
86
+ throw new Error(`Unable to retrieve URL (${url}). Reason: ${res.statusText}`);
87
+ }
88
+
89
+ if (res.headers.get('content-type') === 'application/yaml' || /\.(yaml|yml)/.test(url)) {
90
+ return res.text().then(text => {
91
+ return yaml.load(text);
92
+ });
93
+ }
94
+
95
+ return res.json();
96
+ });
97
+ }
98
+
99
+ static getFile(uri: string) {
100
+ // Support relative paths by resolving them against the cwd.
101
+ const file = path.resolve(process.cwd(), uri);
102
+
103
+ if (!fs.existsSync(file)) {
104
+ throw new Error(
105
+ `Sorry, we were unable to load an API definition from ${file}. Please either supply a URL or a path on your filesystem.`
106
+ );
107
+ }
108
+
109
+ return Promise.resolve(fs.readFileSync(file, 'utf8')).then((res: string) => {
110
+ if (/\.(yaml|yml)/.test(file)) {
111
+ return yaml.load(res);
112
+ }
113
+
114
+ return JSON.parse(res);
115
+ });
116
+ }
117
+
118
+ static validate(json: any) {
119
+ if (json.swagger) {
120
+ throw new Error('Sorry, this module only supports OpenAPI definitions.');
121
+ }
122
+
123
+ // The `validate` method handles dereferencing for us.
124
+ return OpenAPIParser.validate(json, {
125
+ dereference: {
126
+ /**
127
+ * If circular `$refs` are ignored they'll remain in the API definition as `$ref: String`.
128
+ * This allows us to not only do easy circular reference detection but also stringify and
129
+ * save dereferenced API definitions back into the cache directory.
130
+ */
131
+ circular: 'ignore',
132
+ },
133
+ }).catch(err => {
134
+ if (/is not a valid openapi definition/i.test(err.message)) {
135
+ throw new Error("Sorry, that doesn't look like a valid OpenAPI definition.");
136
+ }
137
+
138
+ throw err;
139
+ });
140
+ }
141
+ }
package/src/index.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type { Operation } from 'oas';
2
+ import type { HttpMethods, OASDocument } from 'oas/@types/rmoas.types';
3
+ import type { ConfigOptions } from './core';
4
+
5
+ import Oas from 'oas';
6
+ import APICore from './core';
7
+
8
+ import Cache from './cache';
9
+
10
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './packageInfo';
11
+
12
+ interface SDKOptions {
13
+ cacheDir?: string;
14
+ }
15
+
16
+ class Sdk {
17
+ uri: string | OASDocument;
18
+
19
+ userAgent: string;
20
+
21
+ cacheDir: string | false;
22
+
23
+ constructor(uri: string | OASDocument, opts: SDKOptions = {}) {
24
+ this.uri = uri;
25
+ this.userAgent = `${PACKAGE_NAME} (node)/${PACKAGE_VERSION}`;
26
+
27
+ this.cacheDir = opts.cacheDir ? opts.cacheDir : false;
28
+ }
29
+
30
+ load() {
31
+ const cache = new Cache(this.uri, this.cacheDir);
32
+ const userAgent = this.userAgent;
33
+
34
+ const core = new APICore();
35
+ core.setUserAgent(userAgent);
36
+
37
+ let isLoaded = false;
38
+ let isCached = cache.isCached();
39
+ let sdk = {};
40
+
41
+ /**
42
+ * Create dynamic accessors for every HTTP method that the OpenAPI specification supports.
43
+ *
44
+ * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-7}
45
+ * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#fixed-fields-7}
46
+ */
47
+ function loadMethods() {
48
+ return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']
49
+ .map(httpVerb => {
50
+ return {
51
+ [httpVerb]: ((method: string, path: string, ...args: unknown[]) => {
52
+ return core.fetch(path, method as HttpMethods, ...args);
53
+ }).bind(null, httpVerb),
54
+ };
55
+ })
56
+ .reduce((prev, next) => Object.assign(prev, next));
57
+ }
58
+
59
+ /**
60
+ * Create dynamic accessors for every operation with a defined operation ID. If an operation
61
+ * does not have an operation ID it can be accessed by its `.method('/path')` accessor instead.
62
+ *
63
+ * @param spec
64
+ */
65
+ function loadOperations(spec: Oas) {
66
+ return Object.entries(spec.getPaths())
67
+ .map(([, operations]) => Object.values(operations))
68
+ .reduce((prev, next) => prev.concat(next), [])
69
+ .filter(operation => operation.hasOperationId())
70
+ .reduce((prev, next) => {
71
+ // `getOperationId()` creates dynamic operation IDs when one isn't available but we need
72
+ // to know here if we actually have one present or not. The `camelCase` option here also
73
+ // cleans up any `operationId` that we might have into something that can be used as a
74
+ // valid JS method.
75
+ const operationId = next.getOperationId({ camelCase: true });
76
+
77
+ return Object.assign(prev, {
78
+ [operationId]: ((operation: Operation, ...args: unknown[]) => {
79
+ return core.fetchOperation(operation, ...args);
80
+ }).bind(null, next),
81
+ });
82
+ }, {});
83
+ }
84
+
85
+ async function loadFromCache() {
86
+ let cachedSpec;
87
+ if (isCached) {
88
+ cachedSpec = await cache.get();
89
+ } else {
90
+ cachedSpec = await cache.load();
91
+ isCached = true;
92
+ }
93
+
94
+ const spec = new Oas(cachedSpec);
95
+
96
+ core.setSpec(spec);
97
+
98
+ sdk = Object.assign(sdk, {
99
+ ...loadMethods(),
100
+ ...loadOperations(spec),
101
+ });
102
+
103
+ isLoaded = true;
104
+ }
105
+
106
+ const sdkProxy = {
107
+ // @give this a better type than any
108
+ get(target: any, method: string) {
109
+ // Since auth returns a self-proxy, we **do not** want it to fall through into the async
110
+ // function below as when that'll happen, instead of returning a self-proxy, it'll end up
111
+ // returning a Promise. When that happens, chaining `sdk.auth().operationId()` will fail.
112
+ if (['auth', 'config'].includes(method)) {
113
+ // @todo split this up so we have better types for `auth` and `config`
114
+ return function (...args: any) {
115
+ return target[method].apply(this, args);
116
+ };
117
+ }
118
+
119
+ return async function (...args: unknown[]) {
120
+ if (!(method in target)) {
121
+ // If this method doesn't exist on the proxy, have we loaded the SDK? If we have, then
122
+ // this method isn't valid.
123
+ if (isLoaded) {
124
+ throw new Error(`Sorry, \`${method}\` does not appear to be a valid operation on this API.`);
125
+ }
126
+
127
+ await loadFromCache();
128
+
129
+ // If after loading the SDK and this method still doesn't exist, then it's not real!
130
+ if (!(method in sdk)) {
131
+ throw new Error(`Sorry, \`${method}\` does not appear to be a valid operation on this API.`);
132
+ }
133
+
134
+ // @todo give sdk a better type
135
+ return (sdk as any)[method].apply(this, args);
136
+ }
137
+
138
+ return target[method].apply(this, args);
139
+ };
140
+ },
141
+ };
142
+
143
+ sdk = {
144
+ /**
145
+ * If the API you're using requires authentication you can supply the required credentials
146
+ * through this method and the library will magically determine how they should be used
147
+ * within your API request.
148
+ *
149
+ * With the exception of OpenID and MutualTLS, it supports all forms of authentication
150
+ * supported by the OpenAPI specification.
151
+ *
152
+ * @example <caption>HTTP Basic auth</caption>
153
+ * sdk.auth('username', 'password');
154
+ *
155
+ * @example <caption>Bearer tokens (HTTP or OAuth 2)</caption>
156
+ * sdk.auth('myBearerToken');
157
+ *
158
+ * @example <caption>API Keys</caption>
159
+ * sdk.auth('myApiKey');
160
+ *
161
+ * @see {@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}
162
+ * @see {@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}
163
+ * @param values Your auth credentials for the API. Can specify up to two strings or numbers.
164
+ */
165
+ auth: (...values: string[] | number[]) => {
166
+ core.setAuth(...values);
167
+ },
168
+
169
+ /**
170
+ * Optionally configure various options, such as response parsing, that the SDK allows.
171
+ *
172
+ * @param config Object of supported SDK options and toggles.
173
+ * @param config.parseResponse If responses are parsed according to its `Content-Type` header.
174
+ */
175
+ config: (config: ConfigOptions) => {
176
+ core.setConfig(config);
177
+ },
178
+
179
+ /**
180
+ * If the API you're using offers alternate server URLs, and server variables, you can tell
181
+ * the SDK which one to use with this method. To use it you can supply either one of the
182
+ * server URLs that are contained within the OpenAPI definition (along with any server
183
+ * variables), or you can pass it a fully qualified URL to use (that may or may not exist
184
+ * within the OpenAPI definition).
185
+ *
186
+ * @example <caption>Server URL with server variables</caption>
187
+ * sdk.server('https://{region}.api.example.com/{basePath}', {
188
+ * name: 'eu',
189
+ * basePath: 'v14',
190
+ * });
191
+ *
192
+ * @example <caption>Fully qualified server URL</caption>
193
+ * sdk.server('https://eu.api.example.com/v14');
194
+ *
195
+ * @param url Server URL
196
+ * @param variables An object of variables to replace into the server URL.
197
+ */
198
+ server: (url: string, variables = {}) => {
199
+ core.setServer(url, variables);
200
+ },
201
+ };
202
+
203
+ return new Proxy(sdk, sdkProxy);
204
+ }
205
+ }
206
+
207
+ // Why `export` vs `export default`? If we leave this as `export` then TS will transpile it into
208
+ // a `module.exports` export so that when folks load this they don't need to load it as
209
+ // `require('api').default`.
210
+ export = (uri: string | OASDocument, opts: SDKOptions = {}) => {
211
+ return new Sdk(uri, opts).load();
212
+ };
@@ -0,0 +1,3 @@
1
+ // This file is automatically updated by the build script.
2
+ export const PACKAGE_NAME = 'api';
3
+ export const PACKAGE_VERSION = '5.0.0-beta.2';
@@ -0,0 +1,3 @@
1
+ // These libraries don't have any types so we need to let TS know so we can use them.
2
+ declare module 'fetch-har';
3
+ declare module '@readme/oas-to-har';
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "baseUrl": "./src",
5
+ "declaration": true,
6
+ "esModuleInterop": true,
7
+ "lib": ["dom", "es2020"],
8
+ "noImplicitAny": true,
9
+ "outDir": "dist/",
10
+ "paths": {
11
+ // Because this library uses ES2015+ `#private` syntax that would require us to maket his
12
+ // library ESM-only we're overloading its types with a `paths` config with this empty file.
13
+ // This isn't a great solution as we're losing type checks where this library is used, but
14
+ // it's far too early in the ESM lifecycle for us to make API an ESM-only library.
15
+ //
16
+ // And though TS offers an unstable `node12` module resolution that lets us manage this in
17
+ // another way that module resolution requires TS nightlies to be installed, which no thanks!
18
+ //
19
+ // https://github.com/microsoft/TypeScript/issues/17042
20
+ "form-data-encoder": [".sink.d.ts"]
21
+ }
22
+ },
23
+ "include": ["./src/**/*"]
24
+ }
package/src/cache.js DELETED
@@ -1,225 +0,0 @@
1
- const fetch = require('node-fetch');
2
- const OpenAPIParser = require('@readme/openapi-parser');
3
- const yaml = require('js-yaml');
4
- const crypto = require('crypto');
5
- const findCacheDir = require('find-cache-dir');
6
- const pkg = require('../package.json');
7
- const fs = require('fs');
8
- const os = require('os');
9
- const path = require('path');
10
- const makeDir = require('make-dir');
11
-
12
- class Cache {
13
- constructor(uri, cacheDir = false) {
14
- /**
15
- * Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry.
16
- *
17
- * Examples:
18
- * - @petstore/v1.0#n6kvf10vakpemvplx
19
- * - @petstore#n6kvf10vakpemvplx
20
- *
21
- * @param {String} u
22
- * @returns {String}
23
- */
24
- const resolveReadMeRegistryAccessor = u =>
25
- typeof u === 'string'
26
- ? u.replace(/^@[a-zA-Z0-9-_]+\/?(.+)#([a-z0-9]+)$/, 'https://dash.readme.io/api/v1/api-registry/$2')
27
- : u;
28
-
29
- this.uri = resolveReadMeRegistryAccessor(uri);
30
- this.uriHash = Cache.getCacheHash(this.uri);
31
-
32
- if (cacheDir) {
33
- this.dir = cacheDir;
34
- } else {
35
- this.dir = findCacheDir({ name: pkg.name });
36
- if (typeof this.dir === 'undefined') {
37
- // The `find-cache-dir` module returns `undefined` if the `node_modules/` directory isn't
38
- // writable, or there's no `package.json` in the root-most directory. If this happens, we
39
- // can instead adhoc create a cache directory in the users OS temp directory and store our
40
- // data there.
41
- //
42
- // @link https://github.com/avajs/find-cache-dir/issues/29
43
- this.dir = makeDir.sync(path.join(os.tmpdir(), pkg.name));
44
- }
45
- }
46
-
47
- this.cacheStore = path.join(this.dir, 'cache.json');
48
- this.specsCache = path.join(this.dir, 'specs');
49
-
50
- // This should default to false so we have awareness if we've looked at the cache yet.
51
- this.cached = false;
52
- }
53
-
54
- static getCacheHash(file) {
55
- let data = file;
56
- if (typeof file === 'object') {
57
- // Under certain unit testing circumstances, we might be supplying the class with a raw JSON object so we'll need
58
- // to convert it to a string in order to hand it off to the crypto module.
59
- data = JSON.stringify(file);
60
- }
61
-
62
- return crypto.createHash('md5').update(data).digest('hex');
63
- }
64
-
65
- isCached() {
66
- const cache = this.getCache();
67
- return cache && this.uriHash in cache;
68
- }
69
-
70
- getCache() {
71
- if (typeof this.cached === 'object') {
72
- return this.cached;
73
- }
74
-
75
- this.cached = {};
76
-
77
- if (fs.existsSync(this.cacheStore)) {
78
- this.cached = JSON.parse(fs.readFileSync(this.cacheStore, 'utf8'));
79
- }
80
-
81
- return this.cached;
82
- }
83
-
84
- get() {
85
- // If the class was supplied a raw object, just go ahead and bypass the caching system and return that.
86
- if (typeof this.uri === 'object') {
87
- return this.uri;
88
- }
89
-
90
- if (!this.isCached()) {
91
- throw new Error(`${this.uri} has not been cached yet and must do so before being retrieved.`);
92
- }
93
-
94
- const cache = this.getCache();
95
-
96
- // Prior to v4.5.0 we were putting a fully resolved path to the API definition in the cache
97
- // store but if you had specified a custom caching directory and would generate the cache on
98
- // your system, that filepath would obviously not be the same in other environments. For this
99
- // reason the `path` was removed from the cache store in favor of storing the `hash` instead.
100
- //
101
- // If we still have `path` in the config cache for backwards compatibility we should use it.
102
- if ('path' in cache[this.uriHash]) {
103
- return JSON.parse(fs.readFileSync(cache[this.uriHash].path, 'utf8'));
104
- }
105
-
106
- return JSON.parse(fs.readFileSync(path.join(this.specsCache, `${cache[this.uriHash].hash}.json`)));
107
- }
108
-
109
- async load() {
110
- // If the class was supplied a raw object, just go ahead and bypass the caching system and return that.
111
- if (typeof this.uri === 'object') {
112
- return this.uri;
113
- }
114
-
115
- try {
116
- const url = new URL(this.uri);
117
- this.uri = url.href;
118
-
119
- return this.saveUrl();
120
- } catch (err) {
121
- // Support relative paths by resolving them against the cwd.
122
- this.uri = path.resolve(process.cwd(), this.uri);
123
-
124
- if (!fs.existsSync(this.uri)) {
125
- throw new Error(
126
- `Sorry, we were unable to load that OpenAPI definition. Please either supply a URL or a path on your filesystem.`
127
- );
128
- }
129
-
130
- return this.saveFile();
131
- }
132
- }
133
-
134
- save(json) {
135
- const self = this;
136
-
137
- if (json.swagger) {
138
- throw new Error('Sorry, this module only supports OpenAPI definitions.');
139
- }
140
-
141
- return new Promise(resolve => {
142
- resolve(json);
143
- })
144
- .then(res => {
145
- // The `validate` method handles dereferencing for us.
146
- return OpenAPIParser.validate(res, {
147
- dereference: {
148
- // If circular `$refs` are ignored they'll remain in the API definition as `$ref: String`. This allows us to
149
- // not only do easy circular reference detection but also stringify and save dereferenced API definitions
150
- // back into the cache directory.
151
- circular: 'ignore',
152
- },
153
- }).catch(err => {
154
- if (/is not a valid openapi definition/i.test(err.message)) {
155
- throw new Error("Sorry, that doesn't look like a valid OpenAPI definition.");
156
- }
157
-
158
- throw err;
159
- });
160
- })
161
- .then(async spec => {
162
- if (!fs.existsSync(self.dir)) {
163
- fs.mkdirSync(self.dir, { recursive: true });
164
- }
165
-
166
- if (!fs.existsSync(self.specsCache)) {
167
- fs.mkdirSync(self.specsCache, { recursive: true });
168
- }
169
-
170
- const cache = self.getCache();
171
- if (!(self.uriHash in cache)) {
172
- const saved = JSON.stringify(spec, null, 2);
173
- const fileHash = crypto.createHash('md5').update(saved).digest('hex');
174
-
175
- cache[self.uriHash] = {
176
- hash: fileHash,
177
- original: self.uri,
178
- title: 'title' in spec.info ? spec.info.title : undefined,
179
- version: 'version' in spec.info ? spec.info.version : undefined,
180
- };
181
-
182
- fs.writeFileSync(path.join(self.specsCache, `${fileHash}.json`), saved);
183
- fs.writeFileSync(self.cacheStore, JSON.stringify(cache, null, 2));
184
-
185
- self.cache = cache;
186
- }
187
-
188
- return spec;
189
- });
190
- }
191
-
192
- saveUrl() {
193
- return fetch(this.uri)
194
- .then(res => {
195
- if (!res.ok) {
196
- throw new Error(`Unable to retrieve URL. Reason: ${res.statusText}`);
197
- }
198
-
199
- if (res.headers.get('content-type') === 'application/yaml' || /\.(yaml|yml)/.test(this.uri)) {
200
- return res.text().then(text => {
201
- return yaml.load(text);
202
- });
203
- }
204
-
205
- return res.json();
206
- })
207
- .then(json => this.save(json));
208
- }
209
-
210
- saveFile() {
211
- return new Promise(resolve => {
212
- resolve(fs.readFileSync(this.uri, 'utf8'));
213
- })
214
- .then(res => {
215
- if (/\.(yaml|yml)/.test(this.uri)) {
216
- return yaml.load(res);
217
- }
218
-
219
- return JSON.parse(res);
220
- })
221
- .then(json => this.save(json));
222
- }
223
- }
224
-
225
- module.exports = Cache;