├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.js ├── lerna.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── spec2ts.ts │ ├── package.json │ └── tsconfig.json ├── core │ ├── CHANGELOG.md │ ├── LICENSE │ ├── index.ts │ ├── lib │ │ ├── cli.ts │ │ ├── common.ts │ │ ├── declaration.ts │ │ ├── expression.ts │ │ ├── finder.ts │ │ ├── printer.ts │ │ ├── sourcefile.ts │ │ └── statement.ts │ ├── package.json │ └── tsconfig.json ├── jsonschema │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── jsonschema2ts.ts │ ├── cli │ │ ├── command.ts │ │ └── index.ts │ ├── index.ts │ ├── lib │ │ ├── core-parser.ts │ │ └── schema-parser.ts │ ├── package.json │ ├── tests │ │ ├── assets │ │ │ ├── addresses.schema.json │ │ │ ├── arrays.schema.json │ │ │ ├── composition.schema.json │ │ │ ├── const.schema.json │ │ │ ├── definitions.schema.json │ │ │ ├── extends.schema.json │ │ │ ├── formats.schema.json │ │ │ ├── geographical-location.schema.json │ │ │ ├── importdefs.schema.json │ │ │ ├── nested.schema.json │ │ │ ├── noname.schema.json │ │ │ ├── person.schema.json │ │ │ ├── person.schema.yml │ │ │ ├── persons.schema.json │ │ │ ├── tuples.schema.json │ │ │ └── union.schema.json │ │ ├── helpers.ts │ │ └── schema-parser.spec.ts │ └── tsconfig.json ├── openapi-client │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── oapi2tsclient.ts │ ├── cli │ │ ├── command.ts │ │ └── index.ts │ ├── index.ts │ ├── lib │ │ ├── core-generator.ts │ │ ├── core-parser.ts │ │ ├── openapi-generator.ts │ │ ├── server-parser.ts │ │ ├── templates │ │ │ └── _client.tpl.ts │ │ └── util.ts │ ├── package.json │ ├── tests │ │ ├── assets │ │ │ ├── petstore-expanded.yml │ │ │ └── petstore.yml │ │ ├── helpers.ts │ │ └── openapi-generator.spec.ts │ └── tsconfig.json └── openapi │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ └── oapi2ts.ts │ ├── cli │ ├── command.ts │ └── index.ts │ ├── index.ts │ ├── lib │ ├── core-parser.ts │ └── openapi-parser.ts │ ├── package.json │ ├── tests │ ├── assets │ │ ├── nested-api.yml │ │ ├── nested │ │ │ ├── nested-schemas-1.yml │ │ │ └── nested-schemas-2.yml │ │ ├── petstore-expanded.yml │ │ ├── petstore.yml │ │ └── response-ref.yml │ ├── helpers.ts │ └── openapi-parser.spec.ts │ └── tsconfig.json └── tsconfig.json /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: "@typescript-eslint/parser" 2 | parserOptions: 3 | project: 4 | - ./tsconfig.json 5 | 6 | env: 7 | node: true 8 | 9 | plugins: 10 | - "@typescript-eslint" 11 | 12 | extends: 13 | - "eslint:recommended" 14 | - "plugin:@typescript-eslint/eslint-recommended" 15 | - "plugin:@typescript-eslint/recommended" 16 | 17 | rules: 18 | "@typescript-eslint/no-explicit-any": 19 | - "off" 20 | 21 | "@typescript-eslint/no-use-before-define": 22 | - error 23 | - functions: false 24 | classes: false 25 | typedefs: false 26 | 27 | "@typescript-eslint/explicit-function-return-type": 28 | - error 29 | - allowExpressions: true 30 | 31 | "@typescript-eslint/array-type": 32 | - error 33 | - default: "array-simple" 34 | readonly: "array-simple" 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Problems and issues with code or docs 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 24 | 25 | 33 | 34 | **spec2ts version**: 35 | 36 | **Environment**: 37 | 38 | - **NodeJS** (e.g. `node -v`): 39 | - **NPM** (e.g. `npm -v`): 40 | - **Typescript** (e.g. `npx tsc -v` or globally `tsc -v`): 41 | - **Install tools**: 42 | - **Others**: 43 | 44 | **What happened**: 45 | 46 | 47 | 48 | **What you expected to happen**: 49 | 50 | 51 | 52 | **How to reproduce it**: 53 | 207 | 208 | **Anything else we need to know**: 209 | 210 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project or its docs 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | /kind feature -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## What this PR does / why we need it: 5 | 6 | 7 | 8 | ## Types of changes 9 | 10 | - [ ] Bug fix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 13 | 14 | ## Which issue/s this PR fixes 15 | 20 | 21 | ## How Has This Been Tested? 22 | 23 | 24 | 25 | 26 | ## Checklist: 27 | 28 | 29 | - [ ] My change requires a change to the documentation. 30 | - [ ] I have updated the documentation accordingly. 31 | - [ ] I've read the [CONTRIBUTION](https://github.com/touchifyapp/spec2ts/blob/master/CONTRIBUTING.md) guide 32 | - [ ] I have added tests to cover my changes. 33 | - [ ] All new and existing tests passed. 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x, 18.x, 20.x] 17 | 18 | steps: 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install Dependencies 29 | run: | 30 | npm ci 31 | 32 | - name: Run tests 33 | run: npm run test:ci 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built result 2 | *.d.ts 3 | *.js 4 | !packages/*/types/**/*.d.ts 5 | !*.config.js 6 | 7 | # Test results 8 | **/coverage/ 9 | **/tests/generated/ 10 | 11 | # Dependencies 12 | node_modules/ 13 | 14 | # Temp files 15 | *.tmp 16 | *.log 17 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at dev@touchify.co. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to spec2ts 2 | 3 | > Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). 4 | > By participating in this project you agree to abide by its terms. 5 | 6 | First, ensure you have the [latest `npm`](https://docs.npmjs.com/). 7 | 8 | To get started with the repo: 9 | 10 | ```sh 11 | $ git clone git@github.com:touchifyapp/spec2ts.git && cd spec2ts 12 | $ npm ci 13 | ``` 14 | 15 | ## Code Structure 16 | 17 | Currently, the [source](https://github.com/touchifyapp/spec2ts/tree/master) is a [lerna](https://github.com/lerna/lerna) monorepo containing the following components, located in the `packages` directory: 18 | - `core`: Helpers methods to generate TypeScript code. 19 | - `jsonschema`: Utility to generate types from JSON schemas. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/jsonschema/README.md). 20 | - `openapi`: Utility to generate types from Open API v3 specifications. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi/README.md). 21 | - `openapi-client`: Utility to generate HTTP Client from Open API v3 specifications. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi-client/README.md). 22 | - `cli`: CLI wrapper to generate types from Open API v3 specifications and JSON schemas. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/cli/README.md). 23 | 24 | ## Commands 25 | 26 | Commands are handled by npm scripts, and can be executed: 27 | - from a package directory using `npm run ` (acts on one package) 28 | - from the root directory using `npm run ` (acts on all packages) 29 | - from the root directory using `lerna run ` (acts on each packages containing this `cmd`) 30 | 31 | ### Build 32 | 33 | ```sh 34 | $ npm run build 35 | # or using lerna, prefix with `npx` or install globally 36 | $ lerna run build 37 | ``` 38 | 39 | ### Unit Tests 40 | 41 | ```sh 42 | # Run all test suites 43 | $ npm run test 44 | # or using lerna, prefix with `npx` or install globally 45 | $ lerna run test 46 | 47 | # Run a specific suite from a file (e.g. packages/openapi/tests/openapi-parser.spec.ts from root directory) 48 | $ npm run test packages/openapi/tests/openapi-parser.spec.ts 49 | 50 | # Use pattern matching 51 | $ npm run test openapi-parser 52 | 53 | # Skip cleanup and linting 54 | $ npm run test:jest 55 | # etc 56 | ``` 57 | 58 | ### Coverage 59 | 60 | If you would like to check test coverage, run the coverage command then open `coverage/lcov-report/index.html` in your favorite browser. 61 | 62 | ```sh 63 | # Generate coverage reports 64 | $ npm test:coverage 65 | 66 | # Open reports with OS X 67 | $ open coverage/lcov-report/index.html 68 | 69 | # Open reports with Linux 70 | $ xdg-open coverage/lcov-report/index.html 71 | ``` 72 | 73 | ### Lint 74 | 75 | ```sh 76 | $ npm run lint 77 | ``` 78 | 79 | It's also a good idea to hook up your editor to an eslint plugin. 80 | 81 | To fix lint errors from the command line: 82 | 83 | ```sh 84 | $ npm run lint:fix 85 | ``` 86 | 87 | ### Local CLI Testing 88 | 89 | Lerna handles dependencies between packages through symlinks so local packages are kept up to date regarding one another. 90 | To test out a development build of spec2ts or any other subcommand, simply execute the spec2ts cli using `node`. 91 | 92 | Exemple for @spec2ts/openapi specifying output location with `-o ` option: 93 | ```sh 94 | $ lerna run build 95 | $ cd packages/cli 96 | $ node ./bin/spec2ts.js openapi ../openapi/tests/assets/petstore.yml -o . 97 | 98 | # without build using ts-node, prefix with npx or install globally 99 | $ ts-node ./bin/spec2ts.ts openapi ../openapi/tests/assets/petstore.yml -o . 100 | ``` 101 | 102 | ## Issue 103 | 104 | We use (GitHub Issues)[https://github.com/touchifyapp/spec2ts/issues] for bug reports and feature requests. 105 | Before filling a new issue, try to make sure your problem doesn't already exist. If not, use the provided templates to provide a clear description. 106 | 107 | ## Pull Request 108 | 109 | This project follows [GitHub's standard forking model](https://guides.github.com/activities/forking/). Please fork the project to submit pull requests. 110 | Any new feature must be accompanied by: 111 | - a description expliciting the idea behing the feature 112 | - a documentation of the usage 113 | - valid tests covering the feature 114 | 115 | ## License 116 | By contributing to spec2ts, you agree that your contributions will be licensed under its MIT license. 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spec2ts 2 | 3 | ![CI](https://github.com/touchifyapp/spec2ts/workflows/CI/badge.svg) 4 | 5 | `spec2ts` is an utility to create TypeScript types from JSON schemas and OpenAPI v3 specifications. Unlike other code generators `spec2ts` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree (AST). 6 | 7 | ## Features 8 | 9 | * **AST-based:** Unlike other code generators `spec2ts` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree. 10 | * **Tree-shakeable:** Individually exported types allows you to bundle only the ones you actually use. 11 | * **YAML or JSON:** Use YAML or JSON for your or OpenAPI Specifications and JSON Schemas. 12 | * **External references:** Resolves automatically external references and bundle or import them in generated files. 13 | * **Implementation agnostic:** Use generated types in any projet or framework. 14 | 15 | ## Components 16 | 17 | - `@spec2ts/core`: Helpers methods to generate TypeScript code. 18 | - `@spec2ts/jsonschema`: Utility to generate types from JSON schemas. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/jsonschema/README.md). 19 | - `@spec2ts/openapi`: Utility to generate types from Open API v3 specifications. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi/README.md). 20 | - `@spec2ts/openapi-client`: Utility to generate HTTP Client from Open API v3 specifications. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi-client/README.md). 21 | - `@spec2ts/cli`: CLI wrapper to generate types from Open API v3 specifications and JSON schemas. [More details](https://github.com/touchifyapp/spec2ts/blob/master/packages/cli/README.md). 22 | 23 | ## Installation 24 | 25 | Install in your project: 26 | ```bash 27 | npm install @spec2ts/cli 28 | ``` 29 | 30 | ## CLI Usage 31 | 32 | ```bash 33 | # Generate TypeScript types from JSON Schemas 34 | spec2ts jsonschema -o path/to/output path/to/my/*.schema.json 35 | 36 | # Generate TypeScript types from Open API specification 37 | spec2ts openapi -o path/to/output path/to/my/specification.yml 38 | 39 | # Generate TypeScript HTTP Client from Open API specification 40 | spec2ts openapi-client -o path/to/output.ts path/to/my/specification.yml 41 | ``` 42 | 43 | You can find more details in the `@spec2ts` [documentation](https://github.com/touchifyapp/spec2ts/blob/master/packages/jsonschema/README.md). 44 | 45 | ## Programmatic Usage 46 | 47 | #### Generate TypeScript types from JSON Schemas 48 | 49 | ```typescript 50 | import { printer } from "@spec2ts/core"; 51 | import { parseSchema, JSONSchema } from "@spec2ts/jsonschema"; 52 | 53 | async function generateSchema(schema: JSONSchema): Promise { 54 | const result = await parseSchema(schema); 55 | return printer.printNodes(result); 56 | } 57 | ``` 58 | 59 | #### Generate TypeScript types from OpenAPI Specifications 60 | 61 | ```typescript 62 | import { printer } from "@spec2ts/core"; 63 | import { parseOpenApiFile } from "@spec2ts/openapi"; 64 | 65 | async function generateSpec(path: string): Promise { 66 | const result = await parseOpenApiFile(path); 67 | return printer.printNodes(result); 68 | } 69 | ``` 70 | 71 | #### Generate TypeScript Client types from OpenAPI Specifications 72 | 73 | ```typescript 74 | import { printer } from "@spec2ts/core"; 75 | import { generateClientFile } from "@spec2ts/openapi-client"; 76 | 77 | async function generateClient(path: string): Promise { 78 | const result = await generateClientFile(path); 79 | return printer.printNodes(result); 80 | } 81 | ``` 82 | 83 | ## Compatibility Matrix 84 | 85 | | TypeScript version | spec2ts version | 86 | |--------------------|-----------------| 87 | | v3.x.x | v1 | 88 | | v4.x.x | v2 | 89 | | v5.x.x | v3 | 90 | 91 | ## License 92 | 93 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 94 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type import("@jest/types").Config.InitialOptions */ 2 | const config = { 3 | testEnvironment: "node", 4 | testMatch: ["**/tests/**/*.spec.ts"], 5 | 6 | transform: { 7 | // eslint-disable-next-line no-undef, @typescript-eslint/no-var-requires 8 | ...require("ts-jest/presets").defaults.transform, 9 | }, 10 | }; 11 | 12 | // eslint-disable-next-line no-undef 13 | module.exports = config; -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "command": { 4 | "version": { 5 | "allowBranch": ["master", "@release/*"], 6 | "conventionalCommits": true, 7 | "message": "chore(release): publish packages", 8 | "ignoreChanges": [ 9 | "**/*.md", 10 | "**/LICENSE", 11 | "**/tests/**" 12 | ] 13 | }, 14 | "bootstrap": { 15 | "hoist": "**" 16 | } 17 | }, 18 | "packages": [ 19 | "packages/*" 20 | ] 21 | } -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": [ 7 | "build" 8 | ] 9 | } 10 | } 11 | }, 12 | "targetDefaults": {} 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spec2ts-monorepo", 3 | "version": "1.0.0-monorepo", 4 | "description": "Monorepo for utilies to convert specifications to Typescript using TypeScript native compiler", 5 | "author": "Touchify ", 6 | "private": true, 7 | "license": "MIT", 8 | "homepage": "https://github.com/touchifyapp/spec2ts#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "workspaces": [ 14 | "packages/*" 15 | ], 16 | "scripts": { 17 | "build": "lerna run build", 18 | "test": "lerna run test", 19 | "test:ci": "lerna --ci run test -- -- --ci", 20 | "lint": "lerna run lint", 21 | "clean": "lerna run clean", 22 | "prepare": "husky install" 23 | }, 24 | "devDependencies": { 25 | "@commitlint/cli": "^17.7.1", 26 | "@commitlint/config-conventional": "^17.7.0", 27 | "@types/jest": "^29.5.4", 28 | "@types/node": "^20.6.0", 29 | "@typescript-eslint/eslint-plugin": "^6.6.0", 30 | "@typescript-eslint/parser": "^6.6.0", 31 | "del-cli": "^5.1.0", 32 | "eslint": "^8.49.0", 33 | "husky": "^8.0.0", 34 | "jest": "^29.6.4", 35 | "lerna": "^7.2.0", 36 | "ts-jest": "^29.1.1", 37 | "typescript": "^5.2.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [3.0.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@3.0.4...@spec2ts/cli@3.0.5) (2025-04-10) 7 | 8 | **Note:** Version bump only for package @spec2ts/cli 9 | 10 | 11 | 12 | 13 | 14 | ## [3.0.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@3.0.3...@spec2ts/cli@3.0.4) (2023-10-23) 15 | 16 | **Note:** Version bump only for package @spec2ts/cli 17 | 18 | 19 | 20 | 21 | 22 | ## [3.0.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@3.0.2...@spec2ts/cli@3.0.3) (2023-10-03) 23 | 24 | **Note:** Version bump only for package @spec2ts/cli 25 | 26 | 27 | 28 | 29 | 30 | ## [3.0.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@3.0.1...@spec2ts/cli@3.0.2) (2023-09-25) 31 | 32 | **Note:** Version bump only for package @spec2ts/cli 33 | 34 | 35 | 36 | 37 | 38 | ## [3.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@3.0.0...@spec2ts/cli@3.0.1) (2023-09-12) 39 | 40 | **Note:** Version bump only for package @spec2ts/cli 41 | 42 | 43 | 44 | 45 | 46 | # [3.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0...@spec2ts/cli@3.0.0) (2023-09-12) 47 | 48 | 49 | ### Features 50 | 51 | * upgrade all dependencies ([9be17b6](https://github.com/touchifyapp/spec2ts/commit/9be17b69e2bd5d910bbaa88d4e2f161628fa4135)) 52 | 53 | 54 | 55 | 56 | 57 | # [2.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.13...@spec2ts/cli@2.0.0) (2023-09-07) 58 | 59 | **Note:** Version bump only for package @spec2ts/cli 60 | 61 | 62 | 63 | 64 | 65 | # [2.0.0-beta.13](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.12...@spec2ts/cli@2.0.0-beta.13) (2022-09-21) 66 | 67 | **Note:** Version bump only for package @spec2ts/cli 68 | 69 | 70 | 71 | 72 | 73 | # [2.0.0-beta.12](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.11...@spec2ts/cli@2.0.0-beta.12) (2022-08-23) 74 | 75 | **Note:** Version bump only for package @spec2ts/cli 76 | 77 | 78 | 79 | 80 | 81 | # [2.0.0-beta.11](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.10...@spec2ts/cli@2.0.0-beta.11) (2022-07-29) 82 | 83 | **Note:** Version bump only for package @spec2ts/cli 84 | 85 | 86 | 87 | 88 | 89 | # [2.0.0-beta.10](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.9...@spec2ts/cli@2.0.0-beta.10) (2022-06-27) 90 | 91 | **Note:** Version bump only for package @spec2ts/cli 92 | 93 | 94 | 95 | 96 | 97 | # [2.0.0-beta.9](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.8...@spec2ts/cli@2.0.0-beta.9) (2022-01-27) 98 | 99 | **Note:** Version bump only for package @spec2ts/cli 100 | 101 | 102 | 103 | 104 | 105 | # [2.0.0-beta.8](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.7...@spec2ts/cli@2.0.0-beta.8) (2021-11-03) 106 | 107 | **Note:** Version bump only for package @spec2ts/cli 108 | 109 | 110 | 111 | 112 | 113 | # [2.0.0-beta.7](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.6...@spec2ts/cli@2.0.0-beta.7) (2021-10-23) 114 | 115 | **Note:** Version bump only for package @spec2ts/cli 116 | 117 | 118 | 119 | 120 | 121 | # [2.0.0-beta.6](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.5...@spec2ts/cli@2.0.0-beta.6) (2021-02-27) 122 | 123 | **Note:** Version bump only for package @spec2ts/cli 124 | 125 | 126 | 127 | 128 | 129 | # [2.0.0-beta.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.4...@spec2ts/cli@2.0.0-beta.5) (2020-12-21) 130 | 131 | **Note:** Version bump only for package @spec2ts/cli 132 | 133 | 134 | 135 | 136 | 137 | # [2.0.0-beta.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.3...@spec2ts/cli@2.0.0-beta.4) (2020-12-14) 138 | 139 | **Note:** Version bump only for package @spec2ts/cli 140 | 141 | 142 | 143 | 144 | 145 | # [2.0.0-beta.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.2...@spec2ts/cli@2.0.0-beta.3) (2020-11-29) 146 | 147 | **Note:** Version bump only for package @spec2ts/cli 148 | 149 | 150 | 151 | 152 | 153 | # [2.0.0-beta.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.1...@spec2ts/cli@2.0.0-beta.2) (2020-11-29) 154 | 155 | **Note:** Version bump only for package @spec2ts/cli 156 | 157 | 158 | 159 | 160 | 161 | # [2.0.0-beta.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@2.0.0-beta.0...@spec2ts/cli@2.0.0-beta.1) (2020-11-16) 162 | 163 | **Note:** Version bump only for package @spec2ts/cli 164 | 165 | 166 | 167 | 168 | 169 | # [2.0.0-beta.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.12...@spec2ts/cli@2.0.0-beta.0) (2020-11-13) 170 | 171 | 172 | ### Features 173 | 174 | * **global:** upgrade typescript 4 ([#16](https://github.com/touchifyapp/spec2ts/issues/16)) ([fcd82be](https://github.com/touchifyapp/spec2ts/commit/fcd82be93be3986a2f723680f1c52818eb7ba1bc)) 175 | 176 | 177 | ### BREAKING CHANGES 178 | 179 | * **global:** use typescript v4 180 | * **global:** updates are now immutable 181 | 182 | 183 | 184 | 185 | 186 | ## [1.1.12](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.11...@spec2ts/cli@1.1.12) (2020-10-26) 187 | 188 | **Note:** Version bump only for package @spec2ts/cli 189 | 190 | 191 | 192 | 193 | 194 | ## [1.1.11](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.10...@spec2ts/cli@1.1.11) (2020-10-26) 195 | 196 | **Note:** Version bump only for package @spec2ts/cli 197 | 198 | 199 | 200 | 201 | 202 | ## [1.1.10](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.9...@spec2ts/cli@1.1.10) (2020-10-06) 203 | 204 | **Note:** Version bump only for package @spec2ts/cli 205 | 206 | 207 | 208 | 209 | 210 | ## [1.1.9](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.8...@spec2ts/cli@1.1.9) (2020-07-08) 211 | 212 | **Note:** Version bump only for package @spec2ts/cli 213 | 214 | 215 | 216 | 217 | 218 | ## [1.1.8](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.7...@spec2ts/cli@1.1.8) (2020-06-01) 219 | 220 | **Note:** Version bump only for package @spec2ts/cli 221 | 222 | 223 | 224 | 225 | 226 | ## [1.1.7](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.6...@spec2ts/cli@1.1.7) (2020-05-27) 227 | 228 | **Note:** Version bump only for package @spec2ts/cli 229 | 230 | 231 | 232 | 233 | 234 | ## [1.1.6](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.5...@spec2ts/cli@1.1.6) (2020-05-24) 235 | 236 | **Note:** Version bump only for package @spec2ts/cli 237 | 238 | 239 | 240 | 241 | 242 | ## [1.1.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.4...@spec2ts/cli@1.1.5) (2020-05-24) 243 | 244 | **Note:** Version bump only for package @spec2ts/cli 245 | 246 | 247 | 248 | 249 | 250 | ## [1.1.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.3...@spec2ts/cli@1.1.4) (2020-05-24) 251 | 252 | **Note:** Version bump only for package @spec2ts/cli 253 | 254 | 255 | 256 | 257 | 258 | ## [1.1.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.2...@spec2ts/cli@1.1.3) (2020-05-22) 259 | 260 | **Note:** Version bump only for package @spec2ts/cli 261 | 262 | 263 | 264 | 265 | 266 | ## [1.1.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.1...@spec2ts/cli@1.1.2) (2020-05-18) 267 | 268 | **Note:** Version bump only for package @spec2ts/cli 269 | 270 | 271 | 272 | 273 | 274 | ## [1.1.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.1.0...@spec2ts/cli@1.1.1) (2020-05-18) 275 | 276 | **Note:** Version bump only for package @spec2ts/cli 277 | 278 | 279 | 280 | 281 | 282 | # [1.1.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.0.3...@spec2ts/cli@1.1.0) (2020-05-18) 283 | 284 | 285 | ### Features 286 | 287 | * **cli:** add openapi-client cli ([7c9814c](https://github.com/touchifyapp/spec2ts/commit/7c9814c9ce0be84c14b1c6c8d86834fea301d005)) 288 | 289 | 290 | 291 | 292 | 293 | ## [1.0.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.0.2...@spec2ts/cli@1.0.3) (2020-05-16) 294 | 295 | **Note:** Version bump only for package @spec2ts/cli 296 | 297 | 298 | 299 | 300 | 301 | ## [1.0.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.0.1...@spec2ts/cli@1.0.2) (2020-04-27) 302 | 303 | **Note:** Version bump only for package @spec2ts/cli 304 | 305 | 306 | 307 | 308 | 309 | ## [1.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/cli@1.0.0...@spec2ts/cli@1.0.1) (2020-04-27) 310 | 311 | **Note:** Version bump only for package @spec2ts/cli 312 | 313 | 314 | 315 | 316 | 317 | # 1.0.0 (2020-04-24) 318 | 319 | 320 | ### Bug Fixes 321 | 322 | * **release:** fix lerna publish ([fe36558](https://github.com/touchifyapp/spec2ts/commit/fe36558a1a2742e2e3d99aa08061ab9be0cf03f2)) 323 | 324 | 325 | ### Code Refactoring 326 | 327 | * **global:** improve project architecture ([2b2c0a1](https://github.com/touchifyapp/spec2ts/commit/2b2c0a1d98b78457520fff2c116b7f8d0e5c5df5)) 328 | 329 | 330 | ### Features 331 | 332 | * **cli:** add global cli to repo ([5f85dfc](https://github.com/touchifyapp/spec2ts/commit/5f85dfc8762bc10b35411b7d629efa50b122bdb6)) 333 | 334 | 335 | ### BREAKING CHANGES 336 | 337 | * **global:** force v1 338 | -------------------------------------------------------------------------------- /packages/cli/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @spec2ts/cli 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@spec2ts/cli.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/cli) 4 | [![NPM download](https://img.shields.io/npm/dm/@spec2ts/cli.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/cli) 5 | [![Build Status](https://travis-ci.org/touchifyapp/spec2ts.svg?branch=master)](https://travis-ci.org/touchifyapp/spec2ts) 6 | 7 | `@spec2ts/cli` is an utility to create TypeScript types from JSON schemas or OpenAPI v3 specifications. Unlike other code generators `@spec2ts/cli` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree (AST). 8 | 9 | ## Features 10 | 11 | * **AST-based:** Unlike other code generators `@spec2ts/cli` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree. 12 | * **Tree-shakeable:** Individually exported types allows you to bundle only the ones you actually use. 13 | * **YAML or JSON:** Use YAML or JSON for your specifications. 14 | * **External references:** Resolves automatically external references and bundle or import them in generated files. 15 | * **Implementation agnostic:** Use generated types in any projet or framework. 16 | 17 | ## Installation 18 | 19 | Install in your project: 20 | ```bash 21 | npm install @spec2ts/cli 22 | ``` 23 | 24 | ## CLI Usage 25 | 26 | #### JSON Schema command 27 | 28 | ```bash 29 | spec2ts jsonschema [options] 30 | 31 | Generate TypeScript types from JSON Schemas 32 | 33 | Positionals: 34 | input Path to JSON Schema(s) to convert to TypeScript [string] 35 | 36 | Options: 37 | --version Show version number [boolean] 38 | --help Show help usage [boolean] 39 | --output, -o Output directory for generated types [string] 40 | --cwd, -c Root directory for resolving $refs [string] 41 | --avoidAny Avoid the `any` type and use `unknown` instead [boolean] 42 | --enableDate Build `Date` for format `date` and `date-time` [boolean] 43 | --banner, -b Comment prepended to the top of each generated file [string] 44 | ``` 45 | 46 | #### OpenAPI command 47 | 48 | ```bash 49 | spec2ts openapi [options] 50 | 51 | Generate TypeScript types from OpenAPI specification 52 | 53 | Positionals: 54 | input Path to OpenAPI Specification(s) to convert to TypeScript [string] 55 | 56 | Options: 57 | --version Show version number [boolean] 58 | --help Show help usage [boolean] 59 | --output, -o Output directory for generated types [string] 60 | --cwd, -c Root directory for resolving $refs [string] 61 | --avoidAny Avoid the `any` type and use `unknown` instead [boolean] 62 | --enableDate Build `Date` for format `date` and `date-time` [boolean] 63 | --banner, -b Comment prepended to the top of each generated file [string] 64 | ``` 65 | 66 | ## Implementations 67 | 68 | - **JSON Schema:** See `@spec2ts/jsonschema`'s [documentation](https://github.com/touchifyapp/spec2ts/blob/master/packages/jsonschema/README.md). 69 | - **Open API Specification:** See `@spec2ts/openapi`'s [documentation](https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi/README.md). 70 | 71 | ## Compatibility Matrix 72 | 73 | | TypeScript version | spec2ts version | 74 | |--------------------|-----------------| 75 | | v3.x.x | v1 | 76 | | v4.x.x | v2 | 77 | 78 | ## License 79 | 80 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 81 | -------------------------------------------------------------------------------- /packages/cli/bin/spec2ts.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as yargs from "yargs"; 4 | 5 | import * as jsonschema from "@spec2ts/jsonschema/cli/command"; 6 | import * as openapi from "@spec2ts/openapi/cli/command"; 7 | import * as openapiClient from "@spec2ts/openapi-client/cli/command"; 8 | 9 | yargs 10 | .command( 11 | jsonschema.usage.replace("$0", "jsonschema"), 12 | jsonschema.describe, 13 | jsonschema.builder, 14 | jsonschema.handler 15 | ) 16 | 17 | .command( 18 | openapi.usage.replace("$0", "openapi"), 19 | openapi.describe, 20 | openapi.builder, 21 | openapi.handler 22 | ) 23 | 24 | .command( 25 | openapiClient.usage.replace("$0", "openapi-client"), 26 | openapiClient.describe, 27 | openapiClient.builder, 28 | openapiClient.handler 29 | ) 30 | 31 | .help("help", "Show help usage") 32 | .demandCommand() 33 | 34 | .argv; 35 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spec2ts/cli", 3 | "version": "3.0.5", 4 | "description": "Utility to convert specifications (Open API, JSON Schemas) to TypeScript using TypeScript native compiler", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/touchifyapp/spec2ts/blob/master/packages/cli#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "bin": { 17 | "spec2ts": "./bin/spec2ts.js" 18 | }, 19 | "files": [ 20 | "*.js", 21 | "*.d.ts", 22 | "bin/**/*.js", 23 | "bin/**/*.d.ts" 24 | ], 25 | "scripts": { 26 | "build": "npm run clean && npm run lint && npm run build:ts", 27 | "build:ts": "tsc -p .", 28 | "test": "npm run clean && npm run lint && npm run test:jest", 29 | "test:jest": "jest -c ../../jest.config.js --rootDir . --passWithNoTests", 30 | "test:coverage": "npm run test -- -- --coverage", 31 | "lint": "npm run lint:ts", 32 | "lint:ts": "eslint 'bin/**/*.ts'", 33 | "lint:fix": "npm run lint -- -- --fix", 34 | "clean": "npm run clean:ts", 35 | "clean:ts": "del '*.{js,d.ts}' 'bin/**/*.{js,d.ts}'", 36 | "prepublishOnly": "npm test && npm run build" 37 | }, 38 | "dependencies": { 39 | "@spec2ts/jsonschema": "^3.0.5", 40 | "@spec2ts/openapi": "^3.1.3", 41 | "@spec2ts/openapi-client": "^3.1.3" 42 | }, 43 | "devDependencies": { 44 | "del-cli": "^5.1.0", 45 | "eslint": "^8.49.0", 46 | "jest": "^29.6.4", 47 | "typescript": "^5.2.2" 48 | }, 49 | "keywords": [ 50 | "spec", 51 | "jsonschema", 52 | "json", 53 | "schema", 54 | "openapi", 55 | "openapiv3", 56 | "typescript", 57 | "compile", 58 | "compiler", 59 | "client", 60 | "http", 61 | "rest", 62 | "ast", 63 | "transpile", 64 | "interface", 65 | "typing", 66 | "spec2ts", 67 | "share" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [3.0.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@3.0.1...@spec2ts/core@3.0.2) (2023-10-03) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * move typescript to runtime dependencies ([6e17d21](https://github.com/touchifyapp/spec2ts/commit/6e17d21187ee6d8af226a92595d6c93df04db2ea)) 12 | 13 | 14 | 15 | 16 | 17 | ## [3.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@3.0.0...@spec2ts/core@3.0.1) (2023-09-25) 18 | 19 | **Note:** Version bump only for package @spec2ts/core 20 | 21 | 22 | 23 | 24 | 25 | # [3.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@2.0.0...@spec2ts/core@3.0.0) (2023-09-12) 26 | 27 | 28 | * feat!: migrate to typescript v5 ([84d2bb0](https://github.com/touchifyapp/spec2ts/commit/84d2bb0719f63dbff334bf8ba83b89501e18aeff)) 29 | 30 | 31 | ### Features 32 | 33 | * upgrade all dependencies ([9be17b6](https://github.com/touchifyapp/spec2ts/commit/9be17b69e2bd5d910bbaa88d4e2f161628fa4135)) 34 | 35 | 36 | ### BREAKING CHANGES 37 | 38 | * removed all decorators, use modifiers instead as specified by API v5 of Typescript compiler 39 | 40 | 41 | 42 | 43 | 44 | # [2.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@2.0.0-beta.3...@spec2ts/core@2.0.0) (2023-09-07) 45 | 46 | **Note:** Version bump only for package @spec2ts/core 47 | 48 | 49 | 50 | 51 | 52 | # [2.0.0-beta.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@2.0.0-beta.2...@spec2ts/core@2.0.0-beta.3) (2022-09-21) 53 | 54 | **Note:** Version bump only for package @spec2ts/core 55 | 56 | 57 | 58 | 59 | 60 | # [2.0.0-beta.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@2.0.0-beta.1...@spec2ts/core@2.0.0-beta.2) (2022-01-27) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * make imports works with typescript 4.5 ([78f54b8](https://github.com/touchifyapp/spec2ts/commit/78f54b81a6ccfaff42dbbe640ffbd1afbc41f8bb)) 66 | 67 | 68 | 69 | 70 | 71 | # [2.0.0-beta.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@2.0.0-beta.0...@spec2ts/core@2.0.0-beta.1) (2021-10-23) 72 | 73 | 74 | ### Features 75 | 76 | * allow --ext option to specify output extension ([4c70ca1](https://github.com/touchifyapp/spec2ts/commit/4c70ca13f3fc12ce1fd16c0430c7f90f90b0ed64)) 77 | 78 | 79 | 80 | 81 | 82 | # [2.0.0-beta.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.2.2...@spec2ts/core@2.0.0-beta.0) (2020-11-13) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * core avoid any ([af8e7ef](https://github.com/touchifyapp/spec2ts/commit/af8e7efe9e073e07f98c6962e94cef6cbe98212e)) 88 | 89 | 90 | ### Features 91 | 92 | * **global:** upgrade typescript 4 ([#16](https://github.com/touchifyapp/spec2ts/issues/16)) ([fcd82be](https://github.com/touchifyapp/spec2ts/commit/fcd82be93be3986a2f723680f1c52818eb7ba1bc)) 93 | 94 | 95 | ### BREAKING CHANGES 96 | 97 | * **global:** use typescript v4 98 | * **global:** updates are now immutable 99 | 100 | 101 | 102 | 103 | 104 | ## [1.2.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.2.1...@spec2ts/core@1.2.2) (2020-10-26) 105 | 106 | **Note:** Version bump only for package @spec2ts/core 107 | 108 | 109 | 110 | 111 | 112 | ## [1.2.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.2.0...@spec2ts/core@1.2.1) (2020-05-27) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * **jsonschema:** improve external references parsing ([23f3868](https://github.com/touchifyapp/spec2ts/commit/23f3868980a78ad880237dfdff829e7b3e5a4d6e)), closes [#1](https://github.com/touchifyapp/spec2ts/issues/1) 118 | 119 | 120 | 121 | 122 | 123 | # [1.2.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.1.0...@spec2ts/core@1.2.0) (2020-05-24) 124 | 125 | 126 | ### Features 127 | 128 | * **core:** add new imports utils ([f8329b8](https://github.com/touchifyapp/spec2ts/commit/f8329b8772ea5b7dcfde5ec28830a921223eb8bf)) 129 | 130 | 131 | 132 | 133 | 134 | # [1.1.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.0.1...@spec2ts/core@1.1.0) (2020-05-18) 135 | 136 | 137 | ### Features 138 | 139 | * **core:** add new methods : ([f4dfc6d](https://github.com/touchifyapp/spec2ts/commit/f4dfc6d98848cc95512b38c1aea5c9fb016e275a)) 140 | * **core:** improve type generation ([7abd848](https://github.com/touchifyapp/spec2ts/commit/7abd84800ce27d81a7868d4ec0a67f28bf26b355)) 141 | 142 | 143 | 144 | 145 | 146 | ## [1.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/core@1.0.0...@spec2ts/core@1.0.1) (2020-05-16) 147 | 148 | **Note:** Version bump only for package @spec2ts/core 149 | 150 | 151 | 152 | 153 | 154 | # 1.0.0 (2020-04-24) 155 | 156 | 157 | ### Bug Fixes 158 | 159 | * **release:** fix lerna publish ([fe36558](https://github.com/touchifyapp/spec2ts/commit/fe36558a1a2742e2e3d99aa08061ab9be0cf03f2)) 160 | 161 | 162 | ### Code Refactoring 163 | 164 | * **jsonschema:** create lib base structure ([7365f94](https://github.com/touchifyapp/spec2ts/commit/7365f94ae0d32a3ef427dce02891c602f98a5edc)) 165 | 166 | 167 | ### Features 168 | 169 | * **jsonschema:** add cli command ([7592c43](https://github.com/touchifyapp/spec2ts/commit/7592c439be99fabb97cc270aa7a09794ee86f738)) 170 | * **openapi:** add openapi parser to monorepo ([e9ca537](https://github.com/touchifyapp/spec2ts/commit/e9ca5375e2692f909d32eacae653f918cd348040)) 171 | 172 | 173 | * refactor(core)!: refactor core package ([1e59d55](https://github.com/touchifyapp/spec2ts/commit/1e59d55ec6342cd56510876f9f31a948bb1f272b)) 174 | 175 | 176 | ### BREAKING CHANGES 177 | 178 | * **jsonschema:** no more printer in library 179 | * methods does not throw 180 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/common"; 2 | export * from "./lib/declaration"; 3 | export * from "./lib/expression"; 4 | export * from "./lib/finder"; 5 | export * from "./lib/sourcefile"; 6 | export * from "./lib/statement"; 7 | 8 | export * as cli from "./lib/cli"; 9 | export * as printer from "./lib/printer"; 10 | -------------------------------------------------------------------------------- /packages/core/lib/cli.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import * as path from "path"; 3 | import { glob, type GlobOptions } from "glob"; 4 | 5 | export function writeFile(path: string, content: string): Promise { 6 | return fs.writeFile( 7 | path, 8 | content, 9 | { encoding: "utf8" } 10 | ); 11 | } 12 | 13 | export async function mkdirp(file: string): Promise { 14 | await fs.mkdir(path.dirname(file), { recursive: true }); 15 | } 16 | 17 | export function getOutputPath(src: string, { output, ext }: { output?: string, ext?: string }): string { 18 | if (output) { 19 | return path.join( 20 | output, 21 | getOutputFileName(src) 22 | ); 23 | } 24 | 25 | if (src.startsWith("http")) { 26 | return path.basename(src); 27 | } 28 | 29 | return path.join( 30 | path.dirname(src), 31 | getOutputFileName(src, ext) 32 | ); 33 | } 34 | 35 | export function getOutputFileName(src: string, ext = ".d.ts"): string { 36 | return path.basename(src) 37 | .replace(path.extname(src), "") 38 | + ext; 39 | } 40 | 41 | export async function findFiles(pattern: string | string[], options?: GlobOptions): Promise { 42 | if (!Array.isArray(pattern)) { 43 | return findFilesOne(pattern, options); 44 | } 45 | 46 | const res = await Promise.all(pattern.map(p => findFilesOne(p, options))); 47 | return res.flat(); 48 | } 49 | 50 | async function findFilesOne(pattern: string, options: GlobOptions = {}): Promise { 51 | if (pattern.startsWith("http")) { 52 | return [pattern]; 53 | } 54 | 55 | return await glob(pattern, { ...options, withFileTypes: false }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/lib/common.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | export type KeywordTypeName = "any" | "number" | "object" | "string" | "boolean" | "bigint" | "symbol" | "this" | "void" | "unknown" | "undefined" | "null" | "never"; 4 | 5 | export const questionToken = ts.factory.createToken(ts.SyntaxKind.QuestionToken); 6 | export const questionDotToken = ts.factory.createToken(ts.SyntaxKind.QuestionDotToken); 7 | 8 | export const keywordType: Record = { 9 | any: ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), 10 | number: ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), 11 | object: ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword), 12 | string: ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), 13 | boolean: ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), 14 | bigint: ts.factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword), 15 | symbol: ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword), 16 | this: ts.factory.createThisTypeNode(), 17 | void: ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), 18 | unknown: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 19 | undefined: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), 20 | null: ts.factory.createLiteralTypeNode(ts.factory.createNull()), 21 | never: ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) 22 | }; 23 | 24 | export const modifier: Record = { 25 | async: ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword), 26 | export: ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) 27 | }; 28 | 29 | export function getName(name: ts.Node): string | ts.__String { 30 | if (ts.isIdentifier(name)) { 31 | return name.escapedText; 32 | } 33 | if (ts.isLiteralExpression(name)) { 34 | return name.text; 35 | } 36 | return ""; 37 | } 38 | 39 | export function getString(expr: ts.Expression): string { 40 | if (ts.isIdentifier(expr)) { 41 | return expr.escapedText.toString(); 42 | } 43 | if (ts.isLiteralExpression(expr)) { 44 | return expr.text; 45 | } 46 | return ""; 47 | } 48 | 49 | export function createQuestionToken(token?: boolean | ts.QuestionToken): ts.QuestionToken | undefined { 50 | if (!token) return undefined; 51 | if (token === true) return questionToken; 52 | return token; 53 | } 54 | 55 | export function createKeywordType(type: KeywordTypeName): ts.TypeNode { 56 | return keywordType[type]; 57 | } 58 | 59 | export function appendNodes( 60 | array: ts.NodeArray, 61 | ...nodes: T[] 62 | ): ts.NodeArray { 63 | return ts.factory.createNodeArray([...array, ...nodes]); 64 | } 65 | 66 | export function replaceNode(array: ts.NodeArray, oldNode: T, newNode: T): ts.NodeArray { 67 | const i = array.indexOf(oldNode); 68 | if (i === -1) return array; 69 | 70 | return ts.factory.createNodeArray([ 71 | ...array.slice(0, i), 72 | newNode, 73 | ...array.slice(i + 1) 74 | ]); 75 | } 76 | 77 | export function block(...statements: ts.Statement[]): ts.Block { 78 | return ts.factory.createBlock(statements, true); 79 | } 80 | 81 | export function isKeywordTypeName(type: string): type is KeywordTypeName { 82 | return type in keywordType; 83 | } 84 | 85 | export function isKeywordTypeNode(node?: ts.Node): node is ts.KeywordTypeNode { 86 | if (!node) return false; 87 | 88 | return node.kind === ts.SyntaxKind.AnyKeyword || 89 | node.kind === ts.SyntaxKind.UnknownKeyword || 90 | node.kind === ts.SyntaxKind.NumberKeyword || 91 | node.kind === ts.SyntaxKind.BigIntKeyword || 92 | node.kind === ts.SyntaxKind.ObjectKeyword || 93 | node.kind === ts.SyntaxKind.BooleanKeyword || 94 | node.kind === ts.SyntaxKind.StringKeyword || 95 | node.kind === ts.SyntaxKind.SymbolKeyword || 96 | node.kind === ts.SyntaxKind.ThisKeyword || 97 | node.kind === ts.SyntaxKind.VoidKeyword || 98 | node.kind === ts.SyntaxKind.UndefinedKeyword || 99 | node.kind === ts.SyntaxKind.NullKeyword || 100 | node.kind === ts.SyntaxKind.NeverKeyword; 101 | } 102 | -------------------------------------------------------------------------------- /packages/core/lib/expression.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | import { getName, appendNodes } from "./common"; 4 | import { createPropertyAssignment } from "./declaration"; 5 | 6 | export function toExpression(ex: ts.Expression | string): ts.Expression; 7 | export function toExpression(ex: ts.Expression | string | undefined): ts.Expression | undefined; 8 | export function toExpression(ex: ts.Expression | string | undefined): ts.Expression | undefined { 9 | if (typeof ex === "string") return ts.factory.createIdentifier(ex); 10 | return ex; 11 | } 12 | 13 | export function toIdentifier(ex: ts.Identifier | string): ts.Identifier; 14 | export function toIdentifier(ex: ts.Identifier | string | undefined): ts.Identifier | undefined; 15 | export function toIdentifier(ex: ts.Identifier | string | undefined): ts.Identifier | undefined { 16 | if (typeof ex === "string") return ts.factory.createIdentifier(ex); 17 | return ex; 18 | } 19 | 20 | export function toLiteral(ex: ts.Expression | string | number | bigint): ts.Expression; 21 | export function toLiteral(ex: ts.Expression | string | number | bigint | undefined): ts.Expression | undefined; 22 | export function toLiteral(ex: ts.Expression | string | number | bigint | undefined): ts.Expression | undefined { 23 | if (ex === "true") return ts.factory.createTrue(); 24 | if (ex === "false") return ts.factory.createFalse(); 25 | if (typeof ex === "string") return ts.factory.createStringLiteral(ex); 26 | if (typeof ex === "number") return ts.factory.createNumericLiteral(ex); 27 | if (typeof ex === "bigint") return ts.factory.createBigIntLiteral(ex.toString()); 28 | return ex; 29 | } 30 | 31 | export function toPropertyName(ex: ts.PropertyName | string): ts.PropertyName; 32 | export function toPropertyName(ex: ts.PropertyName | string | undefined): ts.PropertyName | undefined; 33 | export function toPropertyName(name: ts.PropertyName | string | undefined): ts.PropertyName | undefined { 34 | if (typeof name === "string") { 35 | return isValidIdentifier(name) 36 | ? ts.factory.createIdentifier(name) 37 | : ts.factory.createStringLiteral(name); 38 | } 39 | return name; 40 | } 41 | 42 | export function isValidIdentifier(str: string): boolean { 43 | if (!str.length || str.trim() !== str) return false; 44 | const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest); 45 | return ( 46 | !!node && 47 | node.kind === ts.SyntaxKind.Identifier && 48 | !ts.identifierToKeywordKind(node) 49 | ); 50 | } 51 | 52 | export function isIdentifier(n: unknown | null | undefined): n is ts.Identifier { 53 | return !!n && ts.isIdentifier(n as ts.Node); 54 | } 55 | 56 | export function createCall( 57 | expression: ts.Expression | string, 58 | { 59 | typeArgs, 60 | args 61 | }: { 62 | typeArgs?: ts.TypeNode[]; 63 | args?: ts.Expression[]; 64 | } = {} 65 | ): ts.CallExpression { 66 | return ts.factory.createCallExpression(toExpression(expression), typeArgs, args); 67 | } 68 | 69 | export function createMethodCall( 70 | method: string, 71 | opts: { 72 | typeArgs?: ts.TypeNode[]; 73 | args?: ts.Expression[]; 74 | } 75 | ): ts.CallExpression { 76 | return createCall(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), method), opts); 77 | } 78 | 79 | export function createTemplateString( 80 | head: string, 81 | spans: Array<{ literal: string; expression: ts.Expression }> 82 | ): ts.Expression { 83 | if (!spans.length) { 84 | return ts.factory.createStringLiteral(head); 85 | } 86 | 87 | return ts.factory.createTemplateExpression( 88 | ts.factory.createTemplateHead(head), 89 | spans.map(({ expression, literal }, i) => 90 | ts.factory.createTemplateSpan( 91 | expression, 92 | i === spans.length - 1 93 | ? ts.factory.createTemplateTail(literal) 94 | : ts.factory.createTemplateMiddle(literal) 95 | ) 96 | ) 97 | ); 98 | } 99 | 100 | export function createObjectLiteral(props: Array<[string, string | ts.Expression]>): ts.ObjectLiteralExpression { 101 | return ts.factory.createObjectLiteralExpression( 102 | props.map(([name, identifier]) => 103 | createPropertyAssignment(name, toExpression(identifier)) 104 | ), 105 | true 106 | ); 107 | } 108 | 109 | export function createArrowFunction( 110 | parameters: ts.ParameterDeclaration[], 111 | body: ts.ConciseBody, 112 | { 113 | modifiers, 114 | typeParameters, 115 | type, 116 | equalsGreaterThanToken 117 | }: { 118 | modifiers?: ts.Modifier[]; 119 | typeParameters?: ts.TypeParameterDeclaration[]; 120 | type?: ts.TypeNode; 121 | equalsGreaterThanToken?: ts.EqualsGreaterThanToken; 122 | } = {} 123 | ): ts.ArrowFunction { 124 | return ts.factory.createArrowFunction( 125 | modifiers, 126 | typeParameters, 127 | parameters, 128 | type, 129 | equalsGreaterThanToken, 130 | body 131 | ); 132 | } 133 | 134 | export function createObjectBinding( 135 | elements: Array<{ 136 | name: string | ts.BindingName; 137 | dotDotDotToken?: ts.DotDotDotToken; 138 | propertyName?: string | ts.PropertyName; 139 | initializer?: ts.Expression; 140 | }> 141 | ): ts.ObjectBindingPattern { 142 | return ts.factory.createObjectBindingPattern( 143 | elements.map(({ dotDotDotToken, propertyName, name, initializer }) => 144 | ts.factory.createBindingElement(dotDotDotToken, propertyName, name, initializer) 145 | ) 146 | ); 147 | } 148 | 149 | export function changePropertyValue( 150 | o: ts.ObjectLiteralExpression, 151 | property: string, 152 | value: ts.Expression 153 | ): ts.ObjectLiteralExpression { 154 | const i = o.properties.findIndex( 155 | p => ts.isPropertyAssignment(p) && getName(p.name) === property 156 | ); 157 | 158 | if (i === -1) { 159 | throw new Error(`No such property: ${property}`); 160 | } 161 | 162 | const p = o.properties[i]; 163 | if (!ts.isPropertyAssignment(p)) { 164 | throw new Error(`Invalid node: ${property}`); 165 | } 166 | 167 | return ts.factory.updateObjectLiteralExpression(o, [ 168 | ...o.properties.slice(0, i), 169 | ts.factory.updatePropertyAssignment(p, p.name, value), 170 | ...o.properties.slice(i + 1) 171 | ]); 172 | } 173 | 174 | export function upsertPropertyValue( 175 | o: ts.ObjectLiteralExpression, 176 | property: string, 177 | value: ts.Expression 178 | ): ts.ObjectLiteralExpression { 179 | const i = o.properties.findIndex( 180 | p => ts.isPropertyAssignment(p) && getName(p.name) === property 181 | ); 182 | 183 | if (i === -1) { 184 | return ts.factory.updateObjectLiteralExpression(o, appendNodes( 185 | o.properties, 186 | ts.factory.createPropertyAssignment(property, value) 187 | )); 188 | } 189 | 190 | const p = o.properties[i]; 191 | if (!ts.isPropertyAssignment(p)) { 192 | throw new Error(`Invalid node: ${property}`); 193 | } 194 | 195 | return ts.factory.updateObjectLiteralExpression(o, [ 196 | ...o.properties.slice(0, i), 197 | ts.factory.updatePropertyAssignment(p, p.name, value), 198 | ...o.properties.slice(i + 1) 199 | ]); 200 | } 201 | 202 | export function addComment(node: T, comment?: string): T { 203 | if (!comment) return node; 204 | return ts.addSyntheticLeadingComment( 205 | node, 206 | ts.SyntaxKind.MultiLineCommentTrivia, 207 | `*\n * ${comment.replace(/\n/g, "\n * ")}\n `, 208 | true 209 | ); 210 | } 211 | -------------------------------------------------------------------------------- /packages/core/lib/finder.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { getName } from "./common"; 3 | 4 | export function findNode( 5 | nodes: ts.NodeArray, 6 | kind: T extends { kind: infer K } ? K : never, 7 | test?: (node: T) => boolean | undefined 8 | ): T | undefined { 9 | const node = nodes.find(s => s.kind === kind && (!test || test(s as T))) as T; 10 | return node; 11 | } 12 | 13 | export function filterNodes( 14 | nodes: ts.NodeArray, 15 | kind: T extends { kind: infer K } ? K : never, 16 | test?: (node: T) => boolean | undefined 17 | ): T[] { 18 | return nodes.filter(s => s.kind === kind && (!test || test(s as T))) as T[]; 19 | } 20 | 21 | export function findFirstVariableDeclaration( 22 | nodes: ts.NodeArray, 23 | name: string 24 | ): ts.VariableDeclaration | undefined { 25 | const statement = findNode( 26 | nodes, 27 | ts.SyntaxKind.VariableStatement, 28 | n => findFirstVariableDeclarationName(n) === name 29 | ); 30 | if (!statement) return; 31 | const [first] = statement.declarationList.declarations; 32 | return first; 33 | } 34 | 35 | export function findFirstVariableDeclarationName(n: ts.VariableStatement): string | ts.__String | undefined { 36 | const name = ts.getNameOfDeclaration(n.declarationList.declarations[0]); 37 | return name && getName(name); 38 | } 39 | 40 | export function findVariableDeclarationName(variable: ts.VariableStatement, name: string): ts.VariableDeclaration | null { 41 | for (const decla of variable.declarationList.declarations) { 42 | const declaName = ts.getNameOfDeclaration(decla); 43 | if (declaName && getName(declaName) === name) { 44 | return decla; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/lib/printer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | const printer = ts.createPrinter({ 4 | newLine: ts.NewLineKind.LineFeed 5 | }); 6 | 7 | export function printNode(node: ts.Node): string { 8 | const file = ts.createSourceFile( 9 | "someFileName.ts", 10 | "", 11 | ts.ScriptTarget.Latest, 12 | /*setParentNodes*/ false, 13 | ts.ScriptKind.TS 14 | ); 15 | 16 | return printer.printNode(ts.EmitHint.Unspecified, node, file); 17 | } 18 | 19 | export function printNodes(nodes: ts.Node[]): string { 20 | const file = ts.createSourceFile( 21 | "someFileName.ts", 22 | "", 23 | ts.ScriptTarget.Latest, 24 | /*setParentNodes*/ false, 25 | ts.ScriptKind.TS 26 | ); 27 | 28 | return nodes 29 | .map(node => printer.printNode(ts.EmitHint.Unspecified, node, file)) 30 | .join("\n\n"); 31 | } 32 | 33 | export function printFile(sourceFile: ts.SourceFile): string { 34 | return printer.printFile(sourceFile); 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/lib/sourcefile.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { promises as fs } from "fs"; 3 | 4 | export async function createSourceFileFromFile(file: string): Promise { 5 | const content = await fs.readFile(file, "utf8"); 6 | 7 | return ts.createSourceFile( 8 | file, 9 | content, 10 | ts.ScriptTarget.Latest, 11 | /*setParentNodes*/ false, 12 | ts.ScriptKind.TS 13 | ); 14 | } 15 | 16 | export function updateSourceFileStatements(file: ts.SourceFile, statements: ts.Statement[]): ts.SourceFile { 17 | return ts.factory.updateSourceFile( 18 | file, 19 | ts.factory.createNodeArray(statements), 20 | file.isDeclarationFile, 21 | file.referencedFiles, 22 | file.typeReferenceDirectives, 23 | file.hasNoDefaultLib, 24 | file.libReferenceDirectives 25 | ); 26 | } 27 | 28 | export function appendSourceFileStatements(file: ts.SourceFile, ...statements: ts.Statement[]): ts.SourceFile { 29 | return updateSourceFileStatements( 30 | file, 31 | [...file.statements, ...statements] 32 | ); 33 | } 34 | 35 | export function prependSourceFileStatements(file: ts.SourceFile, ...statements: ts.Statement[]): ts.SourceFile { 36 | return updateSourceFileStatements( 37 | file, 38 | [...statements, ...file.statements] 39 | ); 40 | } 41 | 42 | export function replaceSourceFileStatement(file: ts.SourceFile, oldStatement: ts.Statement, newStatement: ts.Statement): ts.SourceFile { 43 | const i = file.statements.indexOf(oldStatement); 44 | if (i === -1) { 45 | throw new Error(`Unable to find this statement!`); 46 | } 47 | 48 | return updateSourceFileStatements( 49 | file, 50 | [ 51 | ...file.statements.slice(0, i), 52 | newStatement, 53 | ...file.statements.slice(i + 1) 54 | ] 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/lib/statement.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { replaceNode } from "./common"; 3 | 4 | import { updateVariableDeclarationInitializer } from "./declaration"; 5 | import { upsertPropertyValue } from "./expression"; 6 | import { findNode, findVariableDeclarationName } from "./finder"; 7 | 8 | export function findFirstVariableStatement(nodes: ts.NodeArray, variableName: string): ts.VariableStatement | undefined { 9 | return findNode( 10 | nodes, 11 | ts.SyntaxKind.VariableStatement, 12 | n => !!findVariableDeclarationName(n, variableName) 13 | ); 14 | } 15 | 16 | export function updateVariableStatementValue(statement: ts.VariableStatement, variableName: string, value: ts.Expression): ts.VariableStatement { 17 | const decla = findVariableDeclarationName(statement, variableName); 18 | if (!decla) { 19 | throw new Error(`Could not find variable declaration in given statement: ${variableName}`); 20 | } 21 | 22 | return ts.factory.updateVariableStatement( 23 | statement, 24 | statement.modifiers, 25 | ts.factory.updateVariableDeclarationList( 26 | statement.declarationList, 27 | replaceNode( 28 | statement.declarationList.declarations, 29 | decla, 30 | updateVariableDeclarationInitializer(decla, value) 31 | ) 32 | ) 33 | ); 34 | } 35 | 36 | export function updateVariableStatementPropertyValue(statement: ts.VariableStatement, variableName: string, propertyName: string, value: ts.Expression): ts.VariableStatement { 37 | const decla = findVariableDeclarationName(statement, variableName); 38 | if (!decla) { 39 | throw new Error(`Could not find variable declaration in given statement: ${variableName}`); 40 | } 41 | 42 | if (!decla.initializer || !ts.isObjectLiteralExpression(decla.initializer)) { 43 | return ts.factory.updateVariableStatement( 44 | statement, 45 | statement.modifiers, 46 | ts.factory.updateVariableDeclarationList( 47 | statement.declarationList, 48 | replaceNode( 49 | statement.declarationList.declarations, 50 | decla, 51 | updateVariableDeclarationInitializer( 52 | decla, 53 | ts.factory.createObjectLiteralExpression([ 54 | ts.factory.createPropertyAssignment(propertyName, value) 55 | ]) 56 | ) 57 | ) 58 | ) 59 | ); 60 | } 61 | 62 | return ts.factory.updateVariableStatement( 63 | statement, 64 | statement.modifiers, 65 | ts.factory.updateVariableDeclarationList( 66 | statement.declarationList, 67 | replaceNode( 68 | statement.declarationList.declarations, 69 | decla, 70 | updateVariableDeclarationInitializer( 71 | decla, 72 | upsertPropertyValue(decla.initializer, propertyName, value) 73 | ) 74 | ) 75 | ) 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spec2ts/core", 3 | "version": "3.0.2", 4 | "description": "Core module for @spec2ts modules, includes codegen helpers and common parsing methods", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/touchifyapp/spec2ts#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "files": [ 17 | "*.js", 18 | "*.d.ts", 19 | "lib/**/*.js", 20 | "lib/**/*.d.ts" 21 | ], 22 | "scripts": { 23 | "build": "npm run clean && npm run lint && npm run build:ts", 24 | "build:ts": "tsc -p .", 25 | "test": "npm run clean && npm run lint && npm run test:jest", 26 | "test:jest": "jest -c ../../jest.config.js --rootDir . --passWithNoTests", 27 | "test:coverage": "npm run test -- -- --coverage", 28 | "lint": "npm run lint:ts", 29 | "lint:ts": "eslint '*.ts' 'lib/**/*.ts'", 30 | "lint:fix": "npm run lint -- -- --fix", 31 | "clean": "npm run clean:ts", 32 | "clean:ts": "del '*.{js,d.ts}' '{bin,cli,lib}/**/*.{js,d.ts}'", 33 | "prepublishOnly": "npm test && npm run build" 34 | }, 35 | "dependencies": { 36 | "glob": "^10.3.4", 37 | "typescript": "^5.0.0" 38 | }, 39 | "devDependencies": { 40 | "@types/glob": "^8.1.0", 41 | "del-cli": "^5.1.0", 42 | "eslint": "^8.49.0", 43 | "jest": "^29.6.4" 44 | }, 45 | "keywords": [ 46 | "spec", 47 | "typescript", 48 | "compile", 49 | "compiler", 50 | "ast", 51 | "transpile", 52 | "interface", 53 | "typing", 54 | "spec2ts", 55 | "share" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/jsonschema/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [3.0.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@3.0.4...@spec2ts/jsonschema@3.0.5) (2025-04-10) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **json-schema:** allow access to defaultParseReference from context ([fb4f17e](https://github.com/touchifyapp/spec2ts/commit/fb4f17e9257ef18627e48439c46791b67d4d4197)) 12 | 13 | 14 | 15 | 16 | 17 | ## [3.0.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@3.0.3...@spec2ts/jsonschema@3.0.4) (2023-10-23) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * circular reference in external schema ([7762c71](https://github.com/touchifyapp/spec2ts/commit/7762c712a0cdffa2d0a12852979c9f33be645207)) 23 | 24 | 25 | 26 | 27 | 28 | ## [3.0.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@3.0.2...@spec2ts/jsonschema@3.0.3) (2023-10-03) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * move typescript to runtime dependencies ([6e17d21](https://github.com/touchifyapp/spec2ts/commit/6e17d21187ee6d8af226a92595d6c93df04db2ea)) 34 | 35 | 36 | 37 | 38 | 39 | ## [3.0.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@3.0.1...@spec2ts/jsonschema@3.0.2) (2023-09-25) 40 | 41 | **Note:** Version bump only for package @spec2ts/jsonschema 42 | 43 | 44 | 45 | 46 | 47 | ## [3.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@3.0.0...@spec2ts/jsonschema@3.0.1) (2023-09-12) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **jsonschema,openapi,openapi-client:** use ref parser as adviced ([e65a291](https://github.com/touchifyapp/spec2ts/commit/e65a2919f9d37ffdea773132dd906fca11b6240b)) 53 | 54 | 55 | 56 | 57 | 58 | # [3.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0...@spec2ts/jsonschema@3.0.0) (2023-09-12) 59 | 60 | 61 | * feat!: migrate to typescript v5 ([84d2bb0](https://github.com/touchifyapp/spec2ts/commit/84d2bb0719f63dbff334bf8ba83b89501e18aeff)) 62 | 63 | 64 | ### Features 65 | 66 | * upgrade all dependencies ([9be17b6](https://github.com/touchifyapp/spec2ts/commit/9be17b69e2bd5d910bbaa88d4e2f161628fa4135)) 67 | 68 | 69 | ### BREAKING CHANGES 70 | 71 | * removed all decorators, use modifiers instead as specified by API v5 of Typescript compiler 72 | 73 | 74 | 75 | 76 | 77 | # [2.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.8...@spec2ts/jsonschema@2.0.0) (2023-09-07) 78 | 79 | **Note:** Version bump only for package @spec2ts/jsonschema 80 | 81 | 82 | 83 | 84 | 85 | # [2.0.0-beta.8](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.7...@spec2ts/jsonschema@2.0.0-beta.8) (2022-09-21) 86 | 87 | **Note:** Version bump only for package @spec2ts/jsonschema 88 | 89 | 90 | 91 | 92 | 93 | # [2.0.0-beta.7](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.6...@spec2ts/jsonschema@2.0.0-beta.7) (2022-07-29) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * **jsonschema:** adapt to new TS 4.3 API ([c28532c](https://github.com/touchifyapp/spec2ts/commit/c28532c21643eda990abf8833c2027eb0a27a6a5)) 99 | 100 | 101 | 102 | 103 | 104 | # [2.0.0-beta.6](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.5...@spec2ts/jsonschema@2.0.0-beta.6) (2022-01-27) 105 | 106 | **Note:** Version bump only for package @spec2ts/jsonschema 107 | 108 | 109 | 110 | 111 | 112 | # [2.0.0-beta.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.4...@spec2ts/jsonschema@2.0.0-beta.5) (2021-11-03) 113 | 114 | 115 | ### Features 116 | 117 | * allow prefixItems to create tuples ([7fb44ca](https://github.com/touchifyapp/spec2ts/commit/7fb44ca88c14623392f1d6076b390370da9e0a69)) 118 | 119 | 120 | 121 | 122 | 123 | # [2.0.0-beta.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.3...@spec2ts/jsonschema@2.0.0-beta.4) (2021-10-23) 124 | 125 | 126 | ### Features 127 | 128 | * allow --ext option to specify output extension ([4c70ca1](https://github.com/touchifyapp/spec2ts/commit/4c70ca13f3fc12ce1fd16c0430c7f90f90b0ed64)) 129 | 130 | 131 | 132 | 133 | 134 | # [2.0.0-beta.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.2...@spec2ts/jsonschema@2.0.0-beta.3) (2021-02-27) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * **jsonschema:** avoid external references duplication ([9508d9e](https://github.com/touchifyapp/spec2ts/commit/9508d9eee0ae19523d03a2874bad73808ec5bf71)) 140 | 141 | 142 | 143 | 144 | 145 | # [2.0.0-beta.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.1...@spec2ts/jsonschema@2.0.0-beta.2) (2020-11-29) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * **jsonschema:** bad yargs options for enableDate ([4a17520](https://github.com/touchifyapp/spec2ts/commit/4a17520cbc95c18354860750da1e3344dd66865f)) 151 | 152 | 153 | 154 | 155 | 156 | # [2.0.0-beta.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@2.0.0-beta.0...@spec2ts/jsonschema@2.0.0-beta.1) (2020-11-29) 157 | 158 | 159 | ### Features 160 | 161 | * **jsonschema:** add lax style dates (string | Date) ([e6475aa](https://github.com/touchifyapp/spec2ts/commit/e6475aa84d0330457c91e2d0e32911ce66135cec)) 162 | 163 | 164 | 165 | 166 | 167 | # [2.0.0-beta.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.4.2...@spec2ts/jsonschema@2.0.0-beta.0) (2020-11-13) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * **jsonschema:** incluce numbers in pascalCase ([13c87c3](https://github.com/touchifyapp/spec2ts/commit/13c87c3d5d5a7c550e46d9cddfc9de617c6263b6)) 173 | 174 | 175 | ### Features 176 | 177 | * **global:** upgrade typescript 4 ([#16](https://github.com/touchifyapp/spec2ts/issues/16)) ([fcd82be](https://github.com/touchifyapp/spec2ts/commit/fcd82be93be3986a2f723680f1c52818eb7ba1bc)) 178 | 179 | 180 | ### BREAKING CHANGES 181 | 182 | * **global:** use typescript v4 183 | * **global:** updates are now immutable 184 | 185 | 186 | 187 | 188 | 189 | ## [1.4.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.4.1...@spec2ts/jsonschema@1.4.2) (2020-10-26) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * **jsonschema,openapi:** cover more nested refs cases ([1badafb](https://github.com/touchifyapp/spec2ts/commit/1badafbe0865a186ef5fc92bfc0ab5b334d4fa6e)) 195 | 196 | 197 | 198 | 199 | 200 | ## [1.4.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.4.0...@spec2ts/jsonschema@1.4.1) (2020-10-26) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * **jsonschema,openapi:** nested references not resolved ([9825a40](https://github.com/touchifyapp/spec2ts/commit/9825a405630c101e7a70452ce3a18e02ccad9ce8)), closes [#15](https://github.com/touchifyapp/spec2ts/issues/15) 206 | 207 | 208 | 209 | 210 | 211 | # [1.4.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.3.1...@spec2ts/jsonschema@1.4.0) (2020-10-06) 212 | 213 | 214 | ### Features 215 | 216 | * **jsonschema:** add const parsing ([d0db5f1](https://github.com/touchifyapp/spec2ts/commit/d0db5f1dac8a020a99407a942c3a39abc3a89a48)), closes [#3](https://github.com/touchifyapp/spec2ts/issues/3) 217 | 218 | 219 | 220 | 221 | 222 | ## [1.3.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.3.0...@spec2ts/jsonschema@1.3.1) (2020-05-27) 223 | 224 | 225 | ### Bug Fixes 226 | 227 | * **jsonschema:** improve external references parsing ([23f3868](https://github.com/touchifyapp/spec2ts/commit/23f3868980a78ad880237dfdff829e7b3e5a4d6e)), closes [#1](https://github.com/touchifyapp/spec2ts/issues/1) 228 | 229 | 230 | 231 | 232 | 233 | # [1.3.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.2.0...@spec2ts/jsonschema@1.3.0) (2020-05-24) 234 | 235 | 236 | ### Features 237 | 238 | * **cli:** add default banner ([b0945e0](https://github.com/touchifyapp/spec2ts/commit/b0945e08b2c1da4dc494dca1890d491768a13e60)) 239 | 240 | 241 | 242 | 243 | 244 | # [1.2.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.1.0...@spec2ts/jsonschema@1.2.0) (2020-05-18) 245 | 246 | 247 | ### Features 248 | 249 | * **core:** improve type generation ([7abd848](https://github.com/touchifyapp/spec2ts/commit/7abd84800ce27d81a7868d4ec0a67f28bf26b355)) 250 | 251 | 252 | 253 | 254 | 255 | # [1.1.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/jsonschema@1.0.0...@spec2ts/jsonschema@1.1.0) (2020-05-16) 256 | 257 | 258 | ### Features 259 | 260 | * **jsonschema:** add parseReference option ([1b8f637](https://github.com/touchifyapp/spec2ts/commit/1b8f637725bc3e4a4499656d5dbd213ddaecd860)) 261 | 262 | 263 | 264 | 265 | 266 | # 1.0.0 (2020-04-24) 267 | 268 | 269 | ### Bug Fixes 270 | 271 | * **release:** fix lerna publish ([fe36558](https://github.com/touchifyapp/spec2ts/commit/fe36558a1a2742e2e3d99aa08061ab9be0cf03f2)) 272 | 273 | 274 | ### Code Refactoring 275 | 276 | * **jsonschema:** create lib base structure ([7365f94](https://github.com/touchifyapp/spec2ts/commit/7365f94ae0d32a3ef427dce02891c602f98a5edc)) 277 | 278 | 279 | ### Features 280 | 281 | * **jsonschema:** add cli command ([7592c43](https://github.com/touchifyapp/spec2ts/commit/7592c439be99fabb97cc270aa7a09794ee86f738)) 282 | * **openapi:** add openapi parser to monorepo ([e9ca537](https://github.com/touchifyapp/spec2ts/commit/e9ca5375e2692f909d32eacae653f918cd348040)) 283 | 284 | 285 | ### BREAKING CHANGES 286 | 287 | * **jsonschema:** no more printer in library 288 | -------------------------------------------------------------------------------- /packages/jsonschema/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/jsonschema/README.md: -------------------------------------------------------------------------------- 1 | # @spec2ts/jsonschema 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@spec2ts/jsonschema.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/jsonschema) 4 | [![NPM download](https://img.shields.io/npm/dm/@spec2ts/jsonschema.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/jsonschema) 5 | [![Build Status](https://travis-ci.org/touchifyapp/spec2ts.svg?branch=master)](https://travis-ci.org/touchifyapp/spec2ts) 6 | 7 | `@spec2ts/jsonschema` is an utility to create TypeScript types from JSON schemas. Unlike other code generators `@spec2ts/jsonschema` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree (AST). 8 | 9 | ## Features 10 | 11 | * **AST-based:** Unlike other code generators `@spec2ts/jsonschema` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree. 12 | * **Tree-shakeable:** Individually exported types allows you to bundle only the ones you actually use. 13 | * **YAML or JSON:** Use YAML or JSON for your JSON Schemas. 14 | * **External references:** Resolves automatically external references and bundle or import them in generated files. 15 | * **Implementation agnostic:** Use generated types in any projet or framework. 16 | 17 | ## Installation 18 | 19 | Install in your project: 20 | ```bash 21 | npm install @spec2ts/jsonschema 22 | ``` 23 | 24 | ## CLI Usage 25 | 26 | ```bash 27 | jsonschema2ts [options] 28 | 29 | Generate TypeScript types from JSON Schemas 30 | 31 | Positionals: 32 | input Path to JSON Schema(s) to convert to TypeScript [string] 33 | 34 | Options: 35 | --version Show version number [boolean] 36 | --help Show help usage [boolean] 37 | --output, -o Output directory for generated types [string] 38 | --cwd, -c Root directory for resolving $refs [string] 39 | --avoidAny Avoid the `any` type and use `unknown` instead [boolean] 40 | --enableDate Build `Date` for format `date` and `date-time` [boolean] 41 | --banner, -b Comment prepended to the top of each generated file [string] 42 | ``` 43 | 44 | ## Programmatic Usage 45 | 46 | ```typescript 47 | import { printer } from "@spec2ts/core"; 48 | import { parseSchema, JSONSchema } from "@spec2ts/jsonschema"; 49 | 50 | async function generateSchema(schema: JSONSchema): Promise { 51 | const result = await parseSchema(schema); 52 | return printer.printNodes(result); 53 | } 54 | ``` 55 | 56 | ## Implementations 57 | 58 | - [x] Primitive types: 59 | - [x] array 60 | - [x] boolean 61 | - [x] integer 62 | - [x] number 63 | - [x] null 64 | - [x] object 65 | - [x] string 66 | - [x] homogeneous enum 67 | - [x] heterogeneous enum 68 | - [x] Special types: 69 | - [x] Date (`date` and `date-time` formats) 70 | - [x] Blob (`binary` format) 71 | - [x] Automatic type naming: 72 | - [x] From `id` 73 | - [x] From `path` 74 | - [x] From `title` 75 | - [ ] Custom JSON-schema extensions 76 | - [x] Nested properties 77 | - [x] Schema definitions 78 | - [x] [Schema references](http://json-schema.org/latest/json-schema-core.html#rfc.section.7.2.2) 79 | - [x] Local (filesystem) schema references 80 | - [x] External (network) schema references 81 | - [x] Modular architecture 82 | - [x] Import local references 83 | - [x] Embed external references 84 | - [x] `allOf` ("intersection") 85 | - [x] `anyOf` ("union") 86 | - [x] `oneOf` (treated like `anyOf`) 87 | - [ ] `maxItems` 88 | - [ ] `minItems` 89 | - [x] `additionalProperties` of type 90 | - [ ] `patternProperties` (partial support) 91 | - [x] `extends` (with `allOf`) 92 | - [x] `required` properties on objects 93 | - [ ] `validateRequired` 94 | - [ ] literal objects in enum 95 | - [x] referencing schema by id 96 | 97 | ## Compatibility Matrix 98 | 99 | | TypeScript version | spec2ts version | 100 | |--------------------|-----------------| 101 | | v3.x.x | v1 | 102 | | v4.x.x | v2 | 103 | 104 | ## License 105 | 106 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 107 | -------------------------------------------------------------------------------- /packages/jsonschema/bin/jsonschema2ts.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../cli"; 3 | -------------------------------------------------------------------------------- /packages/jsonschema/cli/command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Argv 3 | } from "yargs"; 4 | 5 | import { 6 | printer, 7 | cli 8 | } from "@spec2ts/core"; 9 | 10 | import { 11 | parseSchemaFile, 12 | ParseSchemaOptions 13 | } from "../lib/schema-parser"; 14 | 15 | export interface BuildTsFromSchemaOptions extends ParseSchemaOptions { 16 | input: string | string[]; 17 | output?: string; 18 | ext?: string; 19 | banner?: string; 20 | } 21 | 22 | export const usage = "$0 "; 23 | 24 | export const describe = "Generate TypeScript types from JSON Schemas"; 25 | 26 | export function builder(argv: Argv): Argv { 27 | return argv 28 | .positional("input", { 29 | array: true, 30 | type: "string", 31 | describe: "Path to JSON Schema(s) to convert to TypeScript", 32 | demandOption: true 33 | }) 34 | 35 | .option("output", { 36 | type: "string", 37 | alias: "o", 38 | describe: "Output directory for generated types" 39 | }) 40 | .option("ext", { 41 | type: "string", 42 | alias: "e", 43 | describe: "Output extension for generated types", 44 | choices: [".d.ts", ".ts"] 45 | }) 46 | 47 | .option("cwd", { 48 | type: "string", 49 | alias: "c", 50 | describe: "Root directory for resolving $refs" 51 | }) 52 | 53 | .option("avoidAny", { 54 | type: "boolean", 55 | describe: "Avoid the `any` type and use `unknown` instead" 56 | }) 57 | .option("enableDate", { 58 | choices: [true, "strict", "lax"] as const, 59 | describe: "Build `Date` for format `date` and `date-time`" 60 | }) 61 | 62 | .option("banner", { 63 | type: "string", 64 | alias: "b", 65 | describe: "Comment prepended to the top of each generated file" 66 | }); 67 | } 68 | 69 | export async function handler(options: BuildTsFromSchemaOptions): Promise { 70 | const files = await cli.findFiles(options.input); 71 | 72 | for (const file of files) { 73 | const ast = await parseSchemaFile(file, options); 74 | const content = printer.printNodes(ast); 75 | 76 | const output = cli.getOutputPath(file, options); 77 | await cli.mkdirp(output); 78 | 79 | await cli.writeFile( 80 | output, 81 | (options.banner || defaultBanner()) + 82 | "\n\n" + 83 | content 84 | ); 85 | } 86 | } 87 | 88 | function defaultBanner(): string { 89 | return `/** 90 | * DO NOT MODIFY 91 | * Generated using @spec2ts/jsonschema. 92 | * See https://www.npmjs.com/package/@spec2ts/jsonschema 93 | */ 94 | 95 | /* eslint-disable */`; 96 | } 97 | -------------------------------------------------------------------------------- /packages/jsonschema/cli/index.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from "yargs"; 2 | 3 | import { 4 | usage, 5 | describe, 6 | builder, 7 | handler 8 | } from "../cli/command"; 9 | 10 | yargs 11 | .command(usage, describe, builder, handler) 12 | .help("help", "Show help usage") 13 | .demandCommand() 14 | .argv; 15 | -------------------------------------------------------------------------------- /packages/jsonschema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/schema-parser"; 2 | -------------------------------------------------------------------------------- /packages/jsonschema/lib/schema-parser.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as ts from "typescript"; 3 | 4 | import $RefParser from "@apidevtools/json-schema-ref-parser"; 5 | 6 | import * as core from "@spec2ts/core"; 7 | 8 | import { 9 | JSONSchema, 10 | ParserOptions, 11 | 12 | getTypeFromSchema, 13 | parseDefinitions, 14 | 15 | getSchemaName, 16 | createContext, 17 | } from "./core-parser"; 18 | 19 | export interface ParseSchemaOptions extends ParserOptions { 20 | name?: string; 21 | } 22 | 23 | export async function parseSchemaFile(file: string, options: ParseSchemaOptions = {}): Promise { 24 | const schema = await $RefParser.parse(file); 25 | 26 | return parseSchema(schema, { 27 | name: getSchemaName(schema, file), 28 | cwd: path.resolve(path.dirname(file)) + "/", 29 | ...options 30 | }); 31 | } 32 | 33 | export async function parseSchema(schema: JSONSchema, options: ParseSchemaOptions = {}): Promise { 34 | const context = await createContext(schema, options); 35 | const type = getTypeFromSchema(context.schema, context); 36 | 37 | parseDefinitions(context.schema, context); 38 | 39 | const res: ts.Statement[] = [ 40 | ...context.imports, 41 | ...context.aliases 42 | ]; 43 | 44 | // Ignore schema type if schema is only composed of definitions 45 | if ((type === core.keywordType.any || type === core.keywordType.unknown) && !context.schema.type && context.schema.definitions) { 46 | return res; 47 | } 48 | 49 | let decla = core.createTypeOrInterfaceDeclaration({ 50 | modifiers: [core.modifier.export], 51 | name: options.name || getSchemaName(context.schema), 52 | type 53 | }); 54 | 55 | if (schema.description) { 56 | decla = core.addComment(decla, schema.description); 57 | } 58 | 59 | return [...res, decla]; 60 | } 61 | -------------------------------------------------------------------------------- /packages/jsonschema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spec2ts/jsonschema", 3 | "version": "3.0.5", 4 | "description": "Utility to convert JSON Schemas to Typescript using TypeScript native compiler", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/touchifyapp/spec2ts/blob/master/packages/jsonschema#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "bin": { 17 | "jsonschema2ts": "./bin/jsonschema2ts.js" 18 | }, 19 | "files": [ 20 | "*.js", 21 | "*.d.ts", 22 | "bin/**/*.js", 23 | "cli/**/*.js", 24 | "cli/**/*.d.ts", 25 | "lib/**/*.js", 26 | "lib/**/*.d.ts" 27 | ], 28 | "scripts": { 29 | "build": "npm run clean && npm run lint && npm run build:ts", 30 | "build:ts": "tsc -p .", 31 | "test": "npm run clean && npm run lint && npm run test:jest", 32 | "test:jest": "jest -c ../../jest.config.js --rootDir .", 33 | "test:coverage": "npm run test -- -- --coverage", 34 | "lint": "npm run lint:ts", 35 | "lint:ts": "eslint '*.ts' '{bin,cli,lib}/**/*.ts'", 36 | "lint:fix": "npm run lint -- -- --fix", 37 | "clean": "npm run clean:ts", 38 | "clean:ts": "del '*.{js,d.ts}' '{bin,cli,lib}/**/*.{js,d.ts}'", 39 | "prepublishOnly": "npm test && npm run build" 40 | }, 41 | "dependencies": { 42 | "@apidevtools/json-schema-ref-parser": "^10.1.0", 43 | "@spec2ts/core": "^3.0.2", 44 | "typescript": "^5.0.0", 45 | "yargs": "^17.7.2" 46 | }, 47 | "devDependencies": { 48 | "@types/json-schema": "^7.0.12", 49 | "del-cli": "^5.1.0", 50 | "eslint": "^8.49.0", 51 | "jest": "^29.6.4" 52 | }, 53 | "keywords": [ 54 | "json", 55 | "schema", 56 | "spec", 57 | "typescript", 58 | "compile", 59 | "compiler", 60 | "ast", 61 | "transpile", 62 | "interface", 63 | "typing", 64 | "spec2ts", 65 | "share" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/addresses.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/addresses.schema.json", 3 | "$schema": "http://json-schema.org/draft-06/schema#", 4 | "title": "Addresses", 5 | "definitions": { 6 | "address": { 7 | "type": "object", 8 | "properties": { 9 | "street_address": { 10 | "type": "string" 11 | }, 12 | "city": { 13 | "type": "string" 14 | }, 15 | "state": { 16 | "type": "string" 17 | }, 18 | "circular": { 19 | "$ref": "#/definitions/address" 20 | } 21 | }, 22 | "required": [ 23 | "street_address", 24 | "city", 25 | "state" 26 | ] 27 | } 28 | }, 29 | "type": "object", 30 | "properties": { 31 | "billing_address": { 32 | "$ref": "#/definitions/address" 33 | }, 34 | "shipping_address": { 35 | "allOf": [ 36 | { 37 | "$ref": "#/definitions/address" 38 | }, 39 | { 40 | "properties": { 41 | "type": { 42 | "enum": [ 43 | "residential", 44 | "business" 45 | ] 46 | } 47 | }, 48 | "required": [ 49 | "type" 50 | ] 51 | } 52 | ] 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/arrays.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/arrays.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "description": "A representation of a person, company, organization, or place", 5 | "type": "object", 6 | "properties": { 7 | "fruits": { 8 | "type": "array", 9 | "items": { 10 | "type": "string" 11 | } 12 | }, 13 | "vegetables": { 14 | "type": "array", 15 | "items": { 16 | "$ref": "#/definitions/veggie" 17 | } 18 | } 19 | }, 20 | "definitions": { 21 | "veggie": { 22 | "type": "object", 23 | "required": [ 24 | "veggieName", 25 | "veggieLike" 26 | ], 27 | "properties": { 28 | "veggieName": { 29 | "type": "string", 30 | "description": "The name of the vegetable." 31 | }, 32 | "veggieLike": { 33 | "type": "boolean", 34 | "description": "Do I like this vegetable?" 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/composition.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/composition.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "description": "Composition of multiple entities", 5 | "type": "object", 6 | "properties": { 7 | "userId": { 8 | "anyOf": [ 9 | { 10 | "type": "string", 11 | "maxLength": 5 12 | }, 13 | { 14 | "type": "number", 15 | "minimum": 0 16 | } 17 | ] 18 | }, 19 | "details": { 20 | "oneOf": [ 21 | { 22 | "$ref": "./addresses.schema.json#/definitions/address" 23 | }, 24 | { 25 | "$ref": "./person.schema.json#" 26 | } 27 | ] 28 | }, 29 | "billing_address": { 30 | "$ref": "./addresses.schema.json#/definitions/address" 31 | }, 32 | "shipping_address": { 33 | "allOf": [ 34 | { 35 | "$ref": "./addresses.schema.json#/definitions/address" 36 | }, 37 | { 38 | "properties": { 39 | "type": { 40 | "enum": [ 41 | "residential", 42 | "business" 43 | ] 44 | } 45 | }, 46 | "required": [ 47 | "type" 48 | ] 49 | } 50 | ] 51 | }, 52 | "extended_address": { 53 | "$ref": "./extends.schema.json#" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/const.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "str": { 5 | "const": "value" 6 | }, 7 | "num": { 8 | "const": 0 9 | }, 10 | "bool": { 11 | "const": false 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/definitions.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "int": { 4 | "type": "integer" 5 | }, 6 | "str": { 7 | "type": "string" 8 | }, 9 | "obj": { 10 | "type": "object", 11 | "properties": { 12 | "firstName": { 13 | "type": "string" 14 | }, 15 | "lastName": { 16 | "type": "string" 17 | }, 18 | "age": { 19 | "description": "Age in years", 20 | "type": "integer", 21 | "minimum": 0 22 | }, 23 | "hairColor": { 24 | "enum": [ 25 | "black", 26 | "brown", 27 | "blue" 28 | ], 29 | "type": "string" 30 | } 31 | }, 32 | "required": [ 33 | "firstName", 34 | "lastName" 35 | ] 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/extends.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/extends.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "description": "Extend entity", 5 | "allOf": [ 6 | { 7 | "$ref": "./addresses.schema.json#/definitions/address" 8 | }, 9 | { 10 | "$ref": "./person.schema.json#" 11 | }, 12 | { 13 | "properties": { 14 | "type": { 15 | "enum": [ 16 | "residential", 17 | "business" 18 | ] 19 | } 20 | }, 21 | "required": [ 22 | "type" 23 | ] 24 | }, 25 | { 26 | "properties": { 27 | "newProperty": { 28 | "type": "string" 29 | } 30 | }, 31 | "required": [ 32 | "newProperty" 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/formats.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/formats.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "title": "Formats object", 5 | "type": "object", 6 | "properties": { 7 | "birthDate": { 8 | "type": "string", 9 | "format": "date" 10 | }, 11 | "createdAt": { 12 | "type": "string", 13 | "format": "date-time" 14 | }, 15 | "picture": { 16 | "type": "string", 17 | "format": "binary" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/geographical-location.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/geographical-location.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "title": "Longitude and Latitude Values", 5 | "description": "A geographical coordinate.", 6 | "type": "object", 7 | "properties": { 8 | "latitude": { 9 | "type": "number", 10 | "minimum": -90, 11 | "maximum": 90 12 | }, 13 | "longitude": { 14 | "type": "number", 15 | "minimum": -180, 16 | "maximum": 180 17 | } 18 | }, 19 | "required": [ 20 | "latitude", 21 | "longitude" 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/importdefs.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://example.com/schemas/importdefs.schema.json", 3 | "type": "object", 4 | "title": "MyObj", 5 | "properties": { 6 | "int": { 7 | "$ref": "definitions.schema.json#/definitions/int" 8 | }, 9 | "str": { 10 | "$ref": "definitions.schema.json#/definitions/str" 11 | }, 12 | "obj": { 13 | "$ref": "definitions.schema.json#/definitions/obj" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/nested.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://example.com/schemas/importdefs.schema.json", 3 | "type": "object", 4 | "title": "MyObj", 5 | "properties": { 6 | "obj": { 7 | "$ref": "addresses.schema.json#" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/noname.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "firstName": { 6 | "type": "string" 7 | }, 8 | "lastName": { 9 | "type": "string" 10 | }, 11 | "age": { 12 | "description": "Age in years", 13 | "type": "integer", 14 | "minimum": 0 15 | }, 16 | "hairColor": { 17 | "enum": [ 18 | "black", 19 | "brown", 20 | "blue" 21 | ], 22 | "type": "string" 23 | } 24 | }, 25 | "required": [ 26 | "firstName", 27 | "lastName" 28 | ] 29 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/person.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/person.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "title": "Person", 5 | "type": "object", 6 | "properties": { 7 | "firstName": { 8 | "type": "string" 9 | }, 10 | "lastName": { 11 | "type": "string" 12 | }, 13 | "age": { 14 | "description": "Age in years", 15 | "type": "integer", 16 | "minimum": 0 17 | }, 18 | "hairColor": { 19 | "enum": [ 20 | "black", 21 | "brown", 22 | "blue" 23 | ], 24 | "type": "string" 25 | } 26 | }, 27 | "required": [ 28 | "firstName", 29 | "lastName" 30 | ] 31 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/person.schema.yml: -------------------------------------------------------------------------------- 1 | $id: https://example.com/person.schema.json 2 | $schema: http://json-schema.org/draft-07/schema# 3 | 4 | title: Person 5 | type: object 6 | 7 | properties: 8 | firstName: 9 | type: string 10 | 11 | lastName: 12 | type: string 13 | 14 | age: 15 | type: integer 16 | description: Age in years 17 | minimum: 0 18 | 19 | hairColor: 20 | type: string 21 | enum: 22 | - black 23 | - brown 24 | - blue 25 | 26 | required: 27 | - firstName 28 | - lastName 29 | -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/persons.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Persons", 3 | "type": "array", 4 | "items": { 5 | "$ref": "./person.schema.json#" 6 | } 7 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/tuples.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/tuple.schema.json", 3 | "$schema": "https://json-schema.org/draft/2019-09/schema", 4 | "description": "Tuple entities", 5 | "definitions": { 6 | "strict-tuple": { 7 | "items": false, 8 | "prefixItems": [ 9 | { "type": "string" }, 10 | { "type": "boolean" }, 11 | { "$ref": "./addresses.schema.json#/definitions/address" } 12 | ] 13 | }, 14 | "lax-type-tuple": { 15 | "items": { "$ref": "./addresses.schema.json#/definitions/address" }, 16 | "prefixItems": [ 17 | { "type": "string" }, 18 | { "type": "boolean" }, 19 | { "$ref": "./addresses.schema.json#/definitions/address" } 20 | ] 21 | }, 22 | "lax-any-tuple": { 23 | "prefixItems": [ 24 | { "type": "string" }, 25 | { "type": "boolean" }, 26 | { "$ref": "./addresses.schema.json#/definitions/address" } 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/assets/union.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/union.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "description": "Union entity", 5 | "oneOf": [ 6 | { 7 | "$ref": "./addresses.schema.json#/definitions/address" 8 | }, 9 | { 10 | "$ref": "./person.schema.json#" 11 | }, 12 | { 13 | "properties": { 14 | "type": { 15 | "enum": [ 16 | "residential", 17 | "business" 18 | ] 19 | } 20 | }, 21 | "required": [ 22 | "type" 23 | ] 24 | }, 25 | { 26 | "properties": { 27 | "newProperty": { 28 | "type": "string" 29 | } 30 | }, 31 | "required": [ 32 | "newProperty" 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /packages/jsonschema/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { readFileSync } from "fs"; 3 | 4 | import type $RefParser from "@apidevtools/json-schema-ref-parser"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const jsYaml = require("js-yaml"); 8 | type JSONSchema = NonNullable<$RefParser["schema"]>; 9 | 10 | export function loadSchema(file: string): JSONSchema { 11 | return loadFile(file); 12 | } 13 | 14 | export function getAssetsPath(file?: string): string { 15 | return file ? 16 | path.join(__dirname, "assets", file) : 17 | path.join(__dirname, "assets"); 18 | } 19 | 20 | function loadFile(file: string): T { 21 | if (file.endsWith(".json")) { 22 | return require("./assets/" + file); 23 | } 24 | 25 | if (file.endsWith(".yml") || file.endsWith(".yaml")) { 26 | return jsYaml.load(readFileSync(path.join(__dirname, "assets", file), "utf8")); 27 | } 28 | 29 | throw new Error("Unsupported extension: " + file); 30 | } -------------------------------------------------------------------------------- /packages/jsonschema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/openapi-client/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/openapi-client/README.md: -------------------------------------------------------------------------------- 1 | # @spec2ts/openapi-client 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@spec2ts/openapi-client.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/openapi-client) 4 | [![NPM download](https://img.shields.io/npm/dm/@spec2ts/openapi-client.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/openapi-client) 5 | [![Build Status](https://travis-ci.org/touchifyapp/spec2ts.svg?branch=master)](https://travis-ci.org/touchifyapp/spec2ts) 6 | 7 | `@spec2ts/openapi-client` is an utility to create a TypeScript HTTP Client with entity types from OpenAPI v3 specification. Unlike other code generators `@spec2ts/openapi-client` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree (AST). 8 | 9 | ## Features 10 | 11 | * **AST-based:** Unlike other code generators `@spec2ts/openapi-client` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree. 12 | * **Tree-shakeable:** Individually exported types allows you to bundle only the ones you actually use. 13 | * **YAML or JSON:** Use YAML or JSON for your OpenAPI v3 specification. 14 | * **External references:** Resolves automatically external references and bundle or import them in generated files. 15 | * **Implementation agnostic:** Use generated types in any projet or framework. 16 | 17 | ## Installation 18 | 19 | Install in your project: 20 | ```bash 21 | npm install @spec2ts/openapi-client 22 | ``` 23 | 24 | ## CLI Usage 25 | 26 | ``` 27 | oapi2tsclient 28 | 29 | Generate TypeScript HTTP client from OpenAPI specification 30 | 31 | Positionals: 32 | input Path to OpenAPI Specification(s) to convert to TypeScript HTTP client 33 | [string] 34 | 35 | Options: 36 | --version Show version number [boolean] 37 | --help Show help usage [boolean] 38 | --output, -o Output file for generated client [string] 39 | --cwd, -c Root directory for resolving $refs [string] 40 | --avoidAny Avoid the `any` type and use `unknown` instead [boolean] 41 | --inlineRequired Create a method argument for each required parameter 42 | [boolean] 43 | --importFetch Use a custom fetch implementation 44 | [choices: "node-fetch", "cross-fetch", "isomorphic-fetch"] 45 | --packageName Generate a package.json with given name [string] 46 | --packageVersion Sets the version of the package.json [string] 47 | --packageAuthor Sets the author of the package.json [string] 48 | --packageLicense Sets the license of the package.json [string] 49 | --packagePrivate Sets the package.json private [boolean] 50 | --banner, -b Comment prepended to the top of each generated file [string] 51 | ``` 52 | 53 | ## Programmatic Usage 54 | 55 | ```typescript 56 | import { printer } from "@spec2ts/core"; 57 | import { generateClientFile } from "@spec2ts/openapi-client"; 58 | 59 | async function generate(path: string): Promise { 60 | const result = await generateClientFile(path); 61 | return printer.printNodes(result); 62 | } 63 | ``` 64 | 65 | ## Compatibility Matrix 66 | 67 | | TypeScript version | spec2ts version | 68 | |--------------------|-----------------| 69 | | v3.x.x | v1 | 70 | | v4.x.x | v2 | 71 | 72 | ## License 73 | 74 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 75 | -------------------------------------------------------------------------------- /packages/openapi-client/bin/oapi2tsclient.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../cli"; 3 | -------------------------------------------------------------------------------- /packages/openapi-client/cli/command.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { promises as fs } from "fs"; 3 | 4 | import * as ts from "typescript"; 5 | 6 | import { 7 | Argv 8 | } from "yargs"; 9 | 10 | import { 11 | printer, 12 | cli 13 | } from "@spec2ts/core"; 14 | 15 | import { 16 | generateClientFromFile, 17 | OApiGeneratorOptions 18 | } from "../lib/openapi-generator"; 19 | 20 | export interface BuildClientFromOpenApiOptions extends OApiGeneratorOptions { 21 | input: string | string[]; 22 | output?: string; 23 | banner?: string; 24 | 25 | importFetchVersion?: string; 26 | importFormDataVersion?: string; 27 | 28 | packageName?: string; 29 | packageVersion?: string; 30 | packageAuthor?: string; 31 | packageLicense?: string; 32 | packagePrivate?: boolean; 33 | packageBuildTarget?: string; 34 | packageBuildModule?: string; 35 | } 36 | 37 | export const usage = "$0 "; 38 | 39 | export const describe = "Generate TypeScript HTTP client from OpenAPI specification"; 40 | 41 | export function builder(argv: Argv): Argv { 42 | return argv 43 | .positional("input", { 44 | array: true, 45 | type: "string", 46 | describe: "Path to OpenAPI Specification(s) to convert to TypeScript HTTP client", 47 | demandOption: true 48 | }) 49 | 50 | .option("output", { 51 | type: "string", 52 | alias: "o", 53 | describe: "Output file for generated client" 54 | }) 55 | .option("cwd", { 56 | type: "string", 57 | alias: "c", 58 | describe: "Root directory for resolving $refs" 59 | }) 60 | 61 | .option("baseUrl", { 62 | type: "string", 63 | describe: "Base url of the server" 64 | }) 65 | .option("prefix", { 66 | type: "string", 67 | describe: "Only generate paths with this prefix" 68 | }) 69 | 70 | .option("avoidAny", { 71 | type: "boolean", 72 | describe: "Avoid the `any` type and use `unknown` instead" 73 | }) 74 | .option("enableDate", { 75 | choices: [true, "strict", "lax"] as const, 76 | describe: "Build `Date` for format `date` and `date-time`" 77 | }) 78 | 79 | .option("inlineRequired", { 80 | type: "boolean", 81 | describe: "Create a method argument for each required parameter" 82 | }) 83 | .option("importFetch", { 84 | choices: ["node-fetch", "cross-fetch", "isomorphic-fetch"], 85 | describe: "Use a custom fetch implementation" 86 | }) 87 | .option("typesPath", { 88 | type: "string", 89 | describe: "Generate client types in external file relative to the output file" 90 | }) 91 | 92 | .option("importFetchVersion", { 93 | type: "string", 94 | describe: "Use a custom fetch implementation version" 95 | }) 96 | .option("importFormDataVersion", { 97 | type: "string", 98 | describe: "Use a custom form-data implementation version" 99 | }) 100 | 101 | 102 | .option("packageName", { 103 | type: "string", 104 | describe: "Generate a package.json with given name" 105 | }) 106 | .option("packageVersion", { 107 | type: "string", 108 | describe: "Set the version of the package.json" 109 | }) 110 | .option("packageAuthor", { 111 | type: "string", 112 | describe: "Set the author of the package.json" 113 | }) 114 | .option("packageLicense", { 115 | type: "string", 116 | describe: "Set the license of the package.json" 117 | }) 118 | .option("packagePrivate", { 119 | type: "boolean", 120 | describe: "Set the package.json private" 121 | }) 122 | .option("packageBuildTarget", { 123 | type: "string", 124 | describe: "Set the TypeScript build target" 125 | }) 126 | .option("packageBuildModule", { 127 | type: "string", 128 | describe: "Set the TypeScript build module" 129 | }) 130 | 131 | .option("banner", { 132 | type: "string", 133 | alias: "b", 134 | describe: "Comment prepended to the top of each generated file" 135 | }) as Argv; 136 | } 137 | 138 | export async function handler(options: BuildClientFromOpenApiOptions): Promise { 139 | const files = await cli.findFiles(options.input); 140 | 141 | for (const file of files) { 142 | const output = options.output || cli.getOutputPath(file, options); 143 | await cli.mkdirp(output); 144 | 145 | if (options.typesPath) { 146 | if (!options.typesPath.startsWith(".")) { 147 | options.typesPath = "./" + options.typesPath; 148 | } 149 | 150 | const res = await generateClientFromFile(file, options as BuildClientFromOpenApiOptions & { typesPath: string }); 151 | printFile(res.client, output, options); 152 | 153 | const outputTypes = path.resolve(path.dirname(output), options.typesPath + ".ts"); 154 | printFile(res.types, outputTypes, options); 155 | } 156 | else { 157 | const sourceFile = await generateClientFromFile(file, options); 158 | printFile(sourceFile, output, options); 159 | } 160 | 161 | if (options.packageName) { 162 | await generatePackage(output, options); 163 | } 164 | } 165 | } 166 | 167 | async function printFile(file: ts.SourceFile, output: string, options: BuildClientFromOpenApiOptions): Promise { 168 | const content = printer.printFile(file); 169 | await cli.mkdirp(output); 170 | 171 | await cli.writeFile( 172 | output, 173 | (options.banner || defaultBanner()) + 174 | "\n\n" + 175 | content 176 | ); 177 | } 178 | 179 | function defaultBanner(): string { 180 | return `/** 181 | * DO NOT MODIFY 182 | * Generated using @spec2ts/openapi-client. 183 | * See https://www.npmjs.com/package/@spec2ts/openapi-client 184 | */ 185 | 186 | /* eslint-disable */`; 187 | } 188 | 189 | async function generatePackage(output: string, options: BuildClientFromOpenApiOptions): Promise { 190 | const outputDir = path.dirname(output); 191 | const main = path.relative(outputDir, output); 192 | 193 | const pkg: Record = { 194 | name: options.packageName, 195 | version: options.packageVersion || "1.0.0", 196 | description: "OpenAPI v3 client for " + options.packageName, 197 | author: options.packageAuthor || "@spec2ts/openapi-client", 198 | license: options.packageLicense || "UNLICENSED", 199 | main: main.replace(/\.ts$/, ".js"), 200 | files: ["*.js", "*.d.ts"], 201 | scripts: { 202 | build: `tsc ${main} --strict --target ${options.packageBuildTarget || "ES2018"} --module ${options.packageBuildModule || "UMD"} --moduleResolution node --skipLibCheck`, 203 | prepublishOnly: "npm run build" 204 | }, 205 | dependencies: {}, 206 | devDependencies: { 207 | typescript: "^4.2.0" 208 | } 209 | }; 210 | 211 | if (options.importFetch) { 212 | pkg.dependencies[options.importFetch] = options.importFetchVersion || "*"; 213 | pkg.dependencies["form-data"] = options.importFormDataVersion || "*"; 214 | pkg.devDependencies["@types/node"] = "*"; 215 | } 216 | 217 | if (options.packagePrivate) { 218 | pkg.private = options.packagePrivate; 219 | } 220 | 221 | await fs.writeFile(path.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2), "utf8"); 222 | } 223 | -------------------------------------------------------------------------------- /packages/openapi-client/cli/index.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from "yargs"; 2 | 3 | import { 4 | usage, 5 | describe, 6 | builder, 7 | handler 8 | } from "../cli/command"; 9 | 10 | yargs 11 | .command(usage, describe, builder, handler) 12 | .help("help", "Show help usage") 13 | .demandCommand() 14 | .argv; 15 | -------------------------------------------------------------------------------- /packages/openapi-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/openapi-generator"; 2 | -------------------------------------------------------------------------------- /packages/openapi-client/lib/core-generator.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as core from "@spec2ts/core"; 3 | 4 | import type { 5 | OpenAPIObject, 6 | ParameterObject, 7 | PathItemObject, 8 | OperationObject 9 | } from "openapi3-ts/oas31"; 10 | 11 | import { 12 | ParserContext, 13 | resolveReference 14 | } from "@spec2ts/jsonschema/lib/core-parser"; 15 | 16 | import { 17 | parseServers, 18 | defaultBaseUrl 19 | } from "./server-parser"; 20 | 21 | import { 22 | Method, 23 | OApiGeneratorContext, 24 | isMethod, 25 | parseOperation 26 | } from "./core-parser"; 27 | 28 | import { camelCase } from "./util"; 29 | 30 | export type Formatter = "space" | "pipe" | "deep" | "explode" | "form"; 31 | 32 | //#region Public 33 | 34 | export function generateServers(file: ts.SourceFile, { servers }: OpenAPIObject, context: OApiGeneratorContext): ts.SourceFile { 35 | servers = servers || []; 36 | 37 | if (context.options.baseUrl) servers = [{ url: context.options.baseUrl }]; 38 | 39 | const serversConst = core.findFirstVariableStatement(file.statements, "servers"); 40 | const defaultsConst = core.findFirstVariableStatement(file.statements, "defaults"); 41 | 42 | if (!serversConst || !defaultsConst) { 43 | throw new Error("Invalid template: missing servers or defaults const"); 44 | } 45 | 46 | file = core.replaceSourceFileStatement( 47 | file, 48 | serversConst, 49 | core.updateVariableStatementValue(serversConst, "servers", parseServers(servers)) 50 | ); 51 | 52 | file = core.replaceSourceFileStatement( 53 | file, 54 | defaultsConst, 55 | core.updateVariableStatementPropertyValue( 56 | defaultsConst, 57 | "defaults", 58 | "baseUrl", 59 | defaultBaseUrl(servers) 60 | ) 61 | ); 62 | 63 | return file; 64 | } 65 | 66 | export function generateDefaults(file: ts.SourceFile, context: OApiGeneratorContext): ts.SourceFile { 67 | if (context.options.importFetch) { 68 | const defaultsConst = core.findFirstVariableStatement(file.statements, "defaults"); 69 | if (!defaultsConst) { 70 | throw new Error("Invalid template: missing defaults const"); 71 | } 72 | 73 | file = core.prependSourceFileStatements( 74 | file, 75 | 76 | core.createDefaultImportDeclaration({ 77 | moduleSpecifier: context.options.importFetch, 78 | name: "fetch", 79 | bindings: ["RequestInit", "Headers"] 80 | }), 81 | 82 | core.createNamespaceImportDeclaration({ 83 | moduleSpecifier: "form-data", 84 | name: "FormData" 85 | }) 86 | ); 87 | 88 | file = core.replaceSourceFileStatement( 89 | file, 90 | defaultsConst, 91 | core.updateVariableStatementPropertyValue( 92 | defaultsConst, 93 | "defaults", 94 | "fetch", 95 | core.toExpression("fetch") 96 | ) 97 | ); 98 | } 99 | 100 | return file; 101 | } 102 | 103 | export function generateFunctions(file: ts.SourceFile, spec: OpenAPIObject, context: OApiGeneratorContext): ts.SourceFile { 104 | const paths: typeof spec.paths = Object.fromEntries(Object.entries(spec.paths ?? {}) 105 | .filter(([path]) => !context.options.prefix || path.startsWith(context.options.prefix))); 106 | 107 | const functions: ts.FunctionDeclaration[] = Object.entries(paths).map(([path, pathSpec]) => { 108 | const item = resolveReference(pathSpec, context); 109 | 110 | return Object.entries(item) 111 | .filter(([verb,]) => isMethod(verb.toUpperCase())) 112 | .map(([verb, entry]) => generateFunction(path, item, (verb.toUpperCase() as Method), entry, context)); 113 | }).flat(); 114 | 115 | if (context.options.typesPath && context.typesFile) { 116 | context.typesFile = core.updateSourceFileStatements(context.typesFile, context.aliases); 117 | 118 | file = core.updateSourceFileStatements(file, [ 119 | core.createNamedImportDeclaration({ 120 | moduleSpecifier: context.options.typesPath, 121 | bindings: context.aliases.map(a => a.name.text) 122 | }), 123 | ...file.statements, 124 | ...functions 125 | ]); 126 | } 127 | else { 128 | file = core.appendSourceFileStatements( 129 | file, 130 | ...context.aliases, 131 | ...functions 132 | ); 133 | } 134 | 135 | return file; 136 | } 137 | 138 | function generateFunction(path: string, item: PathItemObject, method: Method, operation: OperationObject, context: ParserContext): ts.FunctionDeclaration { 139 | const { 140 | name, 141 | query, 142 | header, 143 | paramsVars, 144 | args, 145 | bodyMode, 146 | bodyVar, 147 | response, 148 | responseVoid, 149 | responseJSON 150 | } = parseOperation(path, item, method, operation, context); 151 | 152 | const qs = generateQs(query, paramsVars); 153 | const url = generateUrl(path, qs); 154 | 155 | const init: ts.ObjectLiteralElementLike[] = [ 156 | ts.factory.createSpreadAssignment(ts.factory.createIdentifier("options")), 157 | ]; 158 | 159 | if (method !== "GET") { 160 | init.push( 161 | ts.factory.createPropertyAssignment("method", ts.factory.createStringLiteral(method)) 162 | ); 163 | } 164 | 165 | if (bodyVar) { 166 | init.push( 167 | core.createPropertyAssignment("body", ts.factory.createIdentifier(bodyVar)) 168 | ); 169 | } 170 | 171 | if (header.length) { 172 | init.push( 173 | ts.factory.createPropertyAssignment( 174 | "headers", 175 | ts.factory.createObjectLiteralExpression( 176 | [ 177 | ts.factory.createSpreadAssignment( 178 | ts.factory.createPropertyAccessChain( 179 | ts.factory.createIdentifier("options"), 180 | core.questionDotToken, 181 | ts.factory.createIdentifier("headers") 182 | ) 183 | ), 184 | ...header.map(({ name }) => 185 | core.createPropertyAssignment( 186 | name, 187 | ts.factory.createIdentifier(paramsVars[name]) 188 | ) 189 | ), 190 | ], 191 | true 192 | ) 193 | ) 194 | ); 195 | } 196 | 197 | const fetchArgs: ts.Expression[] = [url]; 198 | 199 | if (init.length) { 200 | const initObj = ts.factory.createObjectLiteralExpression(init, true); 201 | fetchArgs.push(bodyMode ? callFunction("http", bodyMode, [initObj]) : initObj); 202 | } 203 | 204 | return core.addComment( 205 | core.createFunctionDeclaration( 206 | name, 207 | { 208 | modifiers: [core.modifier.export, core.modifier.async], 209 | type: ts.factory.createTypeReferenceNode("Promise", [ 210 | ts.factory.createTypeReferenceNode("ApiResponse", [response]) 211 | ]) 212 | }, 213 | args, 214 | core.block( 215 | ts.factory.createReturnStatement( 216 | ts.factory.createAwaitExpression( 217 | callFunction( 218 | "http", 219 | responseJSON ? "fetchJson" : 220 | responseVoid ? "fetchVoid" : 221 | "fetch", 222 | fetchArgs 223 | ) 224 | ) 225 | ) 226 | ) 227 | ), 228 | operation.summary || operation.description 229 | ); 230 | } 231 | 232 | function generateQs(parameters: ParameterObject[], paramsVars: Record): ts.CallExpression | undefined { 233 | if (!parameters.length) { 234 | return; 235 | } 236 | 237 | const paramsByFormatter = groupByFormatter(parameters); 238 | 239 | return callFunction( 240 | "QS", "query", 241 | Object.entries(paramsByFormatter).map(([format, params]) => { 242 | return callFunction("QS", format, [ 243 | core.createObjectLiteral( 244 | params.map((p) => [p.name, paramsVars[p.name]]) 245 | ), 246 | ]); 247 | }) 248 | ); 249 | } 250 | 251 | function generateUrl(path: string, qs?: ts.CallExpression): ts.Expression { 252 | const spans: Array<{ expression: ts.Expression; literal: string }> = []; 253 | // Use a replacer function to collect spans as a side effect: 254 | const head = path.replace( 255 | /(.*?)\{(.+?)\}(.*?)(?=\{|$)/g, 256 | (_, head, name, literal) => { 257 | const expression = camelCase(name); 258 | spans.push({ expression: ts.factory.createIdentifier(expression), literal }); 259 | return head; 260 | } 261 | ); 262 | 263 | if (qs) { 264 | // add the query string as last span 265 | spans.push({ expression: qs, literal: "" }); 266 | } 267 | 268 | return core.createTemplateString(head, spans); 269 | } 270 | 271 | //#endregion 272 | 273 | //#region Utils 274 | 275 | function callFunction(ns: string, name: string, args: ts.Expression[]): ts.CallExpression { 276 | return core.createCall( 277 | ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(ns), name), 278 | { args } 279 | ); 280 | } 281 | 282 | function groupByFormatter(parameters: ParameterObject[]): Record { 283 | const res: Record = {}; 284 | 285 | parameters.forEach(param => { 286 | const formatter = getFormatter(param); 287 | res[formatter] = res[formatter] || []; 288 | res[formatter].push(param); 289 | }); 290 | 291 | return res; 292 | } 293 | 294 | function getFormatter({ style, explode }: ParameterObject): Formatter { 295 | if (style === "spaceDelimited") return "space"; 296 | if (style === "pipeDelimited") return "pipe"; 297 | if (style === "deepObject") return "deep"; 298 | 299 | if (style === "form") { 300 | return explode === false ? "form" : "explode"; 301 | } 302 | 303 | return explode ? "explode" : "form"; 304 | } 305 | 306 | //#endregion 307 | -------------------------------------------------------------------------------- /packages/openapi-client/lib/openapi-generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as ts from "typescript"; 3 | import * as core from "@spec2ts/core"; 4 | import $RefParser from "@apidevtools/json-schema-ref-parser"; 5 | 6 | import type { 7 | OpenAPIObject 8 | } from "openapi3-ts/oas31"; 9 | 10 | import { 11 | ParserOptions, 12 | createContext 13 | } from "@spec2ts/jsonschema/lib/core-parser"; 14 | 15 | import { 16 | parseReference 17 | } from "@spec2ts/openapi/lib/core-parser"; 18 | 19 | import { 20 | OApiGeneratorContext 21 | } from "./core-parser"; 22 | 23 | import { 24 | generateServers, 25 | generateDefaults, 26 | generateFunctions 27 | } from "./core-generator"; 28 | 29 | export interface OApiGeneratorOptions extends ParserOptions { 30 | inlineRequired?: boolean; 31 | importFetch?: "node-fetch" | "cross-fetch" | "isomorphic-fetch"; 32 | typesPath?: string; 33 | baseUrl?: string; 34 | prefix?: string; 35 | } 36 | 37 | export async function generateClientFromFile(file: string, options: OApiGeneratorOptions & { typesPath: string }): Promise; 38 | export async function generateClientFromFile(file: string, options?: OApiGeneratorOptions): Promise; 39 | export async function generateClientFromFile(file: string, options: OApiGeneratorOptions = {}): Promise { 40 | const schema = await $RefParser.parse(file) as OpenAPIObject; 41 | 42 | return generateClient(schema, { 43 | cwd: path.resolve(path.dirname(file)) + "/", 44 | ...options 45 | }); 46 | } 47 | 48 | export async function generateClient(spec: OpenAPIObject, options: OApiGeneratorOptions & { typesPath: string }): Promise; 49 | export async function generateClient(spec: OpenAPIObject, options?: OApiGeneratorOptions): Promise; 50 | export async function generateClient(spec: OpenAPIObject, options: OApiGeneratorOptions = {}): Promise { 51 | if (!options.parseReference) { 52 | options.parseReference = parseReference; 53 | } 54 | 55 | const context = await createContext(spec, options) as OApiGeneratorContext; 56 | let file = await core.createSourceFileFromFile(__dirname + "/templates/_client.tpl.ts"); 57 | 58 | if (context.options.typesPath) { 59 | context.typesFile = ts.createSourceFile("types.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); 60 | } 61 | 62 | file = generateServers(file, spec, context); 63 | file = generateDefaults(file, context); 64 | file = generateFunctions(file, spec, context); 65 | 66 | if (context.options.typesPath) { 67 | return { 68 | client: file, 69 | types: context.typesFile as ts.SourceFile 70 | }; 71 | } 72 | 73 | return file; 74 | } 75 | 76 | export interface SeparatedClientResult { 77 | client: ts.SourceFile; 78 | types: ts.SourceFile; 79 | } 80 | -------------------------------------------------------------------------------- /packages/openapi-client/lib/server-parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as core from "@spec2ts/core"; 3 | 4 | import type { 5 | ServerObject, 6 | ServerVariableObject 7 | } from "openapi3-ts/oas31"; 8 | 9 | import { camelCase } from "./util"; 10 | 11 | export function parseServers(servers: ServerObject[]): ts.ObjectLiteralExpression { 12 | const props = servers.map((server, i) => [serverName(server, i), generateServerExpression(server)] as [string, ts.Expression]); 13 | return core.createObjectLiteral(props); 14 | } 15 | 16 | export function defaultBaseUrl(servers: ServerObject[]): ts.StringLiteral { 17 | return ts.factory.createStringLiteral(defaultUrl(servers[0])); 18 | } 19 | 20 | function serverName(server: ServerObject, index: number): string { 21 | return server.description ? 22 | camelCase(server.description.replace(/\W+/, " ")) : 23 | `server${index + 1}`; 24 | } 25 | 26 | function generateServerExpression(server: ServerObject): ts.Expression { 27 | return server.variables ? 28 | createServerFunction(server.url, server.variables) : 29 | ts.factory.createStringLiteral(server.url); 30 | } 31 | 32 | function createServerFunction(template: string, vars: Record): ts.ArrowFunction { 33 | const params = [ 34 | core.createParameter( 35 | core.createObjectBinding( 36 | Object.entries(vars || {}).map(([name, value]) => { 37 | return { 38 | name, 39 | initializer: createLiteral(value.default), 40 | }; 41 | }) 42 | ), 43 | { 44 | type: ts.factory.createTypeLiteralNode( 45 | Object.entries(vars || {}).map(([name, value]) => { 46 | return core.createPropertySignature({ 47 | name, 48 | type: value.enum ? 49 | ts.factory.createUnionTypeNode(createUnion(value.enum)) : 50 | ts.factory.createUnionTypeNode([ 51 | core.keywordType.string, 52 | core.keywordType.number, 53 | core.keywordType.boolean, 54 | ]), 55 | }); 56 | }) 57 | ), 58 | } 59 | ), 60 | ]; 61 | 62 | return core.createArrowFunction(params, createTemplate(template)); 63 | } 64 | 65 | 66 | function createUnion(strs: Array): ts.LiteralTypeNode[] { 67 | return strs.map((e) => ts.factory.createLiteralTypeNode(createLiteral(e))); 68 | } 69 | 70 | function createTemplate(url: string): ts.TemplateLiteral { 71 | const tokens = url.split(/{([\s\S]+?)}/g); 72 | const spans: ts.TemplateSpan[] = []; 73 | const len = tokens.length; 74 | 75 | for (let i = 1; i < len; i += 2) { 76 | spans.push( 77 | ts.factory.createTemplateSpan( 78 | ts.factory.createIdentifier(tokens[i]), 79 | (i === len - 2 ? ts.factory.createTemplateTail : ts.factory.createTemplateMiddle)(tokens[i + 1]) 80 | ) 81 | ); 82 | } 83 | 84 | return ts.factory.createTemplateExpression(ts.factory.createTemplateHead(tokens[0]), spans); 85 | } 86 | 87 | function createLiteral(v: string | boolean | number): ts.StringLiteral | ts.BooleanLiteral | ts.NumericLiteral { 88 | switch (typeof v) { 89 | case "string": 90 | return ts.factory.createStringLiteral(v); 91 | case "boolean": 92 | return v ? ts.factory.createTrue() : ts.factory.createFalse(); 93 | case "number": 94 | return ts.factory.createNumericLiteral(String(v)); 95 | } 96 | } 97 | 98 | function defaultUrl(server?: ServerObject): string { 99 | if (!server) return "/"; 100 | 101 | const { url, variables } = server; 102 | if (!variables) return url; 103 | 104 | return url.replace( 105 | /\{(.+?)\}/g, 106 | (m, name) => variables[name] ? String(variables[name].default) : m 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /packages/openapi-client/lib/templates/_client.tpl.ts: -------------------------------------------------------------------------------- 1 | export const defaults: RequestOptions = { 2 | baseUrl: "/", 3 | }; 4 | 5 | export const servers = {}; 6 | 7 | export type RequestOptions = { 8 | baseUrl?: string; 9 | fetch?: typeof fetch; 10 | headers?: Record; 11 | } & Omit; 12 | 13 | export type ApiResponse = { 14 | status: number; 15 | statusText: string; 16 | headers: Record; 17 | data: T; 18 | }; 19 | 20 | type Encoders = Array<(s: string) => string>; 21 | type TagFunction = (strings: TemplateStringsArray, ...values: any[]) => string; 22 | 23 | type FetchRequestOptions = RequestOptions & { 24 | body?: string | FormData; 25 | }; 26 | 27 | type JsonRequestOptions = RequestOptions & { 28 | body: unknown; 29 | }; 30 | 31 | type FormRequestOptions> = RequestOptions & { 32 | body: T; 33 | }; 34 | 35 | type MultipartRequestOptions = RequestOptions & { 36 | body: Record; // string | Blob 37 | }; 38 | 39 | /** Utilities functions */ 40 | export const _ = { 41 | // Encode param names and values as URIComponent 42 | encodeReserved: [encodeURI, encodeURIComponent], 43 | allowReserved: [encodeURI, encodeURI], 44 | 45 | /** Deeply remove all properties with undefined values. */ 46 | stripUndefined, U>(obj?: T): Record | undefined { 47 | return obj && JSON.parse(JSON.stringify(obj)); 48 | }, 49 | 50 | isEmpty(v: unknown): boolean { 51 | return typeof v === "object" && !!v ? 52 | Object.keys(v).length === 0 && v.constructor === Object : 53 | v === undefined; 54 | }, 55 | 56 | /** Creates a tag-function to encode template strings with the given encoders. */ 57 | encode(encoders: Encoders, delimiter = ","): TagFunction { 58 | return (strings: TemplateStringsArray, ...values: any[]) => { 59 | return strings.reduce((prev, s, i) => `${prev}${s}${q(values[i] ?? "", i)}`, ""); 60 | }; 61 | 62 | function q(v: any, i: number): string { 63 | const encoder = encoders[i % encoders.length]; 64 | if (typeof v === "object") { 65 | if (Array.isArray(v)) { 66 | return v.map(encoder).join(delimiter); 67 | } 68 | const flat = Object.entries(v).reduce( 69 | (flat, entry) => [...flat, ...entry], 70 | [] as any 71 | ); 72 | return flat.map(encoder).join(delimiter); 73 | } 74 | 75 | return encoder(String(v)); 76 | } 77 | }, 78 | 79 | /** Separate array values by the given delimiter. */ 80 | delimited(delimiter = ","): (params: Record, encoders?: Encoders) => string { 81 | return (params: Record, encoders = _.encodeReserved) => 82 | Object.entries(params) 83 | .filter(([, value]) => !_.isEmpty(value)) 84 | .map(([name, value]) => _.encode(encoders, delimiter)`${name}=${value}`) 85 | .join("&"); 86 | }, 87 | 88 | /** Join URLs parts. */ 89 | joinUrl(...parts: Array): string { 90 | return parts 91 | .filter(Boolean) 92 | .join("/") 93 | .replace(/([^:]\/)\/+/, "$1"); 94 | } 95 | }; 96 | 97 | /** Functions to serialize query parameters in different styles. */ 98 | export const QS = { 99 | /** Join params using an ampersand and prepends a questionmark if not empty. */ 100 | query(...params: string[]): string { 101 | const s = params.filter(p => !!p).join("&"); 102 | return s && `?${s}`; 103 | }, 104 | 105 | /** 106 | * Serializes nested objects according to the `deepObject` style specified in 107 | * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#style-values 108 | */ 109 | deep(params: Record, [k, v] = _.encodeReserved): string { 110 | const qk = _.encode([(s) => s, k]); 111 | const qv = _.encode([(s) => s, v]); 112 | // don't add index to arrays 113 | // https://github.com/expressjs/body-parser/issues/289 114 | const visit = (obj: any, prefix = ""): string => 115 | Object.entries(obj) 116 | .filter(([, v]) => !_.isEmpty(v)) 117 | .map(([prop, v]) => { 118 | const isValueObject = typeof v === "object"; 119 | const index = Array.isArray(obj) && !isValueObject ? "" : prop; 120 | const key = prefix ? qk`${prefix}[${index}]` : prop; 121 | if (isValueObject) { 122 | return visit(v, key); 123 | } 124 | return qv`${key}=${v}`; 125 | }) 126 | .join("&"); 127 | 128 | return visit(params); 129 | }, 130 | 131 | /** 132 | * Property values of type array or object generate separate parameters 133 | * for each value of the array, or key-value-pair of the map. 134 | * For other types of properties this property has no effect. 135 | * See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#encoding-object 136 | */ 137 | explode(params: Record, encoders = _.encodeReserved): string { 138 | const q = _.encode(encoders); 139 | return Object.entries(params) 140 | .filter(([, value]) => typeof value !== "undefined") 141 | .map(([name, value]) => { 142 | if (Array.isArray(value)) { 143 | return value.map((v) => q`${name}=${v}`).join("&"); 144 | } 145 | 146 | if (typeof value === "object") { 147 | return QS.explode(value, encoders); 148 | } 149 | 150 | return q`${name}=${value}`; 151 | }) 152 | .join("&"); 153 | }, 154 | 155 | form: _.delimited(), 156 | pipe: _.delimited("|"), 157 | space: _.delimited("%20"), 158 | }; 159 | 160 | /** Http request base methods. */ 161 | export const http = { 162 | async fetch(url: string, req?: FetchRequestOptions): Promise> { 163 | const { baseUrl, headers, fetch: customFetch, ...init } = { ...defaults, ...req }; 164 | const href = _.joinUrl(baseUrl, url); 165 | const res = await (customFetch || fetch)(href, { 166 | ...init, 167 | headers: _.stripUndefined({ ...defaults.headers, ...headers }), 168 | }); 169 | 170 | let text: string | undefined; 171 | try { text = await res.text(); } 172 | catch (err) { /* ok */ } 173 | 174 | if (!res.ok) { 175 | throw new HttpError(res.status, res.statusText, href, res.headers, text); 176 | } 177 | 178 | return { 179 | status: res.status, 180 | statusText: res.statusText, 181 | headers: http.headers(res.headers), 182 | data: text 183 | }; 184 | }, 185 | 186 | async fetchJson(url: string, req: FetchRequestOptions = {}): Promise> { 187 | const res = await http.fetch(url, { 188 | ...req, 189 | headers: { 190 | ...req.headers, 191 | Accept: "application/json", 192 | }, 193 | }); 194 | 195 | res.data = res.data && JSON.parse(res.data); 196 | return res; 197 | }, 198 | 199 | async fetchVoid(url: string, req: FetchRequestOptions = {}): Promise> { 200 | const res = await http.fetch(url, { 201 | ...req, 202 | headers: { 203 | ...req.headers, 204 | Accept: "application/json", 205 | }, 206 | }); 207 | 208 | return res as ApiResponse; 209 | }, 210 | 211 | json({ body, headers, ...req }: JsonRequestOptions): FetchRequestOptions { 212 | return { 213 | ...req, 214 | body: JSON.stringify(body), 215 | headers: { 216 | ...headers, 217 | "Content-Type": "application/json", 218 | }, 219 | }; 220 | }, 221 | 222 | form>({ body, headers, ...req }: FormRequestOptions): FetchRequestOptions { 223 | return { 224 | ...req, 225 | body: QS.form(body), 226 | headers: { 227 | ...headers, 228 | "Content-Type": "application/x-www-form-urlencoded", 229 | }, 230 | }; 231 | }, 232 | 233 | multipart({ body, ...req }: MultipartRequestOptions): FetchRequestOptions { 234 | const data = new FormData(); 235 | Object.entries(body).forEach(([name, value]) => { 236 | data.append(name, value); 237 | }); 238 | return { 239 | ...req, 240 | body: data, 241 | }; 242 | }, 243 | 244 | headers(headers: Headers): Record { 245 | const res: Record = {}; 246 | headers.forEach((value, key) => res[key] = value); 247 | return res; 248 | } 249 | }; 250 | 251 | export class HttpError extends Error { 252 | status: number; 253 | statusText: string; 254 | headers: Record; 255 | data?: Record; 256 | 257 | constructor(status: number, statusText: string, url: string, headers: Headers, text?: string) { 258 | super(`${url} - ${statusText} (${status})`); 259 | this.status = status; 260 | this.statusText = statusText; 261 | this.headers = http.headers(headers); 262 | 263 | if (text) { 264 | try { this.data = JSON.parse(text); } 265 | catch (err) { /* ok */ } 266 | } 267 | } 268 | } 269 | 270 | /** Utility Type to extract returns type from a method. */ 271 | export type ApiResult = Fn extends (...args: any) => Promise> ? T : never; 272 | -------------------------------------------------------------------------------- /packages/openapi-client/lib/util.ts: -------------------------------------------------------------------------------- 1 | export function camelCase(str: string): string { 2 | const regex = /[A-Z\xC0-\xD6\xD8-\xDE_$]?[a-z\xDF-\xF6\xF8-\xFF_$]+|[A-Z\xC0-\xD6\xD8-\xDE_$]+(?![a-z\xDF-\xF6\xF8-\xFF_$])|\d+/g; 3 | const words = str.match(regex); 4 | if (!words) return ""; 5 | 6 | let result = ""; 7 | const len = words.length; 8 | 9 | for (let i = 0; i < len; i++) { 10 | const word = words[i]; 11 | let tmp = word.toLowerCase(); 12 | 13 | if (i !== 0) { 14 | tmp = tmp[0].toUpperCase() + tmp.substr(1); 15 | } 16 | 17 | result += tmp; 18 | } 19 | 20 | return result; 21 | } -------------------------------------------------------------------------------- /packages/openapi-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spec2ts/openapi-client", 3 | "version": "3.1.3", 4 | "description": "Utility to convert OpenAPI v3 specifications to Typescript HTTP client using TypeScript native compiler", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "bin": { 17 | "oapi2tsclient": "./bin/oapi2tsclient.js" 18 | }, 19 | "files": [ 20 | "*.js", 21 | "*.d.ts", 22 | "bin/**/*.js", 23 | "cli/**/*.js", 24 | "cli/**/*.d.ts", 25 | "lib/**/*.js", 26 | "lib/**/*.d.ts", 27 | "lib/templates/*.ts" 28 | ], 29 | "scripts": { 30 | "build": "npm run clean && npm run lint && npm run build:ts", 31 | "build:ts": "tsc -p .", 32 | "test": "npm run clean && npm run lint && npm run test:jest", 33 | "test:jest": "jest -c ../../jest.config.js --rootDir .", 34 | "test:coverage": "npm run test -- -- --coverage", 35 | "lint": "npm run lint:ts", 36 | "lint:ts": "eslint '*.ts' '{bin,cli,lib}/**/*.ts'", 37 | "lint:fix": "npm run lint -- -- --fix", 38 | "clean": "npm run clean:ts", 39 | "clean:ts": "del '*.{js,d.ts}' '{bin,cli,lib}/**/*.{js,d.ts}'", 40 | "prepublishOnly": "npm test && npm run build" 41 | }, 42 | "dependencies": { 43 | "@spec2ts/core": "^3.0.2", 44 | "@spec2ts/jsonschema": "^3.0.5", 45 | "@spec2ts/openapi": "^3.1.3", 46 | "openapi3-ts": "^4.1.2", 47 | "typescript": "^5.0.0", 48 | "yargs": "^17.7.2" 49 | }, 50 | "devDependencies": { 51 | "del-cli": "^5.1.0", 52 | "eslint": "^8.49.0", 53 | "jest": "^29.6.4" 54 | }, 55 | "keywords": [ 56 | "openapi", 57 | "specification", 58 | "openapi3", 59 | "spec", 60 | "typescript", 61 | "client", 62 | "http", 63 | "rest", 64 | "compile", 65 | "compiler", 66 | "ast", 67 | "transpile", 68 | "interface", 69 | "typing", 70 | "spec2ts", 71 | "share" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /packages/openapi-client/tests/assets/petstore-expanded.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | - name: X-Header 43 | in: header 44 | description: Custom header 45 | required: false 46 | schema: 47 | type: string 48 | responses: 49 | "200": 50 | description: pet response 51 | content: 52 | application/json: 53 | schema: 54 | type: array 55 | items: 56 | $ref: "#/components/schemas/Pet" 57 | default: 58 | description: unexpected error 59 | content: 60 | application/json: 61 | schema: 62 | $ref: "#/components/schemas/Error" 63 | post: 64 | description: Creates a new pet in the store. Duplicates are allowed 65 | operationId: addPet 66 | requestBody: 67 | description: Pet to add to the store 68 | required: true 69 | content: 70 | application/json: 71 | schema: 72 | $ref: "#/components/schemas/NewPet" 73 | responses: 74 | "200": 75 | description: pet response 76 | content: 77 | application/json: 78 | schema: 79 | $ref: "#/components/schemas/Pet" 80 | default: 81 | description: unexpected error 82 | content: 83 | application/json: 84 | schema: 85 | $ref: "#/components/schemas/Error" 86 | /pets/{id}: 87 | get: 88 | description: Returns a user based on a single ID, if the user does not have access to the pet 89 | operationId: find pet by id 90 | parameters: 91 | - name: id 92 | in: path 93 | description: ID of pet to fetch 94 | required: true 95 | schema: 96 | type: integer 97 | format: int64 98 | responses: 99 | "200": 100 | description: pet response 101 | content: 102 | application/json: 103 | schema: 104 | $ref: "#/components/schemas/Pet" 105 | default: 106 | description: unexpected error 107 | content: 108 | application/json: 109 | schema: 110 | $ref: "#/components/schemas/Error" 111 | delete: 112 | description: deletes a single pet based on the ID supplied 113 | operationId: deletePet 114 | parameters: 115 | - name: id 116 | in: path 117 | description: ID of pet to delete 118 | required: true 119 | schema: 120 | type: integer 121 | format: int64 122 | - name: force 123 | in: query 124 | description: Force delete 125 | required: false 126 | schema: 127 | type: boolean 128 | responses: 129 | "204": 130 | description: pet deleted 131 | default: 132 | description: unexpected error 133 | content: 134 | application/json: 135 | schema: 136 | $ref: "#/components/schemas/Error" 137 | components: 138 | schemas: 139 | Pet: 140 | allOf: 141 | - $ref: "#/components/schemas/NewPet" 142 | - type: object 143 | required: 144 | - id 145 | properties: 146 | id: 147 | type: integer 148 | format: int64 149 | 150 | NewPet: 151 | type: object 152 | required: 153 | - name 154 | properties: 155 | name: 156 | type: string 157 | tag: 158 | type: string 159 | 160 | Error: 161 | type: object 162 | required: 163 | - code 164 | - message 165 | properties: 166 | code: 167 | type: integer 168 | format: int32 169 | message: 170 | type: string 171 | -------------------------------------------------------------------------------- /packages/openapi-client/tests/assets/petstore.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | servers: 8 | - url: http://petstore.swagger.io/v1 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | operationId: listPets 14 | tags: 15 | - pets 16 | parameters: 17 | - name: limit 18 | in: query 19 | description: How many items to return at one time (max 100) 20 | required: false 21 | schema: 22 | type: integer 23 | format: int32 24 | responses: 25 | '200': 26 | description: A paged array of pets 27 | headers: 28 | x-next: 29 | description: A link to the next page of responses 30 | schema: 31 | type: string 32 | content: 33 | application/json: 34 | schema: 35 | $ref: "#/components/schemas/Pets" 36 | default: 37 | description: unexpected error 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/Error" 42 | post: 43 | summary: Create a pet 44 | operationId: createPets 45 | tags: 46 | - pets 47 | responses: 48 | '201': 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Error" 56 | /pets/{petId}: 57 | get: 58 | summary: Info for a specific pet 59 | operationId: showPetById 60 | tags: 61 | - pets 62 | parameters: 63 | - name: petId 64 | in: path 65 | required: true 66 | description: The id of the pet to retrieve 67 | schema: 68 | type: string 69 | responses: 70 | '200': 71 | description: Expected response to a valid request 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Pet" 76 | default: 77 | description: unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: "#/components/schemas/Error" 82 | components: 83 | schemas: 84 | Pet: 85 | type: object 86 | required: 87 | - id 88 | - name 89 | properties: 90 | id: 91 | type: integer 92 | format: int64 93 | name: 94 | type: string 95 | tag: 96 | type: string 97 | Pets: 98 | type: array 99 | items: 100 | $ref: "#/components/schemas/Pet" 101 | Error: 102 | type: object 103 | required: 104 | - code 105 | - message 106 | properties: 107 | code: 108 | type: integer 109 | format: int32 110 | message: 111 | type: string -------------------------------------------------------------------------------- /packages/openapi-client/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { readFileSync } from "fs"; 3 | 4 | import type { OpenAPIObject } from "openapi3-ts/oas31"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const jsYaml = require("js-yaml"); 8 | 9 | export function loadSpec(file: string): OpenAPIObject { 10 | return loadFile(file); 11 | } 12 | 13 | export function getAssetsPath(file?: string): string { 14 | return file ? 15 | path.join(__dirname, "assets", file) : 16 | path.join(__dirname, "assets"); 17 | } 18 | 19 | function loadFile(file: string): T { 20 | if (file.endsWith(".json")) { 21 | return require("./assets/" + file); 22 | } 23 | 24 | if (file.endsWith(".yml") || file.endsWith(".yaml")) { 25 | return jsYaml.load(readFileSync(path.join(__dirname, "assets", file), "utf8")); 26 | } 27 | 28 | throw new Error("Unsupported extension: " + file); 29 | } -------------------------------------------------------------------------------- /packages/openapi-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/openapi/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [3.1.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@3.1.2...@spec2ts/openapi@3.1.3) (2025-04-10) 7 | 8 | **Note:** Version bump only for package @spec2ts/openapi 9 | 10 | 11 | 12 | 13 | 14 | ## [3.1.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@3.1.1...@spec2ts/openapi@3.1.2) (2023-10-23) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * circular reference in external schema ([7762c71](https://github.com/touchifyapp/spec2ts/commit/7762c712a0cdffa2d0a12852979c9f33be645207)) 20 | 21 | 22 | 23 | 24 | 25 | ## [3.1.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@3.1.0...@spec2ts/openapi@3.1.1) (2023-10-03) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * move typescript to runtime dependencies ([6e17d21](https://github.com/touchifyapp/spec2ts/commit/6e17d21187ee6d8af226a92595d6c93df04db2ea)) 31 | 32 | 33 | 34 | 35 | 36 | # [3.1.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@3.0.1...@spec2ts/openapi@3.1.0) (2023-09-25) 37 | 38 | 39 | ### Features 40 | 41 | * **openapi,openapi-client:** use openapi v3.1 ([0ee283b](https://github.com/touchifyapp/spec2ts/commit/0ee283baf028fa6891f00fb14e53bb2fbc41dc91)) 42 | 43 | 44 | 45 | 46 | 47 | ## [3.0.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@3.0.0...@spec2ts/openapi@3.0.1) (2023-09-12) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **jsonschema,openapi,openapi-client:** use ref parser as adviced ([e65a291](https://github.com/touchifyapp/spec2ts/commit/e65a2919f9d37ffdea773132dd906fca11b6240b)) 53 | 54 | 55 | 56 | 57 | 58 | # [3.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0...@spec2ts/openapi@3.0.0) (2023-09-12) 59 | 60 | 61 | ### Features 62 | 63 | * upgrade all dependencies ([9be17b6](https://github.com/touchifyapp/spec2ts/commit/9be17b69e2bd5d910bbaa88d4e2f161628fa4135)) 64 | 65 | 66 | 67 | 68 | 69 | # [2.0.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.8...@spec2ts/openapi@2.0.0) (2023-09-07) 70 | 71 | **Note:** Version bump only for package @spec2ts/openapi 72 | 73 | 74 | 75 | 76 | 77 | # [2.0.0-beta.8](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.7...@spec2ts/openapi@2.0.0-beta.8) (2022-09-21) 78 | 79 | 80 | ### Features 81 | 82 | * **openapi:** add option to disable date parsing in querystring ([e69547f](https://github.com/touchifyapp/spec2ts/commit/e69547ffdeb87cebe18cb622eee1d46829b35ad4)) 83 | 84 | 85 | 86 | 87 | 88 | # [2.0.0-beta.7](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.6...@spec2ts/openapi@2.0.0-beta.7) (2022-07-29) 89 | 90 | **Note:** Version bump only for package @spec2ts/openapi 91 | 92 | 93 | 94 | 95 | 96 | # [2.0.0-beta.6](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.5...@spec2ts/openapi@2.0.0-beta.6) (2022-01-27) 97 | 98 | **Note:** Version bump only for package @spec2ts/openapi 99 | 100 | 101 | 102 | 103 | 104 | # [2.0.0-beta.5](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.4...@spec2ts/openapi@2.0.0-beta.5) (2021-11-03) 105 | 106 | **Note:** Version bump only for package @spec2ts/openapi 107 | 108 | 109 | 110 | 111 | 112 | # [2.0.0-beta.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.3...@spec2ts/openapi@2.0.0-beta.4) (2021-10-23) 113 | 114 | 115 | ### Features 116 | 117 | * allow --ext option to specify output extension ([4c70ca1](https://github.com/touchifyapp/spec2ts/commit/4c70ca13f3fc12ce1fd16c0430c7f90f90b0ed64)) 118 | 119 | 120 | 121 | 122 | 123 | # [2.0.0-beta.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.2...@spec2ts/openapi@2.0.0-beta.3) (2021-02-27) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * **jsonschema:** avoid external references duplication ([9508d9e](https://github.com/touchifyapp/spec2ts/commit/9508d9eee0ae19523d03a2874bad73808ec5bf71)) 129 | 130 | 131 | 132 | 133 | 134 | # [2.0.0-beta.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.1...@spec2ts/openapi@2.0.0-beta.2) (2020-11-29) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * **openapi:** bad yargs options for enableDate ([84e7ec9](https://github.com/touchifyapp/spec2ts/commit/84e7ec9977d910b71e0e6e20b5eacc113b89d24b)) 140 | 141 | 142 | 143 | 144 | 145 | # [2.0.0-beta.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@2.0.0-beta.0...@spec2ts/openapi@2.0.0-beta.1) (2020-11-29) 146 | 147 | 148 | ### Features 149 | 150 | * **openapi:** add support for new lax date style ([4d7eb0b](https://github.com/touchifyapp/spec2ts/commit/4d7eb0baad32e83c7f9cbf4feeec01cd34ec3be3)) 151 | 152 | 153 | 154 | 155 | 156 | # [2.0.0-beta.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.2.4...@spec2ts/openapi@2.0.0-beta.0) (2020-11-13) 157 | 158 | 159 | ### Features 160 | 161 | * **global:** upgrade typescript 4 ([#16](https://github.com/touchifyapp/spec2ts/issues/16)) ([fcd82be](https://github.com/touchifyapp/spec2ts/commit/fcd82be93be3986a2f723680f1c52818eb7ba1bc)) 162 | 163 | 164 | ### BREAKING CHANGES 165 | 166 | * **global:** use typescript v4 167 | * **global:** updates are now immutable 168 | 169 | 170 | 171 | 172 | 173 | ## [1.2.4](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.2.3...@spec2ts/openapi@1.2.4) (2020-10-26) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * **jsonschema,openapi:** cover more nested refs cases ([1badafb](https://github.com/touchifyapp/spec2ts/commit/1badafbe0865a186ef5fc92bfc0ab5b334d4fa6e)) 179 | 180 | 181 | 182 | 183 | 184 | ## [1.2.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.2.2...@spec2ts/openapi@1.2.3) (2020-10-26) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * **jsonschema,openapi:** nested references not resolved ([9825a40](https://github.com/touchifyapp/spec2ts/commit/9825a405630c101e7a70452ce3a18e02ccad9ce8)), closes [#15](https://github.com/touchifyapp/spec2ts/issues/15) 190 | 191 | 192 | 193 | 194 | 195 | ## [1.2.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.2.1...@spec2ts/openapi@1.2.2) (2020-10-06) 196 | 197 | 198 | ### Bug Fixes 199 | 200 | * **openapi:** resolve response reference ([29e7ee5](https://github.com/touchifyapp/spec2ts/commit/29e7ee51a18049e2335eda08ceb68460b22de055)), closes [#7](https://github.com/touchifyapp/spec2ts/issues/7) 201 | 202 | 203 | 204 | 205 | 206 | ## [1.2.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.2.0...@spec2ts/openapi@1.2.1) (2020-05-27) 207 | 208 | **Note:** Version bump only for package @spec2ts/openapi 209 | 210 | 211 | 212 | 213 | 214 | # [1.2.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.1.3...@spec2ts/openapi@1.2.0) (2020-05-24) 215 | 216 | 217 | ### Features 218 | 219 | * **cli:** add default banner ([b0945e0](https://github.com/touchifyapp/spec2ts/commit/b0945e08b2c1da4dc494dca1890d491768a13e60)) 220 | 221 | 222 | 223 | 224 | 225 | ## [1.1.3](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.1.2...@spec2ts/openapi@1.1.3) (2020-05-18) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * **openapi:** fix missing await on mkdirp ([ba2ae64](https://github.com/touchifyapp/spec2ts/commit/ba2ae64805626b706f25f4caadec4bfb96a1055e)) 231 | 232 | 233 | 234 | 235 | 236 | ## [1.1.2](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.1.1...@spec2ts/openapi@1.1.2) (2020-05-16) 237 | 238 | 239 | ### Bug Fixes 240 | 241 | * **openapi:** fix avoid imports ([be6cf64](https://github.com/touchifyapp/spec2ts/commit/be6cf64e84588ee8773c2756fed0e24ea9d18ae1)) 242 | 243 | 244 | 245 | 246 | 247 | ## [1.1.1](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.1.0...@spec2ts/openapi@1.1.1) (2020-04-27) 248 | 249 | 250 | ### Bug Fixes 251 | 252 | * **openapi:** pass options to parser ([7b02e71](https://github.com/touchifyapp/spec2ts/commit/7b02e7146eafbf8dc2f0cf1fe97cc1051095df63)) 253 | 254 | 255 | 256 | 257 | 258 | # [1.1.0](https://github.com/touchifyapp/spec2ts/compare/@spec2ts/openapi@1.0.0...@spec2ts/openapi@1.1.0) (2020-04-27) 259 | 260 | 261 | ### Features 262 | 263 | * **openapi:** add lowerHeaders option ([7520550](https://github.com/touchifyapp/spec2ts/commit/752055038827457c5058578be0d1ddf01ffead04)) 264 | 265 | 266 | 267 | 268 | 269 | # 1.0.0 (2020-04-24) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * **release:** fix lerna publish ([fe36558](https://github.com/touchifyapp/spec2ts/commit/fe36558a1a2742e2e3d99aa08061ab9be0cf03f2)) 275 | 276 | 277 | ### Code Refactoring 278 | 279 | * **global:** improve project architecture ([2b2c0a1](https://github.com/touchifyapp/spec2ts/commit/2b2c0a1d98b78457520fff2c116b7f8d0e5c5df5)) 280 | 281 | 282 | ### Features 283 | 284 | * **jsonschema:** add cli command ([7592c43](https://github.com/touchifyapp/spec2ts/commit/7592c439be99fabb97cc270aa7a09794ee86f738)) 285 | * **openapi:** add openapi parser to monorepo ([e9ca537](https://github.com/touchifyapp/spec2ts/commit/e9ca5375e2692f909d32eacae653f918cd348040)) 286 | 287 | 288 | ### BREAKING CHANGES 289 | 290 | * **global:** force v1 291 | -------------------------------------------------------------------------------- /packages/openapi/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/openapi/README.md: -------------------------------------------------------------------------------- 1 | # @spec2ts/openapi 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@spec2ts/openapi.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/openapi) 4 | [![NPM download](https://img.shields.io/npm/dm/@spec2ts/openapi.svg?style=flat-square)](https://npmjs.org/package/@spec2ts/openapi) 5 | [![Build Status](https://travis-ci.org/touchifyapp/spec2ts.svg?branch=master)](https://travis-ci.org/touchifyapp/spec2ts) 6 | 7 | `@spec2ts/openapi` is an utility to create TypeScript types from OpenAPI v3 specification. Unlike other code generators `@spec2ts/openapi` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree (AST). 8 | 9 | ## Features 10 | 11 | * **AST-based:** Unlike other code generators `@spec2ts/openapi` does not use templates to generate code but uses TypeScript's built-in API to generate and pretty-print an abstract syntax tree. 12 | * **Tree-shakeable:** Individually exported types allows you to bundle only the ones you actually use. 13 | * **YAML or JSON:** Use YAML or JSON for your OpenAPI v3 specification. 14 | * **External references:** Resolves automatically external references and bundle or import them in generated files. 15 | * **Implementation agnostic:** Use generated types in any projet or framework. 16 | 17 | ## Installation 18 | 19 | Install in your project: 20 | ```bash 21 | npm install @spec2ts/openapi 22 | ``` 23 | 24 | ## CLI Usage 25 | 26 | ```bash 27 | oapi2ts [options] 28 | 29 | Generate TypeScript types from OpenAPI specification 30 | 31 | Positionals: 32 | input Path to OpenAPI Specification(s) to convert to TypeScript [string] 33 | 34 | Options: 35 | --version Show version number [boolean] 36 | --help Show help usage [boolean] 37 | --output, -o Output directory for generated types [string] 38 | --cwd, -c Root directory for resolving $refs [string] 39 | --avoidAny Avoid the `any` type and use `unknown` instead [boolean] 40 | --enableDate Build `Date` for format `date` and `date-time` [boolean] 41 | --banner, -b Comment prepended to the top of each generated file [string] 42 | ``` 43 | 44 | ## Programmatic Usage 45 | 46 | ```typescript 47 | import { printer } from "@spec2ts/core"; 48 | import { parseOpenApiFile } from "@spec2ts/openapi"; 49 | 50 | async function generateSpec(path: string): Promise { 51 | const result = await parseOpenApiFile(path); 52 | return printer.printNodes(result); 53 | } 54 | ``` 55 | 56 | ## Implementations 57 | 58 | - [x] Types for parameters: 59 | - [x] path 60 | - [x] header 61 | - [x] query 62 | - [x] cookie 63 | - [x] Types for requestBody 64 | - [x] Types for responses 65 | - [x] Automatic naming 66 | - [x] From operationId 67 | - [x] From path 68 | - [x] Parameters merging 69 | - [x] From path item 70 | - [x] From operation 71 | - [x] Override from operation 72 | - [x] [Schema references](http://json-schema.org/latest/json-schema-core.html#rfc.section.7.2.2) 73 | - [x] Local (filesystem) schema references 74 | - [x] External (network) schema references 75 | - [x] Modular architecture 76 | - [x] Import local references 77 | - [x] Embed external references 78 | 79 | ## Compatibility Matrix 80 | 81 | | TypeScript version | spec2ts version | 82 | |--------------------|-----------------| 83 | | v3.x.x | v1 | 84 | | v4.x.x | v2 | 85 | 86 | ## License 87 | 88 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 89 | -------------------------------------------------------------------------------- /packages/openapi/bin/oapi2ts.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../cli"; 3 | -------------------------------------------------------------------------------- /packages/openapi/cli/command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Argv 3 | } from "yargs"; 4 | 5 | import { 6 | printer, 7 | cli 8 | } from "@spec2ts/core"; 9 | 10 | import { 11 | parseOpenApiFile, 12 | ParseOpenApiOptions 13 | } from "../lib/openapi-parser"; 14 | 15 | export interface BuildTsFromOpenApiOptions extends ParseOpenApiOptions { 16 | input: string | string[]; 17 | output?: string; 18 | ext?: string; 19 | banner?: string; 20 | } 21 | 22 | export const usage = "$0 "; 23 | 24 | export const describe = "Generate TypeScript types from OpenAPI specification"; 25 | 26 | export function builder(argv: Argv): Argv { 27 | return argv 28 | .positional("input", { 29 | array: true, 30 | type: "string", 31 | describe: "Path to OpenAPI Specification(s) to convert to TypeScript", 32 | demandOption: true 33 | }) 34 | 35 | .option("output", { 36 | type: "string", 37 | alias: "o", 38 | describe: "Output directory for generated types" 39 | }) 40 | .option("ext", { 41 | type: "string", 42 | alias: "e", 43 | describe: "Output extension for generated types", 44 | choices: [".d.ts", ".ts"] 45 | }) 46 | 47 | .option("cwd", { 48 | type: "string", 49 | alias: "c", 50 | describe: "Root directory for resolving $refs" 51 | }) 52 | 53 | .option("avoidAny", { 54 | type: "boolean", 55 | describe: "Avoid the `any` type and use `unknown` instead" 56 | }) 57 | .option("enableDate", { 58 | choices: [true, "strict", "lax"] as const, 59 | describe: "Build `Date` for format `date` and `date-time`" 60 | }) 61 | 62 | .option("lowerHeaders", { 63 | type: "boolean", 64 | describe: "Lowercase headers keys to match Node.js standard" 65 | }) 66 | 67 | .option("banner", { 68 | type: "string", 69 | alias: "b", 70 | describe: "Comment prepended to the top of each generated file" 71 | }); 72 | } 73 | 74 | export async function handler(options: BuildTsFromOpenApiOptions): Promise { 75 | const files = await cli.findFiles(options.input); 76 | 77 | for (const file of files) { 78 | const ast = await parseOpenApiFile(file, options); 79 | const content = printer.printNodes(ast.all); 80 | 81 | const output = cli.getOutputPath(file, options); 82 | await cli.mkdirp(output); 83 | 84 | await cli.writeFile( 85 | output, 86 | (options.banner || defaultBanner()) + 87 | "\n\n" + 88 | content 89 | ); 90 | } 91 | } 92 | 93 | function defaultBanner(): string { 94 | return `/** 95 | * DO NOT MODIFY 96 | * Generated using @spec2ts/openapi. 97 | * See https://www.npmjs.com/package/@spec2ts/openapi 98 | */ 99 | 100 | /* eslint-disable */`; 101 | } 102 | -------------------------------------------------------------------------------- /packages/openapi/cli/index.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from "yargs"; 2 | 3 | import { 4 | usage, 5 | describe, 6 | builder, 7 | handler 8 | } from "../cli/command"; 9 | 10 | yargs 11 | .command(usage, describe, builder, handler) 12 | .help("help", "Show help usage") 13 | .demandCommand() 14 | .argv; 15 | -------------------------------------------------------------------------------- /packages/openapi/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/openapi-parser"; 2 | -------------------------------------------------------------------------------- /packages/openapi/lib/core-parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as core from "@spec2ts/core"; 3 | 4 | import type { 5 | SchemaObject, 6 | ReferenceObject, 7 | PathItemObject, 8 | OperationObject, 9 | ParameterObject, 10 | ContentObject, 11 | ResponseObject, 12 | RequestBodyObject, 13 | } from "openapi3-ts/oas31"; 14 | 15 | import { 16 | JSONSchema, 17 | ParserContext, 18 | ParsedReference, 19 | 20 | getTypeFromSchema, 21 | getTypeFromProperties, 22 | 23 | createRefContext, 24 | resolveReference, 25 | pascalCase, 26 | } from "@spec2ts/jsonschema/lib/core-parser"; 27 | 28 | import type { ParseOpenApiOptions } from "./openapi-parser"; 29 | 30 | export interface ParseOpenApiResult { 31 | import: ts.Statement[]; 32 | params: ts.Statement[]; 33 | query: ts.Statement[]; 34 | headers: ts.Statement[]; 35 | body: ts.Statement[]; 36 | responses: ts.Statement[]; 37 | models: ts.Statement[]; 38 | cookie: ts.Statement[]; 39 | all: ts.Statement[]; 40 | } 41 | 42 | const VERBS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]; 43 | 44 | export interface OApiParserContext extends ParserContext { 45 | options: ParseOpenApiOptions; 46 | } 47 | 48 | export function parsePathItem(path: string, item: PathItemObject, context: OApiParserContext, result: ParseOpenApiResult): void { 49 | const baseParams = item.parameters && parseParameters( 50 | getPathName(path, context), 51 | item.parameters, 52 | undefined, 53 | context, 54 | result 55 | ); 56 | 57 | Object.entries(item) 58 | .filter(([verb,]) => VERBS.includes(verb.toUpperCase())) 59 | .forEach(([verb, entry]) => parseOperation(path, verb, entry, baseParams, context, result)); 60 | } 61 | 62 | export function parseOperation(path: string, verb: string, operation: OperationObject, baseParams: ParsedParams | undefined, context: OApiParserContext, result: ParseOpenApiResult): void { 63 | const name = getOperationName(verb, path, operation.operationId, context); 64 | if (operation.parameters) { 65 | parseParameters(name, operation.parameters, baseParams, context, result); 66 | } 67 | 68 | if (operation.requestBody) { 69 | const requestBody = resolveReference(operation.requestBody, context); 70 | const decla = getContentDeclaration(name + "Body", requestBody.content, context); 71 | if (decla) { addToOpenApiResult(result, "body", decla); } 72 | } 73 | 74 | if (operation.responses) { 75 | const responses = resolveReference(operation.responses, context); 76 | Object.entries(responses).forEach(([status, responseObj]) => { 77 | const response = resolveReference(responseObj, context); 78 | 79 | const decla = getContentDeclaration(getResponseName(name, status, context), response.content, context); 80 | if (decla) { addToOpenApiResult(result, "responses", decla); } 81 | }); 82 | } 83 | } 84 | 85 | export type ParamType = "params" | "headers" | "query" | "cookie"; 86 | 87 | export function parseParameters(baseName: string, data: Array, baseParams: ParsedParams = {}, context: OApiParserContext, result: ParseOpenApiResult): ParsedParams { 88 | const params: ParameterObject[] = []; 89 | const query: ParameterObject[] = []; 90 | const headers: ParameterObject[] = []; 91 | const cookie: ParameterObject[] = []; 92 | 93 | const res: ParsedParams = {} 94 | 95 | data.forEach(item => { 96 | item = resolveReference(item, context); 97 | 98 | switch (item.in) { 99 | case "path": 100 | params.push(item); 101 | break; 102 | case "header": 103 | headers.push(item); 104 | break; 105 | case "query": 106 | query.push(item); 107 | break; 108 | case "cookie": 109 | cookie.push(item); 110 | break; 111 | } 112 | }); 113 | 114 | 115 | addParams(params, "params"); 116 | addParams(headers, "headers"); 117 | addParams(query, "query"); 118 | addParams(cookie, "cookie"); 119 | 120 | return res; 121 | 122 | function addParams(params: ParameterObject[], paramType: ParamType): void { 123 | if (!params.length) return; 124 | 125 | const name = baseName + pascalCase(paramType); 126 | const type = getParamType(paramType, params, baseParams[paramType], context); 127 | 128 | addToOpenApiResult(result, paramType, 129 | core.createTypeOrInterfaceDeclaration({ 130 | modifiers: [core.modifier.export], 131 | name, 132 | type 133 | }) 134 | ); 135 | 136 | res[paramType] = ts.factory.createTypeReferenceNode(name, undefined); 137 | } 138 | } 139 | 140 | export function parseReference(ref: ParsedReference, context: ParserContext): void { 141 | const type = getTypeFromSchema(ref.schema, createRefContext(ref, context)); 142 | context.aliases.push( 143 | core.createTypeOrInterfaceDeclaration({ 144 | modifiers: [core.modifier.export], 145 | name: ref.name, 146 | type 147 | }) 148 | ); 149 | } 150 | 151 | //#endregion 152 | 153 | //#region Utils 154 | 155 | export function getContentDeclaration(name: string, content: ReferenceObject | ContentObject | undefined, context: OApiParserContext): ts.Statement | undefined { 156 | if (!content) return; 157 | 158 | content = resolveReference(content, context); 159 | 160 | const schema = getSchemaFromContent(content); 161 | if (!schema) return; 162 | 163 | const type = getTypeFromSchema(schema as JSONSchema, context); 164 | return core.createTypeOrInterfaceDeclaration({ 165 | modifiers: [core.modifier.export], 166 | name, 167 | type 168 | }); 169 | } 170 | 171 | export function getParamType(paramType: ParamType, data: ParameterObject[], baseType: ts.TypeReferenceNode | undefined, context: OApiParserContext): ts.TypeNode { 172 | const required: string[] = []; 173 | 174 | const props: Record = {}; 175 | data.forEach(m => { 176 | let name = m.name; 177 | if (paramType === "headers" && context.options.lowerHeaders) { 178 | name = name.toLowerCase(); 179 | } 180 | 181 | props[name] = m.schema || {}; 182 | if (m.required) { 183 | required.push(name); 184 | } 185 | }); 186 | 187 | const ctx = paramType === "query" && typeof context.options.enableDateForQueryParams !== "undefined" 188 | ? { ...context, options: { ...context.options, enableDate: context.options.enableDateForQueryParams } } 189 | : context; 190 | 191 | const type = getTypeFromProperties(props as Record, required, false, ctx); 192 | if (baseType) { 193 | return ts.factory.createIntersectionTypeNode([baseType, type]); 194 | } 195 | 196 | return type; 197 | } 198 | 199 | export function getSchemaFromContent(content: ContentObject): SchemaObject | ReferenceObject | undefined { 200 | return content?.["application/json"]?.schema || 201 | content?.["application/x-www-form-urlencoded"]?.schema || 202 | content?.["multipart/form-data"]?.schema || 203 | content?.["*/*"]?.schema; 204 | } 205 | 206 | export function getResponseName(operationName: string, statusCode: string, context: OApiParserContext): string { 207 | let name = operationName + "Response"; 208 | 209 | const status = parseInt(statusCode); 210 | if (status >= 200 && status < 300) { 211 | const count = (context.names[name] = (context.names[name] || 0) + 1); 212 | if (count > 1) { 213 | name += statusCode; 214 | } 215 | } 216 | else if (!isNaN(status)) { 217 | name += statusCode; 218 | } 219 | else { 220 | // default 221 | name += pascalCase(statusCode); 222 | } 223 | 224 | return name; 225 | } 226 | 227 | export function getOperationName(verb: string, path: string, operationId: string | undefined, context: OApiParserContext): string { 228 | const id = getOperationIdentifier(operationId); 229 | if (id) { 230 | return id; 231 | } 232 | 233 | return getPathName(`${verb} ${path}`, context); 234 | } 235 | 236 | export function getPathName(path: string, context: OApiParserContext): string { 237 | path = path.replace(/\{(.+?)\}/, "by $1").replace(/\{(.+?)\}/g, "and $1"); 238 | let name = pascalCase(path); 239 | 240 | const count = (context.names[name] = (context.names[name] || 0) + 1); 241 | if (count > 1) { 242 | name += count; 243 | } 244 | 245 | return name; 246 | } 247 | 248 | export function getOperationIdentifier(id?: string): string | void { 249 | if (!id) return; 250 | if (id.match(/[^\w\s]/)) return; 251 | id = pascalCase(id); 252 | if (core.isValidIdentifier(id)) return id; 253 | } 254 | 255 | export function addToOpenApiResult(result: ParseOpenApiResult, prop: keyof ParseOpenApiResult, statement: ts.Statement | ts.Statement[]): void { 256 | const statements = Array.isArray(statement) ? statement : [statement]; 257 | result[prop].push(...statements); 258 | result.all.push(...statements); 259 | } 260 | 261 | export function createOpenApiResult(): ParseOpenApiResult { 262 | return { params: [], query: [], headers: [], body: [], responses: [], models: [], cookie: [], import: [], all: [] }; 263 | } 264 | 265 | interface ParsedParams { 266 | params?: ts.TypeReferenceNode; 267 | query?: ts.TypeReferenceNode; 268 | headers?: ts.TypeReferenceNode; 269 | cookie?: ts.TypeReferenceNode; 270 | } 271 | -------------------------------------------------------------------------------- /packages/openapi/lib/openapi-parser.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import $RefParser from "@apidevtools/json-schema-ref-parser"; 3 | 4 | import type { 5 | OpenAPIObject, 6 | } from "openapi3-ts/oas31"; 7 | 8 | import { 9 | ParserOptions, 10 | createContext 11 | } from "@spec2ts/jsonschema/lib/core-parser"; 12 | 13 | import { 14 | ParseOpenApiResult, 15 | 16 | parsePathItem, 17 | parseReference, 18 | createOpenApiResult, 19 | addToOpenApiResult 20 | } from "./core-parser"; 21 | 22 | export { 23 | ParseOpenApiResult 24 | }; 25 | 26 | export interface ParseOpenApiOptions extends ParserOptions { 27 | lowerHeaders?: boolean; 28 | enableDateForQueryParams?: boolean | "strict" | "lax"; 29 | } 30 | 31 | export async function parseOpenApiFile(file: string, options: ParseOpenApiOptions = {}): Promise { 32 | const schema = await $RefParser.parse(file) as OpenAPIObject; 33 | 34 | return parseOpenApi(schema, { 35 | cwd: path.resolve(path.dirname(file)) + "/", 36 | ...options 37 | }); 38 | } 39 | 40 | export async function parseOpenApi(spec: OpenAPIObject, options: ParseOpenApiOptions = {}): Promise { 41 | if (!options.parseReference) { 42 | options.parseReference = parseReference; 43 | } 44 | 45 | const context = await createContext(spec, options); 46 | const result: ParseOpenApiResult = createOpenApiResult(); 47 | 48 | Object.entries(spec.paths ?? {}).forEach(([path, item]) => { 49 | parsePathItem(path, item, context, result); 50 | }); 51 | 52 | addToOpenApiResult(result, "models", context.aliases); 53 | 54 | return result; 55 | } 56 | -------------------------------------------------------------------------------- /packages/openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spec2ts/openapi", 3 | "version": "3.1.3", 4 | "description": "Utility to convert OpenAPI v3 specifications to Typescript using TypeScript native compiler", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/touchifyapp/spec2ts/blob/master/packages/openapi#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/touchifyapp/spec2ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "bin": { 17 | "oapi2ts": "./bin/oapi2ts.js" 18 | }, 19 | "files": [ 20 | "*.js", 21 | "*.d.ts", 22 | "bin/**/*.js", 23 | "cli/**/*.js", 24 | "cli/**/*.d.ts", 25 | "lib/**/*.js", 26 | "lib/**/*.d.ts" 27 | ], 28 | "scripts": { 29 | "build": "npm run clean && npm run lint && npm run build:ts", 30 | "build:ts": "tsc -p .", 31 | "test": "npm run clean && npm run lint && npm run test:jest", 32 | "test:jest": "jest -c ../../jest.config.js --rootDir .", 33 | "test:coverage": "npm run test -- -- --coverage", 34 | "lint": "npm run lint:ts", 35 | "lint:ts": "eslint '*.ts' '{bin,cli,lib}/**/*.ts'", 36 | "lint:fix": "npm run lint -- -- --fix", 37 | "clean": "npm run clean:ts", 38 | "clean:ts": "del '*.{js,d.ts}' '{bin,cli,lib}/**/*.{js,d.ts}'", 39 | "prepublishOnly": "npm test && npm run build" 40 | }, 41 | "dependencies": { 42 | "@spec2ts/core": "^3.0.2", 43 | "@spec2ts/jsonschema": "^3.0.5", 44 | "openapi3-ts": "^4.1.2", 45 | "typescript": "^5.0.0", 46 | "yargs": "^17.7.2" 47 | }, 48 | "devDependencies": { 49 | "del-cli": "^5.1.0", 50 | "eslint": "^8.49.0", 51 | "jest": "^29.6.4" 52 | }, 53 | "keywords": [ 54 | "openapi", 55 | "specification", 56 | "openapi3", 57 | "spec", 58 | "typescript", 59 | "compile", 60 | "compiler", 61 | "ast", 62 | "transpile", 63 | "interface", 64 | "typing", 65 | "spec2ts", 66 | "share" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /packages/openapi/tests/assets/nested-api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: chicken 5 | version: 0.0.0 6 | 7 | paths: 8 | /get: 9 | post: 10 | operationId: getOneThing 11 | requestBody: 12 | $ref: "#/components/requestBodies/GetOneThingRequest" 13 | responses: 14 | 200: 15 | $ref: "#/components/responses/GetOneThingResponse" 16 | 201: 17 | $ref: "#/components/responses/GetThingDataResponse" 18 | 202: 19 | description: thing nested 20 | content: 21 | application/json: 22 | schema: 23 | $ref: "#/components/schemas/ThingNested" 24 | 25 | components: 26 | requestBodies: 27 | GetOneThingRequest: 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/ThingKey" 32 | 33 | responses: 34 | GetOneThingResponse: 35 | description: one thing 36 | content: 37 | application/json: 38 | schema: 39 | $ref: "#/components/schemas/Thing" 40 | GetThingDataResponse: 41 | description: thing data 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "nested/nested-schemas-2.yml#/definitions/ThingData" 46 | 47 | schemas: 48 | Thing: 49 | $ref: "nested/nested-schemas-1.yml#/components/schemas/Thing" 50 | ThingKey: 51 | $ref: "nested/nested-schemas-1.yml#/components/schemas/ThingKey" 52 | ThingNested: 53 | $ref: "nested/nested-schemas-2.yml#/definitions/ThingNested" 54 | -------------------------------------------------------------------------------- /packages/openapi/tests/assets/nested/nested-schemas-1.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | ThingKey: 4 | type: object 5 | properties: 6 | userId: 7 | type: integer 8 | thingId: 9 | type: integer 10 | circular: 11 | $ref: "#/components/schemas/ThingKey" 12 | required: 13 | - userId 14 | - thingId 15 | 16 | Thing: 17 | type: object 18 | allOf: 19 | - $ref: "#/components/schemas/ThingKey" 20 | - $ref: "nested-schemas-2.yml#/definitions/ThingData" -------------------------------------------------------------------------------- /packages/openapi/tests/assets/nested/nested-schemas-2.yml: -------------------------------------------------------------------------------- 1 | definitions: 2 | ThingData: 3 | type: object 4 | properties: 5 | lastSeen: 6 | type: string 7 | format: date-time 8 | value: 9 | type: integer 10 | ThingNested: 11 | type: object 12 | properties: 13 | nested: 14 | $ref: "#/definitions/ThingData" -------------------------------------------------------------------------------- /packages/openapi/tests/assets/petstore-expanded.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | - name: X-Header 43 | in: header 44 | description: Custom header 45 | required: false 46 | schema: 47 | type: string 48 | responses: 49 | "200": 50 | description: pet response 51 | content: 52 | application/json: 53 | schema: 54 | type: array 55 | items: 56 | $ref: "#/components/schemas/Pet" 57 | default: 58 | description: unexpected error 59 | content: 60 | application/json: 61 | schema: 62 | $ref: "#/components/schemas/Error" 63 | post: 64 | description: Creates a new pet in the store. Duplicates are allowed 65 | operationId: addPet 66 | requestBody: 67 | description: Pet to add to the store 68 | required: true 69 | content: 70 | application/json: 71 | schema: 72 | $ref: "#/components/schemas/NewPet" 73 | responses: 74 | "200": 75 | description: pet response 76 | content: 77 | application/json: 78 | schema: 79 | $ref: "#/components/schemas/Pet" 80 | default: 81 | description: unexpected error 82 | content: 83 | application/json: 84 | schema: 85 | $ref: "#/components/schemas/Error" 86 | /pets/{id}: 87 | get: 88 | description: Returns a user based on a single ID, if the user does not have access to the pet 89 | operationId: find pet by id 90 | parameters: 91 | - name: id 92 | in: path 93 | description: ID of pet to fetch 94 | required: true 95 | schema: 96 | type: integer 97 | format: int64 98 | responses: 99 | "200": 100 | description: pet response 101 | content: 102 | application/json: 103 | schema: 104 | $ref: "#/components/schemas/Pet" 105 | default: 106 | description: unexpected error 107 | content: 108 | application/json: 109 | schema: 110 | $ref: "#/components/schemas/Error" 111 | delete: 112 | description: deletes a single pet based on the ID supplied 113 | operationId: deletePet 114 | parameters: 115 | - name: id 116 | in: path 117 | description: ID of pet to delete 118 | required: true 119 | schema: 120 | type: integer 121 | format: int64 122 | responses: 123 | "204": 124 | description: pet deleted 125 | default: 126 | description: unexpected error 127 | content: 128 | application/json: 129 | schema: 130 | $ref: "#/components/schemas/Error" 131 | components: 132 | schemas: 133 | Pet: 134 | allOf: 135 | - $ref: "#/components/schemas/NewPet" 136 | - type: object 137 | required: 138 | - id 139 | properties: 140 | id: 141 | type: integer 142 | format: int64 143 | 144 | NewPet: 145 | type: object 146 | required: 147 | - name 148 | properties: 149 | name: 150 | type: string 151 | tag: 152 | type: string 153 | 154 | Error: 155 | type: object 156 | required: 157 | - code 158 | - message 159 | properties: 160 | code: 161 | type: integer 162 | format: int32 163 | message: 164 | type: string 165 | -------------------------------------------------------------------------------- /packages/openapi/tests/assets/petstore.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | servers: 8 | - url: http://petstore.swagger.io/v1 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | operationId: listPets 14 | tags: 15 | - pets 16 | parameters: 17 | - name: limit 18 | in: query 19 | description: How many items to return at one time (max 100) 20 | required: false 21 | schema: 22 | type: integer 23 | format: int32 24 | responses: 25 | '200': 26 | description: A paged array of pets 27 | headers: 28 | x-next: 29 | description: A link to the next page of responses 30 | schema: 31 | type: string 32 | content: 33 | application/json: 34 | schema: 35 | $ref: "#/components/schemas/Pets" 36 | default: 37 | description: unexpected error 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/Error" 42 | post: 43 | summary: Create a pet 44 | operationId: createPets 45 | tags: 46 | - pets 47 | responses: 48 | '201': 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Error" 56 | /pets/{petId}: 57 | get: 58 | summary: Info for a specific pet 59 | operationId: showPetById 60 | tags: 61 | - pets 62 | parameters: 63 | - name: petId 64 | in: path 65 | required: true 66 | description: The id of the pet to retrieve 67 | schema: 68 | type: string 69 | responses: 70 | '200': 71 | description: Expected response to a valid request 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Pet" 76 | default: 77 | description: unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: "#/components/schemas/Error" 82 | components: 83 | schemas: 84 | Pet: 85 | type: object 86 | required: 87 | - id 88 | - name 89 | properties: 90 | id: 91 | type: integer 92 | format: int64 93 | name: 94 | type: string 95 | tag: 96 | type: string 97 | Pets: 98 | type: array 99 | items: 100 | $ref: "#/components/schemas/Pet" 101 | Error: 102 | type: object 103 | required: 104 | - code 105 | - message 106 | properties: 107 | code: 108 | type: integer 109 | format: int32 110 | message: 111 | type: string -------------------------------------------------------------------------------- /packages/openapi/tests/assets/response-ref.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: chicken 5 | version: 0.0.0 6 | 7 | paths: 8 | /get: 9 | post: 10 | operationId: getOneThing 11 | requestBody: 12 | $ref: '#/components/requestBodies/GetOneThingRequest' 13 | responses: 14 | 200: 15 | $ref: '#/components/responses/GetOneThingResponse' 16 | 17 | components: 18 | schemas: 19 | ThingKey: 20 | type: object 21 | properties: 22 | userId: 23 | type: integer 24 | thingId: 25 | type: integer 26 | required: 27 | - userId 28 | - thingId 29 | 30 | ThingData: 31 | type: object 32 | properties: 33 | lastSeen: 34 | type: string 35 | format: date-time 36 | value: 37 | type: integer 38 | 39 | Thing: 40 | type: object 41 | allOf: 42 | - $ref: '#/components/schemas/ThingKey' 43 | - $ref: '#/components/schemas/ThingData' 44 | 45 | requestBodies: 46 | GetOneThingRequest: 47 | content: 48 | application/json: 49 | schema: 50 | $ref: '#/components/schemas/ThingKey' 51 | 52 | responses: 53 | GetOneThingResponse: 54 | description: one thing 55 | content: 56 | application/json: 57 | schema: 58 | $ref: '#/components/schemas/Thing' -------------------------------------------------------------------------------- /packages/openapi/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { readFileSync } from "fs"; 3 | 4 | import type { OpenAPIObject } from "openapi3-ts/oas31"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const jsYaml = require("js-yaml"); 8 | 9 | export function loadSpec(file: string): OpenAPIObject { 10 | return loadFile(file); 11 | } 12 | 13 | export function getAssetsPath(file?: string): string { 14 | return file ? 15 | path.join(__dirname, "assets", file) : 16 | path.join(__dirname, "assets"); 17 | } 18 | 19 | function loadFile(file: string): T { 20 | if (file.endsWith(".json")) { 21 | return require("./assets/" + file); 22 | } 23 | 24 | if (file.endsWith(".yml") || file.endsWith(".yaml")) { 25 | return jsYaml.load(readFileSync(path.join(__dirname, "assets", file), "utf8")); 26 | } 27 | 28 | throw new Error("Unsupported extension: " + file); 29 | } -------------------------------------------------------------------------------- /packages/openapi/tests/openapi-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseOpenApi } from "../lib/openapi-parser"; 2 | 3 | import { getAssetsPath, loadSpec } from "./helpers"; 4 | 5 | describe("openapi-parser", () => { 6 | 7 | describe(".parseOpenApi()", () => { 8 | 9 | test("should resolves with an object with parsed results", async () => { 10 | const schema = loadSpec("petstore.yml"); 11 | const res = await parseOpenApi(schema); 12 | 13 | expect(res).toHaveProperty("import"); 14 | expect(res).toHaveProperty("params"); 15 | expect(res).toHaveProperty("query"); 16 | expect(res).toHaveProperty("headers"); 17 | expect(res).toHaveProperty("body"); 18 | expect(res).toHaveProperty("responses"); 19 | expect(res).toHaveProperty("models"); 20 | expect(res).toHaveProperty("cookie"); 21 | expect(res).toHaveProperty("all"); 22 | }); 23 | 24 | test("should resolve with a all property containing all results", async () => { 25 | const schema = loadSpec("petstore.yml"); 26 | const res = await parseOpenApi(schema); 27 | 28 | expect(res.all).toHaveLength( 29 | res.import.length + 30 | res.params.length + 31 | res.query.length + 32 | res.headers.length + 33 | res.body.length + 34 | res.responses.length + 35 | res.models.length + 36 | res.cookie.length 37 | ); 38 | }); 39 | 40 | test("should export a type from operations path parameters", async () => { 41 | const schema = loadSpec("petstore.yml"); 42 | const { params, all } = await parseOpenApi(schema); 43 | 44 | expect(all).toContain(params[0]); 45 | expect(params[0]).toHaveProperty("name.text", "ShowPetByIdParams"); 46 | }); 47 | 48 | test("should export a type for operations querystrings", async () => { 49 | const schema = loadSpec("petstore.yml"); 50 | const { query, all } = await parseOpenApi(schema); 51 | 52 | expect(all).toContain(query[0]); 53 | expect(query[0]).toHaveProperty("name.text", "ListPetsQuery"); 54 | }); 55 | 56 | test("should export a type for operations headers", async () => { 57 | const schema = loadSpec("petstore-expanded.yml"); 58 | const { headers, all } = await parseOpenApi(schema); 59 | 60 | expect(all).toContain(headers[0]); 61 | expect(headers[0]).toHaveProperty("name.text", "FindPetsHeaders"); 62 | }); 63 | 64 | test("should export a type with raw names for operations headers", async () => { 65 | const schema = loadSpec("petstore-expanded.yml"); 66 | const { headers } = await parseOpenApi(schema); 67 | 68 | expect(headers[0]).toHaveProperty(["members", 0, "name", "text"], "X-Header"); 69 | }); 70 | 71 | test("should export a type with lower names for operations headers if lowerHeaders option is specified", async () => { 72 | const schema = loadSpec("petstore-expanded.yml"); 73 | const { headers } = await parseOpenApi(schema, { lowerHeaders: true }); 74 | 75 | expect(headers[0]).toHaveProperty(["members", 0, "name", "text"], "x-header"); 76 | }); 77 | 78 | test("should export a type for operations responses", async () => { 79 | const schema = loadSpec("petstore-expanded.yml"); 80 | const { responses, all } = await parseOpenApi(schema); 81 | 82 | expect(all).toContain(responses[0]); 83 | expect(responses[0]).toHaveProperty("name.text", "FindPetsResponse"); 84 | }); 85 | 86 | test("should export a type for schemas in definitions", async () => { 87 | const schema = loadSpec("petstore-expanded.yml"); 88 | const { models, all } = await parseOpenApi(schema); 89 | 90 | expect(all).toContain(models[0]); 91 | expect(models[0]).toHaveProperty("name.text", "NewPet"); 92 | }); 93 | 94 | test("should parse response reference", async () => { 95 | const schema = loadSpec("response-ref.yml"); 96 | const { models } = await parseOpenApi(schema); 97 | 98 | expect(models[0]).toHaveProperty("name.text", "ThingKey"); 99 | expect(models[1]).toHaveProperty("name.text", "ThingData"); 100 | expect(models[2]).toHaveProperty("name.text", "Thing"); 101 | expect(models[2]).toHaveProperty(["heritageClauses", 0, "types", 0, "expression", "escapedText"], "ThingKey"); 102 | expect(models[2]).toHaveProperty(["heritageClauses", 0, "types", 1, "expression", "escapedText"], "ThingData"); 103 | }); 104 | 105 | test("should properly resolve nested references", async () => { 106 | const schema = loadSpec("nested-api.yml"); 107 | const { models } = await parseOpenApi(schema, { cwd: getAssetsPath() }); 108 | 109 | expect(models).toHaveLength(4); 110 | expect(models[0]).toHaveProperty("name.text", "ThingKey"); 111 | expect(models[1]).toHaveProperty("name.text", "ThingData"); 112 | expect(models[2]).toHaveProperty("name.text", "Thing"); 113 | expect(models[2]).toHaveProperty(["heritageClauses", 0, "types", 0, "expression", "escapedText"], "ThingKey"); 114 | expect(models[2]).toHaveProperty(["heritageClauses", 0, "types", 1, "expression", "escapedText"], "ThingData"); 115 | expect(models[3]).toHaveProperty("name.text", "ThingNested"); 116 | }); 117 | 118 | }); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /packages/openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "**/tests/**" 17 | ] 18 | } --------------------------------------------------------------------------------