@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,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR006 – Use find_each / in_batches for Bulk Iteration"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "find_each limits memory to batch size (1000 rows) instead of loading all records at once, preventing OOM in background jobs and rake tasks."
|
|
5
|
+
tags: [ruby, rails, performance, database]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR006 – Use find_each / in_batches for Bulk Iteration
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
When iterating over all records (in jobs, scripts, migrations, Rake tasks), always use `find_each` or `in_batches` instead of `all.each` or `where(...).each`.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`Model.all.each` fetches all matching rows in one SQL query and holds them all in memory. For large tables this causes OOM. `find_each` internally runs batched queries (`LIMIT 1000 ORDER BY id`) and yields one record at a time.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Loads every user into memory at once
|
|
22
|
+
User.all.each do |user|
|
|
23
|
+
UserMailer.renewal(user).deliver_later
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Same problem with a where scope
|
|
27
|
+
Order.where(status: 'pending').each { |o| processOrder(o) }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Correct
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# find_each: yields one record at a time, batched under the hood
|
|
34
|
+
User.find_each(batch_size: 500) do |user|
|
|
35
|
+
UserMailer.renewal(user).deliver_later
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# in_batches: yields an ActiveRecord::Relation per batch (useful for bulk updates)
|
|
39
|
+
Order.where(status: 'pending').in_batches(of: 200) do |batch|
|
|
40
|
+
batch.update_all(processed: true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# start: / finish: let you resume from a checkpoint
|
|
44
|
+
User.find_each(start: last_processed_id) do |user|
|
|
45
|
+
# ...
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
- `find_each` requires a primary key — does not work with `select` that omits the PK.
|
|
52
|
+
- Avoid adding an `ORDER BY` other than PK with `find_each`; use `in_batches` if you need custom ordering.
|
|
53
|
+
- For read-only iteration on PostgreSQL, consider `cursor()` from the `activerecord-cursor-pagination` gem for server-side cursors.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR007 – Return Correct HTTP Status Codes"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Returning 200 OK for errors silently breaks API clients, prevents correct retry logic, and hides failures in monitoring."
|
|
5
|
+
tags: [ruby, rails, api, http]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR007 – Return Correct HTTP Status Codes
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Always pass an explicit `status:` argument to `render json:`. Never return HTTP 200 for validation errors, authorization failures, or not-found resources.
|
|
13
|
+
|
|
14
|
+
## Wrong
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
def create
|
|
18
|
+
@user = User.new(user_params)
|
|
19
|
+
if @user.save
|
|
20
|
+
render json: @user # ❌ no status — defaults to 200
|
|
21
|
+
else
|
|
22
|
+
render json: { errors: @user.errors } # ❌ 200 on failure
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
@user = User.find_by(id: params[:id])
|
|
28
|
+
render json: @user # ❌ returns 200 with null body if not found
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Correct
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
def create
|
|
36
|
+
@user = User.new(user_params)
|
|
37
|
+
if @user.save
|
|
38
|
+
render json: UserResource.new(@user), status: :created # 201
|
|
39
|
+
else
|
|
40
|
+
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity # 422
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def show
|
|
45
|
+
@user = User.find(params[:id]) # raises ActiveRecord::RecordNotFound → rescued to 404
|
|
46
|
+
render json: UserResource.new(@user), status: :ok
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# In ApplicationController
|
|
50
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
51
|
+
rescue_from Pundit::NotAuthorizedError, with: :forbidden
|
|
52
|
+
|
|
53
|
+
def not_found
|
|
54
|
+
render json: { error: 'Not found' }, status: :not_found # 404
|
|
55
|
+
end
|
|
56
|
+
def forbidden
|
|
57
|
+
render json: { error: 'Forbidden' }, status: :forbidden # 403
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Status code reference
|
|
62
|
+
|
|
63
|
+
| Scenario | Status |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Create success | `201 :created` |
|
|
66
|
+
| Update/delete success | `200 :ok` |
|
|
67
|
+
| No content | `204 :no_content` |
|
|
68
|
+
| Validation failed | `422 :unprocessable_entity` |
|
|
69
|
+
| Not found | `404 :not_found` |
|
|
70
|
+
| Unauthorized (not logged in) | `401 :unauthorized` |
|
|
71
|
+
| Forbidden (not allowed) | `403 :forbidden` |
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- Use symbol form (`:created`) — it's self-documenting and immune to numeric typos.
|
|
76
|
+
- Centralize rescue_from in `ApplicationController` or a concern rather than per-action `rescue`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR008 – Enforce Authentication with before_action"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Missing before_action :authenticate_user! leaves actions publicly accessible by default — opt-out is safer than opt-in."
|
|
5
|
+
tags: [ruby, rails, security, authentication]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR008 – Enforce Authentication with before_action
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Place `before_action :authenticate_user!` in `ApplicationController`. Use `skip_before_action` only for explicitly public endpoints. Never rely on per-action checks or assume private routes are protected.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
If authentication is applied per-action (opt-in), a new action will be public by default — a common source of auth bypass vulnerabilities. Opt-out (skip for public endpoints) is the safe default.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Opt-in per controller — new actions are public by default
|
|
22
|
+
class PostsController < ApplicationController
|
|
23
|
+
def index
|
|
24
|
+
authenticate_user! # ❌ developer must remember to call this
|
|
25
|
+
@posts = Post.all
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create
|
|
29
|
+
# ❌ forgot authenticate_user! — publicly writable
|
|
30
|
+
Post.create!(post_params)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ApplicationController with no global hook
|
|
35
|
+
class ApplicationController < ActionController::API
|
|
36
|
+
# nothing — every action is public until protected
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Correct
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# ApplicationController — everything requires auth
|
|
44
|
+
class ApplicationController < ActionController::API
|
|
45
|
+
include Devise::Controllers::Helpers # or your auth layer
|
|
46
|
+
before_action :authenticate_user!
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def current_user_id
|
|
51
|
+
current_user.id
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Public endpoints explicitly opt out
|
|
56
|
+
class SessionsController < ApplicationController
|
|
57
|
+
skip_before_action :authenticate_user!, only: [:create, :destroy]
|
|
58
|
+
|
|
59
|
+
def create
|
|
60
|
+
# login — intentionally public
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class HealthController < ApplicationController
|
|
65
|
+
skip_before_action :authenticate_user!
|
|
66
|
+
|
|
67
|
+
def show
|
|
68
|
+
render json: { status: 'ok' }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- For Devise APIs, use `authenticate_user!` from `Devise::Controllers::Helpers` or the Devise-JWT extension.
|
|
76
|
+
- Documenting `skip_before_action` with a comment explaining WHY the action is public improves auditability.
|
|
77
|
+
- Add an integration test that requests every route without auth and asserts `status: 401` for protected ones.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR009 – Store Secrets in Rails Credentials, Not in Code"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Hardcoded secrets in source code are committed to Git history and exposed permanently even after deletion."
|
|
5
|
+
tags: [ruby, rails, security, secrets]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR009 – Store Secrets in Rails Credentials
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
All secret values (API keys, tokens, signing secrets, passwords) must live in `config/credentials.yml.enc` or environment variables. Never hardcode them in source files, `config/initializers/`, or `database.yml`.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Once a secret is committed, it lives in Git history forever. Rotating it requires a force-push and re-access revocation. Rails credentials are encrypted at rest; the master key (`config/master.key` or `RAILS_MASTER_KEY`) is the only secret that must stay outside source control.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# config/initializers/stripe.rb
|
|
22
|
+
Stripe.api_key = 'sk_live_XXXXXXXXXXXXX' # ❌ hardcoded
|
|
23
|
+
|
|
24
|
+
# config/database.yml
|
|
25
|
+
production:
|
|
26
|
+
password: 'my_db_password_123' # ❌ hardcoded
|
|
27
|
+
|
|
28
|
+
# app/services/sendgrid_service.rb
|
|
29
|
+
API_KEY = 'SG.xxxxxxxxxxxx' # ❌ constant with embedded secret
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Correct
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Add / edit secrets (encrypts automatically)
|
|
36
|
+
bin/rails credentials:edit --environment production
|
|
37
|
+
|
|
38
|
+
# config/credentials/production.yml.enc (decrypted view)
|
|
39
|
+
stripe:
|
|
40
|
+
api_key: sk_live_XXXXXXXXXXXXX
|
|
41
|
+
sendgrid:
|
|
42
|
+
api_key: SG.xxxxxxxxxxxx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Access anywhere in application code
|
|
47
|
+
Stripe.api_key = Rails.application.credentials.stripe[:api_key]
|
|
48
|
+
|
|
49
|
+
# Or via environment variable (suitable for container deployments)
|
|
50
|
+
Stripe.api_key = ENV.fetch('STRIPE_API_KEY')
|
|
51
|
+
|
|
52
|
+
# config/database.yml (env var form)
|
|
53
|
+
production:
|
|
54
|
+
password: <%= ENV['DATABASE_PASSWORD'] %>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
- `config/master.key` and `config/credentials/production.key` must be in `.gitignore` and never committed.
|
|
60
|
+
- In CI/CD, inject `RAILS_MASTER_KEY` as a masked environment variable, not in cleartext config.
|
|
61
|
+
- For team environments with multiple people needing access, prefer environment variables or a vault (e.g., HashiCorp Vault, AWS Secrets Manager) over shared credentials files.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR010 – Define Reusable Query Scopes in Models"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Inline where() chains scattered across controllers make conditions hard to reuse, test, and change in one place."
|
|
5
|
+
tags: [ruby, rails, code-quality, database]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR010 – Define Reusable Query Scopes in Models
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Encapsulate frequently used `where`, `order`, and `joins` conditions as named scopes on the model. Controllers call scopes by name — they do not build query logic inline.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Query conditions duplicated across controllers and services violate DRY and make maintenance hazardous. A scope is a named, composable, chainable query fragment that belongs to the model (the owner of the data).
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Controller 1
|
|
22
|
+
@posts = Post.where(published: true).where('created_at > ?', 30.days.ago).order(created_at: :desc)
|
|
23
|
+
|
|
24
|
+
# Controller 2 (duplicate, slightly different)
|
|
25
|
+
@featured = Post.where(published: true).where(featured: true).order(created_at: :desc)
|
|
26
|
+
|
|
27
|
+
# Controller 3 (wrong date, bug introduced by copy-paste)
|
|
28
|
+
@recent = Post.where(published: true).where('created_at > ?', 7.days.ago).order(created_at: :asc)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Correct
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# app/models/post.rb
|
|
35
|
+
class Post < ApplicationRecord
|
|
36
|
+
scope :published, -> { where(published: true) }
|
|
37
|
+
scope :featured, -> { where(featured: true) }
|
|
38
|
+
scope :recent, -> { where('created_at > ?', 30.days.ago) }
|
|
39
|
+
scope :by_newest, -> { order(created_at: :desc) }
|
|
40
|
+
|
|
41
|
+
# Scopes with parameters
|
|
42
|
+
scope :created_after, ->(date) { where('created_at > ?', date) }
|
|
43
|
+
scope :by_author, ->(author_id) { where(author_id: author_id) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Controllers compose scopes — clean, readable, no SQL inline
|
|
47
|
+
@posts = Post.published.recent.by_newest
|
|
48
|
+
@featured = Post.published.featured.by_newest
|
|
49
|
+
@mine = Post.published.by_author(current_user.id).recent
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
- Scopes return `ActiveRecord::Relation` — they are chainable by design.
|
|
55
|
+
- Scopes should be tested independently in model specs: `Post.published` should only return published posts.
|
|
56
|
+
- For complex multi-table logic, consider a Query Object (like a service object) that wraps the relation.
|
|
57
|
+
- Avoid scopes with side effects (no writes, callbacks, or network calls).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR011 – Use counter_cache to Avoid Count N+1"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "post.comments.count in a collection loop fires one COUNT query per row; counter_cache collapses this to zero extra queries."
|
|
5
|
+
tags: [ruby, rails, performance, database]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR011 – Use counter_cache to Avoid Count N+1
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
When displaying association counts in collection views or API responses (comment counts, like counts, etc.), use `counter_cache: true` on the `belongs_to` side instead of computing `association.count` in-loop.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`post.comments.count` (or `.size` when collection is not loaded) executes a `COUNT(*)` SQL query. In a list of 50 posts this fires 50 extra queries. `counter_cache` maintains a denormalized `comments_count` column on the parent table updated atomically by Rails on create/destroy.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# In API serializer or view — fires N COUNT queries
|
|
22
|
+
posts.each do |post|
|
|
23
|
+
count = post.comments.count # ❌ 1 COUNT query per post
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# JSON response that triggers N+1
|
|
27
|
+
render json: @posts.map { |p| { id: p.id, comment_count: p.comments.count } }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Correct
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Migration: add the cache column
|
|
34
|
+
add_column :posts, :comments_count, :integer, default: 0, null: false
|
|
35
|
+
# Backfill: Post.find_each { |p| Post.reset_counters(p.id, :comments) }
|
|
36
|
+
|
|
37
|
+
# Model association
|
|
38
|
+
class Comment < ApplicationRecord
|
|
39
|
+
belongs_to :post, counter_cache: true
|
|
40
|
+
# Rails now auto-increments/decrements posts.comments_count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Post < ApplicationRecord
|
|
44
|
+
has_many :comments
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Usage — no extra query
|
|
48
|
+
posts.each do |post|
|
|
49
|
+
count = post.comments_count # ✅ reads the cached column, zero extra queries
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
render json: @posts.map { |p| { id: p.id, comment_count: p.comments_count } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- Reset cache after backfills: `Post.find_each { |p| Post.reset_counters(p.id, :comments) }`.
|
|
58
|
+
- If using soft-delete (`acts_as_paranoid`, `discard`), counter_cache won't exclude soft-deleted records — use a custom query in that case.
|
|
59
|
+
- Alternative when migration is not possible: `Post.includes(:comments)` + `post.comments.size` (`.size` uses already-loaded collection, no extra query).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "RR012 – Always Include status: in render json:"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Omitting status: defaults to 200, masking errors and breaking API client error-handling."
|
|
5
|
+
tags: [ruby, rails, api, http]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# RR012 – Always Include `status:` in `render json:`
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Every `render json:` call must include an explicit `status:` keyword argument. Do not rely on the implicit `200` default.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
This is a codified subset of RR007 focused on a single easy-to-miss omission. Implicit 200 is the default only because it's the most common case — it doesn't mean it's always correct. Explicit status makes intent clear in code review and prevents errors from silently appearing as successes to API clients.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
render json: @user # ❌ implicit 200
|
|
22
|
+
render json: { errors: @user.errors.full_messages } # ❌ looks like success to client
|
|
23
|
+
render json: { message: 'Deleted' } # ❌ should be 200 or 204 — unclear
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
render json: UserResource.new(@user), status: :ok # 200
|
|
30
|
+
render json: UserResource.new(@user), status: :created # 201 for POST
|
|
31
|
+
render json: { errors: @user.errors.full_messages },
|
|
32
|
+
status: :unprocessable_entity # 422
|
|
33
|
+
render json: { message: 'Deleted' }, status: :ok # 200
|
|
34
|
+
# OR for no-body deletes:
|
|
35
|
+
head :no_content # 204, no body
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- Use Rails symbol names (`:ok`, `:created`, `:no_content`) — not integers — for readability.
|
|
41
|
+
- `head :no_content` is the cleanest response for DELETE with no return body.
|
|
42
|
+
- Pair this rule with consistent `rescue_from` in ApplicationController so all error paths also have correct status codes (see RR007).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tên hàm phải là động từ hoặc động từ + danh từ
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Tên hàm mô tả đúng hành động giúp code dễ đọc và giảm cognitive load khi review PR.
|
|
5
|
+
tags: swift, ios, naming, readability, code-quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Tên hàm phải là động từ hoặc động từ + danh từ
|
|
9
|
+
|
|
10
|
+
Tên hàm phải bắt đầu bằng động từ hoặc cụm động từ + danh từ, mô tả rõ ràng hành động mà hàm thực hiện. Tránh đặt tên mơ hồ như `data()`, `handle()`, `process()`.
|
|
11
|
+
|
|
12
|
+
**Incorrect (tên không rõ hành động):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class UserManager {
|
|
16
|
+
func data() -> User? { ... } // Không rõ là lấy hay tạo
|
|
17
|
+
func handle(_ error: Error) { ... } // handle là gì?
|
|
18
|
+
func process(_ payment: Payment) { ... } // process không nói lên kết quả
|
|
19
|
+
func userInfo(for id: String) { ... } // thiếu động từ
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Correct (tên thể hiện hành động rõ ràng):**
|
|
24
|
+
|
|
25
|
+
```swift
|
|
26
|
+
class UserManager {
|
|
27
|
+
func fetchUser(by id: String) -> User? { ... }
|
|
28
|
+
func logError(_ error: Error) { ... }
|
|
29
|
+
func submitPayment(_ payment: Payment) throws { ... }
|
|
30
|
+
func loadUserProfile(userId: String) async throws -> UserProfile { ... }
|
|
31
|
+
func validateEmail(_ email: String) -> Bool { ... }
|
|
32
|
+
func clearCache() { ... }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Tools:** SwiftLint custom rule, Code Review
|
|
37
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không để code chết trong codebase
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: Code không dùng làm tăng kích thước binary, gây nhầm lẫn khi đọc và bảo trì, khiến reviewer tốn thời gian.
|
|
5
|
+
tags: swift, ios, dead-code, cleanup, code-quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không để code chết trong codebase
|
|
9
|
+
|
|
10
|
+
Code không còn được gọi đến, biến không sử dụng, hàm private không được gọi, hay `#if false` block cần được xóa. Dùng Git history nếu cần khôi phục.
|
|
11
|
+
|
|
12
|
+
**Incorrect (code chết):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class OrderViewController: UIViewController {
|
|
16
|
+
|
|
17
|
+
// Không bao giờ được dùng
|
|
18
|
+
private func legacyFetchOrders() {
|
|
19
|
+
// TODO: remove this
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#if false
|
|
23
|
+
private func debugHelper() {
|
|
24
|
+
print("Debug info: \(someVar)")
|
|
25
|
+
}
|
|
26
|
+
#endif
|
|
27
|
+
|
|
28
|
+
// Biến khai báo nhưng không dùng
|
|
29
|
+
private let unusedFormatter = DateFormatter()
|
|
30
|
+
|
|
31
|
+
override func viewDidLoad() {
|
|
32
|
+
super.viewDidLoad()
|
|
33
|
+
loadOrders()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func loadOrders() { ... }
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct (chỉ giữ code đang được dùng):**
|
|
41
|
+
|
|
42
|
+
```swift
|
|
43
|
+
class OrderViewController: UIViewController {
|
|
44
|
+
|
|
45
|
+
override func viewDidLoad() {
|
|
46
|
+
super.viewDidLoad()
|
|
47
|
+
loadOrders()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func loadOrders() { ... }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Tools:** SwiftLint (`unused_private_declaration`), Xcode Warnings, SwiftFormat (`unusedPrivateDeclarations`)
|
|
55
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dùng Dependency Injection thay vì khởi tạo trực tiếp
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: DI qua protocol giúp dễ dàng mock trong unit test, tách biệt các tầng và tránh coupling cứng giữa các class.
|
|
5
|
+
tags: swift, ios, dependency-injection, testing, architecture, protocol
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Dùng Dependency Injection thay vì khởi tạo trực tiếp
|
|
9
|
+
|
|
10
|
+
Thay vì khởi tạo dependency trực tiếp bên trong class, hãy inject chúng qua `init` sử dụng protocol. Điều này đặc biệt quan trọng trong iOS khi viết unit test cho ViewModel, Interactor, hay Service.
|
|
11
|
+
|
|
12
|
+
**Incorrect (khởi tạo trực tiếp):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class OrderViewModel {
|
|
16
|
+
// Coupling cứng - không thể mock khi test
|
|
17
|
+
private let apiService = OrderAPIService()
|
|
18
|
+
private let database = CoreDataManager()
|
|
19
|
+
private let analytics = FirebaseAnalytics()
|
|
20
|
+
|
|
21
|
+
func loadOrders() {
|
|
22
|
+
apiService.fetchOrders { [weak self] result in
|
|
23
|
+
// xử lý
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct (inject qua protocol):**
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
protocol OrderAPIServiceProtocol {
|
|
33
|
+
func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protocol AnalyticsProtocol {
|
|
37
|
+
func track(event: String)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class OrderViewModel {
|
|
41
|
+
private let apiService: OrderAPIServiceProtocol
|
|
42
|
+
private let analytics: AnalyticsProtocol
|
|
43
|
+
|
|
44
|
+
init(
|
|
45
|
+
apiService: OrderAPIServiceProtocol = OrderAPIService(),
|
|
46
|
+
analytics: AnalyticsProtocol = FirebaseAnalytics()
|
|
47
|
+
) {
|
|
48
|
+
self.apiService = apiService
|
|
49
|
+
self.analytics = analytics
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func loadOrders() {
|
|
53
|
+
apiService.fetchOrders { [weak self] result in
|
|
54
|
+
// xử lý
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Unit test dễ dàng
|
|
60
|
+
class MockOrderAPIService: OrderAPIServiceProtocol {
|
|
61
|
+
func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void) {
|
|
62
|
+
completion(.success([Order(id: "1", total: 100)]))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let vm = OrderViewModel(apiService: MockOrderAPIService(), analytics: MockAnalytics())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Tools:** Code Review, Needle/Swinject (DI Framework)
|
|
69
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không đặt logic nghiệp vụ trong init
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Logic phức tạp trong init làm khó test, gây side-effect không mong muốn và vi phạm nguyên tắc Single Responsibility.
|
|
5
|
+
tags: swift, ios, init, constructor, architecture, code-quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không đặt logic nghiệp vụ trong init
|
|
9
|
+
|
|
10
|
+
`init` chỉ nên gán giá trị cho các property. Đừng gọi API, đọc file, khởi động timer, hay thực hiện tính toán phức tạp bên trong `init`. Nếu cần, tách ra thành hàm `configure()` hoặc `setup()` gọi sau khi khởi tạo.
|
|
11
|
+
|
|
12
|
+
**Incorrect (logic trong init):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class UserProfileViewModel {
|
|
16
|
+
var user: User?
|
|
17
|
+
var recentOrders: [Order] = []
|
|
18
|
+
|
|
19
|
+
init(userId: String) {
|
|
20
|
+
// Gọi API trong init - không thể control được
|
|
21
|
+
let user = UserAPIService.shared.fetchUserSync(userId: userId)
|
|
22
|
+
self.user = user
|
|
23
|
+
|
|
24
|
+
// Logic nghiệp vụ trong init
|
|
25
|
+
if let user = user, user.isPremium {
|
|
26
|
+
recentOrders = OrderAPIService.shared.fetchOrdersSync(userId: userId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Đọc file trong init
|
|
30
|
+
let config = try? JSONDecoder().decode(Config.self, from: Data(contentsOf: configURL))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (init đơn giản + hàm setup riêng):**
|
|
36
|
+
|
|
37
|
+
```swift
|
|
38
|
+
class UserProfileViewModel {
|
|
39
|
+
private let userId: String
|
|
40
|
+
private let apiService: UserAPIServiceProtocol
|
|
41
|
+
|
|
42
|
+
var user: User?
|
|
43
|
+
var recentOrders: [Order] = []
|
|
44
|
+
|
|
45
|
+
init(userId: String, apiService: UserAPIServiceProtocol) {
|
|
46
|
+
self.userId = userId
|
|
47
|
+
self.apiService = apiService
|
|
48
|
+
// Không có logic nghiệp vụ ở đây
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Gọi sau khi khởi tạo - dễ test, dễ control
|
|
52
|
+
func loadData() async {
|
|
53
|
+
do {
|
|
54
|
+
user = try await apiService.fetchUser(userId: userId)
|
|
55
|
+
if user?.isPremium == true {
|
|
56
|
+
recentOrders = try await apiService.fetchOrders(userId: userId)
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// xử lý lỗi
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Tools:** Code Review, SwiftLint custom rule
|
|
66
|
+
|