@sun-asterisk/sunlint 1.3.48 → 1.3.49
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/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Ignore Superclass Logic
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: ensures proper inheritance behavior
|
|
5
|
+
tags: inheritance, override, superclass, oop, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Ignore Superclass Logic
|
|
9
|
+
|
|
10
|
+
When overriding methods, ensure superclass behavior is preserved unless explicitly intended otherwise. This is especially important in Flutter for `initState`, `dispose`, `build`, and lifecycle methods.
|
|
11
|
+
|
|
12
|
+
**Incorrect (ignoring superclass):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
class BaseRepository {
|
|
16
|
+
Future<void> save(Entity entity) async {
|
|
17
|
+
validate(entity);
|
|
18
|
+
await beforeSave(entity);
|
|
19
|
+
await database.insert(entity.toMap());
|
|
20
|
+
await afterSave(entity);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class UserRepository extends BaseRepository {
|
|
25
|
+
@override
|
|
26
|
+
Future<void> save(User user) async {
|
|
27
|
+
// Completely ignores validation, hooks, etc.
|
|
28
|
+
await database.insert(user.toMap());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (calling super):**
|
|
34
|
+
|
|
35
|
+
```dart
|
|
36
|
+
class UserRepository extends BaseRepository {
|
|
37
|
+
@override
|
|
38
|
+
Future<void> save(User user) async {
|
|
39
|
+
// Add user-specific preprocessing
|
|
40
|
+
user = user.copyWith(updatedAt: DateTime.now());
|
|
41
|
+
|
|
42
|
+
// Call superclass implementation
|
|
43
|
+
await super.save(user);
|
|
44
|
+
|
|
45
|
+
// Add user-specific postprocessing
|
|
46
|
+
await updateSearchIndex(user);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Flutter lifecycle - always call super at correct position:**
|
|
52
|
+
|
|
53
|
+
```dart
|
|
54
|
+
// initState: super FIRST
|
|
55
|
+
@override
|
|
56
|
+
void initState() {
|
|
57
|
+
super.initState(); // Must be first
|
|
58
|
+
_controller = AnimationController(vsync: this);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// dispose: super LAST
|
|
62
|
+
@override
|
|
63
|
+
void dispose() {
|
|
64
|
+
_controller.dispose(); // Clean up first
|
|
65
|
+
super.dispose(); // Must be last
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// didChangeDependencies: super FIRST
|
|
69
|
+
@override
|
|
70
|
+
void didChangeDependencies() {
|
|
71
|
+
super.didChangeDependencies();
|
|
72
|
+
_theme = Theme.of(context);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**When to intentionally skip super:**
|
|
77
|
+
|
|
78
|
+
```dart
|
|
79
|
+
class UserRepository extends BaseRepository {
|
|
80
|
+
/// Override: users require special serialization,
|
|
81
|
+
/// base validation is not applicable for legacy schema.
|
|
82
|
+
@override
|
|
83
|
+
Future<void> save(User user) async {
|
|
84
|
+
await validateUser(user); // Custom validation replaces base
|
|
85
|
+
// Intentionally not calling super.save()
|
|
86
|
+
await userTable.upsert(user.toLegacyMap());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Tools:** dart_analyzer, Code Review, flutter_lints
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Hardcode Configuration
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables environment-specific deployments
|
|
5
|
+
tags: configuration, environment, deployment, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Hardcode Configuration
|
|
9
|
+
|
|
10
|
+
Hardcoded config requires code changes to deploy to different environments.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcoded config):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
const apiUrl = 'https://api.production.example.com';
|
|
16
|
+
const timeout = 5000;
|
|
17
|
+
const maxFileSize = 10485760;
|
|
18
|
+
const enableAnalytics = true;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct (externalized config via dart-define):**
|
|
22
|
+
|
|
23
|
+
```dart
|
|
24
|
+
// lib/config/app_config.dart
|
|
25
|
+
class AppConfig {
|
|
26
|
+
AppConfig._();
|
|
27
|
+
|
|
28
|
+
static const String apiUrl = String.fromEnvironment(
|
|
29
|
+
'API_URL',
|
|
30
|
+
defaultValue: 'https://api.dev.example.com',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
static const int timeoutMs = int.fromEnvironment(
|
|
34
|
+
'TIMEOUT_MS',
|
|
35
|
+
defaultValue: 5000,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
static const int maxFileSizeBytes = int.fromEnvironment(
|
|
39
|
+
'MAX_FILE_SIZE',
|
|
40
|
+
defaultValue: 10 * 1024 * 1024, // 10MB
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
static const bool enableAnalytics = bool.fromEnvironment(
|
|
44
|
+
'ENABLE_ANALYTICS',
|
|
45
|
+
defaultValue: false,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Usage
|
|
50
|
+
final client = HttpClient(baseUrl: AppConfig.apiUrl, timeout: Duration(milliseconds: AppConfig.timeoutMs));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Build with environment config:**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Development
|
|
57
|
+
flutter run --dart-define=API_URL=https://api.dev.example.com --dart-define=ENABLE_ANALYTICS=false
|
|
58
|
+
|
|
59
|
+
# Production
|
|
60
|
+
flutter build apk --dart-define=API_URL=https://api.production.example.com --dart-define=ENABLE_ANALYTICS=true
|
|
61
|
+
|
|
62
|
+
# Using a JSON config file (flutter_launcher_icons approach)
|
|
63
|
+
flutter run --dart-define-from-file=config/dev.json
|
|
64
|
+
flutter build apk --dart-define-from-file=config/prod.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Example config files:**
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
// config/dev.json
|
|
71
|
+
{
|
|
72
|
+
"API_URL": "https://api.dev.example.com",
|
|
73
|
+
"ENABLE_ANALYTICS": "false",
|
|
74
|
+
"TIMEOUT_MS": "10000"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
// config/prod.json
|
|
80
|
+
{
|
|
81
|
+
"API_URL": "https://api.production.example.com",
|
|
82
|
+
"ENABLE_ANALYTICS": "true",
|
|
83
|
+
"TIMEOUT_MS": "5000"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**For flavor-based config:**
|
|
88
|
+
|
|
89
|
+
```dart
|
|
90
|
+
// lib/config/flavors.dart
|
|
91
|
+
enum Flavor { dev, staging, production }
|
|
92
|
+
|
|
93
|
+
class FlavorConfig {
|
|
94
|
+
static Flavor? _flavor;
|
|
95
|
+
|
|
96
|
+
static void initialize(Flavor flavor) {
|
|
97
|
+
_flavor = flavor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static String get apiUrl => switch (_flavor!) {
|
|
101
|
+
Flavor.dev => 'https://api.dev.example.com',
|
|
102
|
+
Flavor.staging => 'https://api.staging.example.com',
|
|
103
|
+
Flavor.production => 'https://api.example.com',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Tools:** dart-define, envied, flutter_flavor, Code Review
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Go Gin Framework — SunLint Agent Guide
|
|
2
|
+
|
|
3
|
+
> Priority directives for AI agents working on Go + Gin projects.
|
|
4
|
+
> Rule files: `.agent/skills/sunlint-code-quality/rules/`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Critical Patterns — Apply Every Time
|
|
9
|
+
|
|
10
|
+
### Middleware Chain Termination
|
|
11
|
+
- After sending a response that should stop the chain → `c.AbortWithStatusJSON(code, body)` then `return`
|
|
12
|
+
- `c.AbortWithStatusJSON` calls `c.Abort()` internally — don't call both
|
|
13
|
+
- Plain `return` from a handler does NOT stop downstream handlers
|
|
14
|
+
- See: `GN001-abort-after-response.md`
|
|
15
|
+
|
|
16
|
+
### Request Binding + Validation
|
|
17
|
+
- Every `c.ShouldBindJSON(&req)` → must be followed by `if err != nil { c.AbortWithStatusJSON(400, ...); return }`
|
|
18
|
+
- Declare all validation rules as struct `binding:` tags: `binding:"required,email"`, `binding:"min=1,max=255"`
|
|
19
|
+
- **Never** skip the error check from `ShouldBind*`
|
|
20
|
+
- See: `GN003-bind-error-handling.md`, `GN008-struct-validation-tags.md`
|
|
21
|
+
|
|
22
|
+
### Context Usage
|
|
23
|
+
- Inside handlers: `ctx := c.Request.Context()` — pass to all downstream calls
|
|
24
|
+
- **Never** `context.Background()` inside a handler
|
|
25
|
+
- **Never** pass `*gin.Context` to a goroutine — copy scalar values first, pass `c.Request.Context()`
|
|
26
|
+
- See: `GN002-request-context.md`, `GN010-context-scope.md`
|
|
27
|
+
|
|
28
|
+
### Do Not Log Sensitive DataTesting commands rõ ràng trong package.json/Makefile
|
|
29
|
+
Standard conventions (eslint, prettier, gofmt đã config sẵn)
|
|
30
|
+
4. Test với agents trước khi commit
|
|
31
|
+
Quy trình recommended:
|
|
32
|
+
|
|
33
|
+
Bước 1: Tạo baseline
|
|
34
|
+
|
|
35
|
+
# Chạy agent KHÔNG có AGENTS.md trên vài tasks nhỏ
|
|
36
|
+
# Đo success rate, cost
|
|
37
|
+

|
|
38
|
+
Bước 2: Thêm AGENTS.md, measure lại
|
|
39
|
+
|
|
40
|
+
# So sánh với baseline
|
|
41
|
+
# Nếu không improve hoặc cost tăng quá nhiều → bỏ AGENTS.md
|
|
42
|
+

|
|
43
|
+
Bước 3: Iterate
|
|
44
|
+
|
|
45
|
+
# Nếu quyết định giữ AGENTS.md, hãy giữ nó concise
|
|
46
|
+
# Monitor agent behavior qua time
|
|
47
|
+

|
|
48
|
+
5. Monitor Context Length
|
|
49
|
+
Context files nên dưới 500 tokens (t
|
|
50
|
+
- Logging middleware logs only: method, path, status, duration, request_id, client IP
|
|
51
|
+
- **Never** log request bodies, `Authorization` header values, or any field named password/token/card
|
|
52
|
+
- See: `GN012-no-log-sensitive.md`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Architecture Patterns
|
|
57
|
+
|
|
58
|
+
### Handler struct — dependency injection
|
|
59
|
+
Handlers are structs with injected service interfaces. No globals. No `new(Service)` inside handlers.
|
|
60
|
+
```go
|
|
61
|
+
type OrderHandler struct {
|
|
62
|
+
service OrderService // interface — mockable
|
|
63
|
+
logger *slog.Logger
|
|
64
|
+
}
|
|
65
|
+
func NewOrderHandler(svc OrderService, logger *slog.Logger) *OrderHandler {
|
|
66
|
+
return &OrderHandler{service: svc, logger: logger}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
See: `GN004-dependency-injection.md`
|
|
70
|
+
|
|
71
|
+
### Route organisation
|
|
72
|
+
```go
|
|
73
|
+
r := gin.New()
|
|
74
|
+
r.Use(gin.Recovery(), RequestID(), RequestLogger(logger)) // global
|
|
75
|
+
|
|
76
|
+
public := r.Group("/api/v1")
|
|
77
|
+
{ /* unauthenticated routes */ }
|
|
78
|
+
|
|
79
|
+
private := r.Group("/api/v1")
|
|
80
|
+
private.Use(JWTAuth(validator))
|
|
81
|
+
{ /* authenticated routes */ }
|
|
82
|
+
```
|
|
83
|
+
See: `GN005-route-groups-middleware.md`, `GN011-middleware-concerns.md`
|
|
84
|
+
|
|
85
|
+
### Production setup checklist
|
|
86
|
+
```go
|
|
87
|
+
gin.SetMode(os.Getenv("GIN_MODE")) // "release" in prod — GN007
|
|
88
|
+
r := gin.New()
|
|
89
|
+
r.Use(CustomRecovery(logger)) // GN009
|
|
90
|
+
r.Use(RequestID()) // GN011
|
|
91
|
+
r.Use(RequestLogger(logger)) // GN012
|
|
92
|
+
// ... register groups (GN005)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### HTTP Status Codes
|
|
96
|
+
- `POST` success → `c.JSON(http.StatusCreated, obj)` (201)
|
|
97
|
+
- `DELETE` no body → `c.Status(http.StatusNoContent)` (204)
|
|
98
|
+
- Not found → `c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})` (404)
|
|
99
|
+
- Validation error → `c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})` (400)
|
|
100
|
+
- **Always** use `net/http` constants — never magic integers
|
|
101
|
+
- See: `GN006-http-status-codes.md`
|
|
102
|
+
|
|
103
|
+
### Cross-cutting concerns
|
|
104
|
+
Auth, rate limiting, CORS, request ID, structured logging → **always middleware**, never inside handlers.
|
|
105
|
+
See: `GN011-middleware-concerns.md`
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Run Commands
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Development
|
|
113
|
+
go run ./cmd/api/main.go
|
|
114
|
+
GIN_MODE=debug go run ./cmd/api/main.go
|
|
115
|
+
|
|
116
|
+
# Build
|
|
117
|
+
go build -o bin/api ./cmd/api/
|
|
118
|
+
|
|
119
|
+
# Test (always with race detector)
|
|
120
|
+
go test -race ./...
|
|
121
|
+
go test -race -run TestOrderHandler ./internal/handlers/
|
|
122
|
+
go test -cover ./...
|
|
123
|
+
|
|
124
|
+
# Linting
|
|
125
|
+
golangci-lint run ./...
|
|
126
|
+
staticcheck ./...
|
|
127
|
+
|
|
128
|
+
# Env for production
|
|
129
|
+
GIN_MODE=release
|
|
130
|
+
PORT=8080
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## What NOT to do (quick reference)
|
|
136
|
+
|
|
137
|
+
| ❌ Wrong | ✅ Correct |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `return` after `c.JSON` in middleware | `c.AbortWithStatusJSON(...)` then `return` |
|
|
140
|
+
| `context.Background()` in handler | `c.Request.Context()` |
|
|
141
|
+
| `go func() { use c.Get(...) }()` | Copy value before goroutine: `val := c.GetString(...)` |
|
|
142
|
+
| `c.ShouldBindJSON(&req)` without err check | `if err := c.ShouldBindJSON(&req); err != nil { abort }` |
|
|
143
|
+
| Manual `if req.Email == ""` validation | `binding:"required,email"` struct tag |
|
|
144
|
+
| `var db *gorm.DB` package global | Inject `db *gorm.DB` as struct field |
|
|
145
|
+
| Auth check inside handler | `JWTAuth()` middleware on route group |
|
|
146
|
+
| `log.Printf("body: %s", body)` | Log method+path+status+duration only |
|
|
147
|
+
| `gin.Default()` in production | `gin.New()` + explicit `Recovery()` + structured logger |
|
|
148
|
+
| `c.JSON(200, gin.H{"error": ...})` | `c.AbortWithStatusJSON(http.StatusBadRequest, ...)` |
|
|
149
|
+
| `GIN_MODE` not set (debug default) | `GIN_MODE=release` in production environment |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN001 – Call c.Abort() After Terminating in Middleware"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Missing c.Abort() causes subsequent middleware handlers and the final route handler to still execute after an early response has been sent, causing duplicate writes and data leaks."
|
|
5
|
+
tags: [go, gin, middleware, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN001 – Call `c.Abort()` After Terminating in Middleware
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Any middleware that sends a response and intends to stop the chain **must** call `c.Abort()` (or `c.AbortWithStatus()` / `c.AbortWithStatusJSON()`) immediately after writing. Never rely on `return` alone.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`c.Next()` executes subsequent handlers in sequence. A plain `return` exits the current handler but Gin still runs the remaining handlers in the chain. `c.Abort()` sets the index past all remaining handlers so the chain stops cleanly.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func AuthMiddleware() gin.HandlerFunc {
|
|
22
|
+
return func(c *gin.Context) {
|
|
23
|
+
token := c.GetHeader("Authorization")
|
|
24
|
+
if token == "" {
|
|
25
|
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
26
|
+
return // ❌ returns from this func but downstream handlers still run
|
|
27
|
+
}
|
|
28
|
+
c.Next()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func RateLimitMiddleware() gin.HandlerFunc {
|
|
33
|
+
return func(c *gin.Context) {
|
|
34
|
+
if isRateLimited(c.ClientIP()) {
|
|
35
|
+
c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
|
|
36
|
+
// ❌ forgot both return and Abort — next handler fires and writes again
|
|
37
|
+
}
|
|
38
|
+
c.Next()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Correct
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
func AuthMiddleware() gin.HandlerFunc {
|
|
47
|
+
return func(c *gin.Context) {
|
|
48
|
+
token := c.GetHeader("Authorization")
|
|
49
|
+
if token == "" {
|
|
50
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
51
|
+
// c.Abort() is called inside AbortWithStatusJSON — no c.Next() needed
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
// validation passes — continue chain
|
|
55
|
+
c.Set("user_id", parsedUserID)
|
|
56
|
+
c.Next()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func RateLimitMiddleware() gin.HandlerFunc {
|
|
61
|
+
return func(c *gin.Context) {
|
|
62
|
+
if isRateLimited(c.ClientIP()) {
|
|
63
|
+
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
c.Next()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- `c.AbortWithStatus(code)` and `c.AbortWithStatusJSON(code, obj)` both call `c.Abort()` internally — you don't need to call `c.Abort()` separately when using these.
|
|
74
|
+
- After `c.Abort()`, code after the call in the same function still runs — use `return` to exit the function body.
|
|
75
|
+
- Verify with unit tests: register middleware + handler, send a bad request, assert the handler body was NOT executed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN002 – Use c.Request.Context(), Not context.Background()"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "context.Background() ignores client disconnects and request deadlines; c.Request.Context() propagates cancellation automatically."
|
|
5
|
+
tags: [go, gin, context, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN002 – Use `c.Request.Context()` Not `context.Background()`
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
When passing a context to database calls, external HTTP requests, or any `ctx context.Context` parameter inside a Gin handler, always use `c.Request.Context()`. Never create a fresh `context.Background()` inside a handler.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`c.Request.Context()` is derived from the HTTP request and carries the client's cancellation signal. If the client disconnects mid-request, the context is cancelled and all downstream work (DB queries, gRPC calls) is cancelled too — preventing wasted compute. `context.Background()` is never cancelled and leaks work after the client is gone.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
22
|
+
var req CreateOrderRequest
|
|
23
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
24
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ❌ context.Background() — ignores client disconnect and server deadlines
|
|
29
|
+
order, err := h.service.CreateOrder(context.Background(), req)
|
|
30
|
+
if err != nil {
|
|
31
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
c.JSON(http.StatusCreated, order)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Correct
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
42
|
+
var req CreateOrderRequest
|
|
43
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
44
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ propagates request lifetime, deadline, and cancellation
|
|
49
|
+
ctx := c.Request.Context()
|
|
50
|
+
order, err := h.service.CreateOrder(ctx, req)
|
|
51
|
+
if err != nil {
|
|
52
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
c.JSON(http.StatusCreated, order)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Extract the context once: `ctx := c.Request.Context()` at the top of the handler body.
|
|
62
|
+
- Do **not** store this context in a struct field — contexts must flow through function arguments.
|
|
63
|
+
- If you need a timeout shorter than the request's own deadline: `ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second); defer cancel()`.
|
|
64
|
+
- See GN010 for the rule about not storing Gin context in goroutines.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN003 – Always Handle ShouldBindJSON / ShouldBind Errors"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Ignoring ShouldBindJSON errors means malformed or missing fields are silently treated as zero values, causing corrupted data writes and bypassed validation."
|
|
5
|
+
tags: [go, gin, validation, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN003 – Always Handle `ShouldBindJSON` / `ShouldBind` Errors
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Every call to `c.ShouldBindJSON`, `c.ShouldBind`, `c.ShouldBindQuery`, or `c.ShouldBindUri` must be followed by an error check. If the error is non-nil, abort immediately with a `400 Bad Request` response.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`ShouldBind*` populates the target struct but returns an error for malformed JSON, missing required fields (enforced by `binding:"required"` tags), or type mismatches. Ignoring the error means the handler continues with a partially or incorrectly populated struct, silently writing bad data.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
22
|
+
var req CreateUserRequest
|
|
23
|
+
c.ShouldBindJSON(&req) // ❌ error ignored — continues with zero-value struct fields
|
|
24
|
+
|
|
25
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
26
|
+
// ...
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Worse: using BindJSON (panics on error in some versions, logs but doesn't stop)
|
|
30
|
+
func (h *UserHandler) Update(c *gin.Context) {
|
|
31
|
+
var req UpdateUserRequest
|
|
32
|
+
c.BindJSON(&req) // ❌ BindJSON writes 400 header but handler still continues
|
|
33
|
+
h.service.UpdateUser(c.Request.Context(), req)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Correct
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
type CreateUserRequest struct {
|
|
41
|
+
Name string `json:"name" binding:"required,min=2,max=100"`
|
|
42
|
+
Email string `json:"email" binding:"required,email"`
|
|
43
|
+
Age int `json:"age" binding:"required,min=18"`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
47
|
+
var req CreateUserRequest
|
|
48
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
49
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
50
|
+
"error": "invalid request",
|
|
51
|
+
"details": err.Error(),
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
57
|
+
if err != nil {
|
|
58
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"})
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
c.JSON(http.StatusCreated, user)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- Prefer `ShouldBind*` over `Bind*` — the `Bind*` family writes a `400` header but doesn't abort the handler, making code flow confusing.
|
|
68
|
+
- Use struct tags: `binding:"required"`, `binding:"email"`, `binding:"min=1,max=255"` to shift validation into the binding layer.
|
|
69
|
+
- For detailed error messages, type-assert the error to `validator.ValidationErrors` and format field names cleanly.
|
|
70
|
+
- See GN008 for struct validation tag conventions.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN004 – Use Dependency Injection, Not Global Variables"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Global database connections and service instances in package-level vars are untestable, race-prone, and make initialization order fragile."
|
|
5
|
+
tags: [go, gin, architecture, testing]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN004 – Use Dependency Injection, Not Global Variables
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Database connections, service instances, configuration objects, and HTTP clients must be injected as struct fields into handlers. Never declare them as `var db *gorm.DB` at package level and access them from handler functions.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Global state:
|
|
17
|
+
- Cannot be replaced with a mock in unit tests.
|
|
18
|
+
- Initialization order is implicit — the global may be `nil` if setup hasn't run.
|
|
19
|
+
- Race conditions when tests share globals.
|
|
20
|
+
|
|
21
|
+
Dependency injection via struct fields makes dependencies explicit, swappable, and testable.
|
|
22
|
+
|
|
23
|
+
## Wrong
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// db/db.go — package-level global
|
|
27
|
+
var DB *gorm.DB
|
|
28
|
+
|
|
29
|
+
func Init() {
|
|
30
|
+
DB, _ = gorm.Open(...)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// handlers/user.go — accesses global directly
|
|
34
|
+
func GetUser(c *gin.Context) {
|
|
35
|
+
var user User
|
|
36
|
+
db.DB.First(&user, c.Param("id")) // ❌ depends on global initialization order
|
|
37
|
+
c.JSON(200, user)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Correct
|
|
42
|
+
|
|
43
|
+
```go
|
|
44
|
+
// handlers/user_handler.go
|
|
45
|
+
type UserHandler struct {
|
|
46
|
+
db *gorm.DB // injected
|
|
47
|
+
service UserService // interface — swappable in tests
|
|
48
|
+
logger *slog.Logger
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func NewUserHandler(db *gorm.DB, svc UserService, logger *slog.Logger) *UserHandler {
|
|
52
|
+
return &UserHandler{db: db, service: svc, logger: logger}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
56
|
+
ctx := c.Request.Context()
|
|
57
|
+
user, err := h.service.FindUserByID(ctx, c.Param("id"))
|
|
58
|
+
if err != nil {
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
c.JSON(http.StatusOK, user)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// main.go — wiring
|
|
66
|
+
db := database.Connect(cfg.DSN)
|
|
67
|
+
userSvc := services.NewUserService(db)
|
|
68
|
+
userHandler := handlers.NewUserHandler(db, userSvc, logger)
|
|
69
|
+
|
|
70
|
+
r := gin.New()
|
|
71
|
+
r.GET("/users/:id", userHandler.GetUser)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Define service dependencies as Go interfaces so handlers can be unit-tested with mocks without a real DB.
|
|
77
|
+
- Use a DI framework (e.g., `google/wire`, `samber/do`) for large projects to automate wiring.
|
|
78
|
+
- Configuration (`Config` structs) should be loaded once in `main` and injected — never read from `os.Getenv` directly inside handlers.
|