├── .github └── workflows │ ├── ci.yml │ ├── daily-main-check.yml │ └── npm-publish.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── custom-typings └── swagger2openapi.d.ts ├── openapi-directory.d.ts ├── package-lock.json ├── package.json ├── runkit-demo.js ├── src ├── buildtime │ ├── build-all.ts │ ├── build-index.ts │ ├── generate-apis.ts │ └── known-errors.ts └── runtime │ ├── index.ts │ └── trie.ts ├── test ├── buildtime │ ├── build-index.spec.ts │ └── generate-api.spec.ts ├── integration.spec.ts ├── runtime │ └── trie.spec.ts ├── test-helpers.ts └── tsconfig.json ├── tsconfig.json └── wallaby.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | name: Build & test 6 | runs-on: ubuntu-latest 7 | container: httptoolkit/act-build-base:v3.0.0 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | 15 | - run: npm ci 16 | - run: npm test # Test the code itself 17 | - run: npm run build # Confirm that the API specs & index builds successfully -------------------------------------------------------------------------------- /.github/workflows/daily-main-check.yml: -------------------------------------------------------------------------------- 1 | name: Daily directory build & test 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 12 * * *' 6 | jobs: 7 | build: 8 | name: Build & test 9 | runs-on: ubuntu-latest 10 | container: httptoolkit/act-build-base:v3.0.0 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | 18 | - run: npm ci 19 | 20 | # Check that the tests and overall build all still pass 21 | - run: npm test 22 | - run: npm run build -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Npm directory update 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Once per month, on an offset date/time because apparently GHA has 6 | # problems with spikes of scheduled jobs at midnight etc. 7 | - cron: 30 13 15 * * 8 | jobs: 9 | publish: 10 | name: Build, bump & publish to npm 11 | runs-on: ubuntu-latest 12 | container: httptoolkit/act-build-base:v3.0.0 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - run: npm ci 22 | 23 | # Check that the tests and overall build all still pass: 24 | - run: npm test 25 | - run: npm run build 26 | 27 | # Create the new release: 28 | - name: Bump version & push 29 | run: | 30 | git config --global user.name 'Automated publish' 31 | git config --global user.email 'pimterry@users.noreply.github.com' 32 | 33 | npm version patch 34 | 35 | git push && git push --tags 36 | 37 | # Publish the release to npm: 38 | - run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # The build output 64 | dist/ 65 | 66 | # The generated API files 67 | api/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Mocha Tests", 8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 9 | "args": [ 10 | "-u", 11 | "tdd", 12 | "--timeout", 13 | "999999", 14 | "--colors", 15 | "-r", 16 | "ts-node/register", 17 | "${workspaceFolder}/test/**/*.spec.ts" 18 | ], 19 | "env": { 20 | "TS_NODE_FILES": "true" 21 | }, 22 | "internalConsoleOptions": "openOnSessionStart" 23 | }, 24 | { 25 | "name": "Build All", 26 | "type": "node", 27 | "request": "launch", 28 | "args": ["./src/buildtime/build-all.ts"], 29 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 30 | "sourceMaps": true, 31 | "cwd": "${workspaceRoot}", 32 | "protocol": "inspector", 33 | "env": { 34 | "TS_NODE_FILES": "true" 35 | } 36 | }, 37 | 38 | { 39 | "name": "Current TS File", 40 | "type": "node", 41 | "request": "launch", 42 | "args": ["${relativeFile}"], 43 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 44 | "sourceMaps": true, 45 | "cwd": "${workspaceRoot}", 46 | "protocol": "inspector", 47 | "env": { 48 | "TS_NODE_FILES": "true" 49 | } 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 HTTP Toolkit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An OpenAPI Directory for JS [![Build Status](https://github.com/httptoolkit/openapi-directory-js/workflows/CI/badge.svg)](https://github.com/httptoolkit/openapi-directory-js/actions) [![npm version](https://badge.fury.io/js/openapi-directory.svg)](https://badge.fury.io/js/openapi-directory) 2 | 3 | > _Part of [HTTP Toolkit](https://httptoolkit.com): powerful tools for building, testing & debugging HTTP(S)_ 4 | 5 | This repo builds & bundles the [OpenAPI Directory](https://github.com/APIs-guru/openapi-directory), so you can easily find, require and use any OpenAPI spec from the directory in your JS projects. 6 | 7 | It provides files that can be individually required or remotely downloaded (via https://unpkg.com/openapi-directory/) for every API in the collection, and an index to quickly find the relevant OpenAPI spec for a given URL. 8 | 9 | All specs are: 10 | 11 | * Pre-parsed and exposed as JavaScript objects (not YAML strings). 12 | * Converted to OpenAPI v3. 13 | * Pre-bundled with all external $refs. 14 | 15 | That means you can import them, and immediately & consistently start using them. 16 | 17 | ## How to use it 18 | 19 | First up, install it with: 20 | 21 | ```bash 22 | npm install openapi-directory 23 | ``` 24 | 25 | All OpenAPI specs can be now required with: 26 | 27 | ```js 28 | const spec = require('openapi-directory/api/.json'); 29 | ``` 30 | 31 | (or read from `https://unpkg.com/openapi-directory/api/.json`) 32 | 33 | The easiest way to obtain a spec id is to use the index. You can look up a URL in the index with: 34 | 35 | ```js 36 | const { findApi } = require('openapi-directory'); 37 | 38 | findApi('wikimedia.org/api/rest_v1/feed/availability'); 39 | ``` 40 | 41 | `findApi` takes a URL (host and path, _without_ the protocol) within any API, and will return either: 42 | 43 | * Undefined, if there is no matching APIs. 44 | * A string spec id, if there is exactly one API that's relevant to that URL. 45 | * A list of spec ids, in rare cases where multiple specs may cover the same URL. 46 | 47 | Alternatively if you know in advance which spec you want you can require it directly. The id for every spec in the directory is made up of the provider name, followed by a slash and the service name if a service name exists. Some example ids: 48 | 49 | * `xkcd.com` (provider is xkcd.com, no service name) 50 | * `amazonaws.com/acm` (provider is amazonaws.com, service name is acm). 51 | 52 | You can find the provider and service name in the spec itself (under `info`, `x-providerName` and `x-serviceName`), and you can browse the raw specs directly at https://github.com/APIs-guru/openapi-directory. 53 | 54 | ## License 55 | 56 | This repo/npm module is licensed as MIT. 57 | 58 | The license for API definitions varies by spec, see https://github.com/APIs-guru/openapi-directory#licenses for more information. 59 | 60 | In general it's very likely that your use of any API definition is covered either by CC0 (for specs submitted directly to the directory), the spec's own license (check `info.license`) or by Fair Use provisions when communicating with the corresponding service. This is not formal legal advice though, its your responsibility to confirm this for yourself for the specs you're using. -------------------------------------------------------------------------------- /custom-typings/swagger2openapi.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'swagger2openapi'; -------------------------------------------------------------------------------- /openapi-directory.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'openapi-directory/api/*.json' { 2 | import { OpenAPIObject } from 'openapi3-ts'; 3 | const api: OpenAPIObject; 4 | export = api; 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi-directory", 3 | "version": "1.3.12", 4 | "description": "Building & bundling https://github.com/APIs-guru/openapi-directory for easy use from JS", 5 | "main": "dist/runtime/index.js", 6 | "types": "dist/runtime/index.d.ts", 7 | "files": [ 8 | "api/", 9 | "dist/runtime/", 10 | "openapi-directory.d.ts", 11 | "src/" 12 | ], 13 | "scripts": { 14 | "prepare": "napa APIs-guru/openapi-directory#main:openapi-directory", 15 | "prepack": "npm run build", 16 | "build": "tsc && node dist/buildtime/build-all.js", 17 | "test": "TS_NODE_FILES=true mocha -r ts-node/register 'test/**/*.spec.ts'" 18 | }, 19 | "runkitExampleFilename": "./runkit-demo.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/httptoolkit/openapi-directory-js.git" 23 | }, 24 | "keywords": [ 25 | "openapi", 26 | "directory", 27 | "collection", 28 | "apis", 29 | "swagger" 30 | ], 31 | "author": "Tim Perry ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/httptoolkit/openapi-directory-js/issues" 35 | }, 36 | "homepage": "https://github.com/httptoolkit/openapi-directory-js#readme", 37 | "engines": { 38 | "node": ">=8.0.0" 39 | }, 40 | "dependencies": { 41 | "openapi3-ts": "^1.2.0" 42 | }, 43 | "devDependencies": { 44 | "@apidevtools/swagger-parser": "^10.1.0", 45 | "@types/chai": "^4.1.7", 46 | "@types/fs-extra": "^5.0.4", 47 | "@types/lodash": "^4.14.120", 48 | "@types/mocha": "^5.2.5", 49 | "@types/node": "^18.14.2", 50 | "@types/serialize-javascript": "^1.5.0", 51 | "chai": "^4.2.0", 52 | "fs-extra": "^7.0.1", 53 | "globby": "^11.1.0", 54 | "lodash": "^4.17.21", 55 | "mocha": "^10.1.0", 56 | "napa": "^3.0.0", 57 | "openapi-types": "^7.2.3", 58 | "serialize-javascript": "^5.0.1", 59 | "swagger2openapi": "^7.0.8", 60 | "ts-node": "^8.0.2", 61 | "typescript": "^4.9.5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /runkit-demo.js: -------------------------------------------------------------------------------- 1 | const { findApi } = require('openapi-directory'); 2 | 3 | const requestUrls = [ 4 | "api.nytimes.com/svc/topstories/v2/travel.json", 5 | "sqs.us-east-2.amazonaws.com/123456789012/MyQueue/?Action=SendMessage&MessageBody=test+message", 6 | "api.github.com/repos/httptoolkit/mockttp", 7 | ]; 8 | 9 | requestUrls.forEach((url) => { 10 | console.log(url); 11 | // Look up the API spec id from the URL: 12 | const apiId = findApi(url); 13 | 14 | // With the id, you can require() a full specification for this API: 15 | const apiSpec = require(`openapi-directory/api/${apiId}`); 16 | 17 | // Includes lots of things, e.g. a link straight to the API docs: 18 | console.log(` -> Docs: ${apiSpec.externalDocs.url}`); 19 | }); -------------------------------------------------------------------------------- /src/buildtime/build-all.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import * as serializeToJs from 'serialize-javascript'; 4 | 5 | import { ApiGenerationOptions, generateApis } from "./generate-apis"; 6 | import { buildTrie } from './build-index'; 7 | 8 | const SOURCE_DIRECTORY = path.join('node_modules', 'openapi-directory', 'APIs'); 9 | 10 | export async function buildAll(globs: string[], options: ApiGenerationOptions = {}) { 11 | const index = await generateApis(globs, options); 12 | 13 | console.log('APIs generated and written to disk'); 14 | 15 | await fs.writeFile( 16 | path.join('api', '_index.js'), 17 | 'module.exports = ' + serializeToJs(buildTrie(index), { 18 | unsafe: true // We're not embedded in HTML, we don't want XSS escaping 19 | }) 20 | ); 21 | 22 | console.log(`Index trie for ${index.size} entries generated and written to disk`); 23 | } 24 | 25 | if (require.main === module) { 26 | buildAll([ 27 | `${SOURCE_DIRECTORY}/**/swagger.yaml`, 28 | `${SOURCE_DIRECTORY}/**/openapi.yaml`, 29 | ], { 30 | // When run directly, we expect KNOWN_ERRORS to *exactly* match the current errors 31 | expectKnownErrors: true, 32 | ignoreKnownErrors: true 33 | }).catch((e) => { 34 | console.error(e); 35 | process.exit(1); 36 | }); 37 | } -------------------------------------------------------------------------------- /src/buildtime/build-index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { TrieData, isLeafValue, TrieValue } from "../runtime/trie"; 3 | 4 | type TrieInput = Map< 5 | Array, 6 | string | string[] 7 | >; 8 | 9 | export function buildTrie(map: TrieInput): TrieData { 10 | return optimizeTrie(buildNaiveTrie(map)); 11 | } 12 | 13 | // Build a simple naive trie. One level per char, string nodes at the leaves, 14 | // and '' keys for leaf nodes half way down paths. 15 | export function buildNaiveTrie(map: TrieInput): TrieData { 16 | const root = new Map(); 17 | 18 | // For each key, make a new level for each char in the key (or use an existing 19 | // level), and place the leaf when we get to the end of the key. 20 | 21 | for (let [keys, value] of map) { 22 | let trie: TrieData = root; 23 | 24 | const keyChunks = _.flatMap(keys, (key) => { 25 | if (_.isRegExp(key)) return key; 26 | else return key.split(''); 27 | }); 28 | _.forEach(keyChunks, (chunk, i) => { 29 | let nextStep = chunk instanceof RegExp ? 30 | trie.get(_.find([...trie.keys()], k => _.isEqual(chunk, k))!) : 31 | trie.get(chunk); 32 | 33 | let isLastChunk = i === keyChunks.length - 1; 34 | 35 | if (isLastChunk) { 36 | // We're done - write our value into trie[char] 37 | 38 | if (isLeafValue(nextStep)) { 39 | throw new Error('Duplicate key'); // Should really never happen 40 | } else if (typeof nextStep === 'object') { 41 | // We're half way down another key - add an empty branch 42 | nextStep.set('', value); 43 | } else { 44 | // We're a fresh leaf at the end of a branch 45 | trie.set(chunk, value); 46 | } 47 | } else { 48 | // We have more to go - iterate into trie[char] 49 | 50 | if (isLeafValue(nextStep)) { 51 | // We're at what is currently a leaf value 52 | // Transform it into a node with '' for the value. 53 | nextStep = new Map([['', nextStep]]); 54 | trie.set(chunk, nextStep); 55 | } else if (typeof nextStep === 'undefined') { 56 | // We're adding a new branch to the trie 57 | nextStep = new Map(); 58 | trie.set(chunk, nextStep); 59 | } 60 | 61 | trie = nextStep; 62 | } 63 | }); 64 | } 65 | 66 | return root; 67 | } 68 | 69 | // Compress the trie. Any node with only one child can be combined 70 | // with the child node instead. This results in keys of >1 char, but 71 | // all keys in any given object still always have the same length, 72 | // except for terminated strings. 73 | export function optimizeTrie(trie: TrieData): TrieData { 74 | if (_.isString(trie)) return trie; 75 | 76 | const keys = [...trie.keys()].filter(k => k !== ''); 77 | 78 | if (keys.length === 0) return trie; 79 | 80 | if (keys.length === 1) { 81 | // If this level has one string key, combine it with the level below 82 | const [key] = keys; 83 | const child = trie.get(key)!; 84 | 85 | // If the child is a final value, we can't combine this key with it, and we're done 86 | // TODO: Could optimize further here, and pull the child up in this case? 87 | // (Only if trie.size === 1 too). Seems unnecessary for now, a little risky. 88 | if (isLeafValue(child)) return trie; 89 | 90 | if ( 91 | // Don't combine if our child has a leaf node attached - this would break 92 | // search (en route leaf nodes need to always be under '' keys) 93 | !child.get('') && 94 | // If this key or any child key is a regex, we don't try to combine the 95 | // keys together. It's possible to do so, but a little messy, 96 | // not strictly necessary, and hurts runtime perf (testing up to N regexes 97 | // is worse than testing 1 regex + 1 string hash lookup). 98 | !_.isRegExp(keys[0]) && 99 | !_.some([...child.keys()], k => _.isRegExp(k)) 100 | ) { 101 | // Replace this node with the only child, with every key prefixed with this key 102 | const collapsedChild = mapMap(child, (childKey, value) => 103 | // We know keys are strings because we checked above 104 | [key + (childKey as string), value] 105 | ); 106 | // We might still have an en-route leaf node at this level - don't lose it. 107 | if (trie.get('')) collapsedChild.set('', trie.get('')); 108 | // Then we reoptimize this same level again (we might be able to to collapse further) 109 | return optimizeTrie(collapsedChild); 110 | } 111 | } 112 | 113 | // Recursive DFS through the child values to optimize them in turn 114 | return mapMap(trie, (key, child): [string | RegExp, TrieValue] => { 115 | if (isLeafValue(child)) return [key, child]; 116 | else return [key, optimizeTrie(child!)]; 117 | }); 118 | } 119 | 120 | function mapMap( 121 | map: Map, 122 | mapping: (a: K, b: V) => [K2, V2] 123 | ): Map { 124 | return new Map( 125 | Array.from(map, ([k, v]) => mapping(k, v)) 126 | ); 127 | } -------------------------------------------------------------------------------- /src/buildtime/generate-apis.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs-extra'; 4 | import * as globby from 'globby'; 5 | 6 | import * as swaggerParser from '@apidevtools/swagger-parser'; 7 | import * as swaggerToOpenApi from 'swagger2openapi'; 8 | 9 | import { OpenAPI, OpenAPIV3 } from 'openapi-types'; 10 | import { PathsObject, InfoObject } from 'openapi3-ts'; 11 | 12 | import { 13 | KNOWN_ERRORS, 14 | matchErrors 15 | } from './known-errors'; 16 | 17 | const OUTPUT_DIRECTORY = path.join(__dirname, '..', '..', 'api'); 18 | 19 | type ExtendedInfo = InfoObject & { 20 | 'x-preferred'?: boolean; 21 | 'x-providerName'?: string; 22 | 'x-serviceName'?: string; 23 | } 24 | 25 | type OpenAPIDocument = OpenAPIV3.Document & { 26 | 'x-ms-paths'?: PathsObject; 27 | info: ExtendedInfo 28 | } 29 | 30 | async function generateApi(specPath: string): Promise { 31 | const parsedSwagger = await swaggerParser.parse(specPath); 32 | 33 | // Manually patch up some known issues in existing specs: 34 | patchSpec(parsedSwagger); 35 | 36 | // Convert everything to OpenAPI v3 37 | const openapi: OpenAPIV3.Document = await swaggerToOpenApi.convertObj(parsedSwagger, { 38 | direct: true, 39 | patch: true 40 | }); 41 | 42 | // Extra conversion to transform x-ms-paths into fragment queries 43 | mergeMsPaths(openapi); 44 | 45 | // Bundle all external $ref pointers 46 | return > swaggerParser.bundle(openapi); 47 | } 48 | 49 | function mergeMsPaths(openApi: OpenAPIDocument) { 50 | const msPaths = openApi['x-ms-paths']; 51 | if (!msPaths) return openApi; 52 | 53 | Object.assign(openApi.paths, _(msPaths) 54 | .mapKeys((v, key) => key.replace('?', '#')) // Turn invalid queries into fragments queries like AWS 55 | .pickBy((v, key) => !openApi.paths[key]) // Drop any conflicting paths 56 | .valueOf() 57 | ); 58 | delete openApi['x-ms-paths']; 59 | } 60 | 61 | function patchSpec(spec: OpenAPI.Document & Partial) { 62 | const specId = idFromSpec(spec); 63 | 64 | if (specId === 'spotify.com') { 65 | // Fix a broken reference to an external policies.json file: 66 | delete (spec.components as any)['x-spotify-policy']['$ref']; 67 | } 68 | 69 | if (specId === 'evetech.net') { 70 | // Fix a (not swagger-convertable) parameter definition: 71 | delete spec.paths['/route/{origin}/{destination}/'] 72 | .get 73 | .parameters[1] 74 | .items.collectionFormat; 75 | } 76 | } 77 | 78 | type PathParts = Array; 79 | 80 | function commonStringPrefix(...strings: string[]): string { 81 | const first = strings[0] || ''; 82 | let commonLength = first.length 83 | 84 | for (let i = 1; i < strings.length; ++i) { 85 | for (let j = 0; j < commonLength; ++j) { 86 | if (strings[i].charAt(j) !== first.charAt(j)) { 87 | commonLength = j 88 | break 89 | } 90 | } 91 | } 92 | 93 | return first.slice(0, commonLength) 94 | } 95 | 96 | function commonPartsPrefix(...partsArrays: Array): PathParts { 97 | const first = partsArrays[0] || []; 98 | let commonLength = first.length; 99 | 100 | // Find the length of the exactly matching parts prefix for these arrays 101 | for (let i = 1; i < partsArrays.length; ++i) { 102 | for (let j = 0; j < commonLength; ++j) { 103 | const parts = partsArrays[i]; 104 | if (!_.isEqual(parts[j], first[j])) { 105 | commonLength = j; 106 | break; 107 | } 108 | } 109 | } 110 | 111 | // In the first non-matching part, find the common prefix there (if any) 112 | const nonMatchingIndex = commonLength; 113 | let extraSubPart: string | undefined; 114 | // They must all be strings for us to be able to do this. 115 | if (!_.some(partsArrays, part => !part[nonMatchingIndex] || part[nonMatchingIndex] instanceof RegExp)) { 116 | // Get the common string prefix (if any) of the non-matching part of each parts array 117 | extraSubPart = commonStringPrefix( 118 | ...partsArrays.map(parts => parts[nonMatchingIndex] as string) 119 | ); 120 | } 121 | 122 | if (extraSubPart) { 123 | return first.slice(0, commonLength).concat(extraSubPart); 124 | } else { 125 | return first.slice(0, commonLength) 126 | } 127 | } 128 | 129 | function isAPrefix(possiblePrefix: PathParts, longerParts: PathParts) { 130 | const commonPrefix = commonPartsPrefix(possiblePrefix, longerParts); 131 | return _.isEqual(possiblePrefix, commonPrefix); 132 | } 133 | 134 | // Take a base URL, and a set of specs who all use the same base, and build an index 135 | // within that base, keyed by the unique paths that map to each spec. This might still 136 | // end up with duplicates if two specs define literally the same endpoint, but it 137 | // gets much closer than just using the server URLs standalone. 138 | export function calculateCommonPrefixes( 139 | commonBase: string, 140 | specPaths: { [specId: string]: string[] } 141 | ): Map { 142 | let index = new Map(); 143 | 144 | const specPathParts = _.mapValues(specPaths, (paths) => 145 | paths.map(path => 146 | path 147 | .split('#')[0] // Drop anything in a fragment (#Action=abc) 148 | .split(/\{[^}]+\}/) // Split around param sections 149 | .reduce((pathParts, pathPart) => { 150 | // Inject a param regex between every split section to replace the params, and at the end 151 | return [...pathParts, pathPart, /^[^/]+/]; 152 | }, []) 153 | .slice(0, -1) // Drop the extra param regex from the end 154 | .filter(p => !!p) // Drop any empty strings, e.g. if {param} was the end of the path 155 | ) 156 | ); 157 | 158 | _.forEach(specPathParts, (pathParts, specId) => { 159 | // For each spec, try to work out the minimum set of URL prefixes that unambiguously 160 | // point to this spec, and no others. 161 | 162 | let prefixes: Array = []; 163 | const otherSpecPaths = _(specPathParts) 164 | .omit(specId) 165 | .flatMap((otherPaths) => otherPaths).valueOf(); 166 | 167 | pathParts.forEach((parts) => { 168 | // If the existing prefixes for this spec already work fine for this path, skip it 169 | if (_.some(prefixes, (prefixParts) => 170 | // Does an existing prefix for this spec match the start of this path? 171 | isAPrefix(prefixParts, parts) && 172 | // Do no prefixes for other specs match the start of this path? 173 | !_.some(otherSpecPaths, path => isAPrefix(path, parts)) 174 | )) return; 175 | 176 | // Try to shorten existing prefixes as little as possible to match this path, 177 | // without matching the paths of any other routes 178 | const possibleShortenings = prefixes 179 | .map((prefix) => commonPartsPrefix(parts, prefix)) 180 | .filter((shortenedPrefix) => 181 | shortenedPrefix.length && 182 | !_.some(otherSpecPaths, path => isAPrefix(shortenedPrefix, path)) 183 | ); 184 | 185 | // Sort and [0] to use the longest/most specific shortened prefix we can come up with 186 | const shortening = possibleShortenings 187 | .sort((prefixA, prefixB) => { 188 | if (prefixA.length > prefixB.length) return -1; 189 | if (prefixB.length > prefixA.length) return 1; 190 | 191 | // The have the same number of parts, compare the length of the last parts 192 | const lastIndex = prefixA.length - 1; 193 | const lastPartA = prefixA[lastIndex]; 194 | const lastPartB = prefixB[lastIndex]; 195 | if (lastPartA.toString().length > lastPartB.toString().length) return -1; 196 | if (lastPartA.toString().length > lastPartA.toString().length) return 1; 197 | return 0; 198 | })[0]; 199 | 200 | if (shortening && shortening.length) { 201 | // Drop any existing prefixes that this makes irrelevant 202 | prefixes = prefixes 203 | .filter((existingPrefix) => !isAPrefix(shortening, existingPrefix)) 204 | .concat([shortening]); 205 | } else { 206 | // There are no possible shortenings of existing prefixes that would uniquely 207 | // identify this path - we have nothing unique in commmon with existing prefixes. 208 | // Either we're a new 'branch' of the API (e.g. the first endpoint), or 209 | // we're a prefix/exact match for some other endpoint's full path. 210 | // We create a specific case that covers this one path, which may be shortened 211 | // later on if we find other similar paths for this spec. 212 | prefixes.push(parts); 213 | } 214 | }); 215 | 216 | // Add each prefix that was found to an index of URLs within this base URL. 217 | prefixes.forEach((prefixParts) => { 218 | const baseUrl = commonBase.replace(/\/$/, ''); 219 | const urlParts = 220 | prefixParts[0] === undefined ? 221 | [baseUrl] : 222 | _.isString(prefixParts[0]) ? 223 | [baseUrl + prefixParts[0], ...prefixParts.slice(1)] 224 | : [baseUrl, ...prefixParts]; 225 | 226 | const existingKey = _.find(Array.from(index.keys()), (k) => _.isEqual(k, urlParts)); 227 | const key = existingKey || urlParts; 228 | const existingValue = index.get(key); 229 | 230 | if (_.isArray(existingValue)) { 231 | index.set(key, _.union(existingValue, [specId])); 232 | } else if (existingValue) { 233 | index.set(key, _.union([existingValue], [specId])); 234 | } else { 235 | index.set(key, specId); 236 | } 237 | }); 238 | }); 239 | 240 | // Where we have directly conflicting endpoints (two specs both defining a.com/b), we'll get 241 | // a prefix for each unique endpoint. This is technically correct, but for specs that heavily 242 | // overlap (e.g. Azure's LUIS Runtime/Authoring) these become very noisy and inflate the index 243 | // hugely (approx 100% right now). 244 | // Here, we try to find shortenings for conflicting cases, by grouping each set of conflicts, 245 | // and finding a small set of shortenings that cover these cases. The heuristic for 'small' 246 | // is to include allow shortenings that match no other specs (so therefore are pretty specific). 247 | _(Array.from(index)) 248 | // Get conflicting groups 249 | .filter(([key, value]) => Array.isArray(value)) 250 | // Group by the specs which conflict 251 | .groupBy(([key, value]) => value) 252 | // For each set of conflicts, find the possible shortenings 253 | .forEach((conflicts) => { 254 | const conflictingSpecs = conflicts[0][1]; 255 | const conflictingPaths = conflicts.map(([key]) => key); 256 | 257 | // Collect all paths that map to specs other than these. These are the specs we don't 258 | // want our shortenings to overlap with. 259 | const otherSpecPaths = _(Array.from(index)) 260 | // Don't worry about any paths that only match one of these specs 261 | .omitBy(([_paths, specs]: [unknown, string | string[]]) => { 262 | if (Array.isArray(specs)) { 263 | return _.intersection(specs, conflictingSpecs).length === specs.length; 264 | } else { 265 | return _.includes(conflictingSpecs, specs); 266 | } 267 | }) 268 | .map(([otherPaths]) => otherPaths).valueOf(); 269 | 270 | const shortenings = conflictingPaths.reduce((foundShortenings: PathParts[], conflictingPath) => { 271 | // Find any possible shared prefixes between this conflicting path 272 | // and our shortened prefixes found so far that don't conflict with other specs 273 | let newShortenings = foundShortenings 274 | .map(s => commonPartsPrefix(s, conflictingPath)) 275 | .filter(s => s && !_.some(otherSpecPaths, path => isAPrefix(s, path))); 276 | 277 | // If this can't be shortened, it needs to exist as a path standalone. This is ok - it will 278 | // always happen for the first value at least, and it can be shortened further later. 279 | if (newShortenings.length === 0) newShortenings = [conflictingPath]; 280 | 281 | return foundShortenings 282 | // Remove any existing shortenings that our new shortenings replace 283 | .filter(s => !_.some(newShortenings, 284 | newShortening => isAPrefix(newShortening, s)) 285 | ) 286 | // Add our new shortenings 287 | .concat(newShortenings); 288 | }, []); 289 | 290 | // We now have a set of common paths covering these conflicting paths, which 291 | // is hopefully shorter (at worst the same) as the existing ones. Update the 292 | // index to replace the conflicts with these values. 293 | conflictingPaths.forEach((p) => index.delete(p)); 294 | shortenings.forEach((p) => index.set(p, conflictingSpecs)); 295 | // (An aside: we know writing over index[p] is safe, because shortenings cannot 296 | // contain any paths that conflict with existing paths in the index) 297 | }) 298 | 299 | return index; 300 | } 301 | 302 | function getSpecPath(specId: string) { 303 | return path.join('api', specId) + '.json'; 304 | } 305 | 306 | function idFromSpec(spec: OpenAPIDocument | OpenAPI.Document) { 307 | const info = spec.info as ExtendedInfo; 308 | const provider = info['x-providerName']; 309 | const service = info['x-serviceName']; 310 | 311 | return provider + (service ? path.sep + service : ''); 312 | } 313 | 314 | export interface ApiGenerationOptions { 315 | /** 316 | * If set, API generation will not fail for errors in the known error set. 317 | */ 318 | ignoreKnownErrors?: boolean, 319 | 320 | /** 321 | * If set, API generation will fail if not all errors in the known error set are thrown. 322 | */ 323 | expectKnownErrors?: boolean 324 | 325 | } 326 | 327 | const IGNORED_SPEC_IDS = [ 328 | // Ambiguous GitHub specs - we consider these as duplicate non-preferred versions of 329 | // github.com/api.github.com, and ignore entirely, until this issue is fixed: 330 | // https://github.com/APIs-guru/openapi-directory/issues/1115 331 | 'github.com/ghec.2022-11-28', 332 | 'github.com/ghec', 333 | 'github.com/api.github.com.2022-11-28' 334 | ]; 335 | 336 | const IGNORED_SPEC_PATHS = [ 337 | 'node_modules/openapi-directory/APIs/ably.net/control/1.0.14/openapi.yaml', // Dupe of ably.net/control/v1 338 | 'node_modules/openapi-directory/APIs/visma.net/1.0.14.784/openapi.yaml' // Old & conflicting with Visma 9.0 spec 339 | ]; 340 | 341 | // For some specs, we do want to include the spec in the collection, but the server URLs shouldn't 342 | // be indexed, because they're not valid public URLs. For example, relative URLs, localhost/local 343 | // network URLs, etc. The specs can still be required by id, where required, it's just that they're 344 | // ambiguous enough that looking up these addresses in the index shouldn't return these APIs. 345 | function shouldIndexUrl(url: string) { 346 | if (!url) return false; // Bizarrely, yes, some specs list an empty server URL 347 | if (url.startsWith('/')) return false; // Purely relative URLs aren't indexable 348 | 349 | // Make protocol-less URLs (common from Swagger?) parseable: 350 | if (!url.match(/^http(s)?:\/\//)) url = `https://${url}`; 351 | 352 | try { 353 | const parsedUrl = new URL(url); 354 | 355 | return parsedUrl.hostname !== 'localhost' && // Localhost URLs 356 | !parsedUrl.hostname.endsWith('.localhost') && 357 | !parsedUrl.hostname.endsWith('.local') && // mDNS local addresses 358 | !parsedUrl.hostname.match(/^(127|10|192|0)\.\d+\.\d+\.\d+$/) && // Local network ips 359 | parsedUrl.hostname.includes('.'); // Local-only hostnames 360 | } catch (e) { 361 | console.log('Failed to parse', url); 362 | return false; // If it's not a parseable URL, it's definitely not indexable 363 | } 364 | } 365 | 366 | export async function generateApis(globs: string[], options: ApiGenerationOptions = {}) { 367 | const [specs] = await Promise.all([ 368 | globby(globs), 369 | fs.emptyDir(OUTPUT_DIRECTORY) 370 | ]); 371 | 372 | const index: _.Dictionary = {}; 373 | const specIds: _.Dictionary = {}; 374 | 375 | const errors: { [specSource: string]: Error } = {}; 376 | 377 | await Promise.all( 378 | specs.map(async (specSource) => { 379 | const spec = await generateApi(specSource); 380 | 381 | // If the spec has been explicitly superceded, skip it. 382 | // TODO: In future, accept it, if it adds some distinct paths? 383 | if (spec.info['x-preferred'] === false) return; 384 | 385 | const specId = idFromSpec(spec); 386 | 387 | // To work around some awkward spec overlaps, in some cases we drop specs from the index 388 | // unilaterally, to effectively override our 'preferred' version as the 389 | if (IGNORED_SPEC_IDS.includes(specId) || IGNORED_SPEC_PATHS.includes(specSource)) return; 390 | 391 | const { servers } = spec; 392 | const serverUrls = _(servers!) 393 | // Expand to include possible variable values. This handles v3 specs, or any 394 | // specs converted to v3 variable hosts (e.g. azure's x-ms-parameterized-host). 395 | .flatMap((server) => { 396 | if (!server.variables) { 397 | return server.url; 398 | } 399 | 400 | return _.reduce(server.variables, (urls, variable, variableKey) => { 401 | const { 402 | default: varDefault, 403 | enum: enumValues 404 | } = variable; 405 | 406 | const possibleValues = _.uniq( 407 | [ 408 | varDefault, 409 | ...(enumValues || []) 410 | ].filter(v => !!v) 411 | ); 412 | 413 | // For each URL we have, replace it with each possible var replacement 414 | return _.flatMap(urls, (url) => 415 | possibleValues.map((value) => url.replace(`{${variableKey}}`, value)) 416 | ); 417 | 418 | // Here we will drop specs for any variables without concrete default/enum 419 | // values. Wildcard URL params (e.g. Azure's search-searchservice) could be 420 | // matched with index regexes, but it might get expensive, and it's complicated 421 | // to do. Ignoring this for now, but TODO: we might want to match open wildcards 422 | // in URLs later (as long as their _somewhat_ constrained - 100% wildcard is bad) 423 | }, [server.url]); 424 | }) 425 | // Drop protocols from all URLs 426 | .map((url) => url.replace(/^(https?:)?\/\//, '').toLowerCase()) 427 | .uniq() 428 | .valueOf(); 429 | 430 | // Case-insensitively check for duplicate spec ids, and report conflicts: 431 | const existingSpecSource = specIds[specId.toLowerCase()]; 432 | if (existingSpecSource) { 433 | throw new Error(`Duplicate spec id ${specId} between ${specSource} and ${existingSpecSource}`); 434 | } 435 | specIds[specId.toLowerCase()] = specSource; 436 | 437 | serverUrls.forEach((url) => { 438 | if (!shouldIndexUrl(url)) return; // Skip adding this spec to the index 439 | 440 | if (index[url]) { 441 | index[url] = [specId].concat(index[url]); 442 | } else { 443 | index[url] = specId; 444 | } 445 | }); 446 | 447 | const specPath = getSpecPath(specId); 448 | 449 | const exists = await new Promise((resolve) => 450 | fs.exists(specPath, resolve) 451 | ); 452 | 453 | if (exists) { 454 | console.warn( 455 | `Spec naming collision for ${specId} (from ${specSource})` 456 | ); 457 | } 458 | 459 | await fs.mkdirp(path.dirname(specPath)); 460 | await fs.writeFile(specPath, JSON.stringify(spec)); 461 | }).map((p, i) => p.catch((err: Error) => { 462 | console.log( 463 | `Failed to generate API from ${specs[i]}`, 464 | err.message.split('\n')[0] 465 | ); 466 | 467 | errors[specs[i]] = err; 468 | })) 469 | ); 470 | 471 | const errorPairs = Object.entries(errors); 472 | const expectedErrorsPairs = Object.entries(KNOWN_ERRORS); 473 | 474 | if (options.ignoreKnownErrors) { 475 | const unexpectedErrors = _.differenceWith(errorPairs, expectedErrorsPairs, matchErrors); 476 | if (unexpectedErrors.length) { 477 | throw new Error(`Build failed unexpectedly due to errors in:\n${ 478 | unexpectedErrors.map(([source]) => source).join(', ') 479 | }`); 480 | } 481 | } else { 482 | if (errorPairs.length) throw new Error('Unexpected errors while building specs'); 483 | } 484 | 485 | if (options.expectKnownErrors) { 486 | const missingErrors = _.differenceWith(expectedErrorsPairs, errorPairs, (a, b) => matchErrors(b, a)); 487 | if (missingErrors.length) { 488 | throw new Error(`Build succeeded, but unexpectedly missing known errors from:\n${ 489 | missingErrors.map(([source]) => source).join(', ') 490 | }`); 491 | } 492 | } 493 | 494 | console.log('APIs parsed and loaded'); 495 | 496 | // Try to split duplicate index values for the same key (e.g. two specs for the 497 | // same base server URL) into separate values, by pulling common prefixes of the 498 | // paths from the specs into their index keys. 499 | let dedupedIndex = new Map(); 500 | await Promise.all( 501 | _.map(index, async (specs, commonBase) => { 502 | if (typeof specs === 'string') { 503 | dedupedIndex.set([commonBase], specs); 504 | return; 505 | } else { 506 | const specFiles: _.Dictionary = _.fromPairs( 507 | await Promise.all( 508 | specs.map(async (specId) => [ 509 | specId, 510 | await swaggerParser.parse(getSpecPath(specId)) 511 | .then((api) => 512 | Object.keys(api.paths).map(p => p.toLowerCase()) 513 | ) 514 | ]) 515 | ) 516 | ); 517 | 518 | 519 | console.log(`Deduping index for ${commonBase}`); 520 | const dedupedPaths = calculateCommonPrefixes(commonBase, specFiles); 521 | dedupedIndex = new Map([...dedupedIndex, ...dedupedPaths]); 522 | } 523 | }) 524 | ); 525 | 526 | return dedupedIndex; 527 | } -------------------------------------------------------------------------------- /src/buildtime/known-errors.ts: -------------------------------------------------------------------------------- 1 | export function matchErrors([source, error]: [string, Error], [expectedSource, expectedErrorMessage]: [string, string]) { 2 | return source === expectedSource && 3 | error.message.includes(expectedErrorMessage); 4 | } 5 | 6 | // A list of known errors. During API parsing, we check that these errors (no more, no less) are thrown, to ensure that 7 | // we're aware of all current issues. Where possible of course it's better to either get the issue fixed in the spec 8 | // upstream, or patch the spec ourselves before parsing, but these are remaining examples where that's difficult. 9 | export const KNOWN_ERRORS = { 10 | // Fails due to a bad x-ms-odata internal reference: 11 | "node_modules/openapi-directory/APIs/azure.com/azsadmin-RegionHealth/2016-05-01/swagger.yaml": "Could not resolve reference #/definitions/Alert", 12 | 13 | // A $ref to a key that's a number with an underscore, which is parsed wrong due to: 14 | // https://github.com/nodeca/js-yaml/issues/627 15 | "node_modules/openapi-directory/APIs/statsocial.com/1.0.0/openapi.yaml": "Token \"18_24\" does not exist.", 16 | 17 | // A few specs with $ref links to `null` properties, which fails due to: 18 | // https://github.com/APIDevTools/json-schema-ref-parser/issues/310 19 | "node_modules/openapi-directory/APIs/personio.de/personnel/1.0/openapi.yaml": "Token \"comment\" does not exist.", 20 | "node_modules/openapi-directory/APIs/rebilly.com/2.1/openapi.yaml": "Token \"feature\" does not exist.", 21 | "node_modules/openapi-directory/APIs/viator.com/1.0.0/openapi.yaml": "Token \"pas\" does not exist.", 22 | 23 | // Lots of Azure specs that reference external files which don't exist: 24 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2016-12-01/swagger.yaml": "Error opening file", 25 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2017-09-01/swagger.yaml": "Error opening file", 26 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2017-06-01/swagger.yaml": "Error opening file", 27 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2017-08-01/swagger.yaml": "Error opening file", 28 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2017-10-01/swagger.yaml": "Error opening file", 29 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2017-11-01/swagger.yaml": "Error opening file", 30 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-02-01/swagger.yaml": "Error opening file", 31 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-01-01/swagger.yaml": "Error opening file", 32 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-04-01/swagger.yaml": "Error opening file", 33 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-06-01/swagger.yaml": "Error opening file", 34 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-10-01/swagger.yaml": "Error opening file", 35 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-08-01/swagger.yaml": "Error opening file", 36 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-07-01/swagger.yaml": "Error opening file", 37 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-11-01/swagger.yaml": "Error opening file", 38 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2019-02-01/swagger.yaml": "Error opening file", 39 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCircuit/2018-12-01/swagger.yaml": "Error opening file", 40 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-02-01/swagger.yaml": "Error opening file", 41 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-04-01/swagger.yaml": "Error opening file", 42 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-07-01/swagger.yaml": "Error opening file", 43 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-06-01/swagger.yaml": "Error opening file", 44 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-08-01/swagger.yaml": "Error opening file", 45 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-10-01/swagger.yaml": "Error opening file", 46 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-12-01/swagger.yaml": "Error opening file", 47 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2018-11-01/swagger.yaml": "Error opening file", 48 | "node_modules/openapi-directory/APIs/azure.com/network-expressRouteCrossConnection/2019-02-01/swagger.yaml": "Error opening file", 49 | "node_modules/openapi-directory/APIs/azure.com/network-interfaceEndpoint/2018-10-01/swagger.yaml": "Error opening file", 50 | "node_modules/openapi-directory/APIs/azure.com/network-interfaceEndpoint/2018-11-01/swagger.yaml": "Error opening file", 51 | "node_modules/openapi-directory/APIs/azure.com/network-interfaceEndpoint/2018-08-01/swagger.yaml": "Error opening file", 52 | "node_modules/openapi-directory/APIs/azure.com/network-interfaceEndpoint/2018-12-01/swagger.yaml": "Error opening file", 53 | "node_modules/openapi-directory/APIs/azure.com/network-interfaceEndpoint/2019-02-01/swagger.yaml": "Error opening file", 54 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2016-09-01/swagger.yaml": "Error opening file", 55 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2015-06-15/swagger.yaml": "Error opening file", 56 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-03-01/swagger.yaml": "Error opening file", 57 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-08-01/swagger.yaml": "Error opening file", 58 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2016-12-01/swagger.yaml": "Error opening file", 59 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-06-01/swagger.yaml": "Error opening file", 60 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-09-01/swagger.yaml": "Error opening file", 61 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-01-01/swagger.yaml": "Error opening file", 62 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-10-01/swagger.yaml": "Error opening file", 63 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-02-01/swagger.yaml": "Error opening file", 64 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2017-11-01/swagger.yaml": "Error opening file", 65 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-06-01/swagger.yaml": "Error opening file", 66 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-07-01/swagger.yaml": "Error opening file", 67 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-04-01/swagger.yaml": "Error opening file", 68 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-08-01/swagger.yaml": "Error opening file", 69 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-10-01/swagger.yaml": "Error opening file", 70 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-11-01/swagger.yaml": "Error opening file", 71 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2019-02-01/swagger.yaml": "Error opening file", 72 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2019-04-01/swagger.yaml": "Error opening file", 73 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2018-12-01/swagger.yaml": "Error opening file", 74 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2016-09-01/swagger.yaml": "Error opening file", 75 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2019-07-01/swagger.yaml": "Error opening file", 76 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2015-06-15/swagger.yaml": "Error opening file", 77 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2019-08-01/swagger.yaml": "Error opening file", 78 | "node_modules/openapi-directory/APIs/azure.com/network-loadBalancer/2019-06-01/swagger.yaml": "Error opening file", 79 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2016-12-01/swagger.yaml": "Error opening file", 80 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-03-01/swagger.yaml": "Error opening file", 81 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-06-01/swagger.yaml": "Error opening file", 82 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-08-01/swagger.yaml": "Error opening file", 83 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-11-01/swagger.yaml": "Error opening file", 84 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-09-01/swagger.yaml": "Error opening file", 85 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2017-10-01/swagger.yaml": "Error opening file", 86 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2018-10-01/swagger.yaml": "Error opening file", 87 | "node_modules/openapi-directory/APIs/azure.com/network-networkInterface/2018-01-01/swagger.yaml": "Error opening file", 88 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2018-08-01/swagger.yaml": "Error opening file", 89 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2018-12-01/swagger.yaml": "Error opening file", 90 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2018-11-01/swagger.yaml": "Error opening file", 91 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2019-02-01/swagger.yaml": "Error opening file", 92 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2019-04-01/swagger.yaml": "Error opening file", 93 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2019-07-01/swagger.yaml": "Error opening file", 94 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2019-08-01/swagger.yaml": "Error opening file", 95 | "node_modules/openapi-directory/APIs/azure.com/network-networkProfile/2019-06-01/swagger.yaml": "Error opening file", 96 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2016-09-01/swagger.yaml": "Error opening file", 97 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-03-01/swagger.yaml": "Error opening file", 98 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2015-06-15/swagger.yaml": "Error opening file", 99 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-06-01/swagger.yaml": "Error opening file", 100 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2016-12-01/swagger.yaml": "Error opening file", 101 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-08-01/swagger.yaml": "Error opening file", 102 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-10-01/swagger.yaml": "Error opening file", 103 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-09-01/swagger.yaml": "Error opening file", 104 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-01-01/swagger.yaml": "Error opening file", 105 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-02-01/swagger.yaml": "Error opening file", 106 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2017-11-01/swagger.yaml": "Error opening file", 107 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-07-01/swagger.yaml": "Error opening file", 108 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-04-01/swagger.yaml": "Error opening file", 109 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-06-01/swagger.yaml": "Error opening file", 110 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-08-01/swagger.yaml": "Error opening file", 111 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-11-01/swagger.yaml": "Error opening file", 112 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-10-01/swagger.yaml": "Error opening file", 113 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2018-12-01/swagger.yaml": "Error opening file", 114 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2019-04-01/swagger.yaml": "Error opening file", 115 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2019-02-01/swagger.yaml": "Error opening file", 116 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2019-06-01/swagger.yaml": "Error opening file", 117 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2019-08-01/swagger.yaml": "Error opening file", 118 | "node_modules/openapi-directory/APIs/azure.com/network-networkSecurityGroup/2019-07-01/swagger.yaml": "Error opening file", 119 | "node_modules/openapi-directory/APIs/azure.com/network-privateEndpoint/2019-04-01/swagger.yaml": "Error opening file", 120 | "node_modules/openapi-directory/APIs/azure.com/network-privateEndpoint/2019-07-01/swagger.yaml": "Error opening file", 121 | "node_modules/openapi-directory/APIs/azure.com/network-privateEndpoint/2019-08-01/swagger.yaml": "Error opening file", 122 | "node_modules/openapi-directory/APIs/azure.com/network-privateEndpoint/2019-06-01/swagger.yaml": "Error opening file", 123 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2016-09-01/swagger.yaml": "Error opening file", 124 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2015-06-15/swagger.yaml": "Error opening file", 125 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2016-12-01/swagger.yaml": "Error opening file", 126 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-08-01/swagger.yaml": "Error opening file", 127 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-06-01/swagger.yaml": "Error opening file", 128 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-03-01/swagger.yaml": "Error opening file", 129 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-09-01/swagger.yaml": "Error opening file", 130 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-10-01/swagger.yaml": "Error opening file", 131 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2017-11-01/swagger.yaml": "Error opening file", 132 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-01-01/swagger.yaml": "Error opening file", 133 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-04-01/swagger.yaml": "Error opening file", 134 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-06-01/swagger.yaml": "Error opening file", 135 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-02-01/swagger.yaml": "Error opening file", 136 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-07-01/swagger.yaml": "Error opening file", 137 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-11-01/swagger.yaml": "Error opening file", 138 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-08-01/swagger.yaml": "Error opening file", 139 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-10-01/swagger.yaml": "Error opening file", 140 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2019-02-01/swagger.yaml": "Error opening file", 141 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2019-06-01/swagger.yaml": "Error opening file", 142 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2018-12-01/swagger.yaml": "Error opening file", 143 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2019-07-01/swagger.yaml": "Error opening file", 144 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2019-04-01/swagger.yaml": "Error opening file", 145 | "node_modules/openapi-directory/APIs/azure.com/network-publicIpAddress/2019-08-01/swagger.yaml": "Error opening file", 146 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-03-01/swagger.yaml": "Error opening file", 147 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-06-01/swagger.yaml": "Error opening file", 148 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-08-01/swagger.yaml": "Error opening file", 149 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-10-01/swagger.yaml": "Error opening file", 150 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2016-12-01/swagger.yaml": "Error opening file", 151 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-09-01/swagger.yaml": "Error opening file", 152 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-02-01/swagger.yaml": "Error opening file", 153 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-01-01/swagger.yaml": "Error opening file", 154 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-04-01/swagger.yaml": "Error opening file", 155 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2017-11-01/swagger.yaml": "Error opening file", 156 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-10-01/swagger.yaml": "Error opening file", 157 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-08-01/swagger.yaml": "Error opening file", 158 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-11-01/swagger.yaml": "Error opening file", 159 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-06-01/swagger.yaml": "Error opening file", 160 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-07-01/swagger.yaml": "Error opening file", 161 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2018-12-01/swagger.yaml": "Error opening file", 162 | "node_modules/openapi-directory/APIs/azure.com/network-routeFilter/2019-02-01/swagger.yaml": "Error opening file", 163 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2016-09-01/swagger.yaml": "Error opening file", 164 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2015-06-15/swagger.yaml": "Error opening file", 165 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-03-01/swagger.yaml": "Error opening file", 166 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2016-12-01/swagger.yaml": "Error opening file", 167 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-06-01/swagger.yaml": "Error opening file", 168 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-11-01/swagger.yaml": "Error opening file", 169 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-08-01/swagger.yaml": "Error opening file", 170 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-01-01/swagger.yaml": "Error opening file", 171 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-09-01/swagger.yaml": "Error opening file", 172 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2017-10-01/swagger.yaml": "Error opening file", 173 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-06-01/swagger.yaml": "Error opening file", 174 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-02-01/swagger.yaml": "Error opening file", 175 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-04-01/swagger.yaml": "Error opening file", 176 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-08-01/swagger.yaml": "Error opening file", 177 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-10-01/swagger.yaml": "Error opening file", 178 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-07-01/swagger.yaml": "Error opening file", 179 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2019-02-01/swagger.yaml": "Error opening file", 180 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2019-04-01/swagger.yaml": "Error opening file", 181 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-11-01/swagger.yaml": "Error opening file", 182 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2019-08-01/swagger.yaml": "Error opening file", 183 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2018-12-01/swagger.yaml": "Error opening file", 184 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2019-06-01/swagger.yaml": "Error opening file", 185 | "node_modules/openapi-directory/APIs/azure.com/network-routeTable/2019-07-01/swagger.yaml": "Error opening file", 186 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2018-08-01/swagger.yaml": "Error opening file", 187 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2018-12-01/swagger.yaml": "Error opening file", 188 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2018-10-01/swagger.yaml": "Error opening file", 189 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2018-11-01/swagger.yaml": "Error opening file", 190 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2019-04-01/swagger.yaml": "Error opening file", 191 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2019-02-01/swagger.yaml": "Error opening file", 192 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2019-07-01/swagger.yaml": "Error opening file", 193 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2019-06-01/swagger.yaml": "Error opening file", 194 | "node_modules/openapi-directory/APIs/azure.com/network-serviceEndpointPolicy/2019-08-01/swagger.yaml": "Error opening file", 195 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2015-06-15/swagger.yaml": "Error opening file", 196 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2016-09-01/swagger.yaml": "Error opening file", 197 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2016-12-01/swagger.yaml": "Error opening file", 198 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-08-01/swagger.yaml": "Error opening file", 199 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-06-01/swagger.yaml": "Error opening file", 200 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-03-01/swagger.yaml": "Error opening file", 201 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-10-01/swagger.yaml": "Error opening file", 202 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-09-01/swagger.yaml": "Error opening file", 203 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-01-01/swagger.yaml": "Error opening file", 204 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2017-11-01/swagger.yaml": "Error opening file", 205 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-04-01/swagger.yaml": "Error opening file", 206 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-02-01/swagger.yaml": "Error opening file", 207 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-08-01/swagger.yaml": "Error opening file", 208 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-07-01/swagger.yaml": "Error opening file", 209 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-12-01/swagger.yaml": "Error opening file", 210 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-06-01/swagger.yaml": "Error opening file", 211 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-10-01/swagger.yaml": "Error opening file", 212 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2019-02-01/swagger.yaml": "Error opening file", 213 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2018-11-01/swagger.yaml": "Error opening file", 214 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2019-04-01/swagger.yaml": "Error opening file", 215 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2019-06-01/swagger.yaml": "Error opening file", 216 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2019-07-01/swagger.yaml": "Error opening file", 217 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetwork/2019-08-01/swagger.yaml": "Error opening file", 218 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2018-10-01/swagger.yaml": "Error opening file", 219 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2018-11-01/swagger.yaml": "Error opening file", 220 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2018-12-01/swagger.yaml": "Error opening file", 221 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2018-08-01/swagger.yaml": "Error opening file", 222 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2019-02-01/swagger.yaml": "Error opening file", 223 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2019-04-01/swagger.yaml": "Error opening file", 224 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2019-07-01/swagger.yaml": "Error opening file", 225 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2019-06-01/swagger.yaml": "Error opening file", 226 | "node_modules/openapi-directory/APIs/azure.com/network-virtualNetworkTap/2019-08-01/swagger.yaml": "Error opening file" 227 | }; -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | // Include a global type declaration that covers all 2 | // generated api/* files. 3 | /// 4 | 5 | import { Trie } from "./trie"; 6 | 7 | export * from 'openapi3-ts/dist/model'; 8 | 9 | const trieData = require('../../api/_index.js'); 10 | const apiIndex = new Trie(trieData); 11 | 12 | export const findApi = (url: string) => 13 | apiIndex.getMatchingPrefix(url); -------------------------------------------------------------------------------- /src/runtime/trie.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | type TrieLeafValue = string | string[]; 3 | 4 | export type TrieValue = 5 | | TrieData 6 | | TrieLeafValue 7 | | undefined; 8 | 9 | export interface TrieData extends Map< 10 | string | RegExp, 11 | TrieValue 12 | > {} 13 | 14 | export function isLeafValue(value: any): value is TrieLeafValue { 15 | return typeof value === 'string' || Array.isArray(value); 16 | } 17 | 18 | export class Trie { 19 | 20 | constructor(private root: TrieData) { } 21 | 22 | private _getLongestMatchingPrefix(key: string) { 23 | let remainingKey = key.toLowerCase(); 24 | let node: TrieData | undefined = this.root; 25 | 26 | while (node) { 27 | // Calculate the max key length. String keys should be all the same length, 28 | // except one optional '' key if there's an on-path leaf node here, so we 29 | // just use the first non-zero non-regex length. 30 | let maxKeyLength; 31 | for (let k of node.keys()) { 32 | if (k && !(k instanceof RegExp)) { 33 | maxKeyLength = k.length; 34 | break; 35 | } 36 | } 37 | 38 | // Given a common key length L, we try to see if the first L characters 39 | // of our remaining key are an existing key here 40 | const keyToMatch = remainingKey.slice(0, maxKeyLength); 41 | // We check for the key with a hash lookup (as we know it would have to be an exact match), 42 | // _not_ by looping through keys with startsWith - this is key (ha!) to perf here. 43 | let nextNode: TrieValue = node.get(keyToMatch); 44 | 45 | if (nextNode) { 46 | // If that bit of the key matched, we can remove it from the key to match, 47 | // and move on to match the next bit. 48 | remainingKey = remainingKey.slice(maxKeyLength); 49 | } else { 50 | // If it didn't match, we need to check regexes, if present, and check 51 | // for an on-path leaf node here ('' key) 52 | const matchedRegex: { 53 | matchedNode: TrieValue, 54 | matchLength: number 55 | } | undefined = Array.from(node.keys()).map(k => { 56 | const match = k instanceof RegExp && k.exec(remainingKey) 57 | if (!!match && match.index === 0) { 58 | return { 59 | matchedNode: node!.get(k), 60 | matchLength: match[0].length 61 | }; 62 | }; 63 | }).filter(r => !!r)[0]; 64 | 65 | if (matchedRegex) { 66 | // If we match a regex, we no longer need to match the part of the 67 | // key that the regex has consumed 68 | remainingKey = remainingKey.slice(matchedRegex.matchLength); 69 | nextNode = matchedRegex.matchedNode; 70 | } else { 71 | nextNode = node.get(''); 72 | } 73 | } 74 | 75 | if (isLeafValue(nextNode)) { 76 | // We've reached the end of a key - if we're out of 77 | // input, that's good, if not it's just a prefix. 78 | return { 79 | remainingKey, 80 | matchedKey: key.slice(0, -1 * remainingKey.length), 81 | value: nextNode 82 | }; 83 | } else { 84 | node = nextNode; 85 | } 86 | } 87 | 88 | // We failed to match - this means at some point we either had no key left, and 89 | // no on-path key present, or we had a key left that disagreed with every option. 90 | return undefined; 91 | } 92 | 93 | /* 94 | * Given a key, finds an exact match and returns the value(s). 95 | * Returns undefined if no match can be found. 96 | */ 97 | get(key: string): string | string[] | undefined { 98 | const searchResult = this._getLongestMatchingPrefix(key); 99 | 100 | if (!searchResult) return undefined; 101 | 102 | const { 103 | remainingKey, 104 | value 105 | } = searchResult; 106 | 107 | return remainingKey.length === 0 ? 108 | value : undefined; 109 | } 110 | 111 | /* 112 | * Given a key, finds the longest key that is a prefix of this 113 | * key, and returns its value(s). I.e. for input 'abcdef', 'abc' 114 | * would match in preference to 'ab', and 'abcdefg' would never 115 | * be matched. 116 | * 117 | * Returns undefined if no match can be found. 118 | */ 119 | getMatchingPrefix(key: string): string | string[] | undefined { 120 | const searchResult = this._getLongestMatchingPrefix(key); 121 | 122 | if (!searchResult) return undefined; 123 | 124 | const { value } = searchResult; 125 | 126 | return value; 127 | } 128 | } -------------------------------------------------------------------------------- /test/buildtime/build-index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { objToInput, objToTrie, t } from '../test-helpers'; 3 | 4 | import { buildTrie, optimizeTrie, buildNaiveTrie } from "../../src/buildtime/build-index"; 5 | 6 | 7 | describe('Index generation', () => { 8 | it('can generate simple compressed trie data', () => { 9 | const trie = buildTrie(objToInput({ 'ab': 'hi' })); 10 | 11 | expect(trie).to.deep.equal(objToTrie({ 12 | 'ab': 'hi' 13 | })); 14 | }); 15 | 16 | it('can generate simple branching trie data', () => { 17 | const trie = buildTrie(objToInput({ 18 | 'abc': 'hi', 19 | 'acb': 'there' 20 | })); 21 | 22 | expect(trie).to.deep.equal(objToTrie({ 23 | 'ab': { 'c': 'hi' }, 24 | 'ac': { 'b': 'there' } 25 | })); 26 | }); 27 | 28 | it('can generate a trie for a series of substrings', () => { 29 | const trie = buildTrie(objToInput({ 30 | 'a': 'value1', 31 | 'ab': 'value2', 32 | 'abcd': 'value3', 33 | 'abcdef': 'value4', 34 | })); 35 | 36 | expect(trie).to.deep.equal(objToTrie({ 37 | a: { 38 | '': 'value1', 39 | b: { 40 | '': 'value2', 41 | cd: { 42 | '': 'value3', 43 | ef: 'value4' 44 | } 45 | } 46 | } 47 | })); 48 | }); 49 | 50 | it('can generate trie data including arrays', () => { 51 | const trie = buildTrie(objToInput({ 52 | 'ab': ['hi', 'there'], 53 | 'c': 'bye' 54 | })); 55 | 56 | expect(optimizeTrie(trie)).to.deep.equal(objToTrie({ 57 | 'a': { 'b': ['hi', 'there'] }, 58 | 'c': 'bye' 59 | })); 60 | }); 61 | 62 | it('can generate trie data including regular expressions', () => { 63 | const trie = buildTrie(new Map< 64 | Array, 65 | string | string[] 66 | >([ 67 | [['ab', /.*/, 'd'], ['result']], 68 | [['c'], 'bye'] 69 | ])); 70 | 71 | expect(trie).to.deep.equal(t([ 72 | ['a', t([ 73 | [ 'b', t([ 74 | [/.*/, t([ 75 | ['d', ['result']] 76 | ])] 77 | ])] 78 | ])], 79 | ['c', 'bye'] 80 | ])); 81 | }); 82 | }); 83 | 84 | describe('Naive trie generation', () => { 85 | it('can generate simple trie data', () => { 86 | const trie = buildNaiveTrie(objToInput({ 'ab': 'hi' })); 87 | 88 | expect(trie).to.deep.equal(objToTrie({ 89 | 'a': { 'b': 'hi' } 90 | })); 91 | }); 92 | 93 | it('can generate trie data with overlapping strings', () => { 94 | const trie = buildNaiveTrie(objToInput({ 95 | 'ab': 'hi', 96 | 'abcd': 'bye', 97 | 'a': '123' 98 | })); 99 | 100 | expect(trie).to.deep.equal(objToTrie({ 101 | 'a': { 'b': { '': 'hi', 'c': { 'd': 'bye' } }, '': '123' } 102 | })); 103 | }); 104 | }); 105 | 106 | describe('Trie optimization', () => { 107 | it('can simplify single child nodes', () => { 108 | const trie = objToTrie({ 109 | 'a': { 'b': 'value' } 110 | }); 111 | 112 | expect(optimizeTrie(trie)).to.deep.equal(objToTrie({ 113 | 'ab': 'value' 114 | })); 115 | }); 116 | 117 | it('can simplify single child nodes with branches en route', () => { 118 | const trie = objToTrie({ 119 | 'a': { 'b': { '': 'value1', 'c': 'value2' }, 'c': 'value3' } 120 | }); 121 | 122 | expect(optimizeTrie(trie)).to.deep.equal(objToTrie({ 123 | 'ab': { '': 'value1', 'c': 'value2' }, 124 | 'ac': 'value3' 125 | })); 126 | }); 127 | 128 | it('correctly simplifies complex tries without compressing leaf keys', () => { 129 | const trie = objToTrie({ 130 | a: { 131 | '': 'value1', 132 | b: { 133 | '': 'value3', 134 | c: { 135 | d: 'value4' 136 | } 137 | } 138 | } 139 | }); 140 | 141 | expect(optimizeTrie(trie)).to.deep.equal(objToTrie({ 142 | a: { 143 | '': 'value1', 144 | b: { 145 | '': 'value3', 146 | cd: 'value4' 147 | } 148 | } 149 | })); 150 | }); 151 | }); -------------------------------------------------------------------------------- /test/buildtime/generate-api.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { calculateCommonPrefixes } from '../../src/buildtime/generate-apis'; 4 | import { objToInput, t } from '../test-helpers'; 5 | 6 | describe('Common prefixes', () => { 7 | it('can find a common base for separate subdirectories', () => { 8 | expect( 9 | calculateCommonPrefixes('example.com', { 10 | 'spec1': ['/a'], 11 | 'spec2': ['/b'] 12 | }) 13 | ).to.deep.equal(objToInput({ 14 | 'example.com/a': 'spec1', 15 | 'example.com/b': 'spec2' 16 | })); 17 | }); 18 | 19 | it('can find a simple common base for shared paths', () => { 20 | expect( 21 | calculateCommonPrefixes('example.com', { 22 | 'spec1': ['/a1', '/a2'], 23 | 'spec2': ['/b'] 24 | }) 25 | ).to.deep.equal(objToInput({ 26 | 'example.com/a': 'spec1', 27 | 'example.com/b': 'spec2' 28 | })); 29 | }); 30 | 31 | it('can find multiple common bases for shared paths', () => { 32 | expect( 33 | calculateCommonPrefixes('example.com', { 34 | 'spec1': ['/a1', '/a2', '/a/b', '/b'], 35 | 'spec2': ['/c'] 36 | }) 37 | ).to.deep.equal(objToInput({ 38 | 'example.com/a': 'spec1', 39 | 'example.com/b': 'spec1', 40 | 'example.com/c': 'spec2' 41 | })); 42 | }); 43 | 44 | it('can build common bases with overlapping prefixes', () => { 45 | expect( 46 | calculateCommonPrefixes('example.com', { 47 | 'spec1': ['/a/b/1', '/a/b/2'], 48 | 'spec2': ['/a', '/c'] 49 | }) 50 | ).to.deep.equal(objToInput({ 51 | 'example.com/a': 'spec2', 52 | 'example.com/a/b/': 'spec1', 53 | 'example.com/c': 'spec2' 54 | })); 55 | }); 56 | 57 | it('can separate suffixes for overlapping suffixes', () => { 58 | expect( 59 | calculateCommonPrefixes('example.com', { 60 | 'spec1': ['/a/b/1', '/a/b/2'], 61 | 'spec2': ['/a/b/3', '/a/b/4'] 62 | }) 63 | ).to.deep.equal(objToInput({ 64 | 'example.com/a/b/1': 'spec1', 65 | 'example.com/a/b/2': 'spec1', 66 | 'example.com/a/b/3': 'spec2', 67 | 'example.com/a/b/4': 'spec2', 68 | })); 69 | }); 70 | 71 | it('can simplify common bases with conflicting URLs', () => { 72 | expect( 73 | calculateCommonPrefixes('example.com', { 74 | 'spec1': ['/a/b/1', '/a/b/2', '/a/b/3', '/c/1'], 75 | 'spec2': ['/a/b/1', '/a/b/2', '/c/1'], 76 | 'spec3': ['/b/1'] 77 | }) 78 | ).to.deep.equal(objToInput({ 79 | 'example.com/a/b/': ['spec1', 'spec2'], 80 | 'example.com/a/b/3': 'spec1', 81 | 'example.com/c/1': ['spec1', 'spec2'], 82 | 'example.com/b/1': 'spec3', 83 | })); 84 | }); 85 | 86 | it('deduplicates results with conflicting wildcard params', () => { 87 | expect( 88 | calculateCommonPrefixes('example.com', { 89 | 'spec1': ['/a/1', '/a/{param}', '/a/{param}/b'], 90 | 'spec2': ['/a/1', '/a/2', '/a/{param}', '/c'] 91 | }) 92 | ).to.deep.equal(t([ 93 | [['example.com/a/'], ['spec1', 'spec2']], 94 | [['example.com/a/', /^[^/]+/, '/b'], 'spec1'], 95 | [['example.com/a/2'], 'spec2'], 96 | [['example.com/c'], 'spec2'] 97 | ])); 98 | }); 99 | }); -------------------------------------------------------------------------------- /test/integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { buildAll } from '../src/buildtime/build-all'; 4 | 5 | describe('Integration test:', function () { 6 | this.timeout(1000 * 60); 7 | 8 | it('building all APIs should create a usable index', async () => { 9 | await buildAll([ 10 | // Build only a limited set, to keep the tests quick 11 | 'node_modules/openapi-directory/APIs/amazonaws.com/ap*/**/{swagger,openapi}.yaml', 12 | 'node_modules/openapi-directory/APIs/azure.com/ap*/**/{swagger,openapi}.yaml', 13 | 'node_modules/openapi-directory/APIs/tomtom.com/**/{swagger,openapi}.yaml' 14 | ], { 15 | ignoreKnownErrors: true, // Should be zero errors here 16 | expectKnownErrors: false // Don't expect all errors, since we're not building all specs 17 | }); 18 | 19 | const { findApi } = await import('../src/runtime/index'); 20 | 21 | // Direct string lookup, but requires path-based deduplication to avoid apigateway v2: 22 | const awsSpecId = findApi( 23 | 'apigateway.eu-north-1.amazonaws.com/restapis/1234/deployments/1234' 24 | ); 25 | expect(awsSpecId).to.equal('amazonaws.com/apigateway'); 26 | const awsSpec = await import(`../api/${awsSpecId}.json`); 27 | expect(awsSpec.info.title).to.equal('Amazon API Gateway'); 28 | 29 | // Lookup that requires path-based deduplication using regexes for params, case-insensitive: 30 | const azureSpecId = findApi( 31 | 'management.azure.com/subscriptions/123456/providers/Microsoft.Insights/listMigrationDate' 32 | ); 33 | expect(azureSpecId).to.equal('azure.com/applicationinsights-eaSubscriptionMigration_API'); 34 | const azureSpec = await import(`../api/${azureSpecId}.json`); 35 | expect(azureSpec.info.title).to.equal('ApplicationInsightsManagementClient'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/runtime/trie.spec.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { expect } from "chai"; 3 | 4 | import { buildTrie } from "../../src/buildtime/build-index"; 5 | import { Trie } from "../../src/runtime/trie"; 6 | import { objToInput, t } from "../test-helpers"; 7 | 8 | describe('Trie searching', () => { 9 | describe('with .get()', () => { 10 | it('can search a simple trie for a specific value', () => { 11 | const trie = new Trie(buildTrie(objToInput({ 12 | 'a': 'value' 13 | }))); 14 | 15 | expect(trie.get('a')).to.equal('value'); 16 | }); 17 | 18 | it('returns undefined for missing values', () => { 19 | const trie = new Trie(buildTrie(objToInput({ 20 | 'a': 'value' 21 | }))); 22 | 23 | expect(trie.get('ab')).to.equal(undefined); 24 | }); 25 | 26 | it('can search a simple trie for a value thats part way down a path', () => { 27 | const trie = new Trie(buildTrie(objToInput({ 28 | 'a': 'value', 29 | 'ab': 'value', 30 | 'abc': 'value2' 31 | }))); 32 | 33 | expect(trie.get('ab')).to.equal('value'); 34 | }); 35 | }); 36 | 37 | describe('with .getLongestMatchingPrefix()', () => { 38 | it('can search a simple trie for an exact value', () => { 39 | const trie = new Trie(buildTrie(objToInput({ 40 | 'a': 'value' 41 | }))); 42 | 43 | expect(trie.getMatchingPrefix('a')).to.equal('value'); 44 | }); 45 | 46 | it('can search a simple trie for an array value', () => { 47 | const trie = new Trie(buildTrie(objToInput({ 48 | 'a': ['value1', 'value2'] 49 | }))); 50 | 51 | expect(trie.getMatchingPrefix('a')).to.deep.equal( 52 | ['value1', 'value2'] 53 | ); 54 | }); 55 | 56 | it('can search a trie for an exact value part way down a path', () => { 57 | const trie = new Trie(buildTrie(objToInput({ 58 | 'a': 'value', 59 | 'ab': 'value', 60 | 'abc': 'value2' 61 | }))); 62 | 63 | expect(trie.getMatchingPrefix('ab')).to.equal('value'); 64 | }); 65 | 66 | it('can search a trie for a prefix', () => { 67 | const trie = new Trie(buildTrie(objToInput({ 68 | 'a': 'value', 69 | 'ab': 'value', 70 | 'abcd': 'value2', 71 | 'abcdef': 'value3' 72 | }))); 73 | 74 | expect(trie.getMatchingPrefix('abcde')).to.equal('value2'); 75 | }); 76 | 77 | it('can search a trie through a regex', () => { 78 | const trie = new Trie(buildTrie(t([ 79 | [['a/'], 'value'], 80 | [['ab/', /^[^/]+/, '/d'], 'value2'], 81 | [['ab/', /^[^/]+/], 'value3'], 82 | [['ab/c/d'], 'value4'], 83 | ]))); 84 | 85 | expect(trie.getMatchingPrefix('ab/c/d')).to.equal('value4'); 86 | expect(trie.getMatchingPrefix('ab/X/e')).to.equal('value3'); 87 | expect(trie.getMatchingPrefix('ab/X/d')).to.equal('value2'); 88 | }); 89 | }); 90 | 91 | describe('given a large trie', () => { 92 | 93 | const keys = _.range(10000).map(k => k.toString()); 94 | let rawIndex: any; 95 | let trie: Trie; 96 | 97 | before(() => { 98 | rawIndex = _.zipObject( 99 | keys, 100 | keys.map(k => `value${k}`) 101 | ); 102 | 103 | const trieData = buildTrie(objToInput(rawIndex)); 104 | 105 | trie = new Trie(trieData); 106 | }); 107 | 108 | it('can search by key quickly', function () { 109 | this.timeout(500); // 10000 in < 500ms = < 50 microseconds/search 110 | 111 | keys.forEach((k) => { 112 | expect(trie.get(k)).to.equal(`value${k}`); 113 | }); 114 | 115 | // This is actually ~8x slower than a bare hashtable, for direct 116 | // lookups. As long as it's within an order of magnitude or two though, 117 | // that's ok. The real magic comes in prefix searching. 118 | }); 119 | 120 | it.skip('can search by key quickly (hashtable)', function () { 121 | this.timeout(10000); 122 | 123 | keys.forEach((k) => { 124 | expect(rawIndex[k]).to.equal(`value${k}`); 125 | }); 126 | }); 127 | 128 | it('can search by prefix quickly', function () { 129 | this.timeout(1000); // 10000 in < 1000ms = < 100 microseconds/search 130 | 131 | keys.forEach((k) => { 132 | const withSuffix = k + '-suffix'; 133 | expect(trie.getMatchingPrefix(withSuffix)).to.equal(`value${k}`); 134 | }); 135 | 136 | // This is the key perf step - doing an equivalent loop on a hashtable 137 | // (for all keys, find keys that start with the input, get the longest) 138 | // is approx 25x slower than this, for 10000 elements. 139 | }); 140 | 141 | it.skip('can search a large trie quickly (hashtable for comparison)', function () { 142 | this.timeout(10000); 143 | 144 | keys.forEach((k) => { 145 | expect(_.find(rawIndex, (_v, key) => key.startsWith(k))!).to.equal(`value${k}`); 146 | }); 147 | }); 148 | }); 149 | 150 | }); -------------------------------------------------------------------------------- /test/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { TrieData } from '../src/runtime/trie'; 3 | 4 | // Some messy but convenient helpers, to make defining test data a bit 5 | // friendlier than trying to do so by building maps from array entries 6 | // data by hand. 7 | interface ObjInput { 8 | [key: string]: string | string[]; 9 | } 10 | export function objToInput(object: ObjInput) { 11 | return new Map( 12 | ( 13 | Object.entries(object) 14 | .map(([key, value]) => [[key], value]) 15 | ) as Array<[Array, string | string[]]> 16 | ); 17 | } 18 | 19 | interface ObjTrie { 20 | [key: string]: string | string[] | ObjTrie; 21 | } 22 | export function objToTrie(object: string): string; 23 | export function objToTrie(object: string[]): string[]; 24 | export function objToTrie(object: ObjTrie): TrieData; 25 | export function objToTrie(object: ObjTrie | string | string[]): TrieData | string | string[]; 26 | export function objToTrie(object: ObjTrie | string | string[]): TrieData | string | string[] { 27 | if (_.isArray(object) || _.isString(object)) return object; 28 | 29 | return new Map( 30 | ( 31 | Object.entries(object) 32 | .map(([key, value]) => [key, objToTrie(value)]) 33 | ) as Array<[string, string | string[]]> 34 | ); 35 | } 36 | 37 | type TrieInputEntry = [Array, string | string[]]; 38 | type TrieEntry = [string | RegExp, string | string[] | TrieData]; 39 | export function t(entries: Array): Map< 40 | Array, 41 | string | string[] 42 | >; 43 | export function t(entries: Array): TrieData; 44 | export function t(entries: Array) { 45 | return new Map(entries); 46 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "../custom-typings/*.d.ts", 5 | "../src/**/*.ts", 6 | "./**/*.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "downlevelIteration": true, 5 | "outDir": "dist", 6 | "declaration": true, 7 | "module": "commonjs", 8 | "sourceMap": true, 9 | "strict": true, 10 | }, 11 | "compileOnSave": true, 12 | "buildOnSave": true, 13 | "include": [ 14 | "custom-typings/*.d.ts", 15 | "openapi-directory.d.ts", 16 | "src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = (wallaby) => { 2 | 3 | return { 4 | files: [ 5 | 'package.json', 6 | 'src/**/*.ts', 7 | 'test/**/*.ts', 8 | '!test/**/*.spec.ts' 9 | ], 10 | tests: [ 11 | '!test/integration.spec.ts', 12 | 'test/**/*.spec.ts' 13 | ], 14 | 15 | testFramework: 'mocha', 16 | env: { 17 | type: 'node' 18 | }, 19 | debug: true 20 | }; 21 | }; --------------------------------------------------------------------------------