@ds9-solutions/mcp 0.1.0 → 0.2.0
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.
- package/content/agents/code-reviewer.agent.md +52 -22
- package/content/agents/db-analyser.agent.md +121 -0
- package/content/agents/security-reviewer.agent.md +119 -37
- package/content/agents/test-writer.agent.md +84 -35
- package/content/skills/dart/SKILL.md +139 -0
- package/content/skills/dart/examples/entity.dart +37 -0
- package/content/skills/dart/examples/error_handling.dart +36 -0
- package/content/skills/dart/examples/order_test.dart +34 -0
- package/content/skills/dart/examples/repository.dart +57 -0
- package/content/skills/dart/references/checklist.md +12 -0
- package/content/skills/dart/references/documentation.md +26 -0
- package/content/skills/dart/references/naming.md +27 -0
- package/content/skills/dart-frog/SKILL.md +208 -0
- package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
- package/content/skills/dart-frog/examples/controller.dart +75 -0
- package/content/skills/dart-frog/examples/dto.dart +41 -0
- package/content/skills/dart-frog/examples/error_response.dart +35 -0
- package/content/skills/dart-frog/examples/exceptions.dart +60 -0
- package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
- package/content/skills/dart-frog/examples/model.dart +20 -0
- package/content/skills/dart-frog/examples/port.dart +17 -0
- package/content/skills/dart-frog/examples/repository.dart +56 -0
- package/content/skills/dart-frog/examples/route_collection.dart +25 -0
- package/content/skills/dart-frog/examples/route_item.dart +24 -0
- package/content/skills/dart-frog/examples/route_test.dart +33 -0
- package/content/skills/dart-frog/examples/service.dart +51 -0
- package/content/skills/dart-frog/examples/service_test.dart +55 -0
- package/content/skills/dart-frog/references/checklist.md +31 -0
- package/content/skills/dart-frog/references/error-handling.md +48 -0
- package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
- package/content/skills/dart-frog/references/layers.md +43 -0
- package/content/skills/dart-frog/references/middlewares-di.md +45 -0
- package/content/skills/dart-frog/references/project-structure.md +67 -0
- package/content/skills/dart-frog/references/routing.md +53 -0
- package/content/skills/dart-frog/references/testing.md +24 -0
- package/content/skills/docker/SKILL.md +3 -3
- package/content/skills/driftorm/SKILL.md +249 -0
- package/content/skills/flutter/SKILL.md +419 -0
- package/content/skills/hex-pattern-dart/SKILL.md +83 -0
- package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
- package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
- package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
- package/content/skills/hex-pattern-dart/examples/port.md +11 -0
- package/content/skills/hex-pattern-dart/examples/service.md +20 -0
- package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
- package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
- package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
- package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
- package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
- package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
- package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
- package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
- package/content/skills/hex-pattern-dart/references/testing.md +30 -0
- package/content/skills/mysql/SKILL.md +3 -3
- package/content/skills/typescript/SKILL.md +4 -4
- package/dist/src/content.js +10 -2
- package/dist/src/init.js +1 -0
- package/dist/src/search.js +20 -12
- package/dist/src/server.js +60 -4
- package/dist/src/skill-files.js +125 -0
- package/package.json +2 -2
- package/content/agents/db-specialist.agent.md +0 -78
- package/content/agents/nestjs-api-designer.agent.md +0 -61
- package/content/skills/hexagonal/SKILL.md +0 -156
- package/content/skills/jest-testing/SKILL.md +0 -88
- package/content/skills/nestjs/SKILL.md +0 -100
- package/content/skills/typeorm/SKILL.md +0 -119
- package/dist/test/mcp.test.js +0 -112
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter
|
|
3
|
+
description: Padrões de arquitetura, código e qualidade para apps Flutter com Provider, go_router e fpdart. Use ao criar, editar, revisar ou testar arquivos em lib/ ou test/ de um projeto Flutter, como features, providers, services, models, views, rotas, tema ou testes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flutter best practices
|
|
7
|
+
|
|
8
|
+
Arquitetura em camadas com **Provider + ChangeNotifier**, **go_router** e **fpdart** (`TaskEither`/`Either`), alinhada ao guia oficial de arquitetura do Flutter (MVVM com camada de dados) e ao Effective Dart.
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
- **Estado:** `provider` com `ChangeNotifier`. Não misture Riverpod, Bloc ou GetX.
|
|
13
|
+
- **Navegação:** `go_router`.
|
|
14
|
+
- **Erros e async:** `fpdart`. Operações que podem falhar retornam `TaskEither`, nunca `Future` que lança exceção entre camadas.
|
|
15
|
+
- **Configuração:** `flutter_dotenv` (`.env`) ou `--dart-define`. Nunca coloque segredos ou URLs fixas no código.
|
|
16
|
+
- **Lints:** `flutter_lints`.
|
|
17
|
+
|
|
18
|
+
## Estrutura de pastas
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
lib/
|
|
22
|
+
├── main.dart # bootstrap + composição de dependências
|
|
23
|
+
├── components/ # features
|
|
24
|
+
│ └── <feature>/
|
|
25
|
+
│ ├── models/
|
|
26
|
+
│ ├── providers/ # ChangeNotifier (ViewModel)
|
|
27
|
+
│ ├── services/ # acesso a dados via ports (pode ter dtos/ e mappers/)
|
|
28
|
+
│ ├── views/ # telas
|
|
29
|
+
│ │ └── components/ # widgets exclusivos da feature
|
|
30
|
+
│ └── components/ # subfeatures, com a mesma estrutura
|
|
31
|
+
└── shared/
|
|
32
|
+
├── components/ # widgets reutilizáveis (buttons, inputs, widgets...)
|
|
33
|
+
├── constraints/ # AppConstraints (bordas, breakpoints, limites)
|
|
34
|
+
├── errors/ # Failure, exceções e Log
|
|
35
|
+
├── infra/ # armazenamento local etc.
|
|
36
|
+
├── models/
|
|
37
|
+
├── providers/ # rotas e sessão
|
|
38
|
+
├── services/ # implementações de infraestrutura
|
|
39
|
+
│ └── ports/ # contratos abstratos (HTTP, storage, push...)
|
|
40
|
+
├── theme/ # paleta + extensão de ThemeData
|
|
41
|
+
├── typography/ # AppTypography.of(context)
|
|
42
|
+
└── utils/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
1. **Diretamente em `lib/`** só podem existir `components/`, `shared/`, `main.dart` e arquivos gerados de configuração (por exemplo, `firebase_options.dart`).
|
|
46
|
+
2. **Na raiz de cada feature** (e de cada subfeature em `components/`), as únicas pastas permitidas são `models`, `providers`, `services`, `views` e `components`.
|
|
47
|
+
3. **Use `components/` para subfeatures** quando a feature se divide por perfil ou área (por exemplo, `admin/components/reports`).
|
|
48
|
+
|
|
49
|
+
## Camadas e fluxo de dados
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
View ──context.read/watch──▶ Provider ──▶ Service ──▶ Port (shared/services/ports)
|
|
53
|
+
▲ .run() + match/fold TaskEither<Failure, T> TaskEither<Exception, T> ▲
|
|
54
|
+
└──────── AppSnackBars ◀──── Log.print + Failure ◀───────────────────────────────┘
|
|
55
|
+
implementado em shared/services
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
4. **Ports** são classes `abstract` em `shared/services/ports/` e retornam `TaskEither<Exception, T>`. As implementações concretas ficam em `shared/services/`.
|
|
59
|
+
5. **Services de feature** recebem *ports* pelo construtor, nunca a implementação concreta. Eles convertem JSON em model e retornam `TaskEither<Exception, T>`, sem estado, sem `notifyListeners` e sem `BuildContext`.
|
|
60
|
+
6. **Providers** estendem `ChangeNotifier` e recebem o service pelo construtor. Guardam estado privado exposto por getters e retornam `TaskEither<Failure, T>` (normalmente `Unit`). Em erro, registram com `Log` e devolvem a `Failure` do domínio.
|
|
61
|
+
7. **Views** usam `context.read<T>()` para ações e `context.watch<T>()` para estado. Executam com `.run()` e tratam o resultado com `.match(...)` ou `.fold(...)`.
|
|
62
|
+
8. **fpdart em toda fronteira entre camadas:**
|
|
63
|
+
- Em `views → providers`, a view consome com `.run()` **e** `.match`, `.fold`, `.getOrElse`, `.isLeft` ou `.isRight`. Só `mapLeft` não basta, porque ignora o caminho de sucesso.
|
|
64
|
+
- Em `providers → services`, o arquivo importa `package:fpdart/fpdart.dart` e trabalha com `TaskEither` ou `Either`.
|
|
65
|
+
- Em `services → shared/services`, vale a mesma regra.
|
|
66
|
+
9. **Direção única de dependência.** Uma view nunca importa um service, e um provider nunca importa uma view. A lógica de negócio fica fora dos widgets.
|
|
67
|
+
10. **Crie uma camada de *use case*** só quando a lógica for complexa ou compartilhada entre providers. Caso contrário, ela fica no provider ou no service.
|
|
68
|
+
|
|
69
|
+
## Regras de código
|
|
70
|
+
|
|
71
|
+
### Documentação e estilo
|
|
72
|
+
|
|
73
|
+
11. **Toda declaração tem `///` logo acima:** classes, construtores, métodos, getters, funções, enums, extensions, mixins, typedefs e campos de classe. Variáveis locais estão isentas. Siga o Effective Dart: primeira frase curta terminada em ponto, campos descritos com substantivo e booleanos começando com "Indica se...".
|
|
74
|
+
12. **Não use `var`.** Use `final`, `const` ou tipo explícito.
|
|
75
|
+
13. **Todo arquivo passa por `dart format`.** Código fora do formato não é commitado.
|
|
76
|
+
14. **`flutter analyze --fatal-infos` precisa passar limpo.** Não use `// ignore` sem justificativa no próprio comentário.
|
|
77
|
+
15. **Importe pelo caminho do pacote** (`package:<app>/...`). Use alias (`as admin`, `as customer`) quando houver nomes de classe iguais em features diferentes.
|
|
78
|
+
16. **Ao mudar dependências, versione o `pubspec.lock`**, e no CI use `flutter pub get --enforce-lockfile`.
|
|
79
|
+
|
|
80
|
+
### Models
|
|
81
|
+
|
|
82
|
+
17. **Models são classes imutáveis**, com campos `final`, construtor `const` e `factory fromJson(Map<String, dynamic>)` defensivo (`?.toString() ?? ''`, `DateTime.tryParse`). Nada de `dynamic` vazando para fora do model.
|
|
83
|
+
18. **Textos de exibição derivados** (rótulo, tradução, ícone) ficam em getters do model ou em `shared/utils`, nunca espalhados pela view.
|
|
84
|
+
19. **DTOs e mappers** ficam em `services/dtos/` e `services/mappers/` quando o formato da API difere do model de domínio.
|
|
85
|
+
|
|
86
|
+
### Erros e logs
|
|
87
|
+
|
|
88
|
+
20. **`Failure` é uma `sealed class`** em `shared/errors/failure.dart`, com uma subclasse por domínio (`AuthFailure`, `LoadingFailure`, `ValidationFailure`, `GeneralFailure`...). Use a do domínio certo. Se faltar uma, crie a subclasse em vez de reaproveitar outra.
|
|
89
|
+
21. **A mensagem da `Failure` é texto para o usuário**, claro e sem detalhe técnico. O detalhe técnico vai para o log, com `origin` (classe), `method`, `error` e `stack`.
|
|
90
|
+
22. **Não use `print` nem `debugPrint` soltos.** Centralize em um `Log` com níveis (Debug, Info, Warning, Error, Critical) sobre `dart:developer`. Nunca registre tokens, senhas ou dados pessoais sensíveis.
|
|
91
|
+
|
|
92
|
+
### Estado e injeção de dependência
|
|
93
|
+
|
|
94
|
+
23. **A composição é explícita em `main.dart`.** Ela instancia os ports concretos, depois os services e por fim os providers, sempre por construtor. Não use service locator.
|
|
95
|
+
24. **Escopo dos providers:**
|
|
96
|
+
- Globais (auth, sessão) ficam no `MultiProvider` acima do `MaterialApp.router`.
|
|
97
|
+
- Providers de uma área ficam com `ChangeNotifierProvider.value` no `ShellRoute` dessa área.
|
|
98
|
+
- Providers de tela efêmera são criados no `builder` da `GoRoute`.
|
|
99
|
+
25. **Carga inicial** acontece em `initState` com `WidgetsBinding.instance.addPostFrameCallback((_) => _load())`.
|
|
100
|
+
26. **Depois de qualquer `await` na view**, confira `if (!mounted) return;` (ou `context.mounted`) antes de usar o `context`.
|
|
101
|
+
27. **Paralelize com `Future.wait`** somente chamadas independentes, e trate cada resultado com tipo explícito, sem `as List<T>`.
|
|
102
|
+
28. **Exponha coleções como somente leitura** (`List.unmodifiable`) e só chame `notifyListeners()` quando o estado mudar de fato.
|
|
103
|
+
|
|
104
|
+
### Navegação
|
|
105
|
+
|
|
106
|
+
29. **Paths são constantes em uma classe `RoutesMapping`.** Nunca escreva string de rota solta na view.
|
|
107
|
+
30. **Toda `GoRoute` tem `name`.** Navegue com `context.go(...)` ou `context.push(...)` usando `RoutesMapping`.
|
|
108
|
+
31. **Autenticação e acesso** são decididos no `redirect` do `GoRouter`, com `refreshListenable` apontando para o provider de auth. Não faça checagem de login dentro das telas.
|
|
109
|
+
32. **Áreas com navegação própria** (por perfil, com bottom bar) usam `ShellRoute` com `navigatorKey` dedicado.
|
|
110
|
+
33. **Push notifications e deep links** passam por um único método do gerenciador de rotas.
|
|
111
|
+
|
|
112
|
+
### UI, tema e acessibilidade
|
|
113
|
+
|
|
114
|
+
34. **Cores vêm de uma `enum` de paleta** (com getter `color`) **e de uma extensão de `ThemeData`** (`Theme.of(context).primary`). Evite `Colors.*` e hexadecimais na view.
|
|
115
|
+
35. **Textos usam `AppTypography.of(context).<estilo>(...)`**, que escolhe entre mobile e tablet. Evite `TextStyle(fontSize: ...)` solto.
|
|
116
|
+
36. **Botões, inputs, modais e snackbars** usam os componentes de `shared/components` ou os estilos expostos pela extensão de tema. Raio de borda, breakpoints e limites ficam em `AppConstraints`.
|
|
117
|
+
37. **Feedback ao usuário** passa por `AppSnackBars.showSuccess`, `showError` e `showWarning`.
|
|
118
|
+
38. **Layout responsivo** decide com `AppConstraints.isMobile` ou `isTablet` no nível da tela, e com `LayoutBuilder` dentro dos widgets.
|
|
119
|
+
39. **Widgets pequenos e `const`:** quebre a tela em widgets dentro de `views/components/` em vez de métodos `_buildX()` gigantes.
|
|
120
|
+
40. **Acessibilidade:** ícones clicáveis têm `tooltip` ou `Semantics`, e ícones informativos têm `semanticLabel`.
|
|
121
|
+
|
|
122
|
+
### Testes
|
|
123
|
+
|
|
124
|
+
41. **Testes ficam em `test/`, espelhando `lib/`**, com sufixo `_test.dart`.
|
|
125
|
+
42. **Services são testados com um *fake* do port** (uma classe que implementa o contrato). Providers são testados com o service real sobre esse port fake. Não acesse rede nem serviços externos em teste unitário.
|
|
126
|
+
43. **Widgets são testados com `testWidgets`**, envolvendo a tela em `MaterialApp` e `ChangeNotifierProvider.value`. Use `pump` para mudanças simples e `pumpAndSettle` para animações e operações async.
|
|
127
|
+
|
|
128
|
+
## Fluxo para criar uma feature
|
|
129
|
+
|
|
130
|
+
- [ ] **1. Pasta.** Crie `lib/components/<feature>/` com `models/`, `services/`, `providers/` e `views/`.
|
|
131
|
+
- [ ] **2. Model.** Crie a classe imutável com `fromJson` defensivo.
|
|
132
|
+
- [ ] **3. Port.** Se precisar de infraestrutura nova, crie o contrato em `shared/services/ports/` e a implementação em `shared/services/`.
|
|
133
|
+
- [ ] **4. Service.** Receba os ports pelo construtor e retorne `TaskEither<Exception, T>`.
|
|
134
|
+
- [ ] **5. Provider.** Estenda `ChangeNotifier`, guarde o estado privado e use `TaskEither.tryCatch`, `Log` e a `Failure` do domínio.
|
|
135
|
+
- [ ] **6. View.** Leia com `context.watch`, aja com `context.read` e consuma com `.run()` + `.match`.
|
|
136
|
+
- [ ] **7. Rota.** Adicione a constante em `RoutesMapping` e a `GoRoute` nomeada.
|
|
137
|
+
- [ ] **8. DI.** Instancie o service e o provider em `main.dart` e registre no escopo certo.
|
|
138
|
+
- [ ] **9. Testes.** Teste o service com port fake, o provider e o widget principal.
|
|
139
|
+
- [ ] **10. Validação:**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
dart format lib test
|
|
143
|
+
flutter analyze --fatal-infos
|
|
144
|
+
flutter test
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Exemplos (feature ilustrativa: tarefas)
|
|
148
|
+
|
|
149
|
+
### Port
|
|
150
|
+
|
|
151
|
+
```dart
|
|
152
|
+
// lib/shared/services/ports/app_http_port.dart
|
|
153
|
+
import 'package:fpdart/fpdart.dart';
|
|
154
|
+
|
|
155
|
+
/// Contrato de acesso HTTP à API do app.
|
|
156
|
+
abstract class AppHttpPort {
|
|
157
|
+
/// Executa um GET em [path] e converte a resposta com [fromJson].
|
|
158
|
+
TaskEither<Exception, T> get<T>(String path, T Function(dynamic) fromJson);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Model
|
|
163
|
+
|
|
164
|
+
```dart
|
|
165
|
+
// lib/components/tasks/models/task_model.dart
|
|
166
|
+
|
|
167
|
+
/// Representa uma tarefa do usuário.
|
|
168
|
+
class TaskModel {
|
|
169
|
+
/// Identificador da tarefa.
|
|
170
|
+
final String id;
|
|
171
|
+
|
|
172
|
+
/// Título exibido na lista.
|
|
173
|
+
final String title;
|
|
174
|
+
|
|
175
|
+
/// Data de conclusão, quando houver.
|
|
176
|
+
final DateTime? doneAt;
|
|
177
|
+
|
|
178
|
+
/// Cria uma tarefa imutável.
|
|
179
|
+
const TaskModel({required this.id, required this.title, this.doneAt});
|
|
180
|
+
|
|
181
|
+
/// Constrói a tarefa a partir do JSON da API.
|
|
182
|
+
factory TaskModel.fromJson(Map<String, dynamic> json) {
|
|
183
|
+
return TaskModel(
|
|
184
|
+
id: json['id']?.toString() ?? '',
|
|
185
|
+
title: json['title']?.toString() ?? '',
|
|
186
|
+
doneAt: DateTime.tryParse(json['done_at']?.toString() ?? ''),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Indica se a tarefa foi concluída.
|
|
191
|
+
bool get isDone => doneAt != null;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Service
|
|
196
|
+
|
|
197
|
+
```dart
|
|
198
|
+
// lib/components/tasks/services/tasks_service.dart
|
|
199
|
+
import 'package:fpdart/fpdart.dart';
|
|
200
|
+
import 'package:my_app/components/tasks/models/task_model.dart';
|
|
201
|
+
import 'package:my_app/shared/services/ports/app_http_port.dart';
|
|
202
|
+
|
|
203
|
+
/// Acessa os dados de tarefas.
|
|
204
|
+
class TasksService {
|
|
205
|
+
/// Porta HTTP para a API.
|
|
206
|
+
final AppHttpPort _httpPort;
|
|
207
|
+
|
|
208
|
+
/// Cria o serviço com a porta HTTP injetada.
|
|
209
|
+
TasksService(this._httpPort);
|
|
210
|
+
|
|
211
|
+
/// Busca as tarefas do usuário autenticado.
|
|
212
|
+
TaskEither<Exception, List<TaskModel>> getTasks() {
|
|
213
|
+
return _httpPort.get<List<TaskModel>>('/tasks', (json) {
|
|
214
|
+
final List<dynamic> list = json is List
|
|
215
|
+
? json
|
|
216
|
+
: (json['data'] ?? <dynamic>[]) as List<dynamic>;
|
|
217
|
+
return list
|
|
218
|
+
.map((item) => TaskModel.fromJson(item as Map<String, dynamic>))
|
|
219
|
+
.toList();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Provider
|
|
226
|
+
|
|
227
|
+
```dart
|
|
228
|
+
// lib/components/tasks/providers/tasks_provider.dart
|
|
229
|
+
import 'package:flutter/foundation.dart';
|
|
230
|
+
import 'package:fpdart/fpdart.dart';
|
|
231
|
+
import 'package:my_app/components/tasks/models/task_model.dart';
|
|
232
|
+
import 'package:my_app/components/tasks/services/tasks_service.dart';
|
|
233
|
+
import 'package:my_app/shared/errors/failure.dart';
|
|
234
|
+
import 'package:my_app/shared/errors/log.dart';
|
|
235
|
+
|
|
236
|
+
/// Gerencia o estado da tela de tarefas.
|
|
237
|
+
class TasksProvider extends ChangeNotifier {
|
|
238
|
+
/// Serviço de acesso às tarefas.
|
|
239
|
+
final TasksService _service;
|
|
240
|
+
|
|
241
|
+
/// Cria o provider com o serviço injetado.
|
|
242
|
+
TasksProvider(this._service);
|
|
243
|
+
|
|
244
|
+
/// Tarefas carregadas.
|
|
245
|
+
List<TaskModel> _tasks = const [];
|
|
246
|
+
|
|
247
|
+
/// Indica se as tarefas estão sendo carregadas.
|
|
248
|
+
bool _isLoading = false;
|
|
249
|
+
|
|
250
|
+
/// Tarefas carregadas, somente leitura.
|
|
251
|
+
List<TaskModel> get tasks => List.unmodifiable(_tasks);
|
|
252
|
+
|
|
253
|
+
/// Indica se as tarefas estão sendo carregadas.
|
|
254
|
+
bool get isLoading => _isLoading;
|
|
255
|
+
|
|
256
|
+
/// Carrega as tarefas e notifica a tela.
|
|
257
|
+
TaskEither<Failure, Unit> load() {
|
|
258
|
+
return TaskEither.tryCatch(
|
|
259
|
+
() async {
|
|
260
|
+
_setLoading(true);
|
|
261
|
+
try {
|
|
262
|
+
final result = await _service.getTasks().run();
|
|
263
|
+
_tasks = result.getOrElse((error) => throw error);
|
|
264
|
+
return unit;
|
|
265
|
+
} finally {
|
|
266
|
+
_setLoading(false);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
(error, stack) {
|
|
270
|
+
Log.print<Error>(
|
|
271
|
+
message: 'Erro ao carregar tarefas',
|
|
272
|
+
origin: 'TasksProvider',
|
|
273
|
+
method: 'load',
|
|
274
|
+
error: error,
|
|
275
|
+
stack: stack,
|
|
276
|
+
);
|
|
277
|
+
return const LoadingFailure('Não foi possível carregar suas tarefas.');
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Atualiza o indicador de carregamento e notifica os ouvintes.
|
|
283
|
+
void _setLoading(bool value) {
|
|
284
|
+
_isLoading = value;
|
|
285
|
+
notifyListeners();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### View
|
|
291
|
+
|
|
292
|
+
```dart
|
|
293
|
+
// lib/components/tasks/views/tasks_view.dart
|
|
294
|
+
import 'package:flutter/material.dart';
|
|
295
|
+
import 'package:my_app/components/tasks/providers/tasks_provider.dart';
|
|
296
|
+
import 'package:my_app/shared/components/widgets/app_snackbars.dart';
|
|
297
|
+
import 'package:my_app/shared/theme/app_theme.dart';
|
|
298
|
+
import 'package:my_app/shared/typography/app_typography.dart';
|
|
299
|
+
import 'package:provider/provider.dart';
|
|
300
|
+
|
|
301
|
+
/// Tela com a lista de tarefas.
|
|
302
|
+
class TasksView extends StatefulWidget {
|
|
303
|
+
/// Cria a tela de tarefas.
|
|
304
|
+
const TasksView({super.key});
|
|
305
|
+
|
|
306
|
+
/// Cria o estado da tela.
|
|
307
|
+
@override
|
|
308
|
+
State<TasksView> createState() => _TasksViewState();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Estado da tela de tarefas.
|
|
312
|
+
class _TasksViewState extends State<TasksView> {
|
|
313
|
+
/// Agenda a carga inicial após o primeiro frame.
|
|
314
|
+
@override
|
|
315
|
+
void initState() {
|
|
316
|
+
super.initState();
|
|
317
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Carrega as tarefas e mostra erro amigável em caso de falha.
|
|
321
|
+
Future<void> _load() async {
|
|
322
|
+
final result = await context.read<TasksProvider>().load().run();
|
|
323
|
+
if (!mounted) return;
|
|
324
|
+
result.match(
|
|
325
|
+
(failure) => AppSnackBars.showError(context, failure.message),
|
|
326
|
+
(_) {},
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Monta a lista de tarefas.
|
|
331
|
+
@override
|
|
332
|
+
Widget build(BuildContext context) {
|
|
333
|
+
final provider = context.watch<TasksProvider>();
|
|
334
|
+
final typography = AppTypography.of(context);
|
|
335
|
+
|
|
336
|
+
if (provider.isLoading) {
|
|
337
|
+
return Center(
|
|
338
|
+
child: CircularProgressIndicator(color: Theme.of(context).primary),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return ListView.separated(
|
|
343
|
+
padding: const EdgeInsets.all(16),
|
|
344
|
+
itemCount: provider.tasks.length,
|
|
345
|
+
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
|
346
|
+
itemBuilder: (context, index) {
|
|
347
|
+
final task = provider.tasks[index];
|
|
348
|
+
return ListTile(
|
|
349
|
+
title: Text(task.title, style: typography.topic()),
|
|
350
|
+
trailing: Icon(
|
|
351
|
+
task.isDone ? Icons.check_circle : Icons.schedule,
|
|
352
|
+
color: Theme.of(context).primary,
|
|
353
|
+
semanticLabel: task.isDone ? 'Concluída' : 'Pendente',
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Teste do service com port fake
|
|
363
|
+
|
|
364
|
+
```dart
|
|
365
|
+
// test/components/tasks/services/tasks_service_test.dart
|
|
366
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
367
|
+
import 'package:fpdart/fpdart.dart';
|
|
368
|
+
import 'package:my_app/components/tasks/services/tasks_service.dart';
|
|
369
|
+
import 'package:my_app/shared/services/ports/app_http_port.dart';
|
|
370
|
+
|
|
371
|
+
/// Porta HTTP falsa que devolve uma resposta fixa.
|
|
372
|
+
class _FakeHttpPort implements AppHttpPort {
|
|
373
|
+
/// Cria a porta com a resposta a devolver.
|
|
374
|
+
_FakeHttpPort(this.response);
|
|
375
|
+
|
|
376
|
+
/// Resposta JSON simulada.
|
|
377
|
+
final Object response;
|
|
378
|
+
|
|
379
|
+
/// Devolve a resposta simulada convertida pelo [fromJson].
|
|
380
|
+
@override
|
|
381
|
+
TaskEither<Exception, T> get<T>(String path, T Function(dynamic) fromJson) =>
|
|
382
|
+
TaskEither.of(fromJson(response));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
void main() {
|
|
386
|
+
test('converte a lista da API em TaskModel', () async {
|
|
387
|
+
final service = TasksService(
|
|
388
|
+
_FakeHttpPort({
|
|
389
|
+
'data': [
|
|
390
|
+
{'id': '1', 'title': 'Revisar PR', 'done_at': '2026-05-10'},
|
|
391
|
+
],
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
final result = await service.getTasks().run();
|
|
396
|
+
|
|
397
|
+
final tasks = result.getOrElse((error) => throw error);
|
|
398
|
+
expect(tasks.single.title, 'Revisar PR');
|
|
399
|
+
expect(tasks.single.isDone, isTrue);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Evite
|
|
405
|
+
|
|
406
|
+
- **`Failure` genérica ou errada:** por exemplo, `AuthFailure` para erro de carregamento.
|
|
407
|
+
- **Estilos fixos na view:** `Colors.white`, `TextStyle(fontSize: 16)`, `Colors.grey.shade300`.
|
|
408
|
+
- **Casts depois de `Future.wait`:** algo como `results[0] as List<T>`.
|
|
409
|
+
- **Import duplicado** do mesmo arquivo com e sem alias.
|
|
410
|
+
- **Declarações sem `///`.**
|
|
411
|
+
- **Consumo de `TaskEither` só com `mapLeft`**, que ignora o caminho de sucesso.
|
|
412
|
+
- **`try/catch` dentro da view** para tratar erro de provider. O erro já chega como `Failure`.
|
|
413
|
+
|
|
414
|
+
## Referências
|
|
415
|
+
|
|
416
|
+
- Guia de arquitetura do Flutter: https://docs.flutter.dev/app-architecture
|
|
417
|
+
- Testes no Flutter: https://docs.flutter.dev/testing/overview
|
|
418
|
+
- Effective Dart: https://dart.dev/effective-dart
|
|
419
|
+
- Skills oficiais do Flutter: https://github.com/flutter/skills
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hex-pattern-dart
|
|
3
|
+
description: Padrão hexagonal (ports & adapters) da DS9 para projetos Dart, independente de framework — organização por contexto em domain (models, ports, services) e adapter (controllers, dtos, repository), regra de dependência, camadas de erro fixas (BusinessException, ClientException, MiddlewareException), composição de dependências na borda e resultado tipado com TaskEither. Use ao criar, revisar ou refatorar módulos/features, definir camadas, ports, services, adapters ou repositórios em Dart, ou quando o usuário pedir "arquitetura hexagonal", "ports and adapters" ou "organização de novo contexto".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Padrão Hexagonal DS9 para Dart (`hex-pattern-dart`)
|
|
7
|
+
|
|
8
|
+
Base arquitetural dos projetos Dart da DS9. Isola a regra de negócio de protocolo, banco e framework, deixando o núcleo testável e substituível. Siga junto com a skill `dart` (nomenclatura, `///`, OOP). Para um framework específico, use também a skill dele (ex.: `dart-frog` aplica este padrão em APIs HTTP). Esta skill não depende de nenhum framework nem de projeto específico. **Escopo:** back-end e pacotes Dart de domínio. Apps Flutter seguem a skill `flutter` (organização própria em components/providers/services); dela aproveite só os princípios (regra de dependência, ports, `TaskEither`).
|
|
9
|
+
|
|
10
|
+
Os blocos de código nos arquivos complementares são **pseudocódigo** ilustrativo: adapte à sintaxe e às versões do projeto.
|
|
11
|
+
|
|
12
|
+
## 1. Regras inegociáveis
|
|
13
|
+
|
|
14
|
+
1. **Regra de dependência:** `adapter → domain`. O `domain` nunca importa `adapter`, framework, driver de banco nem tipos de protocolo (request/response).
|
|
15
|
+
2. **Resultado tipado:** operações que podem falhar retornam `TaskEither<Exception, T>` (`fpdart`), criado com `TaskEither.tryCatch`. Não lance exceção através das camadas. Se `fpdart` não estiver no `pubspec.yaml`, informe que ele é obrigatório para seguir o padrão DS9 e peça autorização para instalar.
|
|
16
|
+
3. **Composição na borda:** dependências são montadas em um único ponto (composition root) e injetadas por construtor. Nunca instancie service, repositório ou controller dentro da rota/handler.
|
|
17
|
+
4. **Camadas de erro fixas:** `BusinessException` (domínio), `ClientException` (resposta ao cliente) e `MiddlewareException` (borda). Erro 5xx nunca expõe mensagem interna.
|
|
18
|
+
5. **Sem regra de negócio no adapter:** controller traduz protocolo ↔ chamada do service; repositório só persiste e mapeia.
|
|
19
|
+
6. **Projeto existente:** preserve a estrutura atual; aplique o padrão no código novo e no que for pedido, sem refatorar o restante.
|
|
20
|
+
|
|
21
|
+
## 2. Estrutura por contexto
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
lib/
|
|
25
|
+
├── <contexto>/ # bounded context (ex.: orders)
|
|
26
|
+
│ ├── domain/
|
|
27
|
+
│ │ ├── models/ # entidades e value objects (invariantes)
|
|
28
|
+
│ │ ├── ports/ # contratos que o domínio precisa (saída)
|
|
29
|
+
│ │ └── services/ # casos de uso; dependem só de ports e models
|
|
30
|
+
│ └── adapter/
|
|
31
|
+
│ ├── controllers/ # entrada: traduz protocolo → service → resposta
|
|
32
|
+
│ ├── dtos/ # contratos de entrada/saída + validação
|
|
33
|
+
│ └── repository/ # saída: implementa ports (persistência, clients)
|
|
34
|
+
└── common/ # exceções, tipos e utilitários compartilhados
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Imports **relativos** dentro de `lib/`; o composition root e os testes usam `package:`.
|
|
38
|
+
- Um contexto não importa `domain`/`adapter` de outro. Comunique-se por port ou por tipos em `common/`.
|
|
39
|
+
|
|
40
|
+
## 3. Responsabilidades
|
|
41
|
+
|
|
42
|
+
| Peça | Faz | Não faz |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `model` | Invariantes, comportamento do negócio | I/O, serialização de protocolo |
|
|
45
|
+
| `port` | Declara o contrato (`abstract interface class`) | Implementação |
|
|
46
|
+
| `service` | Orquestra ports, aplica regra e autorização de recurso, retorna `TaskEither` | Conhece HTTP, SQL, DTO |
|
|
47
|
+
| `controller` | Valida DTO, chama service, mapeia erro de negócio → `ClientException` | Regra de negócio |
|
|
48
|
+
| `dto` | Entrada/saída do protocolo, `fromJson`/`toJson` | Ser usado no domínio |
|
|
49
|
+
| `repository` | Implementa port, mapeia modelo ↔ persistência | Regra de negócio |
|
|
50
|
+
|
|
51
|
+
## 4. Erros por camada
|
|
52
|
+
|
|
53
|
+
- Repositório/service: `TaskEither.tryCatch` converte falha em `BusinessException` (com `domainName`, `statusCode`, mensagem segura, erro original e stack).
|
|
54
|
+
- Controller: converte `BusinessException` em `ClientException` (mensagem segura; 5xx genérico).
|
|
55
|
+
- Borda (middleware/handler): converte `ClientException`/`MiddlewareException` em resposta de protocolo.
|
|
56
|
+
|
|
57
|
+
## 5. Checklist rápido
|
|
58
|
+
|
|
59
|
+
- [ ] O `domain` não importa nada de `adapter` nem de framework?
|
|
60
|
+
- [ ] Todo retorno que pode falhar é `TaskEither` via `tryCatch`?
|
|
61
|
+
- [ ] Dependências vêm do composition root por construtor?
|
|
62
|
+
- [ ] Os erros usam as 3 camadas e 5xx não vaza detalhe?
|
|
63
|
+
- [ ] Controller sem regra de negócio; repositório sem regra de negócio?
|
|
64
|
+
- [ ] DTO fica fora do `domain`?
|
|
65
|
+
- [ ] Existem testes do service com fakes das ports?
|
|
66
|
+
|
|
67
|
+
## 6. Arquivos complementares (leitura sob demanda)
|
|
68
|
+
|
|
69
|
+
Use `list_skill_files` e `get_skill_file` com `skill: hex-pattern-dart`.
|
|
70
|
+
|
|
71
|
+
| Dúvida | Arquivo |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Camadas e regra de dependência | `references/layers-and-dependency-rule.md` |
|
|
74
|
+
| Como desenhar ports e adapters | `references/ports-and-adapters.md` |
|
|
75
|
+
| Camadas de erro | `references/error-layers.md` |
|
|
76
|
+
| Injeção e composition root | `references/dependency-injection.md` |
|
|
77
|
+
| Persistência agnóstica e mapper | `references/persistence.md` |
|
|
78
|
+
| Estratégia de testes | `references/testing.md` |
|
|
79
|
+
| Erros comuns | `references/anti-patterns.md` |
|
|
80
|
+
| Checklist completo de review | `references/checklist.md` |
|
|
81
|
+
| Árvore de pastas de exemplo | `examples/structure-tree.md` |
|
|
82
|
+
| Port (`<Papel>Port`), service e adapters (pseudocódigo) | `examples/port.md`, `examples/service.md`, `examples/adapter-in.md`, `examples/adapter-out.md` |
|
|
83
|
+
| Hierarquia de exceções | `examples/errors.md` |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Adapter de entrada — controller (pseudocódigo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
// adapter/controllers/order_controller.dart
|
|
5
|
+
/// Traduz o protocolo para o service e o resultado para a resposta.
|
|
6
|
+
class OrderController
|
|
7
|
+
final OrderService _service
|
|
8
|
+
|
|
9
|
+
OrderController(this._service)
|
|
10
|
+
|
|
11
|
+
Task<Response> getById(Context context, String id)
|
|
12
|
+
return _service.getById(id, context.requesterId)
|
|
13
|
+
.mapLeft(toClientException) // BusinessException -> ClientException
|
|
14
|
+
.match(
|
|
15
|
+
(client) => Response(status: client.statusCode, body: client.message),
|
|
16
|
+
(order) => Response.ok(OrderResponse.from(order)),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
toClientException(BusinessException e)
|
|
20
|
+
safe = e.statusCode >= 500 ? "Erro interno" : e.message // 5xx genérico
|
|
21
|
+
return ClientException(statusCode: e.statusCode, message: safe)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Pontos-chave: sem regra de negócio; service recebido por construtor; erro 5xx sem detalhe interno; o log guarda `e.error` e `e.stackTrace`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Adapter de saída — repository (pseudocódigo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
// adapter/repository/order_repository.dart
|
|
5
|
+
/// Implementa OrderRepositoryPort sobre a tecnologia de persistência do projeto.
|
|
6
|
+
class OrderRepository implements OrderRepositoryPort
|
|
7
|
+
final DataSource _source // abstração do acesso físico
|
|
8
|
+
|
|
9
|
+
OrderRepository(this._source)
|
|
10
|
+
|
|
11
|
+
TaskEither<BusinessException, Order> findById(String id)
|
|
12
|
+
return TaskEither.tryCatch(
|
|
13
|
+
() async => OrderMapper.toDomain(await _source.read(id)),
|
|
14
|
+
(error, stack) => BusinessException(
|
|
15
|
+
domainName: "orders", statusCode: 500,
|
|
16
|
+
message: "Falha ao consultar pedido", error: error, stackTrace: stack),
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pontos-chave: toda chamada de I/O em `tryCatch`; mapper converte persistência → domínio; a falha vira `BusinessException` com mensagem segura.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Hierarquia de exceções (pseudocódigo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
// common/exceptions
|
|
5
|
+
/// Falha de regra de negócio ou de infraestrutura vista pelo domínio.
|
|
6
|
+
class BusinessException implements Exception
|
|
7
|
+
String domainName
|
|
8
|
+
int statusCode
|
|
9
|
+
String message // segura para o cliente
|
|
10
|
+
Object? error // causa original (somente log)
|
|
11
|
+
StackTrace? stackTrace
|
|
12
|
+
|
|
13
|
+
/// Falha pronta para ser devolvida ao cliente.
|
|
14
|
+
class ClientException implements Exception
|
|
15
|
+
int statusCode
|
|
16
|
+
String message
|
|
17
|
+
|
|
18
|
+
/// Falha da borda (autenticação, parsing, composição).
|
|
19
|
+
class MiddlewareException implements Exception
|
|
20
|
+
int statusCode
|
|
21
|
+
String message
|
|
22
|
+
StackTrace? stackTrace
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Regra: `BusinessException` → (controller) → `ClientException`. A borda converte qualquer uma das duas, ou `MiddlewareException`, em resposta de protocolo.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Port (pseudocódigo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
// domain/ports/order_repository.dart
|
|
5
|
+
/// Contrato de persistência de pedidos exigido pelo domínio.
|
|
6
|
+
abstract interface class OrderRepositoryPort
|
|
7
|
+
TaskEither<BusinessException, Order> findById(String id)
|
|
8
|
+
TaskEither<BusinessException, Unit> save(Order order)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Pontos-chave: só depende de `Order` e de `BusinessException`; nenhuma menção a banco, SQL ou protocolo.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Service (pseudocódigo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
// domain/services/order_service.dart
|
|
5
|
+
/// Casos de uso de pedidos.
|
|
6
|
+
class OrderService
|
|
7
|
+
final OrderRepositoryPort _repository // recebido por construtor
|
|
8
|
+
|
|
9
|
+
OrderService(this._repository)
|
|
10
|
+
|
|
11
|
+
TaskEither<BusinessException, Order> getById(String id, String requesterId)
|
|
12
|
+
return _repository.findById(id).flatMap((order) =>
|
|
13
|
+
order.belongsTo(requesterId) // autorização de recurso aqui
|
|
14
|
+
? TaskEither.right(order)
|
|
15
|
+
: TaskEither.left(BusinessException(
|
|
16
|
+
domainName: "orders", statusCode: 403, message: "Acesso negado"))
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pontos-chave: depende só da port; sem DTO, sem protocolo; regra e autorização no service; retorno `TaskEither`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Árvore de pastas (exemplo)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
lib/
|
|
5
|
+
├── common/
|
|
6
|
+
│ ├── exceptions/
|
|
7
|
+
│ │ ├── business_exception.dart
|
|
8
|
+
│ │ ├── client_exception.dart
|
|
9
|
+
│ │ └── middleware_exception.dart
|
|
10
|
+
│ └── error_response.dart
|
|
11
|
+
└── orders/
|
|
12
|
+
├── domain/
|
|
13
|
+
│ ├── models/order.dart
|
|
14
|
+
│ ├── ports/order_repository.dart
|
|
15
|
+
│ └── services/order_service.dart
|
|
16
|
+
└── adapter/
|
|
17
|
+
├── controllers/order_controller.dart
|
|
18
|
+
├── dtos/create_order_dto.dart
|
|
19
|
+
└── repository/
|
|
20
|
+
├── order_mapper.dart
|
|
21
|
+
└── order_repository.dart
|
|
22
|
+
test/
|
|
23
|
+
└── orders/
|
|
24
|
+
├── domain/services/order_service_test.dart
|
|
25
|
+
└── adapter/controllers/order_controller_test.dart
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Adicione um novo contexto criando a mesma árvore sob `lib/<contexto>/`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Anti-padrões
|
|
2
|
+
|
|
3
|
+
| Anti-padrão | Por que é problema | Correção |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| Instanciar controller/service dentro da rota | Impede troca e teste; acopla borda ao concreto | Injetar pelo composition root |
|
|
6
|
+
| Regra de negócio em middleware/controller | Some em testes de domínio; duplica em outros adapters | Mover para o service |
|
|
7
|
+
| Ler segmento de path por posição para autorizar | Frágil e fácil de burlar | Autorizar no service com o identificador do recurso |
|
|
8
|
+
| `error.toString()` na resposta | Vaza detalhe interno ao cliente | Mensagem segura em `ClientException` |
|
|
9
|
+
| `catch (_) {}` genérico | Engole a causa e o stack | `TaskEither.tryCatch` + `BusinessException` |
|
|
10
|
+
| DTO usado no `domain` | Acopla negócio ao protocolo | DTO só em `adapter/dtos` |
|
|
11
|
+
| Model de persistência exposto ao `domain` | Acopla negócio ao banco | Mapper no repositório |
|
|
12
|
+
| Service que acessa banco/HTTP direto | Quebra a regra de dependência | Criar port e adapter |
|
|
13
|
+
| Service "faz tudo" (god service) | Difícil de testar e evoluir | Um service por caso de uso/agregado |
|
|
14
|
+
| Contexto importando `domain` de outro | Acoplamento entre contextos | Port ou tipo em `common/` |
|
|
15
|
+
| Exceção lançada entre camadas | Fluxo de erro invisível no tipo | Retornar `TaskEither` |
|
|
16
|
+
| `null` como "não encontrado" | Ambíguo | `Option`/`left` explícito |
|