swaggie 1.8.0 → 1.8.2

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
@@ -80,7 +80,8 @@ swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore
80
80
  -t, --template <string> Template to use for code generation (default: "axios")
81
81
  -m, --mode <mode> Generation mode: "full" or "schemas" (default: "full")
82
82
  -d, --schemaStyle <style> Schema object style: "interface" or "type" (default: "interface")
83
- --preferAny Use "any" instead of "unknown" for untyped values (default: false)
83
+ --enumStyle <style> Enum style for plain string enums: "union" or "enum" (default: "union")
84
+ --preferAny Use "any" instead of "unknown" for untyped values (default: false)
84
85
  --skipDeprecated Exclude deprecated operations from the output (default: false)
85
86
  --servicePrefix Prefix for service names — useful when generating multiple APIs
86
87
  --allowDots Use dot notation to serialize nested object query params
@@ -123,6 +124,7 @@ swaggie -c swaggie.config.json
123
124
  "nullableStrategy": "ignore",
124
125
  "generationMode": "full",
125
126
  "schemaDeclarationStyle": "interface",
127
+ "enumDeclarationStyle": "union",
126
128
  "queryParamsSerialization": {
127
129
  "arrayFormat": "repeat",
128
130
  "allowDots": true
@@ -279,6 +281,14 @@ Use `schemaDeclarationStyle` (or CLI `--schemaStyle`) to control object schema o
279
281
  | `"interface"`| `export interface Tag { ... }` (default) |
280
282
  | `"type"` | `export type Tag = { ... };` |
281
283
 
284
+ ### Enum Declaration Style
285
+
286
+ Use `enumDeclarationStyle` (or CLI `--enumStyle`) for plain string enums:
287
+ - `"union"` (default): `export type Status = "active" | "disabled";`
288
+ - `"enum"`: `export enum Status { active = "active", disabled = "disabled" }`
289
+
290
+ Note: this applies only to plain string enums. Non-string enums are still emitted as union types.
291
+
282
292
  ### Parameter Modifiers
283
293
 
284
294
  Sometimes an API spec marks a parameter as required, but your client handles it in an interceptor and you don't want it cluttering every method signature. Parameter modifiers let you override this globally without touching the spec.
@@ -360,12 +370,12 @@ Swaggie only needs a JSON or YAML OpenAPI spec file — it does not require a ru
360
370
  | Supported | Not Supported |
361
371
  | ------------------------------------------------------------------------------ | ---------------------------------------------------- |
362
372
  | OpenAPI 3.0, 3.1, 3.2 | Swagger / OpenAPI 2.0 |
363
- | `allOf`, `oneOf`, `anyOf`, `$ref` | `not` keyword |
373
+ | `allOf`, `oneOf`, `anyOf`, `$ref`, external $refs | `not` keyword |
364
374
  | Spec formats: JSON, YAML | Very complex query parameter structures |
365
375
  | Extensions: `x-position`, `x-name`, `x-enumNames`, `x-enum-varnames` | Multiple response types (only the first is used) |
366
376
  | Content types: JSON, plain text, multipart/form-data | Multiple request body types (only the first is used) |
367
- | Content types: `application/x-www-form-urlencoded`, `application/octet-stream` | References to external spec files |
368
- | Various enum definition styles | OpenAPI callbacks and webhooks |
377
+ | Content types: `application/x-www-form-urlencoded`, `application/octet-stream` | OpenAPI callbacks and webhooks |
378
+ | Various enum definition styles, support for additionalProperties | |
369
379
  | Nullable types, path inheritance, JSDoc descriptions | |
370
380
  | Remote URLs and local file paths as spec source | |
371
381
  | Grouping by tags, graceful handling of duplicate operation IDs | |
package/dist/cli.js CHANGED
@@ -23,6 +23,10 @@ const schemaStyleOption = new (0, _commander.Option)(
23
23
  '-d, --schemaStyle <style>',
24
24
  'Schema object declaration style'
25
25
  ).choices(['interface', 'type']);
26
+ const enumStyleOption = new (0, _commander.Option)(
27
+ '--enumStyle <style>',
28
+ 'Enum declaration style for plain string enums'
29
+ ).choices(['union', 'enum']);
26
30
 
27
31
  const program = new (0, _commander.Command)();
28
32
  program
@@ -64,7 +68,8 @@ program
64
68
  )
65
69
  .addOption(arrayFormatOption)
66
70
  .addOption(modeOption)
67
- .addOption(schemaStyleOption);
71
+ .addOption(schemaStyleOption)
72
+ .addOption(enumStyleOption);
68
73
 
69
74
  program.parse(process.argv);
70
75
 
@@ -96,45 +96,63 @@ function prepareClient(
96
96
  }
97
97
 
98
98
  return ops.map((op) => {
99
- const [respObject, responseContentType] = _utils.getBestResponse.call(void 0, op, components);
100
- const returnType = _swagger.getParameterType.call(void 0, respObject, options);
99
+ const operationContext = `${op.method.toUpperCase()} ${op.path} (${op.operationId || 'unknown operationId'})`;
101
100
 
102
- const body = getRequestBody(op.requestBody, components, options);
103
- const queryParams = getParams(op.parameters , options, ['query']);
104
- const params = getParams(op.parameters , options);
101
+ try {
102
+ const [respObject, responseContentType] = _utils.getBestResponse.call(void 0, op, components);
103
+ const returnType = _swagger.getParameterType.call(void 0, respObject, options);
105
104
 
106
- if (body) {
107
- params.unshift(body);
108
- }
105
+ const body = getRequestBody(op.requestBody, components, options);
106
+ const queryParams = getParams(op.parameters , options, ['query']);
107
+ const params = getParams(op.parameters , options);
108
+
109
+ if (body) {
110
+ params.unshift(body);
111
+ }
109
112
 
110
113
  // If all parameters have 'x-position' defined, sort them by it
111
- if (params.every((p) => p.original['x-position'])) {
112
- params.sort((a, b) => a.original['x-position'] - b.original['x-position']);
113
- }
114
+ if (params.every((p) => p.original['x-position'])) {
115
+ params.sort((a, b) => a.original['x-position'] - b.original['x-position']);
116
+ }
114
117
 
115
- markParametersAsSkippable(params);
118
+ markParametersAsSkippable(params);
116
119
 
117
- const headers = getParams(op.parameters , options, ['header']);
118
- // Some libraries need to know the content type of the request body in case of urlencoded body
119
- if (_optionalChain([body, 'optionalAccess', _2 => _2.contentType]) === 'urlencoded') {
120
- headers.push({
121
- originalName: 'Content-Type',
122
- value: 'application/x-www-form-urlencoded',
123
- });
124
- }
120
+ const headers = getParams(
121
+ op.parameters ,
122
+ options,
123
+ ['header']
124
+ );
125
+ // Some libraries need explicit Content-Type for request bodies.
126
+ if (_optionalChain([body, 'optionalAccess', _2 => _2.contentType]) === 'urlencoded') {
127
+ upsertFixedHeader(headers, 'Content-Type', 'application/x-www-form-urlencoded');
128
+ } else if (_optionalChain([body, 'optionalAccess', _3 => _3.contentType]) === 'json' && options.template === 'fetch') {
129
+ upsertFixedHeader(headers, 'Content-Type', 'application/json');
130
+ }
125
131
 
126
- return {
127
- jsDocs: _jsDocs.prepareJsDocsForOperation.call(void 0, op, params),
128
- returnType,
129
- responseContentType,
130
- method: op.method.toUpperCase(),
131
- name: getOperationName(op.operationId, op.group),
132
- url: prepareUrl(op.path),
133
- parameters: params,
134
- query: queryParams,
135
- body,
136
- headers,
137
- };
132
+ return {
133
+ jsDocs: _jsDocs.prepareJsDocsForOperation.call(void 0, op, params),
134
+ returnType,
135
+ responseContentType,
136
+ method: op.method.toUpperCase(),
137
+ name: getOperationName(op.operationId, op.group),
138
+ url: prepareUrl(op.path),
139
+ parameters: params,
140
+ query: queryParams,
141
+ body,
142
+ headers,
143
+ };
144
+ } catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ if (message.includes('Invalid schema at')) {
147
+ throw new Error(
148
+ `Failed to prepare operation ${operationContext}. ` +
149
+ 'Check if schema is valid for this operation. ' +
150
+ 'Most common culprit is `properties.$ref` (use `schema.$ref` at root, or put `$ref` under a named property).'
151
+ );
152
+ }
153
+
154
+ throw new Error(`Failed to prepare operation ${operationContext}: ${message}`);
155
+ }
138
156
  });
139
157
  } exports.prepareOperations = prepareOperations;
140
158
 
@@ -237,10 +255,10 @@ function prepareUrl(path) {
237
255
  type: _swagger.getParameterType.call(void 0, p, options),
238
256
  optional: p.required === undefined || p.required === null ? true : !p.required,
239
257
  original: p,
240
- jsDoc: _optionalChain([p, 'access', _3 => _3.description, 'optionalAccess', _4 => _4.trim, 'call', _5 => _5()]),
258
+ jsDoc: _optionalChain([p, 'access', _4 => _4.description, 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()]),
241
259
  }));
242
260
 
243
- if (_optionalChain([options, 'access', _6 => _6.modifiers, 'optionalAccess', _7 => _7.parameters])) {
261
+ if (_optionalChain([options, 'access', _7 => _7.modifiers, 'optionalAccess', _8 => _8.parameters])) {
244
262
  for (const [name, modifier] of Object.entries(options.modifiers.parameters)) {
245
263
  const paramIndex = result.findIndex(
246
264
  (p) => p.original.in !== 'path' && (p.originalName === name || p.name === name)
@@ -291,7 +309,7 @@ function getRequestBody(
291
309
  let reqBody;
292
310
  if ('$ref' in rawReqBody) {
293
311
  const refName = rawReqBody.$ref.replace('#/components/requestBodies/', '');
294
- const resolved = _optionalChain([components, 'optionalAccess', _8 => _8.requestBodies, 'optionalAccess', _9 => _9[refName]]);
312
+ const resolved = _optionalChain([components, 'optionalAccess', _9 => _9.requestBodies, 'optionalAccess', _10 => _10[refName]]);
295
313
  if (!resolved || '$ref' in resolved) {
296
314
  console.error(`RequestBody $ref '${rawReqBody.$ref}' not found in components/requestBodies`);
297
315
  return null;
@@ -317,3 +335,19 @@ function getRequestBody(
317
335
 
318
336
  return null;
319
337
  }
338
+
339
+ function upsertFixedHeader(headers, headerName, value) {
340
+ const headerIndex = headers.findIndex(
341
+ (header) => header.originalName.toLowerCase() === headerName.toLowerCase()
342
+ );
343
+
344
+ if (headerIndex >= 0) {
345
+ headers[headerIndex].value = value;
346
+ return;
347
+ }
348
+
349
+ headers.push({
350
+ originalName: headerName,
351
+ value,
352
+ });
353
+ }
@@ -61,6 +61,7 @@ function renderSchema(
61
61
  }
62
62
 
63
63
  const result = [];
64
+ const schemaContext = `components.schemas.${safeName}`;
64
65
  if (_nullishCoalesce(schema.description, () => ( schema.title))) {
65
66
  result.push(_jsDocs.renderComment.call(void 0, _nullishCoalesce(schema.description, () => ( schema.title))));
66
67
  }
@@ -70,7 +71,7 @@ function renderSchema(
70
71
  return result.join('\n');
71
72
  }
72
73
  if ('enum' in schema) {
73
- result.push(renderEnumType(safeName, schema));
74
+ result.push(renderEnumType(safeName, schema, options));
74
75
  return result.join('\n');
75
76
  }
76
77
 
@@ -83,7 +84,15 @@ function renderSchema(
83
84
  if ('allOf' in schema) {
84
85
  const types = _swagger.getRefCompositeTypes.call(void 0, schema);
85
86
  const mergedSchema = getMergedCompositeObjects(schema);
86
- const objectContents = generateObjectTypeContents(mergedSchema, options);
87
+ const objectType = _swagger.getTypeFromSchema.call(void 0, mergedSchema, options, `${schemaContext}.allOf`);
88
+ const objectContents = generateObjectTypeContents(mergedSchema, options, schemaContext);
89
+ const hasAdditionalProperties = !!mergedSchema.additionalProperties;
90
+
91
+ if (hasAdditionalProperties) {
92
+ const compositeTypes = [...types, objectType].join(' & ');
93
+ result.push(`export type ${safeName} = ${compositeTypes};`);
94
+ return `${result.join('\n')}\n`;
95
+ }
87
96
 
88
97
  if (useTypeAliases) {
89
98
  const compositeTypes = [...types, `{${objectContents ? `\n${objectContents}\n` : ''}}`].join(' & ');
@@ -95,7 +104,7 @@ function renderSchema(
95
104
  result.push(`export interface ${safeName} ${extensions}{`);
96
105
  result.push(objectContents);
97
106
  } else if ('oneOf' in schema || 'anyOf' in schema) {
98
- const typeDefinition = getTypesFromAnyOrOneOf(schema, options);
107
+ const typeDefinition = _swagger.getTypeFromSchema.call(void 0, schema, options, schemaContext);
99
108
  result.push(`export type ${safeName} = ${typeDefinition};`);
100
109
 
101
110
  return `${result.join('\n')}\n`;
@@ -105,7 +114,15 @@ function renderSchema(
105
114
  result.push(`export type ${safeName} = ${generateItemsType(schema.items, options)}[];`);
106
115
  return result.join('\n');
107
116
  } else {
108
- const objectContents = generateObjectTypeContents(schema, options);
117
+ const objectType = _swagger.getTypeFromSchema.call(void 0, schema, options, schemaContext);
118
+ const hasAdditionalProperties = !!schema.additionalProperties;
119
+
120
+ const objectContents = generateObjectTypeContents(schema, options, schemaContext);
121
+ if (hasAdditionalProperties) {
122
+ result.push(`export type ${safeName} = ${objectType};`);
123
+ return `${result.join('\n')}\n`;
124
+ }
125
+
109
126
  if (useTypeAliases) {
110
127
  result.push(`export type ${safeName} = {`);
111
128
  result.push(objectContents);
@@ -119,25 +136,14 @@ function renderSchema(
119
136
  return `${result.join('\n')}\n}\n`;
120
137
  }
121
138
 
122
- /**
123
- * Generates the type definition for an `anyOf` or `oneOf` schema.
124
- * @param schema - The schema object to generate the type definition for.
125
- * @param options - The options for the generation.
126
- * @returns The type definition for the `anyOf` or `oneOf` schema.
127
- */
128
- function getTypesFromAnyOrOneOf(schema, options) {
129
- const composite = schema.allOf || schema.oneOf || schema.anyOf;
130
- if (!composite) {
131
- return '';
132
- }
133
-
134
- return composite.map((s) => _swagger.getTypeFromSchema.call(void 0, s, options)).join(' | ');
135
- }
136
-
137
139
  /**
138
140
  * Generates the inline contents of an object type.
139
141
  */
140
- function generateObjectTypeContents(schema, options) {
142
+ function generateObjectTypeContents(
143
+ schema,
144
+ options,
145
+ schemaContext = 'components.schemas.unknown'
146
+ ) {
141
147
  const result = [];
142
148
  const required = schema.required || [];
143
149
  const props = Object.keys(schema.properties || {});
@@ -145,7 +151,7 @@ function generateObjectTypeContents(schema, options) {
145
151
  for (const prop of props) {
146
152
  const propDefinition = schema.properties[prop];
147
153
  const isRequired = !!~required.indexOf(prop);
148
- result.push(renderTypeProp(prop, propDefinition, isRequired, options));
154
+ result.push(renderTypeProp(prop, propDefinition, isRequired, options, schemaContext));
149
155
  }
150
156
 
151
157
  return result.join('\n');
@@ -182,11 +188,36 @@ function renderExtendedEnumType(name, def) {
182
188
  /**
183
189
  * Render simple enum types (just a union of values)
184
190
  */
185
- function renderEnumType(name, def) {
191
+ function renderEnumType(name, def, options) {
192
+ if (options.enumDeclarationStyle === 'enum' && shouldRenderStringEnumDeclaration(def)) {
193
+ return renderStringEnumDeclaration(name, def);
194
+ }
195
+
186
196
  const values = def.enum.map((v) => (typeof v === 'number' ? v : `"${v}"`)).join(' | ');
187
197
  return `export type ${name} = ${values};\n`;
188
198
  }
189
199
 
200
+ function shouldRenderStringEnumDeclaration(def)
201
+
202
+ {
203
+ return (
204
+ def.type === 'string' &&
205
+ Array.isArray(def.enum) &&
206
+ def.enum.every((value) => typeof value === 'string')
207
+ );
208
+ }
209
+
210
+ function renderStringEnumDeclaration(name, def) {
211
+ let res = `export enum ${name} {\n`;
212
+ for (let index = 0; index < def.enum.length; index++) {
213
+ const value = def.enum[index];
214
+ const memberName = _nullishCoalesce(_utils.escapePropName.call(void 0, value), () => ( `VALUE_${index}`));
215
+ res += ` ${memberName} = ${JSON.stringify(value)},\n`;
216
+ }
217
+
218
+ return `${res}}\n`;
219
+ }
220
+
190
221
  /**
191
222
  * OpenApi 3.1 introduced a new way to define enums that we support here.
192
223
  */
@@ -207,10 +238,11 @@ function renderTypeProp(
207
238
  propName,
208
239
  definition,
209
240
  required,
210
- options
241
+ options,
242
+ schemaContext
211
243
  ) {
212
244
  const lines = [];
213
- const type = _swagger.getTypeFromSchema.call(void 0, definition, options);
245
+ const type = _swagger.getTypeFromSchema.call(void 0, definition, options, `${schemaContext}.properties.${propName}`);
214
246
 
215
247
  if ('description' in definition || 'title' in definition) {
216
248
  const renderedComment = _jsDocs.renderComment.call(void 0, _nullishCoalesce(definition.description, () => ( definition.title)));
@@ -267,7 +299,7 @@ function getMergedCompositeObjects(schema) {
267
299
  subSchemas.push(safeSchema);
268
300
  }
269
301
 
270
- return deepMerge({}, ...subSchemas);
302
+ return deepMerge({}, ...subSchemas) ;
271
303
  }
272
304
 
273
305
  function isObject(item) {
package/dist/index.js CHANGED
@@ -83,6 +83,7 @@ function readFile(filePath) {
83
83
  arrayFormat,
84
84
  mode,
85
85
  schemaStyle,
86
+ enumStyle,
86
87
  template,
87
88
  queryParamsSerialization = {},
88
89
  ...rest
@@ -104,6 +105,8 @@ function readFile(filePath) {
104
105
  generationMode: _nullishCoalesce(_nullishCoalesce(mode, () => ( rest.generationMode)), () => ( _swagger.APP_DEFAULTS.generationMode)),
105
106
  schemaDeclarationStyle:
106
107
  _nullishCoalesce(_nullishCoalesce(schemaStyle, () => ( rest.schemaDeclarationStyle)), () => ( _swagger.APP_DEFAULTS.schemaDeclarationStyle)),
108
+ enumDeclarationStyle:
109
+ _nullishCoalesce(_nullishCoalesce(enumStyle, () => ( rest.enumDeclarationStyle)), () => ( _swagger.APP_DEFAULTS.enumDeclarationStyle)),
107
110
  queryParamsSerialization: mergedQueryParamsSerialization,
108
111
  };
109
112
  } exports.prepareAppOptions = prepareAppOptions;
@@ -16,7 +16,8 @@ var _utils = require('../utils');
16
16
  */
17
17
  function getParameterType(
18
18
  param,
19
- options
19
+ options,
20
+ context = 'schema'
20
21
  ) {
21
22
  const unknownType = options.preferAny ? 'any' : 'unknown';
22
23
 
@@ -24,7 +25,7 @@ var _utils = require('../utils');
24
25
  return unknownType;
25
26
  }
26
27
 
27
- return getTypeFromSchemaResolved(param.schema, options);
28
+ return getTypeFromSchemaResolved(param.schema, options, `${context}.schema`);
28
29
  } exports.getParameterType = getParameterType;
29
30
 
30
31
  /**
@@ -35,9 +36,10 @@ var _utils = require('../utils');
35
36
  */
36
37
  function getTypeFromSchema(
37
38
  schema,
38
- options
39
+ options,
40
+ context = 'schema'
39
41
  ) {
40
- return getTypeFromSchemaResolved(schema, options);
42
+ return getTypeFromSchemaResolved(schema, options, context);
41
43
  } exports.getTypeFromSchema = getTypeFromSchema;
42
44
 
43
45
  /**
@@ -46,7 +48,8 @@ var _utils = require('../utils');
46
48
  */
47
49
  function getTypeFromSchemaResolved(
48
50
  schema,
49
- options
51
+ options,
52
+ context
50
53
  ) {
51
54
  const unknownType = options.preferAny ? 'any' : 'unknown';
52
55
 
@@ -54,6 +57,17 @@ function getTypeFromSchemaResolved(
54
57
  return unknownType;
55
58
  }
56
59
 
60
+ if (typeof schema !== 'object' || Array.isArray(schema)) {
61
+ if (context.endsWith('.properties.$ref') && typeof schema === 'string') {
62
+ throw new Error(
63
+ `Invalid schema at ${context}: string value under properties.$ref is not a valid schema. ` +
64
+ 'Did you mean to use schema.$ref at the root, or wrap it in a named property (for example properties.data.$ref)?'
65
+ );
66
+ }
67
+
68
+ throw new Error(`Invalid schema at ${context}: expected an object schema, got ${typeof schema}.`);
69
+ }
70
+
57
71
  if ('$ref' in schema) {
58
72
  const refName = schema.$ref.split('/').pop();
59
73
  return getSafeIdentifier(refName) || unknownType;
@@ -65,13 +79,13 @@ function getTypeFromSchemaResolved(
65
79
 
66
80
  // OpenAPI 3.1 nullable: type is an array containing 'null', e.g. ["string", "null"]
67
81
  if (Array.isArray(schema.type)) {
68
- return getTypeFromOA31ArrayType(schema , options);
82
+ return getTypeFromOA31ArrayType(schema , options, context);
69
83
  }
70
84
 
71
85
  // OpenAPI 3.0 nullable: nullable: true
72
86
  const isNullable = 'nullable' in schema && schema.nullable === true;
73
87
  const isNullableSuffix = isNullable && options.nullableStrategy === 'include' ? ' | null' : '';
74
- const type = getTypeFromSchemaInternal(schema, options);
88
+ const type = getTypeFromSchemaInternal(schema, options, context);
75
89
 
76
90
  if (isNullableSuffix && type.endsWith('| null')) {
77
91
  return type;
@@ -84,7 +98,11 @@ function getTypeFromSchemaResolved(
84
98
  * The presence of `"null"` in the array is the OA3.1 way of marking a field as nullable.
85
99
  * Respects `nullableStrategy` the same way as OA3.0 `nullable: true`.
86
100
  */
87
- function getTypeFromOA31ArrayType(schema, options) {
101
+ function getTypeFromOA31ArrayType(
102
+ schema,
103
+ options,
104
+ context
105
+ ) {
88
106
  const unknownType = options.preferAny ? 'any' : 'unknown';
89
107
  const types = schema.type ;
90
108
  const isNullable = types.includes('null');
@@ -97,11 +115,17 @@ function getTypeFromOA31ArrayType(schema, options) {
97
115
  } else if (nonNullTypes.length === 1) {
98
116
  // Synthesize a single-type schema to reuse existing resolution logic
99
117
  const singleTypeSchema = { ...schema, type: nonNullTypes[0] } ;
100
- baseType = getTypeFromSchemaInternal(singleTypeSchema, options);
118
+ baseType = getTypeFromSchemaInternal(singleTypeSchema, options, context);
101
119
  } else {
102
120
  // Multiple non-null types — resolve each independently and join as a union
103
121
  baseType = nonNullTypes
104
- .map((t) => getTypeFromSchemaInternal({ ...schema, type: t } , options))
122
+ .map((t) =>
123
+ getTypeFromSchemaInternal(
124
+ { ...schema, type: t } ,
125
+ options,
126
+ `${context}.type`
127
+ )
128
+ )
105
129
  .join(' | ');
106
130
  }
107
131
 
@@ -129,22 +153,23 @@ function getTypeFromOA31ArrayType(schema, options) {
129
153
 
130
154
  function getTypeFromSchemaInternal(
131
155
  schema,
132
- options
156
+ options,
157
+ context
133
158
  ) {
134
159
  const unknownType = options.preferAny ? 'any' : 'unknown';
135
160
 
136
161
  if ('allOf' in schema || 'oneOf' in schema || 'anyOf' in schema) {
137
- return getTypeFromComposites(schema , options);
162
+ return getTypeFromComposites(schema , options, context);
138
163
  }
139
164
 
140
165
  if (schema.type === 'array') {
141
166
  if (schema.items) {
142
- return `${getNestedTypeFromSchema(schema.items, options)}[]`;
167
+ return `${getNestedTypeFromSchema(schema.items, options, `${context}.items`)}[]`;
143
168
  }
144
169
  return `${unknownType}[]`;
145
170
  }
146
171
  if (schema.type === 'object') {
147
- return getTypeFromObject(schema, options);
172
+ return getTypeFromObject(schema, options, undefined, context);
148
173
  }
149
174
  if ('enum' in schema) {
150
175
  return `${schema.enum.map((v) => JSON.stringify(v)).join(' | ')}`;
@@ -166,7 +191,8 @@ function getTypeFromSchemaInternal(
166
191
 
167
192
  function getNestedTypeFromSchema(
168
193
  schema,
169
- options
194
+ options,
195
+ context
170
196
  ) {
171
197
  // OA3.0 nullable: true
172
198
  const isOA30NullableAndActive =
@@ -180,10 +206,10 @@ function getNestedTypeFromSchema(
180
206
  options.nullableStrategy === 'include';
181
207
 
182
208
  if (isOA30NullableAndActive || isOA31NullableAndActive || ('enum' in schema && schema.enum)) {
183
- return `(${getTypeFromSchemaResolved(schema, options)})`;
209
+ return `(${getTypeFromSchemaResolved(schema, options, context)})`;
184
210
  }
185
211
 
186
- return getTypeFromSchemaResolved(schema, options);
212
+ return getTypeFromSchemaResolved(schema, options, context);
187
213
  }
188
214
 
189
215
  /**
@@ -192,20 +218,17 @@ function getNestedTypeFromSchema(
192
218
  */
193
219
  function getTypeFromObject(
194
220
  schema,
195
- options
221
+ options,
222
+ requiredOverride,
223
+ context = 'schema'
196
224
  ) {
197
225
  const unknownType = options.preferAny ? 'any' : 'unknown';
198
-
199
- if (schema.additionalProperties) {
200
- const extraProps = schema.additionalProperties;
201
- return `{ [key: string]: ${
202
- extraProps === true ? 'any' : getTypeFromSchemaResolved(extraProps, options)
203
- } }`;
204
- }
226
+ let objectWithNamedPropsType = '';
227
+ let objectWithIndexSignatureType = '';
205
228
 
206
229
  if (schema.properties) {
207
230
  const props = Object.keys(schema.properties);
208
- const required = schema.required || [];
231
+ const required = _nullishCoalesce(_nullishCoalesce(requiredOverride, () => ( schema.required)), () => ( []));
209
232
  const result = [];
210
233
 
211
234
  for (const prop of props) {
@@ -213,11 +236,36 @@ function getTypeFromObject(
213
236
  const isRequired = required.includes(prop);
214
237
  const safePropName = _utils.escapePropName.call(void 0, prop);
215
238
  result.push(
216
- `${safePropName}${isRequired ? '' : '?'}: ${getTypeFromSchemaResolved(propDefinition, options)};`
239
+ `${safePropName}${isRequired ? '' : '?'}: ${getTypeFromSchemaResolved(
240
+ propDefinition,
241
+ options,
242
+ `${context}.properties.${prop}`
243
+ )};`
217
244
  );
218
245
  }
219
246
 
220
- return `{ ${result.join('\n')} }`;
247
+ objectWithNamedPropsType = `{ ${result.join('\n')} }`;
248
+ }
249
+
250
+ if (schema.additionalProperties) {
251
+ const extraProps = schema.additionalProperties;
252
+ objectWithIndexSignatureType = `{ [key: string]: ${
253
+ extraProps === true
254
+ ? 'any'
255
+ : getTypeFromSchemaResolved(extraProps, options, `${context}.additionalProperties`)
256
+ } }`;
257
+ }
258
+
259
+ if (objectWithNamedPropsType && objectWithIndexSignatureType) {
260
+ return `${objectWithNamedPropsType} & ${objectWithIndexSignatureType}`;
261
+ }
262
+
263
+ if (objectWithNamedPropsType) {
264
+ return objectWithNamedPropsType;
265
+ }
266
+
267
+ if (objectWithIndexSignatureType) {
268
+ return objectWithIndexSignatureType;
221
269
  }
222
270
 
223
271
  return unknownType;
@@ -226,14 +274,76 @@ function getTypeFromObject(
226
274
  /**
227
275
  * Simplified way of extracting correct type from `anyOf`, `oneOf` or `allOf` schema.
228
276
  */
229
- function getTypeFromComposites(schema, options) {
277
+ function getTypeFromComposites(
278
+ schema,
279
+ options,
280
+ context = 'schema'
281
+ ) {
230
282
  const composite = schema.allOf || schema.oneOf || schema.anyOf;
231
283
 
284
+ if (!composite) {
285
+ return options.preferAny ? 'any' : 'unknown';
286
+ }
287
+
288
+ const isUnionComposite = !!schema.oneOf || !!schema.anyOf;
289
+ const hasParentObjectShape = !!schema.properties || !!schema.additionalProperties;
290
+
291
+ if (isUnionComposite && hasParentObjectShape) {
292
+ const fallbackType = options.preferAny ? 'any' : 'unknown';
293
+ const parentObjectType = getTypeFromObject(schema, options, undefined, context);
294
+ const parentRequired = schema.required || [];
295
+ const parentPropSet = new Set(Object.keys(schema.properties || {}));
296
+
297
+ return composite
298
+ .map((subSchema) => {
299
+ if (!('$ref' in subSchema) && isRequiredOnlyCompositeBranch(subSchema)) {
300
+ const branchRequired = subSchema.required || [];
301
+ const validRequired = branchRequired.filter((name) => {
302
+ const isKnown = parentPropSet.has(name);
303
+ if (!isKnown) {
304
+ console.warn(
305
+ `Composite required key '${name}' is not present in schema properties and will be ignored.`
306
+ );
307
+ }
308
+ return isKnown;
309
+ });
310
+
311
+ return getTypeFromObject(
312
+ schema,
313
+ options,
314
+ Array.from(new Set([...parentRequired, ...validRequired])),
315
+ context
316
+ );
317
+ }
318
+
319
+ const subType = getTypeFromSchemaResolved(subSchema, options, `${context}.composite`);
320
+ if (subType === fallbackType) {
321
+ return parentObjectType;
322
+ }
323
+
324
+ return `${parentObjectType} & ${subType}`;
325
+ })
326
+ .join(' | ');
327
+ }
328
+
232
329
  return composite
233
- .map((s) => getTypeFromSchemaResolved(s, options))
330
+ .map((s, index) => getTypeFromSchemaResolved(s, options, `${context}.composite[${index}]`))
234
331
  .join(schema.allOf ? ' & ' : ' | ');
235
332
  }
236
333
 
334
+ function isRequiredOnlyCompositeBranch(schema) {
335
+ return (
336
+ Array.isArray(schema.required) &&
337
+ !schema.type &&
338
+ !schema.properties &&
339
+ !schema.additionalProperties &&
340
+ !schema.allOf &&
341
+ !schema.oneOf &&
342
+ !schema.anyOf &&
343
+ !schema.enum
344
+ );
345
+ }
346
+
237
347
  /**
238
348
  * Escapes name so it can be used as a valid identifier in the generated code.
239
349
  * Component names can contain certain characters that are not allowed in identifiers.
@@ -264,6 +374,7 @@ function getTypeFromComposites(schema, options) {
264
374
  nullableStrategy: 'ignore',
265
375
  generationMode: 'full',
266
376
  schemaDeclarationStyle: 'interface',
377
+ enumDeclarationStyle: 'union',
267
378
  queryParamsSerialization: {
268
379
  allowDots: true,
269
380
  arrayFormat: 'repeat',
@@ -283,6 +394,7 @@ function getTypeFromComposites(schema, options) {
283
394
  nullableStrategy: _nullishCoalesce(opts.nullableStrategy, () => ( exports.APP_DEFAULTS.nullableStrategy)),
284
395
  generationMode: _nullishCoalesce(opts.generationMode, () => ( exports.APP_DEFAULTS.generationMode)),
285
396
  schemaDeclarationStyle: _nullishCoalesce(opts.schemaDeclarationStyle, () => ( exports.APP_DEFAULTS.schemaDeclarationStyle)),
397
+ enumDeclarationStyle: _nullishCoalesce(opts.enumDeclarationStyle, () => ( exports.APP_DEFAULTS.enumDeclarationStyle)),
286
398
  queryParamsSerialization: {
287
399
  ...exports.APP_DEFAULTS.queryParamsSerialization,
288
400
  ...opts.queryParamsSerialization,
package/dist/types.d.ts CHANGED
@@ -33,6 +33,8 @@ export interface ClientOptions {
33
33
  generationMode?: GenerationMode;
34
34
  /** Controls whether object schemas are emitted as interfaces or type aliases */
35
35
  schemaDeclarationStyle?: SchemaDeclarationStyle;
36
+ /** Controls whether plain string enums are emitted as unions or TypeScript enums */
37
+ enumDeclarationStyle?: EnumDeclarationStyle;
36
38
  /** Offers ability to adjust the OpenAPI spec before it is processed */
37
39
  modifiers?: {
38
40
  /** Global-level modifiers for parameter with a given name */
@@ -46,6 +48,7 @@ export interface CliOptions extends FullAppOptions {
46
48
  arrayFormat?: ArrayFormat;
47
49
  mode?: GenerationMode;
48
50
  schemaStyle?: SchemaDeclarationStyle;
51
+ enumStyle?: EnumDeclarationStyle;
49
52
  }
50
53
  export interface FullAppOptions extends ClientOptions {
51
54
  /** Path to the configuration file that contains actual config to be used */
@@ -58,6 +61,7 @@ export type ArrayFormat = 'indices' | 'repeat' | 'brackets';
58
61
  export type NullableStrategy = 'include' | 'nullableAsOptional' | 'ignore';
59
62
  export type GenerationMode = 'full' | 'schemas';
60
63
  export type SchemaDeclarationStyle = 'interface' | 'type';
64
+ export type EnumDeclarationStyle = 'union' | 'enum';
61
65
  /**
62
66
  * Internal options type used throughout the app after `prepareAppOptions` has run.
63
67
  * All fields that have defaults are required here so the rest of the codebase never
@@ -69,6 +73,7 @@ export interface AppOptions extends ClientOptions {
69
73
  nullableStrategy: NullableStrategy;
70
74
  generationMode: GenerationMode;
71
75
  schemaDeclarationStyle: SchemaDeclarationStyle;
76
+ enumDeclarationStyle: EnumDeclarationStyle;
72
77
  queryParamsSerialization: {
73
78
  allowDots: boolean;
74
79
  arrayFormat: ArrayFormat;
@@ -240,12 +240,11 @@ function resolveResponseRef(
240
240
  const sortByKey = (key) => (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0);
241
241
 
242
242
  const orderedContentTypes = [
243
- 'application/json',
244
- 'text/json',
245
243
  'text/plain',
246
244
  'application/x-www-form-urlencoded',
247
245
  'multipart/form-data',
248
246
  ];
247
+ const preferredJsonContentTypes = ['application/json', 'text/json'];
249
248
  function getBestContentType(
250
249
  reqBody
251
250
  ) {
@@ -254,6 +253,19 @@ const orderedContentTypes = [
254
253
  return [null, null];
255
254
  }
256
255
 
256
+ const preferredJsonContentType = preferredJsonContentTypes.find((ct) => contentTypes.includes(ct));
257
+ if (preferredJsonContentType) {
258
+ const typeObject = reqBody.content[preferredJsonContentType];
259
+ const type = getContentType(preferredJsonContentType);
260
+ return [typeObject, type];
261
+ }
262
+
263
+ const jsonLikeContentType = contentTypes.find(isJsonLikeContentType);
264
+ if (jsonLikeContentType) {
265
+ const typeObject = reqBody.content[jsonLikeContentType];
266
+ return [typeObject, 'json'];
267
+ }
268
+
257
269
  const firstContentType = orderedContentTypes.find((ct) => contentTypes.includes(ct));
258
270
  if (firstContentType) {
259
271
  const typeObject = reqBody.content[firstContentType];
@@ -266,6 +278,11 @@ const orderedContentTypes = [
266
278
  return [typeObject, type];
267
279
  } exports.getBestContentType = getBestContentType;
268
280
 
281
+ function isJsonLikeContentType(contentType) {
282
+ const normalized = contentType.split(';')[0].trim().toLowerCase();
283
+ return normalized === 'application/*+json' || /^application\/.+\+json$/.test(normalized);
284
+ }
285
+
269
286
  function getContentType(type) {
270
287
  if (type === 'application/x-www-form-urlencoded') {
271
288
  return 'urlencoded';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swaggie",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Generate a fully typed TypeScript API client from your OpenAPI 3 spec",
5
5
  "author": {
6
6
  "name": "Piotr Dabrowski",
@@ -11,6 +11,21 @@ $config?: RequestInit
11
11
  '<%= parameter.originalName %>': <%= parameter.name %>,
12
12
  <% }); %>})}<% } %>`;
13
13
 
14
+ <% if(it.headers && it.headers.length > 0) { %>
15
+ const { headers: $configHeaders, ...$configRest } = $config ?? {};
16
+ const headers = new Headers({
17
+ <% it.headers.forEach((parameter) => { %>
18
+ <% if (parameter.value) { %>
19
+ '<%= parameter.originalName %>': '<%= parameter.value %>',
20
+ <% } else { %>
21
+ '<%= parameter.originalName %>': <%= parameter.name %> ?? '',
22
+ <% } %>
23
+ <% }); %>
24
+ });
25
+ if ($configHeaders) {
26
+ new Headers($configHeaders).forEach((value, key) => headers.set(key, value));
27
+ }
28
+
14
29
  return fetch(url, {
15
30
  method: '<%= it.method %>',
16
31
  <% if(it.body) { %>
@@ -22,19 +37,24 @@ $config?: RequestInit
22
37
  body: <%= it.body.name %>,
23
38
  <% } %>
24
39
  <% } %>
25
- <% if(it.headers && it.headers.length > 0) { %>
26
- headers: {
27
- <% it.headers.forEach((parameter) => { %>
28
- <% if (parameter.value) { %>
29
- '<%= parameter.originalName %>': '<%= parameter.value %>',
30
- <% } else { %>
31
- '<%= parameter.originalName %>': <%= parameter.name %> ?? '',
32
- <% } %>
33
- <% }); %>
34
- },
40
+ headers,
41
+ ...$configRest,
42
+ })
43
+ <% } else { %>
44
+ return fetch(url, {
45
+ method: '<%= it.method %>',
46
+ <% if(it.body) { %>
47
+ <% if(it.body.contentType === 'json') { %>
48
+ body: JSON.stringify(<%= it.body.name %>),
49
+ <% } else if(it.body.contentType === 'urlencoded') { %>
50
+ body: new URLSearchParams(<%= it.body.name %> as any),
51
+ <% } else { %>
52
+ body: <%= it.body.name %>,
53
+ <% } %>
35
54
  <% } %>
36
55
  ...$config,
37
56
  })
57
+ <% } %>
38
58
  <% if(it.responseContentType === 'binary') { %>
39
59
  .then((response) => response.blob() as Promise<<%~ it.returnType %>>);
40
60
  <% } else if(it.responseContentType === 'text') { %>