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,1030 @@
1
+ """Component generation functions"""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional, List, Dict
6
+ from generators.templates.copier import generate_file
7
+ from generators.templates._core.core_generator import ensure_common_widgets
8
+ from .utils import to_pascal_case, to_pascal_case_preserve, map_field_type, get_form_field_metadata, PRIMITIVE_TYPES
9
+
10
+
11
+ def infer_lib_path(component_dir: Path) -> Path:
12
+ """Resolve ``lib/`` from a directory inside the Flutter project (e.g. a component folder)."""
13
+ current = component_dir.resolve()
14
+ for parent in [current, *current.parents]:
15
+ pub = parent / "pubspec.yaml"
16
+ if pub.is_file():
17
+ lib = parent / "lib"
18
+ if lib.is_dir():
19
+ return lib
20
+ return current.parent
21
+
22
+
23
+ def get_repository_info(lib_path: Path, domain_folder: str, model_name: str, model_folder: Optional[str] = None) -> Dict:
24
+ """Extract failure type and available methods from a repository interface.
25
+
26
+ Reads i_{model_name}_repository.dart and parses:
27
+ - The failure class name from Either<FailureType, ...>
28
+ - The failure import path
29
+ - Which CRUD methods are available
30
+
31
+ Returns:
32
+ Dict with keys: failure_class, failure_import, methods (set of method names)
33
+ Falls back to {ModelName}Failure conventions if parsing fails.
34
+ """
35
+ if model_folder is None:
36
+ model_folder = model_name
37
+
38
+ repo_file = lib_path / domain_folder / model_folder / "model" / f"i_{model_name}_repository.dart"
39
+
40
+ model_pascal = to_pascal_case_preserve(model_name)
41
+ fallback = {
42
+ 'failure_class': f"{model_pascal}Failure",
43
+ 'failure_import': f"package:{Path(domain_folder) / model_folder / 'model' / f'{model_name}_failure.dart'}",
44
+ 'methods': {'getAll', 'getById', 'create', 'update', 'delete'},
45
+ }
46
+
47
+ if not repo_file.exists():
48
+ return fallback
49
+
50
+ try:
51
+ content = repo_file.read_text()
52
+ except Exception:
53
+ return fallback
54
+
55
+ # Extract failure class from Either<FailureType, ...>
56
+ failure_match = re.search(r'Either<(\w+),', content)
57
+ failure_class = failure_match.group(1) if failure_match else fallback['failure_class']
58
+
59
+ # Extract the import line for the failure class
60
+ failure_import = fallback['failure_import']
61
+ failure_file_stem = model_name + '_failure'
62
+ for line in content.splitlines():
63
+ if 'import' in line and failure_file_stem in line:
64
+ import_match = re.search(r"import\s+'(package:[^']+)'", line)
65
+ if import_match:
66
+ failure_import = import_match.group(1)
67
+ break
68
+
69
+ # Detect available methods by matching method signatures (name followed by '(')
70
+ methods = set()
71
+ method_patterns = {
72
+ 'getAll': r'\bgetAll\s*\(',
73
+ 'getById': r'\bgetById\s*\(',
74
+ 'create': r'\bcreate\s*\(',
75
+ 'update': r'\bupdate\s*\(',
76
+ 'delete': r'\bdelete\s*\(',
77
+ 'getByAuthId': r'\bgetByAuthId\s*\(',
78
+ 'createOrUpdate': r'\bcreateOrUpdate\s*\(',
79
+ 'getCurrentUserProfile': r'\bgetCurrentUserProfile\s*\(',
80
+ 'updateCurrentUserProfile': r'\bupdateCurrentUserProfile\s*\(',
81
+ 'uploadAvatar': r'\buploadAvatar\s*\(',
82
+ 'logout': r'\blogout\s*\(',
83
+ }
84
+ for method_name, pattern in method_patterns.items():
85
+ if re.search(pattern, content):
86
+ methods.add(method_name)
87
+
88
+ return {
89
+ 'failure_class': failure_class,
90
+ 'failure_import': failure_import,
91
+ 'methods': methods,
92
+ }
93
+
94
+
95
+ def ensure_base_form_bloc(project_name: str, lib_path: Path) -> None:
96
+ """Generate core/bloc/base_form_bloc.dart if it doesn't already exist.
97
+
98
+ Called automatically when creating a form component, so the BaseFormBloc
99
+ is only added to projects that actually use form components.
100
+ """
101
+ target = lib_path / "core" / "bloc" / "base_form_bloc.dart"
102
+ if not target.exists():
103
+ generate_file(project_name, lib_path, "core/bloc/base_form_bloc_template.jinja", "core/bloc/base_form_bloc.dart")
104
+
105
+
106
+ def create_component_form_layers(component_dir: Path, component_name: str, field_list: list[dict], project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
107
+ """Create all layers for a form component
108
+
109
+ Args:
110
+ component_dir: Path to component directory
111
+ component_name: Name of the component
112
+ field_list: List of field dictionaries
113
+ project_name: Name of the project
114
+ folder: Optional folder path
115
+ domain_model_name: Optional domain model name / file stem (if using domain model)
116
+ domain_folder: Optional domain folder name (e.g., 'domain')
117
+ domain_model_folder: Optional containing folder for the model. Defaults to domain_model_name.
118
+ lib_path: Path to lib/ directory (used to discover enums)
119
+ """
120
+ # Discover enums for type-aware form generation
121
+ from .feature import find_enums_with_info
122
+ known_enums: set = set()
123
+ enums_info: Dict = {}
124
+ if lib_path and domain_folder:
125
+ enums_info = find_enums_with_info(lib_path, domain_folder)
126
+ known_enums = set(enums_info.keys())
127
+
128
+ # Build the import path prefix
129
+ if folder:
130
+ import_prefix = f"{folder.replace('/', '/')}/{component_name}"
131
+ else:
132
+ import_prefix = component_name
133
+
134
+ # Application layer
135
+ app_dir = component_dir / "application"
136
+ app_dir.mkdir(exist_ok=True)
137
+
138
+ domain_value_objects_import: Optional[str] = None
139
+ if domain_folder and domain_model_name:
140
+ model_folder = domain_model_folder or domain_model_name
141
+ domain_value_objects_import = f"{domain_folder}/{model_folder}/model/value_objects.dart"
142
+
143
+ # Create form-specific BLoC files using templates
144
+ generate_form_event_from_template(component_name, field_list, app_dir, project_name, known_enums=known_enums)
145
+ generate_form_state_from_template(component_name, field_list, app_dir, project_name, known_enums=known_enums)
146
+ generate_form_bloc_from_template(
147
+ component_name,
148
+ field_list,
149
+ app_dir,
150
+ project_name,
151
+ known_enums=known_enums,
152
+ enums_info=enums_info,
153
+ domain_value_objects_import=domain_value_objects_import,
154
+ )
155
+
156
+ # Presentation layer
157
+ presentation_dir = component_dir / "presentation"
158
+ presentation_dir.mkdir(exist_ok=True)
159
+
160
+ # Create component widget - pass field_list and domain info for form components
161
+ generate_form_widget_from_template(
162
+ component_name,
163
+ field_list,
164
+ presentation_dir,
165
+ project_name,
166
+ import_prefix,
167
+ known_enums=known_enums,
168
+ enums_info=enums_info,
169
+ domain_value_objects_import=domain_value_objects_import,
170
+ )
171
+
172
+
173
+ def create_component_layers(component_dir: Path, component_name: str, project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
174
+ """Create all layers for a single-item component (load by ID).
175
+
176
+ Args:
177
+ component_dir: Path to component directory
178
+ component_name: Name of the component
179
+ project_name: Name of the project
180
+ folder: Optional folder path
181
+ domain_model_name: Optional domain model name / file stem (if using domain model)
182
+ domain_folder: Optional domain folder name (e.g., 'domain')
183
+ domain_model_folder: Optional containing folder for the model (e.g., 'auth' for user_profile).
184
+ Defaults to domain_model_name if not specified.
185
+ lib_path: Optional path to lib/ directory (for reading repository interface)
186
+ """
187
+ if domain_model_folder is None:
188
+ domain_model_folder = domain_model_name
189
+
190
+ # Build the import path prefix
191
+ if folder:
192
+ import_prefix = f"{folder.replace('/', '/')}/{component_name}"
193
+ else:
194
+ import_prefix = component_name
195
+
196
+ resolved_lib_path = lib_path or infer_lib_path(component_dir)
197
+ ensure_common_widgets(project_name, resolved_lib_path)
198
+
199
+ # Application layer
200
+ app_dir = component_dir / "application"
201
+ app_dir.mkdir(exist_ok=True)
202
+
203
+ # Determine import prefix for domain model
204
+ if domain_model_name and domain_folder:
205
+ domain_import_prefix = f"{domain_folder}/{domain_model_folder}"
206
+ else:
207
+ domain_import_prefix = None
208
+
209
+ # Get PascalCase names
210
+ component_pascal = to_pascal_case_preserve(component_name)
211
+ failure_class_for_widget: Optional[str] = None
212
+ widget_domain_import_prefix = ""
213
+ widget_domain_model_name = ""
214
+ widget_domain_model_pascal = ""
215
+
216
+ # Generate event and state files
217
+ if domain_import_prefix:
218
+ domain_model_pascal = to_pascal_case_preserve(domain_model_name)
219
+ widget_domain_import_prefix = domain_import_prefix
220
+ widget_domain_model_name = domain_model_name or ""
221
+ widget_domain_model_pascal = domain_model_pascal
222
+
223
+ # Read repository interface to detect failure type
224
+ repo_info = None
225
+ if resolved_lib_path:
226
+ repo_info = get_repository_info(resolved_lib_path, domain_folder, domain_model_name, domain_model_folder)
227
+
228
+ failure_class = repo_info['failure_class'] if repo_info else f"{domain_model_pascal}Failure"
229
+ failure_class_for_widget = failure_class
230
+ failure_import = f"import '{repo_info['failure_import']}';" if repo_info else f"import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}_failure.dart';"
231
+
232
+ freezed_mixin_event = "_$" + component_pascal + "Event"
233
+ freezed_mixin_state = "_$" + component_pascal + "State"
234
+
235
+ # Determine load method: use getCurrentUserProfile if available, otherwise getById
236
+ available_methods = repo_info['methods'] if repo_info else {'getById'}
237
+ has_get_current = 'getCurrentUserProfile' in available_methods
238
+
239
+ if has_get_current:
240
+ event_load_factory = f" const factory {component_pascal}Event.loadRequested() = LoadRequested;"
241
+ load_await_rhs = "_repository.getCurrentUserProfile()"
242
+ reload_await_rhs = "_repository.getCurrentUserProfile()"
243
+ last_id_field = ""
244
+ fold_success = f" ({domain_model_pascal} item) => emit({component_pascal}State.loaded(item)),"
245
+ else:
246
+ event_load_factory = f" const factory {component_pascal}Event.loadRequested(String id) = LoadRequested;"
247
+ load_await_rhs = "_repository.getById(event.id)"
248
+ reload_await_rhs = "_repository.getById(_lastLoadedId!)"
249
+ last_id_field = "\n String? _lastLoadedId;"
250
+ fold_success = f""" ({domain_model_pascal} item) {{
251
+ _lastLoadedId = event.id;
252
+ emit({component_pascal}State.loaded(item));
253
+ }},"""
254
+
255
+ event_reload_factory = f" const factory {component_pascal}Event.reloadRequested() = ReloadRequested;"
256
+
257
+ # Generate event file (load + reload)
258
+ event_content = f"""part of '{component_name}_bloc.dart';
259
+
260
+ @freezed
261
+ abstract class {component_pascal}Event with {freezed_mixin_event} {{
262
+ {event_load_factory}
263
+ {event_reload_factory}
264
+ }}
265
+ """
266
+ (app_dir / f"{component_name}_event.dart").write_text(event_content)
267
+
268
+ # Generate state file with domain model type
269
+ state_content = f"""part of '{component_name}_bloc.dart';
270
+
271
+ @freezed
272
+ abstract class {component_pascal}State with {freezed_mixin_state} {{
273
+ const factory {component_pascal}State.initial() = Initial;
274
+ const factory {component_pascal}State.loading() = Loading;
275
+ const factory {component_pascal}State.loaded({domain_model_pascal} item, {{@Default(false) bool isReloading}}) = Loaded;
276
+ const factory {component_pascal}State.error({failure_class} failure) = Error;
277
+ }}
278
+ """
279
+ (app_dir / f"{component_name}_state.dart").write_text(state_content)
280
+ else:
281
+ # Empty (Vuoto) component: minimal event/state without domain model
282
+ freezed_mixin_event_empty = "_$" + component_pascal + "Event"
283
+ freezed_mixin_state_empty = "_$" + component_pascal + "State"
284
+
285
+ event_content = f"""part of '{component_name}_bloc.dart';
286
+
287
+ @freezed
288
+ abstract class {component_pascal}Event with {freezed_mixin_event_empty} {{
289
+ const factory {component_pascal}Event.loadRequested() = LoadRequested;
290
+ }}
291
+ """
292
+ (app_dir / f"{component_name}_event.dart").write_text(event_content)
293
+
294
+ state_content = f"""part of '{component_name}_bloc.dart';
295
+
296
+ @freezed
297
+ abstract class {component_pascal}State with {freezed_mixin_state_empty} {{
298
+ const factory {component_pascal}State.initial() = Initial;
299
+ const factory {component_pascal}State.loading() = Loading;
300
+ const factory {component_pascal}State.loaded() = Loaded;
301
+ const factory {component_pascal}State.error(String message) = Error;
302
+ }}
303
+ """
304
+ (app_dir / f"{component_name}_state.dart").write_text(state_content)
305
+
306
+ # Generate BLoC with domain model imports if provided
307
+ if domain_import_prefix:
308
+ bloc_content = f"""import 'package:bloc/bloc.dart';
309
+ import 'package:dartz/dartz.dart';
310
+ import 'package:freezed_annotation/freezed_annotation.dart';
311
+ import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}.dart';
312
+ {failure_import}
313
+ import 'package:{project_name}/{domain_import_prefix}/model/i_{domain_model_name}_repository.dart';
314
+
315
+ part '{component_name}_bloc.freezed.dart';
316
+ part '{component_name}_event.dart';
317
+ part '{component_name}_state.dart';
318
+
319
+ class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
320
+ final I{domain_model_pascal}Repository _repository;{last_id_field}
321
+
322
+ {component_pascal}Bloc(this._repository) : super(const {component_pascal}State.initial()) {{
323
+ on<LoadRequested>(_onLoadRequested);
324
+ on<ReloadRequested>(_onReloadRequested);
325
+ }}
326
+
327
+ Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
328
+ emit(const {component_pascal}State.loading());
329
+ final Either<{failure_class}, {domain_model_pascal}> result = await {load_await_rhs};
330
+ result.fold(
331
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
332
+ {fold_success}
333
+ );
334
+ }}
335
+
336
+ Future<void> _onReloadRequested(ReloadRequested event, Emitter<{component_pascal}State> emit) async {{
337
+ if (state is! Loaded) return;
338
+ final Loaded current = state as Loaded;
339
+ emit(current.copyWith(isReloading: true));
340
+ final Either<{failure_class}, {domain_model_pascal}> result = await {reload_await_rhs};
341
+ result.fold(
342
+ ({failure_class} failure) => emit(current.copyWith(isReloading: false)),
343
+ ({domain_model_pascal} item) => emit({component_pascal}State.loaded(item)),
344
+ );
345
+ }}
346
+ }}
347
+ """
348
+ (app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
349
+ else:
350
+ # Empty (Vuoto) BLoC: no repository, stub handlers
351
+ bloc_content = f"""import 'package:bloc/bloc.dart';
352
+ import 'package:freezed_annotation/freezed_annotation.dart';
353
+
354
+ part '{component_name}_bloc.freezed.dart';
355
+ part '{component_name}_event.dart';
356
+ part '{component_name}_state.dart';
357
+
358
+ class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
359
+ {component_pascal}Bloc() : super(const {component_pascal}State.initial()) {{
360
+ on<LoadRequested>(_onLoadRequested);
361
+ }}
362
+
363
+ Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
364
+ emit(const {component_pascal}State.loading());
365
+ // TODO: implement load logic
366
+ emit(const {component_pascal}State.loaded());
367
+ }}
368
+ }}
369
+ """
370
+ (app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
371
+
372
+ # Presentation layer
373
+ presentation_dir = component_dir / "presentation"
374
+ presentation_dir.mkdir(exist_ok=True)
375
+
376
+ # Create component widget
377
+ generate_component_widget_from_template(
378
+ component_name,
379
+ presentation_dir,
380
+ project_name,
381
+ 'single',
382
+ import_prefix,
383
+ failure_class=failure_class_for_widget,
384
+ domain_import_prefix=widget_domain_import_prefix,
385
+ domain_model_name=widget_domain_model_name,
386
+ domain_model_pascal=widget_domain_model_pascal,
387
+ )
388
+
389
+
390
+ def generate_form_widget_from_template(
391
+ component_name: str,
392
+ field_list: list[dict],
393
+ presentation_dir: Path,
394
+ project_name: str,
395
+ import_prefix: str,
396
+ known_enums: set = None,
397
+ enums_info: Dict = None,
398
+ domain_value_objects_import: Optional[str] = None,
399
+ ) -> None:
400
+ """Generate form component widget file using Jinja template"""
401
+ _enums = known_enums or set()
402
+ _enums_info = enums_info or {}
403
+ pascal_name = to_pascal_case(component_name)
404
+ bloc_name = f"{pascal_name}FormBloc"
405
+ state_name = f"{pascal_name}FormState"
406
+ bloc_import = f"import 'package:{project_name}/{import_prefix}/application/{component_name}_form_bloc.dart';"
407
+
408
+ # Build extra imports needed by the template (enum imports)
409
+ extra_imports: list[str] = []
410
+
411
+ processed_field_list = []
412
+ for field in field_list:
413
+ field_name = field['name']
414
+ field_type = field['type']
415
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
416
+
417
+ field_dict = dict(field)
418
+ field_dict['pascal_name'] = to_pascal_case_preserve(field_name)
419
+ field_dict['label'] = ' '.join(word.capitalize() for word in field_name.split('_'))
420
+
421
+ meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
422
+ field_dict.update(meta)
423
+
424
+ if meta['is_enum']:
425
+ info = _enums_info.get(base_ft)
426
+ if info:
427
+ imp = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
428
+ if imp not in extra_imports:
429
+ extra_imports.append(imp)
430
+
431
+ processed_field_list.append(field_dict)
432
+
433
+ generate_file(project_name, presentation_dir, "component/component_form_widget_template.jinja", f"{component_name}_component.dart", {
434
+ "component_name": component_name,
435
+ "pascal_name": pascal_name,
436
+ "bloc_name": bloc_name,
437
+ "state_name": state_name,
438
+ "bloc_import": bloc_import,
439
+ "extra_imports": "\n".join(extra_imports),
440
+ "field_list": processed_field_list,
441
+ "domain_value_objects_import": domain_value_objects_import,
442
+ })
443
+
444
+
445
+ def generate_component_widget_from_template(
446
+ component_name: str,
447
+ presentation_dir: Path,
448
+ project_name: str,
449
+ component_type: str,
450
+ import_prefix: str,
451
+ failure_class: Optional[str] = None,
452
+ domain_import_prefix: str = "",
453
+ domain_model_name: str = "",
454
+ domain_model_pascal: str = "",
455
+ ) -> None:
456
+ """Generate component widget file using Jinja template
457
+
458
+ Args:
459
+ component_name: Name of the component
460
+ presentation_dir: Path to presentation directory
461
+ project_name: Name of the project
462
+ component_type: Type of component ('form', 'list', or 'single')
463
+ import_prefix: Import path prefix for the component
464
+ domain_import_prefix: Domain lib path (e.g. domain/note) for package imports in the widget
465
+ domain_model_name: Domain entity file stem (e.g. note)
466
+ domain_model_pascal: Domain entity class name (e.g. Note) for typed switch patterns
467
+ """
468
+ pascal_name = to_pascal_case(component_name)
469
+
470
+ if component_type == 'form':
471
+ # Form components now use generate_form_widget_from_template
472
+ # This should not be called for form components anymore
473
+ raise ValueError("Use generate_form_widget_from_template for form components")
474
+ elif component_type == 'list':
475
+ raise ValueError(
476
+ "List components must be generated via create_component_list_layers / "
477
+ "create_component_list_layers_empty (not generate_component_widget_from_template).",
478
+ )
479
+ else: # single
480
+ bloc_name = f"{pascal_name}Bloc"
481
+ state_name = f"{pascal_name}State"
482
+ bloc_import = f"import 'package:{project_name}/{import_prefix}/application/{component_name}_bloc.dart';"
483
+ generate_file(project_name, presentation_dir, "component/component_widget_template.jinja", f"{component_name}_component.dart", {
484
+ "component_name": component_name,
485
+ "pascal_name": pascal_name,
486
+ "is_form": False,
487
+ "bloc_name": bloc_name,
488
+ "state_name": state_name,
489
+ "bloc_import": bloc_import,
490
+ "failure_class": failure_class or "",
491
+ "domain_import_prefix": domain_import_prefix,
492
+ "domain_model_name": domain_model_name,
493
+ "domain_model_pascal": domain_model_pascal,
494
+ })
495
+
496
+
497
+ def generate_form_event_from_template(component_name: str, field_list: list[dict], app_dir: Path, project_name: str, known_enums: set = None) -> None:
498
+ """Generate form event file using Jinja template"""
499
+ _enums = known_enums or set()
500
+ pascal_name = to_pascal_case(component_name)
501
+
502
+ field_events = []
503
+ for field in field_list:
504
+ field_name = field['name']
505
+ if field_name == 'id':
506
+ continue
507
+ field_type = field['type']
508
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
509
+ meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
510
+ capitalized_name = to_pascal_case_preserve(field_name)
511
+ param_type = meta['event_param_type']
512
+ param_name = meta['event_param_name']
513
+ field_events.append(
514
+ f" const factory {pascal_name}FormEvent.{field_name}Changed({param_type} {param_name}) = {capitalized_name}Changed;"
515
+ )
516
+
517
+ field_events_str = "\n".join(field_events)
518
+
519
+ generate_file(project_name, app_dir, "component/component_form_event_template.jinja", f"{component_name}_form_event.dart", {
520
+ "component_name": component_name,
521
+ "pascal_name": pascal_name,
522
+ "field_events": field_events_str,
523
+ })
524
+
525
+
526
+ def generate_form_state_from_template(component_name: str, field_list: list[dict], app_dir: Path, project_name: str, known_enums: set = None) -> None:
527
+ """Generate form state file using Jinja template"""
528
+ _enums = known_enums or set()
529
+ pascal_name = to_pascal_case(component_name)
530
+
531
+ field_declarations = []
532
+ initial_values = []
533
+ for field in field_list:
534
+ field_name = field['name']
535
+ if field_name == 'id':
536
+ continue
537
+ field_type = field['type']
538
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
539
+ meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
540
+ capitalized_name = to_pascal_case_preserve(field_name)
541
+
542
+ if meta['is_enum']:
543
+ field_declarations.append(f" required {meta['enum_class']} {field_name},")
544
+ initial_values.append(f" {field_name}: {meta['initial_value_expr']},")
545
+ else:
546
+ field_declarations.append(f" required {capitalized_name} {field_name},")
547
+ initial_values.append(f" {field_name}: {capitalized_name}({meta['initial_value_expr']}),")
548
+
549
+ field_declarations_str = "\n".join(field_declarations)
550
+ initial_values_str = "\n".join(initial_values)
551
+
552
+ generate_file(project_name, app_dir, "component/component_form_state_template.jinja", f"{component_name}_form_state.dart", {
553
+ "component_name": component_name,
554
+ "pascal_name": pascal_name,
555
+ "field_declarations": field_declarations_str,
556
+ "initial_values": initial_values_str,
557
+ })
558
+
559
+
560
+ def generate_form_bloc_from_template(
561
+ component_name: str,
562
+ field_list: list[dict],
563
+ app_dir: Path,
564
+ project_name: str,
565
+ known_enums: set = None,
566
+ enums_info: Dict = None,
567
+ domain_value_objects_import: Optional[str] = None,
568
+ ) -> None:
569
+ """Generate form bloc file using Jinja template"""
570
+ _enums = known_enums or set()
571
+ _enums_info = enums_info or {}
572
+ pascal_name = to_pascal_case(component_name)
573
+
574
+ extra_imports: list[str] = []
575
+ processed_field_list = []
576
+ for field in field_list:
577
+ field_name = field['name']
578
+ field_type = field['type']
579
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
580
+
581
+ field_dict = dict(field)
582
+ field_dict['pascal_name'] = to_pascal_case_preserve(field_name)
583
+ meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
584
+ field_dict.update(meta)
585
+
586
+ if meta['is_enum']:
587
+ info = _enums_info.get(base_ft)
588
+ if info:
589
+ imp = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
590
+ if imp not in extra_imports:
591
+ extra_imports.append(imp)
592
+
593
+ processed_field_list.append(field_dict)
594
+
595
+ generate_file(project_name, app_dir, "component/component_form_bloc_template.jinja", f"{component_name}_form_bloc.dart", {
596
+ "component_name": component_name,
597
+ "pascal_name": pascal_name,
598
+ "field_list": processed_field_list,
599
+ "domain_value_objects_import": domain_value_objects_import,
600
+ "extra_imports": "\n".join(extra_imports),
601
+ })
602
+
603
+
604
+ def _parse_value_objects_file(vo_file: Path) -> Dict[str, str]:
605
+ """Parse a value_objects.dart file and return a mapping from VO class name
606
+ to its underlying Dart type.
607
+
608
+ Looks for ``class Foo extends ValueObject<Bar>`` patterns.
609
+
610
+ Returns:
611
+ e.g. {'Title': 'String', 'Vote': 'int', 'CreatedAt': 'DateTime'}
612
+ """
613
+ if not vo_file.exists():
614
+ return {}
615
+ try:
616
+ content = vo_file.read_text()
617
+ except Exception:
618
+ return {}
619
+ vo_pattern = r'class\s+(\w+)\s+extends\s+ValueObject\s*<\s*(\w+)\s*>'
620
+ return {m.group(1): m.group(2) for m in re.finditer(vo_pattern, content)}
621
+
622
+
623
+ def _dart_type_to_field_type(dart_type: str) -> str:
624
+ """Convert a Dart type string (from ``ValueObject<T>``) to the lowercase
625
+ field-type convention used by the rest of the pipeline.
626
+
627
+ 'String' -> 'string', 'DateTime' -> 'datetime', 'int' -> 'int', etc.
628
+ """
629
+ mapping = {
630
+ 'String': 'string',
631
+ 'int': 'int',
632
+ 'double': 'double',
633
+ 'bool': 'bool',
634
+ 'DateTime': 'datetime',
635
+ }
636
+ return mapping.get(dart_type, dart_type.lower())
637
+
638
+
639
+ def _resolve_entity_field_type(
640
+ raw_type: str,
641
+ vo_type_map: Dict[str, str],
642
+ known_enums: set,
643
+ ) -> str:
644
+ """Resolve an entity field's Dart type to the form-field type string.
645
+
646
+ Resolution order:
647
+ 1. Nullable (``T?``) – strip ``?``, resolve inner, re-append ``?``
648
+ 2. ``Option<T>`` – resolve inner T, append ``?``
649
+ 3. Known ValueObject – map via *vo_type_map* to underlying primitive
650
+ 4. Known enum – keep class name as-is
651
+ 5. Dart primitive – lowercase
652
+ 6. Generic collection (``List<T>``, etc.) – returned as-is
653
+ 7. Fallback – ``'string'``
654
+ """
655
+ if raw_type.endswith('?'):
656
+ base = raw_type[:-1].strip()
657
+ inner = _resolve_entity_field_type(base, vo_type_map, known_enums)
658
+ return inner if inner.endswith('?') else inner + '?'
659
+
660
+ option_match = re.match(r'^Option\s*<\s*(.+)\s*>$', raw_type)
661
+ if option_match:
662
+ inner = option_match.group(1).strip()
663
+ resolved = _resolve_entity_field_type(inner, vo_type_map, known_enums)
664
+ return resolved if resolved.endswith('?') else resolved + '?'
665
+
666
+ if raw_type in vo_type_map:
667
+ return _dart_type_to_field_type(vo_type_map[raw_type])
668
+
669
+ if raw_type in known_enums:
670
+ return raw_type
671
+
672
+ primitive_map = {
673
+ 'String': 'string',
674
+ 'int': 'int',
675
+ 'double': 'double',
676
+ 'bool': 'bool',
677
+ 'DateTime': 'datetime',
678
+ }
679
+ if raw_type in primitive_map:
680
+ return primitive_map[raw_type]
681
+
682
+ if '<' in raw_type and '>' in raw_type:
683
+ return raw_type
684
+
685
+ return 'string'
686
+
687
+
688
+ def get_model_fields_from_domain(lib_path: Path, domain_folder: str, model_name: str, model_folder: Optional[str] = None) -> List[dict]:
689
+ """Extract field information from a domain model entity file.
690
+
691
+ Reads the entity file **and** both core and local ``value_objects.dart``
692
+ to resolve ValueObject wrappers to their underlying Dart types. Also
693
+ detects enums from the ``domain/enums/`` folder.
694
+
695
+ Args:
696
+ lib_path: Path to lib/ directory
697
+ domain_folder: Name of domain folder (e.g., 'domain')
698
+ model_name: Name of the domain model (file stem)
699
+ model_folder: Containing folder for the model. Defaults to model_name.
700
+
701
+ Returns:
702
+ List of field dicts ``{'name': ..., 'type': ...}`` compatible with
703
+ the existing ``field_list`` format used by form generators.
704
+ """
705
+ if model_folder is None:
706
+ model_folder = model_name
707
+
708
+ entity_file = lib_path / domain_folder / model_folder / "model" / f"{model_name}.dart"
709
+
710
+ if not entity_file.exists():
711
+ raise FileNotFoundError(f"Domain model entity file not found: {entity_file}")
712
+
713
+ content = entity_file.read_text()
714
+
715
+ # --- Build ValueObject → underlying type mapping ----------------------
716
+ vo_type_map: Dict[str, str] = {}
717
+ core_vo_file = lib_path / "core" / "model" / "value_objects.dart"
718
+ vo_type_map.update(_parse_value_objects_file(core_vo_file))
719
+ local_vo_file = lib_path / domain_folder / model_folder / "model" / "value_objects.dart"
720
+ vo_type_map.update(_parse_value_objects_file(local_vo_file))
721
+
722
+ # --- Discover enums ---------------------------------------------------
723
+ from .feature import find_enums_with_info
724
+ enums_info = find_enums_with_info(lib_path, domain_folder)
725
+ known_enums = set(enums_info.keys())
726
+
727
+ # --- Parse factory constructor fields ---------------------------------
728
+ factory_pattern = r'const factory\s+\w+\(\{([^}]+)\}\)'
729
+ match = re.search(factory_pattern, content, re.DOTALL)
730
+
731
+ if not match:
732
+ raise ValueError(f"Could not parse entity file: {entity_file}")
733
+
734
+ fields_section = match.group(1)
735
+
736
+ # Capture full type including generics: ``required Option<Desc> name,``
737
+ field_pattern = r'required\s+(.+?)\s+(\w+)\s*[,}]'
738
+ fields = []
739
+
740
+ for field_match in re.finditer(field_pattern, fields_section):
741
+ raw_type = field_match.group(1).strip()
742
+ field_name = field_match.group(2)
743
+
744
+ mapped_type = _resolve_entity_field_type(raw_type, vo_type_map, known_enums)
745
+
746
+ fields.append({
747
+ 'name': field_name,
748
+ 'type': mapped_type,
749
+ })
750
+
751
+ return fields
752
+
753
+
754
+ def _create_component_list_layers_empty(component_dir: Path, component_name: str, project_name: str, folder: Optional[str]) -> None:
755
+ """Create all layers for an empty (Vuoto) list component with no domain model."""
756
+ if folder:
757
+ component_import_prefix = f"{folder.replace('/', '/')}/{component_name}"
758
+ else:
759
+ component_import_prefix = component_name
760
+
761
+ ensure_common_widgets(project_name, infer_lib_path(component_dir))
762
+
763
+ app_dir = component_dir / "application"
764
+ app_dir.mkdir(exist_ok=True)
765
+
766
+ component_pascal = to_pascal_case_preserve(component_name)
767
+ freezed_mixin_event = "_$" + component_pascal + "Event"
768
+ freezed_mixin_state = "_$" + component_pascal + "State"
769
+
770
+ event_content = f"""part of '{component_name}_bloc.dart';
771
+
772
+ @freezed
773
+ abstract class {component_pascal}Event with {freezed_mixin_event} {{
774
+ const factory {component_pascal}Event.loadRequested() = LoadRequested;
775
+ }}
776
+ """
777
+ (app_dir / f"{component_name}_event.dart").write_text(event_content)
778
+
779
+ state_content = f"""part of '{component_name}_bloc.dart';
780
+
781
+ @freezed
782
+ abstract class {component_pascal}State with {freezed_mixin_state} {{
783
+ const factory {component_pascal}State.initial() = Initial;
784
+ const factory {component_pascal}State.loading() = Loading;
785
+ const factory {component_pascal}State.loaded() = Loaded;
786
+ const factory {component_pascal}State.error(String message) = Error;
787
+ }}
788
+ """
789
+ (app_dir / f"{component_name}_state.dart").write_text(state_content)
790
+
791
+ bloc_content = f"""import 'package:bloc/bloc.dart';
792
+ import 'package:freezed_annotation/freezed_annotation.dart';
793
+
794
+ part '{component_name}_bloc.freezed.dart';
795
+ part '{component_name}_event.dart';
796
+ part '{component_name}_state.dart';
797
+
798
+ class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
799
+ {component_pascal}Bloc() : super(const {component_pascal}State.initial()) {{
800
+ on<LoadRequested>(_onLoadRequested);
801
+ }}
802
+
803
+ Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
804
+ emit(const {component_pascal}State.loading());
805
+ // TODO: implement load logic
806
+ emit(const {component_pascal}State.loaded());
807
+ }}
808
+ }}
809
+ """
810
+ (app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
811
+
812
+ presentation_dir = component_dir / "presentation"
813
+ presentation_dir.mkdir(exist_ok=True)
814
+
815
+ generate_file(project_name, presentation_dir, "component/component_list_widget_template.jinja", f"{component_name}_component.dart", {
816
+ "component_name": component_name,
817
+ "component_pascal": component_pascal,
818
+ "component_import_prefix": component_import_prefix,
819
+ "empty_component": True,
820
+ })
821
+
822
+
823
+ def create_component_list_layers(component_dir: Path, component_name: str, project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
824
+ """Create all layers for a list component.
825
+
826
+ Generates events and handlers based on the actual methods available
827
+ in the repository interface (e.g., skips delete if no delete method exists).
828
+ If domain_model_name is None, generates an empty (Vuoto) scaffold without a domain model.
829
+
830
+ Args:
831
+ component_dir: Path to component directory
832
+ component_name: Name of the component
833
+ project_name: Name of the project
834
+ folder: Optional folder path
835
+ domain_model_name: Domain model name / file stem (None for empty scaffold)
836
+ domain_folder: Domain folder name (e.g., 'domain')
837
+ domain_model_folder: Containing folder for the model. Defaults to domain_model_name.
838
+ lib_path: Optional path to lib/ directory (for reading repository interface)
839
+ """
840
+ if domain_model_name is None:
841
+ _create_component_list_layers_empty(component_dir, component_name, project_name, folder)
842
+ return
843
+
844
+ if domain_model_folder is None:
845
+ domain_model_folder = domain_model_name
846
+
847
+ # Build import paths
848
+ if folder:
849
+ component_import_prefix = f"{folder.replace('/', '/')}/{component_name}"
850
+ else:
851
+ component_import_prefix = component_name
852
+
853
+ domain_import_prefix = f"{domain_folder}/{domain_model_folder}"
854
+
855
+ resolved_lib_path = lib_path or infer_lib_path(component_dir)
856
+ ensure_common_widgets(project_name, resolved_lib_path)
857
+
858
+ # Application layer
859
+ app_dir = component_dir / "application"
860
+ app_dir.mkdir(exist_ok=True)
861
+
862
+ # Get PascalCase names
863
+ component_pascal = to_pascal_case_preserve(component_name)
864
+ domain_model_pascal = to_pascal_case_preserve(domain_model_name)
865
+
866
+ # Read repository interface to detect failure type and available methods
867
+ repo_info = None
868
+ if resolved_lib_path:
869
+ repo_info = get_repository_info(resolved_lib_path, domain_folder, domain_model_name, domain_model_folder)
870
+
871
+ failure_class = repo_info['failure_class'] if repo_info else f"{domain_model_pascal}Failure"
872
+ failure_import = f"import '{repo_info['failure_import']}';" if repo_info else f"import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}_failure.dart';"
873
+ available_methods = repo_info['methods'] if repo_info else {'getAll', 'getById', 'create', 'update', 'delete'}
874
+
875
+ has_get_all = 'getAll' in available_methods
876
+ has_create = 'create' in available_methods
877
+ has_update = 'update' in available_methods
878
+ has_delete = 'delete' in available_methods
879
+
880
+ freezed_mixin_event = "_$" + component_pascal + "Event"
881
+ freezed_mixin_state = "_$" + component_pascal + "State"
882
+
883
+ # Build events dynamically based on available repository methods
884
+ event_lines = []
885
+ if has_get_all:
886
+ event_lines.append(f" const factory {component_pascal}Event.loadRequested() = LoadRequested;")
887
+ event_lines.append(f" const factory {component_pascal}Event.reloadRequested() = ReloadRequested;")
888
+ if has_create:
889
+ event_lines.append(f" const factory {component_pascal}Event.createRequested({domain_model_pascal} item) = CreateRequested;")
890
+ if has_update:
891
+ event_lines.append(f" const factory {component_pascal}Event.updateRequested({domain_model_pascal} item) = UpdateRequested;")
892
+ if has_delete:
893
+ event_lines.append(f" const factory {component_pascal}Event.deleteRequested(String id) = DeleteRequested;")
894
+ events_str = "\n".join(event_lines)
895
+
896
+ event_content = f"""part of '{component_name}_bloc.dart';
897
+
898
+ @freezed
899
+ abstract class {component_pascal}Event with {freezed_mixin_event} {{
900
+ {events_str}
901
+ }}
902
+ """
903
+ (app_dir / f"{component_name}_event.dart").write_text(event_content)
904
+
905
+ # Create state file
906
+ state_content = f"""part of '{component_name}_bloc.dart';
907
+
908
+ @freezed
909
+ abstract class {component_pascal}State with {freezed_mixin_state} {{
910
+ const factory {component_pascal}State.initial() = Initial;
911
+ const factory {component_pascal}State.loading() = Loading;
912
+ const factory {component_pascal}State.loaded(List<{domain_model_pascal}> items, {{@Default(false) bool isReloading}}) = Loaded;
913
+ const factory {component_pascal}State.error({failure_class} failure) = Error;
914
+ }}
915
+ """
916
+ (app_dir / f"{component_name}_state.dart").write_text(state_content)
917
+
918
+ # Build BLoC handlers dynamically
919
+ reload_snippet = f"""final Either<{failure_class}, List<{domain_model_pascal}>> itemsResult = await _repository.getAll();
920
+ itemsResult.fold(
921
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
922
+ (List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
923
+ );"""
924
+
925
+ on_registrations = []
926
+ handler_methods = []
927
+
928
+ if has_get_all:
929
+ on_registrations.append(" on<LoadRequested>(_onLoadRequested);")
930
+ on_registrations.append(" on<ReloadRequested>(_onReloadRequested);")
931
+ handler_methods.append(f""" Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
932
+ emit(const {component_pascal}State.loading());
933
+ final Either<{failure_class}, List<{domain_model_pascal}>> result = await _repository.getAll();
934
+ result.fold(
935
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
936
+ (List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
937
+ );
938
+ }}""")
939
+ handler_methods.append(f""" Future<void> _onReloadRequested(ReloadRequested event, Emitter<{component_pascal}State> emit) async {{
940
+ if (state is! Loaded) return;
941
+ final Loaded current = state as Loaded;
942
+ emit(current.copyWith(isReloading: true));
943
+ final Either<{failure_class}, List<{domain_model_pascal}>> result = await _repository.getAll();
944
+ result.fold(
945
+ ({failure_class} failure) => emit(current.copyWith(isReloading: false)),
946
+ (List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
947
+ );
948
+ }}""")
949
+
950
+ if has_create:
951
+ on_registrations.append(" on<CreateRequested>(_onCreateRequested);")
952
+ handler_methods.append(f""" Future<void> _onCreateRequested(CreateRequested event, Emitter<{component_pascal}State> emit) async {{
953
+ emit(const {component_pascal}State.loading());
954
+ final Either<{failure_class}, Unit> result = await _repository.create(event.item);
955
+ result.fold(
956
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
957
+ (Unit _) async {{
958
+ {reload_snippet}
959
+ }},
960
+ );
961
+ }}""")
962
+
963
+ if has_update:
964
+ on_registrations.append(" on<UpdateRequested>(_onUpdateRequested);")
965
+ handler_methods.append(f""" Future<void> _onUpdateRequested(UpdateRequested event, Emitter<{component_pascal}State> emit) async {{
966
+ emit(const {component_pascal}State.loading());
967
+ final Either<{failure_class}, Unit> result = await _repository.update(event.item);
968
+ result.fold(
969
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
970
+ (Unit _) async {{
971
+ {reload_snippet}
972
+ }},
973
+ );
974
+ }}""")
975
+
976
+ if has_delete:
977
+ on_registrations.append(" on<DeleteRequested>(_onDeleteRequested);")
978
+ handler_methods.append(f""" Future<void> _onDeleteRequested(DeleteRequested event, Emitter<{component_pascal}State> emit) async {{
979
+ emit(const {component_pascal}State.loading());
980
+ final Either<{failure_class}, Unit> result = await _repository.delete(event.id);
981
+ result.fold(
982
+ ({failure_class} failure) => emit({component_pascal}State.error(failure)),
983
+ (Unit _) async {{
984
+ {reload_snippet}
985
+ }},
986
+ );
987
+ }}""")
988
+
989
+ on_registrations_str = "\n".join(on_registrations)
990
+ handler_methods_str = "\n\n".join(handler_methods)
991
+
992
+ bloc_content = f"""import 'package:bloc/bloc.dart';
993
+ import 'package:dartz/dartz.dart';
994
+ import 'package:freezed_annotation/freezed_annotation.dart';
995
+ import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}.dart';
996
+ {failure_import}
997
+ import 'package:{project_name}/{domain_import_prefix}/model/i_{domain_model_name}_repository.dart';
998
+
999
+ part '{component_name}_bloc.freezed.dart';
1000
+ part '{component_name}_event.dart';
1001
+ part '{component_name}_state.dart';
1002
+
1003
+ class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
1004
+ final I{domain_model_pascal}Repository _repository;
1005
+
1006
+ {component_pascal}Bloc(this._repository) : super(const {component_pascal}State.initial()) {{
1007
+ {on_registrations_str}
1008
+ }}
1009
+
1010
+ {handler_methods_str}
1011
+ }}
1012
+ """
1013
+ (app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
1014
+
1015
+ # Presentation layer
1016
+ presentation_dir = component_dir / "presentation"
1017
+ presentation_dir.mkdir(exist_ok=True)
1018
+
1019
+ # Create component widget using list template
1020
+ generate_file(project_name, presentation_dir, "component/component_list_widget_template.jinja", f"{component_name}_component.dart", {
1021
+ "component_name": component_name,
1022
+ "component_pascal": component_pascal,
1023
+ "component_import_prefix": component_import_prefix,
1024
+ "empty_component": False,
1025
+ "failure_class": failure_class,
1026
+ "domain_import_prefix": domain_import_prefix,
1027
+ "domain_model_name": domain_model_name,
1028
+ "domain_model_pascal": domain_model_pascal,
1029
+ })
1030
+