From a52647d771bc4777e4f6193741643b4e4c64739e Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 2 Jul 2019 16:40:34 -0500 Subject: [PATCH] Make the API browser more generic Drops the 'version' grouping and adds HTTP method/operation names. --- src/services/twilio-api/api-browser.js | 119 ++++++---------- src/services/twilio-api/index.js | 5 +- test/services/twilio-api.test.js | 186 +++++++++++++------------ 3 files changed, 141 insertions(+), 169 deletions(-) diff --git a/src/services/twilio-api/api-browser.js b/src/services/twilio-api/api-browser.js index 9f960e05..dc197508 100644 --- a/src/services/twilio-api/api-browser.js +++ b/src/services/twilio-api/api-browser.js @@ -1,90 +1,62 @@ const fs = require('fs'); -const url = require('url'); -const { doesObjectHaveProperty } = require('../javascript-utilities'); -const ResourcePathParser = require('../resource-path-parser'); +const { camelCase } = require('../naming-conventions'); +let apiSpec; // Lazy-loaded below. -const isApi2010 = (domain, version) => { - return domain === 'api' && (version === '2010-04-01' || version === 'v2010'); -}; - -function translateLegacyVersions(domain, version) { - // In the Node helper library, api.twilio.com/2010-04-01 is represented as "v2010" - return isApi2010(domain, version) ? 'v2010' : version; -} - -const listResourceMethodMap = { - get: 'list', - post: 'create' -}; - -const instanceResourceMethodMap = { - delete: 'remove', - get: 'fetch', - post: 'update' -}; - -/* - -Notes: - -Disambiguating like-named resources: -If same resource name exists under multiple domains/versions/paths, then have the command -map to a help message when it is ambiguous as to which domain/version/path they want. -Maybe have a setting to change this behavior? Have a custom shortcut map in the config -file? Have a mode of operation where it just picks the most recent version? - -*/ +const OPERATIONS = ['post', 'get', 'delete']; class TwilioApiBrowser { constructor(apiSpec) { - this.apiSpec = apiSpec || this.loadApiSpecFromDisk(); - this.domains = this.loadDomains(); + apiSpec = apiSpec || this.loadApiSpecFromDisk(); + this.domains = this.loadDomains(apiSpec); } loadApiSpecFromDisk() { - // Assume all 'json' files in here are OpenAPI spec files. - return fs.readdirSync(__dirname) - .filter(filename => filename.endsWith('.json')) - .map(filename => require(`./${filename}`)); - } + if (!apiSpec) { + const specPattern = /twilio_(.+)\.json/; + const specNameIndex = 1; + + apiSpec = fs.readdirSync(__dirname) + .filter(filename => filename.match(specPattern)) + .map(filename => { + const domainName = filename.match(specPattern)[specNameIndex]; - loadDomains() { - const domains = {}; + return { [domainName]: require(`./${filename}`) }; + }); - this.apiSpec.forEach(spec => { - Object.keys(spec.paths).forEach(path => { - // Naive assumption: The Twilio API's only have a single domain - const serverUrl = new url.URL(spec.paths[path].servers[0].url); - const domain = serverUrl.host.split('.')[0]; // e.g. 'api' from 'api.twilio.com' + apiSpec = Object.assign({}, ...apiSpec); + } - const resourcePathParser = new ResourcePathParser(path); - resourcePathParser.normalizePath(); // e.g /v1/foo/bar/{Sid}.json --> /foo/bar - const resourcePath = resourcePathParser.getFullPath(); + return apiSpec; + } - const version = translateLegacyVersions(domain, resourcePathParser.version); + loadDomains(apiSpec) { + const domains = apiSpec; - if (!doesObjectHaveProperty(domains, domain)) { - domains[domain] = { versions: {}, tags: spec.tags }; - } + Object.values(domains).forEach(spec => { + Object.values(spec.paths).forEach(path => { + // Naive assumption: The Twilio APIs only have a single server. + path.server = path.servers[0].url; + delete path.servers; - if (!doesObjectHaveProperty(domains[domain].versions, version)) { - domains[domain].versions[version] = { resources: {} }; - } + path.operations = {}; + path.description = path.description.replace(/(\r\n|\n|\r)/gm, ' '); + + // Move the operations into an operations object. + OPERATIONS.forEach(operationName => { + if (operationName in path) { + path.operations[operationName] = path[operationName]; + delete path[operationName]; + } + }); - const resources = domains[domain].versions[version].resources; - if (!doesObjectHaveProperty(resources, resourcePath)) { - resources[resourcePath] = { - actions: {}, - description: spec.paths[path].description.replace(/(\r\n|\n|\r)/gm, ' '), - defaultOutputProperties: spec.paths[path]['x-default-output-properties'] - }; - } + // Convert extensions to camel-cased properties. + Object.entries(path).forEach(([key, value]) => { + const extensionMatch = key.match(/x-(.+)/); - const actions = resources[resourcePath].actions; - const methodMap = resourcePathParser.isInstanceResource ? instanceResourceMethodMap : listResourceMethodMap; - Object.keys(methodMap).forEach(method => { - if (doesObjectHaveProperty(spec.paths[path], method)) { - actions[methodMap[method]] = spec.paths[path][method]; + if (extensionMatch) { + const newKey = camelCase(extensionMatch[1]); + path[newKey] = value; + delete path[key]; } }); }); @@ -94,7 +66,4 @@ class TwilioApiBrowser { } } -module.exports = { - TwilioApiBrowser, - isApi2010 -}; +module.exports = TwilioApiBrowser; diff --git a/src/services/twilio-api/index.js b/src/services/twilio-api/index.js index ef397619..f78a8f8d 100644 --- a/src/services/twilio-api/index.js +++ b/src/services/twilio-api/index.js @@ -1,6 +1,5 @@ -const { TwilioApiBrowser, isApi2010 } = require('./api-browser'); +const TwilioApiBrowser = require('./api-browser'); module.exports = { - TwilioApiBrowser, - isApi2010 + TwilioApiBrowser }; diff --git a/test/services/twilio-api.test.js b/test/services/twilio-api.test.js index fd7af2e2..969aa2f6 100644 --- a/test/services/twilio-api.test.js +++ b/test/services/twilio-api.test.js @@ -8,80 +8,89 @@ describe('services', () => { test.it('loads the JSON from disk', () => { const browser = new TwilioApiBrowser(); // Check some known api endpoints that should be relatively stable - expect(browser.domains.api.versions.v2010.resources['/Accounts/{AccountSid}/Calls'].actions.create).to.exist; - expect(browser.domains.api.versions.v2010.resources['/Accounts/{AccountSid}/Calls/{Sid}'].actions.fetch).to.exist; + expect(browser.domains.api.paths['/2010-04-01/Accounts/{AccountSid}/Calls.json'].operations.post).to.exist; + expect(browser.domains.api.paths['/2010-04-01/Accounts/{AccountSid}/Calls/{Sid}.json'].operations.get).to.exist; }); test.it('loads a specific api spec', () => { - const browser = new TwilioApiBrowser([{ - paths: { - '/2010-04-01/Widgets.json': { - servers: [ - { - url: 'https://api.twilio.com' - } - ], - description: 'Widgets here\nsecond line of text', - 'x-default-output-properties': ['sid'] - }, - '/v1/Gadgets.json': { - servers: [ - { - url: 'https://neato.twilio.com' - } - ], - description: 'v1 Gadgets here', - 'x-default-output-properties': ['sid'] - }, - '/v2/Gadgets.json': { - servers: [ - { - url: 'https://neato.twilio.com' - } - ], - post: { createStuff: '' }, - get: { listStuff: '' }, - description: 'v2 list Gadgets here', - 'x-default-output-properties': ['sid', 'name'] + const browser = new TwilioApiBrowser({ + api: { + paths: { + '/2010-04-01/Widgets.json': { + servers: [ + { + url: 'https://api.twilio.com' + } + ], + description: 'Widgets here\nsecond line of text', + 'x-default-output-properties': ['sid'] + } }, - '/v2/Gadgets/{Sid}.json': { - servers: [ - { - url: 'https://neato.twilio.com' - } - ], - post: { updateStuff: '' }, - get: { fetchStuff: '' }, - delete: { removeStuff: '' }, - description: 'v2 instance Gadgets here', - 'x-default-output-properties': ['sid', 'description'] - } + tags: [ + { + name: 'Beta', + description: 'Betamax!' + } + ] }, - tags: [ - { - name: 'GA', - description: 'Generally Available!' - } - ] - }]); + neato: { + paths: { + '/v1/Gadgets.json': { + servers: [ + { + url: 'https://neato.twilio.com' + } + ], + description: 'v1 Gadgets here', + 'x-default-output-properties': ['sid'] + }, + '/v2/Gadgets.json': { + servers: [ + { + url: 'https://neato.twilio.com' + } + ], + post: { createStuff: '' }, + get: { listStuff: '' }, + description: 'v2 list Gadgets here', + 'x-default-output-properties': ['sid', 'name'] + }, + '/v2/Gadgets/{Sid}.json': { + servers: [ + { + url: 'https://neato.twilio.com' + } + ], + post: { updateStuff: '' }, + get: { fetchStuff: '' }, + delete: { removeStuff: '' }, + description: 'v2 instance Gadgets here', + 'x-default-output-properties': ['sid', 'description'] + } + }, + tags: [ + { + name: 'GA', + description: 'Generally Available!' + } + ] + } + }); expect(browser.domains).to.deep.equal({ api: { tags: [ { - name: 'GA', - description: 'Generally Available!' + name: 'Beta', + description: 'Betamax!' } ], - versions: { - v2010: { - resources: { - '/Widgets': { - actions: {}, - description: 'Widgets here second line of text', - defaultOutputProperties: ['sid'] - } - } + paths: { + '/2010-04-01/Widgets.json': { + operations: {}, + server: 'https://api.twilio.com', + description: 'Widgets here second line of text', + defaultOutputProperties: ['sid'] } } }, @@ -92,36 +101,31 @@ describe('services', () => { description: 'Generally Available!' } ], - versions: { - v1: { - resources: { - '/Gadgets': { - actions: {}, - description: 'v1 Gadgets here', - defaultOutputProperties: ['sid'] - } - } + paths: { + '/v1/Gadgets.json': { + operations: {}, + server: 'https://neato.twilio.com', + description: 'v1 Gadgets here', + defaultOutputProperties: ['sid'] }, - v2: { - resources: { - '/Gadgets': { - actions: { - create: { createStuff: '' }, - list: { listStuff: '' } - }, - description: 'v2 list Gadgets here', - defaultOutputProperties: ['sid', 'name'] - }, - '/Gadgets/{Sid}': { - actions: { - fetch: { fetchStuff: '' }, - update: { updateStuff: '' }, - remove: { removeStuff: '' } - }, - description: 'v2 instance Gadgets here', - defaultOutputProperties: ['sid', 'description'] - } - } + '/v2/Gadgets.json': { + operations: { + post: { createStuff: '' }, + get: { listStuff: '' } + }, + server: 'https://neato.twilio.com', + description: 'v2 list Gadgets here', + defaultOutputProperties: ['sid', 'name'] + }, + '/v2/Gadgets/{Sid}.json': { + operations: { + get: { fetchStuff: '' }, + post: { updateStuff: '' }, + delete: { removeStuff: '' } + }, + server: 'https://neato.twilio.com', + description: 'v2 instance Gadgets here', + defaultOutputProperties: ['sid', 'description'] } } }