-
Notifications
You must be signed in to change notification settings - Fork 29
SDK Generation #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
SDK Generation #28
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
8e21c10
chore: wiping the slate clean for a rewrite
erunion c914cdb
chore: pulling in dev packages for the rewrite
erunion bea8fbe
feat: pulling over the new api sdk from our other repo + tests
erunion 5581667
feat: support for all kinds of oas-supported auth schemes
erunion 4efb16f
test: splitting the auth tests out into its own file
erunion 3ddf767
feat: first draft on body/metadata parameter handling sugar
erunion 6720f5c
style: minor prettier cleanup and remove unused packages
erunion 07c555b
test: adding some tests for body/metadata intersection logic
erunion 902a367
feat: first draft on a mechanism for caching and loading specs
erunion 734d699
test: adding unit tests for the SdkCache component
erunion 3a2cd9b
test: getting tests working again under the new cache system
erunion b01b698
feat: rewriting the sdk to support a proxy-based loading system
erunion 7db0d25
fix: reworking the proxy so it can be chained off `.auth` calls
erunion ccfdb01
fix: caching system no longer logs to the console
erunion bd8ff5d
Merge branch 'master' into feat/sdk-generation
erunion b191c74
chore: adding some more files to npmignore
erunion de9efe3
chore: rebuilding package-lock.json
erunion 0b65f17
Merge branch 'master' into feat/sdk-generation
erunion 6627546
docs: revising the readme and adding tons of docs
erunion c175de6
test: adding an example file with example usage
erunion 4f2184d
docs: readme fixes
erunion 418099a
chore: fixing a typo in a code comment
erunion 230fb24
Merge branch 'feat/sdk-generation' of github.com:readmeio/api into fe…
erunion 47af70e
test: cleaning up some example oas loads
erunion 1993a46
docs: dropping the extension on the license file
erunion 03d7366
test: some unit test yakshaving
erunion File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
.github/ | ||
coverage/ | ||
node_modules/ | ||
example.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
__tests__/ | ||
.github/ | ||
coverage/ | ||
.eslint* | ||
.gitignore | ||
.prettier* | ||
example.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
coverage/ | ||
example.js |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,125 @@ | ||
# api | ||
# 🚀 api | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoooooosh There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need an astronaut Owlbert. |
||
|
||
[](https://npm.im/api) [](https://github.com/readmeio/api) | ||
|
||
Automatic SDK generation from an OpenAPI definition. | ||
|
||
* [Installation](#installation) | ||
* [Usage](#usage) | ||
* [Authentication](#authentication) | ||
* [Parameters and Payloads](#parameters-and-payloads) | ||
* [HTTP requests](#http-requests) | ||
* [How does it work?](#how-does-it-work) | ||
* [Interested in contributing?](#interested-in-contributing) | ||
* [FAQ](#faq) | ||
|
||
## Installation | ||
``` | ||
npm install api --save | ||
``` | ||
|
||
## Usage | ||
Using `api` is as simple as supplying it an OpenAPI and using the SDK as you would any other! | ||
|
||
```js | ||
const sdk = require('api')('https://raw.githubusercontent.com/readmeio/oas/master/packages/examples/3.0/json/petstore.json'); | ||
|
||
sdk.listPets().then(res => res.json()).then(res => { | ||
console.log(`My pets name is ${res[0].name}!`); | ||
}); | ||
``` | ||
|
||
The OpenAPI definition is automatically downloaded, cached, and transformed into a chainable [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) Promise that you can use to make API requests. | ||
|
||
### Authentication | ||
`api` supports API authentication through an `.auth()` method that you can chain to your requests, as such: | ||
|
||
```js | ||
sdk.auth('myApiToken').listPets().then(...); | ||
``` | ||
|
||
With the exception of OpenID, it supports all forms of authentication supported by the OpenAPI specification! Just give `.auth()` your credentials and it'll figure out how to use it according to the API you're using. | ||
|
||
For example: | ||
|
||
* HTTP Basic auth: `sdk.auth('username', 'password')` | ||
* Bearer tokens (HTTP or OAuth 2): `sdk.auth('myBearerToken')` | ||
* API Keys: `sdk.auth('myApiKey')` | ||
|
||
### Parameters and Payloads | ||
When supplying parameters and/or request body payloads to an API request, you don't need to explicitly define what goes where since the API definition contains all that information. All you need to do is supply either one or two objects: | ||
|
||
* `body`: This will contain all data required for a request body payload for a POST, PUT, etc. request. It can either be an array or an object — whichever you need to use the API operation you're using. | ||
* `metadata`: This is an object where all parameters (path, query, header, cookie) go. Again, don't worry about telling the SDK that a path parameter is for the path, that's all handled for you. | ||
|
||
For example, if you wanted to make a simple GET request: | ||
|
||
```js | ||
sdk.showPetById({ petId: 1234 }).then(...) | ||
``` | ||
|
||
Since `petId` matches up with the `petId` path parameter, the SDK here will issue a GET request against `/pets/1234`. | ||
|
||
What about a POST request? | ||
|
||
```js | ||
sdk.createPets({ name: 'Buster' }).then(...) | ||
``` | ||
|
||
Since `name` here would correspond on `createPets` to request body payload, this will issue a POST request against `/pets` to make a new pet named "Buster". | ||
|
||
What about operations that require both? Well you can mix them too! | ||
|
||
```js | ||
sdk.updatePet({ name: 'Buster 2' }, { petId: 1234 }).then(...) | ||
``` | ||
|
||
Since we've supplied two objects here, the SDK automatically knows that you're supplying both a `body` and `metadata`, and can make a PUT request against `/pets/1234` for you. | ||
|
||
### HTTP requests | ||
If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead: | ||
|
||
```js | ||
sdk.get('/pets/{petId}', { petId: 1234 }).then(...) | ||
``` | ||
|
||
The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests. | ||
|
||
## How does it work? | ||
Behind the scenes, `api` will: | ||
|
||
1. Download the supplied OpenAPI definition, either from a publically accessible URLs or an absolute/relative path. | ||
2. Dereference the definition so it's easier for us to handle. | ||
3. Hash the definition and cache it into a directory in `node_modules/.cache/api/`. | ||
4. Process the definition and instantiate chainable methods for HTTP verbs and operation IDs the API contains via a JS [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). | ||
|
||
On subsequent requests, `api` will look in its cache, and if the supplied definition exists there, it'll retrieve it from the cache instead of re-retrieving it again. | ||
|
||
## Interested in contributing? | ||
Welcome! Have a look at [CONTRIBUTING.md](CONTRIBUTING.md). | ||
|
||
## FAQ | ||
#### Does this support YAML definitions? | ||
Yes! YAML definitions will be automatically converted to JSON before they're cached and loaded as an SDK. | ||
|
||
#### Does this support Swagger 2.0 definitions? | ||
At the moment it does not. If you wish to use an API that has a Swagger 2.0 file, you'll need to first convert it to an OpenAPI 3 definition. | ||
|
||
#### Does this support traditional OAuth 2 flows of creating tokens? | ||
Not yet, unfortunately. For APIs that use OAuth 2, you'll need a fully-qualified token already for `api` to make requests. | ||
|
||
#### Does this support APIs that use multiple forms of authentication on a single request? | ||
Not yet! This is something we're thinking about how to handle, but it's difficult with the simple nature of the `.auth()` method as it currently does not require the user to inform the SDK of what kind of authentication scheme the token they're supplying it should match up against. | ||
|
||
#### Will this work in browsers? | ||
Not sure! If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request. | ||
|
||
#### Will this validate my data before it reaches the API? | ||
Not yet! This is something we've got planned down the road. | ||
|
||
#### Does this support OpenAPI definitions that require authentication to download? | ||
Not yet! The URL that you give the module must be publicy accessible. If it isn't, you can download it to your computer/server and then use the absolute path to that file instead. | ||
|
||
```js | ||
const sdk = require('api')('/path/to/downloaded.json'); | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
module.exports = serverUrl => { | ||
return function (method = 'get', path = '/', operation = {}) { | ||
return { | ||
openapi: '3.0.0', | ||
info: { | ||
title: 'OAS test', | ||
}, | ||
servers: [ | ||
{ | ||
url: serverUrl, | ||
}, | ||
], | ||
paths: { | ||
[path]: { | ||
[method]: operation, | ||
}, | ||
}, | ||
}; | ||
}; | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
const nock = require('nock'); | ||
const api = require('../src'); | ||
|
||
const serverUrl = 'https://api.example.com'; | ||
const createOas = require('./__fixtures__/createOas')(serverUrl); | ||
|
||
describe('#auth()', () => { | ||
const baseSecurityOas = createOas('get', '/', { | ||
operationId: 'getSomething', | ||
security: [ | ||
{ | ||
auth: [], | ||
}, | ||
], | ||
}); | ||
|
||
describe('API Keys', () => { | ||
const apiKey = '123457890'; | ||
|
||
describe('in: query', () => { | ||
const securityOas = { | ||
...baseSecurityOas, | ||
components: { | ||
securitySchemes: { | ||
auth: { | ||
type: 'apiKey', | ||
name: 'apiKeyParam', | ||
in: 'query', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('should allow you to supply auth', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl).get('/').query({ apiKeyParam: apiKey }).reply(200, {}); | ||
|
||
return sdk | ||
.auth(apiKey) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
|
||
it('should throw if you supply multiple auth keys', () => { | ||
const sdk = api(securityOas); | ||
|
||
return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single key is needed/i); | ||
}); | ||
}); | ||
|
||
describe('in: header', () => { | ||
const securityOas = { | ||
...baseSecurityOas, | ||
components: { | ||
securitySchemes: { | ||
auth: { | ||
type: 'apiKey', | ||
name: 'apiKeyHeader', | ||
in: 'header', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('should allow you to supply auth', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl, { reqheaders: { apiKeyHeader: apiKey } }) | ||
.get('/') | ||
.reply(200, {}); | ||
|
||
return sdk | ||
.auth(apiKey) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
|
||
it('should throw if you supply multiple auth keys', () => { | ||
const sdk = api(securityOas); | ||
|
||
return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single key is needed/i); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('HTTP', () => { | ||
describe('scheme: basic', () => { | ||
const user = 'username'; | ||
const pass = 'changeme'; | ||
const securityOas = { | ||
...baseSecurityOas, | ||
components: { | ||
securitySchemes: { | ||
auth: { | ||
type: 'http', | ||
scheme: 'basic', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('should allow you to supply auth', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl, { | ||
reqheaders: { authorization: `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}` }, | ||
}) | ||
.get('/') | ||
.reply(200, {}); | ||
|
||
return sdk | ||
.auth(user, pass) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
|
||
it('should allow you to not pass in a password', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl, { | ||
reqheaders: { authorization: `Basic ${Buffer.from(`${user}:`).toString('base64')}` }, | ||
}) | ||
.get('/') | ||
.reply(200, {}); | ||
|
||
return sdk | ||
.auth(user) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('scheme: bearer', () => { | ||
const apiKey = '123457890'; | ||
const securityOas = { | ||
...baseSecurityOas, | ||
components: { | ||
securitySchemes: { | ||
auth: { | ||
type: 'http', | ||
scheme: 'bearer', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('should allow you to supply auth', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl, { reqheaders: { authorization: `Bearer ${apiKey}` } }) | ||
.get('/') | ||
.reply(200, {}); | ||
|
||
return sdk | ||
.auth(apiKey) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
|
||
it('should throw if you pass in multiple bearer tokens', () => { | ||
const sdk = api(securityOas); | ||
|
||
return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single token is needed/i); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('OAuth 2', () => { | ||
const apiKey = '123457890'; | ||
const securityOas = { | ||
...baseSecurityOas, | ||
components: { | ||
securitySchemes: { | ||
auth: { | ||
type: 'oauth2', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('should allow you to supply auth', () => { | ||
const sdk = api(securityOas); | ||
const mock = nock(serverUrl, { reqheaders: { authorization: `Bearer ${apiKey}` } }) | ||
.get('/') | ||
.reply(200, {}); | ||
|
||
return sdk | ||
.auth(apiKey) | ||
.getSomething() | ||
.then(res => { | ||
expect(res.status).toBe(200); | ||
mock.done(); | ||
}); | ||
}); | ||
|
||
it('should throw if you pass in multiple bearer tokens', () => { | ||
const sdk = api(securityOas); | ||
|
||
return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single token is needed/i); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get code coverage sent up to codecov.io?