oh-my-customcode 0.36.2 → 0.37.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/dist/cli/index.js +47 -2
- package/dist/index.js +44 -0
- package/package.json +1 -1
- package/templates/.claude/agents/arch-documenter.md +4 -1
- package/templates/.claude/agents/arch-speckit-agent.md +15 -0
- package/templates/.claude/agents/be-django-expert.md +1 -0
- package/templates/.claude/agents/be-express-expert.md +1 -0
- package/templates/.claude/agents/be-fastapi-expert.md +1 -0
- package/templates/.claude/agents/be-go-backend-expert.md +1 -0
- package/templates/.claude/agents/be-nestjs-expert.md +1 -0
- package/templates/.claude/agents/be-springboot-expert.md +1 -0
- package/templates/.claude/agents/db-postgres-expert.md +1 -0
- package/templates/.claude/agents/db-redis-expert.md +1 -0
- package/templates/.claude/agents/db-supabase-expert.md +1 -0
- package/templates/.claude/agents/de-airflow-expert.md +1 -0
- package/templates/.claude/agents/de-dbt-expert.md +1 -0
- package/templates/.claude/agents/de-kafka-expert.md +1 -0
- package/templates/.claude/agents/de-pipeline-expert.md +1 -0
- package/templates/.claude/agents/de-snowflake-expert.md +1 -0
- package/templates/.claude/agents/de-spark-expert.md +1 -0
- package/templates/.claude/agents/fe-flutter-agent.md +1 -0
- package/templates/.claude/agents/fe-svelte-agent.md +1 -0
- package/templates/.claude/agents/fe-vercel-agent.md +1 -0
- package/templates/.claude/agents/fe-vuejs-agent.md +1 -0
- package/templates/.claude/agents/infra-aws-expert.md +1 -0
- package/templates/.claude/agents/infra-docker-expert.md +1 -0
- package/templates/.claude/agents/lang-golang-expert.md +1 -0
- package/templates/.claude/agents/lang-java21-expert.md +3 -0
- package/templates/.claude/agents/lang-kotlin-expert.md +1 -0
- package/templates/.claude/agents/lang-python-expert.md +1 -0
- package/templates/.claude/agents/lang-rust-expert.md +1 -0
- package/templates/.claude/agents/lang-typescript-expert.md +1 -0
- package/templates/.claude/agents/mgr-claude-code-bible.md +1 -2
- package/templates/.claude/agents/mgr-creator.md +1 -0
- package/templates/.claude/agents/mgr-gitnerd.md +1 -0
- package/templates/.claude/agents/mgr-sauron.md +5 -2
- package/templates/.claude/agents/mgr-supplier.md +1 -3
- package/templates/.claude/agents/mgr-updater.md +1 -0
- package/templates/.claude/agents/qa-engineer.md +1 -0
- package/templates/.claude/agents/qa-planner.md +4 -1
- package/templates/.claude/agents/qa-writer.md +1 -1
- package/templates/.claude/agents/sec-codeql-expert.md +4 -2
- package/templates/.claude/agents/sys-memory-keeper.md +30 -0
- package/templates/.claude/agents/sys-naggy.md +36 -2
- package/templates/.claude/agents/tool-bun-expert.md +1 -1
- package/templates/.claude/agents/tool-npm-expert.md +1 -1
- package/templates/.claude/agents/tool-optimizer.md +1 -2
- package/templates/.claude/hooks/hooks.json +2 -2
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +10 -0
- package/templates/.claude/hooks/scripts/content-hash-validator.sh +2 -3
- package/templates/.claude/hooks/scripts/schema-validator.sh +15 -0
- package/templates/.claude/hooks/scripts/secret-filter.sh +31 -1
- package/templates/.claude/rules/MUST-agent-teams.md +0 -23
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +1 -13
- package/templates/.claude/skills/django-best-practices/SKILL.md +27 -134
- package/templates/.claude/skills/flutter-best-practices/SKILL.md +39 -146
- package/templates/.claude/skills/go-backend-best-practices/SKILL.md +29 -233
- package/templates/.claude/skills/java21-best-practices/SKILL.md +48 -163
- package/templates/CLAUDE.md.en +7 -65
- package/templates/CLAUDE.md.ko +7 -65
- package/templates/manifest.json +1 -1
|
@@ -46,15 +46,7 @@ state_choice:
|
|
|
46
46
|
|
|
47
47
|
build_context:
|
|
48
48
|
rule: "Never store BuildContext across async gaps"
|
|
49
|
-
pattern:
|
|
50
|
-
// BAD
|
|
51
|
-
final ctx = context;
|
|
52
|
-
await Future.delayed(Duration(seconds: 1));
|
|
53
|
-
Navigator.of(ctx).push(...); // context may be invalid
|
|
54
|
-
|
|
55
|
-
// GOOD
|
|
56
|
-
if (!mounted) return;
|
|
57
|
-
Navigator.of(context).push(...);
|
|
49
|
+
pattern: "Check mounted before using context after await"
|
|
58
50
|
|
|
59
51
|
keys:
|
|
60
52
|
ValueKey: "When items have unique business identity"
|
|
@@ -83,6 +75,8 @@ slivers:
|
|
|
83
75
|
avoid: "Nested ListView in ListView"
|
|
84
76
|
```
|
|
85
77
|
|
|
78
|
+
Reference: guides/flutter/fundamentals.md
|
|
79
|
+
|
|
86
80
|
### 2. State Management
|
|
87
81
|
|
|
88
82
|
```yaml
|
|
@@ -99,51 +93,26 @@ riverpod_patterns:
|
|
|
99
93
|
async_state: "AsyncNotifier + AsyncValue (loading/data/error)"
|
|
100
94
|
family: "family modifier for parameterized providers"
|
|
101
95
|
keep_alive: "Only when justified (expensive computations)"
|
|
102
|
-
invalidate_vs_refresh:
|
|
103
|
-
ref.invalidate(provider) // reset to loading, lazy re-fetch
|
|
104
|
-
ref.refresh(provider) // immediate re-fetch, return new value
|
|
96
|
+
invalidate_vs_refresh: "ref.invalidate() resets to loading (lazy); ref.refresh() immediate re-fetch"
|
|
105
97
|
|
|
106
98
|
bloc_patterns:
|
|
107
99
|
one_event_per_action: "One UI action = one event class"
|
|
108
|
-
cubit_vs_bloc:
|
|
109
|
-
Cubit: direct emit(state) — for simple state changes
|
|
110
|
-
Bloc: event → state mapping — when audit trail needed
|
|
100
|
+
cubit_vs_bloc: "Cubit for simple state changes; Bloc when audit trail needed"
|
|
111
101
|
never: "Emit state in constructor body"
|
|
112
|
-
listener_vs_consumer:
|
|
113
|
-
BlocListener: side effects (navigation, snackbar)
|
|
114
|
-
BlocConsumer: rebuild UI + side effects
|
|
115
|
-
BlocBuilder: rebuild UI only (most common)
|
|
102
|
+
listener_vs_consumer: "BlocListener for side effects; BlocConsumer for UI + effects; BlocBuilder for UI only"
|
|
116
103
|
stream_management: "Cancel subscriptions in close()"
|
|
117
104
|
|
|
118
105
|
state_immutability:
|
|
119
106
|
rule: "All state objects must be immutable"
|
|
120
107
|
tool: "freezed package for copyWith/==/hashCode generation"
|
|
121
|
-
pattern: |
|
|
122
|
-
@freezed
|
|
123
|
-
class UserState with _$UserState {
|
|
124
|
-
const factory UserState({
|
|
125
|
-
required String name,
|
|
126
|
-
required int age,
|
|
127
|
-
@Default(false) bool isLoading,
|
|
128
|
-
}) = _UserState;
|
|
129
|
-
}
|
|
130
108
|
|
|
131
109
|
result_type:
|
|
132
110
|
rule: "Return Result<T> from repositories, never throw"
|
|
133
|
-
pattern:
|
|
134
|
-
sealed class Result<T> {
|
|
135
|
-
const Result();
|
|
136
|
-
}
|
|
137
|
-
final class Ok<T> extends Result<T> {
|
|
138
|
-
const Ok(this.value);
|
|
139
|
-
final T value;
|
|
140
|
-
}
|
|
141
|
-
final class Error<T> extends Result<T> {
|
|
142
|
-
const Error(this.error);
|
|
143
|
-
final Exception error;
|
|
144
|
-
}
|
|
111
|
+
pattern: "sealed class Result<T> with Ok<T> and Error<T> subclasses"
|
|
145
112
|
```
|
|
146
113
|
|
|
114
|
+
Reference: guides/flutter/state-management.md
|
|
115
|
+
|
|
147
116
|
### 3. Performance
|
|
148
117
|
|
|
149
118
|
```yaml
|
|
@@ -151,16 +120,7 @@ build_optimization:
|
|
|
151
120
|
const_widgets: "Mark immutable widgets const — zero rebuild"
|
|
152
121
|
localize_setState: "Call setState on smallest possible subtree"
|
|
153
122
|
extract_widgets: "StatelessWidget class > helper method"
|
|
154
|
-
child_parameter:
|
|
155
|
-
// GOOD: static child passed through
|
|
156
|
-
AnimatedBuilder(
|
|
157
|
-
animation: controller,
|
|
158
|
-
builder: (context, child) => Transform.rotate(
|
|
159
|
-
angle: controller.value,
|
|
160
|
-
child: child, // not rebuilt
|
|
161
|
-
),
|
|
162
|
-
child: const ExpensiveWidget(), // built once
|
|
163
|
-
)
|
|
123
|
+
child_parameter: "Pass static child through AnimatedBuilder to avoid rebuild"
|
|
164
124
|
|
|
165
125
|
rebuild_avoidance:
|
|
166
126
|
consumer_placement: "Place Consumer/ListenableBuilder as deep as possible"
|
|
@@ -183,6 +143,8 @@ frame_budget:
|
|
|
183
143
|
tool: "DevTools Performance view for jank detection"
|
|
184
144
|
```
|
|
185
145
|
|
|
146
|
+
Reference: guides/flutter/performance.md
|
|
147
|
+
|
|
186
148
|
### 4. Testing
|
|
187
149
|
|
|
188
150
|
```yaml
|
|
@@ -192,36 +154,16 @@ test_pyramid:
|
|
|
192
154
|
integration: "Full app on device — slow, high confidence"
|
|
193
155
|
golden: "Visual regression via matchesGoldenFile()"
|
|
194
156
|
|
|
195
|
-
widget_test_pattern:
|
|
196
|
-
|
|
197
|
-
await tester.pumpWidget(
|
|
198
|
-
ProviderScope(
|
|
199
|
-
overrides: [productsProvider.overrideWith((_) => fakeProducts)],
|
|
200
|
-
child: const MaterialApp(home: ProductListScreen()),
|
|
201
|
-
),
|
|
202
|
-
);
|
|
203
|
-
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
204
|
-
await tester.pumpAndSettle();
|
|
205
|
-
expect(find.byType(ProductCard), findsNWidgets(3));
|
|
206
|
-
});
|
|
157
|
+
widget_test_pattern:
|
|
158
|
+
rule: "Use pumpWidget with ProviderScope overrides, then pump/pumpAndSettle for async"
|
|
207
159
|
|
|
208
160
|
mocking:
|
|
209
161
|
prefer: "mocktail (null-safe, no codegen)"
|
|
210
162
|
avoid: "Legacy mockito with build_runner"
|
|
211
163
|
fakes: "Use Fake implementations for deterministic tests"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Future<Result<List<Product>>> getAll() async => Ok(testProducts);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
bloc_testing: |
|
|
219
|
-
blocTest<AuthBloc, AuthState>(
|
|
220
|
-
'emits [loading, success] when login succeeds',
|
|
221
|
-
build: () => AuthBloc(FakeAuthRepository()),
|
|
222
|
-
act: (bloc) => bloc.add(LoginRequested('user', 'pass')),
|
|
223
|
-
expect: () => [AuthLoading(), isA<AuthSuccess>()],
|
|
224
|
-
);
|
|
164
|
+
|
|
165
|
+
bloc_testing:
|
|
166
|
+
rule: "Use blocTest<Bloc, State> with build/act/expect pattern"
|
|
225
167
|
|
|
226
168
|
coverage_target:
|
|
227
169
|
widget_tests: "80%+ for UI logic"
|
|
@@ -229,6 +171,8 @@ coverage_target:
|
|
|
229
171
|
integration: "Critical user flows only"
|
|
230
172
|
```
|
|
231
173
|
|
|
174
|
+
Reference: guides/flutter/testing.md
|
|
175
|
+
|
|
232
176
|
### 5. Security
|
|
233
177
|
|
|
234
178
|
```yaml
|
|
@@ -236,14 +180,14 @@ secrets:
|
|
|
236
180
|
never: "Hardcode API keys, tokens, or credentials in source"
|
|
237
181
|
best: "Backend proxy for all sensitive API calls"
|
|
238
182
|
use: "--dart-define-from-file=.env for NON-SECRET build config only (feature flags, environment URLs)"
|
|
239
|
-
warning: "dart-define values are embedded in compiled binary and extractable via static analysis
|
|
183
|
+
warning: "dart-define values are embedded in compiled binary and extractable via static analysis"
|
|
240
184
|
|
|
241
185
|
storage:
|
|
242
186
|
sensitive_data: "flutter_secure_storage v10+ (Keychain/Keystore)"
|
|
243
187
|
never: "SharedPreferences for tokens, PII, or credentials"
|
|
244
188
|
ios: "AppleOptions(useSecureEnclave: true) for high-value"
|
|
245
189
|
android: "AndroidOptions(encryptedSharedPreferences: true)"
|
|
246
|
-
web_warning: "flutter_secure_storage on Web uses localStorage by default
|
|
190
|
+
web_warning: "flutter_secure_storage on Web uses localStorage by default (XSS vulnerable). Use HttpOnly cookies or in-memory-only for sensitive data."
|
|
247
191
|
|
|
248
192
|
network:
|
|
249
193
|
tls: "Certificate pinning (SPKI) for financial/health apps"
|
|
@@ -267,54 +211,35 @@ logging:
|
|
|
267
211
|
never: "Log PII, tokens, or credentials"
|
|
268
212
|
```
|
|
269
213
|
|
|
214
|
+
Reference: guides/flutter/security.md
|
|
215
|
+
|
|
270
216
|
### 6. Dart Language Patterns
|
|
271
217
|
|
|
272
218
|
```yaml
|
|
273
219
|
naming:
|
|
274
|
-
types: "UpperCamelCase for classes, enums, typedefs, extensions, mixins
|
|
275
|
-
variables: "lowerCamelCase for variables, parameters, named constants
|
|
276
|
-
libraries: "lowercase_with_underscores for libraries, packages, directories, source files
|
|
277
|
-
constants: "lowerCamelCase for const (
|
|
278
|
-
private: "Prefix with underscore for library-private
|
|
279
|
-
boolean: "Prefix with is/has/can/should
|
|
280
|
-
avoid: "Hungarian notation, type prefixes
|
|
220
|
+
types: "UpperCamelCase for classes, enums, typedefs, extensions, mixins"
|
|
221
|
+
variables: "lowerCamelCase for variables, parameters, named constants"
|
|
222
|
+
libraries: "lowercase_with_underscores for libraries, packages, directories, source files"
|
|
223
|
+
constants: "lowerCamelCase for const (NOT SCREAMING_CAPS)"
|
|
224
|
+
private: "Prefix with underscore for library-private"
|
|
225
|
+
boolean: "Prefix with is/has/can/should"
|
|
226
|
+
avoid: "Hungarian notation, type prefixes, abbreviations unless universally known"
|
|
281
227
|
|
|
282
228
|
null_safety:
|
|
283
229
|
default: "Non-nullable types — use ? only when null is meaningful"
|
|
284
230
|
avoid_bang: "Minimize ! operator — use only when null is logically impossible"
|
|
285
231
|
late: "Only when initialization is guaranteed before use"
|
|
286
|
-
pattern: |
|
|
287
|
-
// GOOD
|
|
288
|
-
final name = user?.name ?? 'Anonymous';
|
|
289
|
-
|
|
290
|
-
// AVOID
|
|
291
|
-
final name = user!.name; // crashes if null
|
|
292
232
|
|
|
293
233
|
sealed_classes:
|
|
294
234
|
use_for: "Exhaustive pattern matching on state/result types"
|
|
295
|
-
pattern:
|
|
296
|
-
sealed class AuthState {}
|
|
297
|
-
class AuthInitial extends AuthState {}
|
|
298
|
-
class AuthLoading extends AuthState {}
|
|
299
|
-
class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); }
|
|
300
|
-
class AuthError extends AuthState { final String message; AuthError(this.message); }
|
|
301
|
-
|
|
302
|
-
// Exhaustive switch — compiler enforces all cases
|
|
303
|
-
return switch (state) {
|
|
304
|
-
AuthInitial() => LoginScreen(),
|
|
305
|
-
AuthLoading() => CircularProgressIndicator(),
|
|
306
|
-
AuthSuccess(:final user) => HomeScreen(user: user),
|
|
307
|
-
AuthError(:final message) => ErrorWidget(message),
|
|
308
|
-
};
|
|
235
|
+
pattern: "sealed class with subclass per state, exhaustive switch expression"
|
|
309
236
|
|
|
310
237
|
records:
|
|
311
238
|
use_for: "Lightweight multi-value returns without class boilerplate"
|
|
312
|
-
pattern: "(String name, int age) getUserInfo() => ('Alice', 30);"
|
|
313
239
|
avoid: "Records for complex data — use freezed classes instead"
|
|
314
240
|
|
|
315
241
|
extension_types:
|
|
316
242
|
use_for: "Zero-cost type wrappers for primitive IDs"
|
|
317
|
-
pattern: "extension type UserId(int id) implements int {}"
|
|
318
243
|
|
|
319
244
|
immutability:
|
|
320
245
|
prefer: "final variables, const constructors"
|
|
@@ -331,48 +256,19 @@ dynamic:
|
|
|
331
256
|
reason: "No compile-time type checking, reduces IDE support"
|
|
332
257
|
```
|
|
333
258
|
|
|
259
|
+
Reference: guides/flutter/fundamentals.md
|
|
260
|
+
|
|
334
261
|
### 7. Architecture & Project Structure
|
|
335
262
|
|
|
336
263
|
```yaml
|
|
337
264
|
default_structure:
|
|
338
|
-
small_app:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
services/
|
|
342
|
-
screens/
|
|
343
|
-
widgets/
|
|
344
|
-
medium_app: |
|
|
345
|
-
lib/
|
|
346
|
-
ui/
|
|
347
|
-
core/themes/, core/widgets/
|
|
348
|
-
<feature>/<feature>_screen.dart
|
|
349
|
-
<feature>/<feature>_viewmodel.dart
|
|
350
|
-
data/
|
|
351
|
-
repositories/<entity>_repository.dart
|
|
352
|
-
services/<source>_service.dart
|
|
353
|
-
domain/ (optional)
|
|
354
|
-
use_cases/
|
|
355
|
-
large_app: |
|
|
356
|
-
lib/
|
|
357
|
-
core/ (shared)
|
|
358
|
-
features/<feature>/
|
|
359
|
-
data/datasources/, data/repositories/
|
|
360
|
-
domain/entities/, domain/usecases/
|
|
361
|
-
presentation/bloc/, presentation/pages/
|
|
265
|
+
small_app: "lib/{models,services,screens,widgets}/"
|
|
266
|
+
medium_app: "lib/{ui/{core/,<feature>/},data/{repositories/,services/},domain/}"
|
|
267
|
+
large_app: "lib/{core/,features/<feature>/{data/,domain/,presentation/}}"
|
|
362
268
|
|
|
363
269
|
navigation:
|
|
364
270
|
default: "go_router (official recommendation)"
|
|
365
|
-
|
|
366
|
-
GoRoute(
|
|
367
|
-
path: '/product/:id',
|
|
368
|
-
builder: (context, state) {
|
|
369
|
-
final id = state.pathParameters['id']!;
|
|
370
|
-
return ProductDetailScreen(id: id);
|
|
371
|
-
},
|
|
372
|
-
)
|
|
373
|
-
go_vs_push: |
|
|
374
|
-
context.go('/path') // replaces stack (navigation reset)
|
|
375
|
-
context.push('/path') // adds to stack (back button works)
|
|
271
|
+
go_vs_push: "context.go() replaces stack; context.push() adds to stack"
|
|
376
272
|
|
|
377
273
|
dependency_injection:
|
|
378
274
|
riverpod: "Built-in — providers as DI (default)"
|
|
@@ -381,13 +277,12 @@ dependency_injection:
|
|
|
381
277
|
|
|
382
278
|
environments:
|
|
383
279
|
pattern: "Flavors + --dart-define for multi-environment builds"
|
|
384
|
-
command: "flutter run --flavor development --target lib/main/main_development.dart"
|
|
385
280
|
rule: "Separate bundle IDs, API URLs, and Firebase config per flavor"
|
|
386
281
|
```
|
|
387
282
|
|
|
388
|
-
|
|
283
|
+
Reference: guides/flutter/architecture.md
|
|
389
284
|
|
|
390
|
-
|
|
285
|
+
## Default Stack
|
|
391
286
|
|
|
392
287
|
```yaml
|
|
393
288
|
state_management: Riverpod 3.0
|
|
@@ -402,8 +297,6 @@ structure: Official MVVM (lib/{ui,data}/)
|
|
|
402
297
|
|
|
403
298
|
## Enterprise Stack
|
|
404
299
|
|
|
405
|
-
For regulated or large-team projects:
|
|
406
|
-
|
|
407
300
|
```yaml
|
|
408
301
|
state_management: BLoC 9.0 + Cubit
|
|
409
302
|
navigation: go_router or auto_route
|
|
@@ -14,25 +14,7 @@ Apply Go backend patterns for building production-ready services.
|
|
|
14
14
|
### 1. Project Structure (Standard Layout)
|
|
15
15
|
|
|
16
16
|
```yaml
|
|
17
|
-
layout:
|
|
18
|
-
project/
|
|
19
|
-
├── cmd/
|
|
20
|
-
│ └── server/
|
|
21
|
-
│ └── main.go
|
|
22
|
-
├── internal/
|
|
23
|
-
│ ├── handler/
|
|
24
|
-
│ ├── service/
|
|
25
|
-
│ ├── repository/
|
|
26
|
-
│ └── model/
|
|
27
|
-
├── pkg/
|
|
28
|
-
│ └── shared/
|
|
29
|
-
├── api/
|
|
30
|
-
│ └── openapi.yaml
|
|
31
|
-
├── configs/
|
|
32
|
-
├── scripts/
|
|
33
|
-
├── Dockerfile
|
|
34
|
-
├── Makefile
|
|
35
|
-
└── go.mod
|
|
17
|
+
layout: "cmd/{server/main.go} + internal/{handler/,service/,repository/,model/} + pkg/{shared/} + api/{openapi.yaml} + configs/ + scripts/"
|
|
36
18
|
|
|
37
19
|
directories:
|
|
38
20
|
cmd: Main applications (one per binary)
|
|
@@ -43,6 +25,8 @@ directories:
|
|
|
43
25
|
scripts: Build and CI scripts
|
|
44
26
|
```
|
|
45
27
|
|
|
28
|
+
Reference: guides/go-backend/project-layout.md
|
|
29
|
+
|
|
46
30
|
### 2. Error Handling (Uber Style)
|
|
47
31
|
|
|
48
32
|
```yaml
|
|
@@ -52,28 +36,14 @@ principles:
|
|
|
52
36
|
- Use sentinel errors for specific conditions
|
|
53
37
|
- Name error variables with Err prefix
|
|
54
38
|
|
|
55
|
-
patterns:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
ErrInvalidInput = errors.New("invalid input")
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
// Wrap with context
|
|
63
|
-
func getUser(id string) (*User, error) {
|
|
64
|
-
user, err := db.FindUser(id)
|
|
65
|
-
if err != nil {
|
|
66
|
-
return nil, fmt.Errorf("getUser %s: %w", id, err)
|
|
67
|
-
}
|
|
68
|
-
return user, nil
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Check specific errors
|
|
72
|
-
if errors.Is(err, ErrNotFound) {
|
|
73
|
-
return http.StatusNotFound
|
|
74
|
-
}
|
|
39
|
+
patterns:
|
|
40
|
+
sentinel: "var ErrNotFound = errors.New(\"not found\")"
|
|
41
|
+
wrapping: "fmt.Errorf(\"getUser %s: %w\", id, err)"
|
|
42
|
+
checking: "errors.Is(err, ErrNotFound)"
|
|
75
43
|
```
|
|
76
44
|
|
|
45
|
+
Reference: guides/go-backend/uber-style.md
|
|
46
|
+
|
|
77
47
|
### 3. Concurrency (Uber Style)
|
|
78
48
|
|
|
79
49
|
```yaml
|
|
@@ -84,44 +54,11 @@ channels:
|
|
|
84
54
|
goroutines:
|
|
85
55
|
never: fire-and-forget
|
|
86
56
|
always: wait for completion or manage lifecycle
|
|
87
|
-
|
|
88
|
-
patterns: |
|
|
89
|
-
// Wait group for goroutines
|
|
90
|
-
func process(items []Item) error {
|
|
91
|
-
var wg sync.WaitGroup
|
|
92
|
-
errCh := make(chan error, 1)
|
|
93
|
-
|
|
94
|
-
for _, item := range items {
|
|
95
|
-
wg.Add(1)
|
|
96
|
-
go func(item Item) {
|
|
97
|
-
defer wg.Done()
|
|
98
|
-
if err := processItem(item); err != nil {
|
|
99
|
-
select {
|
|
100
|
-
case errCh <- err:
|
|
101
|
-
default:
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}(item)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
wg.Wait()
|
|
108
|
-
close(errCh)
|
|
109
|
-
return <-errCh
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Context for cancellation
|
|
113
|
-
func longRunningTask(ctx context.Context) error {
|
|
114
|
-
for {
|
|
115
|
-
select {
|
|
116
|
-
case <-ctx.Done():
|
|
117
|
-
return ctx.Err()
|
|
118
|
-
default:
|
|
119
|
-
// do work
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
57
|
+
patterns: "sync.WaitGroup + error channel for parallel work; context.Context for cancellation"
|
|
123
58
|
```
|
|
124
59
|
|
|
60
|
+
Reference: guides/go-backend/uber-style.md
|
|
61
|
+
|
|
125
62
|
### 4. HTTP Server
|
|
126
63
|
|
|
127
64
|
```yaml
|
|
@@ -130,88 +67,24 @@ structure:
|
|
|
130
67
|
service: Business logic
|
|
131
68
|
repository: Data access
|
|
132
69
|
|
|
133
|
-
patterns:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
func NewUserHandler(s UserService) *UserHandler {
|
|
140
|
-
return &UserHandler{service: s}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
144
|
-
id := chi.URLParam(r, "id")
|
|
145
|
-
|
|
146
|
-
user, err := h.service.GetUser(r.Context(), id)
|
|
147
|
-
if err != nil {
|
|
148
|
-
if errors.Is(err, ErrNotFound) {
|
|
149
|
-
http.Error(w, "user not found", http.StatusNotFound)
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
json.NewEncoder(w).Encode(user)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Router setup
|
|
160
|
-
func NewRouter(h *UserHandler) *chi.Mux {
|
|
161
|
-
r := chi.NewRouter()
|
|
162
|
-
r.Use(middleware.Logger)
|
|
163
|
-
r.Use(middleware.Recoverer)
|
|
164
|
-
|
|
165
|
-
r.Route("/api/v1", func(r chi.Router) {
|
|
166
|
-
r.Get("/users/{id}", h.GetUser)
|
|
167
|
-
r.Post("/users", h.CreateUser)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
return r
|
|
171
|
-
}
|
|
70
|
+
patterns:
|
|
71
|
+
dependency_injection: "Handler struct with service field, constructor NewXxxHandler()"
|
|
72
|
+
router: "chi.NewRouter() with middleware (Logger, Recoverer) and versioned routes"
|
|
73
|
+
error_mapping: "errors.Is() to map domain errors to HTTP status codes"
|
|
172
74
|
```
|
|
173
75
|
|
|
76
|
+
Reference: guides/go-backend/uber-style.md
|
|
77
|
+
|
|
174
78
|
### 5. Dependency Injection
|
|
175
79
|
|
|
176
80
|
```yaml
|
|
177
81
|
approach: constructor injection
|
|
178
82
|
avoid: global variables
|
|
179
|
-
|
|
180
|
-
patterns: |
|
|
181
|
-
// Service with dependencies
|
|
182
|
-
type UserService struct {
|
|
183
|
-
repo UserRepository
|
|
184
|
-
cache Cache
|
|
185
|
-
logger *slog.Logger
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
func NewUserService(
|
|
189
|
-
repo UserRepository,
|
|
190
|
-
cache Cache,
|
|
191
|
-
logger *slog.Logger,
|
|
192
|
-
) *UserService {
|
|
193
|
-
return &UserService{
|
|
194
|
-
repo: repo,
|
|
195
|
-
cache: cache,
|
|
196
|
-
logger: logger,
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Wire up in main
|
|
201
|
-
func main() {
|
|
202
|
-
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
203
|
-
db := database.New(cfg.DatabaseURL)
|
|
204
|
-
cache := redis.New(cfg.RedisURL)
|
|
205
|
-
|
|
206
|
-
repo := repository.NewUserRepository(db)
|
|
207
|
-
service := service.NewUserService(repo, cache, logger)
|
|
208
|
-
handler := handler.NewUserHandler(service)
|
|
209
|
-
|
|
210
|
-
router := handler.NewRouter(handler)
|
|
211
|
-
http.ListenAndServe(":8080", router)
|
|
212
|
-
}
|
|
83
|
+
pattern: "Struct with dependencies as fields, New* constructor functions, wire up in main()"
|
|
213
84
|
```
|
|
214
85
|
|
|
86
|
+
Reference: guides/go-backend/uber-style.md
|
|
87
|
+
|
|
215
88
|
### 6. Configuration
|
|
216
89
|
|
|
217
90
|
```yaml
|
|
@@ -219,30 +92,11 @@ approach:
|
|
|
219
92
|
- Use environment variables
|
|
220
93
|
- Validate at startup
|
|
221
94
|
- Group related settings
|
|
222
|
-
|
|
223
|
-
patterns: |
|
|
224
|
-
type Config struct {
|
|
225
|
-
Server ServerConfig
|
|
226
|
-
Database DatabaseConfig
|
|
227
|
-
Redis RedisConfig
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
type ServerConfig struct {
|
|
231
|
-
Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
|
|
232
|
-
Port int `env:"SERVER_PORT" envDefault:"8080"`
|
|
233
|
-
ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"5s"`
|
|
234
|
-
WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"`
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
func LoadConfig() (*Config, error) {
|
|
238
|
-
var cfg Config
|
|
239
|
-
if err := env.Parse(&cfg); err != nil {
|
|
240
|
-
return nil, fmt.Errorf("parse config: %w", err)
|
|
241
|
-
}
|
|
242
|
-
return &cfg, nil
|
|
243
|
-
}
|
|
95
|
+
pattern: "Config struct with nested typed configs, env struct tags, Parse at startup"
|
|
244
96
|
```
|
|
245
97
|
|
|
98
|
+
Reference: guides/go-backend/uber-style.md
|
|
99
|
+
|
|
246
100
|
### 7. Testing
|
|
247
101
|
|
|
248
102
|
```yaml
|
|
@@ -250,55 +104,11 @@ patterns:
|
|
|
250
104
|
table_driven: for comprehensive coverage
|
|
251
105
|
interfaces: for mocking
|
|
252
106
|
parallel: for speed
|
|
253
|
-
|
|
254
|
-
example: |
|
|
255
|
-
func TestUserService_GetUser(t *testing.T) {
|
|
256
|
-
tests := []struct {
|
|
257
|
-
name string
|
|
258
|
-
userID string
|
|
259
|
-
mock func(*MockRepository)
|
|
260
|
-
want *User
|
|
261
|
-
wantErr error
|
|
262
|
-
}{
|
|
263
|
-
{
|
|
264
|
-
name: "success",
|
|
265
|
-
userID: "123",
|
|
266
|
-
mock: func(m *MockRepository) {
|
|
267
|
-
m.EXPECT().FindByID("123").Return(&User{ID: "123"}, nil)
|
|
268
|
-
},
|
|
269
|
-
want: &User{ID: "123"},
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
name: "not found",
|
|
273
|
-
userID: "999",
|
|
274
|
-
mock: func(m *MockRepository) {
|
|
275
|
-
m.EXPECT().FindByID("999").Return(nil, ErrNotFound)
|
|
276
|
-
},
|
|
277
|
-
wantErr: ErrNotFound,
|
|
278
|
-
},
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
for _, tt := range tests {
|
|
282
|
-
t.Run(tt.name, func(t *testing.T) {
|
|
283
|
-
t.Parallel()
|
|
284
|
-
ctrl := gomock.NewController(t)
|
|
285
|
-
repo := NewMockRepository(ctrl)
|
|
286
|
-
tt.mock(repo)
|
|
287
|
-
|
|
288
|
-
svc := NewUserService(repo, nil, slog.Default())
|
|
289
|
-
got, err := svc.GetUser(context.Background(), tt.userID)
|
|
290
|
-
|
|
291
|
-
if !errors.Is(err, tt.wantErr) {
|
|
292
|
-
t.Errorf("got error %v, want %v", err, tt.wantErr)
|
|
293
|
-
}
|
|
294
|
-
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
295
|
-
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
}
|
|
107
|
+
tools: "gomock for mocks, cmp.Diff for assertions"
|
|
300
108
|
```
|
|
301
109
|
|
|
110
|
+
Reference: guides/go-backend/uber-style.md
|
|
111
|
+
|
|
302
112
|
### 8. Performance (Uber Style)
|
|
303
113
|
|
|
304
114
|
```yaml
|
|
@@ -307,24 +117,10 @@ guidelines:
|
|
|
307
117
|
- Pre-allocate slices with known capacity
|
|
308
118
|
- Avoid repeated string-to-byte conversions
|
|
309
119
|
- Copy slices/maps at boundaries
|
|
310
|
-
|
|
311
|
-
patterns: |
|
|
312
|
-
// Pre-allocate
|
|
313
|
-
items := make([]Item, 0, len(input))
|
|
314
|
-
|
|
315
|
-
// strconv for conversions
|
|
316
|
-
s := strconv.Itoa(n) // not fmt.Sprintf("%d", n)
|
|
317
|
-
|
|
318
|
-
// Copy at boundaries
|
|
319
|
-
func (s *Store) GetItems() []Item {
|
|
320
|
-
s.mu.RLock()
|
|
321
|
-
defer s.mu.RUnlock()
|
|
322
|
-
items := make([]Item, len(s.items))
|
|
323
|
-
copy(items, s.items)
|
|
324
|
-
return items
|
|
325
|
-
}
|
|
326
120
|
```
|
|
327
121
|
|
|
122
|
+
Reference: guides/go-backend/uber-style.md
|
|
123
|
+
|
|
328
124
|
## Application
|
|
329
125
|
|
|
330
126
|
When writing Go backend code:
|