├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── PULL_REQUEST_TEMPLATE.md ├── contribution-guideline.md └── workflows │ └── build.yml ├── .gitignore ├── PROPOSAL-DRAFT-V1.0.md ├── README.md ├── __tests__ └── parser.test.js ├── jest.config.js ├── nodemon.json ├── package.json ├── src ├── addons │ └── mongoose │ │ └── mongoose-transformer.js ├── parser │ ├── core.pegjs │ ├── init.js │ └── merger.js └── transformer │ └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tauqeer.insta@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please include a summary of the change or which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ### Type of change 6 | 7 | ##### Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | ### How has this been tested? 15 | 16 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 17 | 18 | - [ ] Test A 19 | - [ ] Test B 20 | 21 | ### Screenshot ( applicable for UI changes) 22 | 23 | ### Checklist: 24 | 25 | - [ ] Self-review of my own code 26 | - [ ] Commented the code, particularly in hard-to-understand areas 27 | - [ ] Corresponding changes to the documentation 28 | - [ ] Ran test on local 29 | - [ ] Generated build on local -------------------------------------------------------------------------------- /.github/contribution-guideline.md: -------------------------------------------------------------------------------- 1 | ### Thank You! 2 | Thank you for considering contributing to "The Swagger Schema Generator" - TSSG, an open source project for developer community to ease the generation of OPEN API spec. 3 | 4 | Important: Following these guidelines help to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 5 | 6 | [Code of Conduct](./CODE_OF_CONDUCT.md) 7 | 8 | ##### [Pull Request](./PULL_REQUEST_TEMPLATE.md) 9 | There is a pull request template, which needs to be filled for any pull request. It will be automatically created once you create a pull request, but you can go through it here [PR-template](./PULL_REQUEST_TEMPLATE.md). 10 | Please update the documentation in the [README](../README.md) with details of any new features added, provide examples on how to use the newly added feature. -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | - run: npm install 18 | - run: npm run build:prod 19 | - run: npm run test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | yarn-error.log 5 | src/parser/parser-auto-gen.pegjs 6 | src/parser/parser.js 7 | example 8 | -------------------------------------------------------------------------------- /PROPOSAL-DRAFT-V1.0.md: -------------------------------------------------------------------------------- 1 | # TSSG 2 | 3 | _Current Working Draft_ 4 | 5 | ## Introduction 6 | 7 | This is the specification for TSSG(The Swagger Schema Generator), which enables developers to generate OpenAPI Schema with an easy and concise syntax/grammar. 8 | 9 | > The [OpenAPI Specification](https://swagger.io/specification/) (OAS) defines a standard, language-agnostic 10 | > interface to RESTful APIs which allows both humans and computers to 11 | > discover and understand the capabilities of the service without access 12 | > to source code, documentation, or through network traffic inspection. 13 | > When properly defined, a consumer can understand and interact with the 14 | > remote service with a minimal amount of implementation logic. 15 | 16 | ## Table of Contents 17 | 18 | - [Overview](#overview) 19 | - [Language](#language) 20 | - [White Spaces](#white-spaces) 21 | - [Line Terminators](#line-terminators) 22 | - [Comments](#comments) 23 | - [Lexical Tokens](#lexical-tokens) 24 | - [Number](#number) 25 | - [String](#string) 26 | - [Boolean](#boolean) 27 | - [Object](#object) 28 | - [Array](#array) 29 | - [Schema Block](#schema-block) 30 | - [Schema Expression](#schema-expression) 31 | - [Extendable Schema Expression](#extendable-schema-expression) 32 | - [RequestBodies Block](#requestbodies-block) 33 | - [RequestBody Expression](#requestbody-expression) 34 | - [Extendable RequestBody](#extendable-requestbody) 35 | - [Parameters Block](#parameters-block) 36 | - [Reference](#reference) 37 | - [Paths](#paths) 38 | 39 | ## Overview 40 | 41 | Writing OpenAPI Schema can be tiresome and time wasting task if you write a lot of API Documentation. Updating existing Schema can also be cumbersome and confusing especially when project grows to hundreds of APIs. TSSG is here to help you write schema in an easy, clean and concise way. We have proposed a new and easy to understand Syntax/Grammar for this. It allows you to write less and get full OpenAPI Schema without writing and repeating same line again and again. 42 | 43 | For example, Consider the following object Schema of User when written according to OpenAPI Specification: 44 | 45 | ```json 46 | { 47 | "type": "object", 48 | "properties": { 49 | "name": { 50 | "type": "string" 51 | }, 52 | "age": { 53 | "type": "number" 54 | }, 55 | "email": { 56 | "type": "string" 57 | }, 58 | "address": { 59 | "type": "object", 60 | "properties": { 61 | "street": { 62 | "type": "string" 63 | }, 64 | "city": { 65 | "type": "string" 66 | }, 67 | "country": { 68 | "type": "string" 69 | }, 70 | "zipcode": { 71 | "type": "string" 72 | } 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | The above schema has a lot of repetition and if the schema is more complex that have nested object or array of object, it gets more complex to write. 80 | 81 | On the other hand, with TSSG, above schema can be written as: 82 | 83 | ```javascript 84 | { 85 | name: string, 86 | age: number, 87 | email: string, 88 | address: { 89 | street: string, 90 | city: string, 91 | country: string, 92 | zipcode: string, 93 | } 94 | } 95 | 96 | ``` 97 | 98 | ## Language 99 | 100 | A TSSG document is defined as a syntactic grammar where terminal symbols are tokens (indivisible lexical units). These tokens are defined in a lexical grammar which matches patterns of source characters. 101 | This sequence of lexical tokens are then scanned from left to right to produce an abstract syntax tree (AST) according to the Document syntactical grammar. 102 | 103 | We refer to A TSSG documents as programs. A program may contain expression blocks(schemas, requestbodies, paths, parameters), lexical tokens and Ignored lexical grammars(comments, whitespaces, line terminators). 104 | 105 | ### White Spaces 106 | 107 | White space is used to improve legibility of source text and act as separation between tokens, and any amount of white space may appear before or after any token. White space between tokens is not significant to the semantic meaning of a TSSG Document, however white space characters may appear within a String or Comment token. 108 | 109 | ### Line Terminators 110 | 111 | Like white space, line terminators are used to improve the legibility of source text, any amount may appear before or after any other token and have no significance to the semantic meaning of a TSSG Document. Line terminators are not found within any other token. 112 | 113 | ### Comments 114 | 115 | TSSG source documents may contain multi‐line comments, starting with the `/*` marker and ending with `*/` marker. 116 | 117 | A comment can contain any Unicode code point in SourceCharacter except LineTerminator so a comment always consists of all code points starting with the `/*` character up to `*/` 118 | 119 | Comments are Ignored like white space and may appear after any token, or before a LineTerminator, and have no significance to the semantic meaning of a TSSG Document. 120 | 121 | ```javascript 122 | /* this is 123 | multi line 124 | comment */ 125 | ``` 126 | 127 | ### Lexical Tokens 128 | 129 | #### Number 130 | 131 | ```javascript 132 | age: number; 133 | ``` 134 | 135 | #### String 136 | 137 | ```javascript 138 | name: string; 139 | ``` 140 | 141 | #### Boolean 142 | 143 | ```javascript 144 | isVerified: boolean; 145 | ``` 146 | 147 | #### Object 148 | 149 | ```javascript 150 | 151 | address: { 152 | city: string, 153 | country: string, 154 | zip: number 155 | } 156 | ``` 157 | 158 | > Notice `address` which is an `Object` with 3 properties. 159 | 160 | #### Array 161 | 162 | ```javascript 163 | profileImages: { 164 | size: { 165 | width: number, 166 | height: number 167 | }, 168 | url: string 169 | }[] 170 | ``` 171 | 172 | > Notice `profileImages` which is an `Array` of `Objects` with 2 properties. 173 | 174 | ### Schema Block 175 | 176 | #### Schema Expression 177 | 178 | Schemas block can be written as follow: 179 | 180 | ```javascript 181 | Schemas { 182 | 183 | User { 184 | name: string, 185 | email: string 186 | } 187 | 188 | } 189 | ``` 190 | 191 | #### Extendable Schema Expression 192 | 193 | We can extend schemas using `extends` keyword 194 | 195 | ```javascript 196 | Schemas { 197 | 198 | BaseUser { 199 | name: string, 200 | email: string 201 | } 202 | 203 | Employee extends BaseUser { 204 | salary: number, 205 | department: string 206 | } 207 | 208 | } 209 | ``` 210 | 211 | ### RequestBodies Block 212 | 213 | RequestBodies block can be written similarly as Schemas block: 214 | 215 | #### RequestBody Expression 216 | 217 | ```javascript 218 | RequestBodies { 219 | 220 | ListParams { 221 | page: number, 222 | limit: number, 223 | totalPages: number, 224 | filters: { 225 | ids: string[] 226 | } 227 | } 228 | 229 | } 230 | ``` 231 | 232 | #### Extendable RequestBody 233 | 234 | We can extend RequestBodies using `extends` keyword 235 | 236 | ```javascript 237 | RequestBodies { 238 | 239 | BaseListParams { 240 | page: number, 241 | limit: number, 242 | totalPages: number 243 | } 244 | 245 | ListUsers extends BaseListParams { 246 | filters: { 247 | ids: string[] 248 | } 249 | } 250 | 251 | } 252 | ``` 253 | 254 | ### Parameters Block 255 | 256 | Similarly parameters block can be written as: 257 | 258 | ```javascript 259 | Parameters { 260 | 261 | GetUser { 262 | id: string 263 | } 264 | 265 | } 266 | ``` 267 | 268 | ### Reference 269 | 270 | We can refer to any existing Schema, RequestBodies or any custom type: 271 | 272 | ```javascript 273 | { 274 | user: Schemas.User, 275 | userList: Schemas.User[] 276 | } 277 | ``` 278 | 279 | > Note: Here we are refering to existing [Schemas.User](#schema-expression). 280 | 281 | ### Paths 282 | 283 | ```javascript 284 | /v1-user (user) { 285 | 286 | post: { 287 | description: "description goes here", 288 | requestBody: RequestBodies.V1GetUser.address, 289 | responses: { 290 | 200: { 291 | description: "", 292 | content@application/json: [@Schemas.V1User], 293 | content@text/plain: string 294 | } 295 | } 296 | } 297 | 298 | get: { 299 | description: "description goes here", 300 | requestBody: requestBody.V1GetUser, 301 | responses: { 302 | 200: { 303 | description: "", 304 | content@application/json: Schemas.ArrayOfUsers, 305 | content@text/plain: string 306 | } 307 | } 308 | } 309 | 310 | } 311 | 312 | ``` 313 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

TSSG Syntax Parser

3 |

Parser that generates AST for given TSSG Syntax

4 | 5 | Current Version 6 | 7 | 8 | Current Version 9 | 10 | 11 | Current Version 12 | 13 |
14 | 15 | --- 16 | 17 | This is the complete rewrite of the TSSG Parser, which is now able to build the AST for TSSG Syntax V0.0.1. 18 | 19 | > Please note that this project is work-in-progress and will support other advance features soon. 20 | 21 | ##### Added Support 22 | 23 | Parser supports following expressoins: 24 | 25 | - Schemas Block Expression 26 | - Schema Expression 27 | - Extendable Schema Expression 28 | - Request Bodies Block Expression 29 | - RequestBody Expression 30 | - Extendable RequestBody Expression 31 | - Parameters Block Expression 32 | - Parameter Expression 33 | 34 | Parser support following data types: 35 | 36 | - Literal 37 | - Identifier 38 | - Boolean 39 | - Object 40 | - Array 41 | - Function Calls 42 | 43 | > Note: Complex data types like `Array` of `String`, `Array` of `Object` is also supported. 44 | 45 | Parser also supports miscellaneous features: 46 | 47 | - Whitespaces 48 | - Multiline Comments 49 | 50 | #### Schema Block Expression 51 | 52 | Schemas block can be written as follow: 53 | 54 | ``` 55 | Schemas { 56 | 57 | BaseUser { 58 | name: string, 59 | email: string 60 | } 61 | 62 | Employee extends BaseUser { 63 | salary: number, 64 | department: string 65 | } 66 | 67 | } 68 | ``` 69 | 70 | > Notice how we can `extend` schemas. 71 | 72 | #### RequestBodies Block Expression 73 | 74 | RequestBodies block can be written as `Schemas` block: 75 | 76 | ``` 77 | RequestBodies { 78 | 79 | BaseListParams { 80 | page: number, 81 | limit: number, 82 | totalPages: number 83 | } 84 | 85 | ListUsers extends BaseListParams { 86 | filters: { 87 | ids: string[] 88 | } 89 | } 90 | 91 | } 92 | ``` 93 | 94 | #### Parameters Block Expression 95 | 96 | Similarly parameters block can be written as: 97 | 98 | ``` 99 | Parameters { 100 | 101 | GetUser { 102 | id: string 103 | } 104 | 105 | } 106 | ``` 107 | 108 | ### Data Types 109 | 110 | Data types can be used as follow: 111 | 112 | ``` 113 | User { 114 | name: string, 115 | age: integer, 116 | address: { 117 | city: string, 118 | country: string, 119 | zip: number 120 | }, 121 | isVerified: boolean, 122 | profileImages: { 123 | size: { 124 | width: number, 125 | height: number 126 | } 127 | url: string 128 | }[] 129 | } 130 | ``` 131 | 132 | > Notice `address` which is an `Object` with 3 properties. And `profileImages` which is an `Array` of `Objects` with 2 properties. 133 | 134 | #### Syntax Draft Proposal 135 | 136 | Read our [Proposal Draft](./PROPOSAL-DRAFT-V1.0.md). 137 | 138 | #### Hey there! Want to contribute? 139 | 140 | 1. Update parser code and run `yarn build:prod` to generate the parser. 141 | 2. Update transformer code and again run `yarn build:prod` generate transformer. 142 | 3. Update `tests` and create `PR`. 143 | 144 | Read our [Contribution Guidline](./.github/contribution-guideline.md). 145 | -------------------------------------------------------------------------------- /__tests__/parser.test.js: -------------------------------------------------------------------------------- 1 | const peg = require("pegjs"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | let parser; 6 | 7 | beforeAll(() => { 8 | const grammar = fs.readFileSync( 9 | path.resolve(__dirname, "../src/parser/parser-auto-gen.pegjs"), 10 | { 11 | encoding: "utf-8", 12 | } 13 | ); 14 | parser = peg.generate(grammar); 15 | }); 16 | 17 | describe("tests for Parameters Block", () => { 18 | it("given correct Parameters Block, should return correct parsed output ", () => { 19 | const example = ` 20 | Parameters { 21 | GetUser { 22 | id: string, 23 | filter: {} 24 | } 25 | } 26 | `; 27 | const expected = { 28 | type: "Program", 29 | body: [ 30 | { 31 | type: "ParametersBlockExpression", 32 | body: [ 33 | { 34 | type: "ParameterExpression", 35 | name: "GetUser", 36 | body: { 37 | type: "ObjectExpression", 38 | required: ["id", "filter"], 39 | properties: [ 40 | { 41 | type: "Property", 42 | optional: false, 43 | key: { type: "IdentifierExpression", name: "id" }, 44 | value: { type: "IdentifierExpression", name: "string" }, 45 | }, 46 | { 47 | type: "Property", 48 | optional: false, 49 | key: { type: "IdentifierExpression", name: "filter" }, 50 | value: { type: "ObjectExpression", properties: [] }, 51 | }, 52 | ], 53 | }, 54 | }, 55 | ], 56 | }, 57 | ], 58 | }; 59 | expect(parser.parse(example)).toEqual(expected); 60 | }); 61 | 62 | it("given empty Parameters Block, should return correct parsed output ", () => { 63 | const example = ` 64 | Parameters { 65 | } 66 | `; 67 | const expected = { 68 | type: "Program", 69 | body: [{ type: "ParametersBlockExpression", body: [] }], 70 | }; 71 | expect(parser.parse(example)).toEqual(expected); 72 | }); 73 | 74 | it("given Parameters Block with more spaces, should return correct output ", () => { 75 | const example = ` 76 | Parameters { 77 | GetUser { 78 | id?: string, 79 | filter: {} 80 | } 81 | } 82 | `; 83 | const expected = { 84 | type: "Program", 85 | body: [ 86 | { 87 | type: "ParametersBlockExpression", 88 | body: [ 89 | { 90 | type: "ParameterExpression", 91 | name: "GetUser", 92 | body: { 93 | type: "ObjectExpression", 94 | required: ["filter"], 95 | properties: [ 96 | { 97 | type: "Property", 98 | optional: true, 99 | key: { type: "IdentifierExpression", name: "id" }, 100 | value: { type: "IdentifierExpression", name: "string" }, 101 | }, 102 | { 103 | type: "Property", 104 | optional: false, 105 | key: { type: "IdentifierExpression", name: "filter" }, 106 | value: { type: "ObjectExpression", properties: [] }, 107 | }, 108 | ], 109 | }, 110 | }, 111 | ], 112 | }, 113 | ], 114 | }; 115 | expect(parser.parse(example)).toEqual(expected); 116 | }); 117 | 118 | it("given Parameters Block with missing opening curly bracket {, should return syntaxError ", () => { 119 | function parseExample() { 120 | const example = ` 121 | Parameters 122 | GetUser { 123 | id: string, 124 | filter: {} 125 | } 126 | } 127 | `; 128 | parser.parse(example); 129 | } 130 | expect(parseExample).toThrowError('Expected "{" but "G" found.'); 131 | }); 132 | 133 | it("given Parameters Block with missing closing curly bracket }, should return syntaxError ", () => { 134 | function parseExample() { 135 | const example = ` 136 | Parameters { 137 | GetUser { 138 | id: string, 139 | filter: {} 140 | } 141 | `; 142 | parser.parse(example); 143 | } 144 | expect(parseExample).toThrowError( 145 | 'Expected "}" or [_a-zA-Z] but end of input found.' 146 | ); 147 | }); 148 | 149 | it("given Parameters Block with missing expression block, should return syntaxError ", () => { 150 | function parseExample() { 151 | const example = ` 152 | Parameters { 153 | { 154 | id: string, 155 | filter: {} 156 | } 157 | } 158 | `; 159 | parser.parse(example); 160 | } 161 | expect(parseExample).toThrowError( 162 | 'Expected "}" or [_a-zA-Z] but "{" found.' 163 | ); 164 | }); 165 | 166 | it("given Parameters Block with an incorrect expression, should return syntaxError ", () => { 167 | function parseExample() { 168 | const example = ` 169 | Parameters { 170 | undefined 171 | } 172 | `; 173 | parser.parse(example); 174 | } 175 | expect(parseExample).toThrowError('Expected "{" but "}" found.'); 176 | }); 177 | }); 178 | 179 | describe("tests for RequestBodies Block", () => { 180 | it("given RequestBodies Block, should return correct parsed output ", () => { 181 | const example = ` 182 | RequestBodies { 183 | GetUserById { 184 | id: string, 185 | } 186 | } 187 | `; 188 | const expected = { 189 | type: "Program", 190 | body: [ 191 | { 192 | type: "RequestBodiesBlockExpression", 193 | body: [ 194 | { 195 | type: "RequestBodyExpression", 196 | name: "GetUserById", 197 | body: { 198 | type: "ObjectExpression", 199 | required: ["id"], 200 | properties: [ 201 | { 202 | type: "Property", 203 | optional: false, 204 | key: { type: "IdentifierExpression", name: "id" }, 205 | value: { type: "IdentifierExpression", name: "string" }, 206 | }, 207 | ], 208 | }, 209 | }, 210 | ], 211 | }, 212 | ], 213 | }; 214 | expect(parser.parse(example)).toEqual(expected); 215 | }); 216 | 217 | it("given empty RequestBodies Block, should return correct parsed output ", () => { 218 | const example = ` 219 | RequestBodies { 220 | } 221 | `; 222 | const expected = { 223 | type: "Program", 224 | body: [{ type: "RequestBodiesBlockExpression", body: [] }], 225 | }; 226 | expect(parser.parse(example)).toEqual(expected); 227 | }); 228 | 229 | it("given RequestBodies Block with more spaces, should return correct output ", () => { 230 | const example = ` 231 | RequestBodies { 232 | GetUser { 233 | id: string, 234 | filter: {} 235 | } 236 | } 237 | `; 238 | const expected = { 239 | type: "Program", 240 | body: [ 241 | { 242 | type: "RequestBodiesBlockExpression", 243 | body: [ 244 | { 245 | type: "RequestBodyExpression", 246 | name: "GetUser", 247 | body: { 248 | type: "ObjectExpression", 249 | required: ["id", "filter"], 250 | properties: [ 251 | { 252 | type: "Property", 253 | optional: false, 254 | key: { type: "IdentifierExpression", name: "id" }, 255 | value: { type: "IdentifierExpression", name: "string" }, 256 | }, 257 | { 258 | type: "Property", 259 | optional: false, 260 | key: { type: "IdentifierExpression", name: "filter" }, 261 | value: { type: "ObjectExpression", properties: [] }, 262 | }, 263 | ], 264 | }, 265 | }, 266 | ], 267 | }, 268 | ], 269 | }; 270 | expect(parser.parse(example)).toEqual(expected); 271 | }); 272 | 273 | it("given RequestBodies Block with missing opening curly bracket {, should return syntaxError ", () => { 274 | function parseExample() { 275 | const example = ` 276 | RequestBodies 277 | GetUser { 278 | id: string, 279 | filter: {} 280 | } 281 | } 282 | `; 283 | parser.parse(example); 284 | } 285 | expect(parseExample).toThrowError('Expected "{" but "G" found.'); 286 | }); 287 | 288 | it("given RequestBodies Block with missing closing curly bracket }, should return syntaxError ", () => { 289 | function parseExample() { 290 | const example = ` 291 | RequestBodies { 292 | GetUser { 293 | id: string, 294 | filter: {} 295 | } 296 | `; 297 | parser.parse(example); 298 | } 299 | expect(parseExample).toThrowError( 300 | 'Expected "}" or [_a-zA-Z] but end of input found.' 301 | ); 302 | }); 303 | 304 | it("given RequestBodies Block with missing expression block, should return syntaxError ", () => { 305 | function parseExample() { 306 | const example = ` 307 | RequestBodies { 308 | { 309 | id: string, 310 | filter: {} 311 | } 312 | } 313 | `; 314 | parser.parse(example); 315 | } 316 | expect(parseExample).toThrowError( 317 | 'Expected "}" or [_a-zA-Z] but "{" found.' 318 | ); 319 | }); 320 | 321 | it("given RequestBodies Block with an incorrect expression, should return syntaxError ", () => { 322 | function parseExample() { 323 | const example = ` 324 | RequestBodies { 325 | undefined 326 | } 327 | `; 328 | parser.parse(example); 329 | } 330 | expect(parseExample).toThrowError('Expected "{" but "}" found.'); 331 | }); 332 | }); 333 | 334 | describe("test for repeater expression", () => { 335 | it.each([ 336 | ` 337 | Schemas { 338 | User { 339 | favColors: []string 340 | } 341 | } 342 | `, 343 | ` 344 | Schemas { 345 | User { 346 | favColors: 12[] 347 | } 348 | } 349 | `, 350 | // array of object 351 | ` 352 | Schemas { 353 | User { 354 | favColors: []{} 355 | } 356 | } 357 | `, 358 | ])( 359 | "given incorrect repeater expression, it should return syntaxError", 360 | (example) => { 361 | function parseExample() { 362 | parser.parse(example); 363 | } 364 | expect(parseExample).toThrowError(); 365 | } 366 | ); 367 | 368 | it("given correct repeater expression, should return correct output", () => { 369 | const expected = { 370 | type: "Program", 371 | body: [ 372 | { 373 | type: "SchemasBlockExpression", 374 | body: [ 375 | { 376 | type: "SchemaExpression", 377 | name: "User", 378 | body: { 379 | type: "ObjectExpression", 380 | required: ["favColors", "arrayOfNumbers", "arrayOfObjects"], 381 | properties: [ 382 | { 383 | type: "Property", 384 | optional: false, 385 | key: { 386 | type: "IdentifierExpression", 387 | name: "favColors", 388 | }, 389 | value: { 390 | type: "IdentifierExpression", 391 | name: "string", 392 | repeater: "array", 393 | }, 394 | }, 395 | { 396 | type: "Property", 397 | optional: false, 398 | key: { 399 | type: "IdentifierExpression", 400 | name: "arrayOfNumbers", 401 | }, 402 | value: { 403 | type: "IdentifierExpression", 404 | name: "number", 405 | repeater: "array", 406 | }, 407 | }, 408 | { 409 | type: "Property", 410 | optional: false, 411 | key: { 412 | type: "IdentifierExpression", 413 | name: "arrayOfObjects", 414 | }, 415 | value: { 416 | type: "ObjectExpression", 417 | required: ["something"], 418 | properties: [ 419 | { 420 | type: "Property", 421 | optional: false, 422 | key: { 423 | type: "IdentifierExpression", 424 | name: "something", 425 | }, 426 | value: { 427 | type: "IdentifierExpression", 428 | name: "string", 429 | }, 430 | }, 431 | ], 432 | repeater: "array", 433 | }, 434 | }, 435 | ], 436 | }, 437 | }, 438 | ], 439 | }, 440 | ], 441 | }; 442 | 443 | const example = ` 444 | Schemas { 445 | User { 446 | favColors: string[], 447 | arrayOfNumbers: number[], 448 | arrayOfObjects: { 449 | something: string, 450 | }[] 451 | } 452 | } 453 | `; 454 | 455 | expect(() => parser.parse(example)).not.toThrowError(); 456 | expect(parser.parse(example)).toEqual(expected); 457 | }); 458 | }); 459 | 460 | describe("test for property access expression", () => { 461 | it.each([ 462 | ` 463 | Schemas { 464 | User { 465 | favColors: .Schema.user 466 | } 467 | } 468 | `, 469 | ` 470 | Schemas { 471 | User { 472 | favColors: Schema.user. 473 | } 474 | } 475 | `, 476 | // array of object 477 | ` 478 | Schemas { 479 | User { 480 | favColors: Schema..user 481 | } 482 | } 483 | `, 484 | ])( 485 | "given incorrect property access expression, it should return syntaxError", 486 | (example) => { 487 | function parseExample() { 488 | parser.parse(example); 489 | } 490 | expect(parseExample).toThrowError(); 491 | } 492 | ); 493 | 494 | it("given correct property access expression, should return correct output", () => { 495 | const expected = { 496 | type: "Program", 497 | body: [ 498 | { 499 | type: "SchemasBlockExpression", 500 | body: [ 501 | { 502 | type: "SchemaExpression", 503 | name: "BaseUser", 504 | body: { 505 | type: "ObjectExpression", 506 | required: ["name", "favColors"], 507 | properties: [ 508 | { 509 | type: "Property", 510 | optional: false, 511 | key: { 512 | type: "IdentifierExpression", 513 | name: "name", 514 | }, 515 | value: { 516 | type: "IdentifierExpression", 517 | name: "string", 518 | repeater: "array", 519 | }, 520 | }, 521 | { 522 | type: "Property", 523 | optional: false, 524 | key: { 525 | type: "IdentifierExpression", 526 | name: "favColors", 527 | }, 528 | value: { 529 | type: "PropertyAccessExpression", 530 | list: ["Schemas", "BaseUser"], 531 | repeater: "array", 532 | }, 533 | }, 534 | ], 535 | }, 536 | }, 537 | ], 538 | }, 539 | ], 540 | }; 541 | 542 | const example = ` 543 | Schemas { 544 | BaseUser { 545 | name: string[], 546 | favColors: Schemas.BaseUser[] 547 | } 548 | } 549 | `; 550 | 551 | expect(() => parser.parse(example)).not.toThrowError(); 552 | expect(parser.parse(example)).toEqual(expected); 553 | }); 554 | 555 | it("given correct property access expression, should return correct output", () => { 556 | const expected = { 557 | type: "Program", 558 | body: [ 559 | { 560 | type: "SchemasBlockExpression", 561 | body: [ 562 | { 563 | type: "SchemaExpression", 564 | name: "BaseUser", 565 | body: { 566 | allowAdditional: true, 567 | type: "ObjectExpression", 568 | properties: [ 569 | { 570 | type: "Property", 571 | allowAdditional: true, 572 | key: { 573 | name: "name", 574 | type: "IdentifierExpression", 575 | }, 576 | value: { 577 | name: "string", 578 | type: "IdentifierExpression", 579 | }, 580 | }, 581 | ], 582 | }, 583 | }, 584 | ], 585 | }, 586 | ], 587 | }; 588 | 589 | const example = ` 590 | Schemas { 591 | BaseUser { 592 | [name: string]: string 593 | } 594 | } 595 | `; 596 | 597 | expect(() => parser.parse(example)).not.toThrowError(); 598 | expect(parser.parse(example)).toEqual(expected); 599 | }); 600 | }); 601 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: ["/node_modules/", "/example/"], 3 | testTimeout: 10000, 4 | testRegex: ".*.test.js$", 5 | moduleFileExtensions: ["js", "jsx", "json", "node"], 6 | }; 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["core.pegjs", "init.js"], 3 | "ext": "js, pegjs" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tssg/syntax-parser", 3 | "version": "1.0.0", 4 | "main": "dist/transformer", 5 | "files": [ 6 | "dist" 7 | ], 8 | "author": "Tauqeer Nasir ", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "mkdirp dist && pegjs -o src/parser/parser.js src/parser/parser-auto-gen.pegjs", 12 | "merge": "node src/parser/merger.js", 13 | "dev": "nodemon --exec 'yarn merge && yarn build'", 14 | "test": "jest", 15 | "build:prod": "mkdirp dist/parser && mkdirp dist/transformer && mkdirp dist/addons/mongoose && yarn merge && yarn build && cp src/transformer/index.js dist/transformer/index.js && cp src/parser/parser.js dist/parser/parser.js && cp src/addons/mongoose/mongoose-transformer.js dist/addons/mongoose/mongoose-transformer.js", 16 | "prettier": "prettier --tab-width 2 --write \"src/**/*.{js,json}\" \"__tests__/**/*.{js,json}\"" 17 | }, 18 | "devDependencies": { 19 | "husky": "^4.2.5", 20 | "jest": "^26.0.1", 21 | "lint-staged": "^10.2.7", 22 | "mkdirp": "^1.0.4", 23 | "nodemon": "^2.0.4", 24 | "pegjs": "^0.10.0" 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "pre-commit": "lint-staged" 29 | } 30 | }, 31 | "lint-staged": { 32 | "src/**/*.{js,json}": "prettier --write --tab-width 2", 33 | "__tests__/**/*.{js}": "prettier --write --tab-width 2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/addons/mongoose/mongoose-transformer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate Mongoose Schemas from given OAS3 3 | */ 4 | 5 | const mongooseImportTmpl = ` 6 | import { Schema, model } from 'mongoose'; 7 | 8 | `; 9 | 10 | const mongooseSchemaTmpl = ` 11 | /** 12 | * @schema {{schema_key}} 13 | * 14 | */ 15 | const {{schema_name}}Schema = new Schema({ 16 | {{schema_props}} 17 | }, { 18 | strict: false, 19 | versionKey: false, 20 | timestamps: true, 21 | collection: '{{schema_name_underscored}}', 22 | minimize: false, 23 | }); 24 | 25 | export const {{schema_name}}Model = model('{{schema_key}}', {{schema_name}}Schema); 26 | 27 | // =================== 28 | `; 29 | 30 | const mongoosePropTmpl = ` 31 | {{prop_name}}: {{props}}, 32 | `; 33 | 34 | const mongooseKeyValTmpl = ` 35 | {{key}}: {{value}}, 36 | `; 37 | 38 | const replaceInTemplate = function (template, data) { 39 | const pattern = /{{\s*(\w+?)\s*}}/g; // {property} 40 | return template.replace(pattern, (_, token) => data[token] || ""); 41 | }; 42 | 43 | const lowerCaseFirst = (str) => { 44 | return str 45 | .split("") 46 | .map((char, index) => { 47 | if (index === 0) { 48 | return char.toLowerCase(); 49 | } 50 | return char; 51 | }) 52 | .join(""); 53 | }; 54 | 55 | const lowerAndUnderscore = function (str) { 56 | return lowerCaseFirst(str) 57 | .replaceAll(/([A-Z])/g, " $1") 58 | .split(" ") 59 | .join("_") 60 | .toLowerCase(); 61 | }; 62 | 63 | // Entry point 64 | function mongooseTransformer(spec) { 65 | const { schemas } = spec; 66 | 67 | const schemaKeys = Object.keys(schemas); 68 | 69 | let generatedMongooseCode = mongooseImportTmpl; 70 | for (const key of schemaKeys) { 71 | generatedMongooseCode += replaceInTemplate(mongooseSchemaTmpl, { 72 | schema_name: lowerCaseFirst(key), 73 | schema_key: key, 74 | schema_name_underscored: lowerAndUnderscore(key), 75 | schema_props: processSchema(schemas[key]), 76 | }); 77 | } 78 | 79 | return generatedMongooseCode; 80 | } 81 | 82 | // only proecess top-level schema 83 | function processSchema(schema) { 84 | const propKeys = Object.keys(schema.properties); 85 | 86 | let cachedProps = ""; 87 | for (const propKey of propKeys) { 88 | const { type } = schema.properties[propKey]; 89 | cachedProps += replaceInTemplate(mongoosePropTmpl, { 90 | prop_name: propKey, 91 | props: processProp(type, schema.properties[propKey]), 92 | }); 93 | } 94 | 95 | return cachedProps; 96 | } 97 | 98 | // process only props `key: value` pair 99 | // also handle `values` as sub-schemas to generate nested schema output 100 | function processProp(type, schema) { 101 | if (type === "object") { 102 | return `{ 103 | ${processSchema(schema)} 104 | } 105 | `; 106 | } 107 | 108 | if (type === "array") { 109 | return `[ 110 | ${processProp(schema.items.type, schema.items)} 111 | ]`; 112 | } 113 | 114 | return `{ 115 | ${replaceInTemplate(mongooseKeyValTmpl, { 116 | key: "type", 117 | value: getType(type), 118 | })} 119 | }`; 120 | } 121 | 122 | // get primitive types 123 | function getType(type) { 124 | switch (type) { 125 | case "string": 126 | return "String"; 127 | case "integer": 128 | case "number": 129 | return "Number"; 130 | case "boolean": 131 | return "Boolean"; 132 | } 133 | } 134 | 135 | module.exports = mongooseTransformer; 136 | -------------------------------------------------------------------------------- /src/parser/core.pegjs: -------------------------------------------------------------------------------- 1 | Start 2 | = _ exps:ExpressionList _ { 3 | return exps 4 | } 5 | 6 | ExpressionList 7 | = head:Expression tail:(_ Expression _)* { 8 | return new ProgramNode(buildList(head, tail, 1)) 9 | } 10 | 11 | Expression 12 | = SchemasBlockExpression / RequestBodiesBlockExpression / ParametersBlockExpression / PathsBlockExpression 13 | 14 | // ------- Schemas Block Expression --------- 15 | 16 | SchemasBlockExpression 17 | = _ "Schemas" _ "{" _ objs:(SchemaExpression / ExtendableSchemaExpression)* _"}" _ { 18 | return { 19 | type: "SchemasBlockExpression", 20 | body: objs 21 | } 22 | } 23 | 24 | 25 | SchemaExpression 26 | = name:$Identifier obj:ObjectExpression { 27 | _schemas[name] = obj; 28 | ProgramNode.schemas = _schemas; 29 | 30 | return { 31 | type: "SchemaExpression", 32 | name, 33 | body: obj 34 | } 35 | } 36 | 37 | ExtendableSchemaExpression 38 | = name:$Identifier " " "extends" _ extName:ExtendableSchemaList obj:ObjectExpression { 39 | _schemas[name] = obj; 40 | ProgramNode.prototype.schemas = _schemas; 41 | 42 | return { 43 | type: "SchemaExpression", 44 | extend: extName, 45 | name, 46 | body: obj 47 | } 48 | } 49 | 50 | ExtendableSchemaList 51 | = head:$Identifier tail:(_ "," _ $Identifier)* _ ","? { 52 | return buildList(head, tail, 3); 53 | } 54 | 55 | // ------- Request Bodies Block Expression --------- 56 | 57 | RequestBodiesBlockExpression 58 | = _ "RequestBodies" _ "{" _ objs:(RequestBodyExpression / ExtendableRequestBodyExpression)* _ "}" _ { 59 | 60 | return { 61 | type: "RequestBodiesBlockExpression", 62 | body: objs 63 | } 64 | } 65 | 66 | RequestBodyExpression 67 | = name:$Identifier obj:ObjectExpression { 68 | _requestBodies[name] = obj; 69 | ProgramNode.prototype.requestBodies = _requestBodies; 70 | 71 | return { 72 | type: "RequestBodyExpression", 73 | name, 74 | body: obj 75 | } 76 | } 77 | 78 | ExtendableRequestBodyExpression 79 | = name:$Identifier " " "extends" _ extName:$Identifier obj:ObjectExpression { 80 | _schemas[name] = obj; 81 | ProgramNode.prototype.schemas = _schemas; 82 | 83 | return { 84 | type: "RequestBodyExpression", 85 | extend: extName, 86 | name, 87 | body: obj 88 | } 89 | } 90 | 91 | // ------- Parameters Block Expression --------- 92 | 93 | ParametersBlockExpression 94 | = _ "Parameters" _ "{" _ objs:(ParameterExpression)* _ "}" _ { 95 | return { 96 | type: "ParametersBlockExpression", 97 | body: objs 98 | } 99 | } 100 | 101 | ParameterExpression 102 | = name:$Identifier obj:ObjectExpression { 103 | _parameters[name] = obj; 104 | ProgramNode.prototype.parameters = _parameters; 105 | 106 | return { 107 | type: "ParameterExpression", 108 | name, 109 | body: obj 110 | } 111 | } 112 | 113 | // --------- Paths Block Expression ------------ 114 | 115 | PathsBlockExpression 116 | = _ "Paths" _ "{" _ exps:PathExpressionList _ "}" _ { 117 | return { 118 | type: "PathsBlockExpression", 119 | body: exps 120 | } 121 | } 122 | 123 | PathExpressionList 124 | = head:PathExpression tail:(_ PathExpression)* { 125 | return buildList(head, tail, 1); 126 | } 127 | 128 | PathExpression 129 | = _ endpoint:EndpointName " " _ tag:TagName " " _ "{" _ methods:MethodExpressionList _ "}" _ { 130 | 131 | const method = { 132 | type: "PathExpression", 133 | endpoint, 134 | tag, 135 | methods 136 | } 137 | 138 | _paths[endpoint] = method; 139 | ProgramNode.prototype.paths = _paths; 140 | 141 | return method; 142 | } 143 | 144 | MethodExpressionList 145 | = head:MethodExpression tail:(_ MethodExpression)* { 146 | return buildList(head, tail, 1); 147 | } 148 | 149 | MethodExpression 150 | = _ name:MethodName body:MethodBody _ { 151 | return { 152 | type: "MethodExpression", 153 | name, 154 | body 155 | } 156 | } 157 | 158 | MethodBody 159 | = _ "{" _ properties:MethodBodyMemberExpressionList? _ "}" _ { 160 | return { 161 | type: "MethodBodyObjectExpression", 162 | properties: optionalList(properties), 163 | } 164 | } 165 | 166 | MethodBodyMemberExpressionList 167 | = head:MethodBodyMemberExpression tail:(_ "," MethodBodyMemberExpression)* { 168 | return buildList(head, tail, 2); 169 | } 170 | 171 | MethodBodyMemberExpression 172 | = _ key:"description" _ ":" _ value:Literal _ { 173 | return { 174 | type : "Property", 175 | key: { 176 | type: "IdentifierExpression", 177 | name: key 178 | }, 179 | value 180 | } 181 | } 182 | / 183 | _ "requestBody" _ ":" _ value:ObjectExpression _ { 184 | return { 185 | type: "MethodRequestBodyExpression", 186 | value 187 | } 188 | } 189 | / 190 | _ "responses" _ ":" _ value:ResponseObjectExpression { 191 | return { 192 | type: "MethodResponseExpression", 193 | value 194 | } 195 | } 196 | 197 | ResponseObjectExpression 198 | = _ "{" _ properties:ResponseObjectMemberExpressionList? _ "}" _ { 199 | return { 200 | type: "ResponseObjectExpression", 201 | properties: optionalList(properties) 202 | } 203 | } 204 | 205 | ResponseObjectMemberExpressionList 206 | = head:ResponseObjectMemberExpression tail:(_ "," ResponseObjectMemberExpression)* { 207 | return buildList(head, tail, 2); 208 | } 209 | 210 | ResponseObjectMemberExpression 211 | = _ name:$[0-9]+ _ ":" obj:ObjectExpression { 212 | return { 213 | type: "ResponseObjectMemberExpression", 214 | key: { 215 | type: "Literal", 216 | name, 217 | }, 218 | value: obj 219 | } 220 | } 221 | 222 | MethodName 223 | = "post" / "get" / "put" / "patch" / "delete" 224 | 225 | EndpointName 226 | = endpoint: $[-_a-z0-9?\/]i+ { 227 | return endpoint; 228 | } 229 | 230 | TagName 231 | = "(" tag:$[a-z]i+ ")" { 232 | return tag; 233 | } 234 | 235 | //////////////////////////////////////////// 236 | // -------- General Expressions ---------- 237 | //////////////////////////////////////////// 238 | 239 | // -------- Object Expression ---------- 240 | 241 | ObjectExpression 242 | = _ "{" _ props:MemberExpressionList? _ "}" _ { 243 | const requiredProps = props !== null && Array.isArray(props) ? props.filter((prop) => !prop.optional && !prop.allowAdditional).map((prop) => prop.key.name) : []; 244 | const allowAdditional = props !== null && Array.isArray(props) ? props.some((prop) => prop.allowAdditional) : false; 245 | return { 246 | type: "ObjectExpression", 247 | ...(requiredProps.length ? { required: requiredProps } : {}), 248 | ...(allowAdditional ? { allowAdditional } : {}), 249 | properties: optionalList(props) 250 | } 251 | } 252 | 253 | MemberExpressionList 254 | = head:KeyValueExpression tail:(_ "," _ KeyValueExpression)* _ ","? { 255 | return buildList(head, tail, 3) 256 | } 257 | 258 | KeyValueExpression 259 | = key:Identifier _ optional:"?"? _ ":" _ value:(ArrayExpression / RepeatExpression / ObjectExpression / CallExpression / PropertyAccessExpression / Identifier / Literal / Number) { 260 | return { 261 | type: "Property", 262 | optional: optional ? true : false, 263 | key, 264 | value 265 | } 266 | } 267 | / 268 | key:("[" _ Identifier _ ":" _ Identifier _ "]") _ ":" _ value:(ArrayExpression / RepeatExpression / ObjectExpression / CallExpression / PropertyAccessExpression / Identifier / Literal / Number) { 269 | return { 270 | type: "Property", 271 | allowAdditional: true, 272 | key: key[2], 273 | value 274 | } 275 | } 276 | 277 | PropertyAccessExpression 278 | = _ obj:$Identifier _ keys:(_ "." _ $Identifier)+ _ ![.%^&*(@!#)] { 279 | return { 280 | type: "PropertyAccessExpression", 281 | list: buildList(obj, keys, 3) 282 | } 283 | } 284 | 285 | // -------- Repeat Expression ----------- 286 | 287 | RepeatExpression 288 | = initialBlock:(ObjectExpression / PropertyAccessExpression / Identifier) "[]" _ { 289 | return { 290 | ...initialBlock, 291 | repeater: "array" 292 | } 293 | } 294 | 295 | // -------- Array Expression --------- 296 | 297 | ArrayExpression 298 | = _ "[" _ args:ArrayElementList? _ "]" _ { 299 | return { 300 | type: "ArrayExpression", 301 | elements: optionalList(args) 302 | } 303 | } 304 | 305 | ArrayElementList 306 | = head:ArgumentType tail:(_ "," _ ArgumentType)* _ ","? { 307 | return buildList(head, tail, 3); 308 | } 309 | 310 | ArgumentType 311 | = ArrayExpression / ObjectExpression / CallExpression / Identifier / Literal 312 | 313 | // ------- Call Expression ---------- 314 | 315 | CallExpression 316 | = _ callee:Identifier _ "(" _ args:CallArgumentList? _ ")" _ { 317 | return { 318 | type: "CallExpression", 319 | callee, 320 | arguments: optionalList(args) 321 | } 322 | } 323 | 324 | CallArgumentList 325 | = head:ArgumentType tail:(_ "," _ ArgumentType)* _ ","? { 326 | return buildList(head, tail, 3); 327 | } 328 | 329 | // -------- Comment Expression ---------- 330 | 331 | MultilineCommentExpression 332 | = "/*" comment:$(!"*/" SourceChar)* "*/" { 333 | _comments.push({ type: "MultilineCommentExpression", value: comment.trim(), location: location() }); 334 | ProgramNode.prototype.comments = _comments; 335 | } 336 | 337 | // -------- Identifier Expression ---------- 338 | 339 | Identifier 340 | = name:$([_a-zA-Z][_a-zA-Z0-9]*) { 341 | return { 342 | type: "IdentifierExpression", 343 | name, 344 | } 345 | } 346 | 347 | // -------- Literal Expression ---------- 348 | 349 | Number 350 | = value:$[0-9]+ { 351 | return { 352 | type: "Number", 353 | value: Number(value) 354 | } 355 | } 356 | 357 | Literal 358 | = value:StringLiteral { 359 | return { 360 | type: "Literal", 361 | value 362 | } 363 | } 364 | 365 | StringLiteral 366 | = '"' chars:$(DoubleStringChar*) '"' { 367 | return chars; 368 | } 369 | / 370 | "'" chars:$(SingleStringChar*) "'" { 371 | return chars; 372 | } 373 | 374 | DoubleStringChar 375 | = !('"' / "\\" / LineTerminator) SourceChar { 376 | return text(); 377 | } 378 | 379 | SingleStringChar 380 | = !("'" / "\\" / LineTerminator) SourceChar { 381 | return text(); 382 | } 383 | 384 | LineTerminator 385 | = [\n\r\u2028\u2029] 386 | 387 | SourceChar 388 | = . 389 | 390 | _ "whitespace" 391 | = ( 392 | MultilineCommentExpression 393 | / "\t" 394 | / "\v" 395 | / "\f" 396 | / " " 397 | / "\n" 398 | / "\u00A0" 399 | / "\uFEFF")* 400 | -------------------------------------------------------------------------------- /src/parser/init.js: -------------------------------------------------------------------------------- 1 | // cache all schemas 2 | const _schemas = {}; 3 | 4 | // cache all request bodies 5 | const _requestBodies = {}; 6 | 7 | // cache all comments 8 | const _comments = []; 9 | 10 | // cache all parameters 11 | const _parameters = {}; 12 | 13 | // cache all paths 14 | const _paths = {}; 15 | 16 | function extractList(list, index) { 17 | return list.map((item) => item[index]); 18 | } 19 | 20 | function buildList(head, tail, index) { 21 | return [head].concat(extractList(tail, index)); 22 | } 23 | 24 | function extractOptional(optional, index) { 25 | return optional ? optional[index] : null; 26 | } 27 | 28 | function optionalList(value) { 29 | return value !== null ? value : []; 30 | } 31 | 32 | function ProgramNode(body) { 33 | this.type = "Program"; 34 | this.body = body; 35 | } 36 | -------------------------------------------------------------------------------- /src/parser/merger.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const initContent = fs.readFileSync(path.resolve(__dirname, "init.js"), { 5 | encoding: "utf-8", 6 | }); 7 | const coreContent = fs.readFileSync(path.resolve(__dirname, "core.pegjs"), { 8 | encoding: "utf-8", 9 | }); 10 | 11 | const combinedContent = ` 12 | //////////////////////////////////////// 13 | // DO NOT UPDATE THIS FILE MANUALLY 14 | // THIS FILE WAS AUTOGENERATED BY MERGER 15 | //////////////////////////////////////// 16 | 17 | 18 | { 19 | ${initContent} 20 | } 21 | 22 | ${coreContent} 23 | `; 24 | 25 | fs.writeFileSync( 26 | path.resolve(__dirname, "parser-auto-gen.pegjs"), 27 | combinedContent 28 | ); 29 | -------------------------------------------------------------------------------- /src/transformer/index.js: -------------------------------------------------------------------------------- 1 | const Parser = require("../parser/parser"); 2 | 3 | const OPEN_API_SPEC = {}; 4 | 5 | function ssgToOASParser(str) { 6 | const parsedScript = Parser.parse(str); 7 | 8 | let OAS = {}; 9 | 10 | for (const block of parsedScript.body) { 11 | switch (block.type) { 12 | case "SchemasBlockExpression": 13 | OPEN_API_SPEC.schemas = schemaBlockProcessor(block); 14 | break; 15 | case "RequestBodiesBlockExpression": 16 | OPEN_API_SPEC.requestBodies = schemaBlockProcessor(block); 17 | break; 18 | case "ParametersBlockExpression": 19 | OPEN_API_SPEC.parameters = schemaBlockProcessor(block); 20 | break; 21 | } 22 | } 23 | 24 | OAS = { ...OPEN_API_SPEC }; 25 | 26 | return OAS; 27 | } 28 | 29 | function schemaBlockProcessor(block) { 30 | // if (block.type !== "SchemasBlockExpression") { 31 | // throw new Error( 32 | // `schemaBlockProcessor: cannot process other type ${block.type}` 33 | // ); 34 | // } 35 | 36 | const schemaExps = block.body; 37 | return schemaExps 38 | .map((exp) => schemaExpressionProcessor(exp)) 39 | .reduce((allSchemas, schema) => { 40 | const name = Object.keys(schema)[0]; 41 | const value = Object.values(schema)[0]; 42 | 43 | allSchemas[name] = value; 44 | return allSchemas; 45 | }, {}); 46 | } 47 | 48 | function schemaExpressionProcessor(exp) { 49 | // if (exp.type !== "SchemaExpression") { 50 | // throw new Error( 51 | // `schemaExpressionProcessor: cannot process other type ${exp.type}` 52 | // ); 53 | // } 54 | 55 | if (!exp.extend?.length) { 56 | return { 57 | [exp.name]: reduce(exp.body), 58 | }; 59 | } 60 | 61 | return { 62 | [exp.name]: { 63 | allOf: [ 64 | ...(exp.extend?.map((ext) => { 65 | return { 66 | $ref: `#/components/schemas/${ext}`, 67 | }; 68 | }) || []), 69 | reduce(exp.body), 70 | ], 71 | }, 72 | }; 73 | } 74 | 75 | function objectExpressionProcessor(exp) { 76 | if (exp.type !== "ObjectExpression") { 77 | throw new Error( 78 | `objectExpressionProcessor: cannot process other type ${exp.type}` 79 | ); 80 | } 81 | 82 | const mappedProps = exp.properties 83 | .map((prop) => { 84 | return propertyExpressionProcessor(prop); 85 | }) 86 | .reduce((finalObj, prop) => { 87 | const propName = Object.keys(prop)[0]; 88 | const propValue = Object.values(prop)[0]; 89 | 90 | finalObj = { 91 | ...finalObj, 92 | [propName]: propValue, 93 | }; 94 | return finalObj; 95 | }, {}); 96 | 97 | return { 98 | type: "object", 99 | ...(exp.required?.length ? { required: exp.required } : {}), 100 | properties: mappedProps, 101 | ...(exp.allowAdditional ? { additionalProperties: true } : {}), 102 | }; 103 | } 104 | 105 | function identifierExpressionProcessor(exp) { 106 | if (exp.type !== "IdentifierExpression") { 107 | throw new Error( 108 | `IdentifierExpressionProcessor: cannot process other type ${exp.type}` 109 | ); 110 | } 111 | 112 | return { 113 | type: exp.value.name, 114 | }; 115 | } 116 | 117 | function propertyExpressionProcessor(exp) { 118 | if (exp.type !== "Property") { 119 | throw new Error( 120 | `propertyExpressionProcessor: cannot process other type ${exp.type}` 121 | ); 122 | } 123 | 124 | if (exp.allowAdditional) { 125 | return {}; 126 | } 127 | 128 | if (exp.value.repeater === "array" && exp.value.type !== "ObjectExpression") { 129 | return { 130 | [exp.key.name]: { 131 | type: "array", 132 | items: { 133 | type: exp.value.name, 134 | }, 135 | }, 136 | }; 137 | } else if ( 138 | exp.value.repeater === "array" && 139 | exp.value.type === "ObjectExpression" 140 | ) { 141 | return { 142 | [exp.key.name]: { 143 | type: "array", 144 | items: reduce(exp.value), 145 | }, 146 | }; 147 | } 148 | 149 | if (exp.value.type === "ObjectExpression") { 150 | return { 151 | [exp.key.name]: reduce(exp.value), 152 | }; 153 | } 154 | 155 | return { 156 | [exp.key.name]: { 157 | type: exp.value.name, 158 | }, 159 | }; 160 | } 161 | 162 | function reduce(exp) { 163 | switch (exp.type) { 164 | case "ObjectExpression": 165 | return objectExpressionProcessor(exp); 166 | case "IdentifierExpression": 167 | return identifierExpressionProcessor(exp); 168 | case "Property": 169 | return propertyExpressionProcessor(exp); 170 | } 171 | } 172 | 173 | module.exports = { 174 | parser: ssgToOASParser, 175 | }; 176 | --------------------------------------------------------------------------------