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

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 (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +34 -12
  3. package/package.json +20 -17
  4. package/src/index.js +60 -69
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright © 2020 ReadMe
1
+ Copyright © 2022 ReadMe
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the “Software”), to deal in
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # httpsnippet-client-api
2
2
 
3
- An HTTP Snippet client for generating snippets for the [api](https://npm.im/api) module.
3
+ An [HTTPSnippet](https://npm.im/httpsnippet) client for generating snippets for the [api](https://npm.im/api) module.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/httpsnippet-client-api)](https://npm.im/api) [![Build](https://github.com/readmeio/api/workflows/CI/badge.svg)](https://github.com/readmeio/api)
6
6
 
@@ -21,23 +21,45 @@ const client = require('httpsnippet-client-api');
21
21
 
22
22
  HTTPSnippet.addTargetClient('node', client);
23
23
 
24
- const snippet = new HTTPSnippet(harObject);
25
- console.log(
26
- snippet.convert('node', 'api', {
27
- apiDefinitionUri: 'https://example.com/openapi.json'
28
- apiDefinition: {
29
- /* an OpenAPI definition object */
30
- }
31
- })
32
- );
24
+ const har = {
25
+ "log": {
26
+ "entries": [
27
+ {
28
+ "request": {
29
+ "cookies": [],
30
+ "httpVersion": "HTTP/1.1",
31
+ "method": "PUT",
32
+ "headers": [
33
+ {
34
+ "name": "X-API-KEY",
35
+ "value": "a5a220e"
36
+ }
37
+ ],
38
+ "url": "https://httpbin.org/apiKey"
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ }
44
+
45
+ const snippet = new HTTPSnippet(har);
46
+ const code = snippet.convert('node', 'api', {
47
+ apiDefinitionUri: 'https://api.example.com/openapi.json'
48
+ apiDefinition: {
49
+ /* an OpenAPI definition object */
50
+ }
51
+ });
52
+
53
+ console.log(code);
33
54
  ```
34
55
 
35
56
  Results in the following:
36
57
 
37
58
  ```js
38
- const sdk = require('api')('https://example.com/openapi.json');
59
+ const sdk = require('api')('https://api.example.com/openapi.json');
39
60
 
40
- sdk.get('/har')
61
+ sdk.auth('a5a220e');
62
+ sdk.put('/apiKey')
41
63
  .then(res => console.log(res))
42
64
  .catch(err => console.error(err));
43
65
  ```
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "httpsnippet-client-api",
3
- "version": "4.3.0",
4
- "description": "An HTTP Snippet client for generating snippets for the api module.",
3
+ "version": "5.0.0-beta.0",
4
+ "description": "An HTTPSnippet client for generating snippets for the `api` module.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "lint": "eslint .",
8
8
  "pretest": "npm run lint",
9
9
  "prettier": "prettier --list-different --write \"./**/**.js\"",
10
- "test": "jest --coverage"
10
+ "test": "nyc mocha \"test/**/*.test.js\"",
11
+ "test:watch": "nyc mocha \"test/**/*.test.js\" --watch"
11
12
  },
12
13
  "repository": {
13
14
  "type": "git",
@@ -20,29 +21,31 @@
20
21
  "author": "Jon Ursenbach <jon@readme.io>",
21
22
  "license": "MIT",
22
23
  "engines": {
23
- "node": "^12 || ^14 || ^16"
24
+ "node": ">=14"
24
25
  },
25
26
  "dependencies": {
26
27
  "content-type": "^1.0.4",
27
- "path-to-regexp": "^6.1.0",
28
28
  "stringify-object": "^3.3.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@readme/httpsnippet": "^3.0.0",
32
- "oas": "^18.1.0"
32
+ "oas": "^18.0.1"
33
33
  },
34
34
  "devDependencies": {
35
- "@readme/eslint-config": "^8.0.2",
36
- "@readme/oas-examples": "^4.3.3",
37
- "eslint": "^8.3.0",
38
- "jest": "^27.3.1",
39
- "prettier": "^2.4.1"
35
+ "@readme/eslint-config": "^8.7.3",
36
+ "@readme/oas-examples": "^5.4.1",
37
+ "@readme/openapi-parser": "^2.2.0",
38
+ "api": "^5.0.0-beta.0",
39
+ "chai": "^4.3.6",
40
+ "eslint": "^8.14.0",
41
+ "fetch-mock": "^9.11.0",
42
+ "isomorphic-fetch": "^3.0.0",
43
+ "mocha": "^10.0.0",
44
+ "nyc": "^15.1.0",
45
+ "prettier": "^2.6.2",
46
+ "sinon": "^14.0.0",
47
+ "sinon-chai": "^3.7.0"
40
48
  },
41
49
  "prettier": "@readme/eslint-config/prettier",
42
- "jest": {
43
- "testPathIgnorePatterns": [
44
- "__tests__/__fixtures__/"
45
- ]
46
- },
47
- "gitHead": "d69e58465d8eff63aec29693f70d085247afb7ef"
50
+ "gitHead": "d18f7e3a1c20edaf84d0c5c2e1a64714549a4ee6"
48
51
  }
package/src/index.js CHANGED
@@ -1,4 +1,3 @@
1
- const { match } = require('path-to-regexp');
2
1
  const stringifyObject = require('stringify-object');
3
2
  const CodeBuilder = require('@readme/httpsnippet/src/helpers/code-builder');
4
3
  const contentType = require('content-type');
@@ -21,7 +20,7 @@ function buildAuthSnippet(authKey) {
21
20
  auth.push(`'${token.replace(/'/g, "\\'")}'`);
22
21
  });
23
22
 
24
- return `sdk.auth(${auth.join(', ')})`;
23
+ return `sdk.auth(${auth.join(', ')});`;
25
24
  }
26
25
 
27
26
  return `sdk.auth('${authKey.replace(/'/g, "\\'")}');`;
@@ -53,7 +52,12 @@ function getAuthSources(operation) {
53
52
  if (scheme.in === 'query') {
54
53
  matchers.query.push(scheme.name);
55
54
  } else if (scheme.in === 'header') {
56
- matchers.header[scheme.name] = '*';
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()] = '*';
57
61
  } else if (scheme.in === 'cookie') {
58
62
  matchers.cookie.push(scheme.name);
59
63
  }
@@ -64,21 +68,6 @@ function getAuthSources(operation) {
64
68
  return matchers;
65
69
  }
66
70
 
67
- function getParamsInPath(operation, path) {
68
- const cleanedPath = operation.path.replace(/{(.*?)}/g, ':$1');
69
- const matchStatement = match(cleanedPath, { decode: decodeURIComponent });
70
- const matchResult = matchStatement(path);
71
- const slugs = {};
72
-
73
- if (matchResult && Object.keys(matchResult.params).length) {
74
- Object.keys(matchResult.params).forEach(param => {
75
- slugs[`${param}`] = matchResult.params[param];
76
- });
77
- }
78
-
79
- return slugs;
80
- }
81
-
82
71
  module.exports = function (source, options) {
83
72
  const opts = { indent: ' ', ...options };
84
73
 
@@ -109,9 +98,10 @@ module.exports = function (source, options) {
109
98
  code.push(`const sdk = require('api')('${opts.apiDefinitionUri}');`);
110
99
  code.blank();
111
100
 
112
- // If we have multiple servers configured and our source URL differs from the stock URL that we receive from our
113
- // `oas` library then the URL either has server variables contained in it (that don't match the defaults), or the
114
- // OAS offers alternate server URLs and we should expose that in the generated snippet.
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.
115
105
  const configData = [];
116
106
  if ((apiDefinition.servers || []).length > 1) {
117
107
  const stockUrl = oas.url();
@@ -125,46 +115,53 @@ module.exports = function (source, options) {
125
115
  }
126
116
 
127
117
  let metadata = {};
128
- if (Object.keys(source.queryObj).length) {
129
- const queryParams = source.queryObj;
118
+ Object.keys(source.queryObj).forEach(param => {
119
+ if (authSources.query.includes(param)) {
120
+ authData.push(buildAuthSnippet(source.queryObj[param]));
130
121
 
131
- Object.keys(queryParams).forEach(param => {
132
- if (authSources.query.includes(param)) {
133
- authData.push(buildAuthSnippet(queryParams[param]));
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
+ }
134
126
 
135
- delete queryParams[param];
136
- }
137
- });
127
+ metadata[param] = source.queryObj[param];
128
+ });
138
129
 
139
- metadata = Object.assign(metadata, queryParams);
140
- }
130
+ Object.keys(source.cookiesObj).forEach(cookie => {
131
+ if (authSources.cookie.includes(cookie)) {
132
+ authData.push(buildAuthSnippet(source.cookiesObj[cookie]));
141
133
 
142
- // If we have path parameters present, we should only add them in if we have an operationId as we don't want metadata
143
- // to duplicate what we'll be setting the path in the snippet to.
144
- if ('operationId' in operation.schema) {
145
- const pathParams = getParamsInPath(operation, operation.path);
146
- if (Object.keys(pathParams).length) {
147
- Object.keys(pathParams).forEach(param => {
148
- if (`:${param}` in operationSlugs) {
149
- metadata[param] = operationSlugs[`:${param}`];
150
- } else {
151
- metadata[param] = pathParams[param];
152
- }
153
- });
134
+ // If this cookie is part of an auth source then we don't want it doubled up.
135
+ return;
154
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
+ });
155
152
  }
156
153
 
157
154
  if (Object.keys(source.headersObj).length) {
158
155
  const headers = source.headersObj;
159
156
 
160
157
  Object.keys(headers).forEach(header => {
161
- // Headers in HTTPSnippet are case-insensitive so we need to add in some special handling to make sure we're able
162
- // to match them properly.
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.
163
160
  const headerLc = header.toLowerCase();
164
161
 
165
162
  if (headerLc in authSources.header) {
166
- // If this header has been set up as an authentication header, let's remove it and add it into our auth data
167
- // so we can build up an `.auth()` snippet for the SDK.
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.
168
165
  const authScheme = authSources.header[headerLc];
169
166
  if (authScheme === '*') {
170
167
  authData.push(buildAuthSnippet(headers[header]));
@@ -180,15 +177,16 @@ module.exports = function (source, options) {
180
177
 
181
178
  delete headers[header];
182
179
  } else if (headerLc === 'content-type') {
183
- // Content-Type headers are automatically added within the SDK so we can filter them out if they don't have
184
- // parameters attached to them.
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.
185
182
  const parsedContentType = contentType.parse(headers[header]);
186
183
  if (!Object.keys(parsedContentType.parameters).length) {
187
184
  delete headers[header];
188
185
  }
189
186
  } else if (headerLc === 'accept') {
190
- // If the accept header here is not the default/first accept header for the operations request body, then we
191
- // should add it, otherwise just let the SDK handle it itself.
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.
192
190
  if (headers[header] === operation.getContentType()) {
193
191
  delete headers[header];
194
192
  }
@@ -216,9 +214,10 @@ module.exports = function (source, options) {
216
214
  if (source.postData.params) {
217
215
  body = {};
218
216
 
219
- // If there's a `Content-Type` header present in the metadata, but it's for the form-data
220
- // request then dump it off the snippet. We shouldn't offload that unnecessary bloat to the
221
- // user, instead letting the SDK handle it automatically.
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.
222
221
  if ('content-type' in metadata && metadata['content-type'].indexOf('multipart/form-data') === 0) {
223
222
  delete metadata['content-type'];
224
223
  }
@@ -242,11 +241,11 @@ module.exports = function (source, options) {
242
241
  const args = [];
243
242
 
244
243
  let accessor = method;
245
- if ('operationId' in operation.schema && operation.schema.operationId.length > 0) {
246
- accessor = operation.schema.operationId;
244
+ if (operation.hasOperationId()) {
245
+ accessor = operation.getOperationId({ camelCase: true });
247
246
  } else {
248
- // Since we're not using an operationId as our primary accessor we need to take the current operation that we're
249
- // working with and transpile back our path parameters on top of it.
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.
250
249
  const slugs = Object.fromEntries(
251
250
  Object.keys(operationSlugs).map(slug => [slug.replace(/:(.*)/, '$1'), operationSlugs[slug]])
252
251
  );
@@ -254,16 +253,8 @@ module.exports = function (source, options) {
254
253
  args.push(`'${decodeURIComponent(oas.replaceUrl(path, slugs))}'`);
255
254
  }
256
255
 
257
- // If the operation or method accessor is non-alphanumeric, we need to add it to the SDK object as an array key.
258
- // https://github.com/readmeio/api/issues/119
259
- if (accessor.match(/[^a-zA-Z\d\s:]/)) {
260
- accessor = `['${accessor}']`;
261
- } else {
262
- accessor = `.${accessor}`;
263
- }
264
-
265
- // If we're going to be rendering out body params and metadata we should cut their character limit in half because
266
- // we'll be rendering them in their own lines.
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.
267
258
  const inlineCharacterLimit = typeof body !== 'undefined' && Object.keys(metadata).length > 0 ? 40 : 80;
268
259
  if (typeof body !== 'undefined') {
269
260
  args.push(stringify(body, { inlineCharacterLimit }));
@@ -282,7 +273,7 @@ module.exports = function (source, options) {
282
273
  }
283
274
 
284
275
  code
285
- .push(`sdk${accessor}(${args.join(', ')})`)
276
+ .push(`sdk.${accessor}(${args.join(', ')})`)
286
277
  .push(1, '.then(res => console.log(res))')
287
278
  .push(1, '.catch(err => console.error(err));');
288
279