@sleeksky/alt-swagger 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,515 @@
1
- # Spec API
1
+ # Alt Swagger
2
2
 
3
- Quickly spec your APIs for Swagger / OpenAPI interface. Uses @sleeksky/alt-schema for specifying JSON schema.
3
+ A fluent, programmatic API for generating OpenAPI 3.0 specifications. Quickly define your REST API endpoints with request/response schemas, parameters, security, and more using a simple, chainable syntax.
4
4
 
5
- # Example
6
- ```JavaScript
7
- let refRequest = docs.ref.schema('example','{id:i:1,name:s:foo,label:{id:i:2,name:?s:bar},arr:[{a:b:false}]}');
5
+ Built on top of [@sleeksky/alt-schema](https://github.com/sleeksky-dev/alt-schema) for intuitive JSON schema definitions.
8
6
 
9
- docs.put('/:id').tag("dot-notation").req(refRequest).res(200, '{hello,world}');
7
+ ## Features
8
+
9
+ - 🚀 **Fluent API**: Chain methods to define endpoints declaratively
10
+ - 📝 **Schema-first**: Define request/response schemas using simple string syntax
11
+ - 🔒 **Security**: Built-in support for authentication schemes
12
+ - 🏷️ **Rich Metadata**: Tags, summaries, descriptions, deprecation
13
+ - 🔄 **Reusable Schemas**: Define and reference schemas across endpoints
14
+ - 🎯 **TypeScript Support**: Full TypeScript definitions included
15
+ - 📊 **OpenAPI 3.0**: Generates valid OpenAPI 3.0 specifications
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @sleeksky/alt-swagger
21
+ # or
22
+ yarn add @sleeksky/alt-swagger
23
+ # or
24
+ pnpm add @sleeksky/alt-swagger
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```javascript
30
+ const swagger = require('@sleeksky/alt-swagger');
31
+
32
+ // Define your API
33
+ swagger.server('https://api.example.com', 'Production API');
34
+
35
+ // Define endpoints
36
+ swagger.get('/users')
37
+ .summary('Get users')
38
+ .query('limit:i:10,offset:i:0')
39
+ .res(200, '[{id:i,name:s,email:s}]');
40
+
41
+ swagger.post('/users')
42
+ .summary('Create user')
43
+ .req('{name:s,email:s}')
44
+ .res(201, '{id:i,name:s,email:s}')
45
+ .res(400, '{error:s}');
46
+
47
+ // Generate OpenAPI spec
48
+ const spec = swagger.swaggerDoc('User API', '1.0.0');
49
+ console.log(JSON.stringify(spec, null, 2));
50
+ ```
51
+
52
+ ## Schema Syntax
53
+
54
+ Alt Swagger uses a compact string syntax for defining JSON schemas, powered by alt-schema:
55
+
56
+ ### Basic Types
57
+ - `s` - string
58
+ - `i` - integer
59
+ - `n` - number
60
+ - `b` - boolean
61
+ - `o` - object
62
+ - `a` - array
63
+
64
+ ### Examples
65
+
66
+ ```javascript
67
+ // Simple object
68
+ '{name:s, age:i}'
69
+
70
+ // With default values
71
+ '{name:s:John, age:i:25}'
72
+
73
+ // Optional fields (prefixed with ?)
74
+ '{name:s, age:?i, email:?s}'
75
+
76
+ // Nested objects
77
+ '{user:{name:s, age:i}, active:b}'
78
+
79
+ // Arrays
80
+ '[{id:i, name:s}]'
81
+
82
+ // Complex nested structure
83
+ '{id:i, profile:{name:s, settings:{theme:s:dark, notifications:b:true}}}'
84
+ ```
85
+
86
+ ## API Reference
87
+
88
+ ### Core Methods
89
+
90
+ #### `swagger.server(url, description?)`
91
+ Define API servers.
92
+
93
+ ```javascript
94
+ swagger.server('https://api.example.com', 'Production');
95
+ swagger.server('https://staging.api.example.com', 'Staging');
96
+ ```
97
+
98
+ #### `swagger.tag(name, description?)`
99
+ Define tags for grouping endpoints.
100
+
101
+ ```javascript
102
+ swagger.tag('Users', 'User management endpoints');
103
+ swagger.tag('Posts', 'Blog post endpoints');
104
+ ```
105
+
106
+ ### HTTP Methods
107
+
108
+ All HTTP methods return a fluent API for chaining:
109
+
110
+ #### `swagger.get(path)`
111
+ #### `swagger.post(path)`
112
+ #### `swagger.put(path)`
113
+ #### `swagger.patch(path)`
114
+ #### `swagger.del(path)` (or `swagger.delete(path)`)
115
+
116
+ ```javascript
117
+ swagger.get('/users/{id}')
118
+ .summary('Get user by ID')
119
+ .res(200, '{id:i, name:s, email:s}')
120
+ .res(404, '{error:s}');
121
+ ```
122
+
123
+ ### Path Parameters
124
+
125
+ Parameters in curly braces are automatically detected:
126
+
127
+ ```javascript
128
+ // /users/{id} automatically creates path parameter 'id'
129
+ swagger.get('/users/{id}')
130
+ .res(200, '{id:i, name:s}');
131
+
132
+ // With default values
133
+ swagger.get('/users/{id:123}')
134
+ .res(200, '{id:i, name:s}');
135
+ ```
136
+
137
+ ### Fluent API Methods
138
+
139
+ #### `.req(schema)`
140
+ Define request body schema.
141
+
142
+ ```javascript
143
+ swagger.post('/users')
144
+ .req('{name:s, email:s, age:?i}')
145
+ .res(201, '{id:i, name:s, email:s}');
146
+ ```
147
+
148
+ #### `.res(statusCode, schema)`
149
+ Define response schema for a status code.
150
+
151
+ ```javascript
152
+ swagger.get('/users')
153
+ .res(200, '[{id:i, name:s}]')
154
+ .res(404, '{error:s}')
155
+ .res(500, '{error:s, code:i}');
156
+ ```
157
+
158
+ #### `.query(params)`
159
+ Add query parameters. Accepts string or array.
160
+
161
+ ```javascript
162
+ // Single parameter
163
+ swagger.get('/users').query('limit:i:10');
164
+
165
+ // Multiple parameters (comma-separated)
166
+ swagger.get('/users').query('limit:i:10,offset:i:0');
167
+
168
+ // Array format
169
+ swagger.get('/users').query(['limit:i:10', 'offset:i:0']);
170
+
171
+ // Optional parameters
172
+ swagger.get('/users').query('search:?s');
173
+ ```
174
+
175
+ #### `.header(params)`
176
+ Add header parameters.
177
+
178
+ ```javascript
179
+ swagger.get('/users')
180
+ .header('authorization')
181
+ .header('x-api-key:?');
182
+ ```
183
+
184
+ #### `.tag(name)`
185
+ Add tags to the endpoint.
186
+
187
+ ```javascript
188
+ swagger.get('/users')
189
+ .tag('Users')
190
+ .tag('Public');
191
+ ```
192
+
193
+ #### `.summary(text)`
194
+ Add summary to the endpoint.
195
+
196
+ ```javascript
197
+ swagger.get('/users')
198
+ .summary('Retrieve a list of users');
199
+ ```
200
+
201
+ #### `.desc(text)` or `.description(text)`
202
+ Add description to the endpoint.
203
+
204
+ ```javascript
205
+ swagger.get('/users')
206
+ .desc('Returns a paginated list of users with optional filtering');
207
+ ```
208
+
209
+ #### `.security(name)`
210
+ Apply security scheme to the endpoint.
211
+
212
+ ```javascript
213
+ swagger.get('/users')
214
+ .security('bearerAuth');
215
+ ```
216
+
217
+ #### `.deprecate()`
218
+ Mark endpoint as deprecated.
219
+
220
+ ```javascript
221
+ swagger.get('/old-endpoint')
222
+ .deprecate()
223
+ .res(200, '{message:s}');
224
+ ```
225
+
226
+ ### Schema Management
227
+
228
+ #### `swagger.ref.schema(name, schema)`
229
+ Define reusable schemas.
230
+
231
+ ```javascript
232
+ const userSchema = swagger.ref.schema('User', '{id:i, name:s, email:s}');
233
+ const errorSchema = swagger.ref.schema('Error', '{error:s, code:i}');
234
+
235
+ swagger.get('/users')
236
+ .res(200, userSchema)
237
+ .res(500, errorSchema);
238
+ ```
239
+
240
+ ### Security
241
+
242
+ #### `swagger.security(name, options?)`
243
+ Define security schemes.
244
+
245
+ ```javascript
246
+ // Bearer token
247
+ swagger.security('bearerAuth');
248
+
249
+ // API Key
250
+ swagger.security('apiKey', {
251
+ type: 'apiKey',
252
+ in: 'header',
253
+ name: 'X-API-Key'
254
+ });
255
+
256
+ // Basic auth
257
+ swagger.security('basicAuth', {
258
+ type: 'http',
259
+ scheme: 'basic'
260
+ });
261
+ ```
262
+
263
+ ### Document Generation
264
+
265
+ #### `swagger.swaggerDoc(title?, version?)`
266
+ Generate the complete OpenAPI specification.
267
+
268
+ ```javascript
269
+ const spec = swagger.swaggerDoc('My API', '1.0.0');
270
+ // Returns OpenAPI 3.0 compliant object
271
+ ```
272
+
273
+ ### Utility Methods
274
+
275
+ #### `swagger.reset()`
276
+ Clear all defined specifications.
277
+
278
+ ```javascript
279
+ swagger.reset(); // Start fresh
280
+ ```
281
+
282
+ #### `swagger.remove(options)`
283
+ Remove endpoints.
284
+
285
+ ```javascript
286
+ // Remove specific path
287
+ swagger.remove({ path: '/users' });
288
+
289
+ // Remove by tag
290
+ swagger.remove({ tag: 'Deprecated' });
291
+ ```
292
+
293
+ ## Complete Examples
294
+
295
+ ### Basic CRUD API
296
+
297
+ ```javascript
298
+ const swagger = require('@sleeksky/alt-swagger');
299
+
300
+ swagger.server('https://api.example.com', 'User Management API');
301
+ swagger.tag('Users', 'User operations');
302
+
303
+ // Define schemas
304
+ const userSchema = swagger.ref.schema('User', '{id:i, name:s, email:s, created_at:s}');
305
+ const createUserSchema = swagger.ref.schema('CreateUser', '{name:s, email:s}');
306
+ const errorSchema = swagger.ref.schema('Error', '{error:s, code:i}');
307
+
308
+ // Security
309
+ swagger.security('bearerAuth');
310
+
311
+ // Endpoints
312
+ swagger.get('/users')
313
+ .tag('Users')
314
+ .summary('List users')
315
+ .security('bearerAuth')
316
+ .query('limit:?i:10,offset:?i:0')
317
+ .res(200, `[${userSchema}]`)
318
+ .res(401, errorSchema);
319
+
320
+ swagger.post('/users')
321
+ .tag('Users')
322
+ .summary('Create user')
323
+ .req(createUserSchema)
324
+ .res(201, userSchema)
325
+ .res(400, errorSchema);
326
+
327
+ swagger.get('/users/{id}')
328
+ .tag('Users')
329
+ .summary('Get user by ID')
330
+ .security('bearerAuth')
331
+ .res(200, userSchema)
332
+ .res(404, errorSchema);
333
+
334
+ swagger.put('/users/{id}')
335
+ .tag('Users')
336
+ .summary('Update user')
337
+ .security('bearerAuth')
338
+ .req(createUserSchema)
339
+ .res(200, userSchema)
340
+ .res(400, errorSchema)
341
+ .res(404, errorSchema);
342
+
343
+ swagger.del('/users/{id}')
344
+ .tag('Users')
345
+ .summary('Delete user')
346
+ .security('bearerAuth')
347
+ .res(204)
348
+ .res(404, errorSchema);
349
+
350
+ const spec = swagger.swaggerDoc('User API', '1.0.0');
351
+ ```
352
+
353
+ ### Express Integration
354
+
355
+ ```javascript
356
+ const express = require('express');
357
+ const swaggerUi = require('swagger-ui-express');
358
+ const swagger = require('@sleeksky/alt-swagger');
359
+
360
+ const app = express();
361
+
362
+ // Define API spec
363
+ swagger.server('http://localhost:3000', 'Local API');
364
+
365
+ swagger.get('/health')
366
+ .summary('Health check')
367
+ .res(200, '{status:s, timestamp:s}');
368
+
369
+ swagger.get('/users/{id}')
370
+ .summary('Get user')
371
+ .res(200, '{id:i, name:s}');
372
+
373
+ // Serve Swagger UI
374
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swagger.swaggerDoc('My API')));
375
+
376
+ // API routes
377
+ app.get('/health', (req, res) => {
378
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
379
+ });
380
+
381
+ app.get('/users/:id', (req, res) => {
382
+ const user = { id: parseInt(req.params.id), name: 'John Doe' };
383
+ res.json(user);
384
+ });
385
+
386
+ app.listen(3000, () => {
387
+ console.log('Server running on http://localhost:3000');
388
+ console.log('API docs at http://localhost:3000/api-docs');
389
+ });
390
+ ```
391
+
392
+ ### Advanced Schema Examples
393
+
394
+ ```javascript
395
+ const swagger = require('@sleeksky/alt-swagger');
396
+
397
+ // Complex nested schemas
398
+ const profileSchema = swagger.ref.schema('Profile', `
399
+ {
400
+ personal: {
401
+ firstName: s,
402
+ lastName: s,
403
+ age: ?i,
404
+ email: s
405
+ },
406
+ preferences: {
407
+ theme: s:light,
408
+ notifications: {
409
+ email: b:true,
410
+ push: b:false
411
+ }
412
+ },
413
+ tags: [s]
414
+ }
415
+ `);
416
+
417
+ // Array responses
418
+ swagger.get('/posts')
419
+ .summary('Get blog posts')
420
+ .query('category:?s,tag:?s,published:?b:true')
421
+ .res(200, `
422
+ [{
423
+ id: i,
424
+ title: s,
425
+ content: s,
426
+ author: {id:i, name:s},
427
+ tags: [s],
428
+ published: b,
429
+ created_at: s
430
+ }]
431
+ `);
432
+
433
+ // Union-like responses (use oneOf in OpenAPI)
434
+ swagger.post('/upload')
435
+ .summary('Upload file')
436
+ .req('{file:s, type:s}') // In practice, use multipart/form-data
437
+ .res(200, '{id:s, url:s, size:i}')
438
+ .res(400, '{error:s, code:s}');
439
+ ```
440
+
441
+ ## TypeScript Support
442
+
443
+ Alt Swagger includes full TypeScript definitions:
444
+
445
+ ```typescript
446
+ import * as swagger from '@sleeksky/alt-swagger';
447
+
448
+ swagger.server('https://api.example.com');
449
+ swagger.get('/users').res(200, '[{id:number, name:string}]');
450
+
451
+ const spec: swagger.SwaggerDoc = swagger.swaggerDoc('API');
452
+ ```
453
+
454
+ ## Integration with Frameworks
455
+
456
+ ### Fastify
457
+
458
+ ```javascript
459
+ const fastify = require('fastify')();
460
+ const swagger = require('@sleeksky/alt-swagger');
461
+
462
+ // Define spec
463
+ swagger.server('http://localhost:3000');
464
+ swagger.get('/users').res(200, '[{id:i, name:s}]');
465
+
466
+ // Register Swagger
467
+ fastify.register(require('@fastify/swagger'), {
468
+ specification: {
469
+ document: swagger.swaggerDoc('Fastify API')
470
+ }
471
+ });
472
+ ```
473
+
474
+ ### Hapi
475
+
476
+ ```javascript
477
+ const Hapi = require('@hapi/hapi');
478
+ const swagger = require('@sleeksky/alt-swagger');
479
+
480
+ const server = Hapi.server({ port: 3000 });
481
+
482
+ // Define spec
483
+ swagger.server('http://localhost:3000');
484
+ swagger.get('/users').res(200, '[{id:i, name:s}]');
485
+
486
+ // Use hapi-swagger
487
+ server.register([
488
+ require('hapi-swagger'),
489
+ {
490
+ options: {
491
+ documentationPage: true,
492
+ swaggerOptions: {
493
+ spec: swagger.swaggerDoc('Hapi API')
494
+ }
495
+ }
496
+ }
497
+ ]);
10
498
  ```
11
- ![](https://i.ibb.co/ypWxGZJ/spec-api-ss1.png)
12
- # Installation
13
499
 
14
- npm install -s @sleeksky/alt-swagger
500
+ ## Best Practices
15
501
 
16
- const docs = require('@sleeksky/alt-swagger');
502
+ 1. **Define schemas first**: Use `swagger.ref.schema()` for reusable schemas
503
+ 2. **Use meaningful tags**: Group related endpoints with tags
504
+ 3. **Document thoroughly**: Always include summaries and descriptions
505
+ 4. **Handle errors**: Define error responses for all endpoints
506
+ 5. **Version your API**: Use semantic versioning in `swaggerDoc()`
507
+ 6. **Validate schemas**: Test your generated specs with OpenAPI validators
17
508
 
18
- # Usage
509
+ ## Contributing
19
510
 
20
- See example.js
511
+ Contributions are welcome! Please feel free to submit issues and pull requests.
21
512
 
22
- # License
513
+ ## License
23
514
 
24
- MIT © SleekSky
515
+ MIT © [SleekSky](https://sleeksky.com)
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@sleeksky/alt-swagger",
3
- "version": "2.4.0",
4
- "description": "Quickly spec your APIs for Swagger / OpenAPI interface",
5
- "main": "./src/index.js",
3
+ "version": "3.0.0",
4
+ "description": "A fluent, programmatic API for generating OpenAPI 3.0 specifications with TypeScript support. Define REST API endpoints, schemas, parameters, and security using a simple, chainable syntax.",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
6
7
  "scripts": {
7
- "test": "mocha",
8
+ "build": "tsc",
9
+ "test": "mocha --require ts-node/register test/index.ts",
8
10
  "cover": "nyc --check-coverage npm run test",
9
- "prepublish": "npm run test"
11
+ "prepublish": "npm run build && npm run test"
10
12
  },
11
13
  "files": [
12
14
  "src"
@@ -29,12 +31,19 @@
29
31
  },
30
32
  "homepage": "https://github.com/sleeksky-dev/alt-swagger#readme",
31
33
  "devDependencies": {
34
+ "@types/chai": "^5.2.3",
35
+ "@types/express": "^5.0.6",
36
+ "@types/lodash": "^4.17.21",
37
+ "@types/mocha": "^10.0.10",
38
+ "@types/node": "^25.0.3",
32
39
  "chai": "^4.1.2",
33
40
  "cross-env": "^5.1.3",
34
41
  "express": "^4.17.1",
35
42
  "mocha": "^6.1.3",
36
43
  "nyc": "^13.3.0",
37
- "swagger-ui-express": "^4.1.6"
44
+ "swagger-ui-express": "^4.1.6",
45
+ "ts-node": "^10.9.2",
46
+ "typescript": "^5.9.3"
38
47
  },
39
48
  "dependencies": {
40
49
  "@sleeksky/alt-schema": "^2.1.0",
package/src/index.ts ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ Author: Yusuf Bhabhrawala
3
+ */
4
+ import "./utils";
5
+ import * as _ from "lodash";
6
+ import { toSwaggerSchema, pathParameters, pathClean, toParameter, SwaggerSchema, PathParameter, Parameter } from "./utils";
7
+
8
+ interface Server {
9
+ url: string;
10
+ description?: string;
11
+ }
12
+
13
+ interface Tag {
14
+ name: string;
15
+ description?: string;
16
+ }
17
+
18
+ interface SecurityScheme {
19
+ type: string;
20
+ scheme?: string;
21
+ bearerFormat?: string | null;
22
+ in?: string;
23
+ required?: boolean;
24
+ }
25
+
26
+ interface Components {
27
+ securitySchemes?: { [key: string]: SecurityScheme };
28
+ schemas?: { [key: string]: SwaggerSchema };
29
+ [key: string]: any;
30
+ }
31
+
32
+ interface ResponseSpec {
33
+ content?: { [key: string]: { schema: SwaggerSchema } };
34
+ description: string;
35
+ }
36
+
37
+ interface PathSpec {
38
+ parameters: (PathParameter | Parameter)[];
39
+ responses: { [key: string]: ResponseSpec };
40
+ description: string;
41
+ tags?: string[];
42
+ summary?: string;
43
+ security?: { [key: string]: string[] }[];
44
+ deprecated?: boolean;
45
+ requestBody?: {
46
+ content: { [key: string]: { schema: SwaggerSchema } };
47
+ };
48
+ }
49
+
50
+ interface Paths {
51
+ [key: string]: {
52
+ [method: string]: PathSpec;
53
+ };
54
+ }
55
+
56
+ const paths: Paths = {};
57
+ const components: Components = {};
58
+ const tags: Tag[] = [];
59
+ const servers: Server[] = [];
60
+
61
+ function reset(): void {
62
+ Object.keys(paths).forEach((key) => delete paths[key]);
63
+ Object.keys(components).forEach((key) => delete components[key]);
64
+ tags.splice(0, tags.length);
65
+ servers.splice(0, servers.length);
66
+ }
67
+
68
+ function server(url: string, description?: string): void {
69
+ servers.push({ url, description });
70
+ }
71
+
72
+ interface ApiOptions {
73
+ path: string;
74
+ method: string;
75
+ tag?: string;
76
+ desc?: string;
77
+ summary?: string;
78
+ req?: string;
79
+ header?: string | string[];
80
+ query?: string | string[];
81
+ security?: string;
82
+ deprecated?: boolean;
83
+ [key: string]: any; // for numeric response codes
84
+ }
85
+
86
+ interface ApiExtension {
87
+ req: (flatSch: string) => ApiExtension;
88
+ res: (code: string | number, flatSch: string) => ApiExtension;
89
+ query: (strArr: string | string[]) => ApiExtension;
90
+ header: (strArr: string | string[]) => ApiExtension;
91
+ tag: (str: string) => ApiExtension;
92
+ summary: (str: string) => ApiExtension;
93
+ desc: (str: string) => ApiExtension;
94
+ security: (str: string) => ApiExtension;
95
+ deprecate: () => ApiExtension;
96
+ remove: () => void;
97
+ }
98
+
99
+ function api(opt: ApiOptions): ApiExtension {
100
+ let path = `${pathClean(opt.path)}.${opt.method}`;
101
+ let spec: PathSpec = _.get(paths as any, path, null) as PathSpec;
102
+ if (!spec) {
103
+ spec = { parameters: [], responses: {}, description: "" };
104
+ const pathParams = pathParameters(opt.path);
105
+ if (pathParams.length > 0) spec.parameters = pathParams;
106
+ _.set(paths, path, spec);
107
+ }
108
+
109
+ let ext: ApiExtension = {} as ApiExtension;
110
+
111
+ const req = (flatSch: string): ApiExtension => {
112
+ let schema = flatSch.match(/^#/) ? { $ref: flatSch } : toSwaggerSchema(flatSch);
113
+ spec.requestBody = {
114
+ content: { "application/json": { schema } },
115
+ };
116
+ return ext;
117
+ };
118
+ const res = (code: string | number, flatSch: string): ApiExtension => {
119
+ let schema = flatSch.match(/^#/) ? { $ref: flatSch } : toSwaggerSchema(flatSch);
120
+ spec.responses[code] = {
121
+ content: { [schema.type === "string" ? "text/plain" : "application/json"]: { schema } },
122
+ description: "",
123
+ };
124
+ return ext;
125
+ };
126
+ const query = (strArr: string | string[]): ApiExtension => {
127
+ if (!_.isArray(strArr)) strArr = (strArr as string).split(",");
128
+ strArr.forEach((str) => {
129
+ spec.parameters.push(toParameter('query', str));
130
+ });
131
+ return ext;
132
+ };
133
+ const header = (strArr: string | string[]): ApiExtension => {
134
+ if (!_.isArray(strArr)) strArr = (strArr as string).split(",");
135
+ strArr.forEach((str) => {
136
+ spec.parameters.push(toParameter('header', str));
137
+ });
138
+ return ext;
139
+ };
140
+ const tag = (str: string): ApiExtension => { spec.tags = [str]; return ext; };
141
+ const summary = (str: string): ApiExtension => { spec.summary = str; return ext; };
142
+ const desc = (str: string): ApiExtension => { spec.description = str; return ext; };
143
+ const security = (str: string): ApiExtension => { spec.security = [ { [str]: []}]; return ext; }
144
+ const deprecate = (): ApiExtension => { spec.deprecated = true; return ext; };
145
+ const remove = (): void => { _.unset(paths, `${pathClean(opt.path)}.${opt.method}`); };
146
+
147
+ Object.assign(ext, { req, res, query, header, tag, summary, desc, deprecate, remove, security });
148
+
149
+ // support all ext in parameters
150
+ Object.keys(ext).forEach(k => {
151
+ if (opt[k]) (ext as any)[k](opt[k]);
152
+ });
153
+ Object.keys(opt).filter(k => k.match(/^[0-9]+$/)).forEach(k => ext.res(parseInt(k), opt[k]));
154
+
155
+ return ext;
156
+ }
157
+
158
+ function get(path: string = "", opt: Partial<ApiOptions> = {}): ApiExtension {
159
+ opt.method = "get";
160
+ opt.path = path;
161
+ return api(opt as ApiOptions);
162
+ }
163
+
164
+ function post(path: string = "", opt: Partial<ApiOptions> = {}): ApiExtension {
165
+ opt.method = "post";
166
+ opt.path = path;
167
+ return api(opt as ApiOptions);
168
+ }
169
+
170
+ function put(path: string = "", opt: Partial<ApiOptions> = {}): ApiExtension {
171
+ opt.method = "put";
172
+ opt.path = path;
173
+ return api(opt as ApiOptions);
174
+ }
175
+
176
+ function patch(path: string = "", opt: Partial<ApiOptions> = {}): ApiExtension {
177
+ opt.method = "patch";
178
+ opt.path = path;
179
+ return api(opt as ApiOptions);
180
+ }
181
+
182
+ function del(path: string = "", opt: Partial<ApiOptions> = {}): ApiExtension {
183
+ opt.method = "delete";
184
+ opt.path = path;
185
+ return api(opt as ApiOptions);
186
+ }
187
+
188
+ interface RemoveOptions {
189
+ path?: string;
190
+ tag?: string;
191
+ }
192
+
193
+ function remove({path, tag}: RemoveOptions): void {
194
+ if (path) _.unset(paths, `${pathClean(path)}`);
195
+ if (tag) {
196
+ Object.keys(paths).forEach(p => {
197
+ Object.keys(paths[p]).forEach(method => {
198
+ if (paths[p][method].tags && paths[p][method].tags!.includes(tag)) _.unset(paths[p], method);
199
+ })
200
+ });
201
+ }
202
+ }
203
+
204
+ interface SecurityOptions {
205
+ type?: string;
206
+ schema?: string;
207
+ bearerFormat?: string | null;
208
+ required?: boolean;
209
+ }
210
+
211
+ function security(name: string, {type = "http", schema = "bearer", bearerFormat = null, required = true}: SecurityOptions = {}): string {
212
+ components.securitySchemes = components.securitySchemes || {};
213
+ components.securitySchemes[name] = { type, scheme: schema, bearerFormat, "in": "header", required };
214
+ return `#/components/securitySchemes/${name}`;
215
+ }
216
+
217
+ interface RefOptions {
218
+ type: string;
219
+ name: string;
220
+ def: SwaggerSchema;
221
+ }
222
+
223
+ function _ref({ type, name, def }: RefOptions): string {
224
+ components[type] = components[type] || {};
225
+ (components[type] as any)[name] = def;
226
+ return `#/components/${type}/${name}`;
227
+ }
228
+
229
+ const ref = {
230
+ schema(name: string, flatSch: string): string {
231
+ return _ref({ type: "schemas", name, def: toSwaggerSchema(flatSch) });
232
+ }
233
+ }
234
+
235
+ function tag(name: string, description?: string): void {
236
+ tags.push({ name, description });
237
+ }
238
+
239
+ interface SwaggerDoc {
240
+ info: { title: string; version: string };
241
+ openapi: string;
242
+ servers: Server[];
243
+ tags: Tag[];
244
+ paths: Paths;
245
+ components: Components;
246
+ }
247
+
248
+ function swaggerDoc(title?: string): SwaggerDoc {
249
+ return {
250
+ info: { title: title || "", version: "1.0.0" },
251
+ openapi: "3.0.0",
252
+ servers,
253
+ tags,
254
+ paths,
255
+ components,
256
+ };
257
+ }
258
+
259
+ export {
260
+ tag,
261
+ server,
262
+ ref,
263
+ get,
264
+ post,
265
+ put,
266
+ patch,
267
+ del,
268
+ security,
269
+ swaggerDoc,
270
+ reset,
271
+ remove,
272
+ };
273
+
274
+ export type { ApiOptions, ApiExtension, SwaggerDoc };
package/src/utils.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ Author: Yusuf Bhabhrawala
3
+ */
4
+ import * as _ from "lodash";
5
+ // @ts-ignore
6
+ import { typeShape } from "@sleeksky/alt-schema";
7
+
8
+ const TYPES: { [key: string]: string } = { i: "integer", s: "string", b: "boolean", n: "number", o: "object", a: "array" };
9
+
10
+ const RX_BRACE = /\{([^\}]+)\}/g;
11
+ const RX_COLON = /\:([^\/]+)/g;
12
+
13
+ interface SwaggerSchema {
14
+ type?: string;
15
+ items?: SwaggerSchema;
16
+ properties?: { [key: string]: SwaggerSchema };
17
+ example?: any;
18
+ required?: boolean;
19
+ $ref?: string;
20
+ }
21
+
22
+ function toSwaggerSchema(str: string): SwaggerSchema {
23
+ function traverse(schema: SwaggerSchema, obj: any): void {
24
+ if (_.isArray(obj)) {
25
+ schema.type = "array";
26
+ schema.items = {};
27
+ traverse(schema.items, obj[0]);
28
+ } else if (_.isObject(obj)) {
29
+ schema.type = "object";
30
+ schema.properties = {};
31
+ _.forOwn(obj, (v, k) => {
32
+ schema.properties![k] = {};
33
+ traverse(schema.properties![k], v);
34
+ });
35
+ } else {
36
+ let [optional, type, def] = (obj as string).split(":");
37
+ const isOptional = optional === '?';
38
+ if (isOptional) {
39
+ type = type || 'string';
40
+ }
41
+ if (['string','number','boolean','integer','array','object'].indexOf(type) < 0) type = "string";
42
+ schema.type = type;
43
+ if (def !== undefined && def !== '') {
44
+ if (type === 'number' || type === 'integer') {
45
+ schema.example = parseFloat(def);
46
+ } else if (type === 'boolean') {
47
+ schema.example = ['true','1'].indexOf(def) > -1;
48
+ } else {
49
+ schema.example = def;
50
+ }
51
+ }
52
+ if (!isOptional) schema.required = true;
53
+ }
54
+ }
55
+
56
+ try {
57
+ const obj = typeShape(str);
58
+ const schema: SwaggerSchema = {};
59
+ traverse(schema, obj);
60
+ return schema;
61
+ } catch (error) {
62
+ throw new Error(`Malformed schema: ${str}`);
63
+ }
64
+ }
65
+
66
+ interface PathParameter {
67
+ in: string;
68
+ name: string;
69
+ schema: { type: string; example?: string };
70
+ required: boolean;
71
+ }
72
+
73
+ function pathParameters(str: string): PathParameter[] {
74
+ let params: PathParameter[] = [];
75
+ let matches = str.match(RX_BRACE);
76
+ if (!matches) matches = str.match(RX_COLON);
77
+ if (matches) {
78
+ params = matches.map(m => {
79
+ let [p, def] = m.replace(/^[\{\:]/,"").replace(/[\}]$/,"").split(":");
80
+ if (!def) def = "";
81
+ return { in: "path", name: p, schema: { type: "string", example: def }, required: true };
82
+ });
83
+ }
84
+ return params;
85
+ }
86
+
87
+ function pathClean(path: string): string {
88
+ if (path.match(RX_BRACE)) return path.replace(RX_BRACE, m => `${m.split(/(\:.+)?\}/)[0]}}`)
89
+ return path;
90
+ }
91
+
92
+ interface Parameter {
93
+ name: string;
94
+ in: string;
95
+ required: boolean;
96
+ schema: { type: string; example?: string };
97
+ }
98
+
99
+ // header:?token
100
+ function toParameter(inType: string, str: string | Parameter): Parameter {
101
+ if (!_.isString(str)) return str as Parameter;
102
+ const cleanStr = str.replace(/ /g, "");
103
+ let [name, type, def] = cleanStr.split(":");
104
+ let required = true;
105
+ if (type && type.match(/^\?/)) {
106
+ type = type.replace(/^\?/, "");
107
+ required = false;
108
+ }
109
+ if (type && TYPES[type]) type = TYPES[type];
110
+ else type = "string";
111
+ const schema: { type: string; example?: string } = { type };
112
+ if (def) schema.example = def;
113
+
114
+ return {
115
+ name,
116
+ in: inType,
117
+ required,
118
+ schema
119
+ };
120
+ }
121
+
122
+ export { toSwaggerSchema, pathParameters, pathClean, toParameter };
123
+ export type { SwaggerSchema, PathParameter, Parameter };
124
+
125
+ module.exports = {toSwaggerSchema, pathParameters, pathClean, toParameter};
package/src/index.js DELETED
@@ -1,158 +0,0 @@
1
- /**
2
- Author: Yusuf Bhabhrawala
3
- */
4
- require("./utils");
5
- const _ = require("lodash");
6
- const { toSwaggerSchema, pathParameters, pathClean, toParameter} = require("./utils")
7
-
8
- const paths = {};
9
- const components = {};
10
- const tags = [];
11
- const servers = [];
12
-
13
- function reset() {
14
- Object.keys(paths).forEach((key) => delete paths[key]);
15
- Object.keys(components).forEach((key) => delete paths[key]);
16
- tags.splice(0, tags.length);
17
- servers.splice(0, servers.length);
18
- }
19
-
20
- function server(url, description) {
21
- servers.push({ url, description });
22
- }
23
-
24
- function api(opt) {
25
- let path = `${pathClean(opt.path)}.${opt.method}`;
26
- let spec = _.get(paths, path, false);
27
- if (!spec) {
28
- spec = { parameters: [], responses: {}, description: "" };
29
- const pathParams = pathParameters(opt.path);
30
- if (pathParams.length > 0) spec.parameters = pathParams;
31
- _.set(paths, path, spec);
32
- }
33
-
34
- let ext = {};
35
-
36
- const req = (flatSch) => {
37
- let schema = flatSch.match(/^#/) ? { $ref: flatSch } : toSwaggerSchema(flatSch);
38
- spec.requestBody = {
39
- content: { "application/json": { schema } },
40
- };
41
- return ext;
42
- };
43
- const res = (code, flatSch) => {
44
- let schema = flatSch.match(/^#/) ? { $ref: flatSch } : toSwaggerSchema(flatSch);
45
- spec.responses[code] = {
46
- content: { [schema.type === "string" ? "text/plain" : "application/json"]: { schema } },
47
- description: "",
48
- };
49
- return ext;
50
- };
51
- const query = (strArr) => {
52
- if (!_.isArray(strArr)) strArr = strArr.split(",");
53
- strArr.forEach((str) => {
54
- spec.parameters.push(toParameter('query',str));
55
- });
56
- return ext;
57
- };
58
- const header = (strArr) => {
59
- if (!_.isArray(strArr)) strArr = strArr.split(",");
60
- strArr.forEach((str) => {
61
- spec.parameters.push(toParameter('header',str));
62
- });
63
- return ext;
64
- };
65
- const tag = (str) => { spec.tags = [str]; return ext; };
66
- const summary = (str) => { spec.summary = str; return ext; };
67
- const desc = (str) => { spec.description = str; return ext; };
68
- const security = (str) => { spec.security = [ { [str]: []}]; return ext; }
69
- const deprecate = () => { spec.deprecated = true; return ext; };
70
- const remove = () => { _.unset(paths, `${pathClean(opt.path)}.${opt.method}`); };
71
-
72
- Object.assign(ext, { req, res, query, header, tag, summary, desc, deprecate, remove, security });
73
-
74
- // support all ext in parameters
75
- Object.keys(ext).forEach(k => {
76
- if (opt[k]) ext[k](opt[k]);
77
- });
78
- Object.keys(opt).filter(k => k.match(/^[0-9]+$/)).forEach(k => ext.res(k, opt[k]));
79
-
80
- return ext;
81
- }
82
-
83
- function get(path = "", opt = {}) {
84
- opt.method = "get";
85
- opt.path = path;
86
- return api(opt);
87
- }
88
-
89
- function post(path = "", opt = {}) {
90
- opt.method = "post";
91
- opt.path = path;
92
- return api(opt);
93
- }
94
-
95
- function put(path = "", opt = {}) {
96
- opt.method = "put";
97
- opt.path = path;
98
- return api(opt);
99
- }
100
-
101
- function patch(path = "", opt = {}) {
102
- opt.method = "patch";
103
- opt.path = path;
104
- return api(opt);
105
- }
106
-
107
- function del(path = "", opt = {}) {
108
- opt.method = "delete";
109
- opt.path = path;
110
- return api(opt);
111
- }
112
-
113
- function security(name, {type = "http", schema = "bearer", bearerFormat = null, required = true}) {
114
- components.securitySchemes = components.securitySchemes || {};
115
- components.securitySchemes[name] = { type, scheme: schema, bearerFormat, "in": "header", required };
116
- return `#/components/securitySchemes/${name}`;
117
- }
118
-
119
- function _ref({ type, name, def }) {
120
- components[type] = components[type] || {};
121
- components[type][name] = def;
122
- return `#/components/${type}/${name}`;
123
- }
124
-
125
- const ref = {
126
- schema(name, flatSch) {
127
- return _ref({ type: "schemas", name, def: toSwaggerSchema(flatSch) });
128
- }
129
- }
130
-
131
- function tag(name, description) {
132
- tags.push({ name, description });
133
- }
134
-
135
- function swaggerDoc(title) {
136
- return {
137
- info: { title: title || "", version: "1.0.0" },
138
- openapi: "3.0.0",
139
- servers,
140
- tags,
141
- paths,
142
- components,
143
- };
144
- }
145
-
146
- module.exports = {
147
- tag,
148
- server,
149
- ref,
150
- get,
151
- post,
152
- put,
153
- patch,
154
- del,
155
- security,
156
- swaggerDoc,
157
- reset,
158
- };
package/src/utils.js DELETED
@@ -1,101 +0,0 @@
1
- /**
2
- Author: Yusuf Bhabhrawala
3
- */
4
- const _ = require("lodash");
5
-
6
- const {typeShape} = require("@sleeksky/alt-schema");
7
-
8
- const TYPES = {i: "integer", s: "string", b: "boolean", n: "number", o: "object", a: "array"};
9
- const RX_OBJ = /(\{[^\{\}\[\]]+\})/;
10
- const RX_ARR = /(\[[^\{\}\[\]]+\])/;
11
- const RX_NESTED = /^\:?(\$[0-9]+)$/;
12
- const RX_FLAT_ARR = /^\[([^\{\}\[\]]+)\]$/;
13
- const RX_FLAT_OBJ = /^\{([^\{\}\[\]]+)\}$/;
14
-
15
- const RX_BRACE = /\{([^\}]+)\}/g;
16
- const RX_COLON = /\:([^\/]+)/g;
17
-
18
-
19
- function toSwaggerSchema(str) {
20
-
21
- function traverse(schema, obj) {
22
- if (_.isArray(obj)) {
23
- schema.type = "array";
24
- schema.items = {};
25
- traverse(schema.items, obj[0]);
26
- } else if (_.isObject(obj)) {
27
- schema.type = "object";
28
- schema.properties = {};
29
- _.forOwn(obj, (v, k) => {
30
- schema.properties[k] = {};
31
- traverse(schema.properties[k], v);
32
- });
33
- } else {
34
- let [optional, type, def] = obj.split(":");
35
- optional = (optional === '?') ? true : false;
36
- if (['string','number','boolean','integer','array','object'].indexOf(type) < 0) type = "string";
37
- schema.type = type;
38
- if (def !== '') {
39
- if (type === 'number' || type === 'integer') def = def*1;
40
- if (type === 'boolean') def = ['true','1'].indexOf(def) > -1 ? true : false;
41
- schema.example = def;
42
- } else if (optional) {
43
- // schema.default = null;
44
- }
45
- if (!optional) schema.required = true;
46
- }
47
- }
48
-
49
- try {
50
- let obj = typeShape(str);
51
- let schema = {};
52
- traverse(schema, obj);
53
- return schema;
54
- } catch (error) {
55
- throw new Error(`Malformed schema: ${str}`);
56
- }
57
- }
58
-
59
- function pathParameters(str) {
60
- let params = [];
61
- let matches = str.match(RX_BRACE);
62
- if (!matches) matches = str.match(RX_COLON);
63
- if (matches) {
64
- params = matches.map(m => {
65
- let [p, def] = m.replace(/^[\{\:]/,"").replace(/[\}]$/,"").split(":");
66
- if (!def) def = "";
67
- return { in: "path", name: p, schema: { type: "string", example: def }, required: true };
68
- });
69
- }
70
- return params;
71
- }
72
-
73
- function pathClean(path) {
74
- if (path.match(RX_BRACE)) return path.replace(RX_BRACE, m => `${m.split(/(\:.+)?\}/)[0]}}`)
75
- return path;
76
- }
77
-
78
- // header:?token
79
- function toParameter(inType, str) {
80
- if (!_.isString(str)) return str;
81
- str = str.replace(/ /g, "");
82
- let [name, type, def] = str.split(":");
83
- let required = true;
84
- if (type && type.match(/^\?/)) {
85
- type = type.replace(/^\?/, "");
86
- required = false;
87
- }
88
- if (type && TYPES[type]) type = TYPES[type];
89
- else type = "string";
90
- let schema = { type };
91
- if (def) schema.example = def;
92
-
93
- return {
94
- name,
95
- in: inType,
96
- required,
97
- schema
98
- }
99
- }
100
-
101
- module.exports = {toSwaggerSchema, pathParameters, pathClean, toParameter};