Skip to content

Commit

Permalink
feat(api): improved api
Browse files Browse the repository at this point in the history
client constructor now takes just ONE options argument

BREAKING CHANGE: client constructor now takes just ONE options argument, with "server" property as
an object or a string
  • Loading branch information
Ahmad Nassri committed Mar 28, 2021
1 parent a707d41 commit d408b0b
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 64 deletions.
47 changes: 24 additions & 23 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,42 @@ const querystring = require('querystring')

// modules
const http = require('./http/')
const template = require('./path-template')
const parseServer = require('./parse-server')
const OASRequestError = require('./error')
const parsePathTemplate = require('./parse-path-template')

// main
module.exports = function (oas) {
if (!oas) throw new Error('missing argument: oas')
module.exports = function (spec) {
if (!spec || !spec.paths) throw new OASRequestError('missing argument: spec')

const client = class {
constructor (server, { headers = {}, params = {}, query = {} } = {}) {
if (!server) throw new Error('missing argument: server')

// TODO analyze oas.servers
this.server = server.replace(/\/$/, '')

// default properties
this.headers = headers
this.params = params
this.query = query
constructor (options = {}) {
// process spec.servers & options.server
this.options = {
server: parseServer(options.server, spec),
// global properties
headers: options.headers || {},
params: options.params || {},
query: options.query || {}
}
}

__request (method, url, options) {
// merge params with global defaults
const params = Object.assign({}, this.params, options.params)
const params = { ...this.options.params, ...options.params }

// process path template
const urlPath = template(url, params)
const urlPath = parsePathTemplate(url, params)

// construct final host & url parts
const { protocol, port, hostname, pathname, searchParams } = new URL(`${this.server}${urlPath}`)
const { protocol, port, hostname, pathname, searchParams } = new URL(`${this.options.server}${urlPath}`)

// convert query back to regular object
const searchObj = Object.fromEntries(searchParams.entries())

// overrides
const headers = Object.assign({}, this.headers, options.headers)
const query = Object.assign(searchObj, this.query, options.query)
const headers = { ...this.options.headers, ...options.headers }
const query = Object.assign(searchObj, this.options.query, options.query)

// final query string
const search = querystring.stringify(query)
Expand All @@ -55,13 +56,13 @@ module.exports = function (oas) {
}

// process paths
for (const [url, methods] of Object.entries(oas.paths)) {
for (const [url, methods] of Object.entries(spec.paths)) {
// filter to paths that contain an operationId
const operational = Object.entries(methods).filter(([method, spec]) => spec.operationId)
const operational = Object.entries(methods).filter(([method, operation]) => operation.operationId)

// process each method
for (const [method, spec] of operational) {
Object.defineProperty(client.prototype, spec.operationId, {
// create a method for each operation
for (const [method, operation] of operational) {
Object.defineProperty(client.prototype, operation.operationId, {
enumerable: true,
writable: false,
value: function ({ headers, params, query, body } = {}) {
Expand Down
18 changes: 18 additions & 0 deletions test/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { test } = require('tap')

const OASRequestError = require('../lib/error')

const regex = /(\w|\/)+test\/error\.js:\d{2}:\d{2}/

test('Empty ExtendableError', assert => {
assert.plan(5)

const err = new OASRequestError('foobar')

assert.type(err, OASRequestError)

assert.equal(err.name, 'OASRequestError')
assert.match(err.message, 'foobar')
assert.match(err.stack, regex)
assert.equal(err.toString(), 'OASRequestError: foobar')
})
44 changes: 31 additions & 13 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,29 @@ delete require.cache[require.resolve('../lib/http/')]
// override required module
require.cache[require.resolve('../lib/http/')] = { exports: http }

const oasRequest = require('..')
const spec = require('./fixtures/petstore.json')
const client = require('..')
const OASRequestError = require('../lib/error')

test('throws if no spec', assert => {
assert.plan(2)

assert.throws(() => oasRequest(), new OASRequestError('missing argument: spec'))
assert.throws(() => oasRequest({}), new OASRequestError('missing argument: spec'))
})

test('throws if no serverOptions', assert => {
assert.plan(1)

const API = oasRequest(spec)
assert.throws(() => new API())
})

test('generates methods', assert => {
assert.plan(3)

const API = client(spec)
const api = new API('https://pets.com')
const API = oasRequest(spec)
const api = new API({ server: 'https://pets.com' })

assert.type(api.listPets, Function)
assert.type(api.createPets, Function)
Expand All @@ -39,8 +54,8 @@ test('methods are callable', assert => {
})
})

const API = client(spec)
const api = new API('https://pets.com')
const API = oasRequest(spec)
const api = new API({ server: 'https://pets.com' })

api.showPetById()
})
Expand All @@ -60,8 +75,8 @@ test('methods options', assert => {
})
})

const API = client(spec)
const api = new API('https://pets.com')
const API = oasRequest(spec)
const api = new API({ server: 'https://pets.com' })

api.showPetById({
params: {
Expand All @@ -85,9 +100,10 @@ test('global defaults', assert => {
})
})

const API = client(spec)
const API = oasRequest(spec)

const api = new API('https://pets.com', {
const api = new API({
server: 'https://pets.com',
headers: { 'x-pet-type': 'dog' },
params: { petId: 1 },
query: { name: 'ruby' }
Expand All @@ -113,9 +129,10 @@ test('sub path in server', assert => {
})
})

const API = client(spec)
const API = oasRequest(spec)

const api = new API('https://pets.com/api/v1-0-0', {
const api = new API({
server: 'https://pets.com/api/v1-0-0',
headers: { 'x-pet-type': 'dog' },
params: { petId: 1 },
query: { name: 'ruby' }
Expand All @@ -141,9 +158,10 @@ test('sub path in server without slashes', assert => {
})
})

const API = client(spec)
const API = oasRequest(spec)

const api = new API('https://pets.com/api/v1-0-0/', {
const api = new API({
server: 'https://pets.com/api/v1-0-0/',
headers: { 'x-pet-type': 'dog' },
params: { petId: 1 },
query: { name: 'ruby' }
Expand Down
12 changes: 12 additions & 0 deletions test/parse-path-template.js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { test } = require('tap')

const parsePathTemplate = require('../lib/parse-path-template')

test('parse path templates', assert => {
assert.plan(4)

assert.equal(parsePathTemplate('/pets/{petId}'), '/pets/{petId}', 'keep variable template if no variables present')
assert.equal(parsePathTemplate('/pets/{petId}', { petId: 'foo' }), '/pets/foo', 'replaces one value')
assert.equal(parsePathTemplate('/{entity}/{id}', { entity: 'pet', id: '123' }), '/pet/123', 'replaces all the value')
assert.equal(parsePathTemplate('/{entity}/{id}/{id}', { entity: 'pet', id: '123' }), '/pet/123/123', 'replaces the same value multiple times')
})
82 changes: 82 additions & 0 deletions test/parse-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { test } = require('tap')

const parseServer = require('../lib/parse-server')
const OASRequestError = require('../lib/error')

test('throws if no server', assert => {
assert.plan(1)

assert.throws(() => parseServer(), new OASRequestError('missing argument: server'))
})

test('throws if no server.url', assert => {
assert.plan(1)

assert.throws(() => parseServer({}), new OASRequestError('missing argument: server.url'))
})

test('returns the server.url', assert => {
assert.plan(1)

const url = parseServer({ url: 'foo' }, {})

assert.equal(url, 'foo')
})

test('returns the server if a string', assert => {
assert.plan(1)

const url = parseServer('foo', {})

assert.equal(url, 'foo')
})

test('returns the server.url', assert => {
assert.plan(1)

const url = parseServer({ url: 'foo' }, {})

assert.equal(url, 'foo')
})

test('populates the server.url with a defined spec.servers', assert => {
assert.plan(1)

const spec = {
servers: [{
url: 'localhost'
}]
}

const url = parseServer({ url: 'foo' }, spec)

assert.equal(url, 'foo')
})

test('populates the server.url with spec variables', assert => {
assert.plan(1)

const spec = {
servers: [{
url: 'localhost',
variables: { foo: { default: 'bar' } }
}]
}

const url = parseServer({ url: 'localhost/{foo}' }, spec)

assert.equal(url, 'localhost/bar')
})

test('populates the server.url with server.variables', assert => {
assert.plan(1)

const spec = {
servers: [{
url: 'localhost',
variables: { foo: {} }
}]
}

assert.equal(parseServer({ url: 'localhost/{foo}', variables: { foo: 'bar' } }, spec), 'localhost/bar')
})
10 changes: 0 additions & 10 deletions test/path-template.js

This file was deleted.

2 changes: 1 addition & 1 deletion test/httpbin.js → test/real.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const spec = require('./fixtures/httpbin.json')
const client = require('../lib')

const API = client(spec)
const api = new API('https://httpbin.org:443')
const api = new API({ server: 'https://httpbin.org:443' })

test('generates methods', assert => {
assert.plan(4)
Expand Down
17 changes: 0 additions & 17 deletions test/throws.js

This file was deleted.

0 comments on commit d408b0b

Please sign in to comment.