coregen-cli 3.1.6__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 (172) hide show
  1. coregen.py +1585 -0
  2. coregen_cli-3.1.6.dist-info/METADATA +1211 -0
  3. coregen_cli-3.1.6.dist-info/RECORD +172 -0
  4. coregen_cli-3.1.6.dist-info/WHEEL +5 -0
  5. coregen_cli-3.1.6.dist-info/entry_points.txt +2 -0
  6. coregen_cli-3.1.6.dist-info/top_level.txt +2 -0
  7. generators/__init__.py +1 -0
  8. generators/assets/__init__.py +1 -0
  9. generators/assets/assets.py +30 -0
  10. generators/assets/main.py +4 -0
  11. generators/config/__init__.py +1 -0
  12. generators/config/analisy_options.py +42 -0
  13. generators/config/main.py +11 -0
  14. generators/config/pubspec.py +139 -0
  15. generators/cursor/__init__.py +3 -0
  16. generators/cursor/setup.py +87 -0
  17. generators/helpers/__init__.py +56 -0
  18. generators/helpers/component.py +1030 -0
  19. generators/helpers/config.py +255 -0
  20. generators/helpers/data_source.py +180 -0
  21. generators/helpers/domain.py +469 -0
  22. generators/helpers/feature.py +654 -0
  23. generators/helpers/navigation.py +274 -0
  24. generators/helpers/page.py +63 -0
  25. generators/helpers/project.py +33 -0
  26. generators/helpers/utils.py +338 -0
  27. generators/helpers/validation.py +364 -0
  28. generators/initializator.py +68 -0
  29. generators/main.py +81 -0
  30. generators/static/assets/logo.png +0 -0
  31. generators/static/assets/mock/auth_user.json +5 -0
  32. generators/static/assets/svgs/apple.svg +4 -0
  33. generators/static/assets/svgs/google.svg +2 -0
  34. generators/static/cursor/AGENTS.md.jinja +37 -0
  35. generators/static/cursor/agents/doc-writer.md +20 -0
  36. generators/static/cursor/agents/epic-orchestrator.md +28 -0
  37. generators/static/cursor/agents/feature-implementer.md +29 -0
  38. generators/static/cursor/agents/integration-wiring.md +30 -0
  39. generators/static/cursor/agents/layer-application.md +21 -0
  40. generators/static/cursor/agents/layer-guardian.md +21 -0
  41. generators/static/cursor/agents/layer-infrastructure.md +31 -0
  42. generators/static/cursor/agents/layer-model.md +24 -0
  43. generators/static/cursor/agents/layer-presentation.md +33 -0
  44. generators/static/cursor/docs/architecture/APIS_AND_INTEGRATION.md.jinja +42 -0
  45. generators/static/cursor/docs/architecture/CARAVAGGIO_COMPONENTS.md.jinja +317 -0
  46. generators/static/cursor/docs/architecture/DDD_LAYERS.md.jinja +32 -0
  47. generators/static/cursor/docs/architecture/FILE_TEMPLATES.md.jinja +76 -0
  48. generators/static/cursor/docs/architecture/MOCK_AND_REMOTE_DATA.md.jinja +87 -0
  49. generators/static/cursor/docs/architecture/REFERENCE_IMPLEMENTATIONS.md.jinja +48 -0
  50. generators/static/cursor/docs/architecture/WIDGETS_AND_CARAVAGGIO.md.jinja +76 -0
  51. generators/static/cursor/docs/epics/README.md +17 -0
  52. generators/static/cursor/lib/domain/AGENTS.md +23 -0
  53. generators/static/cursor/lib/features/AGENTS.md +23 -0
  54. generators/static/cursor/lib/widgets/AGENTS.md +21 -0
  55. generators/static/cursor/rules/apis-layer.mdc.jinja +21 -0
  56. generators/static/cursor/rules/application-layer.mdc.jinja +12 -0
  57. generators/static/cursor/rules/architecture-core.mdc.jinja +43 -0
  58. generators/static/cursor/rules/dart-conventions.mdc.jinja +14 -0
  59. generators/static/cursor/rules/domain-layer.mdc.jinja +24 -0
  60. generators/static/cursor/rules/login-context.mdc.jinja +16 -0
  61. generators/static/cursor/rules/presentation-layer.mdc.jinja +15 -0
  62. generators/static/cursor/rules/quality-gate.mdc.jinja +14 -0
  63. generators/static/cursor/rules/ui-caravaggio.mdc.jinja +22 -0
  64. generators/static/cursor/skills/caravaggio-ui/SKILL.md +138 -0
  65. generators/static/cursor/skills/epic-delivery/SKILL.md +74 -0
  66. generators/static/cursor/skills/epic-delivery/templates/epic-status.md +14 -0
  67. generators/static/cursor/skills/epic-delivery/templates/epic.md +36 -0
  68. generators/static/templates/apis/common/constants_template.jinja +48 -0
  69. generators/static/templates/apis/common/data_source_config_template.jinja +47 -0
  70. generators/static/templates/apis/core/api_injectable_module_template.jinja +14 -0
  71. generators/static/templates/apis/interceptors/api_logger_template.jinja +135 -0
  72. generators/static/templates/apis/interceptors/auth_interceptor_template.jinja +207 -0
  73. generators/static/templates/auth/application/auth_bloc_template.jinja +36 -0
  74. generators/static/templates/auth/application/auth_event_template.jinja +7 -0
  75. generators/static/templates/auth/application/auth_state_template.jinja +8 -0
  76. generators/static/templates/auth/infrastructure/auth_facade_module_template.jinja +16 -0
  77. generators/static/templates/auth/infrastructure/auth_facade_template.jinja +288 -0
  78. generators/static/templates/auth/infrastructure/mock_auth_facade_template.jinja +106 -0
  79. generators/static/templates/auth/model/auth_failure_template.jinja +11 -0
  80. generators/static/templates/auth/model/i_auth_facade_template.jinja +24 -0
  81. generators/static/templates/auth/model/user_template.jinja +34 -0
  82. generators/static/templates/auth/model/value_objects_template.jinja +54 -0
  83. generators/static/templates/auth/presentation/login_screen_template.jinja +24 -0
  84. generators/static/templates/auth/sign_in_form/application/sign_in_form_bloc_template.jinja +139 -0
  85. generators/static/templates/auth/sign_in_form/application/sign_in_form_event_template.jinja +15 -0
  86. generators/static/templates/auth/sign_in_form/application/sign_in_form_state_template.jinja +26 -0
  87. generators/static/templates/auth/sign_in_form/presentation/apple_form_template.jinja +16 -0
  88. generators/static/templates/auth/sign_in_form/presentation/email_password_form_template_INACTIVE.jinja +94 -0
  89. generators/static/templates/auth/sign_in_form/presentation/google_form_template.jinja +15 -0
  90. generators/static/templates/auth/sign_in_form/presentation/sign_in_form_template.jinja +381 -0
  91. generators/static/templates/component/component_bloc_template.jinja +49 -0
  92. generators/static/templates/component/component_event_template.jinja +7 -0
  93. generators/static/templates/component/component_form_bloc_template.jinja +56 -0
  94. generators/static/templates/component/component_form_event_template.jinja +7 -0
  95. generators/static/templates/component/component_form_state_template.jinja +18 -0
  96. generators/static/templates/component/component_form_widget_template.jinja +130 -0
  97. generators/static/templates/component/component_list_widget_template.jinja +95 -0
  98. generators/static/templates/component/component_state_template.jinja +9 -0
  99. generators/static/templates/component/component_widget_template.jinja +113 -0
  100. generators/static/templates/core/bloc/base_form_bloc_template.jinja +32 -0
  101. generators/static/templates/core/errors/error_localizer_template.jinja +61 -0
  102. generators/static/templates/core/infrastructure/base_mapper_template.jinja +21 -0
  103. generators/static/templates/core/infrastructure/base_repository_mixin_template.jinja +78 -0
  104. generators/static/templates/core/infrastructure/firebase_injectable_module_template.jinja +11 -0
  105. generators/static/templates/core/infrastructure/firestore_helpers_template.jinja +22 -0
  106. generators/static/templates/core/infrastructure/repository_error_handler_template.jinja +44 -0
  107. generators/static/templates/core/model/common_interfaces_template.jinja +3 -0
  108. generators/static/templates/core/model/entity_template.jinja +5 -0
  109. generators/static/templates/core/model/errors_template.jinja +15 -0
  110. generators/static/templates/core/model/failures_template.jinja +97 -0
  111. generators/static/templates/core/model/value_objects_template.jinja +135 -0
  112. generators/static/templates/core/model/value_validators_template.jinja +105 -0
  113. generators/static/templates/core/presentation/app_drawer_template.jinja +52 -0
  114. generators/static/templates/core/presentation/app_widget_template.jinja +41 -0
  115. generators/static/templates/core/presentation/bottom_nav_bar_template.jinja +32 -0
  116. generators/static/templates/domain/domain_mapper_template.jinja +39 -0
  117. generators/static/templates/domain/domain_remote_service_template.jinja +46 -0
  118. generators/static/templates/domain/domain_repository_template.jinja +90 -0
  119. generators/static/templates/domain/domain_service_module_template.jinja +23 -0
  120. generators/static/templates/domain/enum_template.jinja +3 -0
  121. generators/static/templates/domain/i_domain_service_template.jinja +18 -0
  122. generators/static/templates/domain/mock_domain_service_template.jinja +73 -0
  123. generators/static/templates/feature/feature_bloc_template.jinja +104 -0
  124. generators/static/templates/feature/feature_dto_template.jinja +26 -0
  125. generators/static/templates/feature/feature_entity_template.jinja +23 -0
  126. generators/static/templates/feature/feature_event_template.jinja +20 -0
  127. generators/static/templates/feature/feature_extensions_template.jinja +25 -0
  128. generators/static/templates/feature/feature_failure_template.jinja +11 -0
  129. generators/static/templates/feature/feature_page_template.jinja +113 -0
  130. generators/static/templates/feature/feature_repository_template.jinja +112 -0
  131. generators/static/templates/feature/feature_state_template.jinja +20 -0
  132. generators/static/templates/feature/i_feature_repository_template.jinja +21 -0
  133. generators/static/templates/feature/value_object_field_template.jinja +0 -0
  134. generators/static/templates/feature/value_object_template.jinja +0 -0
  135. generators/static/templates/feature/value_validators_template.jinja +57 -0
  136. generators/static/templates/home/home_screen_template.jinja +68 -0
  137. generators/static/templates/home/presentation/home_screen_template.jinja +52 -0
  138. generators/static/templates/home/screen_template.jinja +12 -0
  139. generators/static/templates/injection_template.jinja +8 -0
  140. generators/static/templates/logging/analytics_logging_template.jinja +42 -0
  141. generators/static/templates/logging/console_template.jinja +50 -0
  142. generators/static/templates/logging/logger_injectable_module_template.jinja +24 -0
  143. generators/static/templates/logging/logger_template.jinja +118 -0
  144. generators/static/templates/main_template.jinja +43 -0
  145. generators/static/templates/page_template.jinja +29 -0
  146. generators/static/templates/router_template.jinja +26 -0
  147. generators/static/templates/splash/presentation/splash_screen_auth_template.jinja +97 -0
  148. generators/static/templates/splash/presentation/splash_screen_template.jinja +51 -0
  149. generators/static/templates/storage/storage_repository_template.jinja +80 -0
  150. generators/static/templates/user_profile/infrastructure/user_profile_dto_template.jinja +27 -0
  151. generators/static/templates/user_profile/infrastructure/user_profile_mapper_template.jinja +43 -0
  152. generators/static/templates/user_profile/infrastructure/user_profile_repository_template.jinja +82 -0
  153. generators/static/templates/user_profile/infrastructure/user_profile_service_template.jinja +27 -0
  154. generators/static/templates/user_profile/model/i_user_profile_repository_template.jinja +34 -0
  155. generators/static/templates/user_profile/model/user_profile_failure_template.jinja +11 -0
  156. generators/static/templates/user_profile/model/user_profile_template.jinja +31 -0
  157. generators/static/templates/widgets/common/custom_scaffold_template.jinja +52 -0
  158. generators/static/templates/widgets/common/error_widget_template.jinja +22 -0
  159. generators/static/templates/widgets/common/loading_widget_template.jinja +14 -0
  160. generators/static/templates/widgets/common/unknown_state_widget_template.jinja +23 -0
  161. generators/templates/__init__.py +1 -0
  162. generators/templates/_core/core_generator.py +121 -0
  163. generators/templates/apis/apis_generator.py +50 -0
  164. generators/templates/auth/auth_generator.py +40 -0
  165. generators/templates/auth/sign_in_form_generator.py +22 -0
  166. generators/templates/copier.py +51 -0
  167. generators/templates/home/home_generator.py +26 -0
  168. generators/templates/lib/lib_generator.py +40 -0
  169. generators/templates/logging/logging_generator.py +28 -0
  170. generators/templates/main.py +75 -0
  171. generators/templates/splash/splash_generator.py +20 -0
  172. generators/templates/storage/storage_generator.py +5 -0
@@ -0,0 +1,338 @@
1
+ """Utility functions for CoreGen"""
2
+
3
+ from typing import Optional, Set
4
+
5
+
6
+ def to_pascal_case(snake_str: str) -> str:
7
+ """Convert snake_case to PascalCase"""
8
+ return ''.join(word.capitalize() for word in snake_str.split('_'))
9
+
10
+
11
+ def to_camel_case(snake_str: str) -> str:
12
+ """
13
+ Convert snake_case to camelCase.
14
+
15
+ Examples:
16
+ todo_item → todoItem
17
+ note → note
18
+ """
19
+ words = snake_str.split('_')
20
+ return words[0] + ''.join(word.capitalize() for word in words[1:])
21
+
22
+
23
+ def pascal_case_to_camel_case(pascal_str: str) -> str:
24
+ """
25
+ Convert PascalCase to camelCase.
26
+
27
+ Examples:
28
+ TodoItem → todoItem
29
+ Note → note
30
+ """
31
+ if not pascal_str:
32
+ return pascal_str
33
+ return pascal_str[0].lower() + pascal_str[1:]
34
+
35
+
36
+ def to_pascal_case_preserve(name: str) -> str:
37
+ """
38
+ Convert camelCase/snake_case to PascalCase, preserving internal capital letters.
39
+
40
+ - If name is snake_case → uses to_pascal_case()
41
+ - If name is already PascalCase → returns as-is
42
+ - If name is camelCase → converts first letter to uppercase, preserves rest
43
+
44
+ Examples:
45
+ inviteCode → InviteCode
46
+ invite_code → InviteCode
47
+ InviteCode → InviteCode
48
+ userName → UserName
49
+ apiKey → ApiKey
50
+ httpStatusCode → HttpStatusCode
51
+ """
52
+ if not name:
53
+ return name
54
+
55
+ # Check if already PascalCase (starts with uppercase and has no underscores)
56
+ if name[0].isupper() and '_' not in name:
57
+ return name
58
+
59
+ # Check if snake_case (contains underscores)
60
+ if '_' in name:
61
+ return to_pascal_case(name)
62
+
63
+ # Otherwise, it's camelCase - capitalize first letter, preserve rest
64
+ return name[0].upper() + name[1:] if name else name
65
+
66
+
67
+ def pascal_case_to_snake_case(pascal_str: str) -> str:
68
+ """
69
+ Convert PascalCase to snake_case.
70
+
71
+ Examples:
72
+ NoteItem → note_item
73
+ Note → note
74
+ HTTPRequest → http_request
75
+ """
76
+ if not pascal_str:
77
+ return pascal_str
78
+
79
+ # Insert underscore before uppercase letters (except the first one)
80
+ result = pascal_str[0].lower()
81
+ for char in pascal_str[1:]:
82
+ if char.isupper():
83
+ result += '_' + char.lower()
84
+ else:
85
+ result += char
86
+
87
+ return result
88
+
89
+
90
+ def pascal_case_to_kebab_case(pascal_str: str) -> str:
91
+ """
92
+ Convert PascalCase to kebab-case.
93
+
94
+ Examples:
95
+ TodoItem → todo-item
96
+ Note → note
97
+ HTTPRequest → http-request
98
+ """
99
+ if not pascal_str:
100
+ return pascal_str
101
+
102
+ # Insert hyphen before uppercase letters (except the first one)
103
+ result = pascal_str[0].lower()
104
+ for char in pascal_str[1:]:
105
+ if char.isupper():
106
+ result += '-' + char.lower()
107
+ else:
108
+ result += char
109
+
110
+ return result
111
+
112
+
113
+ PRIMITIVE_TYPES = {'String', 'int', 'double', 'bool', 'DateTime'}
114
+ KNOWN_VALUE_OBJECTS = {'UniqueId'}
115
+
116
+
117
+ def get_form_field_metadata(base_type: str, field_name: str, known_enums: Optional[Set[str]] = None) -> dict:
118
+ """Return UI-control and code-generation metadata for a form field.
119
+
120
+ The returned dict contains:
121
+ control_type – 'text' | 'number' | 'checkbox' | 'date' | 'dropdown'
122
+ keyboard_type – Dart TextInputType expression (only for 'number')
123
+ validator_input_expr – Dart expression converting the validator param to T
124
+ bloc_parse_expr – Dart expression converting event param to T in bloc handler
125
+ initial_value_expr – Dart literal for FormState.initial()
126
+ mapped_field_type – Dart type string for ValueFailure<T>
127
+ event_param_type – Dart type of the event constructor parameter
128
+ event_param_name – name of the event constructor parameter
129
+ has_value_object – whether the field is wrapped in a ValueObject
130
+ is_enum – True when the field is an enum
131
+ enum_class – enum class name (only when is_enum)
132
+ """
133
+ _enums = known_enums or set()
134
+ is_enum = base_type in _enums
135
+
136
+ if is_enum:
137
+ return {
138
+ 'control_type': 'dropdown',
139
+ 'keyboard_type': '',
140
+ 'validator_input_expr': '',
141
+ 'bloc_parse_expr': f'event.{field_name}Value',
142
+ 'initial_value_expr': f'{base_type}.values.first',
143
+ 'mapped_field_type': base_type,
144
+ 'event_param_type': base_type,
145
+ 'event_param_name': f'{field_name}Value',
146
+ 'has_value_object': False,
147
+ 'is_enum': True,
148
+ 'enum_class': base_type,
149
+ }
150
+
151
+ dart_type = map_field_type(base_type)
152
+
153
+ if dart_type == 'bool':
154
+ return {
155
+ 'control_type': 'checkbox',
156
+ 'keyboard_type': '',
157
+ 'validator_input_expr': 'value ?? false',
158
+ 'bloc_parse_expr': f'event.{field_name}Value',
159
+ 'initial_value_expr': 'false',
160
+ 'mapped_field_type': 'bool',
161
+ 'event_param_type': 'bool',
162
+ 'event_param_name': f'{field_name}Value',
163
+ 'has_value_object': True,
164
+ 'is_enum': False,
165
+ 'enum_class': '',
166
+ }
167
+
168
+ if dart_type == 'DateTime':
169
+ return {
170
+ 'control_type': 'date',
171
+ 'keyboard_type': '',
172
+ 'validator_input_expr': 'value ?? DateTime(0)',
173
+ 'bloc_parse_expr': f'event.{field_name}Value',
174
+ 'initial_value_expr': 'DateTime.now()',
175
+ 'mapped_field_type': 'DateTime',
176
+ 'event_param_type': 'DateTime',
177
+ 'event_param_name': f'{field_name}Value',
178
+ 'has_value_object': True,
179
+ 'is_enum': False,
180
+ 'enum_class': '',
181
+ }
182
+
183
+ if dart_type == 'int':
184
+ return {
185
+ 'control_type': 'number',
186
+ 'keyboard_type': 'TextInputType.number',
187
+ 'validator_input_expr': "int.tryParse(value ?? '') ?? 0",
188
+ 'bloc_parse_expr': f'int.tryParse(event.{field_name}Str) ?? 0',
189
+ 'initial_value_expr': '0',
190
+ 'mapped_field_type': 'int',
191
+ 'event_param_type': 'String',
192
+ 'event_param_name': f'{field_name}Str',
193
+ 'has_value_object': True,
194
+ 'is_enum': False,
195
+ 'enum_class': '',
196
+ }
197
+
198
+ if dart_type == 'double':
199
+ return {
200
+ 'control_type': 'number',
201
+ 'keyboard_type': 'const TextInputType.numberWithOptions(decimal: true)',
202
+ 'validator_input_expr': "double.tryParse(value ?? '') ?? 0.0",
203
+ 'bloc_parse_expr': f'double.tryParse(event.{field_name}Str) ?? 0.0',
204
+ 'initial_value_expr': '0.0',
205
+ 'mapped_field_type': 'double',
206
+ 'event_param_type': 'String',
207
+ 'event_param_name': f'{field_name}Str',
208
+ 'has_value_object': True,
209
+ 'is_enum': False,
210
+ 'enum_class': '',
211
+ }
212
+
213
+ # Default: String
214
+ return {
215
+ 'control_type': 'text',
216
+ 'keyboard_type': '',
217
+ 'validator_input_expr': "value ?? ''",
218
+ 'bloc_parse_expr': f'event.{field_name}Str',
219
+ 'initial_value_expr': "''",
220
+ 'mapped_field_type': 'String',
221
+ 'event_param_type': 'String',
222
+ 'event_param_name': f'{field_name}Str',
223
+ 'has_value_object': True,
224
+ 'is_enum': False,
225
+ 'enum_class': '',
226
+ }
227
+
228
+
229
+ def _is_domain_model(t: str, known_enums: Optional[Set[str]] = None) -> bool:
230
+ """Check if a type is a domain model (PascalCase, not a primitive, known VO, or enum)."""
231
+ if not t or not t[0].isupper():
232
+ return False
233
+ if t in PRIMITIVE_TYPES or t in KNOWN_VALUE_OBJECTS:
234
+ return False
235
+ if known_enums and t in known_enums:
236
+ return False
237
+ return True
238
+
239
+
240
+ def _dto_for_single_type(t: str, known_enums: Optional[Set[str]] = None) -> str:
241
+ """Map a single (non-generic) type to its DTO equivalent."""
242
+ if t in PRIMITIVE_TYPES:
243
+ return t
244
+ if t in KNOWN_VALUE_OBJECTS:
245
+ return 'String'
246
+ if known_enums and t in known_enums:
247
+ return 'String'
248
+ if t and t[0].isupper():
249
+ return f"{t}Dto"
250
+ return t
251
+
252
+
253
+ def map_field_type(field_type: str) -> str:
254
+ """
255
+ Map field type from string to Dart type.
256
+ Supports complex types like List<ModelName>, Option<String>, Map<K, V>.
257
+ For DTOs, use map_field_type_to_dto() instead.
258
+ """
259
+ if '<' in field_type and '>' in field_type:
260
+ return field_type
261
+
262
+ type_mapping = {
263
+ 'string': 'String',
264
+ 'int': 'int',
265
+ 'double': 'double',
266
+ 'bool': 'bool',
267
+ 'date': 'DateTime',
268
+ 'datetime': 'DateTime',
269
+ }
270
+ return type_mapping.get(field_type.lower(), field_type)
271
+
272
+
273
+ def map_field_type_to_dto(field_type: str, known_enums: Optional[Set[str]] = None) -> str:
274
+ """
275
+ Map field type from string to Dart type for DTOs.
276
+
277
+ Conversions:
278
+ Type? -> Type? (nullable). UniqueId? -> String?, Model? -> ModelDto?
279
+ Enum -> String (when known_enums is provided and type is in set)
280
+ Option<T> -> T? (nullable, for static templates). If T is domain model, TDto?
281
+ List<Model> -> List<ModelDto>
282
+ Set<Model> -> Set<ModelDto>
283
+ Map<K, V> -> Map<KDto, VDto> (only domain models get Dto suffix)
284
+ UniqueId -> String
285
+ DomainModel -> DomainModelDto
286
+ primitives -> as-is
287
+ """
288
+ import re
289
+
290
+ # Nullable suffix: Type? -> DtoType? (including List<T>? -> List<TDto>?)
291
+ if field_type.endswith('?'):
292
+ base = field_type[:-1]
293
+ if '<' in base and '>' in base:
294
+ return f"{map_field_type_to_dto(base, known_enums)}?"
295
+ dto_base = _dto_for_single_type(base, known_enums)
296
+ return f"{dto_base}?"
297
+
298
+ if '<' in field_type and '>' in field_type:
299
+ # Option<T> -> T? (kept for backward compat with static templates)
300
+ option_match = re.match(r'^Option\s*<\s*(\w+)\s*>$', field_type)
301
+ if option_match:
302
+ inner = option_match.group(1)
303
+ dto_inner = _dto_for_single_type(inner, known_enums)
304
+ return f"{dto_inner}?"
305
+
306
+ # Map<K, V>
307
+ map_match = re.match(r'^(\w+)\s*<\s*(\w+)\s*,\s*(\w+)\s*>$', field_type)
308
+ if map_match:
309
+ base = map_match.group(1)
310
+ key_type = map_match.group(2)
311
+ val_type = map_match.group(3)
312
+ dto_key = _dto_for_single_type(key_type, known_enums)
313
+ dto_val = _dto_for_single_type(val_type, known_enums)
314
+ return f"{base}<{dto_key}, {dto_val}>"
315
+
316
+ # Single-param generic: List<T>, Set<T>
317
+ single_match = re.match(r'^(\w+)\s*<\s*(\w+)\s*>$', field_type)
318
+ if single_match:
319
+ base = single_match.group(1)
320
+ inner = single_match.group(2)
321
+ dto_inner = _dto_for_single_type(inner, known_enums)
322
+ return f"{base}<{dto_inner}>"
323
+
324
+ return field_type
325
+
326
+ # UniqueId -> String in DTO
327
+ if field_type in KNOWN_VALUE_OBJECTS:
328
+ return 'String'
329
+
330
+ # Enum -> String in DTO
331
+ if known_enums and field_type in known_enums:
332
+ return 'String'
333
+
334
+ # Domain models -> ModelDto
335
+ if _is_domain_model(field_type, known_enums):
336
+ return f"{field_type}Dto"
337
+
338
+ return map_field_type(field_type)