enlace-openapi 0.0.1-beta.2 → 0.0.1-beta.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Enlace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -186,18 +186,30 @@ Parameter names are auto-generated from the parent segment (e.g., `users` → `u
186
186
 
187
187
  ## Programmatic API
188
188
 
189
+ ### Next.js + Swagger UI Example
190
+
189
191
  ```typescript
192
+ import SwaggerUI from "swagger-ui-react";
193
+ import "swagger-ui-react/swagger-ui.css";
190
194
  import { parseSchema, generateOpenAPISpec } from "enlace-openapi";
191
195
 
192
- const { endpoints, schemas } = parseSchema("./types/APISchema.ts", "ApiSchema");
193
-
194
- const spec = generateOpenAPISpec(endpoints, schemas, {
195
- title: "My API",
196
- version: "1.0.0",
197
- baseUrl: "https://api.example.com",
198
- });
196
+ const spec = (() => {
197
+ const { endpoints, schemas } = parseSchema(
198
+ "./APISchema.ts",
199
+ "ApiSchema"
200
+ );
201
+ return generateOpenAPISpec(endpoints, schemas, {
202
+ title: "My API",
203
+ version: "1.0.0",
204
+ baseUrl: "https://api.example.com",
205
+ });
206
+ })();
207
+
208
+ const DocsPage = () => {
209
+ return <SwaggerUI spec={spec} />;
210
+ };
199
211
 
200
- console.log(JSON.stringify(spec, null, 2));
212
+ export default DocsPage;
201
213
  ```
202
214
 
203
215
  ## Viewing the OpenAPI Spec
package/dist/cli.js CHANGED
@@ -299,16 +299,20 @@ function isEndpointStructure(type) {
299
299
  propNames.delete("data");
300
300
  propNames.delete("body");
301
301
  propNames.delete("error");
302
+ propNames.delete("query");
303
+ propNames.delete("formData");
302
304
  const remainingProps = [...propNames].filter(
303
305
  (name) => !name.startsWith("__@") && !name.includes("Brand")
304
306
  );
305
307
  return remainingProps.length === 0;
306
308
  }
307
- function parseEndpointType(type, path2, method, pathParams, ctx) {
309
+ function parseEndpointType(type, pathStr, method, pathParams, ctx) {
308
310
  const { checker } = ctx;
309
311
  let dataType;
310
312
  let bodyType;
311
313
  let errorType;
314
+ let queryType;
315
+ let formDataType;
312
316
  const typesToCheck = type.isIntersection() ? type.types : [type];
313
317
  for (const t of typesToCheck) {
314
318
  const props = t.getProperties();
@@ -320,32 +324,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
320
324
  bodyType = checker.getTypeOfSymbol(prop);
321
325
  } else if (name === "error") {
322
326
  errorType = checker.getTypeOfSymbol(prop);
327
+ } else if (name === "query") {
328
+ queryType = checker.getTypeOfSymbol(prop);
329
+ } else if (name === "formData") {
330
+ formDataType = checker.getTypeOfSymbol(prop);
323
331
  }
324
332
  }
325
333
  }
326
334
  const endpoint = {
327
- path: path2,
335
+ path: pathStr,
328
336
  method,
329
337
  responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
330
338
  pathParams
331
339
  };
332
- if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
340
+ if (formDataType && !(formDataType.flags & import_typescript2.default.TypeFlags.Never)) {
341
+ endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
342
+ endpoint.requestBodyContentType = "multipart/form-data";
343
+ } else if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
333
344
  endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
345
+ endpoint.requestBodyContentType = "application/json";
346
+ }
347
+ if (queryType && !(queryType.flags & import_typescript2.default.TypeFlags.Never)) {
348
+ endpoint.queryParams = queryTypeToParams(queryType, ctx);
334
349
  }
335
350
  if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
336
351
  endpoint.errorSchema = typeToSchema(errorType, ctx);
337
352
  }
338
353
  return endpoint;
339
354
  }
355
+ function queryTypeToParams(queryType, ctx) {
356
+ const { checker } = ctx;
357
+ const params = [];
358
+ const props = queryType.getProperties();
359
+ for (const prop of props) {
360
+ const propName = prop.getName();
361
+ const propType = checker.getTypeOfSymbol(prop);
362
+ const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
363
+ params.push({
364
+ name: propName,
365
+ in: "query",
366
+ required: !isOptional,
367
+ schema: typeToSchema(propType, ctx)
368
+ });
369
+ }
370
+ return params;
371
+ }
372
+ function formDataTypeToSchema(formDataType, ctx) {
373
+ const { checker } = ctx;
374
+ const properties = {};
375
+ const required = [];
376
+ const props = formDataType.getProperties();
377
+ for (const prop of props) {
378
+ const propName = prop.getName();
379
+ const propType = checker.getTypeOfSymbol(prop);
380
+ const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
381
+ const typeName = checker.typeToString(propType);
382
+ if (typeName.includes("File") || typeName.includes("Blob")) {
383
+ properties[propName] = { type: "string", format: "binary" };
384
+ } else if (propType.isUnion()) {
385
+ const hasFile = propType.types.some((t) => {
386
+ const name = checker.typeToString(t);
387
+ return name.includes("File") || name.includes("Blob");
388
+ });
389
+ if (hasFile) {
390
+ properties[propName] = { type: "string", format: "binary" };
391
+ } else {
392
+ properties[propName] = typeToSchema(propType, ctx);
393
+ }
394
+ } else {
395
+ properties[propName] = typeToSchema(propType, ctx);
396
+ }
397
+ if (!isOptional) {
398
+ required.push(propName);
399
+ }
400
+ }
401
+ const schema = {
402
+ type: "object",
403
+ properties
404
+ };
405
+ if (required.length > 0) {
406
+ schema.required = required;
407
+ }
408
+ return schema;
409
+ }
340
410
 
341
411
  // src/generator.ts
342
412
  function generateOpenAPISpec(endpoints, schemas, options = {}) {
343
- const {
344
- title = "API",
345
- version = "1.0.0",
346
- description,
347
- baseUrl
348
- } = options;
413
+ const { title = "API", version = "1.0.0", description, baseUrl } = options;
349
414
  const paths = {};
350
415
  for (const endpoint of endpoints) {
351
416
  if (!paths[endpoint.path]) {
@@ -399,11 +464,15 @@ function createOperation(endpoint) {
399
464
  }
400
465
  };
401
466
  }
467
+ if (endpoint.queryParams && endpoint.queryParams.length > 0) {
468
+ operation.parameters = endpoint.queryParams;
469
+ }
402
470
  if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
471
+ const contentType = endpoint.requestBodyContentType || "application/json";
403
472
  operation.requestBody = {
404
473
  required: true,
405
474
  content: {
406
- "application/json": {
475
+ [contentType]: {
407
476
  schema: endpoint.requestBodySchema
408
477
  }
409
478
  }
package/dist/cli.mjs CHANGED
@@ -276,16 +276,20 @@ function isEndpointStructure(type) {
276
276
  propNames.delete("data");
277
277
  propNames.delete("body");
278
278
  propNames.delete("error");
279
+ propNames.delete("query");
280
+ propNames.delete("formData");
279
281
  const remainingProps = [...propNames].filter(
280
282
  (name) => !name.startsWith("__@") && !name.includes("Brand")
281
283
  );
282
284
  return remainingProps.length === 0;
283
285
  }
284
- function parseEndpointType(type, path2, method, pathParams, ctx) {
286
+ function parseEndpointType(type, pathStr, method, pathParams, ctx) {
285
287
  const { checker } = ctx;
286
288
  let dataType;
287
289
  let bodyType;
288
290
  let errorType;
291
+ let queryType;
292
+ let formDataType;
289
293
  const typesToCheck = type.isIntersection() ? type.types : [type];
290
294
  for (const t of typesToCheck) {
291
295
  const props = t.getProperties();
@@ -297,32 +301,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
297
301
  bodyType = checker.getTypeOfSymbol(prop);
298
302
  } else if (name === "error") {
299
303
  errorType = checker.getTypeOfSymbol(prop);
304
+ } else if (name === "query") {
305
+ queryType = checker.getTypeOfSymbol(prop);
306
+ } else if (name === "formData") {
307
+ formDataType = checker.getTypeOfSymbol(prop);
300
308
  }
301
309
  }
302
310
  }
303
311
  const endpoint = {
304
- path: path2,
312
+ path: pathStr,
305
313
  method,
306
314
  responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
307
315
  pathParams
308
316
  };
309
- if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
317
+ if (formDataType && !(formDataType.flags & ts2.TypeFlags.Never)) {
318
+ endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
319
+ endpoint.requestBodyContentType = "multipart/form-data";
320
+ } else if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
310
321
  endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
322
+ endpoint.requestBodyContentType = "application/json";
323
+ }
324
+ if (queryType && !(queryType.flags & ts2.TypeFlags.Never)) {
325
+ endpoint.queryParams = queryTypeToParams(queryType, ctx);
311
326
  }
312
327
  if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
313
328
  endpoint.errorSchema = typeToSchema(errorType, ctx);
314
329
  }
315
330
  return endpoint;
316
331
  }
332
+ function queryTypeToParams(queryType, ctx) {
333
+ const { checker } = ctx;
334
+ const params = [];
335
+ const props = queryType.getProperties();
336
+ for (const prop of props) {
337
+ const propName = prop.getName();
338
+ const propType = checker.getTypeOfSymbol(prop);
339
+ const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
340
+ params.push({
341
+ name: propName,
342
+ in: "query",
343
+ required: !isOptional,
344
+ schema: typeToSchema(propType, ctx)
345
+ });
346
+ }
347
+ return params;
348
+ }
349
+ function formDataTypeToSchema(formDataType, ctx) {
350
+ const { checker } = ctx;
351
+ const properties = {};
352
+ const required = [];
353
+ const props = formDataType.getProperties();
354
+ for (const prop of props) {
355
+ const propName = prop.getName();
356
+ const propType = checker.getTypeOfSymbol(prop);
357
+ const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
358
+ const typeName = checker.typeToString(propType);
359
+ if (typeName.includes("File") || typeName.includes("Blob")) {
360
+ properties[propName] = { type: "string", format: "binary" };
361
+ } else if (propType.isUnion()) {
362
+ const hasFile = propType.types.some((t) => {
363
+ const name = checker.typeToString(t);
364
+ return name.includes("File") || name.includes("Blob");
365
+ });
366
+ if (hasFile) {
367
+ properties[propName] = { type: "string", format: "binary" };
368
+ } else {
369
+ properties[propName] = typeToSchema(propType, ctx);
370
+ }
371
+ } else {
372
+ properties[propName] = typeToSchema(propType, ctx);
373
+ }
374
+ if (!isOptional) {
375
+ required.push(propName);
376
+ }
377
+ }
378
+ const schema = {
379
+ type: "object",
380
+ properties
381
+ };
382
+ if (required.length > 0) {
383
+ schema.required = required;
384
+ }
385
+ return schema;
386
+ }
317
387
 
318
388
  // src/generator.ts
319
389
  function generateOpenAPISpec(endpoints, schemas, options = {}) {
320
- const {
321
- title = "API",
322
- version = "1.0.0",
323
- description,
324
- baseUrl
325
- } = options;
390
+ const { title = "API", version = "1.0.0", description, baseUrl } = options;
326
391
  const paths = {};
327
392
  for (const endpoint of endpoints) {
328
393
  if (!paths[endpoint.path]) {
@@ -376,11 +441,15 @@ function createOperation(endpoint) {
376
441
  }
377
442
  };
378
443
  }
444
+ if (endpoint.queryParams && endpoint.queryParams.length > 0) {
445
+ operation.parameters = endpoint.queryParams;
446
+ }
379
447
  if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
448
+ const contentType = endpoint.requestBodyContentType || "application/json";
380
449
  operation.requestBody = {
381
450
  required: true,
382
451
  content: {
383
- "application/json": {
452
+ [contentType]: {
384
453
  schema: endpoint.requestBodySchema
385
454
  }
386
455
  }
package/dist/index.d.mts CHANGED
@@ -14,20 +14,24 @@ type JSONSchema = {
14
14
  format?: string;
15
15
  description?: string;
16
16
  };
17
+ type OpenAPIRequestBody = {
18
+ required?: boolean;
19
+ content: {
20
+ "application/json"?: {
21
+ schema: JSONSchema;
22
+ };
23
+ "multipart/form-data"?: {
24
+ schema: JSONSchema;
25
+ };
26
+ };
27
+ };
17
28
  type OpenAPIOperation = {
18
29
  operationId?: string;
19
30
  summary?: string;
20
31
  description?: string;
21
32
  tags?: string[];
22
33
  parameters?: OpenAPIParameter[];
23
- requestBody?: {
24
- required?: boolean;
25
- content: {
26
- "application/json": {
27
- schema: JSONSchema;
28
- };
29
- };
30
- };
34
+ requestBody?: OpenAPIRequestBody;
31
35
  responses: Record<string, {
32
36
  description: string;
33
37
  content?: {
@@ -73,6 +77,8 @@ type ParsedEndpoint = {
73
77
  method: "get" | "post" | "put" | "patch" | "delete";
74
78
  responseSchema: JSONSchema;
75
79
  requestBodySchema?: JSONSchema;
80
+ requestBodyContentType?: "application/json" | "multipart/form-data";
81
+ queryParams?: OpenAPIParameter[];
76
82
  errorSchema?: JSONSchema;
77
83
  pathParams: string[];
78
84
  };
@@ -99,4 +105,4 @@ type GeneratorOptions = {
99
105
  };
100
106
  declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
101
107
 
102
- export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
108
+ export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPIRequestBody, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
package/dist/index.d.ts CHANGED
@@ -14,20 +14,24 @@ type JSONSchema = {
14
14
  format?: string;
15
15
  description?: string;
16
16
  };
17
+ type OpenAPIRequestBody = {
18
+ required?: boolean;
19
+ content: {
20
+ "application/json"?: {
21
+ schema: JSONSchema;
22
+ };
23
+ "multipart/form-data"?: {
24
+ schema: JSONSchema;
25
+ };
26
+ };
27
+ };
17
28
  type OpenAPIOperation = {
18
29
  operationId?: string;
19
30
  summary?: string;
20
31
  description?: string;
21
32
  tags?: string[];
22
33
  parameters?: OpenAPIParameter[];
23
- requestBody?: {
24
- required?: boolean;
25
- content: {
26
- "application/json": {
27
- schema: JSONSchema;
28
- };
29
- };
30
- };
34
+ requestBody?: OpenAPIRequestBody;
31
35
  responses: Record<string, {
32
36
  description: string;
33
37
  content?: {
@@ -73,6 +77,8 @@ type ParsedEndpoint = {
73
77
  method: "get" | "post" | "put" | "patch" | "delete";
74
78
  responseSchema: JSONSchema;
75
79
  requestBodySchema?: JSONSchema;
80
+ requestBodyContentType?: "application/json" | "multipart/form-data";
81
+ queryParams?: OpenAPIParameter[];
76
82
  errorSchema?: JSONSchema;
77
83
  pathParams: string[];
78
84
  };
@@ -99,4 +105,4 @@ type GeneratorOptions = {
99
105
  };
100
106
  declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
101
107
 
102
- export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
108
+ export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPIRequestBody, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
package/dist/index.js CHANGED
@@ -307,16 +307,20 @@ function isEndpointStructure(type) {
307
307
  propNames.delete("data");
308
308
  propNames.delete("body");
309
309
  propNames.delete("error");
310
+ propNames.delete("query");
311
+ propNames.delete("formData");
310
312
  const remainingProps = [...propNames].filter(
311
313
  (name) => !name.startsWith("__@") && !name.includes("Brand")
312
314
  );
313
315
  return remainingProps.length === 0;
314
316
  }
315
- function parseEndpointType(type, path2, method, pathParams, ctx) {
317
+ function parseEndpointType(type, pathStr, method, pathParams, ctx) {
316
318
  const { checker } = ctx;
317
319
  let dataType;
318
320
  let bodyType;
319
321
  let errorType;
322
+ let queryType;
323
+ let formDataType;
320
324
  const typesToCheck = type.isIntersection() ? type.types : [type];
321
325
  for (const t of typesToCheck) {
322
326
  const props = t.getProperties();
@@ -328,32 +332,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
328
332
  bodyType = checker.getTypeOfSymbol(prop);
329
333
  } else if (name === "error") {
330
334
  errorType = checker.getTypeOfSymbol(prop);
335
+ } else if (name === "query") {
336
+ queryType = checker.getTypeOfSymbol(prop);
337
+ } else if (name === "formData") {
338
+ formDataType = checker.getTypeOfSymbol(prop);
331
339
  }
332
340
  }
333
341
  }
334
342
  const endpoint = {
335
- path: path2,
343
+ path: pathStr,
336
344
  method,
337
345
  responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
338
346
  pathParams
339
347
  };
340
- if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
348
+ if (formDataType && !(formDataType.flags & import_typescript2.default.TypeFlags.Never)) {
349
+ endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
350
+ endpoint.requestBodyContentType = "multipart/form-data";
351
+ } else if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
341
352
  endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
353
+ endpoint.requestBodyContentType = "application/json";
354
+ }
355
+ if (queryType && !(queryType.flags & import_typescript2.default.TypeFlags.Never)) {
356
+ endpoint.queryParams = queryTypeToParams(queryType, ctx);
342
357
  }
343
358
  if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
344
359
  endpoint.errorSchema = typeToSchema(errorType, ctx);
345
360
  }
346
361
  return endpoint;
347
362
  }
363
+ function queryTypeToParams(queryType, ctx) {
364
+ const { checker } = ctx;
365
+ const params = [];
366
+ const props = queryType.getProperties();
367
+ for (const prop of props) {
368
+ const propName = prop.getName();
369
+ const propType = checker.getTypeOfSymbol(prop);
370
+ const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
371
+ params.push({
372
+ name: propName,
373
+ in: "query",
374
+ required: !isOptional,
375
+ schema: typeToSchema(propType, ctx)
376
+ });
377
+ }
378
+ return params;
379
+ }
380
+ function formDataTypeToSchema(formDataType, ctx) {
381
+ const { checker } = ctx;
382
+ const properties = {};
383
+ const required = [];
384
+ const props = formDataType.getProperties();
385
+ for (const prop of props) {
386
+ const propName = prop.getName();
387
+ const propType = checker.getTypeOfSymbol(prop);
388
+ const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
389
+ const typeName = checker.typeToString(propType);
390
+ if (typeName.includes("File") || typeName.includes("Blob")) {
391
+ properties[propName] = { type: "string", format: "binary" };
392
+ } else if (propType.isUnion()) {
393
+ const hasFile = propType.types.some((t) => {
394
+ const name = checker.typeToString(t);
395
+ return name.includes("File") || name.includes("Blob");
396
+ });
397
+ if (hasFile) {
398
+ properties[propName] = { type: "string", format: "binary" };
399
+ } else {
400
+ properties[propName] = typeToSchema(propType, ctx);
401
+ }
402
+ } else {
403
+ properties[propName] = typeToSchema(propType, ctx);
404
+ }
405
+ if (!isOptional) {
406
+ required.push(propName);
407
+ }
408
+ }
409
+ const schema = {
410
+ type: "object",
411
+ properties
412
+ };
413
+ if (required.length > 0) {
414
+ schema.required = required;
415
+ }
416
+ return schema;
417
+ }
348
418
 
349
419
  // src/generator.ts
350
420
  function generateOpenAPISpec(endpoints, schemas, options = {}) {
351
- const {
352
- title = "API",
353
- version = "1.0.0",
354
- description,
355
- baseUrl
356
- } = options;
421
+ const { title = "API", version = "1.0.0", description, baseUrl } = options;
357
422
  const paths = {};
358
423
  for (const endpoint of endpoints) {
359
424
  if (!paths[endpoint.path]) {
@@ -407,11 +472,15 @@ function createOperation(endpoint) {
407
472
  }
408
473
  };
409
474
  }
475
+ if (endpoint.queryParams && endpoint.queryParams.length > 0) {
476
+ operation.parameters = endpoint.queryParams;
477
+ }
410
478
  if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
479
+ const contentType = endpoint.requestBodyContentType || "application/json";
411
480
  operation.requestBody = {
412
481
  required: true,
413
482
  content: {
414
- "application/json": {
483
+ [contentType]: {
415
484
  schema: endpoint.requestBodySchema
416
485
  }
417
486
  }
package/dist/index.mjs CHANGED
@@ -270,16 +270,20 @@ function isEndpointStructure(type) {
270
270
  propNames.delete("data");
271
271
  propNames.delete("body");
272
272
  propNames.delete("error");
273
+ propNames.delete("query");
274
+ propNames.delete("formData");
273
275
  const remainingProps = [...propNames].filter(
274
276
  (name) => !name.startsWith("__@") && !name.includes("Brand")
275
277
  );
276
278
  return remainingProps.length === 0;
277
279
  }
278
- function parseEndpointType(type, path2, method, pathParams, ctx) {
280
+ function parseEndpointType(type, pathStr, method, pathParams, ctx) {
279
281
  const { checker } = ctx;
280
282
  let dataType;
281
283
  let bodyType;
282
284
  let errorType;
285
+ let queryType;
286
+ let formDataType;
283
287
  const typesToCheck = type.isIntersection() ? type.types : [type];
284
288
  for (const t of typesToCheck) {
285
289
  const props = t.getProperties();
@@ -291,32 +295,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
291
295
  bodyType = checker.getTypeOfSymbol(prop);
292
296
  } else if (name === "error") {
293
297
  errorType = checker.getTypeOfSymbol(prop);
298
+ } else if (name === "query") {
299
+ queryType = checker.getTypeOfSymbol(prop);
300
+ } else if (name === "formData") {
301
+ formDataType = checker.getTypeOfSymbol(prop);
294
302
  }
295
303
  }
296
304
  }
297
305
  const endpoint = {
298
- path: path2,
306
+ path: pathStr,
299
307
  method,
300
308
  responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
301
309
  pathParams
302
310
  };
303
- if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
311
+ if (formDataType && !(formDataType.flags & ts2.TypeFlags.Never)) {
312
+ endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
313
+ endpoint.requestBodyContentType = "multipart/form-data";
314
+ } else if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
304
315
  endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
316
+ endpoint.requestBodyContentType = "application/json";
317
+ }
318
+ if (queryType && !(queryType.flags & ts2.TypeFlags.Never)) {
319
+ endpoint.queryParams = queryTypeToParams(queryType, ctx);
305
320
  }
306
321
  if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
307
322
  endpoint.errorSchema = typeToSchema(errorType, ctx);
308
323
  }
309
324
  return endpoint;
310
325
  }
326
+ function queryTypeToParams(queryType, ctx) {
327
+ const { checker } = ctx;
328
+ const params = [];
329
+ const props = queryType.getProperties();
330
+ for (const prop of props) {
331
+ const propName = prop.getName();
332
+ const propType = checker.getTypeOfSymbol(prop);
333
+ const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
334
+ params.push({
335
+ name: propName,
336
+ in: "query",
337
+ required: !isOptional,
338
+ schema: typeToSchema(propType, ctx)
339
+ });
340
+ }
341
+ return params;
342
+ }
343
+ function formDataTypeToSchema(formDataType, ctx) {
344
+ const { checker } = ctx;
345
+ const properties = {};
346
+ const required = [];
347
+ const props = formDataType.getProperties();
348
+ for (const prop of props) {
349
+ const propName = prop.getName();
350
+ const propType = checker.getTypeOfSymbol(prop);
351
+ const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
352
+ const typeName = checker.typeToString(propType);
353
+ if (typeName.includes("File") || typeName.includes("Blob")) {
354
+ properties[propName] = { type: "string", format: "binary" };
355
+ } else if (propType.isUnion()) {
356
+ const hasFile = propType.types.some((t) => {
357
+ const name = checker.typeToString(t);
358
+ return name.includes("File") || name.includes("Blob");
359
+ });
360
+ if (hasFile) {
361
+ properties[propName] = { type: "string", format: "binary" };
362
+ } else {
363
+ properties[propName] = typeToSchema(propType, ctx);
364
+ }
365
+ } else {
366
+ properties[propName] = typeToSchema(propType, ctx);
367
+ }
368
+ if (!isOptional) {
369
+ required.push(propName);
370
+ }
371
+ }
372
+ const schema = {
373
+ type: "object",
374
+ properties
375
+ };
376
+ if (required.length > 0) {
377
+ schema.required = required;
378
+ }
379
+ return schema;
380
+ }
311
381
 
312
382
  // src/generator.ts
313
383
  function generateOpenAPISpec(endpoints, schemas, options = {}) {
314
- const {
315
- title = "API",
316
- version = "1.0.0",
317
- description,
318
- baseUrl
319
- } = options;
384
+ const { title = "API", version = "1.0.0", description, baseUrl } = options;
320
385
  const paths = {};
321
386
  for (const endpoint of endpoints) {
322
387
  if (!paths[endpoint.path]) {
@@ -370,11 +435,15 @@ function createOperation(endpoint) {
370
435
  }
371
436
  };
372
437
  }
438
+ if (endpoint.queryParams && endpoint.queryParams.length > 0) {
439
+ operation.parameters = endpoint.queryParams;
440
+ }
373
441
  if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
442
+ const contentType = endpoint.requestBodyContentType || "application/json";
374
443
  operation.requestBody = {
375
444
  required: true,
376
445
  content: {
377
- "application/json": {
446
+ [contentType]: {
378
447
  schema: endpoint.requestBodySchema
379
448
  }
380
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enlace-openapi",
3
- "version": "0.0.1-beta.2",
3
+ "version": "0.0.1-beta.3",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "enlace-openapi": "./dist/cli.mjs"
@@ -15,18 +15,17 @@
15
15
  "require": "./dist/index.js"
16
16
  }
17
17
  },
18
- "scripts": {
19
- "dev": "tsup --watch",
20
- "build": "tsup",
21
- "typecheck": "tsc --noEmit",
22
- "lint": "eslint src --max-warnings 0",
23
- "prepublishOnly": "npm run build && npm run typecheck && npm run lint"
24
- },
25
18
  "dependencies": {
26
19
  "commander": "^12.1.0",
27
20
  "typescript": "^5.9.2"
28
21
  },
29
22
  "devDependencies": {
30
23
  "@types/node": "^22.19.2"
24
+ },
25
+ "scripts": {
26
+ "dev": "tsup --watch",
27
+ "build": "tsup",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "eslint src --max-warnings 0"
31
30
  }
32
- }
31
+ }