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