@nestia/sdk 14.0.0 → 14.0.1

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.
Files changed (186) hide show
  1. package/README.md +1 -1
  2. package/assets/bundle/distribute/package.json +2 -2
  3. package/assets/bundle/distribute/tsconfig.json +15 -105
  4. package/lib/INestiaConfig.d.ts +19 -10
  5. package/lib/NestiaSdkApplication.js +58 -27
  6. package/lib/NestiaSdkApplication.js.map +1 -1
  7. package/lib/NestiaSwaggerComposer.js +11 -10
  8. package/lib/NestiaSwaggerComposer.js.map +1 -1
  9. package/lib/analyses/ConfigAnalyzer.js +4 -4
  10. package/lib/analyses/ConfigAnalyzer.js.map +1 -1
  11. package/lib/analyses/ParameterNameAnalyzer.d.ts +20 -0
  12. package/lib/analyses/ParameterNameAnalyzer.js +48 -0
  13. package/lib/analyses/ParameterNameAnalyzer.js.map +1 -0
  14. package/lib/analyses/PathAnalyzer.d.ts +30 -0
  15. package/lib/analyses/PathAnalyzer.js +108 -16
  16. package/lib/analyses/PathAnalyzer.js.map +1 -1
  17. package/lib/analyses/ReflectControllerAnalyzer.js +2 -1
  18. package/lib/analyses/ReflectControllerAnalyzer.js.map +1 -1
  19. package/lib/analyses/ReflectHttpOperationAnalyzer.js +7 -7
  20. package/lib/analyses/ReflectHttpOperationAnalyzer.js.map +1 -1
  21. package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js +122 -23
  22. package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js.map +1 -1
  23. package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js +5 -2
  24. package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js.map +1 -1
  25. package/lib/analyses/ReflectMcpOperationAnalyzer.js +5 -2
  26. package/lib/analyses/ReflectMcpOperationAnalyzer.js.map +1 -1
  27. package/lib/analyses/ReflectWebSocketOperationAnalyzer.js +6 -3
  28. package/lib/analyses/ReflectWebSocketOperationAnalyzer.js.map +1 -1
  29. package/lib/analyses/SecurityAnalyzer.d.ts +10 -0
  30. package/lib/analyses/SecurityAnalyzer.js +24 -19
  31. package/lib/analyses/SecurityAnalyzer.js.map +1 -1
  32. package/lib/analyses/SwaggerExampleAnalyzer.d.ts +15 -0
  33. package/lib/analyses/SwaggerExampleAnalyzer.js +20 -0
  34. package/lib/analyses/SwaggerExampleAnalyzer.js.map +1 -0
  35. package/lib/analyses/TypedHttpRouteAnalyzer.js +30 -21
  36. package/lib/analyses/TypedHttpRouteAnalyzer.js.map +1 -1
  37. package/lib/analyses/TypedWebSocketRouteAnalyzer.js +1 -1
  38. package/lib/analyses/TypedWebSocketRouteAnalyzer.js.map +1 -1
  39. package/lib/executable/internal/NestiaSdkWatcher.js +3 -3
  40. package/lib/executable/internal/NestiaSdkWatcher.js.map +1 -1
  41. package/lib/executable/sdk.js +16 -1
  42. package/lib/executable/sdk.js.map +1 -1
  43. package/lib/factories/ExpressionFactory.d.ts +3 -0
  44. package/lib/factories/ExpressionFactory.js +6 -2
  45. package/lib/factories/ExpressionFactory.js.map +1 -1
  46. package/lib/factories/LiteralFactory.js +3 -1
  47. package/lib/factories/LiteralFactory.js.map +1 -1
  48. package/lib/generates/SdkGenerator.js +3 -3
  49. package/lib/generates/SdkGenerator.js.map +1 -1
  50. package/lib/generates/SwaggerGenerator.js +135 -27
  51. package/lib/generates/SwaggerGenerator.js.map +1 -1
  52. package/lib/generates/internal/FilePrinter.d.ts +14 -0
  53. package/lib/generates/internal/FilePrinter.js +31 -0
  54. package/lib/generates/internal/FilePrinter.js.map +1 -1
  55. package/lib/generates/internal/ImportDictionary.d.ts +2 -0
  56. package/lib/generates/internal/ImportDictionary.js +13 -0
  57. package/lib/generates/internal/ImportDictionary.js.map +1 -1
  58. package/lib/generates/internal/SdkAliasCollection.d.ts +2 -1
  59. package/lib/generates/internal/SdkAliasCollection.js +8 -4
  60. package/lib/generates/internal/SdkAliasCollection.js.map +1 -1
  61. package/lib/generates/internal/SdkDistributionComposer.js +72 -14
  62. package/lib/generates/internal/SdkDistributionComposer.js.map +1 -1
  63. package/lib/generates/internal/SdkFileProgrammer.js +83 -12
  64. package/lib/generates/internal/SdkFileProgrammer.js.map +1 -1
  65. package/lib/generates/internal/SdkHttpFunctionProgrammer.js +21 -23
  66. package/lib/generates/internal/SdkHttpFunctionProgrammer.js.map +1 -1
  67. package/lib/generates/internal/SdkHttpNamespaceProgrammer.js +27 -34
  68. package/lib/generates/internal/SdkHttpNamespaceProgrammer.js.map +1 -1
  69. package/lib/generates/internal/SdkHttpParameterProgrammer.d.ts +40 -1
  70. package/lib/generates/internal/SdkHttpParameterProgrammer.js +105 -6
  71. package/lib/generates/internal/SdkHttpParameterProgrammer.js.map +1 -1
  72. package/lib/generates/internal/SdkHttpRouteProgrammer.js +16 -32
  73. package/lib/generates/internal/SdkHttpRouteProgrammer.js.map +1 -1
  74. package/lib/generates/internal/SdkHttpSimulationProgrammer.js +18 -14
  75. package/lib/generates/internal/SdkHttpSimulationProgrammer.js.map +1 -1
  76. package/lib/generates/internal/SdkMcpRouteProgrammer.js +39 -11
  77. package/lib/generates/internal/SdkMcpRouteProgrammer.js.map +1 -1
  78. package/lib/generates/internal/SdkPathTemplate.d.ts +21 -0
  79. package/lib/generates/internal/SdkPathTemplate.js +50 -0
  80. package/lib/generates/internal/SdkPathTemplate.js.map +1 -0
  81. package/lib/generates/internal/SdkTypeProgrammer.js +15 -15
  82. package/lib/generates/internal/SdkTypeProgrammer.js.map +1 -1
  83. package/lib/generates/internal/SdkTypeTagProgrammer.d.ts +20 -1
  84. package/lib/generates/internal/SdkTypeTagProgrammer.js +386 -67
  85. package/lib/generates/internal/SdkTypeTagProgrammer.js.map +1 -1
  86. package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js +27 -36
  87. package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js.map +1 -1
  88. package/lib/generates/internal/SdkWebSocketParameterProgrammer.d.ts +35 -1
  89. package/lib/generates/internal/SdkWebSocketParameterProgrammer.js +72 -5
  90. package/lib/generates/internal/SdkWebSocketParameterProgrammer.js.map +1 -1
  91. package/lib/generates/internal/SdkWebSocketRouteProgrammer.js +57 -33
  92. package/lib/generates/internal/SdkWebSocketRouteProgrammer.js.map +1 -1
  93. package/lib/generates/internal/SwaggerOperationComposer.js +19 -8
  94. package/lib/generates/internal/SwaggerOperationComposer.js.map +1 -1
  95. package/lib/generates/internal/SwaggerOperationParameterComposer.d.ts +1 -1
  96. package/lib/generates/internal/SwaggerOperationParameterComposer.js +201 -19
  97. package/lib/generates/internal/SwaggerOperationParameterComposer.js.map +1 -1
  98. package/lib/generates/internal/SwaggerOperationResponseComposer.d.ts +2 -0
  99. package/lib/generates/internal/SwaggerOperationResponseComposer.js +39 -7
  100. package/lib/generates/internal/SwaggerOperationResponseComposer.js.map +1 -1
  101. package/lib/internal/legacy.d.ts +49 -18
  102. package/lib/internal/legacy.js +84 -25
  103. package/lib/internal/legacy.js.map +1 -1
  104. package/lib/structures/IOperationMetadata.d.ts +22 -0
  105. package/lib/structures/IReflectHttpOperationException.d.ts +3 -2
  106. package/lib/structures/IReflectHttpOperationParameter.d.ts +8 -3
  107. package/lib/structures/IReflectHttpOperationSuccess.d.ts +4 -3
  108. package/lib/structures/ITypedHttpRoute.d.ts +8 -0
  109. package/lib/structures/ITypedHttpRouteException.d.ts +3 -1
  110. package/lib/structures/ITypedHttpRouteParameter.d.ts +3 -1
  111. package/lib/structures/ITypedHttpRouteSuccess.d.ts +3 -1
  112. package/lib/structures/ITypedWebSocketRoute.d.ts +8 -0
  113. package/lib/utils/SourceFinder.d.ts +10 -0
  114. package/lib/utils/SourceFinder.js +27 -4
  115. package/lib/utils/SourceFinder.js.map +1 -1
  116. package/lib/utils/VersioningStrategy.d.ts +13 -1
  117. package/lib/utils/VersioningStrategy.js +24 -12
  118. package/lib/utils/VersioningStrategy.js.map +1 -1
  119. package/native/go.mod +1 -1
  120. package/native/go.sum +2 -2
  121. package/native/sdk/sdk_http_rules.go +210 -0
  122. package/native/sdk/sdk_metadata_json.go +73 -3
  123. package/native/sdk/sdk_transform.go +801 -49
  124. package/package.json +6 -6
  125. package/src/INestiaConfig.ts +19 -10
  126. package/src/NestiaSdkApplication.ts +63 -34
  127. package/src/NestiaSwaggerComposer.ts +11 -8
  128. package/src/analyses/ConfigAnalyzer.ts +3 -3
  129. package/src/analyses/ParameterNameAnalyzer.ts +51 -0
  130. package/src/analyses/PathAnalyzer.ts +106 -14
  131. package/src/analyses/ReflectControllerAnalyzer.ts +2 -1
  132. package/src/analyses/ReflectHttpOperationAnalyzer.ts +4 -4
  133. package/src/analyses/ReflectHttpOperationParameterAnalyzer.ts +122 -39
  134. package/src/analyses/ReflectHttpOperationResponseAnalyzer.ts +6 -7
  135. package/src/analyses/ReflectMcpOperationAnalyzer.ts +6 -2
  136. package/src/analyses/ReflectWebSocketOperationAnalyzer.ts +7 -3
  137. package/src/analyses/SecurityAnalyzer.ts +30 -20
  138. package/src/analyses/SwaggerExampleAnalyzer.ts +23 -0
  139. package/src/analyses/TypedHttpRouteAnalyzer.ts +29 -20
  140. package/src/analyses/TypedWebSocketRouteAnalyzer.ts +1 -0
  141. package/src/executable/internal/NestiaSdkWatcher.ts +2 -2
  142. package/src/executable/sdk.ts +24 -1
  143. package/src/factories/ExpressionFactory.ts +8 -2
  144. package/src/factories/LiteralFactory.ts +7 -2
  145. package/src/generates/SdkGenerator.ts +3 -3
  146. package/src/generates/SwaggerGenerator.ts +173 -32
  147. package/src/generates/internal/FilePrinter.ts +28 -0
  148. package/src/generates/internal/ImportDictionary.ts +11 -0
  149. package/src/generates/internal/SdkAliasCollection.ts +11 -4
  150. package/src/generates/internal/SdkDistributionComposer.ts +89 -15
  151. package/src/generates/internal/SdkFileProgrammer.ts +108 -16
  152. package/src/generates/internal/SdkHttpFunctionProgrammer.ts +28 -30
  153. package/src/generates/internal/SdkHttpNamespaceProgrammer.ts +34 -64
  154. package/src/generates/internal/SdkHttpParameterProgrammer.ts +193 -13
  155. package/src/generates/internal/SdkHttpRouteProgrammer.ts +17 -31
  156. package/src/generates/internal/SdkHttpSimulationProgrammer.ts +51 -14
  157. package/src/generates/internal/SdkMcpRouteProgrammer.ts +112 -15
  158. package/src/generates/internal/SdkPathTemplate.ts +73 -0
  159. package/src/generates/internal/SdkTypeProgrammer.ts +19 -22
  160. package/src/generates/internal/SdkTypeTagProgrammer.ts +506 -78
  161. package/src/generates/internal/SdkWebSocketNamespaceProgrammer.ts +43 -68
  162. package/src/generates/internal/SdkWebSocketParameterProgrammer.ts +126 -5
  163. package/src/generates/internal/SdkWebSocketRouteProgrammer.ts +71 -48
  164. package/src/generates/internal/SwaggerOperationComposer.ts +20 -6
  165. package/src/generates/internal/SwaggerOperationParameterComposer.ts +279 -30
  166. package/src/generates/internal/SwaggerOperationResponseComposer.ts +55 -11
  167. package/src/internal/legacy.ts +114 -26
  168. package/src/structures/IOperationMetadata.ts +28 -0
  169. package/src/structures/IReflectHttpOperationException.ts +7 -2
  170. package/src/structures/IReflectHttpOperationParameter.ts +9 -2
  171. package/src/structures/IReflectHttpOperationSuccess.ts +8 -3
  172. package/src/structures/ITypedHttpRoute.ts +8 -0
  173. package/src/structures/ITypedHttpRouteException.ts +4 -1
  174. package/src/structures/ITypedHttpRouteParameter.ts +3 -1
  175. package/src/structures/ITypedHttpRouteSuccess.ts +4 -1
  176. package/src/structures/ITypedWebSocketRoute.ts +8 -0
  177. package/src/utils/SourceFinder.ts +25 -4
  178. package/src/utils/VersioningStrategy.ts +28 -13
  179. package/lib/validators/HttpHeadersValidator.d.ts +0 -11
  180. package/lib/validators/HttpHeadersValidator.js +0 -15
  181. package/lib/validators/HttpHeadersValidator.js.map +0 -1
  182. package/lib/validators/HttpQueryValidator.d.ts +0 -10
  183. package/lib/validators/HttpQueryValidator.js +0 -14
  184. package/lib/validators/HttpQueryValidator.js.map +0 -1
  185. package/src/validators/HttpHeadersValidator.ts +0 -12
  186. package/src/validators/HttpQueryValidator.ts +0 -11
@@ -22,7 +22,7 @@ export namespace SwaggerOperationComposer {
22
22
  ...SwaggerDescriptionComposer.getJsDocTexts({
23
23
  jsDocTags: props.route.jsDocTags,
24
24
  name: "tag",
25
- }).map((t) => t.split(" ")[0]!),
25
+ }).map((t) => t.trim().split(/\s+/)[0]!),
26
26
  ]);
27
27
  if (tags.size) {
28
28
  props.document.tags ??= [];
@@ -33,11 +33,13 @@ export namespace SwaggerOperationComposer {
33
33
  jsDocTags: props.route.jsDocTags,
34
34
  name: "tag",
35
35
  })) {
36
- const [name, ...description] = texts.split(" ");
36
+ // the name is the first word; a tag's text may run over lines
37
+ const name: string = texts.trim().split(/\s+/)[0]!;
38
+ const description: string = texts.trim().substring(name.length).trim();
37
39
  if (description.length)
38
40
  props.document.tags.find(
39
41
  (elem) => elem.name === name,
40
- )!.description ??= description.join(" ");
42
+ )!.description ??= description;
41
43
  }
42
44
  }
43
45
 
@@ -52,8 +54,7 @@ export namespace SwaggerOperationComposer {
52
54
  ? [{}]
53
55
  : tag.text.map((text) => {
54
56
  const line: string[] = text.text
55
- .split(" ")
56
- .filter((s) => s.trim())
57
+ .split(/\s+/)
57
58
  .filter((s) => !!s.length);
58
59
  if (line.length === 0) return {};
59
60
  return {
@@ -79,7 +80,7 @@ export namespace SwaggerOperationComposer {
79
80
  props.route.operationId ??
80
81
  props.config.operationId?.({
81
82
  class: props.route.controller.class.name,
82
- function: props.route.name,
83
+ function: props.route.key,
83
84
  method: props.route.method as "GET",
84
85
  path: props.route.path,
85
86
  }),
@@ -87,6 +88,7 @@ export namespace SwaggerOperationComposer {
87
88
  ...props.route.pathParameters,
88
89
  ...props.route.queryParameters,
89
90
  ...(props.route.queryObject ? [props.route.queryObject] : []),
91
+ ...props.route.headerParameters,
90
92
  ...(props.route.headerObject ? [props.route.headerObject] : []),
91
93
  ]
92
94
  .map((p) =>
@@ -101,6 +103,7 @@ export namespace SwaggerOperationComposer {
101
103
  .flat(),
102
104
  requestBody: writeRequestBody(props),
103
105
  responses: SwaggerOperationResponseComposer.compose({
106
+ config: props.config,
104
107
  schema: props.schema,
105
108
  route: props.route,
106
109
  }),
@@ -108,6 +111,13 @@ export namespace SwaggerOperationComposer {
108
111
  ...(props.route.extensions ?? {}),
109
112
  "x-samchon-accessor": props.route.accessor,
110
113
  "x-samchon-controller": props.route.controller.class.name,
114
+ ...(props.config.additional === true
115
+ ? {
116
+ "x-nestia-method": props.route.method,
117
+ "x-nestia-namespace": props.route.accessor.join("."),
118
+ "x-nestia-jsDocTags": props.route.jsDocTags,
119
+ }
120
+ : {}),
111
121
  };
112
122
  };
113
123
  }
@@ -135,6 +145,8 @@ export namespace SwaggerOperationComposer {
135
145
  * helper across two rules that disagree would trade one defect for another.
136
146
  */
137
147
  const writeRequestBody = (props: {
148
+ config: Omit<INestiaConfig.ISwaggerConfig, "output">;
149
+ document: OpenApi.IDocument;
138
150
  schema: (metadata: MetadataSchema) => OpenApi.IJsonSchema | undefined;
139
151
  route: ITypedHttpRoute;
140
152
  }): OpenApi.IOperation.IRequestBody | undefined => {
@@ -144,6 +156,8 @@ const writeRequestBody = (props: {
144
156
  );
145
157
  if (schema === undefined) return undefined;
146
158
  return SwaggerOperationParameterComposer.body({
159
+ config: props.config,
160
+ document: props.document,
147
161
  schema,
148
162
  jsDocTags: props.route.jsDocTags,
149
163
  parameter: props.route.body,
@@ -1,4 +1,5 @@
1
1
  import { OpenApi } from "@typia/interface";
2
+ import { OpenApiTypeChecker } from "@typia/utils";
2
3
  import { VariadicSingleton } from "tstl";
3
4
  import { IJsDocTagInfo, IJsonSchemaCollection } from "typia";
4
5
 
@@ -6,7 +7,9 @@ import { INestiaConfig } from "../../INestiaConfig";
6
7
  import {
7
8
  JsonSchemasProgrammer,
8
9
  MetadataObjectType,
10
+ MetadataProperty,
9
11
  isRequiredOf,
12
+ isSoleLiteralOf,
10
13
  } from "../../internal/legacy";
11
14
  import { ITypedHttpRouteParameter } from "../../structures/ITypedHttpRouteParameter";
12
15
  import { SwaggerDescriptionComposer } from "./SwaggerDescriptionComposer";
@@ -33,8 +36,8 @@ export namespace SwaggerOperationParameterComposer {
33
36
  : header({ ...props, parameter: props.parameter });
34
37
 
35
38
  export const body = (
36
- props: Omit<IProps<ITypedHttpRouteParameter.IBody>, "config" | "document">,
37
- ): OpenApi.IOperation.IRequestBody => {
39
+ props: IProps<ITypedHttpRouteParameter.IBody>,
40
+ ): OpenApi.IOperation.IRequestBody | undefined => {
38
41
  const description: string | undefined =
39
42
  props.parameter.description ??
40
43
  SwaggerDescriptionComposer.descriptionFromJsDocTag({
@@ -42,7 +45,7 @@ export namespace SwaggerOperationParameterComposer {
42
45
  tag: "param",
43
46
  parameter: props.parameter.name,
44
47
  });
45
- return {
48
+ const output: OpenApi.IOperation.IRequestBody = {
46
49
  description: props.parameter.encrypted
47
50
  ? `${warning.get(!!description)}${description ?? ""}`
48
51
  : description,
@@ -56,6 +59,68 @@ export namespace SwaggerOperationParameterComposer {
56
59
  required: props.parameter.metadata.required,
57
60
  ...(props.parameter.encrypted ? { "x-nestia-encrypted": true } : {}),
58
61
  };
62
+ return props.config.openapi === "2.0"
63
+ ? swaggerV2Body(props, output)
64
+ : output;
65
+ };
66
+
67
+ /**
68
+ * The request body a Swagger 2.0 document can hold.
69
+ *
70
+ * Typia's downgrader refuses what 2.0 has no place for rather than lose it,
71
+ * so one route used to fail the whole document (#1649). A 2.0 body parameter
72
+ * holds only a schema: no example, and no `x-nestia-encrypted` flag, whose
73
+ * warning the description keeps. A form becomes one `formData` parameter per
74
+ * field, which carry neither the body's description nor the form object's own
75
+ * attributes, and are each required or not; so a form keeps only its fields,
76
+ * is required when one of them is, and without fields has no body to list. A
77
+ * `formData` file is one file, never an array, a union, or null, so a field
78
+ * taking several files, or none, is listed as one file, optional unless it
79
+ * requires one ({@link swaggerV2FormField}).
80
+ */
81
+ const swaggerV2Body = (
82
+ props: IProps<ITypedHttpRouteParameter.IBody>,
83
+ body: OpenApi.IOperation.IRequestBody,
84
+ ): OpenApi.IOperation.IRequestBody | undefined => {
85
+ const contentType: string = props.parameter.contentType;
86
+ if (
87
+ contentType !== "multipart/form-data" &&
88
+ contentType !== "application/x-www-form-urlencoded"
89
+ )
90
+ return {
91
+ description: body.description,
92
+ content: { [contentType]: { schema: props.schema } },
93
+ required: body.required,
94
+ };
95
+ const object: OpenApi.IJsonSchema.IObject | undefined = resolveObject(
96
+ props.document,
97
+ props.schema,
98
+ );
99
+ if (
100
+ object === undefined ||
101
+ Object.keys(object.properties ?? {}).length === 0
102
+ )
103
+ return undefined;
104
+ const fields: [string, ISwaggerV2FormField][] = Object.entries(
105
+ object.properties ?? {},
106
+ ).map(([key, value]) => [key, swaggerV2FormField(value)]);
107
+ const required: string[] = (object.required ?? []).filter(
108
+ (key) => fields.find(([name]) => name === key)?.[1].optional !== true,
109
+ );
110
+ return {
111
+ content: {
112
+ [contentType]: {
113
+ schema: {
114
+ type: "object",
115
+ properties: Object.fromEntries(
116
+ fields.map(([key, field]) => [key, field.schema]),
117
+ ),
118
+ required,
119
+ },
120
+ },
121
+ },
122
+ required: required.length !== 0,
123
+ };
59
124
  };
60
125
 
61
126
  const path = (
@@ -103,48 +168,76 @@ export namespace SwaggerOperationParameterComposer {
103
168
  ITypedHttpRouteParameter.IHeaders | ITypedHttpRouteParameter.IQuery
104
169
  >,
105
170
  ): OpenApi.IOperation.IParameter[] => {
171
+ const object: MetadataObjectType | undefined = props.parameter.metadata
172
+ .objects[0]?.type as MetadataObjectType | undefined;
106
173
  const param: OpenApi.IOperation.IParameter = {
107
174
  name: props.parameter.field ?? props.parameter.name,
108
175
  in: props.parameter.category === "query" ? "query" : "header",
109
176
  schema: props.schema,
110
177
  description: parameterDescription(props),
111
- required: props.parameter.metadata.required,
178
+ // An unnamed object's keys are the request's own query keys or headers,
179
+ // so it is mandatory only when one of the keys the document describes
180
+ // is; an object whose described properties are all optional is satisfied
181
+ // when the request sends none of them. A field-named parameter is one
182
+ // key, required as declared.
183
+ required:
184
+ props.parameter.metadata.required &&
185
+ (props.parameter.field !== null ||
186
+ (object?.properties.some(
187
+ (p) => isDescribed(p) && isRequiredOf(p.value),
188
+ ) ??
189
+ true)),
112
190
  example: props.parameter.example,
113
191
  examples: props.parameter.examples,
114
192
  };
193
+ // A field-named parameter is one key, which typia's HTTP rules (#1648)
194
+ // keep atomic, and a query or headers object passed them: one object of
195
+ // statically named atomic or array-of-atomic properties, so each has a
196
+ // name to give its parameter.
197
+ if (props.parameter.field !== null || object === undefined) return [param];
198
+ // `decompose: false` keeps a query object one parameter where the format
199
+ // can say so: OpenAPI 3.x spreads it into its keys with `style: form` and
200
+ // `explode: true`. Swagger 2.0 has no object query parameter, and no
201
+ // format spreads an object into headers, so one parameter would be a key
202
+ // or header no request carries (#1653); those are always decomposed.
115
203
  if (
116
- props.config.decompose === false ||
117
- props.parameter.metadata.objects.length === 0
204
+ props.config.decompose === false &&
205
+ props.parameter.category === "query" &&
206
+ props.config.openapi !== "2.0"
118
207
  )
119
- return [param];
120
- return (
121
- props.parameter.metadata.objects[0]!.type as MetadataObjectType
122
- ).properties
123
- .filter((p) =>
124
- p.jsDocTags.every(
125
- (tag) => tag.name !== "hidden" && tag.name !== "ignore",
126
- ),
127
- )
128
- .map((p) => {
129
- const json: IJsonSchemaCollection = JsonSchemasProgrammer.writeSchemas({
130
- version: "3.1",
131
- metadatas: [p.value],
132
- }) as IJsonSchemaCollection;
208
+ return [{ ...param, style: "form", explode: true }];
209
+ // One parameter per property typia's object schema describes, so the
210
+ // decomposed form says what `decompose: false` would say about the object:
211
+ // the property's schema, and in the parameter's own fields its
212
+ // description, deprecation, and share of the object's examples.
213
+ return object.properties
214
+ .filter(isDescribed)
215
+ .map((p): IDecomposedParameter | null => {
216
+ const key: string = String(p.key.constants[0]!.values[0]!.value);
217
+ const json: IJsonSchemaCollection | null =
218
+ JsonSchemasProgrammer.writeProperty({
219
+ version: "3.1",
220
+ metadata: props.parameter.metadata,
221
+ key,
222
+ value: p.value,
223
+ });
224
+ if (json === null) return null;
133
225
  SwaggerReadonlyArrayEmender.emend({
134
226
  components: json.components,
135
227
  schema: json.schemas[0],
136
228
  metadata: p.value,
137
229
  });
138
- if (Object.keys(json.components.schemas ?? {}).length !== 0) {
139
- props.document.components ??= {};
140
- props.document.components.schemas ??= {};
141
- Object.assign(
142
- props.document.components.schemas,
143
- json.components.schemas,
144
- );
145
- }
230
+ // Only what the document lacks: the parameter's own schema already
231
+ // brought every component its properties reach, emended for readonly
232
+ // arrays, and this copy is not.
233
+ props.document.components ??= {};
234
+ props.document.components.schemas ??= {};
235
+ for (const [name, schema] of Object.entries(
236
+ json.components.schemas ?? {},
237
+ ))
238
+ props.document.components.schemas[name] ??= schema;
146
239
  return {
147
- name: p.key.constants[0]!.values[0]!.value as string,
240
+ name: key,
148
241
  in: props.parameter.category === "query" ? "query" : "header",
149
242
  schema: json.schemas[0]!,
150
243
  required: isRequiredOf(p.value),
@@ -153,11 +246,167 @@ export namespace SwaggerOperationParameterComposer {
153
246
  jsDocTags: p.jsDocTags,
154
247
  kind: "title",
155
248
  }).description,
249
+ // Swagger 2.0 defines no `deprecated` on a parameter, and the
250
+ // downgrader copies the field through rather than refusing it.
251
+ deprecated:
252
+ props.config.openapi !== "2.0" &&
253
+ p.jsDocTags.some((tag) => tag.name === "deprecated")
254
+ ? true
255
+ : undefined,
256
+ example: memberOf(props.parameter.example, key),
257
+ examples: membersOf(props.parameter.examples, key),
156
258
  };
157
- });
259
+ })
260
+ .filter((p): p is IDecomposedParameter => p !== null);
261
+ };
262
+ }
263
+
264
+ /**
265
+ * A decomposed parameter. OpenAPI 3.0 through 3.2 define `deprecated` on the
266
+ * Parameter Object, which typia's `OpenApi.IOperation.IParameter` does not
267
+ * model; its 3.x downgraders carry the field through unchanged.
268
+ */
269
+ type IDecomposedParameter = OpenApi.IOperation.IParameter & {
270
+ deprecated?: boolean;
271
+ };
272
+
273
+ /**
274
+ * Whether the document can describe a property on its own: it has a literal key
275
+ * to name it, and none of the `@hidden`, `@ignore`, and `@internal` tags
276
+ * typia's object schema drops it for.
277
+ */
278
+ const isDescribed = (p: MetadataProperty): boolean =>
279
+ isSoleLiteralOf(p.key) &&
280
+ p.jsDocTags.every(
281
+ (tag) =>
282
+ tag.name !== "hidden" && tag.name !== "ignore" && tag.name !== "internal",
283
+ );
284
+
285
+ /** A form field as a Swagger 2.0 `formData` parameter holds it. */
286
+ interface ISwaggerV2FormField {
287
+ schema: OpenApi.IJsonSchema;
288
+
289
+ /** Whether a request may leave the field out though its type requires it. */
290
+ optional: boolean;
291
+ }
292
+
293
+ /**
294
+ * A form field as a Swagger 2.0 `formData` parameter can hold it.
295
+ *
296
+ * A 2.0 file is one file: never an array, a union, or null. A field whose value
297
+ * is made of files, a union of files or one of them or null, or an array of
298
+ * those, becomes the one file a request may carry, keeping the field's title,
299
+ * description, and deprecation. A request may then carry none, since the server
300
+ * reads a missing field as null or an empty array, so such a field is not
301
+ * required unless its array requires an item; a plain file is kept as it is.
302
+ */
303
+ const swaggerV2FormField = (
304
+ schema: OpenApi.IJsonSchema,
305
+ ): ISwaggerV2FormField => {
306
+ const file: IFileOf | undefined = fileOf(schema, true);
307
+ if (file === undefined || file.binary === schema)
308
+ return { schema, optional: false };
309
+ const attributes = schema as OpenApi.IJsonSchema.IString;
310
+ return {
311
+ schema: {
312
+ ...file.binary,
313
+ ...(attributes.title !== undefined ? { title: attributes.title } : {}),
314
+ ...(attributes.description !== undefined
315
+ ? { description: attributes.description }
316
+ : {}),
317
+ ...(attributes.deprecated !== undefined
318
+ ? { deprecated: attributes.deprecated }
319
+ : {}),
320
+ },
321
+ optional: file.optional,
158
322
  };
323
+ };
324
+
325
+ interface IFileOf {
326
+ binary: OpenApi.IJsonSchema;
327
+ optional: boolean;
159
328
  }
160
329
 
330
+ /** The file a field's value is made of, if it is made of files only. */
331
+ const fileOf = (
332
+ schema: OpenApi.IJsonSchema,
333
+ top: boolean,
334
+ ): IFileOf | undefined => {
335
+ if (isBinary(schema)) return { binary: schema, optional: false };
336
+ if (OpenApiTypeChecker.isOneOf(schema)) {
337
+ const members: OpenApi.IJsonSchema[] = schema.oneOf.filter(
338
+ (s) => OpenApiTypeChecker.isNull(s) === false,
339
+ );
340
+ const files: Array<IFileOf | undefined> = members.map((s) =>
341
+ fileOf(s, top),
342
+ );
343
+ if (files.length === 0 || files.some((f) => f === undefined))
344
+ return undefined;
345
+ return {
346
+ binary: files[0]!.binary,
347
+ optional:
348
+ members.length !== schema.oneOf.length ||
349
+ files.some((f) => f!.optional),
350
+ };
351
+ }
352
+ if (top && OpenApiTypeChecker.isArray(schema)) {
353
+ const item: IFileOf | undefined = fileOf(schema.items, false);
354
+ // an empty array is a request without the field, unless one file is due
355
+ return item === undefined
356
+ ? undefined
357
+ : { binary: item.binary, optional: (schema.minItems ?? 0) === 0 };
358
+ }
359
+ return undefined;
360
+ };
361
+
362
+ const isBinary = (schema: OpenApi.IJsonSchema): boolean =>
363
+ OpenApiTypeChecker.isString(schema) && schema.format === "binary";
364
+
365
+ /** The object schema `schema` is or refers to, if it is one. */
366
+ const resolveObject = (
367
+ document: OpenApi.IDocument,
368
+ schema: OpenApi.IJsonSchema,
369
+ ): OpenApi.IJsonSchema.IObject | undefined => {
370
+ const visited: Set<string> = new Set();
371
+ while (OpenApiTypeChecker.isReference(schema)) {
372
+ if (visited.has(schema.$ref)) return undefined;
373
+ visited.add(schema.$ref);
374
+ const next: OpenApi.IJsonSchema | undefined =
375
+ document.components?.schemas?.[
376
+ schema.$ref.substring("#/components/schemas/".length)
377
+ ];
378
+ if (next === undefined) return undefined;
379
+ schema = next;
380
+ }
381
+ return OpenApiTypeChecker.isObject(schema) ? schema : undefined;
382
+ };
383
+
384
+ /** The `key` member of an object example, if the example has one. */
385
+ const memberOf = (example: unknown, key: string): unknown =>
386
+ typeof example === "object" &&
387
+ example !== null &&
388
+ Object.prototype.hasOwnProperty.call(example, key)
389
+ ? (example as Record<string, unknown>)[key]
390
+ : undefined;
391
+
392
+ /**
393
+ * Named object examples narrowed to the ones whose value has a `key` member,
394
+ * each an Example Object of that member.
395
+ */
396
+ const membersOf = (
397
+ examples: Record<string, OpenApi.IExample> | undefined,
398
+ key: string,
399
+ ): Record<string, OpenApi.IExample> | undefined => {
400
+ if (examples === undefined) return undefined;
401
+ const entries: [string, OpenApi.IExample][] = Object.entries(examples)
402
+ .map(([name, example]): [string, OpenApi.IExample] => [
403
+ name,
404
+ { ...example, value: memberOf(example.value, key) },
405
+ ])
406
+ .filter(([, example]) => example.value !== undefined);
407
+ return entries.length !== 0 ? Object.fromEntries(entries) : undefined;
408
+ };
409
+
161
410
  const warning = new VariadicSingleton((described: boolean): string => {
162
411
  const summary = "Request body must be encrypted.";
163
412
  const component =
@@ -1,6 +1,7 @@
1
1
  import { OpenApi } from "@typia/interface";
2
2
  import { VariadicSingleton } from "tstl";
3
3
 
4
+ import { INestiaConfig } from "../../INestiaConfig";
4
5
  import { MetadataSchema } from "../../internal/legacy";
5
6
  import { ITypedHttpRoute } from "../../structures/ITypedHttpRoute";
6
7
  import { StringUtil } from "../../utils/StringUtil";
@@ -8,9 +9,17 @@ import { SwaggerDescriptionComposer } from "./SwaggerDescriptionComposer";
8
9
 
9
10
  export namespace SwaggerOperationResponseComposer {
10
11
  export const compose = (props: {
12
+ config: Omit<INestiaConfig.ISwaggerConfig, "output">;
11
13
  schema: (metadata: MetadataSchema) => OpenApi.IJsonSchema | undefined;
12
14
  route: ITypedHttpRoute;
13
15
  }): Record<string, OpenApi.IOperation.IResponse> => {
16
+ // Swagger 2.0 keys a response's examples by MIME type, one each, so it has
17
+ // no named examples; the downgrader refuses the whole document rather than
18
+ // lose them (#1649). The single example still fits.
19
+ const named = (
20
+ examples: Record<string, OpenApi.IExample> | undefined,
21
+ ): Record<string, OpenApi.IExample> | undefined =>
22
+ props.config.openapi === "2.0" ? undefined : examples;
14
23
  const output: Record<string, OpenApi.IOperation.IResponse> = {};
15
24
  // FROM DECORATOR
16
25
  for (const [status, error] of Object.entries(props.route.exceptions))
@@ -20,7 +29,7 @@ export namespace SwaggerOperationResponseComposer {
20
29
  contentType: "application/json",
21
30
  schema: props.schema(error.metadata),
22
31
  example: error.example,
23
- examples: error.examples,
32
+ examples: named(error.examples),
24
33
  }),
25
34
  };
26
35
 
@@ -32,8 +41,8 @@ export namespace SwaggerOperationResponseComposer {
32
41
  )?.text;
33
42
  if (text === undefined) continue;
34
43
 
35
- const elements: string[] = text.split(" ").map((str) => str.trim());
36
- const status: string = elements[0]!;
44
+ // the status is the first word; a tag's text may run over lines
45
+ const status: string = text.trim().split(/\s+/)[0]!;
37
46
  if (
38
47
  isNaN(Number(status)) &&
39
48
  status !== "2XX" &&
@@ -43,8 +52,8 @@ export namespace SwaggerOperationResponseComposer {
43
52
  )
44
53
  continue;
45
54
 
46
- const description: string | undefined =
47
- elements.length === 1 ? undefined : elements.slice(1).join(" ");
55
+ const rest: string = text.trim().substring(status.length).trim();
56
+ const description: string | undefined = rest.length ? rest : undefined;
48
57
  const oldbie = output[status];
49
58
  if (description && oldbie !== undefined)
50
59
  oldbie.description ??= description;
@@ -75,10 +84,11 @@ export namespace SwaggerOperationResponseComposer {
75
84
  jsDocTags: props.route.jsDocTags,
76
85
  tag: "return",
77
86
  });
78
- output[
87
+ const status: string = String(
79
88
  props.route.success.status ??
80
- (props.route.method.toLowerCase() === "post" ? 201 : 200)
81
- ] = {
89
+ (props.route.method.toLowerCase() === "post" ? 201 : 200),
90
+ );
91
+ output[status] = {
82
92
  description: props.route.success.encrypted
83
93
  ? `${warning.get(!!description, props.route.method)}${description ?? ""}`
84
94
  : (description ?? ""),
@@ -91,12 +101,46 @@ export namespace SwaggerOperationResponseComposer {
91
101
  }
92
102
  : props.schema(props.route.success.metadata),
93
103
  example: props.route.success.example,
94
- examples: props.route.success.examples,
104
+ examples: named(props.route.success.examples),
95
105
  }),
96
- ...(props.route.success.encrypted ? { "x-nestia-encrypted": true } : {}),
106
+ // Swagger 2.0 has no place for the flag, and the downgrader refuses the
107
+ // whole document rather than lose it; the description keeps the warning.
108
+ ...(props.route.success.encrypted && props.config.openapi !== "2.0"
109
+ ? { "x-nestia-encrypted": true }
110
+ : {}),
97
111
  };
112
+ if (props.config.openapi === "2.0") swaggerV2Produces(output, status);
98
113
  return output;
99
114
  };
115
+
116
+ /**
117
+ * Makes every response body of a Swagger 2.0 operation share one media type.
118
+ *
119
+ * 2.0 declares the media types an operation `produces` once for all of its
120
+ * responses, and the downgrader refuses an operation whose responses disagree
121
+ * rather than list a type a response does not use (#1649). A route whose
122
+ * success body is not JSON, such as an encrypted or a query-string one, still
123
+ * documents its exceptions as JSON, so where the success body has another
124
+ * type, the exceptions keep their status and description without a body.
125
+ */
126
+ const swaggerV2Produces = (
127
+ output: Record<string, OpenApi.IOperation.IResponse>,
128
+ success: string,
129
+ ): void => {
130
+ const kept: string | undefined = mediaOf(output[success]!);
131
+ if (kept === undefined) return;
132
+ for (const [status, response] of Object.entries(output))
133
+ if (status !== success && mediaOf(response) !== kept)
134
+ delete response.content;
135
+ };
136
+
137
+ /** The media types of a response body, or `undefined` without one. */
138
+ const mediaOf = (
139
+ response: OpenApi.IOperation.IResponse,
140
+ ): string | undefined =>
141
+ response.content === undefined
142
+ ? undefined
143
+ : Object.keys(response.content).sort().join(" ");
100
144
  }
101
145
 
102
146
  /**
@@ -126,7 +170,7 @@ const composeContent = (props: {
126
170
  contentType: string | null | undefined;
127
171
  schema: OpenApi.IJsonSchema | undefined;
128
172
  example: any;
129
- examples: Record<string, any> | undefined;
173
+ examples: Record<string, OpenApi.IExample> | undefined;
130
174
  }): OpenApi.IOperation.IContent | undefined => {
131
175
  if (!props.contentType) return undefined;
132
176
  const described: boolean =