source_monitor 0.2.1 → 0.3.1
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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/sm-architecture/SKILL.md +233 -0
- data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
- data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
- data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
- data/.claude/skills/sm-configure/SKILL.md +153 -0
- data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
- data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
- data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
- data/.claude/skills/sm-domain-model/SKILL.md +188 -0
- data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
- data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
- data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
- data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
- data/.claude/skills/sm-engine-test/SKILL.md +302 -0
- data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
- data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
- data/.claude/skills/sm-event-handler/SKILL.md +265 -0
- data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
- data/.claude/skills/sm-health-rule/SKILL.md +327 -0
- data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
- data/.claude/skills/sm-host-setup/SKILL.md +223 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
- data/.claude/skills/sm-job/SKILL.md +263 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
- data/.claude/skills/sm-model-extension/SKILL.md +287 -0
- data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
- data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
- data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
- data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
- data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
- data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
- data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
- data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +246 -0
- data/.vbw-planning/.session-log.jsonl +992 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +222 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +132 -120
- data/Rakefile +2 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +19 -2
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +60 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +4 -1
- metadata +177 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8348a9154458260c8b6a3c3dfdeea65e8327b9d7a48dd6c1a2fbe36f9f1bf46
|
|
4
|
+
data.tar.gz: 2d56c477bc1191f4f505645f9f9f23fceaa1f15d1fd065e5032ac2d40ae3c0d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f875810a4a5e53aae8ec0609e0ab86a3a97ba8661826de6dcd8cf3abd3091f3e4838d8c301ed0f962da0c5849123f8a1fa7c2614e5341126753a8556a0d6fce
|
|
7
|
+
data.tar.gz: 50a7007476a9b989af9bfe989bc758a9febbc80478c541995044e1a6fd55fe54736520f643ca00845de57a4d5fc5ccfe1d83dd7994affff7530a3cd692f6fb96
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-concern
|
|
3
|
+
description: Model and controller concerns for horizontal code sharing across classes
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Concern Agent
|
|
8
|
+
|
|
9
|
+
You are an expert at creating well-bounded ActiveSupport::Concern modules for horizontal code sharing in Rails models and controllers.
|
|
10
|
+
|
|
11
|
+
## Project Conventions
|
|
12
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
13
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
14
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
15
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
16
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
17
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
18
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
19
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
20
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
21
|
+
|
|
22
|
+
## When to Use Concerns
|
|
23
|
+
|
|
24
|
+
Concerns are for **horizontal sharing of behavior** across multiple classes that share a common trait.
|
|
25
|
+
|
|
26
|
+
### Good Use Cases
|
|
27
|
+
|
|
28
|
+
| Pattern | Example | Why |
|
|
29
|
+
|---------|---------|-----|
|
|
30
|
+
| Shared validations | `Contactable` (email + phone on User, Company) | Same validation logic, multiple models |
|
|
31
|
+
| Shared scopes | `Searchable` (search scope on multiple models) | Same query pattern, multiple models |
|
|
32
|
+
| Shared callbacks | `Trackable` (track who changed what) | Same auditing, multiple models |
|
|
33
|
+
| State-as-records | `Closeable` (open/closed state pattern) | Same state pattern, multiple models |
|
|
34
|
+
| Shared associations | `HasComments` (polymorphic comments) | Same association setup |
|
|
35
|
+
|
|
36
|
+
### Bad Use Cases (Do NOT Use Concerns For)
|
|
37
|
+
|
|
38
|
+
| Anti-pattern | Problem | Better Approach |
|
|
39
|
+
|-------------|---------|-----------------|
|
|
40
|
+
| Kitchen-sink concern | Unrelated methods lumped together | Split into focused concerns |
|
|
41
|
+
| Single-model concern | Only one model uses it | Keep in the model |
|
|
42
|
+
| Cross-cutting orchestration | Coordinates multiple unrelated models | Service object |
|
|
43
|
+
| Concern depends on concern | Tight coupling between concerns | Merge or restructure |
|
|
44
|
+
| "Utils" concern | Grab-bag of helper methods | Module or standalone class |
|
|
45
|
+
|
|
46
|
+
## Model Concern Patterns
|
|
47
|
+
|
|
48
|
+
### Pattern: Closeable (State-as-Record)
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/models/concerns/closeable.rb
|
|
52
|
+
module Closeable
|
|
53
|
+
extend ActiveSupport::Concern
|
|
54
|
+
|
|
55
|
+
included do
|
|
56
|
+
has_one :closure, as: :closeable, dependent: :destroy
|
|
57
|
+
|
|
58
|
+
scope :open, -> { where.missing(:closure) }
|
|
59
|
+
scope :closed, -> { joins(:closure) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def closed?
|
|
63
|
+
closure.present?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def open?
|
|
67
|
+
!closed?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def close!(closed_by:, reason: nil)
|
|
71
|
+
create_closure!(closed_by: closed_by, reason: reason)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reopen!
|
|
75
|
+
closure&.destroy!
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Pattern: Searchable
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# app/models/concerns/searchable.rb
|
|
84
|
+
module Searchable
|
|
85
|
+
extend ActiveSupport::Concern
|
|
86
|
+
|
|
87
|
+
included do
|
|
88
|
+
scope :search, ->(query) {
|
|
89
|
+
return all if query.blank?
|
|
90
|
+
columns = searchable_columns.map { |col| arel_table[col] }
|
|
91
|
+
conditions = columns.map { |col| col.matches("%#{sanitize_sql_like(query)}%") }
|
|
92
|
+
where(conditions.reduce(:or))
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class_methods do
|
|
97
|
+
def searchable_columns
|
|
98
|
+
raise NotImplementedError, "#{name} must define .searchable_columns"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Usage:
|
|
104
|
+
class Project < ApplicationRecord
|
|
105
|
+
include Searchable
|
|
106
|
+
|
|
107
|
+
def self.searchable_columns
|
|
108
|
+
%i[name description]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class User < ApplicationRecord
|
|
113
|
+
include Searchable
|
|
114
|
+
|
|
115
|
+
def self.searchable_columns
|
|
116
|
+
%i[name email]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Pattern: Trackable (Audit Trail)
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# app/models/concerns/trackable.rb
|
|
125
|
+
module Trackable
|
|
126
|
+
extend ActiveSupport::Concern
|
|
127
|
+
|
|
128
|
+
included do
|
|
129
|
+
belongs_to :created_by, class_name: "User", optional: true
|
|
130
|
+
belongs_to :updated_by, class_name: "User", optional: true
|
|
131
|
+
|
|
132
|
+
before_create :set_created_by
|
|
133
|
+
before_update :set_updated_by
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def set_created_by
|
|
139
|
+
self.created_by ||= Current.user
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def set_updated_by
|
|
143
|
+
self.updated_by = Current.user if Current.user
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Pattern: HasUuid
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# app/models/concerns/has_uuid.rb
|
|
152
|
+
module HasUuid
|
|
153
|
+
extend ActiveSupport::Concern
|
|
154
|
+
|
|
155
|
+
included do
|
|
156
|
+
before_create :generate_uuid
|
|
157
|
+
|
|
158
|
+
validates :uuid, uniqueness: true, allow_nil: true
|
|
159
|
+
|
|
160
|
+
scope :find_by_uuid!, ->(uuid) { find_by!(uuid: uuid) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def generate_uuid
|
|
166
|
+
self.uuid ||= SecureRandom.uuid
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Pattern: Contactable
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# app/models/concerns/contactable.rb
|
|
175
|
+
module Contactable
|
|
176
|
+
extend ActiveSupport::Concern
|
|
177
|
+
|
|
178
|
+
included do
|
|
179
|
+
validates :email, presence: true,
|
|
180
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
181
|
+
validates :phone, format: { with: /\A\+?[\d\s\-()]+\z/ },
|
|
182
|
+
allow_blank: true
|
|
183
|
+
|
|
184
|
+
before_validation :normalize_email
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def has_phone?
|
|
188
|
+
phone.present?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def normalize_email
|
|
194
|
+
self.email = email&.downcase&.strip
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Controller Concern Patterns
|
|
200
|
+
|
|
201
|
+
### Pattern: SetCurrentAccount
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# app/controllers/concerns/set_current_account.rb
|
|
205
|
+
module SetCurrentAccount
|
|
206
|
+
extend ActiveSupport::Concern
|
|
207
|
+
|
|
208
|
+
included do
|
|
209
|
+
before_action :set_current_account
|
|
210
|
+
helper_method :current_account
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def current_account
|
|
216
|
+
Current.account
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def set_current_account
|
|
220
|
+
Current.account = current_user&.account
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Pattern: Authentication
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# app/controllers/concerns/authentication.rb
|
|
229
|
+
module Authentication
|
|
230
|
+
extend ActiveSupport::Concern
|
|
231
|
+
|
|
232
|
+
included do
|
|
233
|
+
before_action :require_authentication
|
|
234
|
+
helper_method :current_user, :signed_in?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def current_user
|
|
240
|
+
Current.user
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def signed_in?
|
|
244
|
+
current_user.present?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def require_authentication
|
|
248
|
+
resume_session || request_authentication
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def resume_session
|
|
252
|
+
Current.session = find_session_by_cookie
|
|
253
|
+
Current.user = Current.session&.user
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def find_session_by_cookie
|
|
257
|
+
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def request_authentication
|
|
261
|
+
redirect_to new_session_path, alert: "Please sign in"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Pattern: Paginatable
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# app/controllers/concerns/paginatable.rb
|
|
270
|
+
module Paginatable
|
|
271
|
+
extend ActiveSupport::Concern
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
def page
|
|
276
|
+
[params[:page].to_i, 1].max
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def per_page
|
|
280
|
+
[(params[:per_page] || 25).to_i, 100].min
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def paginate(scope)
|
|
284
|
+
scope.offset((page - 1) * per_page).limit(per_page)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Concern Design Rules
|
|
290
|
+
|
|
291
|
+
### 1. Single Responsibility
|
|
292
|
+
|
|
293
|
+
Each concern should represent one clear behavior or trait.
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# GOOD: One behavior
|
|
297
|
+
module Closeable # Manages open/closed state
|
|
298
|
+
module Searchable # Adds search capability
|
|
299
|
+
module Contactable # Validates contact info
|
|
300
|
+
|
|
301
|
+
# BAD: Multiple unrelated behaviors
|
|
302
|
+
module ModelHelpers # Kitchen sink of unrelated methods
|
|
303
|
+
module Utilities # Grab-bag
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 2. Self-Contained
|
|
307
|
+
|
|
308
|
+
A concern should work independently. Never depend on other concerns being included.
|
|
309
|
+
|
|
310
|
+
### 3. Explicit Contract
|
|
311
|
+
|
|
312
|
+
If a concern requires the including class to implement something, use `raise NotImplementedError` in a class method.
|
|
313
|
+
|
|
314
|
+
### 4. Polymorphic Associations for State Records
|
|
315
|
+
|
|
316
|
+
State concerns should use polymorphic `as:` so one closure/publication table serves many models.
|
|
317
|
+
|
|
318
|
+
## Concern Boundaries vs Service Objects
|
|
319
|
+
|
|
320
|
+
| Concern | Service Object |
|
|
321
|
+
|---------|---------------|
|
|
322
|
+
| Adds behavior to a single model | Coordinates multiple models |
|
|
323
|
+
| Shared trait (closeable, searchable) | Business process (onboarding, billing) |
|
|
324
|
+
| No external dependencies | May call APIs, send emails |
|
|
325
|
+
| Stateless (operates on `self`) | Stateful (takes arguments, returns result) |
|
|
326
|
+
|
|
327
|
+
### Decision Example
|
|
328
|
+
|
|
329
|
+
"Users and Companies both need to be archivable"
|
|
330
|
+
- **Use a concern**: `Archivable` adds `archive!`, `archived?`, scopes
|
|
331
|
+
- The behavior is a shared trait of the models
|
|
332
|
+
|
|
333
|
+
"When archiving a user, also archive their projects and notify the team"
|
|
334
|
+
- **Use a service**: `Users::ArchiveService` orchestrates the process
|
|
335
|
+
- Multiple models are involved in a business process
|
|
336
|
+
|
|
337
|
+
## Testing Concerns with Minitest
|
|
338
|
+
|
|
339
|
+
### Testing via the Including Model
|
|
340
|
+
|
|
341
|
+
The simplest and most practical approach:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
# test/models/project_test.rb
|
|
345
|
+
require "test_helper"
|
|
346
|
+
|
|
347
|
+
class ProjectTest < ActiveSupport::TestCase
|
|
348
|
+
# Test Closeable concern through Project
|
|
349
|
+
test "can be closed" do
|
|
350
|
+
project = projects(:website_redesign)
|
|
351
|
+
project.close!(closed_by: users(:alice), reason: "Completed")
|
|
352
|
+
assert project.closed?
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
test "can be reopened" do
|
|
356
|
+
project = projects(:website_redesign)
|
|
357
|
+
project.close!(closed_by: users(:alice))
|
|
358
|
+
project.reopen!
|
|
359
|
+
assert project.open?
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
test ".open scope excludes closed" do
|
|
363
|
+
project = projects(:website_redesign)
|
|
364
|
+
project.close!(closed_by: users(:alice))
|
|
365
|
+
assert_not_includes Project.open, project
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Test Searchable concern through Project
|
|
369
|
+
test ".search finds by name" do
|
|
370
|
+
results = Project.search("Redesign")
|
|
371
|
+
assert_includes results, projects(:website_redesign)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
test ".search returns all when blank" do
|
|
375
|
+
assert_equal Project.count, Project.search("").count
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Testing Concerns in Isolation
|
|
381
|
+
|
|
382
|
+
For concerns shared across many models, test once with a fake model:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
# test/models/concerns/closeable_test.rb
|
|
386
|
+
require "test_helper"
|
|
387
|
+
|
|
388
|
+
class CloseableTest < ActiveSupport::TestCase
|
|
389
|
+
# Test through a real model that includes the concern
|
|
390
|
+
setup do
|
|
391
|
+
@project = projects(:website_redesign)
|
|
392
|
+
@user = users(:alice)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
test "#close! creates a closure record" do
|
|
396
|
+
assert_difference -> { Closure.count }, 1 do
|
|
397
|
+
@project.close!(closed_by: @user, reason: "Done")
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
test "#closed? returns true after closing" do
|
|
402
|
+
@project.close!(closed_by: @user)
|
|
403
|
+
assert @project.closed?
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
test "#open? is inverse of closed?" do
|
|
407
|
+
assert @project.open?
|
|
408
|
+
@project.close!(closed_by: @user)
|
|
409
|
+
assert_not @project.open?
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
test "#reopen! destroys closure record" do
|
|
413
|
+
@project.close!(closed_by: @user)
|
|
414
|
+
@project.reopen!
|
|
415
|
+
assert @project.open?
|
|
416
|
+
assert_nil @project.reload.closure
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
test ".open scope returns unclosed records" do
|
|
420
|
+
open_project = projects(:website_redesign)
|
|
421
|
+
closed_project = projects(:archived_project)
|
|
422
|
+
# archived_project has a closure fixture
|
|
423
|
+
|
|
424
|
+
results = Project.open
|
|
425
|
+
assert_includes results, open_project
|
|
426
|
+
assert_not_includes results, closed_project
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
test ".closed scope returns closed records" do
|
|
430
|
+
@project.close!(closed_by: @user)
|
|
431
|
+
assert_includes Project.closed, @project
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## File Organization
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
app/
|
|
440
|
+
models/
|
|
441
|
+
concerns/
|
|
442
|
+
closeable.rb # State: open/closed
|
|
443
|
+
publishable.rb # State: draft/published
|
|
444
|
+
searchable.rb # Search capability
|
|
445
|
+
trackable.rb # Audit trail (created_by, updated_by)
|
|
446
|
+
has_uuid.rb # UUID generation
|
|
447
|
+
contactable.rb # Email/phone validation
|
|
448
|
+
sortable.rb # Position ordering
|
|
449
|
+
controllers/
|
|
450
|
+
concerns/
|
|
451
|
+
authentication.rb # Session management
|
|
452
|
+
set_current_account.rb # Account scoping
|
|
453
|
+
paginatable.rb # Pagination helpers
|
|
454
|
+
error_handling.rb # Rescue handlers
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Anti-Patterns to Avoid
|
|
458
|
+
|
|
459
|
+
1. **Kitchen-sink concerns** - One concern doing too many unrelated things. Split into focused concerns.
|
|
460
|
+
2. **Concern dependencies** - Concern A requiring Concern B to be included. Each concern should be self-contained.
|
|
461
|
+
3. **Single-use concerns** - If only one model uses it, keep it in the model.
|
|
462
|
+
4. **Logic concerns** - If the concern orchestrates multiple models, it should be a service object.
|
|
463
|
+
5. **Overriding concern methods** - If you need to override a concern method in the including class, the concern boundary is wrong.
|
|
464
|
+
6. **Deeply nested concerns** - Concern including another concern. Keep the hierarchy flat.
|