├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── Changelog.md ├── LICENSE ├── README.md ├── docs └── index.md ├── eslint.config.mjs ├── oas30.d.ts ├── oas31.d.ts ├── package-lock.json ├── package.json ├── src ├── dsl │ ├── openapi-builder30.spec.ts │ ├── openapi-builder30.ts │ ├── openapi-builder31.spec.ts │ └── openapi-builder31.ts ├── index.spec.ts ├── index.ts ├── model │ ├── oas-common.ts │ ├── openapi30.spec.ts │ ├── openapi30.ts │ ├── openapi31.spec.ts │ ├── openapi31.ts │ ├── server.spec.ts │ ├── server.ts │ ├── specification-extension.spec.ts │ └── specification-extension.ts ├── oas30.ts └── oas31.ts ├── test ├── .gitignore ├── fixture │ ├── tsconfig.node.json │ └── tsconfig.node16.json ├── index.cjs ├── index.cts ├── index.mjs ├── index.mts └── package.json ├── tsconfig.json ├── vite.config.d.mts └── vite.config.mts /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Actions 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Install dependencies 14 | run: npm install 15 | - name: Versions used 16 | run: npm run version 17 | - name: Lint 18 | run: npm run lint 19 | - name: Build 20 | run: npm run build 21 | - name: Test + coverage 22 | run: npm run cover:ci 23 | - name: Integration Test 24 | run: | 25 | cd test 26 | npm i 27 | npm run test:cjs 28 | npm run test:mjs 29 | npm run test:ts 30 | npm run test:ts-esm 31 | npm run test:ts-node16 32 | npm run test:ts-esm-node16 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | .nyc_output/ 5 | *.log 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output/ 4 | *.log 5 | .vscode/ 6 | .travis.yml 7 | 8 | # Include dist/ in npm package 9 | !dist/ 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 4, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | after_success: 5 | - node_modules/.bin/coveralls < coverage/lcov.info 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", 14 | "tdd", 15 | "--compilers", "ts:ts-node/register", 16 | "--timeout", 17 | "999999", 18 | "--colors", 19 | "${workspaceRoot}/src/**/*.spec.ts" 20 | ], 21 | "internalConsoleOptions": "openOnSessionStart" 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Iniciar programa", 27 | "program": "${workspaceRoot}\\index.js", 28 | "outFiles": [] 29 | }, 30 | { 31 | "type": "node", 32 | "request": "attach", 33 | "name": "Asociar al proceso", 34 | "address": "localhost", 35 | "port": 5858, 36 | "outFiles": [] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog: openapi3-ts 2 | 3 | ## Version 4.4.0 4 | 5 | 2024.08.27 6 | 7 | - Updated libs 8 | - Updated lo latest eslint (migrated config file) 9 | - Updated vite config to mts 10 | - Added basic documentation. Fix [#75](https://github.com/metadevpro/openapi3-ts/pull/75). PR[#139](https://github.com/metadevpro/openapi3-ts/pull/139) 11 | - Feat: `getSpecAsYaml` accepts the same options as `yaml.stringify`. Fix [#143](https://github.com/metadevpro/openapi3-ts/issues/143). PR[#144](https://github.com/metadevpro/openapi3-ts/pull/144) by @urugator 12 | 13 | ## Version 4.3.3 14 | 15 | 2024.06.10 16 | 17 | - Add mising types definitions. PR [#138](https://github.com/metadevpro/openapi3-ts/pull/138) by @andreujuanc 18 | 19 | ## Version 4.3.2 20 | 21 | 2024.05.23 22 | 23 | - Fix [#134](https://github.com/metadevpro/openapi3-ts/pull/134) exceisve files in distributed package. PR [136](https://github.com/metadevpro/openapi3-ts/pull/136) by @RobinTail 24 | 25 | ## Version 4.3.1 26 | 27 | 2024.04.01 28 | 29 | - Bug fix. `const` must be of type any. [#133](https://github.com/metadevpro/openapi3-ts/pull/133) by @RobinTail 30 | 31 | ## Version 4.3.0 32 | 33 | 2024.04.01 34 | 35 | - Added support for `const` keyword [#132](https://github.com/metadevpro/openapi3-ts/pull/132) by @remidewitte 36 | - Update dependences. 37 | 38 | ## Version 4.2.2 39 | 40 | 2024.02.22 41 | 42 | - Fixed [131](https://github.com/metadevpro/openapi3-ts/pull/131) `responses` to be optional by @remidewitte 43 | 44 | ## Version 4.2.1 45 | 46 | 2023.12.21 47 | 48 | - Merged [#128](https://github.com/metadevpro/openapi3-ts/pull/128) Revert [#126](https://github.com/metadevpro/openapi3-ts/pull/128) `examples` typings by @RobinTail 49 | 50 | 2023.12.20 51 | 52 | - Added [#126](https://github.com/metadevpro/openapi3-ts/pull/126) Adding contentEncoding and contentMediaType to 3.1 by @RobinTail 53 | - Updated dependencies 54 | 55 | ## Version 4.1.2 56 | 57 | 2023.04.25 58 | 59 | - Fix [#113](https://github.com/metadevpro/openapi3-ts/pull/113) Fix nullable on 3.0 types by @samchungy 60 | - Fix [#116](https://github.com/metadevpro/openapi3-ts/pull/116) Export with .mjs extension by @koooge 61 | - Fix [#117](https://github.com/metadevpro/openapi3-ts/pull/117) Support subpath for tsc with moduleResolutin node by @koooge 62 | - Fix [#118](https://github.com/metadevpro/openapi3-ts/pull/118) Bump yaml from 2.2.1 to 2.2.2 63 | - Update dependencies to latest. 64 | 65 | ## Version 4.1.1 66 | 67 | 2023.04.15 68 | 69 | - Fix [#112](https://github.com/metadevpro/openapi3-ts/pull/112) Remove `nullable` & deprecate `example` in OAS 3.1 by @samchungy. 70 | 71 | ## Version 4.1.0 72 | 73 | 2023.04.14 74 | 75 | - [#111](https://github.com/metadevpro/openapi3-ts/pull/111) Improved exports by @koooge 76 | 77 | ## Version 4.0.4 78 | 79 | 2023.04.10 80 | 81 | - [#108](https://github.com/metadevpro/openapi3-ts/pull/108) Bug fix: Remove any type from `paths` by @samchungy. 82 | - [#109](https://github.com/metadevpro/openapi3-ts/pull/109) Bug fix: Fixup types `example` & `prefixItems` by @samchungy. 83 | 84 | ## Version 4.0.3 85 | 86 | 2023.03.30 87 | 88 | - Bug fix. Stricter TS compilation. Fix issue [#105](https://github.com/metadevpro/openapi3-ts/issues/105) 89 | 90 | ## Version 4.0.2 91 | 92 | 2023.03.30 93 | 94 | - PR [#104](https://github.com/metadevpro/openapi3-ts/pull/104) Fix export for cjs by @RobinTail 95 | 96 | ## Version 4.0.1 97 | 98 | 2023.03.29 99 | 100 | - Added instructions to README for consuing the library for v. 3.1.0 vs 3.0.0 101 | - PR [#100](https://github.com/metadevpro/openapi3-ts/pull/99) Fix typo by @RobinTail 102 | - PR [#99](https://github.com/metadevpro/openapi3-ts/pull/99) Fix typo by @RobinTail 103 | - PR [#97](https://github.com/metadevpro/openapi3-ts/pull/97) Export type `SchemaObjectType` by @RobinTail 104 | - PR [#96](https://github.com/metadevpro/openapi3-ts/pull/96) Support prefixItems by @samchungy 105 | 106 | ## Version 4.0.0 107 | 108 | 2023.03.27 109 | 110 | - Breaking change. Adds explicit support for OAS 3.0 and OAS 3.1 as separate implementations. 111 | 112 | ## Version 3.2.0 113 | 114 | - PR [#94](https://github.com/metadevpro/openapi3-ts/pull/94) Export type `SchemaObjectType`. 115 | - PR [#95](https://github.com/metadevpro/openapi3-ts/pull/95) Export type `SchemaObjectFormat`. Both contributed by @beautyfree 116 | 117 | ## Version 3.1.2 118 | 119 | 2022.11.19 120 | 121 | - PR [#91](https://github.com/metadevpro/openapi3-ts/pull/91) Fix `addPath` to include merge semantics. Contributed by @MaurerKrisztian 122 | 123 | ## Version 3.1.1 124 | 125 | 2022.10.10 126 | 127 | - PR [#89](https://github.com/metadevpro/openapi3-ts/pull/89) Make Webhooks optional (not supportet in OPenAPI 3.0). Contributed by @samchungy 128 | 129 | ## Version 3.1.0 130 | 131 | 2022.10.06 132 | 133 | - PR [#88](https://github.com/metadevpro/openapi3-ts/pull/88) Added support to Webhooks. Contributed by @samchungy 134 | 135 | ## Version 3.0.3 136 | 137 | 2022.10.06 138 | 139 | - PR [#87](https://github.com/metadevpro/openapi3-ts/pull/87) Enable type array for schema object on OpenAPI 3.1. contributed by @samchungy 140 | 141 | ## Version 3.0.2 142 | 143 | 2022.08.29 144 | 145 | - PR [#85](https://github.com/metadevpro/openapi3-ts/pull/85) Enable support for version 3.0.1 and 3.1.0 (Lax typing for `exclusiveMinimum` and `exclusiveMaximum`). contributed by @RobinTail 146 | 147 | ## Version 3.0.1 148 | 149 | 2022.08.16 150 | 151 | - PR [#82](https://github.com/metadevpro/openapi3-ts/pull/82) Emit helpers for the CJS build. contributed by @RobinTail 152 | 153 | ## Version 3.0.0 154 | 155 | 2022.08.07 156 | 157 | - PR [#80](https://github.com/metadevpro/openapi3-ts/pull/80) contributed by @jonluca 158 | - Updated libs 159 | - Breaking change: Changed build system to output esm as well as cjs (folders `dist/mjs` and `dist/cjs` now respectively, instead of `dist` for cjs previously). 160 | - Changed test system to use vite 161 | - Added stricter extension prefix typing 162 | 163 | ## Version 2.0.2 164 | 165 | 2022.02.17 166 | 167 | - Updated libs 168 | 169 | ## Version 2.0.1 170 | 171 | 2020.12.31 172 | 173 | - Updated testing libs 174 | - Added ESList + Prettier, removed TSList support 175 | - Refactor to be consistent with ESLint + Prettier code-style rules 176 | - No functional changes in this version: just end of year house-keeping. 177 | 178 | ## Version 2.0.0 179 | 180 | 2020.09.18 181 | 182 | - Added Yaml Support and the first dependence to `yaml` library. Thanks to @DMavani 183 | - Better typing for `type` and `format` properties. Thanks to @xeptore 184 | - Keeping extensibility on `format`. 185 | 186 | ## Version 1.4.0 187 | 188 | 2020.06.06 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2022 Metadev https://metadev.pro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenApi3-TS 2 | 3 | TypeScript library to help building OpenAPI 3.x compliant API contracts. 4 | 5 | [![Coverage Status](https://coveralls.io/repos/github/metadevpro/openapi3-ts/badge.svg?branch=master)](https://coveralls.io/github/metadevpro/openapi3-ts?branch=master) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/metadevpro/openapi3-ts/badge.svg?targetFile=package.json)](https://snyk.io/test/github/metadevpro/openapi3-ts?targetFile=package.json) 7 | [![npm version](https://badge.fury.io/js/openapi3-ts.svg)](http://badge.fury.io/js/openapi3-ts) 8 | 9 | [![NPM](https://nodei.co/npm/openapi3-ts.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/openapi3-ts/) 10 | 11 | ## Version 4 12 | 13 | *Breaking change notice:* 14 | 15 | Version 4.0 Adds explicit support for OAS 3.0 and OAS 3.1 as separate implementations. 16 | 17 | ### To use version 3.1 import 18 | 19 | ```js 20 | import { oas31 } from 'openapi3-ts'; 21 | ``` 22 | 23 | Or directly import from subpath: 24 | 25 | ```js 26 | import { OpenAPIObject, OpenApiBuilder } from 'openapi3-ts/oas31'; 27 | ``` 28 | 29 | ### To use version 3.0 import 30 | 31 | ```js 32 | import { oas30 } from 'openapi3-ts'; 33 | ``` 34 | 35 | Or directly import from subpath: 36 | 37 | ```js 38 | import { OpenAPIObject, OpenApiBuilder } from 'openapi3-ts/oas30'; 39 | ``` 40 | 41 | ## Includes 42 | 43 | * `/src/model` TS typed interfaces for helping building a contract. 44 | * `/src/dsl` Fluent DSL for building a contract. 45 | 46 | ## Install 47 | 48 | Install package via **npm**: 49 | 50 | ```bash 51 | npm i --save openapi3-ts 52 | ``` 53 | 54 | ## Documentation, Versions, and Changelog 55 | 56 | * [Documentation](docs/index.md). 57 | * See [changelog](Changelog.md) for version and changes. 58 | 59 | ## References 60 | 61 | * OpenAPI spec 3.1.0. [https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md) 62 | 63 | ## License 64 | 65 | Licensed under the MIT License. 66 | 67 | ## Credits 68 | 69 | **Contact:** Pedro J. Molina | github: [pjmolina](https://github.com/pjmolina) | twitter: [pmolinam](https://twitter.com/pmolinam) 70 | 71 | (c) 2017-2024. [Pedro J. Molina](http://pjmolina.com) at Metadev S.L. [https://metadev.pro](https://metadev.pro) & contributors. 72 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # OpenAPI3-TS Documentation 2 | 3 | OpenAPI3-TS is a library implemented in TypeScript to help creating documents following the [**OpenAPI Specifications**](https://spec.openapis.org/oas/latest.html) to describe REST APIs in a machine & humam processable format. 4 | 5 | ## Use Cases 6 | 7 | 1. Create a OAS document from scratch and export it to JSON or YAML. 8 | 9 | The builder pattern, the TS compiler & the code-completion will guide you to add the correct data. 10 | 11 | - I can be used in TypeScript or Javascript projects working with NodeJS or Deno (in the backend). 12 | - But also on the browser as the libraries has not dependencies on other libs. 13 | 14 | ## Basic Example 15 | 16 | Minimal example to generate a 3.1 OAS compliant document: 17 | 18 | ```typescript 19 | import { OpenApiBuilder } from 'openapi3-ts/oas31'; // Pick `openapi3-ts/oas31` for 3.1.x (x)or pick `openapi3-ts/oas30` for 3.0.x 20 | 21 | function buildOas() { 22 | const builder = OpenApiBuilder 23 | .create() 24 | .addOpenApiVersion('3.1.0') 25 | .addInfo({ 26 | title: 'app', 27 | version: 'version' 28 | }) 29 | .addLicense({ 30 | name: 'Apache 2.0', 31 | url: 'http://www.apache.org/licenses/LICENSE-2.0.html' 32 | }) 33 | .addTitle('my title') 34 | .addPath('pet/{petId}', { 35 | get: { 36 | tags: ["pet"], 37 | summary: "Find pet by ID", 38 | description: "Returns a single pet", 39 | operationId: "getPetById" 40 | } 41 | }) 42 | .addResponse('Created', { 43 | description: 'Object created' 44 | }) 45 | .addSchema('Email', { 46 | type: 'string', 47 | format: 'email' 48 | }) 49 | .addParameter('id', { 50 | name: 'id', 51 | in: 'header', 52 | schema: { 53 | $ref: '#/components/schemas/id' 54 | } 55 | }); 56 | //...keep building 57 | 58 | const doc = builder.getSpec(); 59 | const docJson = builder.getSpecAsJson(); 60 | const docYaml = builder.getSpecAsYaml(); 61 | } 62 | 63 | ``` 64 | 65 | ## Dependencies 66 | 67 | The unique dependency in runtime is `yaml` to provide the YAML from/to conversions features. 68 | 69 | ## Code Organization 70 | 71 | - `src` contains the production code. 72 | - `dsl` provides the API of the library following the [intenal DSL style](https://martinfowler.com/bliki/InternalDslStyle.html) (or _fluent interface_) with two flavours: 3.0.x & 3.1.x to allow conformance with both versions of the spec. 73 | - `model` provides a DOM (Document Object Model) describing the concepts defined in the OpenAPI spec. Again in two flavours: 3.0.x and 3.1.x. 74 | - Finally `oas30.ts` & `oas31.ts` expose the types as separate namespaces. 75 | - `test` constains the unit tests. 76 | 77 | ## License 78 | 79 | Licensed under the MIT License. 80 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import js from "@eslint/js"; 6 | import { FlatCompat } from "@eslint/eslintrc"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all 14 | }); 15 | 16 | export default [...compat.extends( 17 | "eslint:recommended", 18 | "plugin:@typescript-eslint/eslint-recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | ), { 21 | plugins: { 22 | "@typescript-eslint": typescriptEslint, 23 | }, 24 | 25 | languageOptions: { 26 | parser: tsParser, 27 | }, 28 | }]; -------------------------------------------------------------------------------- /oas30.d.ts: -------------------------------------------------------------------------------- 1 | // To support envs which cannot interpret "exports" field in package.json. e.g. tsc with "moduleResolution": "node" 2 | export * from './dist/oas30' 3 | -------------------------------------------------------------------------------- /oas31.d.ts: -------------------------------------------------------------------------------- 1 | // To support envs which cannot interpret "exports" field in package.json. e.g. tsc with "moduleResolution": "node" 2 | export * from './dist/oas31' 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi3-ts", 3 | "version": "4.4.0", 4 | "description": "TS Model & utils for OpenAPI 3.x specification.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | }, 14 | "./oas30": { 15 | "types": "./dist/oas30.d.ts", 16 | "import": "./dist/oas30.mjs", 17 | "require": "./dist/oas30.js" 18 | }, 19 | "./oas31": { 20 | "types": "./dist/oas31.d.ts", 21 | "import": "./dist/oas31.mjs", 22 | "require": "./dist/oas31.js" 23 | } 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/metadevpro/openapi3-ts.git" 28 | }, 29 | "scripts": { 30 | "clean": "rimraf dist", 31 | "build": "vite build && tsc --emitDeclarationOnly", 32 | "version": "node --version && npm --version && eslint --version && tsc --version && vite --version && vitest --version", 33 | "build:all": "rimraf dist && npm run lint && npm run build && npm test", 34 | "lint": "eslint src/**/*.ts", 35 | "lint:fix": "eslint src/**/*.ts --fix", 36 | "test:tdd": "vitest -w", 37 | "test": "vitest", 38 | "test:ci": "npm run test", 39 | "cover": "vitest --coverage", 40 | "cover:ci": "npm run cover", 41 | "publish": "npm run build:all && npm publish ." 42 | }, 43 | "keywords": [ 44 | "openapi3", 45 | "ts", 46 | "typescript" 47 | ], 48 | "author": "Pedro J. Molina / Metadev", 49 | "license": "MIT", 50 | "dependencies": { 51 | "yaml": "^2.5.0" 52 | }, 53 | "devDependencies": { 54 | "@eslint/eslintrc": "^3.1.0", 55 | "@eslint/js": "^9.9.1", 56 | "@types/node": "^22.5.0", 57 | "@typescript-eslint/eslint-plugin": "^8.3.0", 58 | "@typescript-eslint/parser": "^8.3.0", 59 | "@vitest/coverage-v8": "^2.0.5", 60 | "coveralls": "^3.1.1", 61 | "eslint": "^9.9.1", 62 | "eslint-config-prettier": "^9.1.0", 63 | "eslint-plugin-prettier": "^5.2.1", 64 | "prettier": "^3.3.3", 65 | "rimraf": "^6.0.1", 66 | "typescript": "~5.5.4", 67 | "vite": "^5.4.2", 68 | "vitest": "^2.0.5", 69 | "vitest-teamcity-reporter": "^0.3.1" 70 | }, 71 | "files": [ 72 | "dist", 73 | "*.md", 74 | "oas30.d.ts", 75 | "oas31.d.ts" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /src/dsl/openapi-builder30.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { addExtension, getExtension } from '../model/oas-common'; 3 | import * as oa from '../model/openapi30'; 4 | import { OpenApiBuilder } from './openapi-builder30'; 5 | 6 | describe('OpenApiBuilder', () => { 7 | it('Build empty Spec', () => { 8 | expect(OpenApiBuilder.create().getSpec()).eql({ 9 | openapi: '3.0.0', 10 | info: { 11 | title: 'app', 12 | version: 'version' 13 | }, 14 | paths: {}, 15 | components: { 16 | schemas: {}, 17 | responses: {}, 18 | parameters: {}, 19 | examples: {}, 20 | requestBodies: {}, 21 | headers: {}, 22 | securitySchemes: {}, 23 | links: {}, 24 | callbacks: {} 25 | }, 26 | tags: [], 27 | servers: [] 28 | }); 29 | }); 30 | it('Build with custom object', () => { 31 | const obj: oa.OpenAPIObject = { 32 | openapi: '3.0.0', 33 | info: { 34 | title: 'app1', 35 | version: 'version2' 36 | }, 37 | paths: {}, 38 | components: { 39 | schemas: {}, 40 | responses: {}, 41 | parameters: {}, 42 | examples: {}, 43 | requestBodies: {}, 44 | headers: {}, 45 | securitySchemes: {}, 46 | links: {}, 47 | callbacks: {} 48 | }, 49 | tags: [], 50 | servers: [] 51 | }; 52 | expect(OpenApiBuilder.create(obj).getSpec()).eql(obj); 53 | }); 54 | it('addTitle', () => { 55 | const sut = OpenApiBuilder.create().addTitle('app7').rootDoc; 56 | expect(sut.info.title).eql('app7'); 57 | }); 58 | it('addDescription', () => { 59 | const sut = OpenApiBuilder.create().addDescription('desc 6').rootDoc; 60 | expect(sut.info.description).eql('desc 6'); 61 | }); 62 | it('addOpenApiVersion valid', () => { 63 | const sut = OpenApiBuilder.create().addOpenApiVersion('3.2.4').rootDoc; 64 | expect(sut.openapi).eql('3.2.4'); 65 | }); 66 | it('addOpenApiVersion invalid', () => { 67 | expect(() => OpenApiBuilder.create().addOpenApiVersion('a.b.4').rootDoc).toThrow(); 68 | }); 69 | it('addOpenApiVersion empty', () => { 70 | expect(() => OpenApiBuilder.create().addOpenApiVersion('').rootDoc).toThrow(); 71 | }); 72 | it('addOpenApiVersion lower than 3', () => { 73 | expect(() => OpenApiBuilder.create().addOpenApiVersion('2.5.6').rootDoc).toThrow(); 74 | }); 75 | it('addInfo', () => { 76 | const info: oa.InfoObject = { 77 | title: 'app9', 78 | version: '11.34.678' 79 | }; 80 | const sut = OpenApiBuilder.create().addInfo(info).rootDoc; 81 | expect(sut.info).eql(info); 82 | }); 83 | it('addTitle', () => { 84 | const sut = OpenApiBuilder.create().addTitle('t1').rootDoc; 85 | expect(sut.info.title).eql('t1'); 86 | }); 87 | it('addDescription', () => { 88 | const sut = OpenApiBuilder.create().addDescription('desc 2').rootDoc; 89 | expect(sut.info.description).eql('desc 2'); 90 | }); 91 | it('addTermsOfService', () => { 92 | const sut = OpenApiBuilder.create().addTermsOfService('tos 7').rootDoc; 93 | expect(sut.info.termsOfService).eql('tos 7'); 94 | }); 95 | it('addLicense', () => { 96 | const sut = OpenApiBuilder.create().addLicense({ 97 | name: 'MIT', 98 | url: 'http://mit.edu/license' 99 | }).rootDoc; 100 | expect(sut.info.license).eql({ 101 | name: 'MIT', 102 | url: 'http://mit.edu/license' 103 | }); 104 | }); 105 | it('addContact', () => { 106 | const sut = OpenApiBuilder.create().addContact({ 107 | name: 'Alicia', 108 | email: 'alicia@acme.com', 109 | url: 'http://acme.com/~alicia' 110 | }).rootDoc; 111 | expect(sut.info.contact).eql({ 112 | name: 'Alicia', 113 | email: 'alicia@acme.com', 114 | url: 'http://acme.com/~alicia' 115 | }); 116 | }); 117 | it('addVersion', () => { 118 | const sut = OpenApiBuilder.create().addVersion('7.52.46').rootDoc; 119 | expect(sut.info.version).eql('7.52.46'); 120 | }); 121 | it('addPath', () => { 122 | const path1 = { 123 | get: { 124 | responses: { 125 | default: { 126 | description: 'object created' 127 | } 128 | } 129 | } 130 | }; 131 | const doc = OpenApiBuilder.create().addPath('/path1', path1); 132 | expect(doc.rootDoc.paths['/path1']).eql(path1); 133 | const path2 = { 134 | post: { 135 | responses: { 136 | default: { 137 | description: 'object 2 created' 138 | } 139 | } 140 | } 141 | }; 142 | doc.addPath('/path1', path2); 143 | expect(doc.rootDoc.paths['/path1']).eql({ ...path1, ...path2 }); 144 | }); 145 | 146 | it('addSchema', () => { 147 | const schema1: oa.SchemaObject = { 148 | type: 'string', 149 | format: 'email' 150 | }; 151 | const sut = OpenApiBuilder.create().addSchema('schema01', schema1).rootDoc; 152 | expect(sut.components?.schemas?.schema01).eql(schema1); 153 | }); 154 | it('addSchema reference', () => { 155 | const schema1 = { 156 | $ref: '#/components/schemas/id' 157 | }; 158 | const sut = OpenApiBuilder.create().addSchema('schema01', schema1).rootDoc; 159 | expect(sut.components?.schemas?.schema01).eql(schema1); 160 | }); 161 | it('addResponse', () => { 162 | const resp00 = { 163 | description: 'object created' 164 | }; 165 | const sut = OpenApiBuilder.create().addResponse('resp00', resp00).rootDoc; 166 | expect(sut.components?.responses?.resp00).eql(resp00); 167 | }); 168 | it('addResponse reference', () => { 169 | const resp00 = { 170 | $ref: '#/components/responses/reference' 171 | }; 172 | const sut = OpenApiBuilder.create().addResponse('resp00', resp00).rootDoc; 173 | expect(sut.components?.responses?.resp00).eql(resp00); 174 | }); 175 | it('addParameter', () => { 176 | const par5 = { 177 | name: 'id', 178 | in: 'header' as oa.ParameterLocation, 179 | schema: { 180 | $ref: '#/components/schemas/id' 181 | } 182 | }; 183 | const sut = OpenApiBuilder.create().addParameter('par5', par5).rootDoc; 184 | expect(sut.components?.parameters?.par5).eql(par5); 185 | }); 186 | it('addParameter reference', () => { 187 | const par5 = { 188 | $ref: '#/components/parameters/id' 189 | }; 190 | const sut = OpenApiBuilder.create().addParameter('par5', par5).rootDoc; 191 | expect(sut.components?.parameters?.par5).eql(par5); 192 | }); 193 | it('addExample', () => { 194 | const example4 = { 195 | a: 'a desc', 196 | b: 'a desc' 197 | }; 198 | const sut = OpenApiBuilder.create().addExample('example4', example4).rootDoc; 199 | expect(sut.components?.examples?.example4).eql(example4); 200 | }); 201 | it('addExample reference', () => { 202 | const example4 = { 203 | $ref: '#/components/examples/id' 204 | }; 205 | const sut = OpenApiBuilder.create().addExample('example4', example4).rootDoc; 206 | expect(sut.components?.examples?.example4).eql(example4); 207 | }); 208 | it('addRequestBody', () => { 209 | const reqBody9: oa.RequestBodyObject = { 210 | description: 'Request body details', 211 | content: { 212 | 'application/json': { 213 | schema: { 214 | $ref: '#/components/schemas/User' 215 | }, 216 | examples: { 217 | user: { 218 | $ref: 'http://foo.bar/examples/user-example.json' 219 | } 220 | } 221 | } 222 | }, 223 | required: false 224 | }; 225 | const sut = OpenApiBuilder.create().addRequestBody('reqBody9', reqBody9).rootDoc; 226 | expect(sut.components?.requestBodies?.reqBody9).eql(reqBody9); 227 | }); 228 | it('addRequestBody reference', () => { 229 | const reqBody9 = { 230 | $ref: '#/components/requestBodies/id' 231 | }; 232 | const sut = OpenApiBuilder.create().addRequestBody('reqBody9', reqBody9).rootDoc; 233 | expect(sut.components?.requestBodies?.reqBody9).eql(reqBody9); 234 | }); 235 | it('addHeaders', () => { 236 | const h5: oa.HeaderObject = { 237 | description: 'header 5' 238 | }; 239 | const sut = OpenApiBuilder.create().addHeader('h5', h5).rootDoc; 240 | expect(sut.components?.headers?.h5).eql(h5); 241 | }); 242 | it('addHeaders Reference', () => { 243 | const h5: oa.HeaderObject = { 244 | $ref: '#/components/headers/id' 245 | }; 246 | const sut = OpenApiBuilder.create().addHeader('h5', h5).rootDoc; 247 | expect(sut.components?.headers?.h5).eql(h5); 248 | }); 249 | it('addSecuritySchemes', () => { 250 | const sec7: oa.SecuritySchemeObject = { 251 | type: 'http', 252 | scheme: 'basic' 253 | }; 254 | const sut = OpenApiBuilder.create().addSecurityScheme('sec7', sec7).rootDoc; 255 | expect(sut.components?.securitySchemes?.sec7).eql(sec7); 256 | }); 257 | it('addSecuritySchemes reference', () => { 258 | const sec7 = { 259 | $ref: '#/components/securitySchemes/id' 260 | }; 261 | const sut = OpenApiBuilder.create().addSecurityScheme('sec7', sec7).rootDoc; 262 | expect(sut.components?.securitySchemes?.sec7).eql(sec7); 263 | }); 264 | it('addLink', () => { 265 | const link0: oa.LinkObject = { 266 | href: '/users/10101110/department' 267 | }; 268 | const sut = OpenApiBuilder.create().addLink('link0', link0).rootDoc; 269 | expect(sut.components?.links?.link0).eql(link0); 270 | }); 271 | it('addLink reference', () => { 272 | const link0 = { 273 | $ref: '#/components/links/id' 274 | }; 275 | const sut = OpenApiBuilder.create().addLink('link0', link0).rootDoc; 276 | expect(sut.components?.links?.link0).eql(link0); 277 | }); 278 | it('addCallback', () => { 279 | const cb1: oa.CallbackObject = { 280 | '$request.body#/url': { 281 | post: { 282 | requestBody: { 283 | description: 'Callback payload', 284 | content: { 285 | 'application/json': { 286 | schema: { 287 | $ref: '#/components/schemas/SomePayload' 288 | } 289 | } 290 | } 291 | }, 292 | responses: { 293 | '200': { 294 | description: 295 | 'webhook successfully processed an no retries will be performed' 296 | } 297 | } 298 | } 299 | } 300 | }; 301 | const sut = OpenApiBuilder.create().addCallback('cb1', cb1).rootDoc; 302 | expect(sut.components?.callbacks?.cb1).eql(cb1); 303 | }); 304 | it('addCallback reference', () => { 305 | const cb1 = { 306 | $ref: '#/components/callbacks/id' 307 | }; 308 | const sut = OpenApiBuilder.create().addCallback('cb1', cb1).rootDoc; 309 | expect(sut.components?.callbacks?.cb1).eql(cb1); 310 | }); 311 | it('addTag', () => { 312 | const t1: oa.TagObject = { 313 | name: 'resource', 314 | 'x-admin': true, 315 | description: 'my own tag' 316 | }; 317 | const sut = OpenApiBuilder.create().addTag(t1).rootDoc; 318 | expect(sut?.tags?.[0]).eql(t1); 319 | }); 320 | it('addExternalDocs', () => { 321 | const eDocs: oa.ExternalDocumentationObject = { 322 | url: 'https://acme.com/docs', 323 | description: 'Main doc' 324 | }; 325 | const sut = OpenApiBuilder.create().addExternalDocs(eDocs).rootDoc; 326 | expect(sut.externalDocs).eql(eDocs); 327 | }); 328 | it('addServer', () => { 329 | const s1: oa.ServerObject = { 330 | url: 'http://api.quixote.org', 331 | variables: {} 332 | }; 333 | const sut = OpenApiBuilder.create().addServer(s1).rootDoc; 334 | expect(sut.servers?.[0]).eql(s1); 335 | }); 336 | 337 | it('getPath', () => { 338 | const path1 = { 339 | get: { 340 | responses: { 341 | default: { 342 | description: 'object created' 343 | } 344 | } 345 | } 346 | }; 347 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 348 | addExtension(sut.paths, 'x-my-extension', 42); 349 | 350 | expect(oa.getPath(sut.paths, '/service7')).eql(path1); 351 | expect(oa.getPath(sut.paths, '/service56')).eql(undefined); 352 | }); 353 | it('get invalid Path', () => { 354 | const path1 = { 355 | get: { 356 | responses: { 357 | default: { 358 | description: 'object created' 359 | } 360 | } 361 | } 362 | }; 363 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 364 | addExtension(sut.paths, 'x-my-extension', 42); 365 | 366 | expect(oa.getPath(sut.paths, 'x-path')).eql(undefined); 367 | }); 368 | it('getExtension', () => { 369 | const path1 = { 370 | get: { 371 | responses: { 372 | default: { 373 | description: 'object created' 374 | } 375 | } 376 | } 377 | }; 378 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 379 | addExtension(sut.paths, 'x-my-extension', 42); 380 | 381 | expect(getExtension(sut.paths, 'x-my-extension')).eql(42); 382 | expect(getExtension(sut.paths, 'x-other')).eql(undefined); 383 | }); 384 | it('retrieve invalid extension', () => { 385 | const path1 = { 386 | get: { 387 | responses: { 388 | default: { 389 | description: 'object created' 390 | } 391 | } 392 | } 393 | }; 394 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 395 | addExtension(sut.paths, 'x-my-extension', 42); 396 | 397 | expect(getExtension(sut.paths, 'y-other')).eql(undefined); 398 | }); 399 | 400 | describe('Serialize', () => { 401 | it('getSpecAsJson', () => { 402 | const sut = OpenApiBuilder.create() 403 | .addTitle('app9') 404 | .addVersion('5.6.7') 405 | .getSpecAsJson(); 406 | expect(sut).eql( 407 | `{"openapi":"3.0.0","info":{"title":"app9","version":"5.6.7"},"paths":{},"components":{"schemas":{},"responses":{},"parameters":{},"examples":{},"requestBodies":{},"headers":{},"securitySchemes":{},"links":{},"callbacks":{}},"tags":[],"servers":[]}` 408 | ); 409 | }); 410 | it('getSpecAsYaml', () => { 411 | const sut = OpenApiBuilder.create() 412 | .addTitle('app9') 413 | .addVersion('5.6.7') 414 | .getSpecAsYaml(); 415 | expect(sut).eql( 416 | 'openapi: 3.0.0\ninfo:\n title: app9\n version: 5.6.7\npaths: {}\ncomponents:\n schemas: {}\n responses: {}\n parameters: {}\n examples: {}\n requestBodies: {}\n headers: {}\n securitySchemes: {}\n links: {}\n callbacks: {}\ntags: []\nservers: []\n' 417 | ); 418 | }); 419 | }); 420 | }); 421 | -------------------------------------------------------------------------------- /src/dsl/openapi-builder30.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'yaml'; 2 | import * as oa from '../model/openapi30'; 3 | 4 | // Internal DSL for building an OpenAPI 3.0.x contract 5 | // using a fluent interface 6 | 7 | export class OpenApiBuilder { 8 | rootDoc: oa.OpenAPIObject; 9 | 10 | static create(doc?: oa.OpenAPIObject): OpenApiBuilder { 11 | return new OpenApiBuilder(doc); 12 | } 13 | 14 | constructor(doc?: oa.OpenAPIObject) { 15 | this.rootDoc = doc || { 16 | openapi: '3.0.0', 17 | info: { 18 | title: 'app', 19 | version: 'version' 20 | }, 21 | paths: {}, 22 | components: { 23 | schemas: {}, 24 | responses: {}, 25 | parameters: {}, 26 | examples: {}, 27 | requestBodies: {}, 28 | headers: {}, 29 | securitySchemes: {}, 30 | links: {}, 31 | callbacks: {} 32 | }, 33 | tags: [], 34 | servers: [] 35 | }; 36 | } 37 | 38 | getSpec(): oa.OpenAPIObject { 39 | return this.rootDoc; 40 | } 41 | 42 | getSpecAsJson( 43 | replacer?: (key: string, value: unknown) => unknown, 44 | space?: string | number 45 | ): string { 46 | return JSON.stringify(this.rootDoc, replacer, space); 47 | } 48 | getSpecAsYaml( 49 | replacer?: Parameters[1], 50 | options?: Parameters[2] 51 | ): string { 52 | return yaml.stringify(this.rootDoc, replacer, options); 53 | } 54 | 55 | private static isValidOpenApiVersion(v: string): boolean { 56 | v = v || ''; 57 | const match = /(\d+)\.(\d+).(\d+)/.exec(v); 58 | if (match) { 59 | const major = parseInt(match[1], 10); 60 | if (major >= 3) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | addOpenApiVersion(openApiVersion: string): OpenApiBuilder { 68 | if (!OpenApiBuilder.isValidOpenApiVersion(openApiVersion)) { 69 | throw new Error( 70 | 'Invalid OpenApi version: ' + openApiVersion + '. Follow convention: 3.x.y' 71 | ); 72 | } 73 | this.rootDoc.openapi = openApiVersion; 74 | return this; 75 | } 76 | addInfo(info: oa.InfoObject): OpenApiBuilder { 77 | this.rootDoc.info = info; 78 | return this; 79 | } 80 | addContact(contact: oa.ContactObject): OpenApiBuilder { 81 | this.rootDoc.info.contact = contact; 82 | return this; 83 | } 84 | addLicense(license: oa.LicenseObject): OpenApiBuilder { 85 | this.rootDoc.info.license = license; 86 | return this; 87 | } 88 | addTitle(title: string): OpenApiBuilder { 89 | this.rootDoc.info.title = title; 90 | return this; 91 | } 92 | addDescription(description: string): OpenApiBuilder { 93 | this.rootDoc.info.description = description; 94 | return this; 95 | } 96 | addTermsOfService(termsOfService: string): OpenApiBuilder { 97 | this.rootDoc.info.termsOfService = termsOfService; 98 | return this; 99 | } 100 | addVersion(version: string): OpenApiBuilder { 101 | this.rootDoc.info.version = version; 102 | return this; 103 | } 104 | addPath(path: string, pathItem: oa.PathItemObject): OpenApiBuilder { 105 | this.rootDoc.paths[path] = { ...(this.rootDoc.paths[path] || {}), ...pathItem }; 106 | return this; 107 | } 108 | addSchema(name: string, schema: oa.SchemaObject | oa.ReferenceObject): OpenApiBuilder { 109 | this.rootDoc.components = this.rootDoc.components || {}; 110 | this.rootDoc.components.schemas = this.rootDoc.components.schemas || {}; 111 | this.rootDoc.components.schemas[name] = schema; 112 | return this; 113 | } 114 | addResponse(name: string, response: oa.ResponseObject | oa.ReferenceObject): OpenApiBuilder { 115 | this.rootDoc.components = this.rootDoc.components || {}; 116 | this.rootDoc.components.responses = this.rootDoc.components.responses || {}; 117 | this.rootDoc.components.responses[name] = response; 118 | return this; 119 | } 120 | addParameter(name: string, parameter: oa.ParameterObject | oa.ReferenceObject): OpenApiBuilder { 121 | this.rootDoc.components = this.rootDoc.components || {}; 122 | this.rootDoc.components.parameters = this.rootDoc.components.parameters || {}; 123 | this.rootDoc.components.parameters[name] = parameter; 124 | return this; 125 | } 126 | addExample(name: string, example: oa.ExampleObject | oa.ReferenceObject): OpenApiBuilder { 127 | this.rootDoc.components = this.rootDoc.components || {}; 128 | this.rootDoc.components.examples = this.rootDoc.components.examples || {}; 129 | this.rootDoc.components.examples[name] = example; 130 | return this; 131 | } 132 | addRequestBody( 133 | name: string, 134 | reqBody: oa.RequestBodyObject | oa.ReferenceObject 135 | ): OpenApiBuilder { 136 | this.rootDoc.components = this.rootDoc.components || {}; 137 | this.rootDoc.components.requestBodies = this.rootDoc.components.requestBodies || {}; 138 | this.rootDoc.components.requestBodies[name] = reqBody; 139 | return this; 140 | } 141 | addHeader(name: string, header: oa.HeaderObject | oa.ReferenceObject): OpenApiBuilder { 142 | this.rootDoc.components = this.rootDoc.components || {}; 143 | this.rootDoc.components.headers = this.rootDoc.components.headers || {}; 144 | this.rootDoc.components.headers[name] = header; 145 | return this; 146 | } 147 | addSecurityScheme( 148 | name: string, 149 | secScheme: oa.SecuritySchemeObject | oa.ReferenceObject 150 | ): OpenApiBuilder { 151 | this.rootDoc.components = this.rootDoc.components || {}; 152 | this.rootDoc.components.securitySchemes = this.rootDoc.components.securitySchemes || {}; 153 | this.rootDoc.components.securitySchemes[name] = secScheme; 154 | return this; 155 | } 156 | addLink(name: string, link: oa.LinkObject | oa.ReferenceObject): OpenApiBuilder { 157 | this.rootDoc.components = this.rootDoc.components || {}; 158 | this.rootDoc.components.links = this.rootDoc.components.links || {}; 159 | this.rootDoc.components.links[name] = link; 160 | return this; 161 | } 162 | addCallback(name: string, callback: oa.CallbackObject | oa.ReferenceObject): OpenApiBuilder { 163 | this.rootDoc.components = this.rootDoc.components || {}; 164 | this.rootDoc.components.callbacks = this.rootDoc.components.callbacks || {}; 165 | this.rootDoc.components.callbacks[name] = callback; 166 | return this; 167 | } 168 | addServer(server: oa.ServerObject): OpenApiBuilder { 169 | this.rootDoc.servers = this.rootDoc.servers || []; 170 | this.rootDoc.servers.push(server); 171 | return this; 172 | } 173 | addTag(tag: oa.TagObject): OpenApiBuilder { 174 | this.rootDoc.tags = this.rootDoc.tags || []; 175 | this.rootDoc.tags.push(tag); 176 | return this; 177 | } 178 | addExternalDocs(extDoc: oa.ExternalDocumentationObject): OpenApiBuilder { 179 | this.rootDoc.externalDocs = extDoc; 180 | return this; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/dsl/openapi-builder31.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { addExtension, getExtension } from '../model/oas-common'; 3 | import * as oa from '../model/openapi31'; 4 | import { OpenApiBuilder } from './openapi-builder31'; 5 | 6 | describe('OpenApiBuilder', () => { 7 | it('Build empty Spec', () => { 8 | expect(OpenApiBuilder.create().getSpec()).eql({ 9 | openapi: '3.1.0', 10 | info: { 11 | title: 'app', 12 | version: 'version' 13 | }, 14 | paths: {}, 15 | components: { 16 | schemas: {}, 17 | responses: {}, 18 | parameters: {}, 19 | examples: {}, 20 | requestBodies: {}, 21 | headers: {}, 22 | securitySchemes: {}, 23 | links: {}, 24 | callbacks: {} 25 | }, 26 | tags: [], 27 | servers: [] 28 | }); 29 | }); 30 | it('Build with custom object', () => { 31 | const obj: oa.OpenAPIObject = { 32 | openapi: '3.1.0', 33 | info: { 34 | title: 'app1', 35 | version: 'version2' 36 | }, 37 | paths: {}, 38 | components: { 39 | schemas: {}, 40 | responses: {}, 41 | parameters: {}, 42 | examples: {}, 43 | requestBodies: {}, 44 | headers: {}, 45 | securitySchemes: {}, 46 | links: {}, 47 | callbacks: {} 48 | }, 49 | tags: [], 50 | servers: [] 51 | }; 52 | expect(OpenApiBuilder.create(obj).getSpec()).eql(obj); 53 | }); 54 | it('addTitle', () => { 55 | const sut = OpenApiBuilder.create().addTitle('app7').rootDoc; 56 | expect(sut.info.title).eql('app7'); 57 | }); 58 | it('addDescription', () => { 59 | const sut = OpenApiBuilder.create().addDescription('desc 6').rootDoc; 60 | expect(sut.info.description).eql('desc 6'); 61 | }); 62 | it('addOpenApiVersion valid', () => { 63 | const sut = OpenApiBuilder.create().addOpenApiVersion('3.2.4').rootDoc; 64 | expect(sut.openapi).eql('3.2.4'); 65 | }); 66 | it('addOpenApiVersion invalid', () => { 67 | expect(() => OpenApiBuilder.create().addOpenApiVersion('a.b.4').rootDoc).toThrow(); 68 | }); 69 | it('addOpenApiVersion empty', () => { 70 | expect(() => OpenApiBuilder.create().addOpenApiVersion('').rootDoc).toThrow(); 71 | }); 72 | it('addOpenApiVersion lower than 3', () => { 73 | expect(() => OpenApiBuilder.create().addOpenApiVersion('2.5.6').rootDoc).toThrow(); 74 | }); 75 | it('addInfo', () => { 76 | const info: oa.InfoObject = { 77 | title: 'app9', 78 | version: '11.34.678' 79 | }; 80 | const sut = OpenApiBuilder.create().addInfo(info).rootDoc; 81 | expect(sut.info).eql(info); 82 | }); 83 | it('addTitle', () => { 84 | const sut = OpenApiBuilder.create().addTitle('t1').rootDoc; 85 | expect(sut.info.title).eql('t1'); 86 | }); 87 | it('addDescription', () => { 88 | const sut = OpenApiBuilder.create().addDescription('desc 2').rootDoc; 89 | expect(sut.info.description).eql('desc 2'); 90 | }); 91 | it('addTermsOfService', () => { 92 | const sut = OpenApiBuilder.create().addTermsOfService('tos 7').rootDoc; 93 | expect(sut.info.termsOfService).eql('tos 7'); 94 | }); 95 | it('addLicense', () => { 96 | const sut = OpenApiBuilder.create().addLicense({ 97 | name: 'MIT', 98 | url: 'http://mit.edu/license' 99 | }).rootDoc; 100 | expect(sut.info.license).eql({ 101 | name: 'MIT', 102 | url: 'http://mit.edu/license' 103 | }); 104 | }); 105 | it('addContact', () => { 106 | const sut = OpenApiBuilder.create().addContact({ 107 | name: 'Alicia', 108 | email: 'alicia@acme.com', 109 | url: 'http://acme.com/~alicia' 110 | }).rootDoc; 111 | expect(sut.info.contact).eql({ 112 | name: 'Alicia', 113 | email: 'alicia@acme.com', 114 | url: 'http://acme.com/~alicia' 115 | }); 116 | }); 117 | it('addVersion', () => { 118 | const sut = OpenApiBuilder.create().addVersion('7.52.46').rootDoc; 119 | expect(sut.info.version).eql('7.52.46'); 120 | }); 121 | it('addPath', () => { 122 | const path1 = { 123 | get: { 124 | responses: { 125 | default: { 126 | description: 'object created' 127 | } 128 | } 129 | } 130 | }; 131 | const doc = OpenApiBuilder.create().addPath('/path1', path1); 132 | expect(doc.rootDoc.paths?.['/path1']).eql(path1); 133 | const path2 = { 134 | post: { 135 | responses: { 136 | default: { 137 | description: 'object 2 created' 138 | } 139 | } 140 | } 141 | }; 142 | doc.addPath('/path1', path2); 143 | expect(doc.rootDoc.paths?.['/path1']).eql({ ...path1, ...path2 }); 144 | }); 145 | 146 | it('addWebhook', () => { 147 | const webhook1 = { 148 | post: { 149 | responses: { 150 | default: { 151 | description: 'event processed' 152 | } 153 | } 154 | } 155 | }; 156 | const sut = OpenApiBuilder.create().addWebhook('webhook1', webhook1).rootDoc; 157 | expect(sut.webhooks?.['webhook1']).eql(webhook1); 158 | }); 159 | it('addSchema', () => { 160 | const schema1: oa.SchemaObject = { 161 | type: 'string', 162 | format: 'email' 163 | }; 164 | const sut = OpenApiBuilder.create().addSchema('schema01', schema1).rootDoc; 165 | expect(sut.components?.schemas?.schema01).eql(schema1); 166 | }); 167 | it('addSchema reference', () => { 168 | const schema1 = { 169 | $ref: '#/components/schemas/id' 170 | }; 171 | const sut = OpenApiBuilder.create().addSchema('schema01', schema1).rootDoc; 172 | expect(sut.components?.schemas?.schema01).eql(schema1); 173 | }); 174 | it('addResponse', () => { 175 | const resp00 = { 176 | description: 'object created' 177 | }; 178 | const sut = OpenApiBuilder.create().addResponse('resp00', resp00).rootDoc; 179 | expect(sut.components?.responses?.resp00).eql(resp00); 180 | }); 181 | it('addResponse reference', () => { 182 | const resp00 = { 183 | $ref: '#/components/responses/reference' 184 | }; 185 | const sut = OpenApiBuilder.create().addResponse('resp00', resp00).rootDoc; 186 | expect(sut.components?.responses?.resp00).eql(resp00); 187 | }); 188 | it('addParameter', () => { 189 | const par5 = { 190 | name: 'id', 191 | in: 'header' as oa.ParameterLocation, 192 | schema: { 193 | $ref: '#/components/schemas/id' 194 | } 195 | }; 196 | const sut = OpenApiBuilder.create().addParameter('par5', par5).rootDoc; 197 | expect(sut.components?.parameters?.par5).eql(par5); 198 | }); 199 | it('addParameter reference', () => { 200 | const par5 = { 201 | $ref: '#/components/parameters/id' 202 | }; 203 | const sut = OpenApiBuilder.create().addParameter('par5', par5).rootDoc; 204 | expect(sut.components?.parameters?.par5).eql(par5); 205 | }); 206 | it('addExample', () => { 207 | const example4 = { 208 | a: 'a desc', 209 | b: 'a desc' 210 | }; 211 | const sut = OpenApiBuilder.create().addExample('example4', example4).rootDoc; 212 | expect(sut.components?.examples?.example4).eql(example4); 213 | }); 214 | it('addExample reference', () => { 215 | const example4 = { 216 | $ref: '#/components/examples/id' 217 | }; 218 | const sut = OpenApiBuilder.create().addExample('example4', example4).rootDoc; 219 | expect(sut.components?.examples?.example4).eql(example4); 220 | }); 221 | it('addRequestBody', () => { 222 | const reqBody9: oa.RequestBodyObject = { 223 | description: 'Request body details', 224 | content: { 225 | 'application/json': { 226 | schema: { 227 | $ref: '#/components/schemas/User' 228 | }, 229 | examples: { 230 | user: { 231 | $ref: 'http://foo.bar/examples/user-example.json' 232 | } 233 | } 234 | } 235 | }, 236 | required: false 237 | }; 238 | const sut = OpenApiBuilder.create().addRequestBody('reqBody9', reqBody9).rootDoc; 239 | expect(sut.components?.requestBodies?.reqBody9).eql(reqBody9); 240 | }); 241 | it('addRequestBody reference', () => { 242 | const reqBody9 = { 243 | $ref: '#/components/requestBodies/id' 244 | }; 245 | const sut = OpenApiBuilder.create().addRequestBody('reqBody9', reqBody9).rootDoc; 246 | expect(sut.components?.requestBodies?.reqBody9).eql(reqBody9); 247 | }); 248 | it('addHeaders', () => { 249 | const h5: oa.HeaderObject = { 250 | description: 'header 5' 251 | }; 252 | const sut = OpenApiBuilder.create().addHeader('h5', h5).rootDoc; 253 | expect(sut.components?.headers?.h5).eql(h5); 254 | }); 255 | it('addHeaders Reference', () => { 256 | const h5: oa.HeaderObject = { 257 | $ref: '#/components/headers/id' 258 | }; 259 | const sut = OpenApiBuilder.create().addHeader('h5', h5).rootDoc; 260 | expect(sut.components?.headers?.h5).eql(h5); 261 | }); 262 | it('addSecuritySchemes', () => { 263 | const sec7: oa.SecuritySchemeObject = { 264 | type: 'http', 265 | scheme: 'basic' 266 | }; 267 | const sut = OpenApiBuilder.create().addSecurityScheme('sec7', sec7).rootDoc; 268 | expect(sut.components?.securitySchemes?.sec7).eql(sec7); 269 | }); 270 | it('addSecuritySchemes reference', () => { 271 | const sec7 = { 272 | $ref: '#/components/securitySchemes/id' 273 | }; 274 | const sut = OpenApiBuilder.create().addSecurityScheme('sec7', sec7).rootDoc; 275 | expect(sut.components?.securitySchemes?.sec7).eql(sec7); 276 | }); 277 | it('addLink', () => { 278 | const link0: oa.LinkObject = { 279 | href: '/users/10101110/department' 280 | }; 281 | const sut = OpenApiBuilder.create().addLink('link0', link0).rootDoc; 282 | expect(sut.components?.links?.link0).eql(link0); 283 | }); 284 | it('addLink reference', () => { 285 | const link0 = { 286 | $ref: '#/components/links/id' 287 | }; 288 | const sut = OpenApiBuilder.create().addLink('link0', link0).rootDoc; 289 | expect(sut.components?.links?.link0).eql(link0); 290 | }); 291 | it('addCallback', () => { 292 | const cb1: oa.CallbackObject = { 293 | '$request.body#/url': { 294 | post: { 295 | requestBody: { 296 | description: 'Callback payload', 297 | content: { 298 | 'application/json': { 299 | schema: { 300 | $ref: '#/components/schemas/SomePayload' 301 | } 302 | } 303 | } 304 | }, 305 | responses: { 306 | '200': { 307 | description: 308 | 'webhook successfully processed an no retries will be performed' 309 | } 310 | } 311 | } 312 | } 313 | }; 314 | const sut = OpenApiBuilder.create().addCallback('cb1', cb1).rootDoc; 315 | expect(sut.components?.callbacks?.cb1).eql(cb1); 316 | }); 317 | it('addCallback reference', () => { 318 | const cb1 = { 319 | $ref: '#/components/callbacks/id' 320 | }; 321 | const sut = OpenApiBuilder.create().addCallback('cb1', cb1).rootDoc; 322 | expect(sut.components?.callbacks?.cb1).eql(cb1); 323 | }); 324 | it('addTag', () => { 325 | const t1: oa.TagObject = { 326 | name: 'resource', 327 | 'x-admin': true, 328 | description: 'my own tag' 329 | }; 330 | const sut = OpenApiBuilder.create().addTag(t1).rootDoc; 331 | expect(sut.tags?.[0]).eql(t1); 332 | }); 333 | it('addExternalDocs', () => { 334 | const eDocs: oa.ExternalDocumentationObject = { 335 | url: 'https://acme.com/docs', 336 | description: 'Main doc' 337 | }; 338 | const sut = OpenApiBuilder.create().addExternalDocs(eDocs).rootDoc; 339 | expect(sut.externalDocs).eql(eDocs); 340 | }); 341 | it('addServer', () => { 342 | const s1: oa.ServerObject = { 343 | url: 'http://api.quixote.org', 344 | variables: {} 345 | }; 346 | const sut = OpenApiBuilder.create().addServer(s1).rootDoc; 347 | expect(sut.servers?.[0]).eql(s1); 348 | }); 349 | 350 | it('getPath', () => { 351 | const path1 = { 352 | get: { 353 | responses: { 354 | default: { 355 | description: 'object created' 356 | } 357 | } 358 | } 359 | }; 360 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 361 | addExtension(sut.paths, 'x-my-extension', 42); 362 | 363 | expect(oa.getPath(sut.paths, '/service7')).eql(path1); 364 | expect(oa.getPath(sut.paths, '/service56')).eql(undefined); 365 | }); 366 | it('get invalid Path', () => { 367 | const path1 = { 368 | get: { 369 | responses: { 370 | default: { 371 | description: 'object created' 372 | } 373 | } 374 | } 375 | }; 376 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 377 | addExtension(sut.paths, 'x-my-extension', 42); 378 | 379 | expect(oa.getPath(sut.paths, 'x-path')).eql(undefined); 380 | }); 381 | it('getExtension', () => { 382 | const path1 = { 383 | get: { 384 | responses: { 385 | default: { 386 | description: 'object created' 387 | } 388 | } 389 | } 390 | }; 391 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 392 | addExtension(sut.paths, 'x-my-extension', 42); 393 | 394 | expect(getExtension(sut.paths, 'x-my-extension')).eql(42); 395 | expect(getExtension(sut.paths, 'x-other')).eql(undefined); 396 | }); 397 | it('retrieve invalid extension', () => { 398 | const path1 = { 399 | get: { 400 | responses: { 401 | default: { 402 | description: 'object created' 403 | } 404 | } 405 | } 406 | }; 407 | const sut = OpenApiBuilder.create().addPath('/service7', path1).rootDoc; 408 | addExtension(sut.paths, 'x-my-extension', 42); 409 | 410 | expect(getExtension(sut.paths, 'y-other')).eql(undefined); 411 | }); 412 | 413 | describe('Serialize', () => { 414 | it('getSpecAsJson', () => { 415 | const sut = OpenApiBuilder.create() 416 | .addTitle('app9') 417 | .addVersion('5.6.7') 418 | .getSpecAsJson(); 419 | expect(sut).eql( 420 | `{"openapi":"3.1.0","info":{"title":"app9","version":"5.6.7"},"paths":{},"components":{"schemas":{},"responses":{},"parameters":{},"examples":{},"requestBodies":{},"headers":{},"securitySchemes":{},"links":{},"callbacks":{}},"tags":[],"servers":[]}` 421 | ); 422 | }); 423 | it('getSpecAsYaml', () => { 424 | const sut = OpenApiBuilder.create() 425 | .addTitle('app9') 426 | .addVersion('5.6.7') 427 | .getSpecAsYaml(); 428 | expect(sut).eql( 429 | 'openapi: 3.1.0\ninfo:\n title: app9\n version: 5.6.7\npaths: {}\ncomponents:\n schemas: {}\n responses: {}\n parameters: {}\n examples: {}\n requestBodies: {}\n headers: {}\n securitySchemes: {}\n links: {}\n callbacks: {}\ntags: []\nservers: []\n' 430 | ); 431 | }); 432 | }); 433 | }); 434 | -------------------------------------------------------------------------------- /src/dsl/openapi-builder31.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'yaml'; 2 | import * as oa from '../model/openapi31'; 3 | 4 | // Internal DSL for building an OpenAPI 3.1.x contract 5 | // using a fluent interface 6 | 7 | export class OpenApiBuilder { 8 | rootDoc: oa.OpenAPIObject; 9 | 10 | static create(doc?: oa.OpenAPIObject): OpenApiBuilder { 11 | return new OpenApiBuilder(doc); 12 | } 13 | 14 | constructor(doc?: oa.OpenAPIObject) { 15 | this.rootDoc = doc || { 16 | openapi: '3.1.0', 17 | info: { 18 | title: 'app', 19 | version: 'version' 20 | }, 21 | paths: {}, 22 | components: { 23 | schemas: {}, 24 | responses: {}, 25 | parameters: {}, 26 | examples: {}, 27 | requestBodies: {}, 28 | headers: {}, 29 | securitySchemes: {}, 30 | links: {}, 31 | callbacks: {} 32 | }, 33 | tags: [], 34 | servers: [] 35 | }; 36 | } 37 | 38 | getSpec(): oa.OpenAPIObject { 39 | return this.rootDoc; 40 | } 41 | 42 | getSpecAsJson( 43 | replacer?: (key: string, value: unknown) => unknown, 44 | space?: string | number 45 | ): string { 46 | return JSON.stringify(this.rootDoc, replacer, space); 47 | } 48 | getSpecAsYaml( 49 | replacer?: Parameters[1], 50 | options?: Parameters[2] 51 | ): string { 52 | return yaml.stringify(this.rootDoc, replacer, options); 53 | } 54 | 55 | private static isValidOpenApiVersion(v: string): boolean { 56 | v = v || ''; 57 | const match = /(\d+)\.(\d+).(\d+)/.exec(v); 58 | if (match) { 59 | const major = parseInt(match[1], 10); 60 | if (major >= 3) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | addOpenApiVersion(openApiVersion: string): OpenApiBuilder { 68 | if (!OpenApiBuilder.isValidOpenApiVersion(openApiVersion)) { 69 | throw new Error( 70 | 'Invalid OpenApi version: ' + openApiVersion + '. Follow convention: 3.x.y' 71 | ); 72 | } 73 | this.rootDoc.openapi = openApiVersion; 74 | return this; 75 | } 76 | addInfo(info: oa.InfoObject): OpenApiBuilder { 77 | this.rootDoc.info = info; 78 | return this; 79 | } 80 | addContact(contact: oa.ContactObject): OpenApiBuilder { 81 | this.rootDoc.info.contact = contact; 82 | return this; 83 | } 84 | addLicense(license: oa.LicenseObject): OpenApiBuilder { 85 | this.rootDoc.info.license = license; 86 | return this; 87 | } 88 | addTitle(title: string): OpenApiBuilder { 89 | this.rootDoc.info.title = title; 90 | return this; 91 | } 92 | addDescription(description: string): OpenApiBuilder { 93 | this.rootDoc.info.description = description; 94 | return this; 95 | } 96 | addTermsOfService(termsOfService: string): OpenApiBuilder { 97 | this.rootDoc.info.termsOfService = termsOfService; 98 | return this; 99 | } 100 | addVersion(version: string): OpenApiBuilder { 101 | this.rootDoc.info.version = version; 102 | return this; 103 | } 104 | addPath(path: string, pathItem: oa.PathItemObject): OpenApiBuilder { 105 | this.rootDoc.paths = this.rootDoc.paths || {}; 106 | this.rootDoc.paths[path] = { ...(this.rootDoc.paths[path] || {}), ...pathItem }; 107 | return this; 108 | } 109 | addSchema(name: string, schema: oa.SchemaObject | oa.ReferenceObject): OpenApiBuilder { 110 | this.rootDoc.components = this.rootDoc.components || {}; 111 | this.rootDoc.components.schemas = this.rootDoc.components.schemas || {}; 112 | this.rootDoc.components.schemas[name] = schema; 113 | return this; 114 | } 115 | addResponse(name: string, response: oa.ResponseObject | oa.ReferenceObject): OpenApiBuilder { 116 | this.rootDoc.components = this.rootDoc.components || {}; 117 | this.rootDoc.components.responses = this.rootDoc.components.responses || {}; 118 | this.rootDoc.components.responses[name] = response; 119 | return this; 120 | } 121 | addParameter(name: string, parameter: oa.ParameterObject | oa.ReferenceObject): OpenApiBuilder { 122 | this.rootDoc.components = this.rootDoc.components || {}; 123 | this.rootDoc.components.parameters = this.rootDoc.components.parameters || {}; 124 | this.rootDoc.components.parameters[name] = parameter; 125 | return this; 126 | } 127 | addExample(name: string, example: oa.ExampleObject | oa.ReferenceObject): OpenApiBuilder { 128 | this.rootDoc.components = this.rootDoc.components || {}; 129 | this.rootDoc.components.examples = this.rootDoc.components.examples || {}; 130 | this.rootDoc.components.examples[name] = example; 131 | return this; 132 | } 133 | addRequestBody( 134 | name: string, 135 | reqBody: oa.RequestBodyObject | oa.ReferenceObject 136 | ): OpenApiBuilder { 137 | this.rootDoc.components = this.rootDoc.components || {}; 138 | this.rootDoc.components.requestBodies = this.rootDoc.components.requestBodies || {}; 139 | this.rootDoc.components.requestBodies[name] = reqBody; 140 | return this; 141 | } 142 | addHeader(name: string, header: oa.HeaderObject | oa.ReferenceObject): OpenApiBuilder { 143 | this.rootDoc.components = this.rootDoc.components || {}; 144 | this.rootDoc.components.headers = this.rootDoc.components.headers || {}; 145 | this.rootDoc.components.headers[name] = header; 146 | return this; 147 | } 148 | addSecurityScheme( 149 | name: string, 150 | secScheme: oa.SecuritySchemeObject | oa.ReferenceObject 151 | ): OpenApiBuilder { 152 | this.rootDoc.components = this.rootDoc.components || {}; 153 | this.rootDoc.components.securitySchemes = this.rootDoc.components.securitySchemes || {}; 154 | this.rootDoc.components.securitySchemes[name] = secScheme; 155 | return this; 156 | } 157 | addLink(name: string, link: oa.LinkObject | oa.ReferenceObject): OpenApiBuilder { 158 | this.rootDoc.components = this.rootDoc.components || {}; 159 | this.rootDoc.components.links = this.rootDoc.components.links || {}; 160 | this.rootDoc.components.links[name] = link; 161 | return this; 162 | } 163 | addCallback(name: string, callback: oa.CallbackObject | oa.ReferenceObject): OpenApiBuilder { 164 | this.rootDoc.components = this.rootDoc.components || {}; 165 | this.rootDoc.components.callbacks = this.rootDoc.components.callbacks || {}; 166 | this.rootDoc.components.callbacks[name] = callback; 167 | return this; 168 | } 169 | addServer(server: oa.ServerObject): OpenApiBuilder { 170 | this.rootDoc.servers = this.rootDoc.servers || []; 171 | this.rootDoc.servers.push(server); 172 | return this; 173 | } 174 | addTag(tag: oa.TagObject): OpenApiBuilder { 175 | this.rootDoc.tags = this.rootDoc.tags || []; 176 | this.rootDoc.tags.push(tag); 177 | return this; 178 | } 179 | addExternalDocs(extDoc: oa.ExternalDocumentationObject): OpenApiBuilder { 180 | this.rootDoc.externalDocs = extDoc; 181 | return this; 182 | } 183 | addWebhook(webhook: string, webhookItem: oa.PathItemObject): OpenApiBuilder { 184 | this.rootDoc.webhooks ??= {}; 185 | this.rootDoc.webhooks[webhook] = webhookItem; 186 | return this; 187 | } 188 | } -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { oas30, oas31, Server, ServerVariable } from '.'; 3 | 4 | describe('Top barrel', () => { 5 | it('OpenApiBuilder v. 3.0 is exported', () => { 6 | const sut = oas30.OpenApiBuilder.create(); 7 | expect(sut).not.toBeNull(); 8 | expect(sut.rootDoc.openapi).toBe('3.0.0'); 9 | }); 10 | it('OpenApiBuilder v. 3.1 is exported', () => { 11 | const sut = oas31.OpenApiBuilder.create(); 12 | expect(sut).not.toBeNull(); 13 | expect(sut.rootDoc.openapi).toBe('3.1.0'); 14 | }); 15 | it('Server is exported', () => { 16 | const sut = new Server('a', 'b'); 17 | expect(sut).not.toBeNull(); 18 | }); 19 | it('ServerVariable is exported', () => { 20 | const sut = new ServerVariable('a', ['b'], 'c'); 21 | expect(sut).not.toBeNull(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as oas30 from "./oas30"; 2 | export * as oas31 from "./oas31"; 3 | export { Server, ServerVariable } from "./model/server" 4 | -------------------------------------------------------------------------------- /src/model/oas-common.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { 4 | IExtensionName, 5 | ISpecificationExtension, 6 | SpecificationExtension 7 | } from './specification-extension'; 8 | 9 | export interface ServerObject extends ISpecificationExtension { 10 | url: string; 11 | description?: string; 12 | variables?: { [v: string]: ServerVariableObject }; 13 | } 14 | export interface ServerVariableObject extends ISpecificationExtension { 15 | enum?: string[] | boolean[] | number[]; 16 | default: string | boolean | number; 17 | description?: string; 18 | } 19 | 20 | export function getExtension(obj: ISpecificationExtension | undefined, extensionName: string): any { 21 | if (!obj) { 22 | return undefined; 23 | } 24 | if (SpecificationExtension.isValidExtension(extensionName)) { 25 | return obj[extensionName as IExtensionName]; 26 | } 27 | return undefined; 28 | } 29 | export function addExtension( 30 | obj: ISpecificationExtension | undefined, 31 | extensionName: string, 32 | extension: any 33 | ): void { 34 | if (obj && SpecificationExtension.isValidExtension(extensionName)) { 35 | obj[extensionName as IExtensionName] = extension; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/model/openapi30.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { addExtension } from './oas-common'; 3 | import { ReferenceObject, SchemaObject, isReferenceObject, isSchemaObject } from './openapi30'; 4 | import { IExtensionName, IExtensionType } from './specification-extension'; 5 | 6 | describe('type-guards unit tests', () => { 7 | describe('isSchemaObject()', () => { 8 | it('returns true for a schema object', () => { 9 | const schemaObject = new TestSchemaObject(); 10 | expect(isSchemaObject(schemaObject)).toBe(true); 11 | }); 12 | 13 | it('returns false for a reference object', () => { 14 | const referenceObject = new TestReferenceObject(); 15 | expect(isSchemaObject(referenceObject)).toBe(false); 16 | }); 17 | }); 18 | 19 | describe('isReferenceObject()', () => { 20 | it('returns true for a reference object', () => { 21 | const referenceObject = new TestReferenceObject(); 22 | expect(isReferenceObject(referenceObject)).toBe(true); 23 | }); 24 | 25 | it('returns false for a schema object', () => { 26 | const schemaObject = new TestSchemaObject(); 27 | expect(isReferenceObject(schemaObject)).toBe(false); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('addExtension()', () => { 33 | it('valid extension', () => { 34 | const subject = {}; 35 | addExtension(subject, 'x-extension1', 'myvalue'); 36 | expect(subject['x-extension1']).toBe('myvalue'); 37 | }); 38 | it('invalid extension', () => { 39 | const subject = {}; 40 | addExtension(subject, 'ZZ-extension1', 'myvalue'); 41 | expect(subject['ZZ-extension1']).not.toBe('myvalue'); 42 | }); 43 | }); 44 | 45 | class TestSchemaObject implements SchemaObject { 46 | [k: IExtensionName]: IExtensionType; 47 | // empty schema 48 | } 49 | 50 | class TestReferenceObject implements ReferenceObject { 51 | $ref = 'test'; 52 | } 53 | -------------------------------------------------------------------------------- /src/model/openapi30.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | // Typed interfaces for OpenAPI 3.0.3 4 | // see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md 5 | 6 | import { ServerObject } from './oas-common'; 7 | import { ISpecificationExtension, SpecificationExtension } from './specification-extension'; 8 | 9 | export * from './oas-common'; 10 | export type { ISpecificationExtension, SpecificationExtension } from './specification-extension'; 11 | 12 | export interface OpenAPIObject extends ISpecificationExtension { 13 | openapi: string; 14 | info: InfoObject; 15 | servers?: ServerObject[]; 16 | paths: PathsObject; 17 | components?: ComponentsObject; 18 | security?: SecurityRequirementObject[]; 19 | tags?: TagObject[]; 20 | externalDocs?: ExternalDocumentationObject; 21 | } 22 | export interface InfoObject extends ISpecificationExtension { 23 | title: string; 24 | description?: string; 25 | termsOfService?: string; 26 | contact?: ContactObject; 27 | license?: LicenseObject; 28 | version: string; 29 | } 30 | export interface ContactObject extends ISpecificationExtension { 31 | name?: string; 32 | url?: string; 33 | email?: string; 34 | } 35 | export interface LicenseObject extends ISpecificationExtension { 36 | name: string; 37 | url?: string; 38 | } 39 | 40 | export interface ComponentsObject extends ISpecificationExtension { 41 | schemas?: { [schema: string]: SchemaObject | ReferenceObject }; 42 | responses?: { [response: string]: ResponseObject | ReferenceObject }; 43 | parameters?: { [parameter: string]: ParameterObject | ReferenceObject }; 44 | examples?: { [example: string]: ExampleObject | ReferenceObject }; 45 | requestBodies?: { [request: string]: RequestBodyObject | ReferenceObject }; 46 | headers?: { [header: string]: HeaderObject | ReferenceObject }; 47 | securitySchemes?: { [securityScheme: string]: SecuritySchemeObject | ReferenceObject }; 48 | links?: { [link: string]: LinkObject | ReferenceObject }; 49 | callbacks?: { [callback: string]: CallbackObject | ReferenceObject }; 50 | } 51 | 52 | /** 53 | * Rename it to Paths Object to be consistent with the spec 54 | * See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathsObject 55 | */ 56 | export interface PathsObject extends ISpecificationExtension { 57 | // [path: string]: PathItemObject; 58 | [path: string]: PathItemObject 59 | } 60 | 61 | /** 62 | * @deprecated 63 | * Create a type alias for backward compatibility 64 | */ 65 | export type PathObject = PathsObject; 66 | 67 | export function getPath(pathsObject: PathsObject, path: string): PathItemObject | undefined { 68 | if (SpecificationExtension.isValidExtension(path)) { 69 | return undefined; 70 | } 71 | return pathsObject[path] as PathItemObject; 72 | } 73 | 74 | export interface PathItemObject extends ISpecificationExtension { 75 | $ref?: string; 76 | summary?: string; 77 | description?: string; 78 | get?: OperationObject; 79 | put?: OperationObject; 80 | post?: OperationObject; 81 | delete?: OperationObject; 82 | options?: OperationObject; 83 | head?: OperationObject; 84 | patch?: OperationObject; 85 | trace?: OperationObject; 86 | servers?: ServerObject[]; 87 | parameters?: (ParameterObject | ReferenceObject)[]; 88 | } 89 | export interface OperationObject extends ISpecificationExtension { 90 | tags?: string[]; 91 | summary?: string; 92 | description?: string; 93 | externalDocs?: ExternalDocumentationObject; 94 | operationId?: string; 95 | parameters?: (ParameterObject | ReferenceObject)[]; 96 | requestBody?: RequestBodyObject | ReferenceObject; 97 | responses: ResponsesObject; 98 | callbacks?: CallbacksObject; 99 | deprecated?: boolean; 100 | security?: SecurityRequirementObject[]; 101 | servers?: ServerObject[]; 102 | } 103 | export interface ExternalDocumentationObject extends ISpecificationExtension { 104 | description?: string; 105 | url: string; 106 | } 107 | 108 | /** 109 | * The location of a parameter. 110 | * Possible values are "query", "header", "path" or "cookie". 111 | * Specification: 112 | * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-locations 113 | */ 114 | export type ParameterLocation = 'query' | 'header' | 'path' | 'cookie'; 115 | 116 | /** 117 | * The style of a parameter. 118 | * Describes how the parameter value will be serialized. 119 | * (serialization is not implemented yet) 120 | * Specification: 121 | * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-values 122 | */ 123 | export type ParameterStyle = 124 | | 'matrix' 125 | | 'label' 126 | | 'form' 127 | | 'simple' 128 | | 'spaceDelimited' 129 | | 'pipeDelimited' 130 | | 'deepObject'; 131 | 132 | export interface BaseParameterObject extends ISpecificationExtension { 133 | description?: string; 134 | required?: boolean; 135 | deprecated?: boolean; 136 | allowEmptyValue?: boolean; 137 | 138 | style?: ParameterStyle; // "matrix" | "label" | "form" | "simple" | "spaceDelimited" | "pipeDelimited" | "deepObject"; 139 | explode?: boolean; 140 | allowReserved?: boolean; 141 | schema?: SchemaObject | ReferenceObject; 142 | examples?: { [param: string]: ExampleObject | ReferenceObject }; 143 | example?: any; 144 | content?: ContentObject; 145 | } 146 | 147 | export interface ParameterObject extends BaseParameterObject { 148 | name: string; 149 | in: ParameterLocation; // "query" | "header" | "path" | "cookie"; 150 | } 151 | export interface RequestBodyObject extends ISpecificationExtension { 152 | description?: string; 153 | content: ContentObject; 154 | required?: boolean; 155 | } 156 | export interface ContentObject { 157 | [mediatype: string]: MediaTypeObject; 158 | } 159 | export interface MediaTypeObject extends ISpecificationExtension { 160 | schema?: SchemaObject | ReferenceObject; 161 | examples?: ExamplesObject; 162 | example?: any; 163 | encoding?: EncodingObject; 164 | } 165 | export interface EncodingObject extends ISpecificationExtension { 166 | // [property: string]: EncodingPropertyObject; 167 | [property: string]: EncodingPropertyObject | any; // Hack for allowing ISpecificationExtension 168 | } 169 | export interface EncodingPropertyObject { 170 | contentType?: string; 171 | headers?: { [key: string]: HeaderObject | ReferenceObject }; 172 | style?: string; 173 | explode?: boolean; 174 | allowReserved?: boolean; 175 | [key: string]: any; // (any) = Hack for allowing ISpecificationExtension 176 | } 177 | export interface ResponsesObject extends ISpecificationExtension { 178 | default?: ResponseObject | ReferenceObject; 179 | 180 | // [statuscode: string]: ResponseObject | ReferenceObject; 181 | [statuscode: string]: ResponseObject | ReferenceObject | any; // (any) = Hack for allowing ISpecificationExtension 182 | } 183 | export interface ResponseObject extends ISpecificationExtension { 184 | description: string; 185 | headers?: HeadersObject; 186 | content?: ContentObject; 187 | links?: LinksObject; 188 | } 189 | export interface CallbacksObject extends ISpecificationExtension { 190 | // [name: string]: CallbackObject | ReferenceObject; 191 | [name: string]: CallbackObject | ReferenceObject | any; // Hack for allowing ISpecificationExtension 192 | } 193 | export interface CallbackObject extends ISpecificationExtension { 194 | // [name: string]: PathItemObject; 195 | [name: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 196 | } 197 | export interface HeadersObject { 198 | [name: string]: HeaderObject | ReferenceObject; 199 | } 200 | export interface ExampleObject { 201 | summary?: string; 202 | description?: string; 203 | value?: any; 204 | externalValue?: string; 205 | [property: string]: any; // Hack for allowing ISpecificationExtension 206 | } 207 | export interface LinksObject { 208 | [name: string]: LinkObject | ReferenceObject; 209 | } 210 | export interface LinkObject extends ISpecificationExtension { 211 | operationRef?: string; 212 | operationId?: string; 213 | parameters?: LinkParametersObject; 214 | requestBody?: any | string; 215 | description?: string; 216 | server?: ServerObject; 217 | [property: string]: any; // Hack for allowing ISpecificationExtension 218 | } 219 | export interface LinkParametersObject { 220 | [name: string]: any | string; 221 | } 222 | 223 | export interface HeaderObject extends BaseParameterObject { 224 | $ref?: string; 225 | } 226 | export interface TagObject extends ISpecificationExtension { 227 | name: string; 228 | description?: string; 229 | externalDocs?: ExternalDocumentationObject; 230 | [extension: string]: any; // Hack for allowing ISpecificationExtension 231 | } 232 | export interface ExamplesObject { 233 | [name: string]: ExampleObject | ReferenceObject; 234 | } 235 | 236 | export interface ReferenceObject { 237 | $ref: string; 238 | } 239 | 240 | /** 241 | * A type guard to check if the given value is a `ReferenceObject`. 242 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 243 | * 244 | * @param obj The value to check. 245 | */ 246 | export function isReferenceObject(obj: any): obj is ReferenceObject { 247 | return Object.prototype.hasOwnProperty.call(obj, '$ref'); 248 | } 249 | 250 | export type SchemaObjectType = 251 | | 'integer' 252 | | 'number' 253 | | 'string' 254 | | 'boolean' 255 | | 'object' 256 | | 'null' 257 | | 'array'; 258 | 259 | export type SchemaObjectFormat = 260 | | 'int32' 261 | | 'int64' 262 | | 'float' 263 | | 'double' 264 | | 'byte' 265 | | 'binary' 266 | | 'date' 267 | | 'date-time' 268 | | 'password' 269 | | string; 270 | 271 | export interface SchemaObject extends ISpecificationExtension { 272 | nullable?: boolean; 273 | discriminator?: DiscriminatorObject; 274 | readOnly?: boolean; 275 | writeOnly?: boolean; 276 | xml?: XmlObject; 277 | externalDocs?: ExternalDocumentationObject; 278 | example?: any; 279 | examples?: any[]; 280 | deprecated?: boolean; 281 | 282 | type?: SchemaObjectType | SchemaObjectType[]; 283 | format?: SchemaObjectFormat; 284 | allOf?: (SchemaObject | ReferenceObject)[]; 285 | oneOf?: (SchemaObject | ReferenceObject)[]; 286 | anyOf?: (SchemaObject | ReferenceObject)[]; 287 | not?: SchemaObject | ReferenceObject; 288 | items?: SchemaObject | ReferenceObject; 289 | properties?: { [propertyName: string]: SchemaObject | ReferenceObject }; 290 | additionalProperties?: SchemaObject | ReferenceObject | boolean; 291 | description?: string; 292 | default?: any; 293 | 294 | title?: string; 295 | multipleOf?: number; 296 | maximum?: number; 297 | /** @desc In OpenAPI 3.0: boolean*/ 298 | exclusiveMaximum?: boolean; 299 | minimum?: number; 300 | /** @desc In OpenAPI 3.0: boolean */ 301 | exclusiveMinimum?: boolean; 302 | maxLength?: number; 303 | minLength?: number; 304 | pattern?: string; 305 | maxItems?: number; 306 | minItems?: number; 307 | uniqueItems?: boolean; 308 | maxProperties?: number; 309 | minProperties?: number; 310 | required?: string[]; 311 | enum?: any[]; 312 | } 313 | 314 | /** 315 | * A type guard to check if the given object is a `SchemaObject`. 316 | * Useful to distinguish from `ReferenceObject` values that can be used 317 | * in most places where `SchemaObject` is allowed. 318 | * 319 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 320 | * 321 | * @param schema The value to check. 322 | */ 323 | export function isSchemaObject(schema: SchemaObject | ReferenceObject): schema is SchemaObject { 324 | return !Object.prototype.hasOwnProperty.call(schema, '$ref'); 325 | } 326 | 327 | export interface SchemasObject { 328 | [schema: string]: SchemaObject; 329 | } 330 | 331 | export interface DiscriminatorObject { 332 | propertyName: string; 333 | mapping?: { [key: string]: string }; 334 | } 335 | 336 | export interface XmlObject extends ISpecificationExtension { 337 | name?: string; 338 | namespace?: string; 339 | prefix?: string; 340 | attribute?: boolean; 341 | wrapped?: boolean; 342 | } 343 | export type SecuritySchemeType = 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; 344 | 345 | export interface SecuritySchemeObject extends ISpecificationExtension { 346 | type: SecuritySchemeType; 347 | description?: string; 348 | name?: string; // required only for apiKey 349 | in?: string; // required only for apiKey 350 | scheme?: string; // required only for http 351 | bearerFormat?: string; 352 | flows?: OAuthFlowsObject; // required only for oauth2 353 | openIdConnectUrl?: string; // required only for openIdConnect 354 | } 355 | export interface OAuthFlowsObject extends ISpecificationExtension { 356 | implicit?: OAuthFlowObject; 357 | password?: OAuthFlowObject; 358 | clientCredentials?: OAuthFlowObject; 359 | authorizationCode?: OAuthFlowObject; 360 | } 361 | export interface OAuthFlowObject extends ISpecificationExtension { 362 | authorizationUrl?: string; 363 | tokenUrl?: string; 364 | refreshUrl?: string; 365 | scopes: ScopesObject; 366 | } 367 | export interface ScopesObject extends ISpecificationExtension { 368 | [scope: string]: any; // Hack for allowing ISpecificationExtension 369 | } 370 | export interface SecurityRequirementObject { 371 | [name: string]: string[]; 372 | } 373 | -------------------------------------------------------------------------------- /src/model/openapi31.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { 3 | ReferenceObject, 4 | SchemaObject, 5 | addExtension, 6 | isReferenceObject, 7 | isSchemaObject 8 | } from './openapi31'; 9 | import { IExtensionName, IExtensionType } from './specification-extension'; 10 | 11 | describe('type-guards unit tests', () => { 12 | describe('isSchemaObject()', () => { 13 | it('returns true for a schema object', () => { 14 | const schemaObject = new TestSchemaObject(); 15 | expect(isSchemaObject(schemaObject)).toBe(true); 16 | }); 17 | 18 | it('returns false for a reference object', () => { 19 | const referenceObject = new TestReferenceObject(); 20 | expect(isSchemaObject(referenceObject)).toBe(false); 21 | }); 22 | }); 23 | 24 | describe('isReferenceObject()', () => { 25 | it('returns true for a reference object', () => { 26 | const referenceObject = new TestReferenceObject(); 27 | expect(isReferenceObject(referenceObject)).toBe(true); 28 | }); 29 | 30 | it('returns false for a schema object', () => { 31 | const schemaObject = new TestSchemaObject(); 32 | expect(isReferenceObject(schemaObject)).toBe(false); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('addExtension()', () => { 38 | it('valid extension', () => { 39 | const subject = {}; 40 | addExtension(subject, 'x-extension1', 'myvalue'); 41 | expect(subject['x-extension1']).toBe('myvalue'); 42 | }); 43 | it('invalid extension', () => { 44 | const subject = {}; 45 | addExtension(subject, 'ZZ-extension1', 'myvalue'); 46 | expect(subject['ZZ-extension1']).not.toBe('myvalue'); 47 | }); 48 | }); 49 | 50 | class TestSchemaObject implements SchemaObject { 51 | [k: IExtensionName]: IExtensionType; 52 | // empty schema 53 | } 54 | 55 | class TestReferenceObject implements ReferenceObject { 56 | $ref = 'test'; 57 | } 58 | -------------------------------------------------------------------------------- /src/model/openapi31.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | // Typed interfaces for OpenAPI 3.1.0 4 | // see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md 5 | 6 | import { ServerObject } from './oas-common'; 7 | import { ISpecificationExtension, SpecificationExtension } from './specification-extension'; 8 | 9 | export * from './oas-common'; 10 | export type { ISpecificationExtension, SpecificationExtension } from './specification-extension'; 11 | 12 | export interface OpenAPIObject extends ISpecificationExtension { 13 | openapi: string; 14 | info: InfoObject; 15 | servers?: ServerObject[]; 16 | paths?: PathsObject; 17 | components?: ComponentsObject; 18 | security?: SecurityRequirementObject[]; 19 | tags?: TagObject[]; 20 | externalDocs?: ExternalDocumentationObject; 21 | /** Webhooks added in v. 3.1.0 */ 22 | webhooks?: PathsObject; 23 | } 24 | export interface InfoObject extends ISpecificationExtension { 25 | title: string; 26 | description?: string; 27 | termsOfService?: string; 28 | contact?: ContactObject; 29 | license?: LicenseObject; 30 | version: string; 31 | } 32 | export interface ContactObject extends ISpecificationExtension { 33 | name?: string; 34 | url?: string; 35 | email?: string; 36 | } 37 | export interface LicenseObject extends ISpecificationExtension { 38 | name: string; 39 | identifier?: string; 40 | url?: string; 41 | } 42 | 43 | export interface ComponentsObject extends ISpecificationExtension { 44 | schemas?: { [schema: string]: SchemaObject | ReferenceObject }; 45 | responses?: { [response: string]: ResponseObject | ReferenceObject }; 46 | parameters?: { [parameter: string]: ParameterObject | ReferenceObject }; 47 | examples?: { [example: string]: ExampleObject | ReferenceObject }; 48 | requestBodies?: { [request: string]: RequestBodyObject | ReferenceObject }; 49 | headers?: { [header: string]: HeaderObject | ReferenceObject }; 50 | securitySchemes?: { [securityScheme: string]: SecuritySchemeObject | ReferenceObject }; 51 | links?: { [link: string]: LinkObject | ReferenceObject }; 52 | callbacks?: { [callback: string]: CallbackObject | ReferenceObject }; 53 | } 54 | 55 | /** 56 | * Rename it to Paths Object to be consistent with the spec 57 | * See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#pathsObject 58 | */ 59 | export interface PathsObject extends ISpecificationExtension { 60 | // [path: string]: PathItemObject; 61 | [path: string]: PathItemObject 62 | } 63 | 64 | /** 65 | * @deprecated 66 | * Create a type alias for backward compatibility 67 | */ 68 | export type PathObject = PathsObject; 69 | 70 | export function getPath( 71 | pathsObject: PathsObject | undefined, 72 | path: string 73 | ): PathItemObject | undefined { 74 | if (SpecificationExtension.isValidExtension(path)) { 75 | return undefined; 76 | } 77 | return pathsObject ? (pathsObject[path] as PathItemObject) : undefined; 78 | } 79 | 80 | export interface PathItemObject extends ISpecificationExtension { 81 | $ref?: string; 82 | summary?: string; 83 | description?: string; 84 | get?: OperationObject; 85 | put?: OperationObject; 86 | post?: OperationObject; 87 | delete?: OperationObject; 88 | options?: OperationObject; 89 | head?: OperationObject; 90 | patch?: OperationObject; 91 | trace?: OperationObject; 92 | servers?: ServerObject[]; 93 | parameters?: (ParameterObject | ReferenceObject)[]; 94 | } 95 | export interface OperationObject extends ISpecificationExtension { 96 | tags?: string[]; 97 | summary?: string; 98 | description?: string; 99 | externalDocs?: ExternalDocumentationObject; 100 | operationId?: string; 101 | parameters?: (ParameterObject | ReferenceObject)[]; 102 | requestBody?: RequestBodyObject | ReferenceObject; 103 | responses?: ResponsesObject; 104 | callbacks?: CallbacksObject; 105 | deprecated?: boolean; 106 | security?: SecurityRequirementObject[]; 107 | servers?: ServerObject[]; 108 | } 109 | export interface ExternalDocumentationObject extends ISpecificationExtension { 110 | description?: string; 111 | url: string; 112 | } 113 | 114 | /** 115 | * The location of a parameter. 116 | * Possible values are "query", "header", "path" or "cookie". 117 | * Specification: 118 | * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations 119 | */ 120 | export type ParameterLocation = 'query' | 'header' | 'path' | 'cookie'; 121 | 122 | /** 123 | * The style of a parameter. 124 | * Describes how the parameter value will be serialized. 125 | * (serialization is not implemented yet) 126 | * Specification: 127 | * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values 128 | */ 129 | export type ParameterStyle = 130 | | 'matrix' 131 | | 'label' 132 | | 'form' 133 | | 'simple' 134 | | 'spaceDelimited' 135 | | 'pipeDelimited' 136 | | 'deepObject'; 137 | 138 | export interface BaseParameterObject extends ISpecificationExtension { 139 | description?: string; 140 | required?: boolean; 141 | deprecated?: boolean; 142 | allowEmptyValue?: boolean; 143 | 144 | style?: ParameterStyle; // "matrix" | "label" | "form" | "simple" | "spaceDelimited" | "pipeDelimited" | "deepObject"; 145 | explode?: boolean; 146 | allowReserved?: boolean; 147 | schema?: SchemaObject | ReferenceObject; 148 | examples?: { [param: string]: ExampleObject | ReferenceObject }; 149 | example?: any; 150 | content?: ContentObject; 151 | } 152 | 153 | export interface ParameterObject extends BaseParameterObject { 154 | name: string; 155 | in: ParameterLocation; // "query" | "header" | "path" | "cookie"; 156 | } 157 | export interface RequestBodyObject extends ISpecificationExtension { 158 | description?: string; 159 | content: ContentObject; 160 | required?: boolean; 161 | } 162 | export interface ContentObject { 163 | [mediatype: string]: MediaTypeObject; 164 | } 165 | export interface MediaTypeObject extends ISpecificationExtension { 166 | schema?: SchemaObject | ReferenceObject; 167 | examples?: ExamplesObject; 168 | example?: any; 169 | encoding?: EncodingObject; 170 | } 171 | export interface EncodingObject extends ISpecificationExtension { 172 | // [property: string]: EncodingPropertyObject; 173 | [property: string]: EncodingPropertyObject | any; // Hack for allowing ISpecificationExtension 174 | } 175 | export interface EncodingPropertyObject { 176 | contentType?: string; 177 | headers?: { [key: string]: HeaderObject | ReferenceObject }; 178 | style?: string; 179 | explode?: boolean; 180 | allowReserved?: boolean; 181 | [key: string]: any; // (any) = Hack for allowing ISpecificationExtension 182 | } 183 | export interface ResponsesObject extends ISpecificationExtension { 184 | default?: ResponseObject | ReferenceObject; 185 | 186 | // [statuscode: string]: ResponseObject | ReferenceObject; 187 | [statuscode: string]: ResponseObject | ReferenceObject | any; // (any) = Hack for allowing ISpecificationExtension 188 | } 189 | export interface ResponseObject extends ISpecificationExtension { 190 | description: string; 191 | headers?: HeadersObject; 192 | content?: ContentObject; 193 | links?: LinksObject; 194 | } 195 | export interface CallbacksObject extends ISpecificationExtension { 196 | // [name: string]: CallbackObject | ReferenceObject; 197 | [name: string]: CallbackObject | ReferenceObject | any; // Hack for allowing ISpecificationExtension 198 | } 199 | export interface CallbackObject extends ISpecificationExtension { 200 | // [name: string]: PathItemObject; 201 | [name: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 202 | } 203 | export interface HeadersObject { 204 | [name: string]: HeaderObject | ReferenceObject; 205 | } 206 | export interface ExampleObject { 207 | summary?: string; 208 | description?: string; 209 | value?: any; 210 | externalValue?: string; 211 | [property: string]: any; // Hack for allowing ISpecificationExtension 212 | } 213 | export interface LinksObject { 214 | [name: string]: LinkObject | ReferenceObject; 215 | } 216 | export interface LinkObject extends ISpecificationExtension { 217 | operationRef?: string; 218 | operationId?: string; 219 | parameters?: LinkParametersObject; 220 | requestBody?: any | string; 221 | description?: string; 222 | server?: ServerObject; 223 | [property: string]: any; // Hack for allowing ISpecificationExtension 224 | } 225 | export interface LinkParametersObject { 226 | [name: string]: any | string; 227 | } 228 | 229 | export interface HeaderObject extends BaseParameterObject { 230 | $ref?: string; 231 | } 232 | export interface TagObject extends ISpecificationExtension { 233 | name: string; 234 | description?: string; 235 | externalDocs?: ExternalDocumentationObject; 236 | [extension: string]: any; // Hack for allowing ISpecificationExtension 237 | } 238 | export interface ExamplesObject { 239 | [name: string]: ExampleObject | ReferenceObject; 240 | } 241 | 242 | export interface ReferenceObject { 243 | $ref: string; 244 | summary?: string; 245 | description?: string; 246 | } 247 | 248 | /** 249 | * A type guard to check if the given value is a `ReferenceObject`. 250 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 251 | * 252 | * @param obj The value to check. 253 | */ 254 | export function isReferenceObject(obj: any): obj is ReferenceObject { 255 | return Object.prototype.hasOwnProperty.call(obj, '$ref'); 256 | } 257 | 258 | export type SchemaObjectType = 259 | | 'integer' 260 | | 'number' 261 | | 'string' 262 | | 'boolean' 263 | | 'object' 264 | | 'null' 265 | | 'array'; 266 | 267 | export interface SchemaObject extends ISpecificationExtension { 268 | discriminator?: DiscriminatorObject; 269 | readOnly?: boolean; 270 | writeOnly?: boolean; 271 | xml?: XmlObject; 272 | externalDocs?: ExternalDocumentationObject; 273 | /** @deprecated use examples instead */ 274 | example?: any; 275 | examples?: any[]; 276 | deprecated?: boolean; 277 | 278 | type?: SchemaObjectType | SchemaObjectType[]; 279 | format?: 280 | | 'int32' 281 | | 'int64' 282 | | 'float' 283 | | 'double' 284 | | 'byte' 285 | | 'binary' 286 | | 'date' 287 | | 'date-time' 288 | | 'password' 289 | | string; 290 | allOf?: (SchemaObject | ReferenceObject)[]; 291 | oneOf?: (SchemaObject | ReferenceObject)[]; 292 | anyOf?: (SchemaObject | ReferenceObject)[]; 293 | not?: SchemaObject | ReferenceObject; 294 | items?: SchemaObject | ReferenceObject; 295 | properties?: { [propertyName: string]: SchemaObject | ReferenceObject }; 296 | additionalProperties?: SchemaObject | ReferenceObject | boolean; 297 | propertyNames?: SchemaObject | ReferenceObject; 298 | description?: string; 299 | default?: any; 300 | 301 | title?: string; 302 | multipleOf?: number; 303 | maximum?: number; 304 | const?: any; 305 | /** @desc In OpenAPI 3.1: number */ 306 | exclusiveMaximum?: number; 307 | minimum?: number; 308 | /** @desc In OpenAPI 3.1: number */ 309 | exclusiveMinimum?: number; 310 | maxLength?: number; 311 | minLength?: number; 312 | pattern?: string; 313 | maxItems?: number; 314 | minItems?: number; 315 | uniqueItems?: boolean; 316 | maxProperties?: number; 317 | minProperties?: number; 318 | required?: string[]; 319 | enum?: any[]; 320 | prefixItems?: (SchemaObject | ReferenceObject)[]; 321 | /** 322 | * @desc JSON Schema compliant Content-Type, optional when specified as a key of ContentObject 323 | * @example image/png 324 | */ 325 | contentMediaType?: string; 326 | /** 327 | * @desc Specifies the Content-Encoding for the schema, supports all encodings from RFC4648, and "quoted-printable" from RFC2045 328 | * @override format 329 | * @see https://datatracker.ietf.org/doc/html/rfc4648 330 | * @see https://datatracker.ietf.org/doc/html/rfc2045#section-6.7 331 | * @example base64 332 | */ 333 | contentEncoding?: string; 334 | } 335 | 336 | /** 337 | * A type guard to check if the given object is a `SchemaObject`. 338 | * Useful to distinguish from `ReferenceObject` values that can be used 339 | * in most places where `SchemaObject` is allowed. 340 | * 341 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 342 | * 343 | * @param schema The value to check. 344 | */ 345 | export function isSchemaObject(schema: SchemaObject | ReferenceObject): schema is SchemaObject { 346 | return !Object.prototype.hasOwnProperty.call(schema, '$ref'); 347 | } 348 | 349 | export interface SchemasObject { 350 | [schema: string]: SchemaObject; 351 | } 352 | 353 | export interface DiscriminatorObject { 354 | propertyName: string; 355 | mapping?: { [key: string]: string }; 356 | } 357 | 358 | export interface XmlObject extends ISpecificationExtension { 359 | name?: string; 360 | namespace?: string; 361 | prefix?: string; 362 | attribute?: boolean; 363 | wrapped?: boolean; 364 | } 365 | export type SecuritySchemeType = 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; 366 | 367 | export interface SecuritySchemeObject extends ISpecificationExtension { 368 | type: SecuritySchemeType; 369 | description?: string; 370 | name?: string; // required only for apiKey 371 | in?: string; // required only for apiKey 372 | scheme?: string; // required only for http 373 | bearerFormat?: string; 374 | flows?: OAuthFlowsObject; // required only for oauth2 375 | openIdConnectUrl?: string; // required only for openIdConnect 376 | } 377 | export interface OAuthFlowsObject extends ISpecificationExtension { 378 | implicit?: OAuthFlowObject; 379 | password?: OAuthFlowObject; 380 | clientCredentials?: OAuthFlowObject; 381 | authorizationCode?: OAuthFlowObject; 382 | } 383 | export interface OAuthFlowObject extends ISpecificationExtension { 384 | authorizationUrl?: string; 385 | tokenUrl?: string; 386 | refreshUrl?: string; 387 | scopes: ScopesObject; 388 | } 389 | export interface ScopesObject extends ISpecificationExtension { 390 | [scope: string]: any; // Hack for allowing ISpecificationExtension 391 | } 392 | export interface SecurityRequirementObject { 393 | [name: string]: string[]; 394 | } 395 | -------------------------------------------------------------------------------- /src/model/server.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { Server, ServerVariable } from './server'; 3 | 4 | describe('Server', () => { 5 | it('create server', () => { 6 | const v1 = new ServerVariable('dev', ['dev', 'qa', 'prod'], 'environment'); 7 | const sut = new Server('http://api.qa.machine.org', 'qa maquine'); 8 | sut.addVariable('environment', v1); 9 | 10 | expect(sut.url).toBe('http://api.qa.machine.org'); 11 | expect(sut.description).toBe('qa maquine'); 12 | expect(sut.variables.environment.default).toBe('dev'); 13 | }); 14 | }); 15 | 16 | describe('ServerVariable', () => { 17 | it('server var', () => { 18 | const sut = new ServerVariable('dev', ['dev', 'qa', 'prod'], 'environment'); 19 | 20 | expect(sut.default).toBe('dev'); 21 | expect(sut.description).toBe('environment'); 22 | expect(sut.enum).toStrictEqual(['dev', 'qa', 'prod']); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/model/server.ts: -------------------------------------------------------------------------------- 1 | import { ServerObject, ServerVariableObject } from './oas-common'; 2 | import { IExtensionName, IExtensionType } from './specification-extension'; 3 | 4 | // Server & Server Variable 5 | export class Server implements ServerObject { 6 | url: string; 7 | description?: string; 8 | variables: { [v: string]: ServerVariable }; 9 | [k: IExtensionName]: IExtensionType; 10 | 11 | constructor(url: string, desc?: string) { 12 | this.url = url; 13 | this.description = desc; 14 | this.variables = {}; 15 | } 16 | addVariable(name: string, variable: ServerVariable): void { 17 | this.variables[name] = variable; 18 | } 19 | } 20 | 21 | export class ServerVariable implements ServerVariableObject { 22 | enum?: string[] | boolean[] | number[]; 23 | default: string | boolean | number; 24 | description?: string; 25 | [k: IExtensionName]: IExtensionType; 26 | 27 | constructor( 28 | defaultValue: string | boolean | number, 29 | enums?: string[] | boolean[] | number[], 30 | description?: string 31 | ) { 32 | this.default = defaultValue; 33 | this.enum = enums; 34 | this.description = description; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/model/specification-extension.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { SpecificationExtension } from './specification-extension'; 3 | 4 | describe('SpecificationExtension', () => { 5 | it('addExtension() ok', () => { 6 | const sut = new SpecificationExtension(); 7 | const extensionValue = { payload: 5 }; 8 | sut.addExtension('x-name', extensionValue); 9 | 10 | expect(sut['x-name']).eql(extensionValue); 11 | }); 12 | it('addExtension() invalid', ({ expect }) => { 13 | const sut = new SpecificationExtension(); 14 | const extensionValue = { payload: 5 }; 15 | expect(() => sut.addExtension('y-name', extensionValue)).toThrow(); 16 | }); 17 | it('getExtension() ok', () => { 18 | const sut = new SpecificationExtension(); 19 | const extensionValue1 = { payload: 5 }; 20 | const extensionValue2 = { payload: 6 }; 21 | sut.addExtension('x-name', extensionValue1); 22 | sut.addExtension('x-load', extensionValue2); 23 | 24 | expect(sut.getExtension('x-name')).eql(extensionValue1); 25 | expect(sut.getExtension('x-load')).eql(extensionValue2); 26 | }); 27 | it('getExtension() invalid', ({ expect }) => { 28 | const sut = new SpecificationExtension(); 29 | expect(() => sut.getExtension('y-name')).toThrow(); 30 | }); 31 | it('getExtension() not found', () => { 32 | const sut = new SpecificationExtension(); 33 | expect(sut.getExtension('x-resource')).eql(null); 34 | }); 35 | it('listExtensions()', () => { 36 | const sut = new SpecificationExtension(); 37 | const extensionValue1 = { payload: 5 }; 38 | const extensionValue2 = { payload: 6 }; 39 | sut.addExtension('x-name', extensionValue1); 40 | sut.addExtension('x-load', extensionValue2); 41 | 42 | expect(sut.listExtensions()).eql(['x-name', 'x-load']); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/model/specification-extension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | // Support for Specification Extensions 4 | // as described in 5 | // https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md#specificationExtensions 6 | 7 | // Specification Extensions 8 | // ^x- 9 | export type IExtensionName = `x-${string}`; 10 | export type IExtensionType = any; 11 | export type ISpecificationExtension = { 12 | [extensionName: IExtensionName]: IExtensionType; 13 | }; 14 | 15 | export class SpecificationExtension implements ISpecificationExtension { 16 | [extensionName: IExtensionName]: any; 17 | 18 | static isValidExtension(extensionName: string): boolean { 19 | return /^x-/.test(extensionName); 20 | } 21 | 22 | getExtension(extensionName: string): any { 23 | if (!SpecificationExtension.isValidExtension(extensionName)) { 24 | throw new Error( 25 | `Invalid specification extension: '${extensionName}'. Extensions must start with prefix 'x-` 26 | ); 27 | } 28 | if (this[extensionName as IExtensionName]) { 29 | return this[extensionName as IExtensionName]; 30 | } 31 | return null; 32 | } 33 | addExtension(extensionName: string, payload: any): void { 34 | if (!SpecificationExtension.isValidExtension(extensionName)) { 35 | throw new Error( 36 | `Invalid specification extension: '${extensionName}'. Extensions must start with prefix 'x-` 37 | ); 38 | } 39 | this[extensionName as IExtensionName] = payload; 40 | } 41 | listExtensions(): string[] { 42 | const res: string[] = []; 43 | for (const propName in this) { 44 | if (Object.prototype.hasOwnProperty.call(this, propName)) { 45 | if (SpecificationExtension.isValidExtension(propName)) { 46 | res.push(propName); 47 | } 48 | } 49 | } 50 | return res; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/oas30.ts: -------------------------------------------------------------------------------- 1 | export * from './dsl/openapi-builder30'; 2 | export * from './model/openapi30'; 3 | export { Server, ServerVariable } from './model/server'; 4 | export type { IExtensionName, IExtensionType, ISpecificationExtension } from './model/specification-extension'; 5 | -------------------------------------------------------------------------------- /src/oas31.ts: -------------------------------------------------------------------------------- 1 | export * from './dsl/openapi-builder31'; 2 | export * from './model/openapi31'; 3 | export { Server, ServerVariable } from './model/server'; 4 | export type { IExtensionName, IExtensionType, ISpecificationExtension } from './model/specification-extension'; 5 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | -------------------------------------------------------------------------------- /test/fixture/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2020", 5 | "module": "commonjs", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/tsconfig.node16.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node16", 4 | "target": "es2020", 5 | "module": "commonjs", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/index.cjs: -------------------------------------------------------------------------------- 1 | const assert = require('node:assert'); 2 | const { oas31 } = require('openapi3-ts'); 3 | const { OpenApiBuilder } = require('openapi3-ts/oas31'); 4 | 5 | const builder1 = new oas31.OpenApiBuilder(); 6 | assert.ok(typeof builder1.rootDoc === 'object'); 7 | const builder2 = new OpenApiBuilder(); 8 | assert.ok(typeof builder2.rootDoc === 'object'); 9 | -------------------------------------------------------------------------------- /test/index.cts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { oas31 } from 'openapi3-ts'; 3 | import { OpenApiBuilder } from 'openapi3-ts/oas31'; 4 | 5 | const builder1 = new oas31.OpenApiBuilder(); 6 | assert.ok(typeof builder1.rootDoc === 'object'); 7 | const builder2 = new OpenApiBuilder(); 8 | assert.ok(typeof builder2.rootDoc === 'object'); 9 | -------------------------------------------------------------------------------- /test/index.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { oas31 } from 'openapi3-ts'; 3 | import { OpenApiBuilder } from 'openapi3-ts/oas31'; 4 | 5 | const builder1 = new oas31.OpenApiBuilder(); 6 | assert.ok(typeof builder1.rootDoc === 'object'); 7 | const builder2 = new OpenApiBuilder(); 8 | assert.ok(typeof builder2.rootDoc === 'object'); 9 | -------------------------------------------------------------------------------- /test/index.mts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { oas31 } from 'openapi3-ts'; 3 | import { OpenApiBuilder } from 'openapi3-ts/oas31'; 4 | 5 | const builder1 = new oas31.OpenApiBuilder(); 6 | assert.ok(typeof builder1.rootDoc === 'object'); 7 | const builder2 = new OpenApiBuilder(); 8 | assert.ok(typeof builder2.rootDoc === 'object'); 9 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi3-ts-test", 3 | "version": "1.0.0", 4 | "main": "index.cjs", 5 | "scripts": { 6 | "test:cjs": "node index.cjs", 7 | "test:mjs": "node index.mjs", 8 | "test:ts": "tsx --tsconfig fixture/tsconfig.node.json index.cts", 9 | "test:ts-esm": "tsx --tsconfig fixture/tsconfig.node.json index.mts", 10 | "test:ts-node16": "tsx --tsconfig fixture/tsconfig.node16.json index.cts", 11 | "test:ts-esm-node16": "tsx --tsconfig fixture/tsconfig.node16.json index.mts" 12 | }, 13 | "dependencies": { 14 | "openapi3-ts": "file:.." 15 | }, 16 | "devDependencies": { 17 | "tsx": "^4.6.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "experimentalDecorators": true, 5 | "target": "ES2020", 6 | "preserveConstEnums": true, 7 | "module": "Node16", 8 | "moduleResolution": "Node16", 9 | "noEmitHelpers": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "removeComments": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "rootDir": "src", 18 | "outDir": "dist", 19 | "types": ["vitest/globals", "node"] 20 | }, 21 | "buildOnSave": true, 22 | "compileOnSave": true, 23 | "exclude": ["**/*.spec.ts", "bin", "dist", "test", "vite.config.mts", "node_modules/**"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.d.mts: -------------------------------------------------------------------------------- 1 | declare const _default: import("vite").UserConfig; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { defineConfig } from 'vitest/config'; 3 | import TeamCityReporter from 'vitest-teamcity-reporter'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: { 9 | index: resolve(__dirname, 'src/index.ts'), 10 | oas30: resolve(__dirname, 'src/oas30.ts'), 11 | oas31: resolve(__dirname, 'src/oas31.ts') 12 | }, 13 | name: 'OpenApi3TS' 14 | }, 15 | rollupOptions: { 16 | external: ['yaml'], 17 | output: { 18 | globals: { 19 | yaml: 'yaml' 20 | } 21 | } 22 | }, 23 | sourcemap: true 24 | }, 25 | test: { 26 | environment: 'node', 27 | include: ['src/**/*.spec.ts'], 28 | watch: false, 29 | globals: true, 30 | testTimeout: 5000, 31 | isolate: false, 32 | passWithNoTests: true, 33 | reporters: ['verbose', new TeamCityReporter.default()], 34 | coverage: { 35 | reporter: ['text', 'json', 'html', 'lcov'] 36 | } 37 | } 38 | }); 39 | --------------------------------------------------------------------------------