Repository: BRIKEV/express-jsdoc-swagger Branch: master Commit: 1c3f27dedc04 Files: 147 Total size: 267.8 KB Directory structure: gitextract_uyogyn8t/ ├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── npm-publish.yml │ └── runTests.yml ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── commitlint.config.js ├── config/ │ ├── default.js │ └── swaggerEvents.js ├── consumers/ │ ├── getOnlyComments.js │ ├── globFilesMatches.js │ ├── jsdocInfo.js │ ├── readFiles.js │ └── utils/ │ ├── getComments.js │ └── readFile.js ├── examples/ │ ├── README.md │ ├── combineSchemas/ │ │ └── index.js │ ├── components/ │ │ ├── enum.js │ │ └── simple.js │ ├── configuration/ │ │ ├── index.js │ │ ├── main.js │ │ ├── multiple-files.txt │ │ ├── multipleFiles.js │ │ ├── multipleInstance/ │ │ │ ├── admin-v1-docs.js │ │ │ ├── client-v1-docs.js │ │ │ └── index.js │ │ └── swaggerOptions.js │ ├── eventEmitter/ │ │ ├── index.js │ │ └── multipleInstance.js │ ├── merge/ │ │ ├── simple.js │ │ └── swagger.js │ ├── nullableFields/ │ │ ├── explicitNullable.js │ │ └── nullableByDefault.js │ ├── package.json │ ├── parameters/ │ │ ├── components.js │ │ ├── enum.js │ │ ├── headers.js │ │ ├── multiple.js │ │ └── simple.js │ ├── requestBody/ │ │ ├── components.js │ │ ├── formParameters.js │ │ ├── formUrlencoded.js │ │ ├── formdata.js │ │ ├── multiple.js │ │ ├── simple.js │ │ └── withExamples.js │ ├── responses/ │ │ ├── components.js │ │ ├── full-example.js │ │ ├── multiple.js │ │ ├── simple.js │ │ └── withExamples.js │ ├── security/ │ │ ├── basic-auth.js │ │ ├── basic-oauth2.js │ │ └── swagger.json │ ├── tags/ │ │ └── simple.js │ ├── ts-example/ │ │ ├── README.md │ │ ├── nodemon.json │ │ ├── package.json │ │ ├── simple.ts │ │ └── tsconfig.json │ ├── utils/ │ │ └── logger.js │ └── validator/ │ ├── app.js │ ├── index.js │ └── validator.js ├── index.d.ts ├── index.js ├── package.json ├── processSwagger.js ├── swaggerEvents.js ├── test/ │ ├── consumers/ │ │ ├── getOnlyComments/ │ │ │ └── index.test.js │ │ ├── globFilesMatches/ │ │ │ ├── fixtures/ │ │ │ │ ├── example.txt │ │ │ │ └── excluded/ │ │ │ │ └── example.txt │ │ │ └── index.test.js │ │ ├── jsdocInfo/ │ │ │ └── index.test.js │ │ └── readFiles/ │ │ ├── fixtures/ │ │ │ ├── example-2.txt │ │ │ └── example.txt │ │ └── index.test.js │ ├── e2e/ │ │ ├── components/ │ │ │ ├── components.test.js │ │ │ └── jsdoc-example.js │ │ ├── configuration/ │ │ │ └── configuration.test.js │ │ ├── errors/ │ │ │ ├── errors.test.js │ │ │ ├── jsdoc-parameter-error.js │ │ │ └── jsdoc-requestBody-error.js │ │ ├── logger/ │ │ │ └── logger.test.js │ │ ├── multipleInstance/ │ │ │ ├── admin-v1-docs.js │ │ │ ├── client-v1-docs.js │ │ │ ├── index.js │ │ │ └── multipleInstance.test.js │ │ └── parameters/ │ │ ├── jsdoc-example.js │ │ └── parameters.test.js │ ├── events/ │ │ └── events.test.js │ └── transforms/ │ ├── basic/ │ │ └── index.test.js │ ├── components/ │ │ └── index.test.js │ ├── paths/ │ │ ├── __snapshots__/ │ │ │ └── validStatusCodes.test.js.snap │ │ ├── combineSchemas.test.js │ │ ├── formParameters.test.js │ │ ├── index.test.js │ │ ├── parameters.test.js │ │ ├── requestBody.test.js │ │ ├── responses.test.js │ │ ├── security.test.js │ │ ├── tags.test.js │ │ └── validStatusCodes.test.js │ ├── security/ │ │ └── index.test.js │ ├── tags/ │ │ └── index.test.js │ └── utils/ │ └── index.test.js └── transforms/ ├── basic/ │ ├── contact.js │ ├── index.js │ ├── info.js │ ├── license.js │ └── servers.js ├── components/ │ └── index.js ├── index.js ├── paths/ │ ├── content.js │ ├── examples.js │ ├── formParams.js │ ├── index.js │ ├── parameters.js │ ├── requestBody.js │ ├── responses.js │ ├── schema.js │ ├── security.js │ └── validStatusCodes.js ├── security/ │ └── index.js ├── tags/ │ └── index.js └── utils/ ├── arrays.js ├── combineSchema.js ├── enumValues.js ├── errorMessage.js ├── formatDescription.js ├── formatExamples.js ├── generator.js ├── httpMethods.js ├── mapDescription.js ├── refSchema.js ├── setProperty.js ├── tags.js └── validateTypes.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "bri06", "name": "Briam Martinez Escobar", "avatar_url": "https://avatars0.githubusercontent.com/u/24435223?v=4", "profile": "https://github.com/bri06", "contributions": [ "code" ] }, { "login": "kevinccbsg", "name": "Kevin Julián Martínez Escobar", "avatar_url": "https://avatars2.githubusercontent.com/u/12685053?v=4", "profile": "https://twitter.com/kjmesc", "contributions": [ "code" ] }, { "login": "hoonga", "name": "Heung-yeon Oh", "avatar_url": "https://avatars3.githubusercontent.com/u/10708927?v=4", "profile": "https://github.com/hoonga", "contributions": [ "code" ] }, { "login": "LonelyPrincess", "name": "Sara Hernández", "avatar_url": "https://avatars1.githubusercontent.com/u/17673317?v=4", "profile": "https://github.com/LonelyPrincess", "contributions": [ "code" ] }, { "login": "servatj", "name": "Josep Servat", "avatar_url": "https://avatars0.githubusercontent.com/u/3521485?v=4", "profile": "http://servatj.me", "contributions": [ "code" ] }, { "login": "thuydx55", "name": "Nick Dong", "avatar_url": "https://avatars2.githubusercontent.com/u/1469984?v=4", "profile": "https://github.com/thuydx55", "contributions": [ "code" ] }, { "login": "Stosiu", "name": "Aleksander Stós", "avatar_url": "https://avatars1.githubusercontent.com/u/10252063?v=4", "profile": "https://github.com/Stosiu", "contributions": [ "code" ] }, { "login": "kdankert", "name": "Kjell Dankert", "avatar_url": "https://avatars0.githubusercontent.com/u/46489624?v=4", "profile": "https://github.com/kdankert", "contributions": [ "code" ] }, { "login": "juliendu11", "name": "juliendu11", "avatar_url": "https://avatars0.githubusercontent.com/u/18739442?v=4", "profile": "https://github.com/juliendu11", "contributions": [ "code" ] }, { "login": "meabed", "name": "Mohamed Meabed", "avatar_url": "https://avatars.githubusercontent.com/u/45731?v=4", "profile": "https://me.io", "contributions": [ "code" ] }, { "login": "ofarukaydin", "name": "Faruk Aydın", "avatar_url": "https://avatars.githubusercontent.com/u/32788963?v=4", "profile": "https://github.com/ofarukaydin", "contributions": [ "code" ] }, { "login": "dahlmo", "name": "Dahlmo", "avatar_url": "https://avatars.githubusercontent.com/u/23076026?v=4", "profile": "https://github.com/dahlmo", "contributions": [ "code" ] }, { "login": "gandazgul", "name": "Carlos Ravelo", "avatar_url": "https://avatars.githubusercontent.com/u/108850?v=4", "profile": "https://github.com/gandazgul", "contributions": [ "code" ] }, { "login": "paulish", "name": "Paul Ishenin", "avatar_url": "https://avatars.githubusercontent.com/u/1762032?v=4", "profile": "https://github.com/paulish", "contributions": [ "code" ] }, { "login": "sbingner", "name": "Sam Bingner", "avatar_url": "https://avatars.githubusercontent.com/u/354533?v=4", "profile": "https://github.com/sbingner", "contributions": [ "code" ] }, { "login": "alexstaroselsky", "name": "Alexander Staroselsky", "avatar_url": "https://avatars.githubusercontent.com/u/34102969?v=4", "profile": "https://stackoverflow.com/users/5059657/alexander-staroselsky", "contributions": [ "code" ] }, { "login": "joelabrahamsson", "name": "Joel Abrahamsson", "avatar_url": "https://avatars.githubusercontent.com/u/218986?v=4", "profile": "http://joelabrahamsson.com", "contributions": [ "code" ] }, { "login": "MakakWasTaken", "name": "Markus Moltke", "avatar_url": "https://avatars.githubusercontent.com/u/11789635?v=4", "profile": "https://github.com/MakakWasTaken", "contributions": [ "code" ] } ], "contributorsPerLine": 7, "projectName": "express-jsdoc-swagger", "projectOwner": "BRIKEV", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, "commitType": "docs", "commitConvention": "angular" } ================================================ FILE: .eslintignore ================================================ .nyc_output coverage test-results.xml node_modules .github examples ================================================ FILE: .eslintrc.json ================================================ { "env": { "commonjs": true, "es6": true, "node": true, "jest/globals": true }, "extends": [ "airbnb-base" ], "plugins": ["jest"], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 11 }, "rules": { "arrow-parens": [ "error", "as-needed", { "requireForBlockBody": false } ], "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/prefer-to-have-length": "warn", "jest/valid-expect": "error", "max-len": ["error", { "ignoreComments": true, "code": 130 }] } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: brikev tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Please share your JSDoc comments or your configuration so that we can reproduce the bug **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### What kind of change does this PR introduce? (check at least one) - [ ] Bugfix - [ ] Feature - [ ] Test - [ ] Docs - [ ] Refactor - [ ] Build-related changes - [ ] Other, please describe: ### Description: ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: Publish on: release: types: [published] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: 18 registry-url: https://registry.npmjs.org/ - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ================================================ FILE: .github/workflows/runTests.yml ================================================ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages name: Build on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint -- --fix - run: npm test coverage: name: coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: actions/setup-node@master with: node-version: '20' - run: npm ci - uses: paambaati/codeclimate-action@v2.6.0 env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: coverageCommand: npm test -- --coverage coverageLocations: | ${{github.workspace}}/coverage/lcov.info:lcov ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/node,linux,macos,windows # Edit at https://www.gitignore.io/?templates=node,linux,macos,windows ### Linux ### *~ .vscode # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo build # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # rollup.js default build output dist/ # Uncomment the public line if your project uses Gatsby # https://nextjs.org/blog/next-9-1#public-directory-support # https://create-react-app.dev/docs/using-the-public-folder/#docsNav # public # Storybook build outputs .out .storybook-out # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Temporary folders tmp/ temp/ ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.gitignore.io/api/node,linux,macos,windows .idea ================================================ FILE: .npmignore ================================================ .DS_Store npm-debug.log examples test .github ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello.brikev@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to express-jsdoc-swagger Thanks for your interest in express-jsdoc-swagger. Our goal is to provide an easy Swagger OpenAPI 3.x generator using simple JSDoc comments. ## Contributions express-jsdoc-swagger welcomes contributions from everyone. Contributions to express-jsdoc-swagger should be made in the form of GitHub pull requests. Each pull request will be reviewed by a core contributor ([@bri06](https://github.com/bri06) or [@kevinccbsg](https://github.com/kevinccbsg)) and either landed in the main tree or given feedback for changes that would be required. ## Getting Started express-jsdoc-swagger's [open issues are here](https://github.com/BRIKEV/express-jsdoc-swagger/issues). express-jsdoc-swagger's [docs are here](https://brikev.github.io/express-jsdoc-swagger-docs/#/). You can clone this repository and run this command to start using developing this package. ``` npm install ``` If you want to test it you can add a new test in [this folder](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/test) or you can create a new example [here](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples). To execute package's tests you have run this command: ``` npm test ``` Please also ensure linting is correct while you're developing by running this command: ``` npm run lint ``` ## Pull Request Checklist - [Check validations](https://github.com/BRIKEV/express-jsdoc-swagger/actions?query=workflow%3ABuild) should pass. This one includes linting and testing. - Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). - If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment. ## Conduct We follow the [express-jsdoc-swagger Code of Conduct](https://github.com/BRIKEV/express-jsdoc-swagger/blob/master/CODE_OF_CONDUCT.md). All code in this repository is under [MIT License](https://github.com/BRIKEV/express-jsdoc-swagger/blob/master/LICENSE.md). ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2020 BRIKEV Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![npm](https://img.shields.io/npm/v/express-jsdoc-swagger) ![Node.js Package](https://github.com/BRIKEV/express-jsdoc-swagger/workflows/Build/badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/BRIKEV/express-jsdoc-swagger/badge.svg)](https://snyk.io/test/github/BRIKEV/express-jsdoc-swagger) [![Maintainability](https://api.codeclimate.com/v1/badges/6d5565df0c9c10e75b59/maintainability)](https://codeclimate.com/github/BRIKEV/express-jsdoc-swagger/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/6d5565df0c9c10e75b59/test_coverage)](https://codeclimate.com/github/BRIKEV/express-jsdoc-swagger/test_coverage) ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg) ![npm](https://img.shields.io/npm/dm/express-jsdoc-swagger) # express-jsdoc-swagger With this library, you can document your express endpoints using swagger [OpenAPI 3 Specification](https://swagger.io/specification/) without writing YAML or JSON. You can write comments similar to `jsdoc` on each endpoint, and the dependecy is going to create the swagger UI. ## Table of Contents 1. [Prerequisites](#Prerequisites) 2. [Installation](#Installation) 3. [Basic Usage](#Basic-Usage) 4. [Basic Examples](#Basic-Examples) - [Advanced examples](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples) - [Official docs](https://brikev.github.io/express-jsdoc-swagger-docs/#/) 5. [Validator](#Validator) 6. [VSCode extension](https://marketplace.visualstudio.com/items?itemName=brikev.express-jsdoc-swagger-snippets) ## Prerequisites This library assumes you are using: 1. [NodeJS](https://nodejs.org) 2. [Express.js](http://www.expressjs.com) ## Installation ``` npm i express-jsdoc-swagger ``` ## Basic Usage ```javascript // index.js file const express = require('express'); const expressJSDocSwagger = require('express-jsdoc-swagger'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, security: { BasicAuth: { type: 'http', scheme: 'basic', }, }, // Base directory which we use to locate your JSDOC files baseDir: __dirname, // Glob pattern to find your jsdoc files (multiple patterns can be added in an array) filesPattern: './**/*.js', // URL where SwaggerUI will be rendered swaggerUIPath: '/api-docs', // Expose OpenAPI UI exposeSwaggerUI: true, // Expose Open API JSON Docs documentation in `apiDocsPath` path. exposeApiDocs: false, // Open API JSON Docs endpoint. apiDocsPath: '/v3/api-docs', // Set non-required fields as nullable by default notRequiredAsNullable: false, // You can customize your UI options. // you can extend swagger-ui-express config. You can checkout an example of this // in the `example/configuration/swaggerOptions.js` swaggerUiOptions: {}, // multiple option in case you want more that one instance multiple: true, }; const app = express(); const PORT = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 - success response */ app.get('/api/v1', (req, res) => res.json({ success: true, })); app.listen(PORT, () => console.log(`Example app listening at http://localhost:${PORT}`)); ``` ## Basic Examples 1. Basic configuration options. ```javascript const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, security: { BasicAuth: { type: 'http', scheme: 'basic', }, }, baseDir: __dirname, // Glob pattern to find your jsdoc files (multiple patterns can be added in an array) filesPattern: './**/*.js', }; ``` 2. Components definition ```javascript /** * A song type * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - double */ ``` 3. Endpoint which returns a `Songs` model array in the response. ```javascript /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @tags album * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (req, res) => ( res.json([{ title: 'abum 1', }]) )); ``` 3. Endpoint PUT with body and path params which returns a `Songs` model array in the response. ```javascript /** * PUT /api/v1/albums/{id} * @summary Update album * @tags album * @param {string} name.path - name param description * @param {Song} request.body.required - songs info * @return {array} 200 - success response - application/json */ app.put('/api/v1/albums/:id', (req, res) => ( res.json([{ title: 'abum 1', }]) )); ``` 4. Basic endpoint definition with tags, params and basic authentication ```javascript /** * GET /api/v1/album * @summary This is the summary of the endpoint * @security BasicAuth * @tags album * @param {string} name.query.required - name param description * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response */ app.get('/api/v1/album', (req, res) => ( res.json({ title: 'abum 1', }) )); ``` 5. Basic endpoint definition with code example for response body ```javascript /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @tags album * @return {array} 200 - success response - application/json * @example response - 200 - success response example * [ * { * "title": "Bury the light", * "artist": "Casey Edwards ft. Victor Borba", * "year": 2020 * } * ] */ app.get('/api/v1/albums', (req, res) => ( res.json([{ title: 'track 1', }]) )); ``` You can find more examples [here](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples), or visit our [documentation](https://brikev.github.io/express-jsdoc-swagger-docs/#/). ## Validator We developed a new package works as a validator of your API endpoints and the documentation you create with this package. This package is [express-oas-validator](https://github.com/BRIKEV/express-oas-validator). **Example** Install using the node package registry: ``` npm install --save express-oas-validator ``` We have to wait until we have the full swagger schema to initiate the validator. ```js // validator.js const { init } = require('express-oas-validator'); const validators = instance => new Promise((resolve, reject) => { instance.on('finish', (swaggerDef) => { const { validateRequest, validateResponse } = init(swaggerDef); resolve({ validateRequest, validateResponse }); }); instance.on('error', (error) => { reject(error); }); }); module.exports = validators; ``` You can check out this also in our [example folder](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/validator). ```js // index.js const express = require('express'); const expressJSDocSwagger = require('express-jsdoc-swagger'); const validator = require('./validator'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './**.js', baseDir: __dirname, }; const app = express(); const instance = expressJSDocSwagger(app)(options); const serverApp = async () => { const { validateRequest, validateResponse } = await validator(instance); app.use(express.urlencoded({ extended: true })); app.use(express.json()); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year */ /** * POST /api/v1/songs * @param {Song} request.body.required - song info * @return {object} 200 - song response */ app.post('/api/v1/songs', validateRequest(), (req, res) => res.send('You save a song!')); /** * POST /api/v1/name * @param {string} request.body.required - name body description * @return {object} 200 - song response */ app.post('/api/v1/name', (req, res, next) => { try { // Validate response validateResponse('Error string', req); return res.send('Hello World!'); } catch (error) { return next(error); } }); /** * GET /api/v1/authors * @summary This is the summary or description of the endpoint * @param {string} name.query.required - name param description - enum:type1,type2 * @param {array} license.query - name param description * @return {object} 200 - success response - application/json */ app.get('/api/v1/authors', validateRequest({ headers: false }), (req, res) => ( res.json([{ title: 'album 1', }]) )); // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { res.status(err.status).json(err); }); return app; }; const PORT = process.env.PORT || 4000; serverApp() .then(app => app.listen(PORT, () => console.log(`Listening PORT: ${PORT}`) )) .catch((err) => { console.error(err); process.exit(1); }); ``` You can visit our [documentation](https://brikev.github.io/express-jsdoc-swagger-docs/#/validator). ## Contributors ✨
Briam Martinez Escobar
Briam Martinez Escobar

💻
Kevin Julián Martínez Escobar
Kevin Julián Martínez Escobar

💻
Heung-yeon Oh
Heung-yeon Oh

💻
Sara Hernández
Sara Hernández

💻
Josep Servat
Josep Servat

💻
Nick Dong
Nick Dong

💻
Aleksander Stós
Aleksander Stós

💻
Kjell Dankert
Kjell Dankert

💻
juliendu11
juliendu11

💻
Mohamed Meabed
Mohamed Meabed

💻
Faruk Aydın
Faruk Aydın

💻
Dahlmo
Dahlmo

💻
Carlos Ravelo
Carlos Ravelo

💻
Paul Ishenin
Paul Ishenin

💻
Sam Bingner
Sam Bingner

💻
Alexander Staroselsky
Alexander Staroselsky

💻
Joel Abrahamsson
Joel Abrahamsson

💻
Markus Moltke
Markus Moltke

💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! ================================================ FILE: commitlint.config.js ================================================ /* Examples * chore: run tests on travis ci * fix(server): send cors headers * feat(blog): add comment section */ /* Common Errors * subject must not be sentence-case, start-case, pascal-case, upper-case [subject-case] * type must be lower-case [type-case] * type must be one of * [build, chore, ci, docs, feat, fix, improvement, perf, refactor, revert, style, test] */ /* * docs - Add or edit documentation. * feat - Implementation of new feature. * improvement - Enhancements on existing features. * fix - Changes that solve existing bugs in the application. * perf - Changes to improve the application's performance. * refactor - Code enhancements that do not alter the functionality. * style - Fix front end styles * test - Add or edit tests */ module.exports = { extends: ['@commitlint/config-conventional'], }; ================================================ FILE: config/default.js ================================================ const expressJSDocSwaggerOptions = { // Absolute path of the application baseDir: __dirname, // Glob pattern to find your jsdoc files (multiple patterns can be added in an array) filesPattern: './**/*.js', // URL where SwaggerUI will be rendered swaggerUIPath: '/api-docs', // Expose OpenAPI UI exposeSwaggerUI: true, // Expose Open API JSON Docs documentation in `apiDocsPath` path. exposeApiDocs: false, // Open API JSON Docs endpoint. apiDocsPath: '/v3/api-docs', // Set non-required fields as nullable by default notRequiredAsNullable: false, }; module.exports = expressJSDocSwaggerOptions; ================================================ FILE: config/swaggerEvents.js ================================================ const swaggerEvents = options => ({ multiple: options.multiple !== undefined ? options.multiple : false, }); module.exports = swaggerEvents; ================================================ FILE: consumers/getOnlyComments.js ================================================ const getComments = require('./utils/getComments'); const flat = array => [].concat(...array); const trimComment = comment => comment.trim(); const removeSimpleComments = comment => (comment[0] === '/' && comment[1] !== '/'); const processComments = comment => { const trimedComments = trimComment(comment); return getComments(trimedComments); }; const getOnlyComments = (files = []) => { if (!Array.isArray(files)) return []; const comments = files .map(processComments); return flat(comments).filter(removeSimpleComments); }; module.exports = getOnlyComments; ================================================ FILE: consumers/globFilesMatches.js ================================================ const glob = require('glob'); const path = require('path'); const DEFAULT_EXCLUDED_FOLDER = 'node_modules'; const DEFAULT_GLOB_OPTIONS = { ignore: '**/node_modules/**' }; const globFilesMatches = (baseDir, filesPattern, excludedFolder = DEFAULT_EXCLUDED_FOLDER) => ( new Promise((resolve, reject) => { if (!baseDir || !filesPattern) { const error = new Error('baseDir and filePath are required'); return reject(error); } if (!Array.isArray(filesPattern) && typeof filesPattern !== 'string') { const error = new Error('files pattern has to be a type of string'); return reject(error); } if (Array.isArray(filesPattern)) { if (filesPattern.length === 0) { const error = new Error('if you submit an array of filesPattern it must contain at least one pattern'); return reject(error); } if (filesPattern.some(pattern => typeof pattern !== 'string')) { const error = new Error('all file patterns have to be strings'); return reject(error); } } try { let files; if (!Array.isArray(filesPattern)) { files = glob.sync(path.resolve(baseDir, filesPattern), DEFAULT_GLOB_OPTIONS); } else { files = filesPattern .map(pattern => glob.sync(path.resolve(baseDir, pattern), DEFAULT_EXCLUDED_FOLDER)) .reduce((memo, it) => memo.concat(it), []) .filter((value, index, self) => self.indexOf(value) === index); } const filteredFiles = files.filter(file => !file.includes(excludedFolder)); return resolve(filteredFiles); } catch (error) { return reject(error); } }) ); module.exports = globFilesMatches; ================================================ FILE: consumers/jsdocInfo.js ================================================ const doctrine = require('doctrine'); const jsdocInfo = (options = { unwrap: true }) => comments => { if (!comments || !Array.isArray(comments)) return []; return comments.map(comment => { const parsedValue = doctrine.parse(comment, options); const tags = parsedValue.tags.map(tag => ({ ...tag, description: tag.description ? tag.description.replace('\n/', '').replace(/\/$/, '') : tag.description, })); const description = parsedValue.description.replace('/**\n', '').replace('\n/', ''); return { ...parsedValue, tags, description, }; }); }; module.exports = jsdocInfo; ================================================ FILE: consumers/readFiles.js ================================================ const readFile = require('./utils/readFile'); const readFiles = files => { if (!files || !Array.isArray(files)) return Promise.resolve([]); const filesInfo = files.map(file => readFile(file)); return Promise.all(filesInfo); }; module.exports = readFiles; ================================================ FILE: consumers/utils/getComments.js ================================================ const COMMENTS_PATTERN = /((\/\*\*+[\s\S]*?\*\/)|(\/\*+.*\*\/)|^\/\/.*?[\r\n])[\r\n]*/gm; const BREAK_LINE = /\n/g; const getComments = text => { const comments = text.match(COMMENTS_PATTERN); if (comments) { const filterComments = comments.filter(comment => comment.match(BREAK_LINE)); return filterComments.map(comment => comment.trim()); } return []; }; module.exports = getComments; ================================================ FILE: consumers/utils/readFile.js ================================================ const fs = require('fs'); const readFile = path => ( new Promise((resolve, reject) => { let data = ''; const readStream = fs.createReadStream(path, 'utf8'); readStream.on('data', chunk => { data += chunk; }) .on('end', () => resolve(data)) .on('error', error => reject(error)); }) ); module.exports = readFile; ================================================ FILE: examples/README.md ================================================ # express-jsdoc-swagger examples This page will includes some examples on how to use this package. - [configuration](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/configuration) - [responses](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/responses) - [components](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/components) - [request body](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/requestBody) - [parameters](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/parameters) - [security](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/security) - [tags](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/tags) - [Combine schemas](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/combineSchemas) - [Event Emitter example](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/eventEmitter) - [Merge option](https://github.com/BRIKEV/express-jsdoc-swagger/tree/master/examples/merge) - albumAPI - WIP ## Run examples You can run this examples in your machine following these steps. 1. Install dependencies inside examples folder ``` npm install ``` 2. You can run every example if you execute ``` npm run : npm run responses:simple ``` Then you can go to the [Docs generated page](http://localhost:3000/api-docs). ================================================ FILE: examples/combineSchemas/index.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './index.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} IntrumentalSong * @property {string} title.required - The title * @property {string} band - The band * @property {number} year - The year - double */ /** * A song * @typedef {object} PopSong * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * GET /api/v1/song/{id} * @summary This is the summary of the endpoint * @tags album * @param {number} id.path - song id * @return {oneOf|IntrumentalSong|PopSong} 200 - success response - application/json */ app.get('/api/v1/song/:id', (_req, res) => ( res.json({ title: 'abum 1', }) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/components/enum.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './enum.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} artist - The artist - double * @property {string} license - The year - enum:ISC,MIT */ /** * Author model * @typedef {object} Author * @property {string} name.required - Author name * @property {string} role - enum:singer,guitarrist - The author role * @property {integer} age - Author age - int64 */ /** * Album * @typedef {object} Album * @property {Song} firstSong * @property {Author} author */ app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/components/simple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * Author model * @typedef {object} Author * @property {string} name.required - Author name * @property {number} age - Author age - double */ /** * Album * @typedef {object} Album * @property {Song} firstSong * @property {Author} author */ /** * Album * @typedef {object} Album * @property {string} title - The title * @property {array} years */ /** * Album * @typedef {object} Album * @property {array} Songs */ app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/configuration/index.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const app = express(); const port = 3000; // It is a fictitious configuration const optionsClientAPIInstance = { info: { version: '1.0.0', title: 'Client API', description: 'For client', }, filesPattern: '../client-v1-docs.js', swaggerUIPath: '/api/v1/client/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/client/api-docs', }; // It is a fictitious configuration const optionsAdminAPIInstance = { info: { version: '1.0.0', title: 'Admin API', description: 'Only admin accounts authorized to use this API', }, security: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, filesPattern: './admin-v1-docs.js', swaggerUIPath: '/api/v1/admin/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/admin/api-docs', }; expressJSDocSwagger(app)(optionsClientAPIInstance); expressJSDocSwagger(app)(optionsAdminAPIInstance); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/configuration/main.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); // This is a full set of options // It is not neccesary to complete every option const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [ { url: 'https://{username}.gigantic-server.com:{port}/{basePath}', description: 'The production API server', variables: { username: { default: 'demo', description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', }, port: { enum: [ '8443', '443', ], default: '8443', }, basePath: { default: 'v2', }, }, }, ], filesPattern: './main.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/configuration/multiple-files.txt ================================================ /** * GET /api/v2 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ ================================================ FILE: examples/configuration/multipleFiles.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); // This is a full set of options // It is not neccesary to complete every option const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [ { url: 'https://{username}.gigantic-server.com:{port}/{basePath}', description: 'The production API server', variables: { username: { default: 'demo', description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', }, port: { enum: [ '8443', '443', ], default: '8443', }, basePath: { default: 'v2', }, }, }, ], filesPattern: ['./multipleFiles.js', './multiple-files.txt'], baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/configuration/multipleInstance/admin-v1-docs.js ================================================ /** * GET /api/admin/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ ================================================ FILE: examples/configuration/multipleInstance/client-v1-docs.js ================================================ /** * GET /api/client/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ ================================================ FILE: examples/configuration/multipleInstance/index.js ================================================ const express = require('express'); const logger = require('../../utils/logger'); const expressJSDocSwagger = require('../../..'); const app = express(); const port = 3000; // It is a fictitious configuration const optionsClientAPIInstance = { info: { version: '1.0.0', title: 'Client API', description: 'For client', }, filesPattern: './client-v1-docs.js', swaggerUIPath: '/api/v1/client/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/client/api-docs', multiple: true, }; // It is a fictitious configuration const optionsAdminAPIInstance = { info: { version: '1.0.0', title: 'Admin API', description: 'Only admin accounts authorized to use this API', }, security: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, filesPattern: './admin-v1-docs.js', swaggerUIPath: '/api/v1/admin/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/admin/api-docs', multiple: true, }; const instance = expressJSDocSwagger(app)(optionsClientAPIInstance); const instance2 = expressJSDocSwagger(app)(optionsAdminAPIInstance); instance.on('finish', data => { console.log(data); }) instance2.on('finish', data => { console.log(data); }) app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/configuration/swaggerOptions.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); // This is a full set of options // It is not neccesary to complete every option const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [], filesPattern: './main.js', baseDir: __dirname, swaggerUiOptions: { swaggerOptions: { // This one removes the modals spec // You can checkout more config info here: https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md defaultModelsExpandDepth: -1, }, }, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song type * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => res.json([])); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/eventEmitter/index.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; const listener = expressJSDocSwagger(app)(options); // Event emitter API listener.on('error', error => { logger.error(`Error: ${error}`); }); listener.on('process', ({ entity, swaggerObject }) => { logger.info(`entity: ${entity}`); logger.info('swaggerObject'); logger.info(swaggerObject); }); listener.on('finish', swaggerObject => { logger.info('Finish'); logger.info(swaggerObject); }); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/eventEmitter/multipleInstance.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const app = express(); const port = 3000; // It is a fictitious configuration const optionsClientAPIInstance = { info: { version: '1.0.0', title: 'Client API', description: 'For client', }, filesPattern: '../api/client/v1/*.js', swaggerUIPath: '/api/v1/client/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/client/api-docs', }; // It is a fictitious configuration const optionsAdminAPIInstance = { info: { version: '1.0.0', title: 'Admin API', description: 'Only admin accounts authorized to use this API', }, security: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, filesPattern: '../api/admin/v1/*.js', swaggerUIPath: '/api/v1/admin/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/admin/api-docs', }; const clientAPIInstance = expressJSDocSwagger(app)(optionsClientAPIInstance); const adminAPIInstance = expressJSDocSwagger(app)(optionsAdminAPIInstance); clientAPIInstance.on('error', error => { console.error(`[CLIENT]Error: ${error}`); }); clientAPIInstance.on('process', ({ entity, swaggerObject }) => { console.log(`[CLIENT]entity: ${entity}`); console.log('[CLIENT]swaggerObject'); console.log(swaggerObject); }); clientAPIInstance.on('finish', swaggerObject => { console.log('[CLIENT]Finish'); console.log(swaggerObject); }); adminAPIInstance.on('error', error => { console.error(`[ADMIN]Error: ${error}`); }); adminAPIInstance.on('process', ({ entity, swaggerObject }) => { console.log(`[ADMIN]entity: ${entity}`); console.log('[ADMIN]swaggerObject'); console.log(swaggerObject); }); adminAPIInstance.on('finish', swaggerObject => { console.log('[ADMIN]Finish'); console.log(swaggerObject); }); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/merge/simple.js ================================================ const express = require('express'); const userSwagger = require('./swagger'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options, userSwagger); /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @param {array} name.query.required - name param description * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/merge/swagger.js ================================================ const userSwagger = { openapi: '3.0.0', info: { title: 'Albums store', description: 'Add your description', contact: {}, license: { name: 'MIT', url: '' }, termsOfService: '', version: '1.0.0' }, servers: [], components: { schemas: {} }, paths: { '/users/{id}': { parameters: [ { in: 'path', name: 'id', schema: { type: 'integer' }, required: true, description: 'The user ID.' } ], delete: { summary: 'Deletes the user with the specified ID.', responses: { 204: { description: 'User was deleted.' } } }, get: { summary: 'Gets one or more users by ID.', parameters: [ { in: 'path', name: 'id', required: true, description: 'A comma-separated list of user IDs.', schema: { type: 'array', items: { type: 'integer' }, minItems: 1 }, explode: false, style: 'simple' } ], responses: { 200: { description: 'OK' } } } } }, tags: [] } module.exports = userSwagger; ================================================ FILE: examples/nullableFields/explicitNullable.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', description: 'Example where we define which specific properties are nullable', license: { name: 'MIT', }, }, filesPattern: './index.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * @typedef {object} Song * @property {number} id.required - Id * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 * @property {oneOf|string|null} album - The album to which the song belongs (if any) * @property {(string[]|null)} tags - Associate tags (if any) */ /** * POST /api/v1/songs * @summary Nullable props in request body * @param {Song} request.body.required - Song to add * @return {string} 200 - Success message */ app.post('/api/v1/songs', (_req, res) => res.send('You saved a song!')); /** * GET /api/v1/song * @summary Nullable props in response * @return {array} 200 - List of songs */ app.get('/api/v1/song', (_req, res) => res.json([ { id: 1, title: 'Song from album', album: 'Collection', }, { id: 2, title: 'Song with no album', album: null, }, ])); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/nullableFields/nullableByDefault.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', description: 'Example where non-required fields are nullable by default', license: { name: 'MIT', }, }, filesPattern: './index.js', baseDir: __dirname, notRequiredAsNullable: true, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * @typedef {object} Song * @property {number} id.required - Id * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * POST /api/v1/songs * @summary Adds a new song * @param {Song} request.body.required - Song to add * @return {string} 200 - Success message */ app.post('/api/v1/songs', (_req, res) => res.send('You saved a song!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/package.json ================================================ { "name": "express-jsdoc-swagger-examples", "version": "1.0.0", "description": "A set of examples of the package", "main": "index.js", "scripts": { "configuration:main": "node configuration/main.js", "configuration:multipleFiles": "node configuration/multipleFiles.js", "configuration:swaggerOptions": "node configuration/swaggerOptions.js", "configuration:multipleInstance": "node configuration/multipleInstance/index.js", "parameters:simple": "node parameters/simple.js", "parameters:headers": "node parameters/headers.js", "parameters:multiple": "node parameters/multiple.js", "parameters:components": "node parameters/components.js", "parameters:enum": "node parameters/enum.js", "responses:simple": "node responses/simple.js", "responses:multiple": "node responses/multiple.js", "responses:components": "node responses/components.js", "responses:full-example": "node responses/full-example.js", "responses:withExamples": "node responses/withExamples.js", "requestBody:simple": "node requestBody/simple.js", "requestBody:multiple": "node requestBody/multiple.js", "requestBody:components": "node requestBody/components.js", "requestBody:formdata": "node requestBody/formdata.js", "requestBody:formParameters": "node requestBody/formParameters.js", "requestBody:formUrlencoded": "node requestBody/formUrlencoded.js", "requestBody:withExamples": "node requestBody/withExamples.js", "security:basic-auth": "node security/basic-auth.js", "security:basic-oauth2": "node security/basic-oauth2.js", "components:simple": "node components/simple.js", "components:enum": "node components/enum.js", "tags:simple": "node tags/simple.js", "combineSchemas": "node combineSchemas/index.js", "eventEmitter": "node eventEmitter/index.js", "validator": "node validator/index.js", "eventEmitter:multipleInstance": "node eventEmitter/multipleInstance.js", "merge": "node merge/simple.js", "nullableFields:explicit": "node nullableFields/explicitNullable.js", "nullableFields:byDefault": "node nullableFields/nullableByDefault.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.18.2", "express-oas-validator": "^3.0.0", "winston": "^3.2.1" } } ================================================ FILE: examples/parameters/components.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './components.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song type * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @param {array} name.query.required - name param description * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/parameters/enum.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './enum.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @param {string} name.query.required - name param description - enum:type1,type2 * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @param {string} name.query.required - name param description - enum:type1,type2 * @param {string} license.query - enum:MIT,ISC - name param description * @return {object} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/parameters/headers.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './headers.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @param {string} name.header.required - name param description * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/parameters/multiple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './multiple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1/{id} * @summary This is the summary of the endpoint * @param {string} name.query.required - name param description * @param {number} id.path - phone number * @return {string} 200 - success response */ app.get('/api/v1/:id', (_req, res) => res.send('Hello World!')); /** * GET /api/v1/albums/{id} * @summary This is the summary of the endpoint * @param {array} name.query.required.deprecated - name param description * @param {number} id.path * @return {object} 200 - success response - application/json */ app.get('/api/v1/albums/:id', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/parameters/simple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @param {string} name.query.required - name param description * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @param {array} name.query.required.deprecated - name param description * @return {object} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/components.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './components.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 * @property {string} createdAt - created - date */ /** * POST /api/v1/song * @param {Song} request.body.required - song info * @return {object} 200 - song response */ app.post('/api/v1/songs', (_req, res) => res.send('You save a song!')); /** * POST /api/v1/album * @param {array} request.body.required - songs info * @return {object} 200 - album response */ app.post('/api/v1/album', (_req, res) => res.send('You save a song!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/formParameters.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './formParameters.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); // To use form params it is neccesary to provide a description /** * POST /api/v1/song * @param {string} id.form.required - This is the song id - application/x-www-form-urlencoded * @param {string} title.form.required - This is the song title - application/x-www-form-urlencoded * @return {object} 200 - song response */ app.post('/api/v1/songs', (_req, res) => res.json({})); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/formUrlencoded.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './formUrlencoded.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * POST /api/v1/song * @param {Song} request.body.required - song info - application/x-www-form-urlencoded * @return {object} 200 - song response */ app.post('/api/v1/songs', (_req, res) => res.send('You save a song!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/formdata.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './formdata.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {string} cover - image cover - binary * @property {integer} year - The year - int64 */ /** * POST /api/v1/album * @param {Song} request.body.required - songs info - multipart/form-data * @return {object} 200 - Album created */ app.post('/api/v1/album', (_req, res) => res.send('You save a song!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/multiple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './multiple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); // This one includes a mix of params and requestBody /** * POST /api/v1/album * @param {number} body.query * @param {array} request.body.required - name body description * @return {object} 200 - album response */ app.post('/api/v1/album', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/simple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * POST /api/v1/songs * @param {string} request.body.required - name body description * @return {object} 200 - song response */ app.post('/api/v1/songs', (_req, res) => res.send('Hello World!')); /** * POST /api/v1/albums * @param {array} request.body.required * @return {object} 200 - song response */ app.post('/api/v1/albums', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/requestBody/withExamples.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './withExamples.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * POST /api/v1/song * @param {Song} request.body.required - song info * @return {object} 200 - song response * @return {object} 400 - Bad request response * @example request - payload example * { * "title": "Bury The Light", * "artist": "Casey Edwards ft. Victor Borba", * "year": 2020 * } * @example request - other payload example * { * "title": "The war we made", * "artist": "Red", * "year": 2020 * } */ app.post('/api/v1/song', (_req, res) => res.send({ message: 'You added a song!', })); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/responses/components.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './components.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song type * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year - int64 */ /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/responses/full-example.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './full-example.js', baseDir: __dirname, security: { BasicAuth: { type: 'http', scheme: 'basic', }, }, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A song type * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {array} year */ /** * GET /api/v2/album * @summary This is the summary of the endpoint * @tags album * @security BasicAuth * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response */ app.get('/api/v2/album', (_req, res) => ( res.json({ title: 'abum 1', }) )); /** * GET /api/v1/album * @summary This is the summary of the endpoint * @tags album * @deprecated * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response */ app.get('/api/v1/album', (_req, res) => ( res.json({ title: 'abum 1', }) )); /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @tags album * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/responses/multiple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './multiple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1/album * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response */ app.get('/api/v1/album', (_req, res) => ( res.json({ title: 'abum 1', }) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/responses/simple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json * @return {array} 200 - success response - application/json */ app.get('/api/v1/albums', (_req, res) => ( res.json([{ title: 'abum 1', }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/responses/withExamples.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Pet store', license: { name: 'MIT', }, }, filesPattern: './withExamples.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * A pet * @typedef {object} Pet * @property {string} name.required - The name * @property {string} type.required - The type of pet * @property {string} breed - The breed */ /** * GET /api/v1/pet * @return {array} 200 - success response * @return {object} 403 - forbidden request response * @example response - 200 - example success response * [ * { * "name": "Blaze", * "type": "Dog", * "breed": "Siberian husky" * }, * { * "name": "Luna", * "type": "Cat" * } * ] * @example response - 200 - second example success response * [ * { * "name": "Hachiko", * "type": "Dog", * "breed": "Akita Inu" * } * ] * @example response - 403 - example error response * { * "message": "you cannot access pet data" * } */ app.get('/api/v1/pet', (_req, res) => ( res.json([{ name: 'Hachiko', type: 'Dog', breed: 'Akita Inu' }]) )); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/security/basic-auth.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, security: { BasicAuth: { type: 'http', scheme: 'basic', }, BearerAuth: { type: 'http', scheme: 'bearer', }, }, filesPattern: './basic-auth.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary Endpoint with security info * @return {string} 200 - success response * @security BasicAuth */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); /** * GET /api/v2 * @summary Endpoint with multiple security configuration (AND logic) * @return {string} 200 - success response * @security BasicAuth & BearerAuth */ app.get('/api/v2', (_req, res) => res.send('Hello World!')); /** * GET /api/v3 * @summary Endpoint with multiple security configuration (OR logic) * @return {string} 200 - success response * @security BasicAuth | BearerAuth */ app.get('/api/v3', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/security/basic-oauth2.js ================================================ const express = require('express'); const oauthDetails = require('./swagger'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, security: { oAuthSample: [ 'write_pets', 'read_pets', ] }, filesPattern: './basic-oauth2.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options, oauthDetails); /** * GET /api/v1/oauth * @summary Endpoint with security info * @return {string} 200 - success response * @security oAuthSample */ app.get('/api/v1', (_req, res) => res.send('Hello World!')); app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/security/swagger.json ================================================ { "components": { "securitySchemes": { "oAuthSample": { "type": "oauth2", "description": "This API uses OAuth 2 with the implicit grant flow. [More info](https://api.example.com/docs/auth)", "flows": { "implicit": { "authorizationUrl": "https://api.example.com/oauth2/authorize", "scopes": { "read_pets": "read your pets", "write_pets": "modify pets in your account" } } } } } } } ================================================ FILE: examples/tags/simple.js ================================================ const express = require('express'); const logger = require('../utils/logger'); const expressJSDocSwagger = require('../..'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './simple.js', baseDir: __dirname, }; const app = express(); const port = 3000; expressJSDocSwagger(app)(options); /** * GET /api/v1/album * @tags Album * @return {object} 200 - album response */ /** * GET /api/v1/songs * @tags Album * @tags Songs * @return {object} 200 - album response */ /** * POST /api/v1/song * @summary Create new song * @tags Songs - everything about songs * @return {object} 200 - album response */ /** * PUT /api/v1/song/{songId} * @param {number} songId.path.required - song id * @summary Edit song * @tags Songs * @return {object} 200 - album response */ /** * DELETE /api/v1/song/{songId} * @param {number} songId.path.required - song id * @summary Delete song * @tags Songs * @return {object} 200 - album response */ /** * GET /api/v1/song/{songId} * @param {number} songId.path.required - song id * @summary Get song detail * @tags Songs * @return {object} 200 - album response */ app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`)); ================================================ FILE: examples/ts-example/README.md ================================================ # TS-Example This folder includes a simple TS example. It is important that in the config you add both extensions `ts,js` because when you are running the app using `ts-node` you will watch `ts` files but when it is build you need to watch `js`. ## Install dependencies ``` npm i ``` ## Execute ``` // Execute dev mode npm run dev // Build TS file in build/simple.js npm run tsc:build // Execute ts-node app npm run start // execute build app npm run start:prod ``` ================================================ FILE: examples/ts-example/nodemon.json ================================================ { "ignore": ["**/*.test.ts", "**/*.spec.ts", "node_modules"], "watch": ["simple.ts"], "exec": "npm start", "ext": "ts" } ================================================ FILE: examples/ts-example/package.json ================================================ { "name": "ts-example", "version": "1.0.0", "description": "", "main": "simple.js", "scripts": { "dev": "nodemon", "tsc:build": "tsc", "start": "ts-node simple.ts", "start:prod": "node build/simple.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "nodemon": "^2.0.7", "ts-node": "^9.1.1", "typescript": "^4.0.5" }, "dependencies": { "@types/express": "^4.17.8", "express": "^4.17.1", "express-jsdoc-swagger": "^1.6.0" } } ================================================ FILE: examples/ts-example/simple.ts ================================================ import express from 'express'; import expressJSDocSwagger from 'express-jsdoc-swagger'; const port = 3000; // Create a new express app instance const app: express.Application = express(); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [ { url: 'https://{username}.gigantic-server.com:{port}/{basePath}', description: 'The production API server', variables: { username: { default: 'demo', description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', }, port: { enum: [ '8443', '443', ], default: '8443', }, basePath: { default: 'v2', }, }, }, ], security: { BasicAuth: { type: 'http', scheme: 'basic', }, }, filesPattern: './simple.{ts,js}', baseDir: __dirname, }; expressJSDocSwagger(app)(options); /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ app.get('/api/v1', (_req, res) => { res.send('Hello World!'); }); /** * POST /tag * * @summary create a tag * @param {string} name the name fo the new tag * @returns {object} 200 - success response * @returns {object} 400 - Bad request response * @example response - 200 - success response example * { * "_id": "Bury the light", * "name": "lorem ipsum", * } */ app.listen(port, () => { console.log('App is listening on port 3000!'); }); ================================================ FILE: examples/ts-example/tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./build", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } ================================================ FILE: examples/utils/logger.js ================================================ const winston = require('winston'); const logger = winston.createLogger({ transports: [ new winston.transports.Console(), ], }); module.exports = logger; ================================================ FILE: examples/validator/app.js ================================================ const express = require('express'); const expressJSDocSwagger = require('../..'); const validator = require('./validator'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './**.js', baseDir: __dirname, }; const app = express(); const instance = expressJSDocSwagger(app)(options); const serverApp = async () => { const { validateRequest, validateResponse } = await validator(instance); app.use(express.urlencoded({ extended: true })); app.use(express.json()); /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {integer} year - The year */ /** * POST /api/v1/songs * @param {Song} request.body.required - song info * @return {object} 200 - song response */ app.post('/api/v1/songs', validateRequest(), (_req, res) => res.send('You save a song!')); /** * POST /api/v1/name * @param {string} request.body.required - name body description * @return {object} 200 - song response */ app.post('/api/v1/name', (_req, res, next) => { try { // Validate response validateResponse('Error string', req); return res.send('Hello World!'); } catch (error) { return next(error); } }); /** * GET /api/v1/authors * @summary This is the summary or description of the endpoint * @param {string} name.query.required - name param description - enum:type1,type2 * @param {array} license.query - name param description * @return {object} 200 - success response - application/json */ app.get('/api/v1/authors', validateRequest({ headers: false }), (_req, res) => ( res.json([{ title: 'album 1', }]) )); // eslint-disable-next-line no-unused-vars app.use((err, _req, res, _next) => { res.status(err.status).json(err); }); return app; }; module.exports = serverApp; ================================================ FILE: examples/validator/index.js ================================================ const serverApp = require('./app') const logger = require('../utils/logger'); const PORT = process.env.PORT || 4000; serverApp() .then(app => app.listen(PORT, () => logger.info(`Listening PORT: ${PORT}`) )) .catch((err) => { logger.error(err); process.exit(1); }); ================================================ FILE: examples/validator/validator.js ================================================ const { init } = require('express-oas-validator'); const validators = instance => new Promise((resolve, reject) => { instance.on('finish', (swaggerDef) => { const { validateRequest, validateResponse } = init(swaggerDef); resolve({ validateRequest, validateResponse }); }); instance.on('error', (error) => { reject(error); }); }); module.exports = validators; ================================================ FILE: index.d.ts ================================================ // Type definitions for express-jsdoc-swagger // Project: https://github.com/BRIKEV/express-jsdoc-swagger // Definitions by: Kevin MArtínez // Definitions: https://github.com/BRIKEV/express-jsdoc-swagger/index.d.ts // TypeScript Version: 3.9.7 import { EventEmitter } from "events"; import express from "express"; import { SwaggerUiOptions } from "swagger-ui-express"; interface ContactObject { name: string; url?: string; email?: string; } interface LicenseObject { name: string; url?: string; email?: string; } interface InfoObject { title: string; version: string; description?: string; termsOfService?: string; contact?: ContactObject; license?: LicenseObject; } interface FlowObjectDefaults { refreshUrl?: string scopes: { [key: string]: string } } type SecurityObject = | { description?: string } & ( | { type: "mutualTLS" } | { type: "apiKey" name: string in: "query" | "header" | "cookie" } | { type: "http" scheme: | "basic" | "digest" | "dpop" | "hoba" | "mutual" | "negotiate" | "oauth" | "scram-sha-1" | "scram-sha-256" | "vapid" } | { type: "http" scheme: "bearer" bearerFormat: string } | { type: "oauth2" flows: { implicit?: FlowObjectDefaults & { authorizationUrl: string } password?: FlowObjectDefaults & { tokenUrl: string } clientCredentials?: FlowObjectDefaults & { tokenUrl: string } authorizationCode?: FlowObjectDefaults & { authorizationUrl: string tokenUrl: string } } } | { type: "openIdConnect" openIdConnectUrl: string } ) interface Security { [key: string]: SecurityObject; } interface Servers { url: string; description: string; variables?: object; } interface Options { info: InfoObject; baseDir: string; filesPattern: string | string[]; security?: Security; servers?: string[] | Servers[]; exposeSwaggerUI?: boolean; swaggerUIPath?: string; exposeApiDocs?: boolean; apiDocsPath?: string; swaggerUiOptions?: SwaggerUiOptions; notRequiredAsNullable?: boolean; } type UserSwagger = Record; type EventEmiterHandler = (options: Options, userSwagger?: UserSwagger) => EventEmitter; export default function expressJSDocSwagger(app: express.Application): EventEmiterHandler; ================================================ FILE: index.js ================================================ const swaggerUi = require('swagger-ui-express'); const merge = require('merge'); const defaultOptions = require('./config/default'); const swaggerEventsOptions = require('./config/swaggerEvents'); const processSwagger = require('./processSwagger'); const swaggerEvents = require('./swaggerEvents'); const expressJSDocSwagger = app => (userOptions = {}, userSwagger = {}) => { const events = swaggerEvents(swaggerEventsOptions(userOptions)); const { instance } = events; let swaggerObject = {}; const options = { ...defaultOptions, ...userOptions, }; processSwagger(options, events.processFile) .then(result => { swaggerObject = { ...swaggerObject, ...result.swaggerObject, }; swaggerObject = merge.recursive(true, swaggerObject, userSwagger); events.finish(swaggerObject, { jsdocInfo: result.jsdocInfo, getPaths: result.getPaths, getComponents: result.getComponents, getTags: result.getTags, }); }) .catch(events.error); if (options.exposeSwaggerUI) { app.use(options.swaggerUIPath, (req, res, next) => { swaggerObject = { ...swaggerObject, host: req.get('host'), }; req.swaggerDoc = swaggerObject; next(); }, swaggerUi.serve, swaggerUi.setup(undefined, options.swaggerUiOptions)); } if (options.exposeApiDocs) { app.get(options.apiDocsPath, (req, res) => { res.json({ ...swaggerObject, // we skipped this as is not a valid prop in OpenAPI // This is only being used in the SwaggerUI Library host: undefined, }); }); } return instance; }; module.exports = expressJSDocSwagger; ================================================ FILE: package.json ================================================ { "name": "express-jsdoc-swagger", "version": "1.8.1", "description": "Swagger OpenAPI 3.x generator", "main": "index.js", "dependencies": { "chalk": "^4.1.0", "doctrine": "^3.0.0", "express": "^4.17.3", "glob": "^7.1.6", "merge": "^2.1.1", "swagger-ui-express": "^4.3.0" }, "devDependencies": { "@commitlint/cli": "^17.6.1", "@commitlint/config-conventional": "^11.0.0", "eslint": "^7.19.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.1.3", "husky": "^8.0.3", "jest": "^26.6.3" }, "engines": { "node": ">= 14.0.0" }, "scripts": { "test": "jest", "lint": "./node_modules/.bin/eslint ." }, "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "pre-commit": "npm run lint", "pre-push": "npm run test" } }, "repository": { "type": "git", "url": "git+https://github.com/brikev/express-jsdoc-swagger.git" }, "keywords": [ "swagger", "swagger-generator", "express", "jsdoc", "node", "docs", "documentation", "swagger-ui", "OpenAPI" ], "author": "BRIKEV (https://github.com/brikev)", "license": "MIT", "bugs": { "url": "https://github.com/brikev/express-jsdoc-swagger/issues" }, "homepage": "https://brikev.github.io/express-jsdoc-swagger-docs/#/" } ================================================ FILE: processSwagger.js ================================================ const readFiles = require('./consumers/readFiles'); const globFilesMatches = require('./consumers/globFilesMatches'); const getOnlyComments = require('./consumers/getOnlyComments'); const jsdocInfo = require('./consumers/jsdocInfo'); const { getBasicInfo, getSecuritySchemes, getPaths, getComponents, getTags, } = require('./transforms'); const defaultLogger = () => null; const processSwagger = (options, logger = defaultLogger) => { let swaggerObject = { openapi: '3.0.0', info: options.info, servers: options.servers, security: options.security, }; swaggerObject = getBasicInfo(swaggerObject); logger({ entity: 'basicInfo', swaggerObject }); swaggerObject = getSecuritySchemes(swaggerObject); logger({ entity: 'securitySchemas', swaggerObject }); return globFilesMatches(options.baseDir, options.filesPattern) .then(readFiles) .then(getOnlyComments) .then(jsdocInfo()) .then(data => { swaggerObject = getPaths(swaggerObject, data); logger({ entity: 'paths', swaggerObject }); swaggerObject = getComponents(swaggerObject, data, options); logger({ entity: 'components', swaggerObject }); swaggerObject = getTags(swaggerObject, data); logger({ entity: 'tags', swaggerObject }); return { swaggerObject, jsdocInfo, getPaths, getComponents, getTags, }; }); }; module.exports = processSwagger; ================================================ FILE: swaggerEvents.js ================================================ const { EventEmitter } = require('events'); const ERROR_EVENT_NAME = 'error'; const PROCESS_EVENT_NAME = 'process'; const FINISH_EVENT_NAME = 'finish'; let api = null; const error = eventEmitter => errorInfo => ( eventEmitter.emit(ERROR_EVENT_NAME, errorInfo) ); const processFile = eventEmitter => info => ( eventEmitter.emit(PROCESS_EVENT_NAME, info) ); const finish = eventEmitter => (info, methods) => ( eventEmitter.emit(FINISH_EVENT_NAME, info, methods) ); const swaggerEvents = ({ multiple } = {}) => { if (api && !multiple) return api; const instance = new EventEmitter(); api = { instance, error: error(instance), processFile: processFile(instance), finish: finish(instance), }; return api; }; module.exports = swaggerEvents; ================================================ FILE: test/consumers/getOnlyComments/index.test.js ================================================ const getOnlyComments = require('../../../consumers/getOnlyComments'); describe('get only comments consumer method', () => { it('should return empty array with not params', () => { const input = undefined; const expected = []; const result = getOnlyComments(input); expect(result).toEqual(expected); }); it('should return empty array with not an array as parameter', () => { const input = 2; const expected = []; const result = getOnlyComments(input); expect(result).toEqual(expected); }); it('should return empty array when there is no comments', () => { const input = [` This is a piece of text with no comments `]; const expected = []; const result = getOnlyComments(input); expect(result).toEqual(expected); }); it('should return empty array for simple comments', () => { const input = [` // this is a comment This is a piece of text with // This is a second comment comment // second comment `]; const expected = []; const result = getOnlyComments(input); expect(result).toEqual(expected); }); it('should return array with multiline comments', () => { const input = [` /* this is a comment */ This is a piece of text with /* This is a second comment */ comments `]; const expected = ['/* this is a comment */', '/* This is a second comment */']; const result = getOnlyComments(input); expect(result).toHaveLength(2); expect(result).toEqual(expected); }); it('should return JSdoc comment', () => { const input = [` /** * @param {[type]} * @param {[type]} * @return {[type]} */ // simple comment `]; const expected = [`/** * @param {[type]} * @param {[type]} * @return {[type]} */`]; const result = getOnlyComments(input); expect(result).toHaveLength(1); expect(result).toEqual(expected); }); it('should return only the JSdoc comment when we add a regex in the file pattern', () => { const input = [` const pattern = './*.js'; /** * @param {[type]} * @param {[type]} * @return {[type]} */ `]; const expected = [`/** * @param {[type]} * @param {[type]} * @return {[type]} */`]; const result = getOnlyComments(input); expect(result).toHaveLength(1); expect(result).toEqual(expected); }); it('should return multiline and JSdoc comment', () => { const input = [` /* this is a comment */ This is a piece of text // simple comment with /* This is a second comment */ comments `, ` /** * @param {[type]} * @param {[type]} * @return {[type]} */ // simple comment `]; const expected = ['/* this is a comment */', '/* This is a second comment */', `/** * @param {[type]} * @param {[type]} * @return {[type]} */`]; const result = getOnlyComments(input); expect(result).toHaveLength(3); expect(result).toEqual(expected); }); it('should return JSdoc comment that is not at the begining of the line', () => { const input = [` File with comments with tabulation /** * @param {[type]} * @param {[type]} * @return {[type]} */ `]; const expected = [`/** * @param {[type]} * @param {[type]} * @return {[type]} */`]; const result = getOnlyComments(input); expect(result).toHaveLength(1); expect(result).toEqual(expected); }); }); ================================================ FILE: test/consumers/globFilesMatches/fixtures/example.txt ================================================ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ================================================ FILE: test/consumers/globFilesMatches/fixtures/excluded/example.txt ================================================ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ================================================ FILE: test/consumers/globFilesMatches/index.test.js ================================================ const globFilesMatches = require('../../../consumers/globFilesMatches'); describe('glob Files matches method', () => { it('should return error when required param is not send', done => { globFilesMatches() .catch(error => { expect(error.message).toEqual('baseDir and filePath are required'); done(); }); }); it('should return error when baseDir is undefined', done => { const baseDir = undefined; const filePath = './**/**.txt'; globFilesMatches(baseDir, filePath) .catch(error => { expect(error.message).toEqual('baseDir and filePath are required'); done(); }); }); it('should return error when filePath is not a string', done => { const baseDir = __dirname; const filePath = 3; globFilesMatches(baseDir, filePath) .catch(error => { expect(error.message).toEqual('files pattern has to be a type of string'); done(); }); }); it('should return error when glob method not receives a string type', done => { const baseDir = 2; // This forces error in glob method const filePath = './**/**.txt'; globFilesMatches(baseDir, filePath) .catch(error => { expect(error.message).toEqual('The "paths[0]" argument must be of type string. Received type number (2)'); done(); }); }); it('should return example.txt and excluded/example.txt file', done => { const baseDir = __dirname; const filePath = './**/**.txt'; globFilesMatches(baseDir, filePath) .then(files => { const [exampleFile, excludedFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(excludedFile.includes('excluded/example.txt')).toBe(true); expect(files).toHaveLength(2); done(); }); }); it('should return only example.txt file when we add exclude condition', done => { const baseDir = __dirname; const filePath = './**/**.txt'; const excludedFolder = 'excluded'; globFilesMatches(baseDir, filePath, excludedFolder) .then(files => { const [exampleFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(files).toHaveLength(1); done(); }); }); describe('Array of file paths', () => { it('should return error when passed empty array', done => { const baseDir = __dirname; const filePath = []; globFilesMatches(baseDir, filePath) .catch(error => { expect(error.message).toEqual('if you submit an array of filesPattern it must contain at least one pattern'); done(); }); }); it('should return error when one of the array parameters is not a string', done => { const baseDir = __dirname; const filePath = ['a', 'b', 'c', 2]; globFilesMatches(baseDir, filePath) .catch(error => { expect(error.message).toEqual('all file patterns have to be strings'); done(); }); }); it('should return example.txt and excluded/example.txt file', done => { const baseDir = __dirname; const filePath = ['./**/**.txt']; globFilesMatches(baseDir, filePath) .then(files => { const [exampleFile, excludedFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(excludedFile.includes('excluded/example.txt')).toBe(true); expect(files).toHaveLength(2); done(); }); }); it('should return example.txt and excluded/example.txt file if multiple paths were passed', done => { const baseDir = __dirname; const filePath = ['./fixtures/example.txt', './fixtures/excluded/example.txt']; globFilesMatches(baseDir, filePath) .then(files => { const [exampleFile, excludedFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(excludedFile.includes('excluded/example.txt')).toBe(true); expect(files).toHaveLength(2); done(); }); }); it('should not return duplicated paths', done => { const baseDir = __dirname; const filePath = ['./**/**.txt', './**/**.txt', './fixtures/example.txt']; globFilesMatches(baseDir, filePath) .then(files => { const [exampleFile, excludedFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(excludedFile.includes('excluded/example.txt')).toBe(true); expect(files).toHaveLength(2); done(); }); }); it('should return only example.txt file when we add exclude condition', done => { const baseDir = __dirname; const filePath = ['./**/**.txt']; const excludedFolder = 'excluded'; globFilesMatches(baseDir, filePath, excludedFolder) .then(files => { const [exampleFile] = files; expect(exampleFile.includes('example.txt')).toBe(true); expect(files).toHaveLength(1); done(); }); }); }); }); ================================================ FILE: test/consumers/jsdocInfo/index.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); describe('jsdocInfo method', () => { it('should return function instance', () => { const result = jsdocInfo(); expect(result).toBeInstanceOf(Function); }); it('should return empty array when we do not send params', () => { const input = undefined; const expected = []; const result = jsdocInfo()(input); expect(result).toEqual(expected); }); it('should return empty array when we do not send array as param', () => { const input = 3; const expected = []; const result = jsdocInfo()(input); expect(result).toEqual(expected); }); it('should return jsdoc parsed', () => { const input = [` /** * @param {string} param1 * @param {string} param2 * @return {string} result */ // simple comment `]; const result = jsdocInfo()(input); expect(result).toHaveLength(1); const NUMBER_TAGS = 3; expect(result[0].tags).toHaveLength(NUMBER_TAGS); }); }); ================================================ FILE: test/consumers/readFiles/fixtures/example-2.txt ================================================ goodbye world ================================================ FILE: test/consumers/readFiles/fixtures/example.txt ================================================ hello world ================================================ FILE: test/consumers/readFiles/index.test.js ================================================ const readFiles = require('../../../consumers/readFiles'); describe('readFiles', () => { it('should return empty array when we do not send params', done => { const input = undefined; const expected = []; readFiles(input) .then(result => { expect(result).toEqual(expected); done(); }) .catch(done); }); it('should throw an error when file is not found', done => { const input = [ `${__dirname}/fixtures/not-found.txt`, ]; readFiles(input) .catch(error => { expect(error.message).toMatch(/ENOENT: no such file or directory, open/); done(); }); }); it('should return empty array when we do not send array as param', done => { const input = [ `${__dirname}/fixtures/example.txt`, `${__dirname}/fixtures/example-2.txt`, ]; readFiles(input) .then(result => { const [firstFile, secondFile] = result; expect(firstFile).toEqual('hello world'); expect(secondFile).toEqual('goodbye world'); expect(result).toHaveLength(2); done(); }) .catch(done); }); }); ================================================ FILE: test/e2e/components/components.test.js ================================================ const processSwagger = require('../../../processSwagger'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './jsdoc-example.js', baseDir: __dirname, }; test('should parse components jsdoc from jsdoc-example', async () => { const expected = { openapi: '3.0.0', info: { title: 'Albums store', description: 'Add your description', license: { name: 'MIT', url: '' }, termsOfService: '', version: '1.0.0', }, servers: [], security: undefined, paths: {}, tags: [], components: { schemas: { Song: { description: 'A song', required: [ 'title', ], type: 'object', properties: { title: { description: 'The title', type: 'string', }, artist: { description: 'The artist', type: 'string', }, year: { description: 'The year', type: 'number', format: 'double', }, }, }, Author: { description: 'Author model', required: [ 'name', ], type: 'object', properties: { name: { description: 'Author name', type: 'string', }, age: { description: 'Author age', type: 'integer', format: 'int64', }, }, }, Album: { description: 'Album', type: 'object', properties: { firstSong: { description: '', $ref: '#/components/schemas/Song', }, author: { description: '', $ref: '#/components/schemas/Author', }, }, }, }, }, }; const result = await processSwagger(options); expect(result.swaggerObject).toEqual(expected); }); ================================================ FILE: test/e2e/components/jsdoc-example.js ================================================ /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - double */ /** * Author model * @typedef {object} Author * @property {string} name.required - Author name * @property {integer} age - Author age - int64 */ /** * Album * @typedef {object} Album * @property {Song} firstSong * @property {Author} author */ ================================================ FILE: test/e2e/configuration/configuration.test.js ================================================ const processSwagger = require('../../../processSwagger'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [ { url: 'https://{username}.gigantic-server.com:{port}/{basePath}', description: 'The production API server', variables: { username: { default: 'demo', description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', }, port: { enum: [ '8443', '443', ], default: '8443', }, basePath: { default: 'v2', }, }, }, ], security: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, filesPattern: './main.js', baseDir: __dirname, }; test('should parse basic info', async () => { const expected = { openapi: '3.0.0', info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [ { url: 'https://{username}.gigantic-server.com:{port}/{basePath}', description: 'The production API server', variables: { username: { default: 'demo', description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', }, port: { enum: [ '8443', '443', ], default: '8443', }, basePath: { default: 'v2', }, }, }, ], security: [ { BearerAuth: [], }, ], paths: {}, tags: [], components: { schemas: {}, securitySchemes: { BearerAuth: { bearerFormat: 'JWT', scheme: 'bearer', type: 'http', }, }, }, }; const result = await processSwagger(options); expect(result.swaggerObject).toEqual(expected); }); test('should get transforms(jsdocInfo, getPaths, getComponents, getTags) methods', async () => { const result = await processSwagger(options); expect(result).toHaveProperty('jsdocInfo'); expect(result).toHaveProperty('getPaths'); expect(result).toHaveProperty('getComponents'); expect(result).toHaveProperty('getTags'); }); ================================================ FILE: test/e2e/errors/errors.test.js ================================================ /* eslint-disable no-console */ const chalk = require('chalk'); const processSwagger = require('../../../processSwagger'); test('should give a nice error message for parameters', async () => { const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './jsdoc-parameter-error.js', baseDir: __dirname, }; global.console = { ...global.console, warn: jest.fn() }; await processSwagger(options); expect(console.warn).toHaveBeenCalledTimes(2); expect(console.warn).toHaveBeenNthCalledWith( 1, chalk.yellow('[express-jsdoc-swagger] Entity: @param could not be parsed. Value: "name.query.required" is wrong'), ); expect(console.warn).toHaveBeenNthCalledWith( 2, chalk.yellow('[express-jsdoc-swagger] Entity: @param could not be parsed. Value: "phone.param" is wrong'), ); }); test('should give a nice error message for requestBody', async () => { const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './jsdoc-requestBody-error.js', baseDir: __dirname, }; global.console = { ...global.console, warn: jest.fn() }; await processSwagger(options); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenNthCalledWith( 1, chalk.yellow( // eslint-disable-next-line max-len '[express-jsdoc-swagger] If you want to add one @param as body you must provide "request.body" instead of body.body.required', ), ); }); ================================================ FILE: test/e2e/errors/jsdoc-parameter-error.js ================================================ /** * GET /api/v1/album * @summary This is the summary of the endpoint * @param name.query.required - name param description * @param phone.param - phone number * @return {string} 200 - success response */ ================================================ FILE: test/e2e/errors/jsdoc-requestBody-error.js ================================================ /** * GET /api/v1/album * @summary This is the summary of the endpoint * @param {object} body.body.required - name param description * @return {string} 200 - success response */ ================================================ FILE: test/e2e/logger/logger.test.js ================================================ const processSwagger = require('../../../processSwagger'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, filesPattern: './main.js', baseDir: __dirname, }; test('should called logger method', async () => { const spy = jest.fn(); const expected = { openapi: '3.0.0', info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', url: 'http://example.com', }, description: 'API desctiption', contact: { name: 'contact name', url: 'http://example.com', email: 'test@test.com', }, termsOfService: 'http://example.com', }, servers: [], security: undefined, }; await processSwagger(options, spy); expect(spy).toHaveBeenCalledTimes(5); expect(spy).toHaveBeenNthCalledWith(1, { entity: 'basicInfo', swaggerObject: expected, }); expect(spy).toHaveBeenNthCalledWith(2, { entity: 'securitySchemas', swaggerObject: { ...expected, components: {}, }, }); expect(spy).toHaveBeenNthCalledWith(3, { entity: 'paths', swaggerObject: { ...expected, components: {}, paths: {}, }, }); expect(spy).toHaveBeenNthCalledWith(4, { entity: 'components', swaggerObject: { ...expected, components: { schemas: {}, }, paths: {}, }, }); expect(spy).toHaveBeenNthCalledWith(5, { entity: 'tags', swaggerObject: { ...expected, components: { schemas: {}, }, paths: {}, tags: [], }, }); }); ================================================ FILE: test/e2e/multipleInstance/admin-v1-docs.js ================================================ /** * GET /api/admin/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ ================================================ FILE: test/e2e/multipleInstance/client-v1-docs.js ================================================ /** * GET /api/client/v1 * @summary This is the summary of the endpoint * @return {string} 200 - success response */ ================================================ FILE: test/e2e/multipleInstance/index.js ================================================ const express = require('express'); const expressJSDocSwagger = require('../../..'); const app = express(); const setupInstance = options => new Promise(resolve => { const instance = expressJSDocSwagger(app)(options); instance.on('finish', data => { resolve(data); }); }); module.exports = setupInstance; ================================================ FILE: test/e2e/multipleInstance/multipleInstance.test.js ================================================ const multipleInstance = require('.'); const instance1Options = { info: { version: '1.0.0', title: 'Admin API', description: 'Only admin accounts authorized to use this API', }, security: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, filesPattern: './admin-v1-docs.js', swaggerUIPath: '/api/v1/admin/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/admin/api-docs', }; const instance2Options = { info: { version: '1.0.0', title: 'Client API', description: 'For client', }, filesPattern: './client-v1-docs.js', swaggerUIPath: '/api/v1/client/docs', baseDir: __dirname, exposeSwaggerUI: true, exposeApiDocs: true, apiDocsPath: '/api/v1/client/api-docs', }; describe('multipleInstance test', () => { it('multipleInstance should be different when multiple option is "true"', async () => { const instance1Data = await multipleInstance({ ...instance1Options, multiple: true, }); const instance2Data = await multipleInstance({ ...instance2Options, multiple: true, }); expect(instance1Data).not.toEqual(instance2Data); }); }); ================================================ FILE: test/e2e/parameters/jsdoc-example.js ================================================ /** * GET /api/v1 * @summary This is the summary of the endpoint * @param {string} name.path.required - name param description * @param {number} phone.path.required - phone number * @return {string} 200 - success response */ /** * GET /api/v1/albums * @summary This is the summary of the endpoint * @param {array} name.path.required - name param description * @param {number} phone.path.required * @return {object} 200 - success response - application/json */ ================================================ FILE: test/e2e/parameters/parameters.test.js ================================================ const processSwagger = require('../../../processSwagger'); const options = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, filesPattern: './jsdoc-example.js', baseDir: __dirname, }; test('should parse parameters from jsdoc-example', async () => { const expected = { openapi: '3.0.0', info: { title: 'Albums store', description: 'Add your description', license: { name: 'MIT', url: '' }, termsOfService: '', version: '1.0.0', }, servers: [], security: undefined, paths: { '/api/v1': { get: { deprecated: false, description: undefined, security: [], summary: 'This is the summary of the endpoint', responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'string', }, }, }, }, }, parameters: [ { name: 'name', in: 'path', description: 'name param description', required: true, deprecated: false, schema: { type: 'string', }, }, { name: 'phone', in: 'path', description: 'phone number', required: true, deprecated: false, schema: { type: 'number', }, }, ], tags: [], }, }, '/api/v1/albums': { get: { deprecated: false, description: undefined, security: [], summary: 'This is the summary of the endpoint', responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, parameters: [ { name: 'name', in: 'path', description: 'name param description', required: true, deprecated: false, schema: { type: 'array', items: { type: 'string', }, }, }, { name: 'phone', in: 'path', description: '', required: true, deprecated: false, schema: { type: 'number', }, }, ], tags: [], }, }, }, tags: [], components: { schemas: {}, }, }; const result = await processSwagger(options); expect(result.swaggerObject).toEqual(expected); }); ================================================ FILE: test/events/events.test.js ================================================ const swaggerEvents = require('../../swaggerEvents'); describe('swaggerEvents event emitter', () => { it('should return one instance', () => { const instance = swaggerEvents(); const newInstance = swaggerEvents(); expect(instance).toEqual(newInstance); }); it('should handle error events', done => { const events = swaggerEvents(); const error = 'Example error'; events.instance.on('error', errorInfo => { expect(errorInfo).toEqual(error); done(); }); events.error(error); }); it('should handle process info events', done => { const events = swaggerEvents(); const message = 'Example message'; events.instance.on('process', errorInfo => { expect(errorInfo).toEqual(message); done(); }); events.processFile(message); }); it('should handle finish info events', done => { const events = swaggerEvents(); const message = 'Example message'; events.instance.on('finish', errorInfo => { expect(errorInfo).toEqual(message); done(); }); events.finish(message); }); }); ================================================ FILE: test/transforms/basic/index.test.js ================================================ const getBasicInfo = require('../../../transforms/basic'); describe('basic transform method', () => { it('should not allow empty configuration', () => { const input = undefined; expect(() => { getBasicInfo(input); }).toThrow('Key title is required in item {}'); }); it('should return error as version is required', () => { const input = { info: { title: 'API 1', }, servers: [], }; expect(() => { getBasicInfo(input); }).toThrow('Key version is required in item {"title":"API 1"} for Entity info'); }); it('should return info object', () => { const input = { extra: 'extra info', info: { title: 'API 1', version: '1.0.0', }, }; const expected = { extra: 'extra info', info: { title: 'API 1', version: '1.0.0', termsOfService: '', description: 'Add your description', }, servers: [], }; const result = getBasicInfo(input); expect(result).toEqual(expected); }); it('should send warn type when type is not correct', () => { global.console = { ...global.console, warn: jest.fn() }; const input = { extra: 'extra info', info: { title: 'API 1', version: '1.0.0', description: false, }, }; const expected = { extra: 'extra info', info: { title: 'API 1', version: '1.0.0', termsOfService: '', description: false, }, servers: [], }; const result = getBasicInfo(input); expect(result).toEqual(expected); // eslint-disable-next-line expect(console.warn).toHaveBeenCalled(); }); it('should throw error for invalid server', () => { const input = { info: { title: 'API 1', version: '1.0.0', }, servers: [{ invalid: 'example' }], }; expect(() => { getBasicInfo(input); }).toThrow('Key url is required in item {"invalid":"example"} for Entity servers'); }); it('should return valid configuration', () => { const input = { info: { title: 'API 1', version: '1.0.0', }, servers: [{ url: 'https://url.com', variables: { enum: ['300', '200'], default: 200, }, }], }; const expected = { info: { title: 'API 1', description: 'Add your description', termsOfService: '', version: '1.0.0', }, servers: [ { url: 'https://url.com', description: '', variables: { enum: ['300', '200'], default: 200, }, }, ], }; const result = getBasicInfo(input); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/components/index.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const parseComponents = require('../../../transforms/components'); describe('parseComponents method', () => { it('should return empty array with not params', () => { const initialState = {}; const components = undefined; const expected = { components: { schemas: {}, }, }; const result = parseComponents(initialState, components); expect(result).toEqual(expected); }); it('should return empty array with not an array as parameter', () => { const initialState = {}; const components = 2; const expected = { components: { schemas: {}, }, }; const result = parseComponents(initialState, components); expect(result).toEqual(expected); }); it('Should parse jsdoc and return default value when there is no typedef', () => { const jsodInput = [` /** * A song * @property {string} title - The title * @property {string} artist - The artist * @property {number} year - The year */ `]; const expected = { components: { schemas: {}, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with basic properties', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title - The title * @property {string} artist - The artist * @property {number} year - The year */ `]; const expected = { components: { schemas: { Song: { type: 'object', description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', }, year: { type: 'number', description: 'The year', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with enum properties', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title - The title * @property {string} artist - The artist - enum:value1,value2 * @property {number} year - The year - int64 */ `]; const expected = { components: { schemas: { Song: { type: 'object', description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', enum: [ 'value1', 'value2', ], }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with enum properties in different order', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title - The title * @property {string} artist - enum:value1,value2 - The artist * @property {number} year - The year - int64 */ `]; const expected = { components: { schemas: { Song: { type: 'object', description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', enum: [ 'value1', 'value2', ], }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with json options', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title - The title * @property {string} artist - The artist - json:{"maxLength": 300} * @property {number} year - The year - int64 - json:{"minimum": 2000} */ `]; const expected = { components: { schemas: { Song: { type: 'object', description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', maxLength: 300, }, year: { type: 'number', description: 'The year', format: 'int64', minimum: 2000, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with require and format properties', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - int64 */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse two jsdoc components', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - int64 */ `, ` /** * Album * @typedef {object} Album * @property {string} name.required - Album name * @property {number} length */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, Album: { type: 'object', required: [ 'name', ], description: 'Album', properties: { name: { type: 'string', description: 'Album name', }, length: { type: 'number', description: '', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse one reference between two jsdoc components', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - int64 */ `, ` /** * Album * @typedef {object} Album * @property {Song} firstSong */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, Album: { type: 'object', description: 'Album', properties: { firstSong: { $ref: '#/components/schemas/Song', description: '', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse empty string when description is not defined', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required * @property {string} artist * @property {number} year */ `, ` /** * Album * @typedef {object} Album * @property {Song} firstSong */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: '', }, artist: { type: 'string', description: '', }, year: { type: 'number', description: '', }, }, }, Album: { type: 'object', description: 'Album', properties: { firstSong: { $ref: '#/components/schemas/Song', description: '', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse two reference for one jsdoc component', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - int64 */ `, ` /** * Author model * @typedef {object} Author * @property {string} name.required - Author name * @property {number} age - Author age - int64 */ `, ` /** * Album * @typedef {object} Album * @property {Song} firstSong * @property {Author} author */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', }, year: { type: 'number', description: 'The year', format: 'int64', }, }, }, Author: { type: 'object', required: [ 'name', ], description: 'Author model', properties: { name: { type: 'string', description: 'Author name', }, age: { type: 'number', description: 'Author age', format: 'int64', }, }, }, Album: { type: 'object', description: 'Album', properties: { firstSong: { $ref: '#/components/schemas/Song', description: '', }, author: { $ref: '#/components/schemas/Author', description: '', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec when songs property is an array of strings', () => { const jsodInput = [` /** * Album * @typedef {object} Album * @property {string} title - The title * @property {array} songs - songs array */ `]; const expected = { components: { schemas: { Album: { type: 'object', description: 'Album', properties: { title: { type: 'string', description: 'The title', }, songs: { type: 'array', description: 'songs array', items: { type: 'string', }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec when songs property is an array of numbers', () => { const jsodInput = [` /** * Album * @typedef {object} Album * @property {string} title - The title * @property {array} years - years description */ `]; const expected = { components: { schemas: { Album: { type: 'object', description: 'Album', properties: { title: { type: 'string', description: 'The title', }, years: { type: 'array', description: 'years description', items: { type: 'number', }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec when songs property is an array of numbers with empty description', () => { const jsodInput = [` /** * Album * @typedef {object} Album * @property {string} title - The title * @property {array} years */ `]; const expected = { components: { schemas: { Album: { type: 'object', description: 'Album', properties: { title: { type: 'string', description: 'The title', }, years: { type: 'array', description: '', items: { type: 'number', }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse an album schema with an array of Songs schemas.', () => { const jsodInput = [ [` /** * Album * @typedef {object} Album * @property {array} Songs */ `], [` /** * Album * @typedef {object} Album * @property {Array} Songs */ `], [` /** * Album * @typedef {object} Album * @property {Song[]} Songs */ `], ]; const expected = { components: { schemas: { Album: { type: 'object', description: 'Album', properties: { Songs: { type: 'array', description: '', items: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }; jsodInput.forEach(jsod => { const parsedJSDocs = jsdocInfo()(jsod); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); }); it('Should parse a SingleAlbum schema with allOf reference of Song.', () => { const jsodInput = [` /** * SingleAlbum * @typedef {allOf|Song} SingleAlbum * @property {array} Songs */ `]; const expected = { components: { schemas: { SingleAlbum: { allOf: [ { $ref: '#/components/schemas/Song', }, ], type: 'object', description: 'SingleAlbum', properties: { Songs: { type: 'array', description: '', items: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse a SongOrAlbum schema with oneOf reference of Song and Album.', () => { const jsodInput = [` /** * SongOrAlbum * @typedef {oneOf|Song|Album} SongOrAlbum * @property {array} Songs */ `]; const expected = { components: { schemas: { SongOrAlbum: { oneOf: [ { $ref: '#/components/schemas/Song', }, { $ref: '#/components/schemas/Album', }, ], type: 'object', description: 'SongOrAlbum', properties: { Songs: { type: 'array', description: '', items: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with optional properties nullable by default', () => { const jsdocInput = [` /** * A song * @typedef {object} Song * @property {string} title.required - The title * @property {string} artist - The artist * @property {number} year - The year - int64 */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: 'The title', }, artist: { type: 'string', description: 'The artist', nullable: true, }, year: { type: 'number', description: 'The year', format: 'int64', nullable: true, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = parseComponents({}, parsedJSDocs, { notRequiredAsNullable: true }); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with string type', () => { const jsodInput = [` /** * A song * @typedef {string} Song */ `]; const expected = { components: { schemas: { Song: { type: 'string', description: 'A song', }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec with string type and a format', () => { const jsodInput = [` /** * A song - enum:value1,value2 * @typedef {string} Song */ `]; const expected = { components: { schemas: { Song: { type: 'string', description: 'A song', enum: [ 'value1', 'value2', ], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec dictionary', () => { const jsodInput = [` /** * Profile * @typedef {object} Profile * * @property {string} email */ `, ` /** * Profiles dict * @typedef {Dictionary} Profiles */ `]; const expected = { components: { schemas: { Profile: { type: 'object', description: 'Profile', properties: { email: { type: 'string', description: '', }, }, }, Profiles: { type: 'object', description: 'Profiles dict', properties: {}, additionalProperties: { $ref: '#/components/schemas/Profile', }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc component spec record type', () => { const jsodInput = [` /** * Records dict * @typedef {Dictionary} Records map */ `]; const expected = { components: { schemas: { Records: { type: 'object', description: 'Records dict', properties: {}, additionalProperties: { type: 'string', }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/__snapshots__/validStatusCodes.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Valid status codes snapshot 1`] = ` Object { "100": "Continue", "101": "Switching Protocols", "102": "Processing", "1XX": "1XX Range", "200": "OK", "201": "Created", "202": "Accepted", "203": "Non Authoritative Information", "204": "No Content", "205": "Reset Content", "206": "Partial Content", "207": "Multi-Status", "2XX": "2XX Range", "300": "Multiple Choices", "301": "Moved Permanently", "302": "Moved Temporarily", "303": "See Other", "304": "Not Modified", "305": "Use Proxy", "307": "Temporary Redirect", "308": "Permanent Redirect", "3XX": "3XX Range", "400": "Bad Request", "401": "Unauthorized", "402": "Payment Required", "403": "Forbidden", "404": "Not Found", "405": "Method Not Allowed", "406": "Not Acceptable", "407": "Proxy Authentication Required", "408": "Request Timeout", "409": "Conflict", "410": "Gone", "411": "Length Required", "412": "Precondition Failed", "413": "Request Entity Too Large", "414": "Request-URI Too Long", "415": "Unsupported Media Type", "416": "Requested Range Not Satisfiable", "417": "Expectation Failed", "418": "I'm a teapot", "419": "Insufficient Space on Resource", "420": "Method Failure", "422": "Unprocessable Entity", "423": "Locked", "424": "Failed Dependency", "428": "Precondition Required", "429": "Too Many Requests", "431": "Request Header Fields Too Large", "4XX": "4XX Range", "500": "Server Error", "501": "Not Implemented", "502": "Bad Gateway", "503": "Service Unavailable", "504": "Gateway Timeout", "505": "HTTP Version Not Supported", "507": "Insufficient Storage", "511": "Network Authentication Required", "5XX": "5XX Range", "default": "Default response", } `; ================================================ FILE: test/transforms/paths/combineSchemas.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); const parseComponents = require('../../../transforms/components'); test('should parse jsdoc path response with oneOf keyword', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {oneOf|Song|Album} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { oneOf: [ { $ref: '#/components/schemas/Song', }, { $ref: '#/components/schemas/Album', }, ], }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); test('should parse component with anyOf keyword', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required * @property {string} artist * @property {number} year */ `, ` /** * Album * @typedef {object} Album * @property {anyOf|Song|Album} firstSong */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: '', }, artist: { type: 'string', description: '', }, year: { type: 'number', description: '', }, }, }, Album: { type: 'object', description: 'Album', properties: { firstSong: { description: '', anyOf: [ { $ref: '#/components/schemas/Song', }, { $ref: '#/components/schemas/Album', }, ], }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); test('should parse jsdoc path reference params with allOf keyword', () => { const jsodInput = [` /** * GET /api/v1 * @param {allOf|Song|Album} name.query.required - name param description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { allOf: [ { $ref: '#/components/schemas/Song', }, { $ref: '#/components/schemas/Album', }, ], }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); test('should parse component with anyOf array keyword', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required * @property {string} artist * @property {number} year */ `, ` /** * Album * @typedef {object} Album * @property {anyOf|Song[]|Album|string|string[]|null} firstSong */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: '', }, artist: { type: 'string', description: '', }, year: { type: 'number', description: '', }, }, }, Album: { type: 'object', description: 'Album', properties: { firstSong: { description: '', anyOf: [ { type: 'array', items: { $ref: '#/components/schemas/Song', }, }, { $ref: '#/components/schemas/Album', }, { type: 'string', }, { type: 'array', items: { type: 'string', }, }, ], nullable: true, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); test('should parse component with jsdoc syntax for multiple data types', () => { const jsodInput = [` /** * A song * @typedef {object} Song * @property {string} title.required * @property {string} artist * @property {number} year * @property {(string|null)} album * @property {object|number} releaseDate */ `]; const expected = { components: { schemas: { Song: { type: 'object', required: [ 'title', ], description: 'A song', properties: { title: { type: 'string', description: '', }, artist: { type: 'string', description: '', }, year: { type: 'number', description: '', }, album: { type: 'string', description: '', nullable: true, }, releaseDate: { description: '', oneOf: [ { type: 'object', }, { type: 'number', }, ], }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseComponents({}, parsedJSDocs); expect(result).toEqual(expected); }); ================================================ FILE: test/transforms/paths/formParameters.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('form requestBody tests', () => { it('should add request body using form examples', () => { const jsodInput = [` /** * POST /api/v1/ * @param {string} id.form.required - id description * @param {string} title.form.required - title description */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'string', description: 'id description', }, title: { type: 'string', description: 'title description', }, }, required: ['id', 'title'], }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should add request body using form examples with params', () => { const jsodInput = [` /** * POST /api/v1/ * @param {string} name.path.required - name param description * @param {string} id.form.required - id description * @param {string} title.form.required - title description */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, summary: '', responses: {}, tags: [], security: [], parameters: [ { deprecated: false, description: 'name param description', in: 'path', name: 'name', required: true, schema: { type: 'string', }, }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'string', description: 'id description', }, title: { type: 'string', description: 'title description', }, }, required: ['id', 'title'], }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should not fail when a request.body is sent', () => { const jsodInput = [` /** * POST /api/v1/ * @param {string} request.body.required - name body description * @param {string} id.form.required - id description * @param {string} title.form.required - title description */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'string', description: 'id description', }, title: { type: 'string', description: 'title description', }, }, required: ['id', 'title'], }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should add request body for different application content type', () => { const jsodInput = [` /** * POST /api/v1/ * @param {string} name.path.required - name param description * @param {string} id.form.required - id description - application/x-www-form-urlencoded * @param {string} title.form.required - title description */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], security: [], parameters: [ { deprecated: false, description: 'name param description', in: 'path', name: 'name', required: true, schema: { type: 'string', }, }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { title: { type: 'string', description: 'title description', }, }, required: ['title'], }, }, 'application/x-www-form-urlencoded': { schema: { type: 'object', properties: { id: { type: 'string', description: 'id description', }, }, required: ['id'], }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should add request body for different application content type with an example', () => { const jsodInput = [` /** * POST /api/v1/ * @param {string} name.path.required - name param description * @param {string} id.form.required - id description - application/x-www-form-urlencoded * @param {string} title.form.required - title description * @example request - example payload * sample input string */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [ { deprecated: false, description: 'name param description', in: 'path', name: 'name', required: true, schema: { type: 'string', }, }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { title: { type: 'string', description: 'title description', }, }, required: ['title'], }, examples: { example1: { summary: 'example payload', value: 'sample input string', }, }, }, 'application/x-www-form-urlencoded': { schema: { type: 'object', properties: { id: { type: 'string', description: 'id description', }, }, required: ['id'], }, examples: { example1: { summary: 'example payload', value: 'sample input string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/index.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('setPaths method', () => { it('should return empty array with not params', () => { const initialState = {}; const paths = undefined; const expected = { paths: {}, }; const result = setPaths(initialState, paths); expect(result).toEqual(expected); }); it('should return empty array with not an array as parameter', () => { const initialState = {}; const paths = 2; const expected = { paths: {}, }; const result = setPaths(initialState, paths); expect(result).toEqual(expected); }); it('should parse jsdoc path spec with one response, summary, description and endpoint info', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @description This is the description of the endpoint * @return {object} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, summary: 'This is the summary of the endpoint', description: 'This is the description of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path spec with deprecated value', () => { const jsodInput = [` /** * GET /api/v1 * @deprecated * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: true, summary: 'This is the summary of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path spec with multiple endpoints', () => { const jsodInput = [` /** * GET /api/v1 * @deprecated * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json */ `, ` /** * GET /api/v1/songs * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: true, summary: 'This is the summary of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, '/api/v1/songs': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path spec with multiple methods for one endpoint', () => { const jsodInput = [` /** * GET /api/v1 * @deprecated * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json */ `, ` /** * POST /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: true, summary: 'This is the summary of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, post: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], security: [], tags: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/parameters.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('params tests', () => { it('should parse jsdoc path params', () => { const jsodInput = [` /** * GET /api/v1 * @param {string} name.query.required - name param description * @operationId getInfo */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], operationId: 'getInfo', parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'string', }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should not parse jsdoc path params with malformed info', () => { const jsodInput = [` /** * GET /api/v1 * @param {string} name param description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path params with array type', () => { const jsodInput = [` /** * GET /api/v1 * @param {array} name.query.required.deprecated - name param description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: true, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'array', items: { type: 'string', }, }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path multiple params', () => { const jsodInput = [` /** * GET /api/v1 * @param {array} name.query.required.deprecated - name param description * @param {number} phone.param */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: true, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'array', items: { type: 'string', }, }, }, { deprecated: false, description: '', in: 'param', name: 'phone', required: false, schema: { type: 'number', }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path reference params', () => { const jsodInput = [` /** * GET /api/v1 * @param {Song} name.query.required - name param description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { $ref: '#/components/schemas/Song', }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path params with array of references', () => { const jsodInput = [ [` /** * GET /api/v1 * @param {array} name.query.required.deprecated - name param description */ `], [` /** * GET /api/v1 * @param {Array} name.query.required.deprecated - name param description */ `], [` /** * GET /api/v1 * @param {Song[]} name.query.required.deprecated - name param description */ `], ]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: true, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'array', items: { $ref: '#/components/schemas/Song', }, }, }], }, }, }, }; jsodInput.forEach(jsod => { const parsedJSDocs = jsdocInfo()(jsod); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); it('should parse jsdoc path params with enum values', () => { const jsodInput = [` /** * GET /api/v1 * @param {string} name.query.required - name param description - enum:value1,value2 */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'string', enum: [ 'value1', 'value2', ], }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path params with enum values in diferent order', () => { const jsodInput = [` /** * GET /api/v1 * @param {string} name.query.required - enum:value1,value2 - name param description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'string', enum: [ 'value1', 'value2', ], }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); it('should parse jsdoc path params with json options', () => { const jsodInput = [` /** * GET /api/v1 * @param {string} name.query.required - name param description - enum:value1,value2 - json:{"minLength": 6} * @param {integer} age.query - age param description - json:{"minimum": 0, "maximum": 130} */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', responses: {}, security: [], tags: [], parameters: [{ deprecated: false, description: 'name param description', in: 'query', name: 'name', required: true, schema: { type: 'string', enum: [ 'value1', 'value2', ], minLength: 6, }, }, { deprecated: false, description: 'age param description', in: 'query', name: 'age', required: false, schema: { type: 'integer', minimum: 0, maximum: 130, }, }], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); ================================================ FILE: test/transforms/paths/requestBody.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('request body tests', () => { it('should not add requestBody when there is no body params', () => { const jsodInput = [` /** * POST /api/v1/ */ `]; const expected = { paths: { '/api/v1/': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body', () => { const jsodInput = [` /** * POST /api/v1 * @param {string} request.body.required - name body description */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'string', }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body with array type and no description', () => { const jsodInput = [ ` /** * POST /api/v1/albums * @param {array} request.body.required */ `]; const expected = { paths: { '/api/v1/albums': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: '', required: true, content: { 'application/json': { schema: { type: 'array', items: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body with array type', () => { const jsodInput = [` /** * POST /api/v1 * @param {array} request.body.required - name body description */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'array', items: { type: 'string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body for delete request', () => { const jsdocInput = [` /** * DELETE /message * @summary Delete messages listed under the specified tags * @param {array} request.body.required - Tags of the messages to delete */ `]; const expected = { paths: { '/message': { delete: { deprecated: false, description: undefined, summary: 'Delete messages listed under the specified tags', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'Tags of the messages to delete', required: true, content: { 'application/json': { schema: { type: 'array', items: { type: 'string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path multiple bodys', () => { const jsodInput = [` /** * POST /api/v1 * @param {number} body * @param {array} request.body.required - name body description */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'array', items: { type: 'string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc path reference bodys', () => { const jsodInput = [` /** * POST /api/v1 * @param {Song} request.body.required - name body description */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body with array of references', () => { const jsodInput = [` /** * POST /api/v1 * @param {array} request.body.required - name body description */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body with array of references for different content type', () => { const jsodInput = [` /** * POST /api/v1 * @param {array} request.body.required - name body description * @param {Song} request.body.required - name body description - application/xml * @param {object} request.body.required - name body description - application/x-www-form-urlencoded */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Song', }, }, }, 'application/xml': { schema: { $ref: '#/components/schemas/Song', }, }, 'application/x-www-form-urlencoded': { schema: { type: 'object', }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should parse jsdoc request body with examples', () => { const jsdocInput = [` /** * POST /api/v1 * @param {string} request.body.required - name body description * @example request - example payload * sample input string */ `]; const expected = { paths: { '/api/v1': { post: { deprecated: false, description: undefined, summary: '', responses: {}, tags: [], parameters: [], security: [], requestBody: { description: 'name body description', required: true, content: { 'application/json': { schema: { type: 'string', }, examples: { example1: { summary: 'example payload', value: 'sample input string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/responses.test.js ================================================ const chalk = require('chalk'); const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('response tests', () => { it('should parse jsdoc path spec with more than one response without type', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return 200 - success response - application/json * @return 400 - Bad request response */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', }, 400: { description: 'Bad request response', }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path spec with more than one response', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, 400: { description: 'Bad request response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc with responses, whether @return or @returns tags are used', () => { const jsdocInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json * @returns {object} 400 - Bad request response */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, 400: { description: 'Bad request response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should not parse jsdoc with wrong info and return warning in the console', () => { global.console = { ...global.console, warn: jest.fn(), }; const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', responses: {}, parameters: [], tags: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); // eslint-disable-next-line expect(console.warn) .toHaveBeenCalled(); }); it('should parse jsdoc with wrong info and return warning in the console', () => { global.console = { ...global.console, warn: jest.fn(), }; const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} default - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { default: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); // eslint-disable-next-line expect(console.warn) .not .toHaveBeenCalled(); }); it('should parse jsdoc path response with array type', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {array} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'array', items: { type: 'integer', }, }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path response with components', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {Song} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path response with array of components', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {array} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Song', }, }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path spec with more than one response and multiple content types', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {object} 200 - success response - application/json * @return {object} 400 - Bad request response * @return {string} 400 - Bad request response - application/xml */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, 400: { description: 'Bad request response', content: { 'application/json': { schema: { type: 'object', }, }, 'application/xml': { schema: { type: 'string', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path response with examples', () => { const jsdocInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {Song} 200 - success response - application/json * @return {object} 403 - forbidden response - application/json * @example response - 200 - example success response * { * "title": "untitled song", * "artist": "anonymous" * } * @example response - 403 - example error response * { * "error": "failed to retrieve results" * } */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { $ref: '#/components/schemas/Song', }, examples: { example1: { summary: 'example success response', value: { title: 'untitled song', artist: 'anonymous', }, }, }, }, }, }, 403: { description: 'forbidden response', content: { 'application/json': { schema: { type: 'object', }, examples: { example2: { summary: 'example error response', value: { error: 'failed to retrieve results', }, }, }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should not parse jsdoc path response with examples when an application/json example is malformed', () => { const jsdocInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {Song} 200 - success response - application/json * @return {object} 403 - forbidden response - application/json * @example response - 200 - example success response * { * "title": "untitled song", * "artist": "anonymous" * } * @example response - 403 - example error response * { * "error": "failed to retrieve results", * } */ `]; global.console = { ...global.console, warn: jest.fn(), }; const parsedJSDocs = jsdocInfo()(jsdocInput); setPaths({}, parsedJSDocs); // eslint-disable-next-line expect(console.warn) .toHaveBeenCalledTimes(1); // eslint-disable-next-line expect(console.warn) .toHaveBeenNthCalledWith( 1, chalk.yellow('[express-jsdoc-swagger] response example for status 403 with content-type application/json malformed'), ); }); it('should parse undefined if example has no valid types (request or response)', () => { const jsdocInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {Song} 200 - success response - application/json * @return {object} 403 - forbidden response - application/json * @example response - 200 - example success response * { * "title": "untitled song", * "artist": "anonymous" * } * @example res - 403 - example error response * { * "error": "failed to retrieve results" * } */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { $ref: '#/components/schemas/Song', }, examples: { example1: { summary: 'example success response', value: { title: 'untitled song', artist: 'anonymous', }, }, }, }, }, }, 403: { description: 'forbidden response', content: { 'application/json': { schema: { type: 'object', }, examples: undefined, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should not parse an example if has no valid status', () => { const jsdocInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {Song} 200 - success response - application/json * @return {object} 403 - forbidden response - application/json * @example response - 200 - example success response * { * "title": "untitled song", * "artist": "anonymous" * } * @example response - 333 - example error response * { * "error": "failed to retrieve results" * } */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { $ref: '#/components/schemas/Song', }, examples: { example1: { summary: 'example success response', value: { title: 'untitled song', artist: 'anonymous', }, }, }, }, }, }, 403: { description: 'forbidden response', content: { 'application/json': { schema: { type: 'object', }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsdocInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); it('should parse jsdoc path spec with more than multiple response and multiple content types', () => { const jsodInput = [` /** * GET /api/v1 * @summary This is the summary of the endpoint * @return {array} 200 - success response - application/json */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: 'This is the summary of the endpoint', parameters: [], tags: [], security: [], responses: { 200: { description: 'success response', content: { 'application/json': { schema: { type: 'array', items: { anyOf: [ { $ref: '#/components/schemas/Song', }, { $ref: '#/components/schemas/Song', }, ], }, }, }, }, }, }, }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result) .toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/security.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('Paths - security', () => { it('Should parse jsdoc security params into security array with each security type', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song * @security BasicAuth */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', security: [ { BasicAuth: [], }, ], tags: [], responses: {}, parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc multiple security params into security array with each security type', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song * @security BasicAuth * @security BearerAuth */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', security: [ { BasicAuth: [], }, { BearerAuth: [], }, ], tags: [], responses: {}, parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc multiple security with "and" configuration', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song * @security BasicAuth & BearerAuth */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', security: [ { BasicAuth: [], BearerAuth: [], }, ], tags: [], responses: {}, parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc multiple security with "or" configuration', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song * @security BasicAuth & BearerAuth | Oauth2 */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', security: [ { BasicAuth: [], BearerAuth: [], }, { Oauth2: [], }, ], tags: [], responses: {}, parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc empty security tag into empty array', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song * @security */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', security: [], tags: [], responses: {}, parameters: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc with no security tag and not include security in the swagger object', () => { const jsodInput = [` /** * POST /api/v1/song * @summary Create new song */ `]; const expected = { paths: { '/api/v1/song': { post: { deprecated: false, description: undefined, summary: 'Create new song', tags: [], responses: {}, parameters: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/tags.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const setPaths = require('../../../transforms/paths'); describe('Paths - tags', () => { it('Should parse jsdoc tags params into array with tag name', () => { const jsodInput = [` /** * GET /api/v1 * @tags album */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', tags: [ 'album', ], responses: {}, parameters: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc multiple tags params into array of tags', () => { const jsodInput = [` /** * GET /api/v1 * @tags album * @tags Years */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', tags: [ 'album', 'Years', ], responses: {}, parameters: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc multiple tags params into array of tags without description', () => { const jsodInput = [` /** * GET /api/v1 * @tags album * @tags Years - tag description */ `]; const expected = { paths: { '/api/v1': { get: { deprecated: false, description: undefined, summary: '', tags: [ 'album', 'Years', ], responses: {}, parameters: [], security: [], }, }, }, }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = setPaths({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/paths/validStatusCodes.test.js ================================================ const validStatusCodes = require('../../../transforms/paths/validStatusCodes'); test('Valid status codes snapshot', () => { expect(validStatusCodes).toMatchSnapshot(); }); ================================================ FILE: test/transforms/security/index.test.js ================================================ const parseSecuritySchemas = require('../../../transforms/security'); describe('Transform security schemas', () => { it('Should return a empty object with not securitySchemes', () => { const input = undefined; const expected = { components: {}, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); it('Should return a empty components object with not securitySchemes when security was not set', () => { const input = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, }; const expected = { info: { version: '1.0.0', title: 'Albums store', license: { name: 'MIT', }, }, components: {}, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); it('Should return a security array and securitySchemes within components field for http', () => { const input = { security: { BasicAuth: { type: 'http', scheme: 'basic', }, }, }; const expected = { security: [ { BasicAuth: [], }, ], components: { securitySchemes: { BasicAuth: { type: 'http', scheme: 'basic', }, }, }, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); it('Should return a security array and securitySchemes within components field for http (bearer format)', () => { const input = { security: { HTTPAuth: { type: 'http', scheme: 'basic', bearerFormat: 'JWT', }, }, }; const expected = { security: [ { HTTPAuth: [], }, ], components: { securitySchemes: { HTTPAuth: { type: 'http', scheme: 'basic', bearerFormat: 'JWT', }, }, }, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); it('Should return a security array and securitySchemes within components field for apiKey', () => { const input = { security: { ApiKeyAuth: { type: 'apiKey', in: 'header', name: 'api_key', }, }, }; const expected = { security: [ { ApiKeyAuth: [], }, ], components: { securitySchemes: { ApiKeyAuth: { type: 'apiKey', in: 'header', name: 'api_key', }, }, }, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); it('Should return a security array and securitySchemes, both with each security type', () => { const input = { security: { BasicAuth: { type: 'http', scheme: 'basic', }, bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, }; const expected = { security: [ { BasicAuth: [], }, { bearerAuth: [], }, ], components: { securitySchemes: { BasicAuth: { type: 'http', scheme: 'basic', }, bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, }, }; const result = parseSecuritySchemas(input); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/tags/index.test.js ================================================ const jsdocInfo = require('../../../consumers/jsdocInfo'); const parseTags = require('../../../transforms/tags'); describe('parseTags method', () => { it('should return empty tags array with not params', () => { const initialState = {}; const tags = undefined; const expected = { tags: [], }; const result = parseTags(initialState, tags); expect(result).toEqual(expected); }); it('Should not parse params that aren\'t tags', () => { const jsodInput = [` /** * GET /api/v1/album * @summary example of no summary */ `]; const expected = { tags: [], }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseTags({}, parsedJSDocs); expect(result).toEqual(expected); }); it('should return empty tags array with not an array as parameter', () => { const initialState = {}; const tags = 2; const expected = { tags: [], }; const result = parseTags(initialState, tags); expect(result).toEqual(expected); }); it('Should parse jsdoc tags into tags array with name and description', () => { const jsodInput = [` /** * GET /api/v1/album * @tags album - album tag description */ `, ` /** * GET /api/v1/artists * @tags artist - artist tag description */ `]; const expected = { tags: [ { name: 'album', description: 'album tag description', }, { name: 'artist', description: 'artist tag description', }, ], }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseTags({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should parse jsdoc tags into tags array without duplicate tags', () => { const jsodInput = [` /** * GET /api/v1/album * @tags album - album tag description */ `, ` /** * GET /api/v1/years * @tags artist - artist tag description */ `, ` /** * GET /api/v1/artists * @tags artist - artist tag description */ `]; const expected = { tags: [ { name: 'album', description: 'album tag description', }, { name: 'artist', description: 'artist tag description', }, ], }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseTags({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should return an empty string if the tag has no description', () => { const jsodInput = [` /** * GET /api/v1/album * @tags album - album tag description */ `, ` /** * GET /api/v1/years * @tags artist */ `]; const expected = { tags: [ { name: 'album', description: 'album tag description', }, { name: 'artist', description: '', }, ], }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseTags({}, parsedJSDocs); expect(result).toEqual(expected); }); it('Should return one tag with its description', () => { const jsodInput = [` /** * GET /api/v1/album * @tags album */ `, ` /** * GET /api/v1/years * @tags album - album tag description */ `]; const expected = { tags: [ { name: 'album', description: 'album tag description', }, ], }; const parsedJSDocs = jsdocInfo()(jsodInput); const result = parseTags({}, parsedJSDocs); expect(result).toEqual(expected); }); }); ================================================ FILE: test/transforms/utils/index.test.js ================================================ const setProperty = require('../../../transforms/utils/setProperty'); describe('setProperty method', () => { it('should not allow empty configuration', () => { expect(() => { setProperty()(); }).toThrow('item, key and options para are required'); }); }); ================================================ FILE: transforms/basic/contact.js ================================================ const setProperty = require('../utils/setProperty')('contact'); const getContact = (options = {}) => ({ name: setProperty(options, 'name', { type: 'string', required: true, }), url: setProperty(options, 'url', { type: 'string', defaultValue: '', }), email: setProperty(options, 'email', { type: 'string', defaultValue: '', }), }); module.exports = getContact; ================================================ FILE: transforms/basic/index.js ================================================ const info = require('./info'); const servers = require('./servers'); const getBasicInfo = (swaggerObjet = {}) => ({ ...swaggerObjet, info: { ...info(swaggerObjet.info) }, servers: servers(swaggerObjet.servers), }); module.exports = getBasicInfo; ================================================ FILE: transforms/basic/info.js ================================================ const setProperty = require('../utils/setProperty')('info'); const getContact = require('./contact'); const license = require('./license'); const getInfo = (options = {}) => ({ title: setProperty(options, 'title', { type: 'string', required: true, }), description: setProperty(options, 'description', { type: 'string', defaultValue: 'Add your description', }), ...(options.contact ? { contact: getContact(options.contact) } : {}), ...(options.license ? { license: license(options.license) } : {}), termsOfService: setProperty(options, 'termsOfService', { type: 'string', defaultValue: '', }), version: setProperty(options, 'version', { type: 'string', required: true, }), }); module.exports = getInfo; ================================================ FILE: transforms/basic/license.js ================================================ const setProperty = require('../utils/setProperty')('license'); const getContact = (options = {}) => ({ name: setProperty(options, 'name', { type: 'string', required: true, }), url: setProperty(options, 'url', { type: 'string', defaultValue: '', }), }); module.exports = getContact; ================================================ FILE: transforms/basic/servers.js ================================================ const setProperty = require('../utils/setProperty')('servers'); const setServer = (server = {}) => ({ url: setProperty(server, 'url', { type: 'string', required: true, }), description: setProperty(server, 'description', { type: 'string', defaultValue: '', }), ...(server.variables ? { variables: server.variables } : {}), }); const setServers = (servers = []) => { if (!servers || !Array.isArray(servers)) return []; return servers.map(setServer); }; module.exports = setServers; ================================================ FILE: transforms/components/index.js ================================================ const { getTagInfo, getTagsInfo } = require('../utils/tags'); const mapDescription = require('../utils/mapDescription'); const { refSchema, formatRefSchema } = require('../utils/refSchema'); const addEnumValues = require('../utils/enumValues'); const formatDescription = require('../utils/formatDescription'); const combineSchema = require('../utils/combineSchema'); const validateTypes = require('../utils/validateTypes'); const REQUIRED = 'required'; const getPropertyName = ({ name: propertyName }) => { const [name] = propertyName.split('.'); return name; }; const addTypeApplication = (applications, expression) => { if (!applications && !expression) return {}; return { type: expression.name.toLowerCase(), items: { type: applications[0].name, }, }; }; const addRefSchema = (typeName, applications, elements) => { if (!typeName && !elements) return { items: formatRefSchema(applications) }; return {}; }; const formatProperties = (properties, options = {}) => { if (!properties || !Array.isArray(properties)) return {}; return properties.reduce((acum, property) => { const name = getPropertyName(property); const isRequired = property.name.includes(REQUIRED); const { name: typeName, applications, expression, elements, } = property.type; const [descriptionValue, enumValues, jsonOptions] = formatDescription(property.description); const [description, format] = mapDescription(descriptionValue); return { ...acum, [name]: { description, ...refSchema(typeName), ...combineSchema(elements), ...addTypeApplication(applications, expression), ...addRefSchema(typeName, applications, elements), ...(format ? { format } : {}), ...addEnumValues(enumValues), // Add nullable to non-required fields if option to do that is enabled ...(options.notRequiredAsNullable && !isRequired ? { nullable: true, } : {}), ...(jsonOptions || {}), }, }; }, {}); }; const getRequiredProperties = properties => ( properties.filter(({ name }) => name.includes(REQUIRED)) ); const formatRequiredProperties = requiredProperties => requiredProperties.map(getPropertyName); const addDictionaryAdditionalProperties = typedef => { if ( typedef.type.expression && typedef.type.expression.name === 'Dictionary' ) { const typeName = typedef.type.applications[0].name; const isPrimitive = validateTypes(typeName); return { additionalProperties: { ...(isPrimitive ? { type: typeName } : { $ref: `#/components/schemas/${typeName}` }), }, }; } return {}; }; const parseSchema = (schema, options = {}) => { const typedef = getTagInfo(schema.tags, 'typedef'); const propertyValues = getTagsInfo(schema.tags, 'property'); const requiredProperties = getRequiredProperties(propertyValues); const [descriptionValue, enumValues, jsonOptions] = formatDescription(schema.description); const [description, format] = mapDescription(descriptionValue); if (!typedef || !typedef.name) return {}; const { elements, } = typedef.type; const type = typedef.type.name || 'object'; return { [typedef.name]: { ...combineSchema(elements), description, ...(requiredProperties.length ? { required: formatRequiredProperties(requiredProperties), } : {}), type, ...(type === 'object' ? { properties: formatProperties(propertyValues, options), } : {}), ...(format ? { format } : {}), ...addEnumValues(enumValues), ...addDictionaryAdditionalProperties(typedef), ...(jsonOptions || {}), }, }; }; const parseComponents = (swaggerObject = {}, components = [], options = {}) => { if (!components || !Array.isArray(components)) return { components: { schemas: {} } }; const componentSchema = components.reduce((acum, item) => ({ ...acum, ...parseSchema(item, options), }), {}); return { ...swaggerObject, components: { ...swaggerObject.components, schemas: componentSchema, }, }; }; module.exports = parseComponents; ================================================ FILE: transforms/index.js ================================================ const getBasicInfo = require('./basic'); const getSecuritySchemes = require('./security'); const getPaths = require('./paths'); const getComponents = require('./components'); const getTags = require('./tags'); module.exports = { getBasicInfo, getPaths, getComponents, getTags, getSecuritySchemes, }; ================================================ FILE: transforms/paths/content.js ================================================ const getSchema = require('./schema'); const DEFAULT_CONTENT_TYPE = 'application/json'; const getContent = entity => (type, contentType, originalValue, examples, extendSchema = {}) => { const schema = getSchema(entity, originalValue)(type); return { [contentType || DEFAULT_CONTENT_TYPE]: { schema: { ...schema, ...extendSchema, }, ...(examples ? { examples } : {}), }, }; }; module.exports = getContent; ================================================ FILE: transforms/paths/examples.js ================================================ const STATUS_CODES = require('./validStatusCodes'); const errorMessage = require('../utils/errorMessage'); const mapDescription = require('../utils/mapDescription'); const generator = require('../utils/generator'); const REQUEST_BODY = 'request'; const RESPONSE_BODY = 'response'; // Generates a new object with information on a request body example const parseRequestPayloadExample = (description, content) => { const [summary] = description; return { type: REQUEST_BODY, summary, value: content, }; }; const showError = message => { errorMessage(message); return {}; }; // Generates a new object with information on a response body example const parseResponsePayloadExample = (description, content) => { const [status, summary] = description; if (!STATUS_CODES[status]) { return showError(`${status} is not a valid status for a response`); } return { type: RESPONSE_BODY, status, summary, value: content, }; }; const getParsedExample = ({ type, metadata, content }) => { const types = { [REQUEST_BODY]: () => parseRequestPayloadExample(metadata, content), [RESPONSE_BODY]: () => parseResponsePayloadExample(metadata, content), default: () => showError(`Cannot determine where to use example of type ${type}`), }; return (types[type] || types.default)(); }; /** * Parses a single example tag contents. Depending on the type (response or * request), the expected data and returned structure will be different. * * To prevent compatibility issues between SO, all `\r\n` instances (used in * Windows by default as end of line sequence) will be replaced by `\n`. * * @param {string} exampleTagDescription - Text passed to the example tag. * * @return {object} Structured information about the example, including the * example type (request or response), summary, status code (only applicable to * response types) and the example code itself. */ const parseExample = ({ description: exampleTagDescription }) => { const formattedTagDescription = exampleTagDescription.replace(/\r\n/gi, '\n'); // The example content starts right after the first end of line character const contentStartIndex = formattedTagDescription.indexOf('\n'); const content = formattedTagDescription.substring(contentStartIndex + 1); const description = formattedTagDescription.substring(0, contentStartIndex); const [type, ...metadata] = mapDescription(description); return getParsedExample({ type, metadata, content }); }; /** * This receives a set of values extracted from "example" tags, and translates * them to objects with the example type (request or response), summary, status * code (only applicable to response types) and the example code itself. * * @param {object[]} exampleTags - Set of example tags, which description are * expected to have the following information in order to be parsed properly: * * > For request body examples: * * response - [example summary] * [example content] * * > For response examples: * * response - [http status code] - [example summary] * [example content] * * @return {object[]} List of objects containing the example type (request or * response), summary, status code (only applicable to response types) and * the example code itself. */ const exampleGenerator = generator(parseExample, 'type'); module.exports = exampleGenerator; ================================================ FILE: transforms/paths/formParams.js ================================================ const merge = require('merge'); const getContent = require('./content')('@paramFormBody'); const mapDescription = require('../utils/mapDescription'); const DEFAULT_CONTENT_TYPE = 'application/json'; const getRequiredValues = (currentState, contentType, key, isRequired) => { const paramContentType = contentType || DEFAULT_CONTENT_TYPE; if (currentState.content[paramContentType]) { return [ ...(currentState.content[paramContentType].schema.required || []), key, ]; } return isRequired ? [key] : []; }; const formParams = (currentState, key, body, isRequired, requestExamples) => { const [description, contentType] = mapDescription(body.description); const schema = { properties: { [key]: { type: body.type.name, description, }, }, required: getRequiredValues(currentState, contentType, key, isRequired), }; return { content: { ...merge.recursive( true, currentState.content, getContent({ name: 'object' }, contentType, description, requestExamples, schema), ), }, }; }; module.exports = formParams; ================================================ FILE: transforms/paths/index.js ================================================ const examplesGenerator = require('./examples'); const responsesGenerator = require('./responses'); const parametersGenerator = require('./parameters'); const requestBodyGenerator = require('./requestBody'); const { getTagInfo, getTagsInfo, formatDescriptionTag } = require('../utils/tags'); const { validRequestBodyMethods: bodyMethods, validHTTPMethod, } = require('../utils/httpMethods'); const formatSecurity = require('./security'); const formatTags = (tags = []) => tags.map(({ description }) => { const { name } = formatDescriptionTag(description); return name; }); const formatSummary = summary => (summary || {}).description || ''; const formatDescription = description => (description || {}).description || undefined; const setRequestBody = (lowerCaseMethod, bodyValues, requestExamples) => { const hasBodyValues = bodyValues.length > 0; const requestBody = requestBodyGenerator(requestExamples, bodyValues); return bodyMethods[lowerCaseMethod] && hasBodyValues ? { requestBody } : {}; }; const bodyParams = ({ name }) => name.includes('request.body') || name.includes('.form'); const getOperationId = operationIdTag => { if (!operationIdTag) return {}; return { operationId: operationIdTag.description, }; }; const pathValues = tags => { const examplesValues = getTagsInfo(tags, 'example'); const examples = examplesGenerator(examplesValues); const summary = getTagInfo(tags, 'summary'); const description = getTagInfo(tags, 'description'); const deprecated = getTagInfo(tags, 'deprecated'); const isDeprecated = !!deprecated; /* Response info */ const returnValues = [ ...getTagsInfo(tags, 'return'), ...getTagsInfo(tags, 'returns'), ]; const responseExamples = examples.filter(example => example.type === 'response'); const responses = responsesGenerator(returnValues, responseExamples); /* Parameters info */ const operationId = getTagsInfo(tags, 'operationId'); const paramValues = getTagsInfo(tags, 'param'); const parameters = parametersGenerator(paramValues); /* Tags info */ const tagsValues = getTagsInfo(tags, 'tags'); /* Security info */ const securityValues = getTagsInfo(tags, 'security'); /* Request body info */ const bodyValues = paramValues.filter(bodyParams); return { summary, description, isDeprecated, responses, parameters, tagsValues, bodyValues, securityValues, examples, operationId: operationId[0], }; }; const parsePath = (path, state) => { if (!path.description || !path.tags) return {}; const [method, endpoint] = path.description.split(' '); // if jsdoc comment does not contain structure - is not valid path const lowerCaseMethod = method.toLowerCase(); if (!validHTTPMethod(lowerCaseMethod)) return {}; const { tags } = path; const { summary, description, bodyValues, isDeprecated, responses, parameters, tagsValues, securityValues, examples, operationId, } = pathValues(tags); const requestExamples = examples.filter(example => example.type === 'request'); return { ...state, [endpoint]: { ...state[endpoint], [lowerCaseMethod]: { deprecated: isDeprecated, summary: formatSummary(summary), description: formatDescription(description), security: formatSecurity(securityValues), responses, parameters, tags: formatTags(tagsValues), ...(setRequestBody(lowerCaseMethod, bodyValues, requestExamples)), ...(getOperationId(operationId)), }, }, }; }; const getPathObject = paths => paths.reduce((acum, item) => ({ ...acum, ...parsePath(item, acum), }), {}); const parsePaths = (swaggerObject = {}, paths = []) => { if (!paths || !Array.isArray(paths)) return { paths: {} }; const pathObject = getPathObject(paths); return { ...swaggerObject, paths: pathObject, }; }; module.exports = parsePaths; ================================================ FILE: transforms/paths/parameters.js ================================================ const errorMessage = require('../utils/errorMessage'); const setProperty = require('../utils/setProperty')('parameter'); const formatDescription = require('../utils/formatDescription'); const getSchema = require('./schema'); const generator = require('../utils/generator'); const REQUIRED = 'required'; const DEPRECATED = 'deprecated'; const EXPLODE = 'explode'; const NOEXPLODE = 'noexplode'; const BODY_PARAM = 'body'; const FORM_TYPE = 'form'; const defaultParseParameter = {}; const parameterPayload = (options, schema) => ({ name: setProperty(options, 'name', { type: 'string', required: true, }), in: setProperty(options, 'in', { type: 'string', required: true, }), description: setProperty(options, 'description', { type: 'string', defaultValue: '', }), required: setProperty(options, 'required', { type: 'boolean', defaultValue: false, }), deprecated: setProperty(options, 'deprecated', { type: 'boolean', defaultValue: false, }), explode: setProperty(options, 'explode', { type: 'boolean', }), schema, }); const wrongInOption = paramValue => { if (!paramValue.includes('request.body')) { errorMessage(`If you want to add one @param as body you must provide "request.body" instead of ${paramValue}`); } return defaultParseParameter; }; const parseParameter = param => { const [name, inOption, ...extraOptions] = param.name.split('.'); if (!name || !inOption || inOption === FORM_TYPE) { return defaultParseParameter; } if (inOption === BODY_PARAM) { return wrongInOption(param.name); } const isRequired = extraOptions.includes(REQUIRED); const isDeprecated = extraOptions.includes(DEPRECATED); const shouldExplode = extraOptions.includes(EXPLODE); const shouldNotExplode = extraOptions.includes(NOEXPLODE); const [description, enumValues, jsonOptions] = formatDescription(param.description); const options = { name, in: inOption, required: isRequired, deprecated: isDeprecated, description, }; // Used this format because default when undefined is different depending on if it is a query/form or not if (shouldExplode) { options.explode = true; } else if (shouldNotExplode) { options.explode = false; } const schema = getSchema('@param', param.name)(param.type, enumValues, jsonOptions); return parameterPayload(options, schema); }; module.exports = generator(parseParameter, 'name'); ================================================ FILE: transforms/paths/requestBody.js ================================================ const setProperty = require('../utils/setProperty')('parameter'); const getContent = require('./content')('@paramBody'); const mapDescription = require('../utils/mapDescription'); const formParams = require('./formParams'); const formatExampleValues = require('../utils/formatExamples')('requestBody'); const REQUIRED = 'required'; const FORM_TYPE = 'form'; const formatExamples = (exampleValues = []) => exampleValues .reduce((exampleMap, example, i) => ({ ...exampleMap, [`example${i + 1}`]: { summary: example.summary, value: example.value, }, }), {}); const checkExamples = examples => { const isExample = Array.isArray(examples) && examples.length > 0; return isExample ? formatExamples(examples) : undefined; }; const parseBodyParameter = (currentState, body, examples) => { const [name, ...extraOptions] = body.name.split('.'); const isRequired = extraOptions.includes(REQUIRED); const hasForm = extraOptions.includes(FORM_TYPE); const [description, contentType] = mapDescription(body.description); const options = { name, required: isRequired, description }; const examplesParsed = examples.map(example => ( formatExampleValues(example, contentType) )); if (hasForm) { return formParams(currentState, name, body, isRequired, checkExamples(examplesParsed)); } return { ...currentState, description: setProperty(options, 'description', { type: 'string', }), required: setProperty(options, 'required', { type: 'boolean', defaultValue: false, }), content: { ...currentState.content, ...getContent(body.type, contentType, body.description, checkExamples(examplesParsed)), }, }; }; const INITIAL_STATE = { content: {} }; const requestBodyGenerator = (examples, params = []) => { if (!params || !Array.isArray(params)) return {}; return params.reduce((acc, body) => ( { ...acc, ...parseBodyParameter(acc, body, examples) } ), INITIAL_STATE); }; module.exports = requestBodyGenerator; ================================================ FILE: transforms/paths/responses.js ================================================ const errorMessage = require('../utils/errorMessage'); const STATUS_CODES = require('./validStatusCodes'); const mapDescription = require('../utils/mapDescription'); const getContent = require('./content')('@return'); const formatExampleValues = require('../utils/formatExamples')('responses'); const DEFAULT_CONTENT_TYPE = 'application/json'; const hasOldContent = (value, status) => (value[status] && value[status].content); const formatResponses = (values, examples) => values.reduce((acc, value) => { const [status, description, contentType] = mapDescription(value.description); if (!STATUS_CODES[status]) { errorMessage(`Status ${status} is not valid to create a response`); return {}; } const exampleList = formatExampleValues( examples[status], contentType || DEFAULT_CONTENT_TYPE, status, ); return { ...acc, [status]: { description, ...(value.type ? { content: { ...(hasOldContent(acc, status) ? { ...acc[status].content } : {}), ...getContent(value.type, contentType, value.description, exampleList), }, } : {}), }, }; }, {}); const formatExamples = (exampleValues = []) => exampleValues .reduce((exampleMap, example, i) => ({ ...exampleMap, [example.status]: { ...exampleMap[example.status], [`example${i + 1}`]: { summary: example.summary, value: example.value, }, }, }), {}); const responsesGenerator = (returnValues = [], exampleValues = []) => { if (!returnValues || !Array.isArray(returnValues)) return {}; return formatResponses(returnValues, formatExamples(exampleValues)); }; module.exports = responsesGenerator; ================================================ FILE: transforms/paths/schema.js ================================================ const errorMessage = require('../utils/errorMessage'); const combineSchema = require('../utils/combineSchema'); const addEnumValues = require('../utils/enumValues'); const { refSchema, formatRefSchema } = require('../utils/refSchema'); const getSchema = (entity, message) => (type, enumValues = [], jsonOptions = {}) => { if (!type) { return errorMessage(`Entity: ${entity} could not be parsed. Value: "${message}" is wrong`); } const nameType = type.name; let schema = { ...refSchema(nameType), }; schema = { ...schema, ...combineSchema(type.elements), ...addEnumValues(enumValues), ...jsonOptions, }; const notPrimitiveType = !nameType; if (notPrimitiveType && !type.elements) { const parseItems = formatRefSchema(type.applications); schema = { ...schema, type: type.expression.name.toLowerCase(), items: parseItems.items ? parseItems.items : parseItems, }; } return schema; }; module.exports = getSchema; ================================================ FILE: transforms/paths/security.js ================================================ const { flatArray } = require('../utils/arrays'); const AND_SEPARATOR = ' & '; const OR_SEPARATOR = ' | '; const formatOrValues = ({ description }) => { if (!description) { return []; } const securityNames = description.split(OR_SEPARATOR); return securityNames.map(names => ({ description: names, })); }; const formatSecurity = (securityValues = []) => ( flatArray(securityValues .map(formatOrValues)) .map(({ description }) => { const securityNames = description.split(AND_SEPARATOR); return { ...securityNames.reduce((acum, names) => ( { ...acum, [names]: [], } ), {}), }; }) ); module.exports = formatSecurity; ================================================ FILE: transforms/paths/validStatusCodes.js ================================================ const STATUS_CODES = { 202: 'Accepted', 502: 'Bad Gateway', 400: 'Bad Request', 409: 'Conflict', 100: 'Continue', 201: 'Created', 417: 'Expectation Failed', 424: 'Failed Dependency', 403: 'Forbidden', 504: 'Gateway Timeout', 410: 'Gone', 505: 'HTTP Version Not Supported', 418: 'I\'m a teapot', 419: 'Insufficient Space on Resource', 507: 'Insufficient Storage', 500: 'Server Error', 411: 'Length Required', 423: 'Locked', 420: 'Method Failure', 405: 'Method Not Allowed', 301: 'Moved Permanently', 302: 'Moved Temporarily', 207: 'Multi-Status', 300: 'Multiple Choices', 511: 'Network Authentication Required', 204: 'No Content', 203: 'Non Authoritative Information', 406: 'Not Acceptable', 404: 'Not Found', 501: 'Not Implemented', 304: 'Not Modified', 200: 'OK', 206: 'Partial Content', 402: 'Payment Required', 308: 'Permanent Redirect', 412: 'Precondition Failed', 428: 'Precondition Required', 102: 'Processing', 407: 'Proxy Authentication Required', 431: 'Request Header Fields Too Large', 408: 'Request Timeout', 413: 'Request Entity Too Large', 414: 'Request-URI Too Long', 416: 'Requested Range Not Satisfiable', 205: 'Reset Content', 303: 'See Other', 503: 'Service Unavailable', 101: 'Switching Protocols', 307: 'Temporary Redirect', 429: 'Too Many Requests', 401: 'Unauthorized', 422: 'Unprocessable Entity', 415: 'Unsupported Media Type', 305: 'Use Proxy', '1XX': '1XX Range', '2XX': '2XX Range', '3XX': '3XX Range', '4XX': '4XX Range', '5XX': '5XX Range', default: 'Default response', }; module.exports = STATUS_CODES; ================================================ FILE: transforms/security/index.js ================================================ const formatSecurity = securitySchemes => { const securityTypes = Object.keys(securitySchemes); return securityTypes.map(type => ({ [type]: [] })); }; const parseSecuritySchemas = (swaggerObject = {}) => { const { security } = swaggerObject; return { ...swaggerObject, ...(security ? { security: formatSecurity(security) } : {}), components: { ...swaggerObject.components, ...(security ? { securitySchemes: security } : {}), }, }; }; module.exports = parseSecuritySchemas; ================================================ FILE: transforms/tags/index.js ================================================ const { getTagsInfo, formatDescriptionTag } = require('../utils/tags'); const { flatArray, getIndexBy } = require('../utils/arrays'); const FILTER_TAG_KEY = 'name'; const formatTags = ({ tags = [] }) => { const infoTags = getTagsInfo(tags, 'tags'); return infoTags.map(({ description: tagDescription }) => { const { name, description = '' } = formatDescriptionTag(tagDescription); return { name, description, }; }); }; const sortByDescription = tags => [...tags].sort((a, b) => { if (!a.description && b.description) { return 1; } return -1; }); const sortTagsByName = tags => [...tags].sort((a, b) => ((a.name > b.name) ? 1 : -1)); const filterDuplicateTags = tags => ( tags.filter(({ name }, i) => getIndexBy(tags, FILTER_TAG_KEY, name) === i) ); const parseTags = (swaggerObject, data) => { if (!data || !Array.isArray(data)) return { tags: [] }; const tags = flatArray(data.map(formatTags)); const ordererTags = sortByDescription(tags); const uniqTags = filterDuplicateTags(ordererTags); return { ...swaggerObject, tags: sortTagsByName(uniqTags), }; }; module.exports = parseTags; ================================================ FILE: transforms/utils/arrays.js ================================================ const flatArray = elements => ( elements.reduce((acc, val) => acc.concat(val), []) ); const getIndexBy = (elements, key, value) => ( elements.findIndex(element => element[key] === value) ); module.exports = { flatArray, getIndexBy, }; ================================================ FILE: transforms/utils/combineSchema.js ================================================ const { refSchema } = require('./refSchema'); const VALID_TYPES = ['oneOf', 'anyOf', 'allOf']; /** * This method checks the first item of the data type list to validate if * it contains any of the keywords representing the different union types * in Swagger: 'oneOf', 'anyOf' or 'allOf'. * * @param {object[]} elements - List of data types for the property * @returns {string|null} Name of the union type (if any) */ const getUnionType = elements => { const unionType = elements[0].name; if (!VALID_TYPES.includes(unionType)) { return null; } return unionType; }; /** * This method receives an array of data types passed down to the * 'property' annotation, for example: * * > '{oneOf|string|null}'. * * The aim of this method is to process this array and generate * the schema for the property, including a list of its types. * * @param {object[]} elements - List of data types for the property * @returns Swagger schema for the property */ const combineSchema = elements => { let schema = {}; if (!elements || elements.length === 0) return schema; // Check if 'null' is part of the listed types and remove it from the array const nullIndex = elements.findIndex(el => el.type === 'NullLiteral'); if (nullIndex > 0) { elements.splice(nullIndex, 1); schema = { nullable: true }; } const unionType = getUnionType(elements); const types = !unionType ? elements : elements.slice(1); // If there are multiple types in the list, wrap them into a union type // ('oneOf' will be used by default if none is specified) if (types.length > 1 || unionType === 'allOf') { schema = { ...schema, [unionType || 'oneOf']: types.map(type => refSchema(type)), }; } else { // If there's only a type in the list, don't wrap it in 'oneOf' or 'anyOf' blocks schema = { ...schema, ...refSchema(types[0]), }; } return schema; }; module.exports = combineSchema; ================================================ FILE: transforms/utils/enumValues.js ================================================ const addEnumValues = (values = []) => { if (values.length === 0) return {}; return { enum: values }; }; module.exports = addEnumValues; ================================================ FILE: transforms/utils/errorMessage.js ================================================ const chalk = require('chalk'); const errorMessage = message => { // eslint-disable-next-line console.warn(chalk.yellow(`[express-jsdoc-swagger] ${message}`)); }; module.exports = errorMessage; ================================================ FILE: transforms/utils/formatDescription.js ================================================ const mapDescription = require('./mapDescription'); const errorMessage = require('./errorMessage'); const ENUM_IDENTIFIER = 'enum:'; const JSON_IDENTIFIER = 'json:'; const DESCRIPTION_DIVIDER = ' - '; const formatDescription = description => { const descriptionTypes = mapDescription(description); const descriptionValue = descriptionTypes.filter(value => ( !value.includes(ENUM_IDENTIFIER) && !value.includes(JSON_IDENTIFIER) )).join(DESCRIPTION_DIVIDER); const enumOption = descriptionTypes.find(value => value.includes(ENUM_IDENTIFIER)); const jsonOption = descriptionTypes.find(value => value.includes(JSON_IDENTIFIER)); const res = [descriptionValue]; if (enumOption) { const [, enumOptions] = enumOption.split('enum:'); const enumValues = enumOptions.split(','); res.push(enumValues); } else { res.push(undefined); } if (jsonOption) { try { const jsonOptions = JSON.parse(jsonOption.slice(JSON_IDENTIFIER.length)); if (typeof jsonOptions !== 'object') { throw new Error('options must be object'); } res.push(jsonOptions); } catch (err) { errorMessage(`json options are malformed: ${err.message}`); } } return res; }; module.exports = formatDescription; ================================================ FILE: transforms/utils/formatExamples.js ================================================ const errorMessage = require('./errorMessage'); const DEFAULT_CONTENT_TYPE = 'application/json'; const formatJSONExamples = type => (exampleList, contentType, status) => { let cloneExamples = type === 'requestBody' ? { ...exampleList } : undefined; if (exampleList && contentType === DEFAULT_CONTENT_TYPE) { cloneExamples = { ...exampleList }; Object.keys(cloneExamples) .filter(k => typeof cloneExamples[k].value === 'string') .forEach(k => { try { cloneExamples[k].value = JSON.parse(cloneExamples[k].value); } catch (err) { const message = type === 'requestBody' ? 'requestBody example malformed' : `response example for status ${status} with content-type ${DEFAULT_CONTENT_TYPE} malformed`; errorMessage(message); } }); } return cloneExamples; }; module.exports = formatJSONExamples; ================================================ FILE: transforms/utils/generator.js ================================================ const generator = (parseParameter, filterKey) => paramValues => { if (!paramValues || !Array.isArray(paramValues)) return []; return paramValues.map(parseParameter).filter(param => param[filterKey]); }; module.exports = generator; ================================================ FILE: transforms/utils/httpMethods.js ================================================ const validRequestBodyMethods = { get: false, delete: true, head: false, post: true, put: true, connect: true, options: true, trace: true, patch: true, }; const validHTTPMethod = method => Object.keys(validRequestBodyMethods).includes(method); module.exports = { validRequestBodyMethods, validHTTPMethod, }; ================================================ FILE: transforms/utils/mapDescription.js ================================================ const DECRIPTION_DIVIDER = ' - '; const mapDescription = description => (description || '').split(DECRIPTION_DIVIDER); module.exports = mapDescription; ================================================ FILE: transforms/utils/refSchema.js ================================================ const validateTypes = require('./validateTypes'); const REF_ROUTE = '#/components/schemas/'; const refSchema = value => { // support * @return {array} 200 - fetch Home Content response if (value && value.type === 'UnionType') { const items = []; value.elements.forEach(el => { const isPrimitiveType = validateTypes(el.name); items.push(isPrimitiveType ? { type: el.name } : { $ref: `${REF_ROUTE}${el.name}` }); }); return { anyOf: items, }; } // support * @property {anyOf|Song[]|Album|string|string[]} firstSong if (value && value.expression && value.expression.name.toLowerCase() === 'array' && value.type === 'TypeApplication') { const isPrimitiveType = validateTypes(value.applications[0].name); return isPrimitiveType ? { type: 'array', items: { type: value.applications[0].name }, } : { type: 'array', items: { $ref: `${REF_ROUTE}${value.applications[0].name}` }, }; } if (!value) return {}; const nameValue = value.name || value; // support null if (nameValue.type === 'NullLiteral') return { nullable: true }; const isPrimitive = validateTypes(nameValue); return isPrimitive ? { type: nameValue } : { $ref: `${REF_ROUTE}${nameValue}` }; }; const formatRefSchema = (applications = []) => applications.reduce((itemAcc, itemTypes) => ({ ...itemAcc, ...refSchema(itemTypes), }), {}); module.exports = { refSchema, formatRefSchema, }; ================================================ FILE: transforms/utils/setProperty.js ================================================ const errorMessage = require('./errorMessage'); const setProperty = entity => { const requiredError = (key, item) => ( new Error(`Key ${key} is required in item ${JSON.stringify(item)} for Entity ${entity}`) ); const warnType = (type, value) => ( errorMessage(`${type} is not valid with value ${value} for Entity ${entity}`) ); return (item, key, options) => { if (!item || !key || !options) { throw (new Error('item, key and options para are required')); } const value = item[key]; if (options.required && value === undefined) throw requiredError(key, item); if (value === undefined || value === null) return options.defaultValue; // eslint-disable-next-line if (typeof (value) !== options.type) warnType(options.type, value); return value; }; }; module.exports = setProperty; ================================================ FILE: transforms/utils/tags.js ================================================ const mapDescription = require('./mapDescription'); const getTagInfo = (tags, key) => tags.find(({ title }) => title === key); const getTagsInfo = (tags, key) => tags.filter(({ title }) => title === key); const formatDescriptionTag = desc => { const [name, description] = mapDescription(desc); return { name, description }; }; module.exports = { getTagInfo, getTagsInfo, formatDescriptionTag, }; ================================================ FILE: transforms/utils/validateTypes.js ================================================ const TYPES = [ 'integer', 'number', 'string', 'boolean', 'object', ]; const validateTypes = type => TYPES.includes(type); module.exports = validateTypes;