mockapi-msi 2.4.0 → 2.5.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
@@ -6,6 +6,12 @@ MockAPI let you create fake responses with pre defined and dynamic data for defi
6
6
 
7
7
  MockAPI also is intended to help you when, during testing phase, you cannot afford complex and expensive products (And you do not need them) that requires bulky configuration steps or depends directly on third party providers that you cannot control.
8
8
 
9
+ ## Version 2.5.0 notes
10
+
11
+ - **OpenAPI JSON endpoint**: MockAPI now generates and serves an OpenAPI document at `/openapi.json` based on your configured endpoints.
12
+ - **Interactive docs page**: Swagger UI is available at `/docs`, allowing users to inspect and try endpoints directly from the browser.
13
+ - **OpenAPI configuration**: New optional `openApi` section allows enabling/disabling docs and overriding docs/spec paths and metadata.
14
+
9
15
  ## Version 2.4.0 notes
10
16
 
11
17
  - **HTTPS support**: New `tls` configuration option with `cert` and `key` paths. When provided, MockAPI starts an HTTPS server instead of HTTP.
@@ -66,6 +72,8 @@ Edit ```.mockapi-config``` to add your own endpoints, responses, parsers and dat
66
72
 
67
73
  **staticPath** - Optional configuration. Path to a local directory to serve static files from. Requests that don't match any endpoint will fall back to static file serving.
68
74
 
75
+ **openApi** - Optional configuration to enable/disable generated OpenAPI docs and customize routes and document metadata.
76
+
69
77
  **data** -
70
78
  Holds and describe the available data for all endpoints and responses.
71
79
 
@@ -164,6 +172,36 @@ The previous example will wait ```2000ms``` before sending the response, which i
164
172
 
165
173
  MockAPI watches the ```.mockapi-config``` file for changes. When the file is saved, endpoints and data sources are automatically reloaded without restarting the server. This allows you to add, remove, or modify endpoints while the server is running.
166
174
 
175
+ #### OpenAPI docs
176
+
177
+ MockAPI can auto-generate OpenAPI docs from your configured endpoints and expose them with built-in routes:
178
+
179
+ - ``/openapi.json`` - generated OpenAPI document
180
+ - ``/docs`` - Swagger UI page powered by the generated document
181
+
182
+ These routes update automatically when `.mockapi-config` changes (hot-reload).
183
+ The `/docs` page loads Swagger UI assets from `unpkg.com`.
184
+
185
+ To customize or disable this feature, use the optional `openApi` section:
186
+
187
+ ```yaml
188
+ openApi:
189
+ enabled: true
190
+ docsPath: "/docs"
191
+ specPath: "/openapi.json"
192
+ info:
193
+ title: "My Mock API"
194
+ version: "1.0.0"
195
+ description: "Generated from MockAPI configuration."
196
+ ```
197
+
198
+ Disable docs completely:
199
+
200
+ ```yaml
201
+ openApi:
202
+ enabled: false
203
+ ```
204
+
167
205
  #### CORS configuration
168
206
 
169
207
  CORS can be configured in three ways:
@@ -316,4 +354,4 @@ docker run -p 3001:8001 \
316
354
  -v $(pwd)/testdata:/usr/src/app/testdata \
317
355
  -v $(pwd)/apiHandlers:/usr/src/app/apiHandlers \
318
356
  mockapi
319
- ```
357
+ ```
package/modules/cli.js CHANGED
@@ -15,6 +15,9 @@ class CLI {
15
15
  _configTemplate = {
16
16
  port: 8080,
17
17
  enableCors: true,
18
+ openApi: {
19
+ enabled: true
20
+ },
18
21
  data: {
19
22
  myRows: { path: 'YOUR FOLDER', reader: 'folder' }
20
23
  },
@@ -104,4 +107,4 @@ class CLI {
104
107
  }
105
108
  }
106
109
 
107
- module.exports = CLI;
110
+ module.exports = CLI;
package/modules/core.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
  const constants = require('./constants');
7
7
  const handlerLoader = require('./configurationParser');
8
+ const openApi = require('./openApi');
8
9
 
9
10
  const MIME_TYPES = {
10
11
  '.html': 'text/html',
@@ -35,6 +36,8 @@ class Core {
35
36
  _staticPath = null;
36
37
  _connections = new Set();
37
38
  _tls = null;
39
+ _openApi = null;
40
+ _openApiSpec = null;
38
41
 
39
42
  constructor(logger, configurations, modulesProxy) {
40
43
  this._configurations = configurations;
@@ -45,9 +48,11 @@ class Core {
45
48
  this._modulesProxy = modulesProxy;
46
49
  this._staticPath = configurations.staticPath || null;
47
50
  this._tls = this._parseTls(configurations.tls);
51
+ this._openApi = this._parseOpenApi(configurations.openApi);
48
52
 
49
53
  this._data = configurations.data || { };
50
54
  handlerLoader.loadHandlersFromConfiguration(this._data);
55
+ this._refreshOpenApiSpec();
51
56
  }
52
57
 
53
58
  _parseTls(tlsConfig) {
@@ -78,6 +83,63 @@ class Core {
78
83
  };
79
84
  }
80
85
 
86
+ _normalizeRoutePath(routePath, fallbackPath) {
87
+ if (typeof routePath !== 'string' || routePath.trim() === '') return fallbackPath;
88
+
89
+ let normalized = routePath.trim();
90
+
91
+ if (!normalized.startsWith('/')) {
92
+ normalized = `/${normalized}`;
93
+ }
94
+
95
+ if (normalized.length > 1 && normalized.endsWith('/')) {
96
+ normalized = normalized.slice(0, -1);
97
+ }
98
+
99
+ return normalized;
100
+ }
101
+
102
+ _parseOpenApi(openApiConfig) {
103
+ const defaultConfig = {
104
+ enabled: true,
105
+ docsPath: '/docs',
106
+ specPath: '/openapi.json',
107
+ title: 'MockAPI',
108
+ version: '1.0.0',
109
+ description: 'OpenAPI definition generated from .mockapi-config.'
110
+ };
111
+
112
+ if (openApiConfig === false) {
113
+ return {
114
+ ...defaultConfig,
115
+ enabled: false
116
+ };
117
+ }
118
+
119
+ if (openApiConfig === true || openApiConfig === undefined || openApiConfig === null) {
120
+ return defaultConfig;
121
+ }
122
+
123
+ if (typeof openApiConfig !== 'object') {
124
+ return defaultConfig;
125
+ }
126
+
127
+ const infoConfig = openApiConfig.info || {};
128
+
129
+ return {
130
+ enabled: openApiConfig.enabled !== false,
131
+ docsPath: this._normalizeRoutePath(openApiConfig.docsPath, defaultConfig.docsPath),
132
+ specPath: this._normalizeRoutePath(openApiConfig.specPath, defaultConfig.specPath),
133
+ title: infoConfig.title || openApiConfig.title || defaultConfig.title,
134
+ version: infoConfig.version || openApiConfig.version || defaultConfig.version,
135
+ description: infoConfig.description || openApiConfig.description || defaultConfig.description
136
+ };
137
+ }
138
+
139
+ _refreshOpenApiSpec() {
140
+ this._openApiSpec = openApi.buildSpec(this._openApi, this._endpointList);
141
+ }
142
+
81
143
  _applyCorsHeaders(request, response) {
82
144
  if (!this._cors) return false;
83
145
 
@@ -134,6 +196,30 @@ class Core {
134
196
  return true;
135
197
  }
136
198
 
199
+ _handleOpenApiRequest(request, response, pathName) {
200
+ if (!this._openApi || this._openApi.enabled === false || request.method !== 'GET') {
201
+ return false;
202
+ }
203
+
204
+ if (pathName === this._openApi.specPath) {
205
+ response.statusCode = constants.HTTP_STATUS_CODES.OK;
206
+ response.setHeader('Content-Type', 'application/json');
207
+ this._applyCorsHeaders(request, response);
208
+ response.end(JSON.stringify(this._openApiSpec, null, 2));
209
+ return true;
210
+ }
211
+
212
+ if (pathName === this._openApi.docsPath || pathName === `${this._openApi.docsPath}/`) {
213
+ response.statusCode = constants.HTTP_STATUS_CODES.OK;
214
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
215
+ this._applyCorsHeaders(request, response);
216
+ response.end(openApi.buildDocsPage(this._openApi));
217
+ return true;
218
+ }
219
+
220
+ return false;
221
+ }
222
+
137
223
  run() {
138
224
  const self = this;
139
225
 
@@ -147,6 +233,12 @@ class Core {
147
233
  return;
148
234
  }
149
235
 
236
+ const urlInformation = parser.parse(request.url);
237
+
238
+ if (self._handleOpenApiRequest(request, response, urlInformation.pathname)) {
239
+ return;
240
+ }
241
+
150
242
  let bodyPayload = [];
151
243
 
152
244
  request.on('data', (chunk) => {
@@ -154,8 +246,6 @@ class Core {
154
246
  }).on('end', () => {
155
247
  bodyPayload = Buffer.concat(bodyPayload).toString();
156
248
 
157
- const urlInformation = parser.parse(request.url);
158
-
159
249
  self._logger.info(`Requesting: ${urlInformation.base} - Verb: ${request.method}`);
160
250
 
161
251
  if (bodyPayload !== '') {
@@ -266,9 +356,11 @@ class Core {
266
356
  this._cors = this._parseCors(configurations.enableCors);
267
357
  this._endpointList = configurations.endpoints;
268
358
  this._staticPath = configurations.staticPath || null;
359
+ this._openApi = this._parseOpenApi(configurations.openApi);
269
360
 
270
361
  this._data = configurations.data || {};
271
362
  handlerLoader.loadHandlersFromConfiguration(this._data);
363
+ this._refreshOpenApiSpec();
272
364
 
273
365
  this._logger.info('Configuration reloaded');
274
366
  }
@@ -286,4 +378,4 @@ class Core {
286
378
  }
287
379
  }
288
380
 
289
- module.exports = Core;
381
+ module.exports = Core;
@@ -0,0 +1,178 @@
1
+ const http = require('http');
2
+ const constants = require('./constants');
3
+
4
+ const OPENAPI_VERSION = '3.1.0';
5
+ const DEFAULT_METHODS_FOR_ANY = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
6
+
7
+ const normalizeMethod = (verb) => {
8
+ if (typeof verb !== 'string' || verb.trim() === '') return ['get'];
9
+
10
+ const method = verb.toLowerCase();
11
+ return method === 'any' ? DEFAULT_METHODS_FOR_ANY : [method];
12
+ };
13
+
14
+ const toOpenApiPath = (endpointPath) => {
15
+ if (typeof endpointPath !== 'string' || endpointPath.trim() === '') return '/';
16
+ return endpointPath.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
17
+ };
18
+
19
+ const extractPathParameters = (endpointPath) => {
20
+ if (typeof endpointPath !== 'string' || endpointPath.trim() === '') return [];
21
+
22
+ const pathParameters = endpointPath.match(/:([A-Za-z0-9_]+)/g) || [];
23
+
24
+ return pathParameters.map((parameter) => ({
25
+ name: parameter.substring(1),
26
+ in: 'path',
27
+ required: true,
28
+ schema: { type: 'string' }
29
+ }));
30
+ };
31
+
32
+ const getResponseStatusCode = (endpointConfiguration) => {
33
+ const status = parseInt(endpointConfiguration.responseStatus, 10);
34
+
35
+ if (!Number.isNaN(status) && status >= 100 && status <= 599) {
36
+ return status;
37
+ }
38
+
39
+ return constants.HTTP_STATUS_CODES.OK;
40
+ };
41
+
42
+ const buildResponseContent = (contentType) => {
43
+ if (!contentType) return undefined;
44
+
45
+ const isJsonContentType = contentType.toLowerCase().includes('json');
46
+
47
+ return {
48
+ [contentType]: {
49
+ schema: isJsonContentType ? {} : { type: 'string' }
50
+ }
51
+ };
52
+ };
53
+
54
+ const buildOperation = (method, endpointPath, endpointConfiguration) => {
55
+ const responseStatusCode = getResponseStatusCode(endpointConfiguration);
56
+ const responseContentType = endpointConfiguration.responseContentType || constants.DEFAULT_CONTENT_TYPE;
57
+ const responseDescription = http.STATUS_CODES[responseStatusCode] || 'Configured response';
58
+
59
+ const operation = {
60
+ summary: `${method.toUpperCase()} ${toOpenApiPath(endpointPath)}`,
61
+ responses: {
62
+ [responseStatusCode]: {
63
+ description: responseDescription
64
+ }
65
+ }
66
+ };
67
+
68
+ const responseContent = buildResponseContent(responseContentType);
69
+ if (responseContent !== undefined) {
70
+ operation.responses[responseStatusCode].content = responseContent;
71
+ }
72
+
73
+ const parameters = extractPathParameters(endpointPath);
74
+ if (parameters.length > 0) {
75
+ operation.parameters = parameters;
76
+ }
77
+
78
+ const metadata = {};
79
+ if (endpointConfiguration.data !== undefined) metadata.data = endpointConfiguration.data;
80
+ if (endpointConfiguration.handler !== undefined) metadata.handler = endpointConfiguration.handler;
81
+ if (endpointConfiguration.delay !== undefined) metadata.delay = endpointConfiguration.delay;
82
+
83
+ if (Object.keys(metadata).length > 0) {
84
+ operation['x-mockapi'] = metadata;
85
+ }
86
+
87
+ return operation;
88
+ };
89
+
90
+ const buildSpec = (settings, endpointList) => {
91
+ const openApiSettings = settings || {};
92
+ const endpoints = endpointList || {};
93
+ const paths = {};
94
+
95
+ for (const endpointPath in endpoints) {
96
+ if (!Object.hasOwnProperty.call(endpoints, endpointPath)) continue;
97
+
98
+ const endpointConfiguration = endpoints[endpointPath] || {};
99
+ const openApiPath = toOpenApiPath(endpointPath);
100
+
101
+ if (!paths[openApiPath]) {
102
+ paths[openApiPath] = {};
103
+ }
104
+
105
+ const methods = normalizeMethod(endpointConfiguration.verb);
106
+ for (const method of methods) {
107
+ paths[openApiPath][method] = buildOperation(method, endpointPath, endpointConfiguration);
108
+ }
109
+ }
110
+
111
+ return {
112
+ openapi: OPENAPI_VERSION,
113
+ info: {
114
+ title: openApiSettings.title || 'MockAPI',
115
+ version: openApiSettings.version || '1.0.0',
116
+ description: openApiSettings.description || 'OpenAPI definition generated from .mockapi-config.'
117
+ },
118
+ servers: [{ url: '/' }],
119
+ paths
120
+ };
121
+ };
122
+
123
+ const escapeHtml = (value) => String(value)
124
+ .replace(/&/g, '&amp;')
125
+ .replace(/</g, '&lt;')
126
+ .replace(/>/g, '&gt;')
127
+ .replace(/"/g, '&quot;')
128
+ .replace(/'/g, '&#39;');
129
+
130
+ const escapeJsString = (value) => String(value)
131
+ .replace(/\\/g, '\\\\')
132
+ .replace(/'/g, "\\'");
133
+
134
+ const buildDocsPage = (settings) => {
135
+ const docsSettings = settings || {};
136
+ const title = escapeHtml(docsSettings.title || 'MockAPI');
137
+ const specPath = escapeJsString(docsSettings.specPath || '/openapi.json');
138
+
139
+ return `<!doctype html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="utf-8" />
143
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
144
+ <title>${title} - API Docs</title>
145
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
146
+ <style>
147
+ html, body {
148
+ margin: 0;
149
+ background: #f6f8fa;
150
+ }
151
+ #swagger-ui {
152
+ min-height: 100vh;
153
+ }
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <div id="swagger-ui"></div>
158
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
159
+ <script>
160
+ window.onload = function() {
161
+ window.ui = SwaggerUIBundle({
162
+ url: '${specPath}',
163
+ dom_id: '#swagger-ui',
164
+ deepLinking: true,
165
+ presets: [SwaggerUIBundle.presets.apis],
166
+ layout: 'BaseLayout'
167
+ });
168
+ };
169
+ </script>
170
+ </body>
171
+ </html>`;
172
+ };
173
+
174
+ module.exports = {
175
+ buildSpec,
176
+ buildDocsPage,
177
+ toOpenApiPath
178
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mockapi-msi",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Mock API is a lightweight configurable HTTP API for testing and prototyping.",
5
5
  "main": "main.js",
6
6
  "scripts": {