httpsnippet-client-api 5.0.0-beta.0 → 5.0.0-beta.3

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.
package/README.md CHANGED
@@ -6,7 +6,6 @@ An [HTTPSnippet](https://npm.im/httpsnippet) client for generating snippets for
6
6
 
7
7
  [![](https://d3vv6lp55qjaqc.cloudfront.net/items/1M3C3j0I0s0j3T362344/Untitled-2.png)](https://readme.io)
8
8
 
9
-
10
9
  ## Installation
11
10
 
12
11
  ```sh
@@ -16,10 +15,10 @@ npm install --save httpsnippet-client-api
16
15
  ## Usage
17
16
 
18
17
  ```js
19
- const httpsnippet = require('httpsnippet');
20
- const client = require('httpsnippet-client-api');
18
+ import { HTTPSnippet, addTargetClient } from 'httpsnippet';
19
+ import client = require('httpsnippet-client-api');
21
20
 
22
- HTTPSnippet.addTargetClient('node', client);
21
+ addTargetClient('node', client);
23
22
 
24
23
  const har = {
25
24
  "log": {
@@ -59,7 +58,8 @@ Results in the following:
59
58
  const sdk = require('api')('https://api.example.com/openapi.json');
60
59
 
61
60
  sdk.auth('a5a220e');
62
- sdk.put('/apiKey')
61
+ sdk
62
+ .put('/apiKey')
63
63
  .then(res => console.log(res))
64
64
  .catch(err => console.error(err));
65
65
  ```
@@ -0,0 +1,10 @@
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
+ import type { Client } from '@readme/httpsnippet/dist/targets/targets';
3
+ export interface APIOptions {
4
+ apiDefinition: OASDocument;
5
+ apiDefinitionUri: string;
6
+ indent?: string | false;
7
+ escapeBrackets?: boolean;
8
+ }
9
+ declare const client: Client<APIOptions>;
10
+ export default client;
package/dist/index.js ADDED
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __rest = (this && this.__rest) || function (s, e) {
14
+ var t = {};
15
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
16
+ t[p] = s[p];
17
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
18
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
19
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
20
+ t[p[i]] = s[p[i]];
21
+ }
22
+ return t;
23
+ };
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ exports.__esModule = true;
28
+ var stringify_object_1 = __importDefault(require("stringify-object"));
29
+ var code_builder_1 = require("@readme/httpsnippet/dist/helpers/code-builder");
30
+ var content_type_1 = __importDefault(require("content-type"));
31
+ var oas_1 = __importDefault(require("oas"));
32
+ function stringify(obj, opts) {
33
+ if (opts === void 0) { opts = {}; }
34
+ return (0, stringify_object_1["default"])(obj, __assign({ indent: ' ' }, opts));
35
+ }
36
+ function buildAuthSnippet(authKey) {
37
+ // Auth key will be an array for Basic auth cases.
38
+ if (Array.isArray(authKey)) {
39
+ var auth_1 = [];
40
+ authKey.forEach(function (token, i) {
41
+ // If the token part is the last part of the key and it's empty, don't add it to the snippet.
42
+ if (token.length === 0 && authKey.length > 1 && i === authKey.length - 1) {
43
+ return;
44
+ }
45
+ auth_1.push("'".concat(token.replace(/'/g, "\\'"), "'"));
46
+ });
47
+ return "sdk.auth(".concat(auth_1.join(', '), ");");
48
+ }
49
+ return "sdk.auth('".concat(authKey.replace(/'/g, "\\'"), "');");
50
+ }
51
+ function getAuthSources(operation) {
52
+ var matchers = {
53
+ header: {},
54
+ query: [],
55
+ cookie: []
56
+ };
57
+ if (operation.getSecurity().length === 0) {
58
+ return matchers;
59
+ }
60
+ var security = operation.prepareSecurity();
61
+ Object.keys(security).forEach(function (id) {
62
+ security[id].forEach(function (scheme) {
63
+ if (scheme.type === 'http') {
64
+ if (scheme.scheme === 'basic') {
65
+ matchers.header.authorization = 'Basic';
66
+ }
67
+ else if (scheme.scheme === 'bearer') {
68
+ matchers.header.authorization = 'Bearer';
69
+ }
70
+ }
71
+ else if (scheme.type === 'oauth2') {
72
+ matchers.header.authorization = 'Bearer';
73
+ }
74
+ else if (scheme.type === 'apiKey') {
75
+ if (scheme["in"] === 'query') {
76
+ matchers.query.push(scheme.name);
77
+ }
78
+ else if (scheme["in"] === 'header') {
79
+ // The way that this asterisk header matcher works is that since this `apiKey` goes in a
80
+ // named header (`scheme.name`) because the header is the key, we're matching against the
81
+ // entire header -- counter to the way that the HTTP basic matcher above works where we
82
+ // match and extract the API key from everything after `Basic ` in the `Authorization`
83
+ // header.
84
+ matchers.header[scheme.name.toLowerCase()] = '*';
85
+ }
86
+ else if (scheme["in"] === 'cookie') {
87
+ matchers.cookie.push(scheme.name);
88
+ }
89
+ }
90
+ });
91
+ });
92
+ return matchers;
93
+ }
94
+ var client = {
95
+ info: {
96
+ key: 'api',
97
+ title: 'API',
98
+ link: 'https://npm.im/api',
99
+ description: 'Automatic SDK generation from an OpenAPI definition.'
100
+ },
101
+ convert: function (_a, options) {
102
+ var cookiesObj = _a.cookiesObj, headersObj = _a.headersObj, postData = _a.postData, queryObj = _a.queryObj, url = _a.url, source = __rest(_a, ["cookiesObj", "headersObj", "postData", "queryObj", "url"]);
103
+ var opts = __assign({}, options);
104
+ if (!('apiDefinitionUri' in opts)) {
105
+ throw new Error('This HTTP Snippet client must have an `apiDefinitionUri` option supplied to it.');
106
+ }
107
+ else if (!('apiDefinition' in opts)) {
108
+ throw new Error('This HTTP Snippet client must have an `apiDefinition` option supplied to it.');
109
+ }
110
+ var method = source.method.toLowerCase();
111
+ var oas = new oas_1["default"](opts.apiDefinition);
112
+ var apiDefinition = oas.getDefinition();
113
+ var foundOperation = oas.findOperation(url, method);
114
+ if (!foundOperation) {
115
+ throw new Error("Unable to locate a matching operation in the supplied `apiDefinition` for: ".concat(source.method, " ").concat(url));
116
+ }
117
+ var operationSlugs = foundOperation.url.slugs;
118
+ var operation = oas.operation(foundOperation.url.nonNormalizedPath, method);
119
+ var path = operation.path;
120
+ var authData = [];
121
+ var authSources = getAuthSources(operation);
122
+ var _b = new code_builder_1.CodeBuilder({ indent: opts.indent || ' ' }), blank = _b.blank, push = _b.push, join = _b.join;
123
+ push("const sdk = require('api')('".concat(opts.apiDefinitionUri, "');"));
124
+ blank();
125
+ // If we have multiple servers configured and our source URL differs from the stock URL that we
126
+ // receive from our `oas` library then the URL either has server variables contained in it (that
127
+ // don't match the defaults), or the OAS offers alternate server URLs and we should expose that
128
+ // in the generated snippet.
129
+ var configData = [];
130
+ if ((apiDefinition.servers || []).length > 1) {
131
+ var stockUrl = oas.url();
132
+ var baseUrl = url.replace(path, '');
133
+ if (baseUrl !== stockUrl) {
134
+ var serverVars = oas.splitVariables(baseUrl);
135
+ var serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl;
136
+ configData.push("sdk.server('".concat(serverUrl, "');"));
137
+ }
138
+ }
139
+ var metadata = {};
140
+ Object.keys(queryObj).forEach(function (param) {
141
+ if (authSources.query.includes(param)) {
142
+ authData.push(buildAuthSnippet(queryObj[param]));
143
+ // If this query param is part of an auth source then we don't want it doubled up in the
144
+ // snippet.
145
+ return;
146
+ }
147
+ metadata[param] = queryObj[param];
148
+ });
149
+ Object.keys(cookiesObj).forEach(function (cookie) {
150
+ if (authSources.cookie.includes(cookie)) {
151
+ authData.push(buildAuthSnippet(cookiesObj[cookie]));
152
+ // If this cookie is part of an auth source then we don't want it doubled up.
153
+ return;
154
+ }
155
+ // Note that we may have the potential to overlap any cookie that also shares the name as
156
+ // another metadata parameter. This problem is currently inherent to `api` and not this
157
+ // snippet generator.
158
+ metadata[cookie] = cookiesObj[cookie];
159
+ });
160
+ // If we have path parameters present, we should only add them in if we have an `operationId` as
161
+ // we don't want metadata to duplicate what we'll be setting the path in the snippet to.
162
+ if (operation.hasOperationId()) {
163
+ Array.from(Object.entries(operationSlugs)).forEach(function (_a) {
164
+ var param = _a[0], value = _a[1];
165
+ // The keys in `operationSlugs` will always be prefixed with a `:` in the `oas` library so
166
+ // we can safely do this substring here without asserting this context.
167
+ metadata[param.substring(1)] = value;
168
+ });
169
+ }
170
+ if (Object.keys(headersObj).length) {
171
+ var headers_1 = headersObj;
172
+ var requestHeaders_1 = {};
173
+ Object.keys(headers_1).forEach(function (header) {
174
+ // Headers in HTTPSnippet are case-insensitive so we need to add in some special handling to
175
+ // make sure we're able to match them properly.
176
+ var headerLower = header.toLowerCase();
177
+ if (headerLower in authSources.header) {
178
+ // If this header has been set up as an authentication header, let's remove it and add it
179
+ // into our auth data so we can build up an `.auth()` snippet for the SDK.
180
+ var authScheme = authSources.header[headerLower];
181
+ if (authScheme === '*') {
182
+ authData.push(buildAuthSnippet(headers_1[header]));
183
+ }
184
+ else {
185
+ // @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
186
+ var authKey = headers_1[header].replace("".concat(authSources.header[headerLower], " "), '');
187
+ if (authScheme.toLowerCase() === 'basic') {
188
+ authKey = Buffer.from(authKey, 'base64').toString('ascii');
189
+ authKey = authKey.split(':');
190
+ }
191
+ authData.push(buildAuthSnippet(authKey));
192
+ }
193
+ delete headers_1[header];
194
+ return;
195
+ }
196
+ else if (headerLower === 'content-type') {
197
+ // `Content-Type` headers are automatically added within the SDK so we can filter them out
198
+ // if they don't have parameters attached to them.
199
+ // @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
200
+ var parsedContentType = content_type_1["default"].parse(headers_1[header]);
201
+ if (!Object.keys(parsedContentType.parameters).length) {
202
+ delete headers_1[header];
203
+ return;
204
+ }
205
+ }
206
+ else if (headerLower === 'accept') {
207
+ // If the `Accept` header here is not the default or first `Accept` header for the
208
+ // operations' request body then we should add it otherwise we can let the SDK handle it
209
+ // itself.
210
+ if (headers_1[header] === operation.getContentType()) {
211
+ delete headers_1[header];
212
+ return;
213
+ }
214
+ }
215
+ // If we haven't used our header anywhere else, or we've deleted it from the payload
216
+ // because it'll be handled internally by `api` then we should add the lowercased version
217
+ // of our header into the generated code snippet.
218
+ requestHeaders_1[headerLower] = headers_1[header];
219
+ });
220
+ if (Object.keys(requestHeaders_1).length > 0) {
221
+ metadata = Object.assign(metadata, requestHeaders_1);
222
+ }
223
+ }
224
+ var body;
225
+ switch (postData.mimeType) {
226
+ case 'application/x-www-form-urlencoded':
227
+ body = postData.paramsObj;
228
+ break;
229
+ case 'application/json':
230
+ if (postData.jsonObj) {
231
+ body = postData.jsonObj;
232
+ }
233
+ break;
234
+ case 'multipart/form-data':
235
+ if (postData.params) {
236
+ body = {};
237
+ // If there's a `Content-Type` header present in the metadata, but it's for the
238
+ // `multipart/form-data` request then dump it off the snippet. We shouldn't offload that
239
+ // unnecessary bloat of multipart boundaries to the user, instead letting the SDK handle it
240
+ // automatically.
241
+ if ('content-type' in metadata && metadata['content-type'].indexOf('multipart/form-data') === 0) {
242
+ delete metadata['content-type'];
243
+ }
244
+ postData.params.forEach(function (param) {
245
+ if (param.fileName) {
246
+ body[param.name] = param.fileName;
247
+ }
248
+ else {
249
+ body[param.name] = param.value;
250
+ }
251
+ });
252
+ }
253
+ break;
254
+ default:
255
+ if (postData.text) {
256
+ body = postData.text;
257
+ }
258
+ }
259
+ var args = [];
260
+ var accessor = method;
261
+ if (operation.hasOperationId()) {
262
+ accessor = operation.getOperationId({ camelCase: true });
263
+ }
264
+ else {
265
+ // Since we're not using an operationId as our primary accessor we need to take the current
266
+ // operation that we're working with and transpile back our path parameters on top of it.
267
+ var slugs = Object.fromEntries(Object.keys(operationSlugs).map(function (slug) { return [slug.replace(/:(.*)/, '$1'), operationSlugs[slug]]; }));
268
+ args.push("'".concat(decodeURIComponent(oas.replaceUrl(path, slugs)), "'"));
269
+ }
270
+ // If we're going to be rendering out body params and metadata we should cut their character
271
+ // limit in half because we'll be rendering them in their own lines.
272
+ var inlineCharacterLimit = typeof body !== 'undefined' && Object.keys(metadata).length > 0 ? 40 : 80;
273
+ if (typeof body !== 'undefined') {
274
+ args.push(stringify(body, { inlineCharacterLimit: inlineCharacterLimit }));
275
+ }
276
+ if (Object.keys(metadata).length > 0) {
277
+ args.push(stringify(metadata, { inlineCharacterLimit: inlineCharacterLimit }));
278
+ }
279
+ if (authData.length) {
280
+ push(authData.join('\n'));
281
+ }
282
+ if (configData.length) {
283
+ push(configData.join('\n'));
284
+ }
285
+ push("sdk.".concat(accessor, "(").concat(args.join(', '), ")"));
286
+ push('.then(res => console.log(res))', 1);
287
+ push('.catch(err => console.error(err));', 1);
288
+ return join();
289
+ }
290
+ };
291
+ exports["default"] = client;
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "httpsnippet-client-api",
3
- "version": "5.0.0-beta.0",
3
+ "version": "5.0.0-beta.3",
4
4
  "description": "An HTTPSnippet client for generating snippets for the `api` module.",
5
- "main": "src/index.js",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
6
7
  "scripts": {
7
- "lint": "eslint .",
8
- "pretest": "npm run lint",
9
- "prettier": "prettier --list-different --write \"./**/**.js\"",
10
- "test": "nyc mocha \"test/**/*.test.js\"",
11
- "test:watch": "nyc mocha \"test/**/*.test.js\" --watch"
8
+ "build": "tsc",
9
+ "prebuild": "rm -rf dist/",
10
+ "prepack": "npm run build",
11
+ "test": "nyc mocha \"test/**/*.test.ts\""
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
@@ -28,24 +28,30 @@
28
28
  "stringify-object": "^3.3.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@readme/httpsnippet": "^3.0.0",
32
- "oas": "^18.0.1"
31
+ "@readme/httpsnippet": "^4.0.3",
32
+ "oas": "^18.3.4"
33
33
  },
34
34
  "devDependencies": {
35
- "@readme/eslint-config": "^8.7.3",
36
35
  "@readme/oas-examples": "^5.4.1",
37
36
  "@readme/openapi-parser": "^2.2.0",
38
- "api": "^5.0.0-beta.0",
37
+ "@types/content-type": "^1.1.5",
38
+ "@types/stringify-object": "^4.0.1",
39
+ "api": "^5.0.0-beta.3",
39
40
  "chai": "^4.3.6",
40
- "eslint": "^8.14.0",
41
41
  "fetch-mock": "^9.11.0",
42
42
  "isomorphic-fetch": "^3.0.0",
43
43
  "mocha": "^10.0.0",
44
44
  "nyc": "^15.1.0",
45
- "prettier": "^2.6.2",
46
45
  "sinon": "^14.0.0",
47
- "sinon-chai": "^3.7.0"
46
+ "sinon-chai": "^3.7.0",
47
+ "typescript": "^4.7.4"
48
48
  },
49
49
  "prettier": "@readme/eslint-config/prettier",
50
- "gitHead": "d18f7e3a1c20edaf84d0c5c2e1a64714549a4ee6"
50
+ "nyc": {
51
+ "exclude": [
52
+ "dist/",
53
+ "test/"
54
+ ]
55
+ },
56
+ "gitHead": "24d5b83545735176786d212a69121a029cf6dea1"
51
57
  }
package/src/index.ts ADDED
@@ -0,0 +1,318 @@
1
+ import type { Operation } from 'oas';
2
+ import type { HttpMethods, OASDocument } from 'oas/dist/rmoas.types';
3
+ import type { Client } from '@readme/httpsnippet/dist/targets/targets';
4
+ import type { ReducedHelperObject } from '@readme/httpsnippet/dist/helpers/reducer';
5
+
6
+ import stringifyObject from 'stringify-object';
7
+ import { CodeBuilder } from '@readme/httpsnippet/dist/helpers/code-builder';
8
+ import contentType from 'content-type';
9
+ import Oas from 'oas';
10
+
11
+ // This should really be an exported type in `oas`.
12
+ type SecurityType = 'Basic' | 'Bearer' | 'Query' | 'Header' | 'Cookie' | 'OAuth2' | 'http' | 'apiKey';
13
+
14
+ function stringify(obj: any, opts = {}) {
15
+ return stringifyObject(obj, { indent: ' ', ...opts });
16
+ }
17
+
18
+ function buildAuthSnippet(authKey: string | string[]) {
19
+ // Auth key will be an array for Basic auth cases.
20
+ if (Array.isArray(authKey)) {
21
+ const auth: string[] = [];
22
+ authKey.forEach((token, i) => {
23
+ // If the token part is the last part of the key and it's empty, don't add it to the snippet.
24
+ if (token.length === 0 && authKey.length > 1 && i === authKey.length - 1) {
25
+ return;
26
+ }
27
+
28
+ auth.push(`'${token.replace(/'/g, "\\'")}'`);
29
+ });
30
+
31
+ return `sdk.auth(${auth.join(', ')});`;
32
+ }
33
+
34
+ return `sdk.auth('${authKey.replace(/'/g, "\\'")}');`;
35
+ }
36
+
37
+ function getAuthSources(operation: Operation) {
38
+ const matchers: { header: Record<string, string>; query: string[]; cookie: string[] } = {
39
+ header: {},
40
+ query: [],
41
+ cookie: [],
42
+ };
43
+
44
+ if (operation.getSecurity().length === 0) {
45
+ return matchers;
46
+ }
47
+
48
+ const security = operation.prepareSecurity();
49
+ Object.keys(security).forEach((id: SecurityType) => {
50
+ security[id].forEach(scheme => {
51
+ if (scheme.type === 'http') {
52
+ if (scheme.scheme === 'basic') {
53
+ matchers.header.authorization = 'Basic';
54
+ } else if (scheme.scheme === 'bearer') {
55
+ matchers.header.authorization = 'Bearer';
56
+ }
57
+ } else if (scheme.type === 'oauth2') {
58
+ matchers.header.authorization = 'Bearer';
59
+ } else if (scheme.type === 'apiKey') {
60
+ if (scheme.in === 'query') {
61
+ matchers.query.push(scheme.name);
62
+ } else if (scheme.in === 'header') {
63
+ // The way that this asterisk header matcher works is that since this `apiKey` goes in a
64
+ // named header (`scheme.name`) because the header is the key, we're matching against the
65
+ // entire header -- counter to the way that the HTTP basic matcher above works where we
66
+ // match and extract the API key from everything after `Basic ` in the `Authorization`
67
+ // header.
68
+ matchers.header[scheme.name.toLowerCase()] = '*';
69
+ } else if (scheme.in === 'cookie') {
70
+ matchers.cookie.push(scheme.name);
71
+ }
72
+ }
73
+ });
74
+ });
75
+
76
+ return matchers;
77
+ }
78
+
79
+ export interface APIOptions {
80
+ apiDefinition: OASDocument;
81
+ apiDefinitionUri: string;
82
+ indent?: string | false;
83
+ escapeBrackets?: boolean;
84
+ }
85
+
86
+ const client: Client<APIOptions> = {
87
+ info: {
88
+ key: 'api',
89
+ title: 'API',
90
+ link: 'https://npm.im/api',
91
+ description: 'Automatic SDK generation from an OpenAPI definition.',
92
+ },
93
+ convert: ({ cookiesObj, headersObj, postData, queryObj, url, ...source }, options) => {
94
+ const opts = {
95
+ ...options,
96
+ };
97
+
98
+ if (!('apiDefinitionUri' in opts)) {
99
+ throw new Error('This HTTP Snippet client must have an `apiDefinitionUri` option supplied to it.');
100
+ } else if (!('apiDefinition' in opts)) {
101
+ throw new Error('This HTTP Snippet client must have an `apiDefinition` option supplied to it.');
102
+ }
103
+
104
+ const method = source.method.toLowerCase() as HttpMethods;
105
+ const oas = new Oas(opts.apiDefinition);
106
+ const apiDefinition = oas.getDefinition();
107
+ const foundOperation = oas.findOperation(url, method);
108
+ if (!foundOperation) {
109
+ throw new Error(
110
+ `Unable to locate a matching operation in the supplied \`apiDefinition\` for: ${source.method} ${url}`
111
+ );
112
+ }
113
+
114
+ const operationSlugs = foundOperation.url.slugs;
115
+ const operation = oas.operation(foundOperation.url.nonNormalizedPath, method);
116
+ const path = operation.path;
117
+ const authData: string[] = [];
118
+ const authSources = getAuthSources(operation);
119
+
120
+ const { blank, push, join } = new CodeBuilder({ indent: opts.indent || ' ' });
121
+
122
+ push(`const sdk = require('api')('${opts.apiDefinitionUri}');`);
123
+ blank();
124
+
125
+ // If we have multiple servers configured and our source URL differs from the stock URL that we
126
+ // receive from our `oas` library then the URL either has server variables contained in it (that
127
+ // don't match the defaults), or the OAS offers alternate server URLs and we should expose that
128
+ // in the generated snippet.
129
+ const configData = [];
130
+ if ((apiDefinition.servers || []).length > 1) {
131
+ const stockUrl = oas.url();
132
+ const baseUrl = url.replace(path, '');
133
+ if (baseUrl !== stockUrl) {
134
+ const serverVars = oas.splitVariables(baseUrl);
135
+ const serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl;
136
+
137
+ configData.push(`sdk.server('${serverUrl}');`);
138
+ }
139
+ }
140
+
141
+ let metadata: Record<string, string | string[]> = {};
142
+ Object.keys(queryObj).forEach(param => {
143
+ if (authSources.query.includes(param)) {
144
+ authData.push(buildAuthSnippet(queryObj[param]));
145
+
146
+ // If this query param is part of an auth source then we don't want it doubled up in the
147
+ // snippet.
148
+ return;
149
+ }
150
+
151
+ metadata[param] = queryObj[param];
152
+ });
153
+
154
+ Object.keys(cookiesObj).forEach(cookie => {
155
+ if (authSources.cookie.includes(cookie)) {
156
+ authData.push(buildAuthSnippet(cookiesObj[cookie]));
157
+
158
+ // If this cookie is part of an auth source then we don't want it doubled up.
159
+ return;
160
+ }
161
+
162
+ // Note that we may have the potential to overlap any cookie that also shares the name as
163
+ // another metadata parameter. This problem is currently inherent to `api` and not this
164
+ // snippet generator.
165
+ metadata[cookie] = cookiesObj[cookie];
166
+ });
167
+
168
+ // If we have path parameters present, we should only add them in if we have an `operationId` as
169
+ // we don't want metadata to duplicate what we'll be setting the path in the snippet to.
170
+ if (operation.hasOperationId()) {
171
+ Array.from(Object.entries(operationSlugs)).forEach(([param, value]) => {
172
+ // The keys in `operationSlugs` will always be prefixed with a `:` in the `oas` library so
173
+ // we can safely do this substring here without asserting this context.
174
+ metadata[param.substring(1)] = value;
175
+ });
176
+ }
177
+
178
+ if (Object.keys(headersObj).length) {
179
+ const headers = headersObj;
180
+ const requestHeaders: ReducedHelperObject = {};
181
+
182
+ Object.keys(headers).forEach(header => {
183
+ // Headers in HTTPSnippet are case-insensitive so we need to add in some special handling to
184
+ // make sure we're able to match them properly.
185
+ const headerLower = header.toLowerCase();
186
+
187
+ if (headerLower in authSources.header) {
188
+ // If this header has been set up as an authentication header, let's remove it and add it
189
+ // into our auth data so we can build up an `.auth()` snippet for the SDK.
190
+ const authScheme = authSources.header[headerLower];
191
+ if (authScheme === '*') {
192
+ authData.push(buildAuthSnippet(headers[header]));
193
+ } else {
194
+ // @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
195
+ let authKey = headers[header].replace(`${authSources.header[headerLower]} `, '');
196
+ if (authScheme.toLowerCase() === 'basic') {
197
+ authKey = Buffer.from(authKey, 'base64').toString('ascii');
198
+ authKey = authKey.split(':');
199
+ }
200
+
201
+ authData.push(buildAuthSnippet(authKey));
202
+ }
203
+
204
+ delete headers[header];
205
+ return;
206
+ } else if (headerLower === 'content-type') {
207
+ // `Content-Type` headers are automatically added within the SDK so we can filter them out
208
+ // if they don't have parameters attached to them.
209
+ // @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
210
+ const parsedContentType = contentType.parse(headers[header]);
211
+ if (!Object.keys(parsedContentType.parameters).length) {
212
+ delete headers[header];
213
+ return;
214
+ }
215
+ } else if (headerLower === 'accept') {
216
+ // If the `Accept` header here is not the default or first `Accept` header for the
217
+ // operations' request body then we should add it otherwise we can let the SDK handle it
218
+ // itself.
219
+ if (headers[header] === operation.getContentType()) {
220
+ delete headers[header];
221
+ return;
222
+ }
223
+ }
224
+
225
+ // If we haven't used our header anywhere else, or we've deleted it from the payload
226
+ // because it'll be handled internally by `api` then we should add the lowercased version
227
+ // of our header into the generated code snippet.
228
+ requestHeaders[headerLower] = headers[header];
229
+ });
230
+
231
+ if (Object.keys(requestHeaders).length > 0) {
232
+ metadata = Object.assign(metadata, requestHeaders);
233
+ }
234
+ }
235
+
236
+ let body: any;
237
+ switch (postData.mimeType) {
238
+ case 'application/x-www-form-urlencoded':
239
+ body = postData.paramsObj;
240
+ break;
241
+
242
+ case 'application/json':
243
+ if (postData.jsonObj) {
244
+ body = postData.jsonObj;
245
+ }
246
+ break;
247
+
248
+ case 'multipart/form-data':
249
+ if (postData.params) {
250
+ body = {};
251
+
252
+ // If there's a `Content-Type` header present in the metadata, but it's for the
253
+ // `multipart/form-data` request then dump it off the snippet. We shouldn't offload that
254
+ // unnecessary bloat of multipart boundaries to the user, instead letting the SDK handle it
255
+ // automatically.
256
+ if ('content-type' in metadata && metadata['content-type'].indexOf('multipart/form-data') === 0) {
257
+ delete metadata['content-type'];
258
+ }
259
+
260
+ postData.params.forEach(function (param) {
261
+ if (param.fileName) {
262
+ body[param.name] = param.fileName;
263
+ } else {
264
+ body[param.name] = param.value;
265
+ }
266
+ });
267
+ }
268
+ break;
269
+
270
+ default:
271
+ if (postData.text) {
272
+ body = postData.text;
273
+ }
274
+ }
275
+
276
+ const args = [];
277
+
278
+ let accessor: string = method;
279
+ if (operation.hasOperationId()) {
280
+ accessor = operation.getOperationId({ camelCase: true });
281
+ } else {
282
+ // Since we're not using an operationId as our primary accessor we need to take the current
283
+ // operation that we're working with and transpile back our path parameters on top of it.
284
+ const slugs = Object.fromEntries(
285
+ Object.keys(operationSlugs).map(slug => [slug.replace(/:(.*)/, '$1'), operationSlugs[slug]])
286
+ );
287
+
288
+ args.push(`'${decodeURIComponent(oas.replaceUrl(path, slugs))}'`);
289
+ }
290
+
291
+ // If we're going to be rendering out body params and metadata we should cut their character
292
+ // limit in half because we'll be rendering them in their own lines.
293
+ const inlineCharacterLimit = typeof body !== 'undefined' && Object.keys(metadata).length > 0 ? 40 : 80;
294
+ if (typeof body !== 'undefined') {
295
+ args.push(stringify(body, { inlineCharacterLimit }));
296
+ }
297
+
298
+ if (Object.keys(metadata).length > 0) {
299
+ args.push(stringify(metadata, { inlineCharacterLimit }));
300
+ }
301
+
302
+ if (authData.length) {
303
+ push(authData.join('\n'));
304
+ }
305
+
306
+ if (configData.length) {
307
+ push(configData.join('\n'));
308
+ }
309
+
310
+ push(`sdk.${accessor}(${args.join(', ')})`);
311
+ push('.then(res => console.log(res))', 1);
312
+ push('.catch(err => console.error(err));', 1);
313
+
314
+ return join();
315
+ },
316
+ };
317
+
318
+ export default client;
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
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
+ },
11
+ "include": ["./src/**/*"]
12
+ }
package/src/index.js DELETED
@@ -1,288 +0,0 @@
1
- const stringifyObject = require('stringify-object');
2
- const CodeBuilder = require('@readme/httpsnippet/src/helpers/code-builder');
3
- const contentType = require('content-type');
4
- const Oas = require('oas').default;
5
-
6
- function stringify(obj, opts = {}) {
7
- return stringifyObject(obj, { indent: ' ', ...opts });
8
- }
9
-
10
- function buildAuthSnippet(authKey) {
11
- // Auth key will be an array for Basic auth cases.
12
- if (Array.isArray(authKey)) {
13
- const auth = [];
14
- authKey.forEach((token, i) => {
15
- // If the token part is the last part of the key and it's empty, don't add it to the snippet.
16
- if (token.length === 0 && authKey.length > 1 && i === authKey.length - 1) {
17
- return;
18
- }
19
-
20
- auth.push(`'${token.replace(/'/g, "\\'")}'`);
21
- });
22
-
23
- return `sdk.auth(${auth.join(', ')});`;
24
- }
25
-
26
- return `sdk.auth('${authKey.replace(/'/g, "\\'")}');`;
27
- }
28
-
29
- function getAuthSources(operation) {
30
- const matchers = {
31
- header: [],
32
- query: [],
33
- cookie: [],
34
- };
35
-
36
- if (operation.getSecurity().length === 0) {
37
- return matchers;
38
- }
39
-
40
- const security = operation.prepareSecurity();
41
- Object.keys(security).forEach(id => {
42
- security[id].forEach(scheme => {
43
- if (scheme.type === 'http') {
44
- if (scheme.scheme === 'basic') {
45
- matchers.header.authorization = 'Basic';
46
- } else if (scheme.scheme === 'bearer') {
47
- matchers.header.authorization = 'Bearer';
48
- }
49
- } else if (scheme.type === 'oauth2') {
50
- matchers.header.authorization = 'Bearer';
51
- } else if (scheme.type === 'apiKey') {
52
- if (scheme.in === 'query') {
53
- matchers.query.push(scheme.name);
54
- } else if (scheme.in === 'header') {
55
- // The way that this asterisk header matcher works is that since this `apiKey` goes in a
56
- // named header (`scheme.name`) because the header is the key, we're matching against the
57
- // entire header -- counter to the way that the HTTP basic matcher above works where we
58
- // match and extract the API key from everything after `Basic ` in the `Authorization`
59
- // header.
60
- matchers.header[scheme.name.toLowerCase()] = '*';
61
- } else if (scheme.in === 'cookie') {
62
- matchers.cookie.push(scheme.name);
63
- }
64
- }
65
- });
66
- });
67
-
68
- return matchers;
69
- }
70
-
71
- module.exports = function (source, options) {
72
- const opts = { indent: ' ', ...options };
73
-
74
- if (!('apiDefinitionUri' in opts)) {
75
- throw new Error('This HTTP Snippet client must have an `apiDefinitionUri` option supplied to it.');
76
- } else if (!('apiDefinition' in opts)) {
77
- throw new Error('This HTTP Snippet client must have an `apiDefinition` option supplied to it.');
78
- }
79
-
80
- const method = source.method.toLowerCase();
81
- const oas = new Oas(opts.apiDefinition);
82
- const apiDefinition = oas.getDefinition();
83
- const foundOperation = oas.findOperation(source.url, method);
84
- if (!foundOperation) {
85
- throw new Error(
86
- `Unable to locate a matching operation in the supplied \`apiDefinition\` for: ${source.method} ${source.url}`
87
- );
88
- }
89
-
90
- const operationSlugs = foundOperation.url.slugs;
91
- const operation = oas.operation(foundOperation.url.nonNormalizedPath, method);
92
- const path = operation.path;
93
- const authData = [];
94
- const authSources = getAuthSources(operation);
95
-
96
- const code = new CodeBuilder(opts.indent);
97
-
98
- code.push(`const sdk = require('api')('${opts.apiDefinitionUri}');`);
99
- code.blank();
100
-
101
- // If we have multiple servers configured and our source URL differs from the stock URL that we
102
- // receive from our `oas` library then the URL either has server variables contained in it (that
103
- // don't match the defaults), or the OAS offers alternate server URLs and we should expose that
104
- // in the generated snippet.
105
- const configData = [];
106
- if ((apiDefinition.servers || []).length > 1) {
107
- const stockUrl = oas.url();
108
- const baseUrl = source.url.replace(path, '');
109
- if (baseUrl !== stockUrl) {
110
- const serverVars = oas.splitVariables(baseUrl);
111
- const serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl;
112
-
113
- configData.push(`sdk.server('${serverUrl}');`);
114
- }
115
- }
116
-
117
- let metadata = {};
118
- Object.keys(source.queryObj).forEach(param => {
119
- if (authSources.query.includes(param)) {
120
- authData.push(buildAuthSnippet(source.queryObj[param]));
121
-
122
- // If this query param is part of an auth source then we don't want it doubled up in the
123
- // snippet.
124
- return;
125
- }
126
-
127
- metadata[param] = source.queryObj[param];
128
- });
129
-
130
- Object.keys(source.cookiesObj).forEach(cookie => {
131
- if (authSources.cookie.includes(cookie)) {
132
- authData.push(buildAuthSnippet(source.cookiesObj[cookie]));
133
-
134
- // If this cookie is part of an auth source then we don't want it doubled up.
135
- return;
136
- }
137
-
138
- // Note that we may have the potential to overlap any cookie that also shares the name as
139
- // another metadata parameter. This problem is currently inherent to `api` and not this
140
- // snippet generator.
141
- metadata[cookie] = source.cookiesObj[cookie];
142
- });
143
-
144
- // If we have path parameters present, we should only add them in if we have an `operationId` as
145
- // we don't want metadata to duplicate what we'll be setting the path in the snippet to.
146
- if (operation.hasOperationId()) {
147
- Array.from(Object.entries(operationSlugs)).forEach(([param, value]) => {
148
- // The keys in `operationSlugs` will always be prefixed with a `:` in the `oas` library so
149
- // we can safely do this substring here without asserting this context.
150
- metadata[param.substring(1)] = value;
151
- });
152
- }
153
-
154
- if (Object.keys(source.headersObj).length) {
155
- const headers = source.headersObj;
156
-
157
- Object.keys(headers).forEach(header => {
158
- // Headers in HTTPSnippet are case-insensitive so we need to add in some special handling to
159
- // make sure we're able to match them properly.
160
- const headerLc = header.toLowerCase();
161
-
162
- if (headerLc in authSources.header) {
163
- // If this header has been set up as an authentication header, let's remove it and add it
164
- // into our auth data so we can build up an `.auth()` snippet for the SDK.
165
- const authScheme = authSources.header[headerLc];
166
- if (authScheme === '*') {
167
- authData.push(buildAuthSnippet(headers[header]));
168
- } else {
169
- let authKey = headers[header].replace(`${authSources.header[headerLc]} `, '');
170
- if (authScheme.toLowerCase() === 'basic') {
171
- authKey = Buffer.from(authKey, 'base64').toString('ascii');
172
- authKey = authKey.split(':');
173
- }
174
-
175
- authData.push(buildAuthSnippet(authKey));
176
- }
177
-
178
- delete headers[header];
179
- } else if (headerLc === 'content-type') {
180
- // `Content-Type` headers are automatically added within the SDK so we can filter them out
181
- // if they don't have parameters attached to them.
182
- const parsedContentType = contentType.parse(headers[header]);
183
- if (!Object.keys(parsedContentType.parameters).length) {
184
- delete headers[header];
185
- }
186
- } else if (headerLc === 'accept') {
187
- // If the `Accept` header here is not the default or first `Accept` header for the
188
- // operations' request body then we should add it otherwise we can let the SDK handle it
189
- // itself.
190
- if (headers[header] === operation.getContentType()) {
191
- delete headers[header];
192
- }
193
- }
194
- });
195
-
196
- if (Object.keys(headers).length > 0) {
197
- metadata = Object.assign(metadata, headers);
198
- }
199
- }
200
-
201
- let body;
202
- switch (source.postData.mimeType) {
203
- case 'application/x-www-form-urlencoded':
204
- body = source.postData.paramsObj;
205
- break;
206
-
207
- case 'application/json':
208
- if (source.postData.jsonObj) {
209
- body = source.postData.jsonObj;
210
- }
211
- break;
212
-
213
- case 'multipart/form-data':
214
- if (source.postData.params) {
215
- body = {};
216
-
217
- // If there's a `Content-Type` header present in the metadata, but it's for the
218
- // `multipart/form-data` request then dump it off the snippet. We shouldn't offload that
219
- // unnecessary bloat of multipart boundaries to the user, instead letting the SDK handle it
220
- // automatically.
221
- if ('content-type' in metadata && metadata['content-type'].indexOf('multipart/form-data') === 0) {
222
- delete metadata['content-type'];
223
- }
224
-
225
- source.postData.params.forEach(function (param) {
226
- if (param.fileName) {
227
- body[param.name] = param.fileName;
228
- } else {
229
- body[param.name] = param.value;
230
- }
231
- });
232
- }
233
- break;
234
-
235
- default:
236
- if (source.postData.text) {
237
- body = source.postData.text;
238
- }
239
- }
240
-
241
- const args = [];
242
-
243
- let accessor = method;
244
- if (operation.hasOperationId()) {
245
- accessor = operation.getOperationId({ camelCase: true });
246
- } else {
247
- // Since we're not using an operationId as our primary accessor we need to take the current
248
- // operation that we're working with and transpile back our path parameters on top of it.
249
- const slugs = Object.fromEntries(
250
- Object.keys(operationSlugs).map(slug => [slug.replace(/:(.*)/, '$1'), operationSlugs[slug]])
251
- );
252
-
253
- args.push(`'${decodeURIComponent(oas.replaceUrl(path, slugs))}'`);
254
- }
255
-
256
- // If we're going to be rendering out body params and metadata we should cut their character
257
- // limit in half because we'll be rendering them in their own lines.
258
- const inlineCharacterLimit = typeof body !== 'undefined' && Object.keys(metadata).length > 0 ? 40 : 80;
259
- if (typeof body !== 'undefined') {
260
- args.push(stringify(body, { inlineCharacterLimit }));
261
- }
262
-
263
- if (Object.keys(metadata).length > 0) {
264
- args.push(stringify(metadata, { inlineCharacterLimit }));
265
- }
266
-
267
- if (authData.length) {
268
- code.push(authData.join('\n'));
269
- }
270
-
271
- if (configData.length) {
272
- code.push(configData.join('\n'));
273
- }
274
-
275
- code
276
- .push(`sdk.${accessor}(${args.join(', ')})`)
277
- .push(1, '.then(res => console.log(res))')
278
- .push(1, '.catch(err => console.error(err));');
279
-
280
- return code.join();
281
- };
282
-
283
- module.exports.info = {
284
- key: 'api',
285
- title: 'API',
286
- link: 'https://npm.im/api',
287
- description: 'Automatic SDK generation from an OpenAPI definition.',
288
- };