jararaca 0.2.37a12__py3-none-any.whl → 0.4.0a5__py3-none-any.whl

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 (91) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +267 -15
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +106 -0
  5. jararaca/broker_backend/mapper.py +25 -0
  6. jararaca/broker_backend/redis_broker_backend.py +168 -0
  7. jararaca/cli.py +840 -103
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +55 -16
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +5 -1
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +90 -85
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
  23. jararaca/messagebus/message.py +31 -0
  24. jararaca/messagebus/publisher.py +47 -4
  25. jararaca/messagebus/worker.py +1615 -135
  26. jararaca/microservice.py +248 -36
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +177 -16
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +8 -2
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +213 -18
  34. jararaca/persistence/base.py +40 -3
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +74 -32
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +170 -82
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +120 -41
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +34 -4
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +6 -2
  55. jararaca/presentation/websocket/websocket_interceptor.py +74 -23
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +81 -0
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +76 -0
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +758 -0
  69. jararaca/scheduler/decorators.py +89 -28
  70. jararaca/scheduler/types.py +11 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +10 -4
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1126 -189
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +372 -0
  79. jararaca/utils/retry.py +148 -0
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca/messagebus/types.py +0 -30
  87. jararaca/scheduler/scheduler.py +0 -154
  88. jararaca/tools/metadata.py +0 -47
  89. jararaca-0.2.37a12.dist-info/RECORD +0 -63
  90. /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
  91. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import inspect
2
6
  import re
3
7
  import typing
@@ -8,7 +12,7 @@ from datetime import date, datetime, time
8
12
  from decimal import Decimal
9
13
  from enum import Enum
10
14
  from io import StringIO
11
- from types import NoneType, UnionType
15
+ from types import FunctionType, NoneType, UnionType
12
16
  from typing import (
13
17
  IO,
14
18
  Annotated,
@@ -24,17 +28,27 @@ from typing import (
24
28
  from uuid import UUID
25
29
 
26
30
  from fastapi import Request, Response, UploadFile
27
- from fastapi.params import Body, Cookie, Depends, Header, Path, Query
31
+ from fastapi.params import Body, Cookie, Depends, Form, Header, Path, Query
28
32
  from fastapi.security.http import HTTPBase
29
- from pydantic import BaseModel, PlainValidator
30
- from pydantic_core import PydanticUndefinedType
33
+ from pydantic import BaseModel, PlainValidator, RootModel
34
+ from pydantic_core import PydanticUndefined
31
35
 
32
36
  from jararaca.microservice import Microservice
33
- from jararaca.presentation.decorators import HttpMapping, RestController
37
+ from jararaca.presentation.decorators import HttpMapping, RestController, UseMiddleware
34
38
  from jararaca.presentation.websocket.decorators import RegisterWebSocketMessage
35
39
  from jararaca.presentation.websocket.websocket_interceptor import (
36
40
  WebSocketMessageWrapper,
37
41
  )
42
+ from jararaca.reflect.decorators import (
43
+ resolve_bound_method_decorator,
44
+ resolve_method_decorators,
45
+ )
46
+ from jararaca.tools.typescript.decorators import (
47
+ ExposeType,
48
+ MutationEndpoint,
49
+ QueryEndpoint,
50
+ SplitInputOutput,
51
+ )
38
52
 
39
53
  CONSTANT_PATTERN = re.compile(r"^[A-Z_]+$")
40
54
 
@@ -43,6 +57,206 @@ def is_constant(name: str) -> bool:
43
57
  return CONSTANT_PATTERN.match(name) is not None
44
58
 
45
59
 
60
+ def unwrap_annotated_type(field_type: Any) -> tuple[Any, list[Any]]:
61
+ """
62
+ Recursively unwrap Annotated types to find the real underlying type.
63
+
64
+ Args:
65
+ field_type: The type to unwrap, which may be deeply nested Annotated types
66
+
67
+ Returns:
68
+ A tuple of (unwrapped_type, all_metadata) where:
69
+ - unwrapped_type is the final non-Annotated type
70
+ - all_metadata is a list of all metadata from all Annotated layers
71
+ """
72
+ all_metadata = []
73
+ current_type = field_type
74
+
75
+ while get_origin(current_type) == Annotated:
76
+ # Collect metadata from current layer
77
+ if hasattr(current_type, "__metadata__"):
78
+ all_metadata.extend(current_type.__metadata__)
79
+
80
+ # Move to the next inner type
81
+ if hasattr(current_type, "__args__") and len(current_type.__args__) > 0:
82
+ current_type = current_type.__args__[0]
83
+ else:
84
+ break
85
+
86
+ return current_type, all_metadata
87
+
88
+
89
+ def is_upload_file_type(field_type: Any) -> bool:
90
+ """
91
+ Check if a type is UploadFile or a list/array of UploadFile.
92
+
93
+ Args:
94
+ field_type: The type to check
95
+
96
+ Returns:
97
+ True if it's UploadFile or list[UploadFile], False otherwise
98
+ """
99
+ if field_type == UploadFile:
100
+ return True
101
+
102
+ # Check for list[UploadFile], List[UploadFile], etc.
103
+ origin = get_origin(field_type)
104
+ if origin in (list, frozenset, set):
105
+ args = getattr(field_type, "__args__", ())
106
+ if args and args[0] == UploadFile:
107
+ return True
108
+
109
+ return False
110
+
111
+
112
+ def should_exclude_field(
113
+ field_name: str, field_type: Any, basemodel_type: Type[Any]
114
+ ) -> bool:
115
+ """
116
+ Check if a field should be excluded from TypeScript interface generation.
117
+
118
+ Args:
119
+ field_name: The name of the field
120
+ field_type: The type annotation of the field
121
+ basemodel_type: The BaseModel class containing the field
122
+
123
+ Returns:
124
+ True if the field should be excluded, False otherwise
125
+ """
126
+ # Check if field is private (starts with underscore)
127
+ if field_name.startswith("_"):
128
+ return True
129
+
130
+ # Check if field has Pydantic Field annotation and is excluded via model_fields
131
+ if (
132
+ hasattr(basemodel_type, "model_fields")
133
+ and field_name in basemodel_type.model_fields
134
+ ):
135
+ field_info = basemodel_type.model_fields[field_name]
136
+
137
+ # Check if field is excluded via Field(exclude=True)
138
+ if hasattr(field_info, "exclude") and field_info.exclude:
139
+ return True
140
+
141
+ # Check if field is marked as private via Field(..., alias=None) pattern
142
+ if (
143
+ hasattr(field_info, "alias")
144
+ and field_info.alias is None
145
+ and field_name.startswith("_")
146
+ ):
147
+ return True
148
+
149
+ # Check for Annotated types with Field metadata
150
+ if get_origin(field_type) == Annotated:
151
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
152
+ for metadata in all_metadata:
153
+ # Check if this is a Pydantic Field by looking for expected attributes
154
+ if hasattr(metadata, "exclude") or hasattr(metadata, "alias"):
155
+ # Check if Field has exclude=True
156
+ if hasattr(metadata, "exclude") and metadata.exclude:
157
+ return True
158
+ # Check for private fields with alias=None
159
+ if (
160
+ hasattr(metadata, "alias")
161
+ and metadata.alias is None
162
+ and field_name.startswith("_")
163
+ ):
164
+ return True
165
+
166
+ # Check for Field instances assigned as default values
167
+ # This handles cases like: field_name: str = Field(exclude=True)
168
+ if (
169
+ hasattr(basemodel_type, "__annotations__")
170
+ and field_name in basemodel_type.__annotations__
171
+ ):
172
+ # Check if there's a default value that's a Field instance
173
+ if hasattr(basemodel_type, field_name):
174
+ default_value = getattr(basemodel_type, field_name, None)
175
+ # Check if default value has Field-like attributes (duck typing approach)
176
+ if default_value is not None and hasattr(default_value, "exclude"):
177
+ if getattr(default_value, "exclude", False):
178
+ return True
179
+ # Check for private fields with alias=None in default Field
180
+ if (
181
+ default_value is not None
182
+ and hasattr(default_value, "alias")
183
+ and getattr(default_value, "alias", None) is None
184
+ and field_name.startswith("_")
185
+ ):
186
+ return True
187
+
188
+ return False
189
+
190
+
191
+ def has_default_value(
192
+ field_name: str, field_type: Any, basemodel_type: Type[Any]
193
+ ) -> bool:
194
+ """
195
+ Check if a field has a default value (making it optional in TypeScript).
196
+
197
+ Args:
198
+ field_name: The name of the field
199
+ field_type: The type annotation of the field
200
+ basemodel_type: The BaseModel class containing the field
201
+
202
+ Returns:
203
+ True if the field has a default value, False otherwise
204
+ """
205
+ # Skip literal types as they don't have defaults in the traditional sense
206
+ if get_origin(field_type) is Literal:
207
+ return False
208
+
209
+ # Check if field has default in model_fields (standard Pydantic way)
210
+ if (
211
+ hasattr(basemodel_type, "model_fields")
212
+ and field_name in basemodel_type.model_fields
213
+ ):
214
+ field_info = basemodel_type.model_fields[field_name]
215
+ if field_info.default is not PydanticUndefined:
216
+ return True
217
+
218
+ # Check for Field instances assigned as default values
219
+ # This handles cases like: field_name: str = Field(default="value")
220
+ if (
221
+ hasattr(basemodel_type, "__annotations__")
222
+ and field_name in basemodel_type.__annotations__
223
+ ):
224
+ if hasattr(basemodel_type, field_name):
225
+ default_value = getattr(basemodel_type, field_name, None)
226
+ # Check if it's a Field instance with a default
227
+ if default_value is not None and hasattr(default_value, "default"):
228
+ # Check if the Field has a default value set
229
+ field_default = getattr(default_value, "default", PydanticUndefined)
230
+ if field_default is not PydanticUndefined:
231
+ return True
232
+
233
+ # Check for non-Field default values assigned directly to class attributes
234
+ # This handles cases like: field_name: str = "default_value"
235
+ if hasattr(basemodel_type, field_name):
236
+ default_value = getattr(basemodel_type, field_name, None)
237
+ # If it's not a Field instance but has a value, it's a default
238
+ if (
239
+ default_value is not None
240
+ and not hasattr(default_value, "exclude") # Not a Field instance
241
+ and not hasattr(default_value, "alias")
242
+ ): # Not a Field instance
243
+ return True
244
+
245
+ # Check for Annotated types with Field metadata that have defaults
246
+ if get_origin(field_type) == Annotated:
247
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
248
+ for metadata in all_metadata:
249
+ # Check if this is a Pydantic Field with a default
250
+ if hasattr(metadata, "default") and hasattr(
251
+ metadata, "exclude"
252
+ ): # Ensure it's a Field
253
+ field_default = getattr(metadata, "default", PydanticUndefined)
254
+ if field_default is not PydanticUndefined:
255
+ return True
256
+
257
+ return False
258
+
259
+
46
260
  class ParseContext:
47
261
  def __init__(self) -> None:
48
262
  self.mapped_types: set[Any] = set()
@@ -67,6 +281,13 @@ def snake_to_camel(snake_str: str) -> str:
67
281
  return components[0] + "".join(x.title() for x in components[1:])
68
282
 
69
283
 
284
+ def pascal_to_camel(pascal_str: str) -> str:
285
+ """Convert a PascalCase string to camelCase."""
286
+ if not pascal_str:
287
+ return pascal_str
288
+ return pascal_str[0].lower() + pascal_str[1:]
289
+
290
+
70
291
  def parse_literal_value(value: Any) -> str:
71
292
  if value is None:
72
293
  return "null"
@@ -74,17 +295,80 @@ def parse_literal_value(value: Any) -> str:
74
295
  use_parse_context().mapped_types.add(value.__class__)
75
296
  return f"{value.__class__.__name__}.{value.name}"
76
297
  if isinstance(value, str):
77
- return f'"{value}"'
78
- if isinstance(value, int):
79
- return str(value)
298
+ # Properly escape quotes for TypeScript string literals
299
+ escaped_value = value.replace("\\", "\\\\").replace('"', '\\"')
300
+ return f'"{escaped_value}"'
80
301
  if isinstance(value, float):
81
302
  return str(value)
82
303
  if isinstance(value, bool):
83
- return str(value).lower()
304
+ # Ensure Python's True/False are properly converted to JavaScript's true/false
305
+ return "true" if value else "false"
306
+ # Special handling for Python symbols that might appear in literal types
307
+ if value is True:
308
+ return "true"
309
+ if value is False:
310
+ return "false"
311
+ if value is None:
312
+ return "null"
313
+ if isinstance(value, int):
314
+ return str(value)
84
315
  return "unknown"
85
316
 
86
317
 
87
- def get_field_type_for_ts(field_type: Any) -> Any:
318
+ def get_generic_type_mapping(controller_class: type) -> dict[Any, Any]:
319
+ mapping: dict[Any, Any] = {}
320
+ if not hasattr(controller_class, "__orig_bases__"):
321
+ return mapping
322
+
323
+ for base in controller_class.__orig_bases__:
324
+ origin = get_origin(base)
325
+ if origin and hasattr(origin, "__parameters__"):
326
+ args = typing.get_args(base)
327
+ params = origin.__parameters__
328
+ if len(args) == len(params):
329
+ for param, arg in zip(params, args):
330
+ mapping[param] = arg
331
+ return mapping
332
+
333
+
334
+ def get_field_type_for_ts(
335
+ field_type: Any,
336
+ context_suffix: str = "",
337
+ type_mapping: dict[Any, Any] | None = None,
338
+ ) -> Any:
339
+ """
340
+ Convert a Python type to its TypeScript equivalent.
341
+
342
+ Args:
343
+ field_type: The Python type to convert
344
+ context_suffix: Suffix for split models (e.g., "Input", "Output")
345
+ type_mapping: Mapping of TypeVars to concrete types
346
+ """
347
+ if type_mapping and field_type in type_mapping:
348
+ return get_field_type_for_ts(
349
+ type_mapping[field_type], context_suffix, type_mapping
350
+ )
351
+
352
+ # Handle RootModel types - use the wrapped type directly
353
+ if inspect.isclass(field_type) and issubclass(field_type, RootModel):
354
+ # For concrete RootModel subclasses, get the wrapped type from annotations
355
+ if (
356
+ hasattr(field_type, "__annotations__")
357
+ and "root" in field_type.__annotations__
358
+ ):
359
+ wrapped_type = field_type.__annotations__["root"]
360
+ return get_field_type_for_ts(wrapped_type, context_suffix, type_mapping)
361
+
362
+ # For parameterized RootModel[T] types, get the type from pydantic metadata
363
+ if hasattr(field_type, "__pydantic_generic_metadata__"):
364
+ metadata = field_type.__pydantic_generic_metadata__
365
+ if metadata.get("origin") is RootModel and metadata.get("args"):
366
+ # Get the first (and only) type argument
367
+ wrapped_type = metadata["args"][0]
368
+ return get_field_type_for_ts(wrapped_type, context_suffix, type_mapping)
369
+
370
+ return "unknown"
371
+
88
372
  if field_type is Response:
89
373
  return "unknown"
90
374
  if field_type is Any:
@@ -112,17 +396,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
112
396
  if field_type == Decimal:
113
397
  return "number"
114
398
  if get_origin(field_type) == ClassVar:
115
- return get_field_type_for_ts(field_type.__args__[0])
399
+ return get_field_type_for_ts(
400
+ field_type.__args__[0], context_suffix, type_mapping
401
+ )
116
402
  if get_origin(field_type) == tuple:
117
- return f"[{', '.join([get_field_type_for_ts(field) for field in field_type.__args__])}]"
403
+ return f"[{', '.join([get_field_type_for_ts(field, context_suffix, type_mapping) for field in field_type.__args__])}]"
118
404
  if get_origin(field_type) == list or get_origin(field_type) == frozenset:
119
- return f"Array<{get_field_type_for_ts(field_type.__args__[0])}>"
405
+ return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}>"
120
406
  if get_origin(field_type) == set:
121
- return f"Array<{get_field_type_for_ts(field_type.__args__[0])}> // Set"
407
+ return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}> // Set"
122
408
  if get_origin(field_type) == dict:
123
- return f"{{[key: {get_field_type_for_ts(field_type.__args__[0])}]: {get_field_type_for_ts(field_type.__args__[1])}}}"
409
+ return f"{{[key: {get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}]: {get_field_type_for_ts(field_type.__args__[1], context_suffix, type_mapping)}}}"
124
410
  if inspect.isclass(field_type):
125
411
  if not hasattr(field_type, "__pydantic_generic_metadata__"):
412
+ # Check if this is a split model and use appropriate suffix
413
+ if SplitInputOutput.is_split_model(field_type) and context_suffix:
414
+ return f"{field_type.__name__}{context_suffix}"
126
415
  return field_type.__name__
127
416
  pydantic_metadata = getattr(field_type, "__pydantic_generic_metadata__")
128
417
 
@@ -131,12 +420,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
131
420
  if pydantic_metadata.get("origin") is not None
132
421
  else field_type.__name__
133
422
  )
423
+
424
+ # Check if this is a split model and use appropriate suffix
425
+ if SplitInputOutput.is_split_model(field_type) and context_suffix:
426
+ name = f"{field_type.__name__}{context_suffix}"
427
+
134
428
  args = pydantic_metadata.get("args")
135
429
 
136
430
  if len(args) > 0:
137
431
  return "%s<%s>" % (
138
432
  name,
139
- ", ".join([get_field_type_for_ts(arg) for arg in args]),
433
+ ", ".join(
434
+ [
435
+ get_field_type_for_ts(arg, context_suffix, type_mapping)
436
+ for arg in args
437
+ ]
438
+ ),
140
439
  )
141
440
 
142
441
  return name
@@ -146,16 +445,25 @@ def get_field_type_for_ts(field_type: Any) -> Any:
146
445
  if get_origin(field_type) == Literal:
147
446
  return " | ".join([parse_literal_value(x) for x in field_type.__args__])
148
447
  if get_origin(field_type) == UnionType or get_origin(field_type) == typing.Union:
149
- return " | ".join([get_field_type_for_ts(x) for x in field_type.__args__])
448
+ return " | ".join(
449
+ [
450
+ get_field_type_for_ts(x, context_suffix, type_mapping)
451
+ for x in field_type.__args__
452
+ ]
453
+ )
150
454
  if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
455
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
456
+
151
457
  if (
152
458
  plain_validator := next(
153
- (x for x in field_type.__metadata__ if isinstance(x, PlainValidator)),
459
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
154
460
  None,
155
461
  )
156
462
  ) is not None:
157
- return get_field_type_for_ts(plain_validator.json_schema_input_type)
158
- return get_field_type_for_ts(field_type.__args__[0])
463
+ return get_field_type_for_ts(
464
+ plain_validator.json_schema_input_type, context_suffix, type_mapping
465
+ )
466
+ return get_field_type_for_ts(unwrapped_type, context_suffix, type_mapping)
159
467
  return "unknown"
160
468
 
161
469
 
@@ -170,6 +478,59 @@ def get_generic_args(field_type: Any) -> Any:
170
478
  def parse_type_to_typescript_interface(
171
479
  basemodel_type: Type[Any],
172
480
  ) -> tuple[set[type], str]:
481
+ """
482
+ Parse a Pydantic model into TypeScript interface(s).
483
+
484
+ If the model is decorated with @SplitInputOutput, it generates both Input and Output interfaces.
485
+ Otherwise, it generates a single interface.
486
+ """
487
+ # Check if this model should be split into Input/Output interfaces
488
+ if SplitInputOutput.is_split_model(basemodel_type):
489
+ return parse_split_input_output_interfaces(basemodel_type)
490
+
491
+ return parse_single_typescript_interface(basemodel_type)
492
+
493
+
494
+ def parse_split_input_output_interfaces(
495
+ basemodel_type: Type[Any],
496
+ ) -> tuple[set[type], str]:
497
+ """
498
+ Generate both Input and Output TypeScript interfaces for a split model.
499
+ """
500
+ mapped_types: set[type] = set()
501
+ combined_output = StringIO()
502
+
503
+ # Generate Input interface (with optional fields)
504
+ input_mapped_types, input_interface = parse_single_typescript_interface(
505
+ basemodel_type, interface_suffix="Input", force_optional_defaults=True
506
+ )
507
+ mapped_types.update(input_mapped_types)
508
+ combined_output.write(input_interface)
509
+
510
+ # Generate Output interface (all fields required as they come from the backend)
511
+ output_mapped_types, output_interface = parse_single_typescript_interface(
512
+ basemodel_type, interface_suffix="Output", force_optional_defaults=False
513
+ )
514
+ mapped_types.update(output_mapped_types)
515
+ combined_output.write(output_interface)
516
+
517
+ return mapped_types, combined_output.getvalue()
518
+
519
+
520
+ def parse_single_typescript_interface(
521
+ basemodel_type: Type[Any],
522
+ interface_suffix: str = "",
523
+ force_optional_defaults: bool | None = None,
524
+ ) -> tuple[set[type], str]:
525
+ """
526
+ Generate a single TypeScript interface for a Pydantic model.
527
+
528
+ Args:
529
+ basemodel_type: The Pydantic model class
530
+ interface_suffix: Suffix to add to the interface name (e.g., "Input", "Output")
531
+ force_optional_defaults: If True, fields with defaults are optional. If False, all fields are required.
532
+ If None, uses the default behavior (fields with defaults are optional).
533
+ """
173
534
  string_builder = StringIO()
174
535
  mapped_types: set[type] = set()
175
536
 
@@ -193,10 +554,11 @@ def parse_type_to_typescript_interface(
193
554
  mapped_types.update(inherited_classes)
194
555
 
195
556
  if Enum in inherited_classes:
557
+ enum_values = sorted([(x._name_, x.value) for x in basemodel_type])
196
558
  return (
197
559
  set(),
198
- f"export enum {basemodel_type.__name__} {{\n"
199
- + "\n ".join([f'\t{x._name_} = "{x.value}",' for x in basemodel_type])
560
+ f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
561
+ + "\n ".join([f'\t{name} = "{value}",' for name, value in enum_values])
200
562
  + "\n}\n",
201
563
  )
202
564
 
@@ -219,66 +581,84 @@ def parse_type_to_typescript_interface(
219
581
  for inherited_class in valid_inherited_classes
220
582
  }
221
583
 
222
- extends_expression = (
223
- " extends %s"
224
- % ", ".join(
225
- [
226
- (
227
- "%s" % get_field_type_for_ts(inherited_class)
228
- if not inherited_classes_consts_conflict[inherited_class]
229
- else "Omit<%s, %s>"
230
- % (
231
- get_field_type_for_ts(inherited_class),
232
- " | ".join(
233
- '"%s"' % field_name
234
- for field_name in inherited_classes_consts_conflict[
235
- inherited_class
584
+ # Modify inheritance for split interfaces
585
+ extends_expression = ""
586
+ if len(valid_inherited_classes) > 0:
587
+ extends_base_names = []
588
+ for inherited_class in valid_inherited_classes:
589
+ base_name = get_field_type_for_ts(inherited_class, interface_suffix)
590
+ # If the inherited class is also a split model, use the appropriate suffix
591
+ if SplitInputOutput.is_split_model(inherited_class) and interface_suffix:
592
+ base_name = f"{inherited_class.__name__}{interface_suffix}"
593
+
594
+ if inherited_classes_consts_conflict[inherited_class]:
595
+ base_name = "Omit<%s, %s>" % (
596
+ base_name,
597
+ " | ".join(
598
+ sorted(
599
+ [
600
+ '"%s"' % field_name
601
+ for field_name in inherited_classes_consts_conflict[
602
+ inherited_class
603
+ ]
236
604
  ]
237
- ),
238
- )
605
+ )
606
+ ),
239
607
  )
240
- for inherited_class in valid_inherited_classes
241
- ]
608
+ extends_base_names.append(base_name)
609
+
610
+ extends_expression = " extends %s" % ", ".join(
611
+ sorted(extends_base_names, key=lambda x: str(x))
242
612
  )
243
- if len(valid_inherited_classes) > 0
244
- else ""
245
- )
613
+
614
+ interface_name = f"{basemodel_type.__name__}{interface_suffix}"
246
615
 
247
616
  if is_generic_type(basemodel_type):
617
+ generic_args = get_generic_args(basemodel_type)
248
618
  string_builder.write(
249
- f"export interface {basemodel_type.__name__}<{', '.join([arg.__name__ for arg in get_generic_args(basemodel_type)])}>{extends_expression} {{\n"
619
+ f"export interface {interface_name}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
250
620
  )
251
621
  else:
252
622
  string_builder.write(
253
- f"export interface {basemodel_type.__name__}{extends_expression} {{\n"
623
+ f"export interface {interface_name}{extends_expression} {{\n"
254
624
  )
255
625
 
256
626
  if hasattr(basemodel_type, "__annotations__"):
257
- # for field_name in (f for f in dir(basemodel_type) if is_constant(f)):
258
- # field = getattr(basemodel_type, field_name)
259
- # if field is None:
260
- # continue
261
- # string_builder.write(f" {field_name}: {parse_literal_value(field)};\n")
262
- for field_name, field in basemodel_type.__annotations__.items():
627
+ # Sort fields for consistent output
628
+ annotation_items = sorted(
629
+ basemodel_type.__annotations__.items(), key=lambda x: x[0]
630
+ )
631
+
632
+ for field_name, field in annotation_items:
263
633
  if field_name in cls_consts:
264
634
  continue
265
635
 
266
- has_default_value = (
267
- get_origin(field) is not Literal
268
- and field_name in basemodel_type.model_fields
269
- and not isinstance(
270
- basemodel_type.model_fields[field_name].default,
271
- PydanticUndefinedType,
272
- )
273
- )
636
+ # Check if field should be excluded (private or excluded via Field)
637
+ if should_exclude_field(field_name, field, basemodel_type):
638
+ continue
639
+
640
+ # Determine if field is optional based on the force_optional_defaults parameter
641
+ if force_optional_defaults is True:
642
+ # Input interface: fields with defaults are optional
643
+ is_optional = has_default_value(field_name, field, basemodel_type)
644
+ elif force_optional_defaults is False:
645
+ # Output interface: all fields are required (backend provides complete data)
646
+ is_optional = False
647
+ else:
648
+ # Default behavior: fields with defaults are optional
649
+ is_optional = has_default_value(field_name, field, basemodel_type)
650
+
274
651
  string_builder.write(
275
- f" {snake_to_camel(field_name) if not is_constant(field_name) else field_name}{'?' if has_default_value else ''}: {get_field_type_for_ts(field)};\n"
652
+ f" {snake_to_camel(field_name) if not is_constant(field_name) else field_name}{'?' if is_optional else ''}: {get_field_type_for_ts(field, interface_suffix)};\n"
276
653
  )
277
654
  mapped_types.update(extract_all_envolved_types(field))
278
655
  mapped_types.add(field)
279
656
 
280
- ## Loop over computed fields
281
- members = inspect.getmembers(basemodel_type, lambda a: isinstance(a, property))
657
+ ## Loop over computed fields - sort them for consistent output
658
+ members = sorted(
659
+ inspect.getmembers_static(basemodel_type, lambda a: isinstance(a, property)),
660
+ key=lambda x: x[0],
661
+ )
282
662
  for field_name, field in members:
283
663
  if hasattr(field, "fget"):
284
664
  module_func_name = field.fget.__module__ + "." + field.fget.__qualname__
@@ -296,7 +676,7 @@ def parse_type_to_typescript_interface(
296
676
  return_type = NoneType
297
677
 
298
678
  string_builder.write(
299
- f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type)};\n"
679
+ f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type, interface_suffix)};\n"
300
680
  )
301
681
  mapped_types.update(extract_all_envolved_types(return_type))
302
682
  mapped_types.add(return_type)
@@ -326,20 +706,28 @@ def write_microservice_to_typescript_interface(
326
706
  websocket_registries: set[RegisterWebSocketMessage] = set()
327
707
  mapped_types_set.add(WebSocketMessageWrapper)
328
708
 
709
+ # Add all explicitly exposed types
710
+ mapped_types_set.update(ExposeType.get_all_exposed_types())
711
+
329
712
  for controller in microservice.controllers:
330
- rest_controller = RestController.get_controller(controller)
713
+ rest_controller = RestController.get_last(controller)
331
714
 
332
715
  if rest_controller is None:
333
716
  continue
334
717
 
335
- controller_class_str, types = write_rest_controller_to_typescript_interface(
336
- rest_controller, controller
718
+ controller_class_strio, types, hooks_strio = (
719
+ write_rest_controller_to_typescript_interface(
720
+ rest_controller,
721
+ controller,
722
+ )
337
723
  )
338
724
 
339
725
  mapped_types_set.update(types)
340
- rest_controller_buffer.write(controller_class_str)
726
+ rest_controller_buffer.write(controller_class_strio.getvalue())
727
+ if hooks_strio is not None:
728
+ rest_controller_buffer.write(hooks_strio.getvalue())
341
729
 
342
- registered = RegisterWebSocketMessage.get(controller)
730
+ registered = RegisterWebSocketMessage.get_last(controller)
343
731
 
344
732
  if registered is not None:
345
733
  for message_type in registered.message_types:
@@ -354,49 +742,59 @@ def write_microservice_to_typescript_interface(
354
742
 
355
743
  final_buffer.write(
356
744
  """
357
- export type WebSocketMessageMap = {
358
- %s
359
- }
360
- """
361
- % "\n".join(
362
- [
363
- f'\t"{message.MESSAGE_ID}": {message.__name__};'
364
- for registers in websocket_registries
365
- for message in registers.message_types
366
- ]
367
- )
368
- )
745
+ /* eslint-disable */
369
746
 
370
- final_buffer.write(
371
- """
372
- export type ResponseType =
373
- | "arraybuffer"
374
- | "blob"
375
- | "document"
376
- | "json"
377
- | "text"
378
- | "stream"
379
- | "formdata";
380
-
381
-
382
- export interface HttpBackendRequest {
383
- method: string;
384
- path: string;
385
- headers: { [key: string]: string };
386
- query: { [key: string]: unknown };
387
- body: unknown;
388
- responseType?: ResponseType;
747
+ // @ts-nocheck
748
+
749
+ // noinspection JSUnusedGlobalSymbols
750
+
751
+ import { HttpService, HttpBackend, HttpBackendRequest, ResponseType, createClassQueryHooks , createClassMutationHooks, createClassInfiniteQueryHooks, paginationModelByFirstArgPaginationFilter, recursiveCamelToSnakeCase } from "@jararaca/core";
752
+
753
+ function makeFormData(data: Record<string, any>): FormData {
754
+ const formData = new FormData();
755
+ for (const key in data) {
756
+ const value = data[key];
757
+ for (const v of genFormDataValue(value)) {
758
+ formData.append(key, v);
759
+ }
760
+ }
761
+ return formData;
389
762
  }
390
763
 
391
- export interface HttpBackend {
392
- request<T>(request: HttpBackendRequest): Promise<T>;
764
+ function* genFormDataValue(value: any): any {
765
+ if (Array.isArray(value)) {
766
+ // Stringify arrays as JSON
767
+ for (const item of value) {
768
+ // formData.append(`${key}`, item);
769
+ yield* genFormDataValue(item);
770
+ }
771
+ } else if (typeof value === "object" && value.constructor === Object) {
772
+ // Stringify plain objects as JSON
773
+ // formData.append(key, JSON.stringify(value));
774
+ yield JSON.stringify(
775
+ recursiveCamelToSnakeCase(value)
776
+ );
777
+ } else {
778
+ // For primitives (string, number, boolean), append as-is
779
+ yield value;
780
+ }
393
781
  }
394
782
 
395
- export abstract class HttpService {
396
- constructor(protected readonly httpBackend: HttpBackend) {}
783
+ export type WebSocketMessageMap = {
784
+ %s
397
785
  }
398
786
  """
787
+ % "\n".join(
788
+ sorted(
789
+ [
790
+ f'\t"{message.MESSAGE_ID}": {message.__name__};'
791
+ for registers in websocket_registries
792
+ for message in registers.message_types
793
+ ]
794
+ )
795
+ )
399
796
  )
797
+
400
798
  processed_types: set[Any] = set()
401
799
  backlog: set[Any] = mapped_types_set.copy()
402
800
 
@@ -469,29 +867,108 @@ def is_primitive(field_type: Any) -> bool:
469
867
 
470
868
  def write_rest_controller_to_typescript_interface(
471
869
  rest_controller: RestController, controller: type
472
- ) -> tuple[str, set[Any]]:
870
+ ) -> tuple[StringIO, set[Any], StringIO | None]:
871
+
872
+ class_name = controller.__name__
873
+
874
+ decorated_queries: list[tuple[str, FunctionType, QueryEndpoint]] = []
875
+ decorated_mutations: list[tuple[str, FunctionType]] = []
473
876
 
474
877
  class_buffer = StringIO()
475
878
 
476
- class_buffer.write(f"export class {controller.__name__} extends HttpService {{\n")
879
+ class_buffer.write(f"export class {class_name} extends HttpService {{\n")
477
880
 
478
881
  mapped_types: set[Any] = set()
479
882
 
480
- for name, member in inspect.getmembers(controller, predicate=inspect.isfunction):
481
- if (mapping := HttpMapping.get_http_mapping(member)) is not None:
883
+ # Compute type mapping for generics
884
+ type_mapping = get_generic_type_mapping(controller)
885
+
886
+ # Sort members for consistent output
887
+ member_items = sorted(
888
+ inspect.getmembers_static(controller, predicate=inspect.isfunction),
889
+ key=lambda x: x[0],
890
+ )
891
+
892
+ class_usemiddlewares = UseMiddleware.get_all_from_type(
893
+ controller, rest_controller.class_inherits_decorators
894
+ )
895
+
896
+ for name, member in member_items:
897
+ mapping = resolve_bound_method_decorator(
898
+ controller, name, HttpMapping, rest_controller.methods_inherit_decorators
899
+ )
900
+ effective_member = member
901
+
902
+ if mapping is not None:
482
903
  return_type = member.__annotations__.get("return")
483
904
 
484
905
  if return_type is None:
485
906
  return_type = NoneType
486
907
 
908
+ if query_endpoint := resolve_bound_method_decorator(
909
+ controller,
910
+ name,
911
+ QueryEndpoint,
912
+ rest_controller.methods_inherit_decorators,
913
+ ):
914
+ decorated_queries.append((name, member, query_endpoint))
915
+ if MutationEndpoint.is_mutation(effective_member):
916
+ decorated_mutations.append((name, member))
917
+
487
918
  mapped_types.update(extract_all_envolved_types(return_type))
488
919
 
489
- return_value_repr = get_field_type_for_ts(return_type)
920
+ # For return types, use Output suffix if it's a split model
921
+ return_value_repr = get_field_type_for_ts(
922
+ return_type, "Output", type_mapping
923
+ )
490
924
 
491
925
  arg_params_spec, parametes_mapped_types = extract_parameters(
492
- member, rest_controller, mapping
926
+ member, rest_controller, mapping, type_mapping
493
927
  )
494
928
 
929
+ # Collect middleware parameters separately
930
+ middleware_params_list: list[HttpParemeterSpec] = []
931
+
932
+ # Extract parameters from controller-level middlewares
933
+ for middleware_type in rest_controller.middlewares:
934
+ middleware_params, middleware_mapped_types = (
935
+ extract_middleware_parameters(
936
+ middleware_type, rest_controller, mapping
937
+ )
938
+ )
939
+ middleware_params_list.extend(middleware_params)
940
+ parametes_mapped_types.update(middleware_mapped_types)
941
+
942
+ # Extract parameters from class-level UseMiddleware decorators
943
+ for middleware_instance in class_usemiddlewares:
944
+ middleware_params, middleware_mapped_types = (
945
+ extract_middleware_parameters(
946
+ middleware_instance.middleware, rest_controller, mapping
947
+ )
948
+ )
949
+ middleware_params_list.extend(middleware_params)
950
+ parametes_mapped_types.update(middleware_mapped_types)
951
+
952
+ # Extract parameters from method-level middlewares (UseMiddleware)
953
+ # Get the method from the class to access its middleware decorators
954
+ method_middlewares = resolve_method_decorators(
955
+ controller,
956
+ name,
957
+ UseMiddleware,
958
+ rest_controller.methods_inherit_decorators,
959
+ )
960
+ for middleware_instance in method_middlewares:
961
+ middleware_params, middleware_mapped_types = (
962
+ extract_middleware_parameters(
963
+ middleware_instance.middleware, rest_controller, mapping
964
+ )
965
+ )
966
+ middleware_params_list.extend(middleware_params)
967
+ parametes_mapped_types.update(middleware_mapped_types)
968
+
969
+ # Combine parameters: middleware params first, then controller params
970
+ arg_params_spec = middleware_params_list + arg_params_spec
971
+
495
972
  for param in parametes_mapped_types:
496
973
  mapped_types.update(extract_all_envolved_types(param))
497
974
 
@@ -509,28 +986,73 @@ def write_rest_controller_to_typescript_interface(
509
986
  class_buffer.write(f'\t\t\tmethod: "{mapping.method}",\n')
510
987
 
511
988
  endpoint_path = parse_path_with_params(mapping.path, arg_params_spec)
512
- final_path = "/".join(
513
- s.strip("/") for s in [rest_controller.path, endpoint_path]
989
+
990
+ # Properly handle path joining to avoid double slashes
991
+ controller_path = rest_controller.path or ""
992
+ # Also apply path transformation to the controller path
993
+ controller_path = parse_path_with_params(controller_path, arg_params_spec)
994
+ path_parts = []
995
+
996
+ if controller_path and controller_path.strip("/"):
997
+ path_parts.append(controller_path.strip("/"))
998
+ if endpoint_path and endpoint_path.strip("/"):
999
+ path_parts.append(endpoint_path.strip("/"))
1000
+
1001
+ final_path = "/".join(path_parts) if path_parts else ""
1002
+ # Ensure the path starts with a single slash
1003
+ formatted_path = f"/{final_path}" if final_path else "/"
1004
+
1005
+ class_buffer.write(f"\t\t\tpath: `{formatted_path}`,\n")
1006
+
1007
+ # Sort path params
1008
+ path_params = sorted(
1009
+ [param for param in arg_params_spec if param.type_ == "path"],
1010
+ key=lambda x: x.name,
514
1011
  )
515
- class_buffer.write(f"\t\t\tpath: `/{final_path}`,\n")
1012
+ class_buffer.write("\t\t\tpathParams: {\n")
1013
+ for param in path_params:
1014
+ class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
1015
+ class_buffer.write("\t\t\t},\n")
516
1016
 
1017
+ # Sort headers
1018
+ header_params = sorted(
1019
+ [param for param in arg_params_spec if param.type_ == "header"],
1020
+ key=lambda x: x.name,
1021
+ )
517
1022
  class_buffer.write("\t\t\theaders: {\n")
518
- for param in arg_params_spec:
519
- if param.type_ == "header":
520
- class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
521
-
1023
+ for param in header_params:
1024
+ class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
522
1025
  class_buffer.write("\t\t\t},\n")
1026
+
1027
+ # Sort query params
1028
+ query_params = sorted(
1029
+ [param for param in arg_params_spec if param.type_ == "query"],
1030
+ key=lambda x: x.name,
1031
+ )
523
1032
  class_buffer.write("\t\t\tquery: {\n")
1033
+ for param in query_params:
1034
+ class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
1035
+ class_buffer.write("\t\t\t},\n")
524
1036
 
525
- for param in arg_params_spec:
526
- if param.type_ == "query":
1037
+ # Check if we need to use FormData (for file uploads or form parameters)
1038
+ form_params = [param for param in arg_params_spec if param.type_ == "form"]
1039
+ body_param = next((x for x in arg_params_spec if x.type_ == "body"), None)
1040
+
1041
+ if form_params:
1042
+ # Use FormData for file uploads and form parameters
1043
+ class_buffer.write("\t\t\tbody: makeFormData({\n")
1044
+
1045
+ # Add form parameters (including file uploads)
1046
+ for param in form_params:
527
1047
  class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
528
- class_buffer.write("\t\t\t},\n")
529
1048
 
530
- if (
531
- body := next((x for x in arg_params_spec if x.type_ == "body"), None)
532
- ) is not None:
533
- class_buffer.write(f"\t\t\tbody: {body.name}\n")
1049
+ # Add body parameter if it exists alongside form params
1050
+ if body_param:
1051
+ class_buffer.write(f"\t\t\t\t...{body_param.name},\n")
1052
+
1053
+ class_buffer.write("\t\t\t})\n")
1054
+ elif body_param is not None:
1055
+ class_buffer.write(f"\t\t\tbody: {body_param.name}\n")
534
1056
  else:
535
1057
  class_buffer.write("\t\t\tbody: undefined\n")
536
1058
 
@@ -541,7 +1063,44 @@ def write_rest_controller_to_typescript_interface(
541
1063
 
542
1064
  class_buffer.write("}\n")
543
1065
 
544
- return class_buffer.getvalue(), mapped_types
1066
+ controller_hooks_builder: StringIO | None = None
1067
+
1068
+ if decorated_queries or decorated_mutations:
1069
+ controller_hooks_builder = StringIO()
1070
+ controller_hooks_builder.write(
1071
+ f"export const {pascal_to_camel(class_name)} = {{\n"
1072
+ )
1073
+
1074
+ if decorated_queries:
1075
+ controller_hooks_builder.write(
1076
+ f"\t...createClassQueryHooks({class_name},\n"
1077
+ )
1078
+ for name, member, _ in decorated_queries:
1079
+ controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
1080
+ controller_hooks_builder.write("\t),\n")
1081
+
1082
+ if decorated_queries and any(
1083
+ query.has_infinite_query for _, _, query in decorated_queries
1084
+ ):
1085
+ controller_hooks_builder.write(
1086
+ f"\t...createClassInfiniteQueryHooks({class_name}, {{\n"
1087
+ )
1088
+ for name, member, query in decorated_queries:
1089
+ if query.has_infinite_query:
1090
+ controller_hooks_builder.write(
1091
+ f'\t\t"{snake_to_camel(name)}": paginationModelByFirstArgPaginationFilter(),\n'
1092
+ )
1093
+ controller_hooks_builder.write("\t}),\n")
1094
+ if decorated_mutations:
1095
+ controller_hooks_builder.write(
1096
+ f"\t...createClassMutationHooks({class_name},\n"
1097
+ )
1098
+ for name, member in decorated_mutations:
1099
+ controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
1100
+ controller_hooks_builder.write("\t),\n")
1101
+ controller_hooks_builder.write("};\n")
1102
+
1103
+ return class_buffer, mapped_types, controller_hooks_builder
545
1104
 
546
1105
 
547
1106
  EXCLUDED_REQUESTS_TYPES = [Request, Response]
@@ -549,18 +1108,233 @@ EXCLUDED_REQUESTS_TYPES = [Request, Response]
549
1108
 
550
1109
  @dataclass
551
1110
  class HttpParemeterSpec:
552
- type_: Literal["query", "path", "body", "header", "cookie"]
1111
+ type_: Literal["query", "path", "body", "header", "cookie", "form"]
553
1112
  name: str
554
1113
  required: bool
555
1114
  argument_type_str: str
556
1115
 
557
1116
 
558
1117
  def parse_path_with_params(path: str, parameters: list[HttpParemeterSpec]) -> str:
1118
+ # Use a regular expression to match both simple parameters {param} and
1119
+ # parameters with converters {param:converter}
1120
+ pattern = re.compile(r"{([^:}]+)(?::[^}]*)?}")
1121
+
1122
+ # For each parameter found in the path, replace it with :param format
559
1123
  for parameter in parameters:
560
- path = path.replace(f"{{{parameter.name}}}", f"${{{parameter.name}}}")
1124
+ path = pattern.sub(
1125
+ lambda m: f":{m.group(1)}" if m.group(1) == parameter.name else m.group(0),
1126
+ path,
1127
+ )
1128
+
561
1129
  return path
562
1130
 
563
1131
 
1132
+ def extract_middleware_parameters(
1133
+ middleware_type: type,
1134
+ controller: RestController,
1135
+ mapping: HttpMapping,
1136
+ ) -> tuple[list[HttpParemeterSpec], set[Any]]:
1137
+ """
1138
+ Extract parameters from a middleware class's intercept method.
1139
+ """
1140
+ parameters_list: list[HttpParemeterSpec] = []
1141
+ mapped_types: set[Any] = set()
1142
+
1143
+ # Get the intercept method from the middleware class
1144
+ if not hasattr(middleware_type, "intercept"):
1145
+ return parameters_list, mapped_types
1146
+
1147
+ intercept_method = getattr(middleware_type, "intercept")
1148
+
1149
+ # Use the same logic as extract_parameters but specifically for the intercept method
1150
+ try:
1151
+ signature = inspect.signature(intercept_method)
1152
+ for parameter_name, parameter in signature.parameters.items():
1153
+ # Skip 'self' parameter
1154
+ if parameter_name == "self":
1155
+ continue
1156
+
1157
+ parameter_type = parameter.annotation
1158
+ if parameter_type == inspect.Parameter.empty:
1159
+ continue
1160
+
1161
+ if parameter_type in EXCLUDED_REQUESTS_TYPES:
1162
+ continue
1163
+
1164
+ if get_origin(parameter_type) == Annotated:
1165
+ unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
1166
+ # Look for FastAPI parameter annotations in all metadata layers
1167
+ annotated_type_hook = None
1168
+ for metadata in all_metadata:
1169
+ if isinstance(
1170
+ metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
1171
+ ):
1172
+ annotated_type_hook = metadata
1173
+ break
1174
+
1175
+ if annotated_type_hook is None and all_metadata:
1176
+ # Fallback to first metadata if no FastAPI annotation found
1177
+ annotated_type_hook = all_metadata[0]
1178
+
1179
+ annotated_type = unwrapped_type
1180
+ if isinstance(annotated_type_hook, Header):
1181
+ mapped_types.add(str)
1182
+ parameters_list.append(
1183
+ HttpParemeterSpec(
1184
+ type_="header",
1185
+ name=parameter_name,
1186
+ required=True,
1187
+ argument_type_str=get_field_type_for_ts(str),
1188
+ )
1189
+ )
1190
+ elif isinstance(annotated_type_hook, Cookie):
1191
+ mapped_types.add(str)
1192
+ parameters_list.append(
1193
+ HttpParemeterSpec(
1194
+ type_="cookie",
1195
+ name=parameter_name,
1196
+ required=True,
1197
+ argument_type_str=get_field_type_for_ts(str),
1198
+ )
1199
+ )
1200
+ elif isinstance(annotated_type_hook, Form):
1201
+ mapped_types.add(annotated_type)
1202
+ parameters_list.append(
1203
+ HttpParemeterSpec(
1204
+ type_="form",
1205
+ name=parameter_name,
1206
+ required=True,
1207
+ argument_type_str=get_field_type_for_ts(annotated_type),
1208
+ )
1209
+ )
1210
+ elif isinstance(annotated_type_hook, Body):
1211
+ mapped_types.update(extract_all_envolved_types(parameter_type))
1212
+ # For body parameters, use Input suffix if it's a split model
1213
+ context_suffix = (
1214
+ "Input"
1215
+ if (
1216
+ inspect.isclass(parameter_type)
1217
+ and hasattr(parameter_type, "__dict__")
1218
+ and SplitInputOutput.is_split_model(parameter_type)
1219
+ )
1220
+ else ""
1221
+ )
1222
+ parameters_list.append(
1223
+ HttpParemeterSpec(
1224
+ type_="body",
1225
+ name=parameter_name,
1226
+ required=True,
1227
+ argument_type_str=get_field_type_for_ts(
1228
+ parameter_type, context_suffix
1229
+ ),
1230
+ )
1231
+ )
1232
+ elif isinstance(annotated_type_hook, Query):
1233
+ mapped_types.add(parameter_type)
1234
+ # For query parameters, use Input suffix if it's a split model
1235
+ context_suffix = (
1236
+ "Input"
1237
+ if (
1238
+ inspect.isclass(parameter_type)
1239
+ and hasattr(parameter_type, "__dict__")
1240
+ and SplitInputOutput.is_split_model(parameter_type)
1241
+ )
1242
+ else ""
1243
+ )
1244
+ parameters_list.append(
1245
+ HttpParemeterSpec(
1246
+ type_="query",
1247
+ name=parameter_name,
1248
+ required=True,
1249
+ argument_type_str=get_field_type_for_ts(
1250
+ parameter_type, context_suffix
1251
+ ),
1252
+ )
1253
+ )
1254
+ elif isinstance(annotated_type_hook, Path):
1255
+ mapped_types.add(parameter_type)
1256
+ # For path parameters, use Input suffix if it's a split model
1257
+ context_suffix = (
1258
+ "Input"
1259
+ if (
1260
+ inspect.isclass(parameter_type)
1261
+ and hasattr(parameter_type, "__dict__")
1262
+ and SplitInputOutput.is_split_model(parameter_type)
1263
+ )
1264
+ else ""
1265
+ )
1266
+ parameters_list.append(
1267
+ HttpParemeterSpec(
1268
+ type_="path",
1269
+ name=parameter_name,
1270
+ required=True,
1271
+ argument_type_str=get_field_type_for_ts(
1272
+ parameter_type, context_suffix
1273
+ ),
1274
+ )
1275
+ )
1276
+ elif isinstance(annotated_type_hook, Depends):
1277
+ # For Dependencies, recursively extract parameters
1278
+ depends_hook = (
1279
+ annotated_type_hook.dependency or parameter_type.__args__[0]
1280
+ )
1281
+ if isinstance(depends_hook, HTTPBase):
1282
+ # Skip HTTP authentication dependencies
1283
+ pass
1284
+ else:
1285
+ # TODO: We might need to recursively extract from dependencies
1286
+ # For now, skip to avoid infinite recursion
1287
+ pass
1288
+ else:
1289
+ # Handle non-annotated parameters - check if they are path parameters
1290
+ mapped_types.add(parameter_type)
1291
+
1292
+ # Check if parameter matches path parameters in controller or method paths
1293
+ if (
1294
+ # Match both simple parameters {param} and parameters with converters {param:converter}
1295
+ re.search(f"{{{parameter_name}(:.*?)?}}", controller.path)
1296
+ is not None
1297
+ or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path)
1298
+ is not None
1299
+ ):
1300
+ # This is a path parameter
1301
+ context_suffix = (
1302
+ "Input"
1303
+ if (
1304
+ inspect.isclass(parameter_type)
1305
+ and hasattr(parameter_type, "__dict__")
1306
+ and SplitInputOutput.is_split_model(parameter_type)
1307
+ )
1308
+ else ""
1309
+ )
1310
+ parameters_list.append(
1311
+ HttpParemeterSpec(
1312
+ type_="path",
1313
+ name=parameter_name,
1314
+ required=True,
1315
+ argument_type_str=get_field_type_for_ts(
1316
+ parameter_type, context_suffix
1317
+ ),
1318
+ )
1319
+ )
1320
+ elif is_primitive(parameter_type):
1321
+ # Default to query parameters for simple types that aren't in the path
1322
+ parameters_list.append(
1323
+ HttpParemeterSpec(
1324
+ type_="query",
1325
+ name=parameter_name,
1326
+ required=True,
1327
+ argument_type_str=get_field_type_for_ts(parameter_type),
1328
+ )
1329
+ )
1330
+
1331
+ except (ValueError, TypeError):
1332
+ # If we can't inspect the signature, return empty
1333
+ pass
1334
+
1335
+ return parameters_list, mapped_types
1336
+
1337
+
564
1338
  def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
565
1339
  return ", ".join(
566
1340
  [f"{parameter.name}: {parameter.argument_type_str}" for parameter in parameters]
@@ -568,7 +1342,10 @@ def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
568
1342
 
569
1343
 
570
1344
  def extract_parameters(
571
- member: Any, controller: RestController, mapping: HttpMapping
1345
+ member: Any,
1346
+ controller: RestController,
1347
+ mapping: HttpMapping,
1348
+ type_mapping: dict[Any, Any] | None = None,
572
1349
  ) -> tuple[list[HttpParemeterSpec], set[Any]]:
573
1350
  parameters_list: list[HttpParemeterSpec] = []
574
1351
  mapped_types: set[Any] = set()
@@ -577,7 +1354,7 @@ def extract_parameters(
577
1354
  if is_primitive(arg):
578
1355
  continue
579
1356
  rec_parameters, rec_mapped_types = extract_parameters(
580
- arg, controller, mapping
1357
+ arg, controller, mapping, type_mapping
581
1358
  )
582
1359
  mapped_types.update(rec_mapped_types)
583
1360
  parameters_list.extend(rec_parameters)
@@ -586,22 +1363,23 @@ def extract_parameters(
586
1363
  if is_primitive(member):
587
1364
 
588
1365
  if get_origin(member) is Annotated:
1366
+ unwrapped_type, all_metadata = unwrap_annotated_type(member)
589
1367
  if (
590
1368
  plain_validator := next(
591
- (x for x in member.__metadata__ if isinstance(x, PlainValidator)),
1369
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
592
1370
  None,
593
1371
  )
594
1372
  ) is not None:
595
1373
  mapped_types.add(plain_validator.json_schema_input_type)
596
1374
  return parameters_list, mapped_types
597
- return extract_parameters(member.__args__[0], controller, mapping)
1375
+ return extract_parameters(unwrapped_type, controller, mapping, type_mapping)
598
1376
  return parameters_list, mapped_types
599
1377
 
600
1378
  if hasattr(member, "__bases__"):
601
1379
  for base in member.__bases__:
602
1380
  # if base is not BaseModel:
603
1381
  rec_parameters, rec_mapped_types = extract_parameters(
604
- base, controller, mapping
1382
+ base, controller, mapping, type_mapping
605
1383
  )
606
1384
  mapped_types.update(rec_mapped_types)
607
1385
  parameters_list.extend(rec_parameters)
@@ -613,9 +1391,26 @@ def extract_parameters(
613
1391
  if parameter_type in EXCLUDED_REQUESTS_TYPES:
614
1392
  continue
615
1393
 
1394
+ # Resolve generic type
1395
+ if type_mapping and parameter_type in type_mapping:
1396
+ parameter_type = type_mapping[parameter_type]
1397
+
616
1398
  if get_origin(parameter_type) == Annotated:
617
- annotated_type_hook = parameter_type.__metadata__[0]
618
- annotated_type = parameter_type.__args__[0]
1399
+ unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
1400
+ # Look for FastAPI parameter annotations in all metadata layers
1401
+ annotated_type_hook = None
1402
+ for metadata in all_metadata:
1403
+ if isinstance(
1404
+ metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
1405
+ ):
1406
+ annotated_type_hook = metadata
1407
+ break
1408
+
1409
+ if annotated_type_hook is None and all_metadata:
1410
+ # Fallback to first metadata if no FastAPI annotation found
1411
+ annotated_type_hook = all_metadata[0]
1412
+
1413
+ annotated_type = unwrapped_type
619
1414
  if isinstance(annotated_type_hook, Header):
620
1415
  mapped_types.add(str)
621
1416
  parameters_list.append(
@@ -623,7 +1418,9 @@ def extract_parameters(
623
1418
  type_="header",
624
1419
  name=parameter_name,
625
1420
  required=True,
626
- argument_type_str=get_field_type_for_ts(str),
1421
+ argument_type_str=get_field_type_for_ts(
1422
+ str, "", type_mapping
1423
+ ),
627
1424
  )
628
1425
  )
629
1426
  elif isinstance(annotated_type_hook, Cookie):
@@ -633,37 +1430,87 @@ def extract_parameters(
633
1430
  type_="cookie",
634
1431
  name=parameter_name,
635
1432
  required=True,
636
- argument_type_str=get_field_type_for_ts(str),
1433
+ argument_type_str=get_field_type_for_ts(
1434
+ str, "", type_mapping
1435
+ ),
1436
+ )
1437
+ )
1438
+ elif isinstance(annotated_type_hook, Form):
1439
+ mapped_types.add(annotated_type)
1440
+ parameters_list.append(
1441
+ HttpParemeterSpec(
1442
+ type_="form",
1443
+ name=parameter_name,
1444
+ required=True,
1445
+ argument_type_str=get_field_type_for_ts(
1446
+ annotated_type, "", type_mapping
1447
+ ),
637
1448
  )
638
1449
  )
639
1450
  elif isinstance(annotated_type_hook, Body):
640
1451
  mapped_types.update(extract_all_envolved_types(parameter_type))
1452
+ # For body parameters, use Input suffix if it's a split model
1453
+ context_suffix = (
1454
+ "Input"
1455
+ if (
1456
+ inspect.isclass(parameter_type)
1457
+ and hasattr(parameter_type, "__dict__")
1458
+ and SplitInputOutput.is_split_model(parameter_type)
1459
+ )
1460
+ else ""
1461
+ )
641
1462
  parameters_list.append(
642
1463
  HttpParemeterSpec(
643
1464
  type_="body",
644
1465
  name=parameter_name,
645
1466
  required=True,
646
- argument_type_str=get_field_type_for_ts(parameter_type),
1467
+ argument_type_str=get_field_type_for_ts(
1468
+ parameter_type, context_suffix, type_mapping
1469
+ ),
647
1470
  )
648
1471
  )
649
1472
  elif isinstance(annotated_type_hook, Query):
650
1473
  mapped_types.add(parameter_type)
1474
+ # For query parameters, use Input suffix if it's a split model
1475
+ context_suffix = (
1476
+ "Input"
1477
+ if (
1478
+ inspect.isclass(parameter_type)
1479
+ and hasattr(parameter_type, "__dict__")
1480
+ and SplitInputOutput.is_split_model(parameter_type)
1481
+ )
1482
+ else ""
1483
+ )
651
1484
  parameters_list.append(
652
1485
  HttpParemeterSpec(
653
1486
  type_="query",
654
1487
  name=parameter_name,
655
1488
  required=True,
656
- argument_type_str=get_field_type_for_ts(parameter_type),
1489
+ argument_type_str=get_field_type_for_ts(
1490
+ parameter_type, context_suffix, type_mapping
1491
+ ),
657
1492
  )
658
1493
  )
659
1494
  elif isinstance(annotated_type_hook, Path):
660
1495
  mapped_types.add(parameter_type)
1496
+ # For path parameters, use Input suffix if it's a split model
1497
+ context_suffix = (
1498
+ "Input"
1499
+ if (
1500
+ inspect.isclass(parameter_type)
1501
+ and hasattr(parameter_type, "__dict__")
1502
+ and SplitInputOutput.is_split_model(parameter_type)
1503
+ )
1504
+ else ""
1505
+ )
661
1506
  parameters_list.append(
662
1507
  HttpParemeterSpec(
663
1508
  type_="path",
664
1509
  name=parameter_name,
665
1510
  required=True,
666
- argument_type_str=get_field_type_for_ts(parameter_type),
1511
+ argument_type_str=get_field_type_for_ts(
1512
+ parameter_type, context_suffix, type_mapping
1513
+ ),
667
1514
  )
668
1515
  )
669
1516
 
@@ -677,66 +1524,162 @@ def extract_parameters(
677
1524
 
678
1525
  else:
679
1526
  rec_parameters, rec_mapped_types = extract_parameters(
680
- depends_hook, controller, mapping
1527
+ depends_hook, controller, mapping, type_mapping
681
1528
  )
682
1529
  mapped_types.update(rec_mapped_types)
683
1530
  parameters_list.extend(rec_parameters)
684
- elif controller.path.find(f":{parameter_name}") != -1:
1531
+ elif (
1532
+ re.search(f":{parameter_name}(?:/|$)", controller.path) is not None
1533
+ ):
685
1534
  mapped_types.add(annotated_type)
1535
+ # For path parameters, use Input suffix if it's a split model
1536
+ context_suffix = (
1537
+ "Input"
1538
+ if (
1539
+ inspect.isclass(annotated_type)
1540
+ and hasattr(annotated_type, "__dict__")
1541
+ and SplitInputOutput.is_split_model(annotated_type)
1542
+ )
1543
+ else ""
1544
+ )
686
1545
  parameters_list.append(
687
1546
  HttpParemeterSpec(
688
1547
  type_="path",
689
1548
  name=parameter_name,
690
1549
  required=True,
691
- argument_type_str=get_field_type_for_ts(annotated_type),
1550
+ argument_type_str=get_field_type_for_ts(
1551
+ annotated_type, context_suffix, type_mapping
1552
+ ),
692
1553
  )
693
1554
  )
694
1555
  else:
695
1556
  mapped_types.add(annotated_type)
696
- parameters_list.append(
697
- HttpParemeterSpec(
698
- type_="query",
699
- name=parameter_name,
700
- required=True,
701
- argument_type_str=get_field_type_for_ts(annotated_type),
1557
+ # Special handling for UploadFile and list[UploadFile] - should be treated as form data
1558
+ if is_upload_file_type(annotated_type):
1559
+ parameters_list.append(
1560
+ HttpParemeterSpec(
1561
+ type_="form",
1562
+ name=parameter_name,
1563
+ required=True,
1564
+ argument_type_str=get_field_type_for_ts(
1565
+ annotated_type, "", type_mapping
1566
+ ),
1567
+ )
1568
+ )
1569
+ else:
1570
+ # For default parameters (treated as query), use Input suffix if it's a split model
1571
+ context_suffix = (
1572
+ "Input"
1573
+ if (
1574
+ inspect.isclass(annotated_type)
1575
+ and hasattr(annotated_type, "__dict__")
1576
+ and SplitInputOutput.is_split_model(annotated_type)
1577
+ )
1578
+ else ""
1579
+ )
1580
+ parameters_list.append(
1581
+ HttpParemeterSpec(
1582
+ type_="query",
1583
+ name=parameter_name,
1584
+ required=True,
1585
+ argument_type_str=get_field_type_for_ts(
1586
+ annotated_type, context_suffix, type_mapping
1587
+ ),
1588
+ )
702
1589
  )
703
- )
704
1590
 
705
1591
  elif inspect.isclass(parameter_type) and issubclass(
706
1592
  parameter_type, BaseModel
707
1593
  ):
708
1594
  mapped_types.update(extract_all_envolved_types(parameter_type))
1595
+ # For BaseModel parameters, use Input suffix if it's a split model
1596
+ context_suffix = (
1597
+ "Input" if SplitInputOutput.is_split_model(parameter_type) else ""
1598
+ )
709
1599
  parameters_list.append(
710
1600
  HttpParemeterSpec(
711
1601
  type_="body",
712
1602
  name=parameter_name,
713
1603
  required=True,
714
- argument_type_str=get_field_type_for_ts(parameter_type),
1604
+ argument_type_str=get_field_type_for_ts(
1605
+ parameter_type, context_suffix, type_mapping
1606
+ ),
715
1607
  )
716
1608
  )
717
- elif (
718
- controller.path.find(f"{{{parameter_name}}}") != -1
719
- or mapping.path.find(f"{{{parameter_name}}}") != -1
720
- ):
1609
+ elif parameter_type == UploadFile or is_upload_file_type(parameter_type):
1610
+ # UploadFile and list[UploadFile] should be treated as form data
721
1611
  mapped_types.add(parameter_type)
722
1612
  parameters_list.append(
723
1613
  HttpParemeterSpec(
724
- type_="path",
1614
+ type_="form",
725
1615
  name=parameter_name,
726
1616
  required=True,
727
- argument_type_str=get_field_type_for_ts(parameter_type),
1617
+ argument_type_str=get_field_type_for_ts(
1618
+ parameter_type, "", type_mapping
1619
+ ),
728
1620
  )
729
1621
  )
730
- else:
1622
+ elif (
1623
+ # Match both simple parameters {param} and parameters with converters {param:converter}
1624
+ re.search(f"{{{parameter_name}(:.*?)?}}", controller.path) is not None
1625
+ or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path) is not None
1626
+ ):
731
1627
  mapped_types.add(parameter_type)
1628
+ # For path parameters, use Input suffix if it's a split model
1629
+ context_suffix = (
1630
+ "Input"
1631
+ if (
1632
+ inspect.isclass(parameter_type)
1633
+ and hasattr(parameter_type, "__dict__")
1634
+ and SplitInputOutput.is_split_model(parameter_type)
1635
+ )
1636
+ else ""
1637
+ )
732
1638
  parameters_list.append(
733
1639
  HttpParemeterSpec(
734
- type_="query",
1640
+ type_="path",
735
1641
  name=parameter_name,
736
1642
  required=True,
737
- argument_type_str=get_field_type_for_ts(parameter_type),
1643
+ argument_type_str=get_field_type_for_ts(
1644
+ parameter_type, context_suffix, type_mapping
1645
+ ),
738
1646
  )
739
1647
  )
1648
+ else:
1649
+ mapped_types.add(parameter_type)
1650
+ # Special handling for UploadFile and list[UploadFile] - should be treated as form data
1651
+ if is_upload_file_type(parameter_type):
1652
+ parameters_list.append(
1653
+ HttpParemeterSpec(
1654
+ type_="form",
1655
+ name=parameter_name,
1656
+ required=True,
1657
+ argument_type_str=get_field_type_for_ts(
1658
+ parameter_type, "", type_mapping
1659
+ ),
1660
+ )
1661
+ )
1662
+ else:
1663
+ # For default parameters (treated as query), use Input suffix if it's a split model
1664
+ context_suffix = (
1665
+ "Input"
1666
+ if (
1667
+ inspect.isclass(parameter_type)
1668
+ and hasattr(parameter_type, "__dict__")
1669
+ and SplitInputOutput.is_split_model(parameter_type)
1670
+ )
1671
+ else ""
1672
+ )
1673
+ parameters_list.append(
1674
+ HttpParemeterSpec(
1675
+ type_="query",
1676
+ name=parameter_name,
1677
+ required=True,
1678
+ argument_type_str=get_field_type_for_ts(
1679
+ parameter_type, context_suffix, type_mapping
1680
+ ),
1681
+ )
1682
+ )
740
1683
 
741
1684
  if inspect.isclass(parameter_type) and not is_primitive(parameter_type):
742
1685
  signature = inspect.signature(parameter_type)
@@ -746,28 +1689,29 @@ def extract_parameters(
746
1689
  for _, parameter_type in parameter_members.items():
747
1690
  if is_primitive(parameter_type.annotation):
748
1691
  if get_origin(parameter_type.annotation) is not None:
749
- if (
750
- get_origin(parameter_type.annotation) == Annotated
751
- and (
752
- plain_validator := next(
753
- (
754
- x
755
- for x in parameter_type.annotation.__metadata__
756
- if isinstance(x, PlainValidator)
757
- ),
758
- None,
759
- )
1692
+ if get_origin(parameter_type.annotation) == Annotated:
1693
+ unwrapped_type, all_metadata = unwrap_annotated_type(
1694
+ parameter_type.annotation
1695
+ )
1696
+ plain_validator = next(
1697
+ (
1698
+ x
1699
+ for x in all_metadata
1700
+ if isinstance(x, PlainValidator)
1701
+ ),
1702
+ None,
760
1703
  )
761
- is not None
762
- ):
763
- mapped_types.add(plain_validator.json_schema_input_type)
1704
+ if plain_validator is not None:
1705
+ mapped_types.add(
1706
+ plain_validator.json_schema_input_type
1707
+ )
764
1708
  else:
765
1709
  args = parameter_type.annotation.__args__
766
1710
  mapped_types.update(args)
767
1711
  else:
768
1712
  continue
769
1713
  _, types = extract_parameters(
770
- parameter_type.annotation, controller, mapping
1714
+ parameter_type.annotation, controller, mapping, type_mapping
771
1715
  )
772
1716
  mapped_types.update(types)
773
1717
 
@@ -775,7 +1719,7 @@ def extract_parameters(
775
1719
  for arg in member.__args__:
776
1720
 
777
1721
  rec_parameters, rec_mapped_types = extract_parameters(
778
- arg, controller, mapping
1722
+ arg, controller, mapping, type_mapping
779
1723
  )
780
1724
  mapped_types.update(rec_mapped_types)
781
1725
  parameters_list.extend(rec_parameters)
@@ -791,22 +1735,15 @@ def extract_all_envolved_types(field_type: Any) -> set[Any]:
791
1735
 
792
1736
  if is_primitive(field_type):
793
1737
  if get_origin(field_type) is not None:
794
- if (
795
- get_origin(field_type) == Annotated
796
- and (
797
- plain_validator := next(
798
- (
799
- x
800
- for x in field_type.__metadata__
801
- if isinstance(x, PlainValidator)
802
- ),
803
- None,
804
- )
1738
+ if get_origin(field_type) == Annotated:
1739
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
1740
+ plain_validator = next(
1741
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
1742
+ None,
805
1743
  )
806
- is not None
807
- ):
808
- mapped_types.add(plain_validator.json_schema_input_type)
809
- return mapped_types
1744
+ if plain_validator is not None:
1745
+ mapped_types.add(plain_validator.json_schema_input_type)
1746
+ return mapped_types
810
1747
  else:
811
1748
  mapped_types.update(
812
1749
  *[extract_all_envolved_types(arg) for arg in field_type.__args__]