@nestia/sdk 13.0.4 → 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 +8 -8
  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
@@ -1,12 +1,16 @@
1
1
  package sdk
2
2
 
3
3
  import (
4
+ "encoding"
4
5
  "encoding/json"
5
6
  "fmt"
7
+ "math"
6
8
  "os"
7
9
  "path/filepath"
10
+ "reflect"
8
11
  "regexp"
9
12
  "sort"
13
+ "strconv"
10
14
  "strings"
11
15
 
12
16
  shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -40,9 +44,10 @@ type nestiaSDKContext struct {
40
44
  }
41
45
 
42
46
  type nestiaSDKSchemaCacheKey struct {
43
- Type *shimchecker.Type
44
- Text string
45
- Escape bool
47
+ Type *shimchecker.Type
48
+ Text string
49
+ Escape bool
50
+ Properties bool
46
51
  }
47
52
 
48
53
  func newNestiaSDKContext(prog *driver.Program) *nestiaSDKContext {
@@ -148,7 +153,16 @@ func nestiaSDKMetadataText(context *nestiaSDKContext, file *shimast.SourceFile,
148
153
  for index, param := range methodDecl.Parameters.Nodes {
149
154
  typ := prog.Checker.GetTypeAtLocation(param)
150
155
  name := nestiaSDKParameterName(param)
151
- response := nestiaSDKResponse(context, imports, typ, nestiaSDKParameterTypeNode(param))
156
+ response := nestiaSDKResponse(context, imports, typ, nestiaSDKParameterTypeNode(param), true)
157
+ if ref, refs, err := nestiaSDKWebSocketParameterType(context, param); err != nil {
158
+ return "", err
159
+ } else if ref != nil {
160
+ response["type"] = ref
161
+ response["imports"] = refs
162
+ }
163
+ if err := nestiaSDKWebSocketHeaderError(context.prog, param, typ); err != nil {
164
+ return "", err
165
+ }
152
166
  parameters = append(parameters, map[string]any{
153
167
  "name": name,
154
168
  "index": index,
@@ -166,7 +180,7 @@ func nestiaSDKMetadataText(context *nestiaSDKContext, file *shimast.SourceFile,
166
180
  exceptions := nestiaSDKExceptionResponses(context, imports, method)
167
181
  metadata := map[string]any{
168
182
  "parameters": parameters,
169
- "success": nestiaSDKResponse(context, imports, returnType, returnTypeNode),
183
+ "success": nestiaSDKResponse(context, imports, returnType, returnTypeNode, false),
170
184
  "exceptions": exceptions,
171
185
  "description": nestiaSDKNullableString(doc.Description),
172
186
  "jsDocTags": doc.Tags,
@@ -177,7 +191,7 @@ func nestiaSDKMetadataText(context *nestiaSDKContext, file *shimast.SourceFile,
177
191
  const nestiaSDKLiteralNull = "__NESTIA_LITERAL_NULL__"
178
192
 
179
193
  func nestiaSDKMetadataLiteralText(metadata map[string]any) (string, error) {
180
- data, err := json.Marshal(metadata)
194
+ data, err := json.Marshal(nestiaSDKFiniteLiteral(metadata))
181
195
  if err != nil {
182
196
  return "", err
183
197
  }
@@ -186,6 +200,88 @@ func nestiaSDKMetadataLiteralText(metadata map[string]any) (string, error) {
186
200
  return text, nil
187
201
  }
188
202
 
203
+ // nestiaSDKFiniteLiteral copies a metadata value for encoding/json, writing
204
+ // every non-finite number as null, the way JavaScript's `JSON.stringify` does.
205
+ //
206
+ // typia hands over NaN and ±Infinity wherever a type or a comment spells one: a
207
+ // numeric literal type such as `1e999`, a type tag argument such as
208
+ // `tags.Minimum<1e999>`, and a JSDoc `@x-` extension whose text parses as
209
+ // a float, which `NaN`, `Infinity`, and `inf` all do. encoding/json
210
+ // refuses those values, so one of them anywhere in the types a route reaches
211
+ // used to abort the whole metadata pass. The metadata is marshaled only here,
212
+ // so walking it here gives every value, baked schema and metadata literal
213
+ // alike, the same rule.
214
+ //
215
+ // The walk rebuilds maps, slices, and typia's ordered objects, whose
216
+ // MarshalJSON would otherwise marshal a non-finite member itself. A null is
217
+ // written as typia's explicit `LiteralFactory_Null` marker rather than a Go
218
+ // nil, because an ordered object drops a nil member instead of printing it,
219
+ // while `JSON.stringify({ a: NaN })` keeps the key. Any other value that
220
+ // marshals itself is left as it is.
221
+ func nestiaSDKFiniteLiteral(input any) any {
222
+ switch value := input.(type) {
223
+ case nil:
224
+ return nil
225
+ case nativefactories.LiteralFactory_OrderedObject:
226
+ return nestiaSDKFiniteOrderedLiteral(value)
227
+ case *nativefactories.LiteralFactory_OrderedObject:
228
+ if value == nil {
229
+ return value
230
+ }
231
+ return nestiaSDKFiniteOrderedLiteral(*value)
232
+ case json.Marshaler, encoding.TextMarshaler:
233
+ return value
234
+ }
235
+ reflected := reflect.ValueOf(input)
236
+ switch reflected.Kind() {
237
+ case reflect.Float32, reflect.Float64:
238
+ // by kind, because the checker's numbers are typescript-go's named
239
+ // `jsnum.Number`, not a plain float64
240
+ if number := reflected.Float(); math.IsNaN(number) || math.IsInf(number, 0) {
241
+ return nativefactories.LiteralFactory_Null{}
242
+ }
243
+ return input
244
+ case reflect.Map:
245
+ if reflected.IsNil() || reflected.Type().Key().Kind() != reflect.String {
246
+ return input
247
+ }
248
+ output := make(map[string]any, reflected.Len())
249
+ iterator := reflected.MapRange()
250
+ for iterator.Next() {
251
+ output[iterator.Key().String()] = nestiaSDKFiniteLiteral(iterator.Value().Interface())
252
+ }
253
+ return output
254
+ case reflect.Slice, reflect.Array:
255
+ if (reflected.Kind() == reflect.Slice && reflected.IsNil()) || reflected.Type().Elem().Kind() == reflect.Uint8 {
256
+ return input
257
+ }
258
+ output := make([]any, reflected.Len())
259
+ for i := range output {
260
+ output[i] = nestiaSDKFiniteLiteral(reflected.Index(i).Interface())
261
+ }
262
+ return output
263
+ case reflect.Pointer, reflect.Interface:
264
+ if reflected.IsNil() {
265
+ return input
266
+ }
267
+ return nestiaSDKFiniteLiteral(reflected.Elem().Interface())
268
+ }
269
+ return input
270
+ }
271
+
272
+ func nestiaSDKFiniteOrderedLiteral(
273
+ input nativefactories.LiteralFactory_OrderedObject,
274
+ ) nativefactories.LiteralFactory_OrderedObject {
275
+ output := nativefactories.LiteralFactory_OrderedObject{
276
+ Keys: input.Keys,
277
+ Values: make(map[string]any, len(input.Values)),
278
+ }
279
+ for key, value := range input.Values {
280
+ output.Values[key] = nestiaSDKFiniteLiteral(value)
281
+ }
282
+ return output
283
+ }
284
+
189
285
  type nestiaSDKJSDoc struct {
190
286
  Description string
191
287
  Tags []any
@@ -205,34 +301,83 @@ func nestiaSDKMethodJSDoc(file *shimast.SourceFile, method *shimast.Node) nestia
205
301
  if comment == "" {
206
302
  return doc
207
303
  }
304
+ // A tag runs until the next one, and each line loses the comment's margin
305
+ // as TypeScript takes it off: the `*`, then the indentation up to the
306
+ // column the text began at. That is the description's first line, the
307
+ // text after a tag's name, a `@param` description past the parameter's
308
+ // name, a `@returns` one past its type, or, when the tag's text starts on
309
+ // the next line, the tag itself, so an `@example` keeps its code's
310
+ // indentation while a wrapped `@param` description does not.
208
311
  description := []string{}
209
- inTags := false
312
+ descriptionMargin := -1
313
+ var tag *nestiaSDKPendingTag
314
+ flush := func() {
315
+ if tag == nil {
316
+ return
317
+ }
318
+ body := strings.TrimLeft(strings.Join(tag.lines, "\n"), "\n")
319
+ body = strings.TrimRight(body, " \t\n")
320
+ if tag.name == "param" {
321
+ param, desc := nestiaSDKParseParamTag(body)
322
+ doc.Tags = append(doc.Tags, nestiaSDKJSDocParamTag(param, desc))
323
+ if param != "" {
324
+ doc.Params[param] = desc
325
+ }
326
+ } else {
327
+ if nestiaSDKIsReturnTag(tag.name) {
328
+ // TypeScript's text leaves out a type the tag declares
329
+ body = strings.TrimLeft(body[nestiaSDKReturnTypeEnd(body):], "\n")
330
+ }
331
+ doc.Tags = append(doc.Tags, nestiaSDKJSDocTag(tag.name, body))
332
+ }
333
+ tag = nil
334
+ }
210
335
  for _, line := range strings.Split(comment, "\n") {
211
- text := strings.TrimSpace(line)
212
- text = strings.TrimPrefix(text, "*")
213
- text = strings.TrimSpace(text)
214
- if text == "" {
215
- if inTags == false && len(description) != 0 {
216
- description = append(description, "")
336
+ rest := strings.TrimLeft(line, " \t")
337
+ rest = strings.TrimPrefix(rest, "*")
338
+ rest = strings.TrimRight(rest, " \t\r")
339
+ trimmed := strings.TrimSpace(rest)
340
+ if strings.HasPrefix(trimmed, "@") {
341
+ flush()
342
+ name, body := nestiaSDKParseJSDocTag(trimmed)
343
+ margin := nestiaSDKJSDocIndent(rest)
344
+ // where the text begins in the body: a `@param` description after
345
+ // the parameter's name, a `@returns` one after its type
346
+ text := 0
347
+ if name == "param" {
348
+ _, text = nestiaSDKParamTagName(body)
349
+ } else if nestiaSDKIsReturnTag(name) {
350
+ text = nestiaSDKReturnTypeEnd(body)
217
351
  }
352
+ if text < len(body) {
353
+ after := trimmed[1+len(name):]
354
+ margin += 1 + len(name) + len(after) - len(strings.TrimLeft(after, " \t")) + text
355
+ }
356
+ tag = &nestiaSDKPendingTag{name: name, lines: []string{body}, margin: margin}
218
357
  continue
219
358
  }
220
- if strings.HasPrefix(text, "@") {
221
- inTags = true
222
- name, body := nestiaSDKParseJSDocTag(text)
223
- doc.Tags = append(doc.Tags, nestiaSDKJSDocTag(name, body))
224
- if name == "param" {
225
- param, desc := nestiaSDKParseParamTag(body)
226
- if param != "" {
227
- doc.Params[param] = desc
228
- }
359
+ if tag != nil {
360
+ tag.lines = append(tag.lines, nestiaSDKJSDocOutdent(rest, tag.margin))
361
+ continue
362
+ }
363
+ if trimmed == "" {
364
+ if len(description) != 0 {
365
+ description = append(description, "")
229
366
  }
230
367
  continue
231
368
  }
232
- if inTags == false {
233
- description = append(description, text)
369
+ if descriptionMargin == -1 {
370
+ // text on the opening `/**` line has no `*` to measure from, so the
371
+ // lines after it keep the usual `* ` margin
372
+ if strings.HasPrefix(strings.TrimLeft(line, " \t"), "*") {
373
+ descriptionMargin = nestiaSDKJSDocIndent(rest)
374
+ } else {
375
+ descriptionMargin = 1
376
+ }
234
377
  }
378
+ description = append(description, nestiaSDKJSDocOutdent(rest, descriptionMargin))
235
379
  }
380
+ flush()
236
381
  doc.Description = strings.TrimSpace(strings.Join(description, "\n"))
237
382
  return doc
238
383
  }
@@ -288,14 +433,108 @@ func nestiaSDKParseJSDocTag(text string) (string, string) {
288
433
  return name, body
289
434
  }
290
435
 
436
+ // nestiaSDKPendingTag is a JSDoc tag whose text may continue on the next
437
+ // lines.
438
+ type nestiaSDKPendingTag struct {
439
+ name string
440
+ lines []string
441
+ margin int
442
+ }
443
+
444
+ // nestiaSDKJSDocIndent counts the spaces and tabs a JSDoc line starts with.
445
+ func nestiaSDKJSDocIndent(line string) int {
446
+ return len(line) - len(strings.TrimLeft(line, " \t"))
447
+ }
448
+
449
+ // nestiaSDKJSDocOutdent takes up to margin spaces and tabs off a JSDoc line.
450
+ func nestiaSDKJSDocOutdent(line string, margin int) string {
451
+ indent := nestiaSDKJSDocIndent(line)
452
+ if indent > margin {
453
+ indent = margin
454
+ }
455
+ return line[indent:]
456
+ }
457
+
458
+ // nestiaSDKParseParamTag splits a `@param` body into the parameter's name and
459
+ // its description, as TypeScript does. A description that starts on the next
460
+ // line keeps the indentation its margin leaves it.
291
461
  func nestiaSDKParseParamTag(body string) (string, string) {
292
- parts := strings.Fields(body)
293
- if len(parts) == 0 {
294
- return "", ""
462
+ param, text := nestiaSDKParamTagName(body)
463
+ return param, strings.TrimLeft(body[text:], "\n")
464
+ }
465
+
466
+ // nestiaSDKParamTagName reads the parameter a `@param` body names, as
467
+ // TypeScript does: a leading `{Type}` is not the name, and an optional
468
+ // parameter's brackets and default, `[name=value]`, are not part of it. It
469
+ // also returns where the description begins, past the spaces after the name.
470
+ func nestiaSDKParamTagName(body string) (string, int) {
471
+ offset := nestiaSDKSkipBlank(body, 0, true)
472
+ if end := nestiaSDKClosing(body, offset, '{', '}'); end != -1 {
473
+ offset = nestiaSDKSkipBlank(body, end+1, true)
474
+ }
475
+ name := ""
476
+ if end := nestiaSDKClosing(body, offset, '[', ']'); end != -1 {
477
+ // the bracket that closes the first, past any in a default value
478
+ name = body[offset+1 : end]
479
+ if equal := strings.Index(name, "="); equal != -1 {
480
+ name = name[:equal]
481
+ }
482
+ name = strings.TrimSpace(name)
483
+ offset = end + 1
484
+ } else {
485
+ start := offset
486
+ for offset < len(body) && strings.IndexByte(" \t\n", body[offset]) == -1 {
487
+ offset++
488
+ }
489
+ name = body[start:offset]
490
+ }
491
+ return name, nestiaSDKSkipBlank(body, offset, false)
492
+ }
493
+
494
+ // nestiaSDKIsReturnTag tells a `@returns` tag, which TypeScript also reads as
495
+ // `@return`.
496
+ func nestiaSDKIsReturnTag(name string) bool {
497
+ return name == "returns" || name == "return"
498
+ }
499
+
500
+ // nestiaSDKReturnTypeEnd returns where a `@returns` body's text begins past a
501
+ // leading `{Type}`, which TypeScript reads as the type rather than the text,
502
+ // and the spaces after it; 0 without a type.
503
+ func nestiaSDKReturnTypeEnd(body string) int {
504
+ if end := nestiaSDKClosing(body, nestiaSDKSkipBlank(body, 0, true), '{', '}'); end != -1 {
505
+ return nestiaSDKSkipBlank(body, end+1, false)
295
506
  }
296
- param := parts[0]
297
- desc := strings.TrimSpace(strings.TrimPrefix(body, param))
298
- return param, desc
507
+ return 0
508
+ }
509
+
510
+ // nestiaSDKClosing finds the bracket closing the one text opens at start, or
511
+ // -1 when text opens none there or never closes it.
512
+ func nestiaSDKClosing(text string, start int, open byte, close byte) int {
513
+ if start >= len(text) || text[start] != open {
514
+ return -1
515
+ }
516
+ depth := 0
517
+ for i := start; i < len(text); i++ {
518
+ if text[i] == open {
519
+ depth++
520
+ } else if text[i] == close {
521
+ depth--
522
+ if depth == 0 {
523
+ return i
524
+ }
525
+ }
526
+ }
527
+ return -1
528
+ }
529
+
530
+ // nestiaSDKSkipBlank moves offset past spaces and tabs, and line breaks too
531
+ // when newline is set.
532
+ func nestiaSDKSkipBlank(text string, offset int, newline bool) int {
533
+ for offset < len(text) &&
534
+ (text[offset] == ' ' || text[offset] == '\t' || (newline && text[offset] == '\n')) {
535
+ offset++
536
+ }
537
+ return offset
299
538
  }
300
539
 
301
540
  func nestiaSDKJSDocTag(name string, text string) map[string]any {
@@ -315,6 +554,33 @@ func nestiaSDKJSDocTag(name string, text string) map[string]any {
315
554
  }
316
555
  }
317
556
 
557
+ // nestiaSDKJSDocParamTag writes a `@param` tag the way TypeScript's
558
+ // `JSDocTagInfo` does, which the SDK generators read: the parameter's name
559
+ // as its own `parameterName` part, then a space and the description. Written
560
+ // as one text part, the name could not be matched, so a WebSocket route lost
561
+ // every `@param` line and the generators' tag fallbacks never applied.
562
+ func nestiaSDKJSDocParamTag(param string, desc string) map[string]any {
563
+ if param == "" {
564
+ return nestiaSDKJSDocTag("param", "")
565
+ }
566
+ text := []any{
567
+ map[string]any{
568
+ "text": param,
569
+ "kind": "parameterName",
570
+ },
571
+ }
572
+ if desc != "" {
573
+ text = append(text,
574
+ map[string]any{"text": " ", "kind": "space"},
575
+ map[string]any{"text": desc, "kind": "text"},
576
+ )
577
+ }
578
+ return map[string]any{
579
+ "name": "param",
580
+ "text": text,
581
+ }
582
+ }
583
+
318
584
  func nestiaSDKNullableString(value string) any {
319
585
  value = strings.TrimSpace(value)
320
586
  if value == "" {
@@ -335,7 +601,7 @@ func nestiaSDKExceptionResponses(
335
601
  if exception == nil {
336
602
  continue
337
603
  }
338
- responses = append(responses, nestiaSDKResponse(context, imports, exception.Type, exception.Node))
604
+ responses = append(responses, nestiaSDKResponse(context, imports, exception.Type, exception.Node, false))
339
605
  }
340
606
  return responses
341
607
  }
@@ -354,7 +620,9 @@ func nestiaSDKTypedExceptionInfo(prog *driver.Program, decorator *shimast.Node)
354
620
  return nil
355
621
  }
356
622
  call := expression.AsCallExpression()
357
- segments := transform.NestiaCoreExpressionSegments(call.Expression)
623
+ // by the export the import binds, as the runtime registers an aliased
624
+ // `TypedException` too; a lexical name lost it, misaligning the exceptions
625
+ segments := transform.NestiaCoreCanonicalDecoratorSegments(decorator)
358
626
  if len(segments) == 0 || segments[len(segments)-1] != "TypedException" {
359
627
  return nil
360
628
  }
@@ -538,11 +806,16 @@ func nestiaSDKImportLiteral(imp nestiaSDKImportInfo, prefixes map[string]bool) m
538
806
  return output
539
807
  }
540
808
 
809
+ // nestiaSDKResponse reflects one route input or output. `parameter` marks a
810
+ // method parameter: the Swagger generator reads query and headers parameters
811
+ // from the resolved schema and may decompose their object into one OpenAPI
812
+ // parameter per property, so only that schema bakes the property schemas.
541
813
  func nestiaSDKResponse(
542
814
  context *nestiaSDKContext,
543
815
  imports []nestiaSDKImportInfo,
544
816
  typ *shimchecker.Type,
545
817
  typeNode *shimast.Node,
818
+ parameter bool,
546
819
  ) map[string]any {
547
820
  prog := context.prog
548
821
  refType, refImports := nestiaSDKReflectType(prog, imports, typ, typeNode)
@@ -552,17 +825,18 @@ func nestiaSDKResponse(
552
825
  return map[string]any{
553
826
  "type": refType,
554
827
  "imports": refImports,
555
- "primitive": nestiaSDKSchemaPipe(context, typ, typeNode, true),
556
- "resolved": nestiaSDKSchemaPipe(context, typ, typeNode, false),
828
+ "primitive": nestiaSDKSchemaPipe(context, typ, typeNode, true, false),
829
+ "resolved": nestiaSDKSchemaPipe(context, typ, typeNode, false, parameter),
557
830
  }
558
831
  }
559
832
 
560
- func nestiaSDKSchemaPipe(context *nestiaSDKContext, typ *shimchecker.Type, typeNode *shimast.Node, escape bool) any {
833
+ func nestiaSDKSchemaPipe(context *nestiaSDKContext, typ *shimchecker.Type, typeNode *shimast.Node, escape bool, properties bool) any {
561
834
  prog := context.prog
562
835
  key := nestiaSDKSchemaCacheKey{
563
- Type: typ,
564
- Text: nestiaSDKTypeNodeText(typeNode),
565
- Escape: escape,
836
+ Type: typ,
837
+ Text: nestiaSDKTypeNodeText(typeNode),
838
+ Escape: escape,
839
+ Properties: properties,
566
840
  }
567
841
  if cached, ok := context.schemaCache[key]; ok {
568
842
  context.schemaHits++
@@ -617,15 +891,20 @@ func nestiaSDKSchemaPipe(context *nestiaSDKContext, typ *shimchecker.Type, typeN
617
891
  // and omit the field — the sdk generator falls back to its own derived
618
892
  // schema path. This must never mask a real bug, so re-raise anything we
619
893
  // don't recognize as a transformer error from the typia runtime.
620
- if baked := nestiaSDKTryBakeJsonSchema(prog, typeNode, result.Data); baked != nil {
894
+ if baked := nestiaSDKTryBakeJsonSchema(prog, typeNode, result.Data, properties); baked != nil {
621
895
  metadataLiteral["jsonSchema"] = baked
622
896
  }
897
+ data := map[string]any{
898
+ "components": nestiaSDKMetadataComponentsLiteral(nestiaSDKVisitedMetadataComponents(context.collection, result.Data)),
899
+ "metadata": metadataLiteral,
900
+ }
901
+ if properties {
902
+ // a route parameter's HTTP rule verdicts, from its own analysis
903
+ data["http"] = nestiaSDKHttpRules(prog.Checker, typ)
904
+ }
623
905
  value := map[string]any{
624
906
  "success": true,
625
- "data": map[string]any{
626
- "components": nestiaSDKMetadataComponentsLiteral(nestiaSDKVisitedMetadataComponents(context.collection, result.Data)),
627
- "metadata": metadataLiteral,
628
- },
907
+ "data": data,
629
908
  }
630
909
  context.schemaCache[key] = value
631
910
  return value
@@ -636,10 +915,15 @@ func nestiaSDKSchemaPipe(context *nestiaSDKContext, typ *shimchecker.Type, typeN
636
915
  // when typia signals the metadata has no JSON-schema representation (e.g.
637
916
  // `void` returns, function-only types) — the JS-side reader handles missing
638
917
  // `jsonSchema` fields. Any other panic is re-raised so real bugs surface.
918
+ //
919
+ // With `properties`, the literal also carries the schema of each property of
920
+ // the metadata's first object type, for the Swagger generator to decompose into
921
+ // individual parameters (see `nestiaSDKPropertySchemas`).
639
922
  func nestiaSDKTryBakeJsonSchema(
640
923
  prog *driver.Program,
641
924
  typeNode *shimast.Node,
642
925
  metadata *schemametadata.MetadataSchema,
926
+ properties bool,
643
927
  ) (baked map[string]any) {
644
928
  defer func() {
645
929
  if r := recover(); r != nil {
@@ -660,6 +944,12 @@ func nestiaSDKTryBakeJsonSchema(
660
944
  if len(collection.Schemas) == 0 {
661
945
  return nil
662
946
  }
947
+ // Baked before the components are flattened: the property schemas share
948
+ // the parent's components, so any component they reach must be included.
949
+ var propertySchemas map[string]any
950
+ if properties && collection.Components != nil {
951
+ propertySchemas = nestiaSDKPropertySchemas(metadata, collection.Components)
952
+ }
663
953
  // `iterate.OpenApi_IComponents` has no JSON tags on its Schemas field,
664
954
  // so default Go marshaling would emit `"Schemas"` (capital). The JS
665
955
  // side reads `components.schemas`, so flatten to a plain map here.
@@ -667,19 +957,236 @@ func nestiaSDKTryBakeJsonSchema(
667
957
  if collection.Components != nil && collection.Components.Schemas != nil {
668
958
  schemasLiteral := map[string]any{}
669
959
  for key, val := range collection.Components.Schemas {
670
- schemasLiteral[key] = map[string]any(val)
960
+ schemasLiteral[key] = nestiaSDKJsonSchemaLiteral(val)
671
961
  }
672
962
  componentsLiteral["schemas"] = schemasLiteral
673
963
  }
674
964
  baked = map[string]any{
675
965
  "version": collection.Version,
676
966
  "components": componentsLiteral,
677
- "schema": map[string]any(collection.Schemas[0]),
967
+ "schema": nestiaSDKJsonSchemaLiteral(collection.Schemas[0]),
968
+ }
969
+ if propertySchemas != nil {
970
+ baked["properties"] = propertySchemas
678
971
  }
679
972
  nestiaSDKMarkReadonlyArrayJsonSchema(prog, typeNode, baked)
680
973
  return baked
681
974
  }
682
975
 
976
+ // nestiaSDKPropertySchemas bakes the schema of each property of the metadata's
977
+ // first object type, keyed by property name, for a decomposed query or headers
978
+ // parameter. It is the property schema typia's object writer produces, minus
979
+ // the fields the OpenAPI parameter carries in its own members: typia merges the
980
+ // property's title, description, and deprecation into that schema, plus
981
+ // readOnly, which has no meaning for a request parameter. What remains is the
982
+ // value schema, written by typia's `Json_schema_station` exactly as
983
+ // `WriteSchemas` writes one metadata but against the parent's components, so a
984
+ // named type the value reaches resolves to the component the parent already
985
+ // carries, and the property's `x-` JSDoc extensions.
986
+ //
987
+ // Only properties typia's object schema describes are baked: a property must
988
+ // have a literal key and no `@hidden`, `@ignore`, or `@internal` tag (the
989
+ // filter `json_schema_object` applies), and a value with no JSON form
990
+ // (function-only or `never`) yields no schema. Skipping them here also keeps
991
+ // their types out of the shared components.
992
+ func nestiaSDKPropertySchemas(
993
+ metadata *schemametadata.MetadataSchema,
994
+ components *nativeiterate.OpenApi_IComponents,
995
+ ) map[string]any {
996
+ if len(metadata.Objects) == 0 || metadata.Objects[0].Type == nil {
997
+ return nil
998
+ }
999
+ output := map[string]any{}
1000
+ for _, property := range metadata.Objects[0].Type.Properties {
1001
+ if property == nil || property.Key == nil || property.Value == nil {
1002
+ continue
1003
+ }
1004
+ key := property.Key.GetSoleLiteral()
1005
+ if key == nil || nestiaSDKHasJSDocTag(property.JsDocTags, "hidden", "ignore", "internal") {
1006
+ continue
1007
+ }
1008
+ schema := nativeiterate.Json_schema_station(nativeiterate.Json_schema_station_props{
1009
+ BlockNever: true,
1010
+ Components: components,
1011
+ Attribute: nativeiterate.JsonSchema{},
1012
+ Metadata: property.Value,
1013
+ })
1014
+ if schema == nil {
1015
+ continue
1016
+ }
1017
+ nestiaSDKJsDocExtensions(schema, property.JsDocTags)
1018
+ output[*key] = nestiaSDKJsonSchemaLiteral(schema)
1019
+ }
1020
+ return output
1021
+ }
1022
+
1023
+ // nestiaSDKJsDocExtensions writes a property's `x-` JSDoc tags into its schema
1024
+ // the way typia's object writer does (`json_schema_jsDocTags`, unexported):
1025
+ // the first text part, trimmed, read as a boolean, a number, null, or else a
1026
+ // string.
1027
+ func nestiaSDKJsDocExtensions(schema nativeiterate.JsonSchema, tags []schemametadata.IJsDocTagInfo) {
1028
+ for _, tag := range tags {
1029
+ if strings.HasPrefix(tag.Name, "x-") == false {
1030
+ continue
1031
+ }
1032
+ for _, text := range tag.Text {
1033
+ if text.Kind != "text" {
1034
+ continue
1035
+ }
1036
+ value := strings.ReplaceAll(strings.TrimSpace(text.Text), "\r\n", "\n")
1037
+ if value == "true" || value == "false" {
1038
+ schema[tag.Name] = value == "true"
1039
+ } else if number, err := strconv.ParseFloat(value, 64); err == nil {
1040
+ schema[tag.Name] = number
1041
+ } else if value == "null" {
1042
+ schema[tag.Name] = nil
1043
+ } else {
1044
+ schema[tag.Name] = value
1045
+ }
1046
+ break
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ func nestiaSDKHasJSDocTag(tags []schemametadata.IJsDocTagInfo, names ...string) bool {
1052
+ for _, tag := range tags {
1053
+ for _, name := range names {
1054
+ if tag.Name == name {
1055
+ return true
1056
+ }
1057
+ }
1058
+ }
1059
+ return false
1060
+ }
1061
+
1062
+ // nestiaSDKJsonSchemaLiteral prepares one typia JSON schema for the
1063
+ // encoding/json serialization of the SDK metadata, dropping the members typia
1064
+ // leaves absent.
1065
+ //
1066
+ // typia's schema writer stores a Go nil for an absent member: a constant with
1067
+ // no `@title` gets a nil *string title and description, and `any` gets a nil
1068
+ // type. typia's own literal printer skips nil object members, but encoding/json
1069
+ // prints them as null, which JSON Schema rejects for those keywords. A nil
1070
+ // cannot be dropped everywhere, though: in an instance-valued keyword (`const`,
1071
+ // `default`, `enum`, `example`, `examples`) or a vendor extension it can be a
1072
+ // real null. typia marks the nulls a type declares, such as
1073
+ // `tags.Example<null>`, with `LiteralFactory_Null`, which is no nil and
1074
+ // marshals as null, but a JSDoc `@x-foo null` still reaches the schema as a
1075
+ // bare nil, from typia's object writer and from nestiaSDKJsDocExtensions alike.
1076
+ //
1077
+ // So the walk follows JSON Schema structure: it drops a nil member of a schema
1078
+ // object, recurses through the applicator keywords into subschemas, and leaves
1079
+ // every other value, instance data included, as it is.
1080
+ func nestiaSDKJsonSchemaLiteral(input any) any {
1081
+ reflected := reflect.ValueOf(input)
1082
+ if reflected.Kind() != reflect.Map || reflected.Type().Key().Kind() != reflect.String {
1083
+ return input // a boolean schema, or a value that is no schema object
1084
+ }
1085
+ output := make(map[string]any, reflected.Len())
1086
+ iterator := reflected.MapRange()
1087
+ for iterator.Next() {
1088
+ key := iterator.Key().String()
1089
+ member := iterator.Value().Interface()
1090
+ if nestiaSDKJsonSchemaInstanceKeyword(key) {
1091
+ output[key] = member
1092
+ } else if nestiaSDKIsNilLike(member) == false {
1093
+ output[key] = nestiaSDKJsonSchemaMember(key, member)
1094
+ }
1095
+ }
1096
+ return output
1097
+ }
1098
+
1099
+ // nestiaSDKJsonSchemaMember recurses into the subschemas a JSON Schema 2020-12
1100
+ // applicator keyword holds, and returns any other keyword value unchanged.
1101
+ func nestiaSDKJsonSchemaMember(key string, member any) any {
1102
+ switch key {
1103
+ case "items", "additionalItems", "additionalProperties", "unevaluatedItems",
1104
+ "unevaluatedProperties", "contains", "propertyNames", "not", "if", "then",
1105
+ "else", "contentSchema":
1106
+ return nestiaSDKJsonSchemaLiteral(member)
1107
+ case "allOf", "anyOf", "oneOf", "prefixItems":
1108
+ reflected := reflect.ValueOf(member)
1109
+ if reflected.Kind() != reflect.Slice && reflected.Kind() != reflect.Array {
1110
+ return member
1111
+ }
1112
+ output := make([]any, reflected.Len())
1113
+ for i := range output {
1114
+ output[i] = nestiaSDKJsonSchemaLiteral(reflected.Index(i).Interface())
1115
+ }
1116
+ return output
1117
+ case "properties", "patternProperties", "dependentSchemas", "$defs", "definitions":
1118
+ return nestiaSDKJsonSchemaNamedLiteral(member)
1119
+ }
1120
+ return member
1121
+ }
1122
+
1123
+ // nestiaSDKJsonSchemaNamedLiteral normalizes each schema of a name-keyed schema
1124
+ // map, such as `properties`, keeping the order of typia's ordered objects.
1125
+ func nestiaSDKJsonSchemaNamedLiteral(input any) any {
1126
+ switch value := input.(type) {
1127
+ case nativefactories.LiteralFactory_OrderedObject:
1128
+ return nestiaSDKJsonSchemaOrderedLiteral(value)
1129
+ case *nativefactories.LiteralFactory_OrderedObject:
1130
+ if value == nil {
1131
+ return nil
1132
+ }
1133
+ return nestiaSDKJsonSchemaOrderedLiteral(*value)
1134
+ }
1135
+ reflected := reflect.ValueOf(input)
1136
+ if reflected.Kind() != reflect.Map || reflected.Type().Key().Kind() != reflect.String {
1137
+ return input
1138
+ }
1139
+ output := make(map[string]any, reflected.Len())
1140
+ iterator := reflected.MapRange()
1141
+ for iterator.Next() {
1142
+ output[iterator.Key().String()] = nestiaSDKJsonSchemaLiteral(iterator.Value().Interface())
1143
+ }
1144
+ return output
1145
+ }
1146
+
1147
+ func nestiaSDKJsonSchemaOrderedLiteral(
1148
+ input nativefactories.LiteralFactory_OrderedObject,
1149
+ ) nativefactories.LiteralFactory_OrderedObject {
1150
+ output := nativefactories.LiteralFactory_OrderedObject{
1151
+ Keys: make([]string, 0, len(input.Keys)),
1152
+ Values: make(map[string]any, len(input.Keys)),
1153
+ }
1154
+ for _, key := range input.Keys {
1155
+ value, ok := input.Values[key]
1156
+ if ok == false || nestiaSDKIsNilLike(value) {
1157
+ continue
1158
+ }
1159
+ output.Keys = append(output.Keys, key)
1160
+ output.Values[key] = nestiaSDKJsonSchemaLiteral(value)
1161
+ }
1162
+ return output
1163
+ }
1164
+
1165
+ // nestiaSDKJsonSchemaInstanceKeyword reports a keyword whose value is JSON
1166
+ // instance data, where null is a legitimate value: the instance keywords of
1167
+ // JSON Schema and OpenAPI, and `x-` vendor extensions.
1168
+ func nestiaSDKJsonSchemaInstanceKeyword(key string) bool {
1169
+ switch key {
1170
+ case "const", "default", "enum", "example", "examples":
1171
+ return true
1172
+ }
1173
+ return strings.HasPrefix(key, "x-")
1174
+ }
1175
+
1176
+ // nestiaSDKIsNilLike mirrors typia's `literalFactory_isNilLike`.
1177
+ func nestiaSDKIsNilLike(value any) bool {
1178
+ if value == nil {
1179
+ return true
1180
+ }
1181
+ reflected := reflect.ValueOf(value)
1182
+ switch reflected.Kind() {
1183
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
1184
+ return reflected.IsNil()
1185
+ default:
1186
+ return false
1187
+ }
1188
+ }
1189
+
683
1190
  func nestiaSDKMarkReadonlyArrayJsonSchema(prog *driver.Program, typeNode *shimast.Node, baked map[string]any) {
684
1191
  components, _ := baked["components"].(map[string]any)
685
1192
  schemas, _ := components["schemas"].(map[string]any)
@@ -1505,18 +2012,261 @@ func nestiaSDKParentClassName(node *shimast.Node) string {
1505
2012
  return ""
1506
2013
  }
1507
2014
 
2015
+ // nestiaSDKMethodName names a decorated method's site. The contributor visits
2016
+ // every decorated method of the program, controller or not, and a computed
2017
+ // name such as `[key]` has no identifier: it reads as its literal key when it
2018
+ // has one, and as its source otherwise. The metadata still reaches the method,
2019
+ // because a decorator attaches it under the key the runtime computes.
1508
2020
  func nestiaSDKMethodName(node *shimast.Node) string {
1509
2021
  if node == nil || node.Name() == nil {
1510
2022
  return ""
1511
2023
  }
1512
- return strings.Trim(node.Name().Text(), "\"'")
2024
+ name := node.Name()
2025
+ if name.Kind == shimast.KindComputedPropertyName {
2026
+ switch expression := name.AsComputedPropertyName().Expression; expression.Kind {
2027
+ case shimast.KindStringLiteral, shimast.KindNumericLiteral, shimast.KindNoSubstitutionTemplateLiteral:
2028
+ return expression.Text()
2029
+ }
2030
+ }
2031
+ return strings.Trim(shimast.NodeText(name), "\"'")
2032
+ }
2033
+
2034
+ // nestiaSDKWebSocketHeaderError reports a WebSocket route whose handshake
2035
+ // header the generated SDK cannot carry: the header type of an
2036
+ // @WebSocketRoute.Acceptor() (its first type argument) or the type of an
2037
+ // @WebSocketRoute.Header() parameter must be an object type or undefined,
2038
+ // because the SDK function sends it as connection.headers, which @nestia/fetcher
2039
+ // types `IConnection<Headers extends object | undefined>`. tgrid itself leaves
2040
+ // the header unconstrained, so a null or primitive header serves, yet its SDK
2041
+ // would not compile.
2042
+ func nestiaSDKWebSocketHeaderError(
2043
+ prog *driver.Program,
2044
+ param *shimast.Node,
2045
+ typ *shimchecker.Type,
2046
+ ) error {
2047
+ category := transform.NestiaCoreWebSocketParameterCategory(prog, param)
2048
+ var header *shimchecker.Type
2049
+ switch category {
2050
+ case "Acceptor":
2051
+ // only tgrid's acceptor has a header; the transform already rejects any
2052
+ // other type, and asking a non-reference type for its type arguments
2053
+ // would fault the checker
2054
+ if _, name := transform.NestiaCoreWebSocketTypeReference(prog, nestiaSDKParameterTypeNode(param)); name != "WebSocketAcceptor" {
2055
+ return nil
2056
+ }
2057
+ if typ == nil || typ.Flags()&shimchecker.TypeFlagsObject == 0 || typ.ObjectFlags()&shimchecker.ObjectFlagsReference == 0 {
2058
+ return nil
2059
+ }
2060
+ if args := shimchecker.Checker_getTypeArguments(prog.Checker, typ); len(args) != 0 {
2061
+ header = args[0]
2062
+ }
2063
+ case "Header":
2064
+ header = typ
2065
+ default:
2066
+ return nil
2067
+ }
2068
+ if header == nil || nestiaSDKIsConnectionHeader(header) {
2069
+ return nil
2070
+ }
2071
+ return fmt.Errorf(
2072
+ "@WebSocketRoute.%s() parameter %q has the header type %q, which the SDK cannot send: it carries the handshake header as connection.headers, which must be an object type or undefined. Use an object type, or undefined for no header.",
2073
+ category,
2074
+ nestiaSDKParameterName(param),
2075
+ prog.Checker.TypeToString(header),
2076
+ )
2077
+ }
2078
+
2079
+ // nestiaSDKIsConnectionHeader reports whether typ is assignable to
2080
+ // `object | undefined`, the constraint of IConnection's Headers: any, never,
2081
+ // undefined, an object type, or a union of them; an intersection only when
2082
+ // every member is an object type, so `string & {}` stays a string.
2083
+ func nestiaSDKIsConnectionHeader(typ *shimchecker.Type) bool {
2084
+ flags := typ.Flags()
2085
+ if flags&(shimchecker.TypeFlagsAny|shimchecker.TypeFlagsNever|shimchecker.TypeFlagsUndefined|shimchecker.TypeFlagsObject|shimchecker.TypeFlagsNonPrimitive) != 0 {
2086
+ return true
2087
+ }
2088
+ if flags&shimchecker.TypeFlagsUnion != 0 {
2089
+ for _, elem := range typ.AsUnionOrIntersectionType().Types() {
2090
+ if nestiaSDKIsConnectionHeader(elem) == false {
2091
+ return false
2092
+ }
2093
+ }
2094
+ return true
2095
+ }
2096
+ if flags&shimchecker.TypeFlagsIntersection != 0 {
2097
+ for _, elem := range typ.AsUnionOrIntersectionType().Types() {
2098
+ if elem.Flags()&(shimchecker.TypeFlagsObject|shimchecker.TypeFlagsNonPrimitive) == 0 {
2099
+ return false
2100
+ }
2101
+ }
2102
+ return true
2103
+ }
2104
+ return false
2105
+ }
2106
+
2107
+ // nestiaSDKWebSocketParameterType reflects an @WebSocketRoute.Acceptor() or
2108
+ // .Driver() parameter annotated through a type alias or an import type as the
2109
+ // tgrid reference it spells, such as `WebSocketAcceptor<Header, Provider,
2110
+ // Listener>`, because the generated client needs those type arguments. Each
2111
+ // argument is reflected in the file that writes it, and one naming a type
2112
+ // parameter of a generic alias is replaced by what the annotation passes for
2113
+ // it. It returns nil for an annotation writing the tgrid reference itself as a
2114
+ // type reference, and an error for an alias using its type parameter inside an
2115
+ // argument, such as `IRoom<P>`, which no written node spells.
2116
+ func nestiaSDKWebSocketParameterType(
2117
+ context *nestiaSDKContext,
2118
+ param *shimast.Node,
2119
+ ) (map[string]any, []any, error) {
2120
+ category := transform.NestiaCoreWebSocketParameterCategory(context.prog, param)
2121
+ switch category {
2122
+ case "Acceptor", "Driver":
2123
+ default:
2124
+ return nil, nil, nil
2125
+ }
2126
+ chain, name := transform.NestiaCoreWebSocketTypeReference(context.prog, nestiaSDKParameterTypeNode(param))
2127
+ // the annotation's own reflection serves a tgrid reference written as a
2128
+ // plain type reference, but reflects an import type without its arguments
2129
+ if len(chain) == 0 || (len(chain) == 1 && chain[0].Kind == shimast.KindTypeReference) {
2130
+ return nil, nil, nil
2131
+ }
2132
+ args := []any{}
2133
+ groups := [][]any{}
2134
+ if target := chain[len(chain)-1].TypeArgumentList(); target != nil {
2135
+ for _, node := range target.Nodes {
2136
+ argument := nestiaSDKWebSocketTypeArgument(context.prog, chain, node)
2137
+ if argument == nil {
2138
+ return nil, nil, fmt.Errorf(
2139
+ "@WebSocketRoute.%s() parameter %q is typed by a type alias whose %s type argument %q uses a type parameter of the alias inside it, which the SDK cannot write. Pass each type parameter as a whole type argument, or write the %s type directly.",
2140
+ category,
2141
+ nestiaSDKParameterName(param),
2142
+ name,
2143
+ nestiaSDKTypeNodeText(node),
2144
+ name,
2145
+ )
2146
+ }
2147
+ imports := context.imports(shimast.GetSourceFileOfNode(argument))
2148
+ arg, refs, ok := nestiaSDKReflectTypeNode(context.prog, imports, argument)
2149
+ if ok == false {
2150
+ text := nestiaSDKTypeNodeText(argument)
2151
+ arg = map[string]any{"name": text}
2152
+ refs = nestiaSDKReflectImports(text, imports)
2153
+ }
2154
+ args = append(args, arg)
2155
+ groups = append(groups, refs)
2156
+ }
2157
+ }
2158
+ refs := nestiaSDKMergeImportLiterals(groups...)
2159
+ if refs == nil {
2160
+ refs = []any{}
2161
+ }
2162
+ return map[string]any{
2163
+ "name": name,
2164
+ "typeArguments": args,
2165
+ }, refs, nil
2166
+ }
2167
+
2168
+ // nestiaSDKWebSocketTypeArgument follows a type argument of the last reference
2169
+ // in chain back through the aliases while it names a type parameter of the
2170
+ // alias it is written in, to what the reference before passes for it or else
2171
+ // to the parameter's default. It returns nil when the argument uses such a
2172
+ // type parameter any other way, which no written node spells.
2173
+ func nestiaSDKWebSocketTypeArgument(prog *driver.Program, chain []*shimast.Node, node *shimast.Node) *shimast.Node {
2174
+ for level := len(chain) - 1; level > 0; {
2175
+ alias := nestiaSDKEnclosingTypeAlias(chain[level])
2176
+ if alias == nil {
2177
+ return nil
2178
+ }
2179
+ parameters := alias.AsTypeAliasDeclaration().TypeParameters
2180
+ index := nestiaSDKTypeParameterIndex(prog, parameters, node)
2181
+ if index == -1 {
2182
+ if nestiaSDKUsesAliasTypeParameter(prog, node) {
2183
+ return nil
2184
+ }
2185
+ return node
2186
+ }
2187
+ // the reference naming the alias, a type reference or an import type
2188
+ reference := chain[level-1].TypeArgumentList()
2189
+ if reference != nil && index < len(reference.Nodes) {
2190
+ node = reference.Nodes[index]
2191
+ level--
2192
+ continue
2193
+ }
2194
+ // a default may name an earlier parameter of the same alias
2195
+ node = parameters.Nodes[index].AsTypeParameterDeclaration().DefaultType
2196
+ if node == nil {
2197
+ return nil
2198
+ }
2199
+ }
2200
+ return node
2201
+ }
2202
+
2203
+ func nestiaSDKEnclosingTypeAlias(node *shimast.Node) *shimast.Node {
2204
+ for parent := node.Parent; parent != nil; parent = parent.Parent {
2205
+ if parent.Kind == shimast.KindTypeAliasDeclaration {
2206
+ return parent
2207
+ }
2208
+ }
2209
+ return nil
2210
+ }
2211
+
2212
+ // nestiaSDKTypeParameterIndex is the position of the type parameter node names
2213
+ // by itself, such as `P` or `(P)`, among parameters, or -1.
2214
+ func nestiaSDKTypeParameterIndex(prog *driver.Program, parameters *shimast.NodeList, node *shimast.Node) int {
2215
+ for node != nil && node.Kind == shimast.KindParenthesizedType {
2216
+ node = node.AsParenthesizedTypeNode().Type
2217
+ }
2218
+ if parameters == nil || node == nil || node.Kind != shimast.KindTypeReference {
2219
+ return -1
2220
+ }
2221
+ if arguments := node.AsTypeReferenceNode().TypeArguments; arguments != nil && len(arguments.Nodes) != 0 {
2222
+ return -1
2223
+ }
2224
+ symbol := prog.Checker.GetSymbolAtLocation(node.AsTypeReferenceNode().TypeName)
2225
+ if symbol == nil || symbol.Flags&shimast.SymbolFlagsTypeParameter == 0 {
2226
+ return -1
2227
+ }
2228
+ for index, parameter := range parameters.Nodes {
2229
+ for _, declaration := range symbol.Declarations {
2230
+ if declaration == parameter {
2231
+ return index
2232
+ }
2233
+ }
2234
+ }
2235
+ return -1
2236
+ }
2237
+
2238
+ // nestiaSDKUsesAliasTypeParameter reports whether node refers to a type
2239
+ // parameter a type alias declares anywhere inside it.
2240
+ func nestiaSDKUsesAliasTypeParameter(prog *driver.Program, node *shimast.Node) bool {
2241
+ if node == nil {
2242
+ return false
2243
+ }
2244
+ if node.Kind == shimast.KindTypeReference {
2245
+ symbol := prog.Checker.GetSymbolAtLocation(node.AsTypeReferenceNode().TypeName)
2246
+ if symbol != nil && symbol.Flags&shimast.SymbolFlagsTypeParameter != 0 {
2247
+ for _, declaration := range symbol.Declarations {
2248
+ if declaration != nil && declaration.Parent != nil && declaration.Parent.Kind == shimast.KindTypeAliasDeclaration {
2249
+ return true
2250
+ }
2251
+ }
2252
+ }
2253
+ }
2254
+ found := false
2255
+ node.ForEachChild(func(child *shimast.Node) bool {
2256
+ found = nestiaSDKUsesAliasTypeParameter(prog, child)
2257
+ return found
2258
+ })
2259
+ return found
1513
2260
  }
1514
2261
 
2262
+ // nestiaSDKParameterName reads a parameter's identifier, or "" for a
2263
+ // destructuring pattern such as `{ organizationId }`, which declares no name
2264
+ // the SDK could reuse; the SDK names such a parameter after its role.
1515
2265
  func nestiaSDKParameterName(node *shimast.Node) string {
1516
- if node == nil || node.Name() == nil {
2266
+ if node == nil || node.Name() == nil || node.Name().Kind != shimast.KindIdentifier {
1517
2267
  return ""
1518
2268
  }
1519
- return strings.Trim(node.Name().Text(), "\"'")
2269
+ return node.Name().Text()
1520
2270
  }
1521
2271
  func nestiaSDKParameterTypeNode(node *shimast.Node) *shimast.Node {
1522
2272
  if node != nil && node.AsParameterDeclaration() != nil {
@@ -1640,7 +2390,9 @@ type sdkOperationMetadataInsertResult struct {
1640
2390
  func nestiaSDKDiagnostic(site nestiaSDKSite, message string) transform.Diagnostic {
1641
2391
  line, column := 0, 0
1642
2392
  if site.File != nil && site.Method != nil {
1643
- if pos := site.Method.Pos(); pos >= 0 {
2393
+ // the first token's position: Pos() starts at the leading trivia, the
2394
+ // previous line's end and any JSDoc
2395
+ if pos := shimscanner.GetTokenPosOfNode(site.Method, site.File, false); pos >= 0 {
1644
2396
  l, c := shimscanner.GetECMALineAndByteOffsetOfPosition(site.File, pos)
1645
2397
  line, column = l+1, c+1
1646
2398
  }