├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------