source_monitor 0.3.0 → 0.3.2
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/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/.vbw-planning/.notification-log.jsonl +102 -0
- data/.vbw-planning/.session-log.jsonl +505 -0
- data/AGENTS.md +20 -57
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +44 -1
- data/CONTRIBUTING.md +5 -5
- data/Gemfile.lock +20 -21
- data/README.md +18 -5
- data/VERSION +1 -0
- data/docs/deployment.md +1 -1
- data/docs/setup.md +4 -4
- data/lib/source_monitor/setup/skills_installer.rb +94 -0
- data/lib/source_monitor/setup/workflow.rb +17 -2
- data/lib/source_monitor/version.rb +1 -1
- data/lib/tasks/source_monitor_setup.rake +58 -0
- data/source_monitor.gemspec +1 -0
- metadata +39 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sm-model-extension
|
|
3
|
+
description: Use when extending SourceMonitor engine models from a host app, including adding concerns, validations, scopes, associations, and customizing table name prefixes via ModelExtensions.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# sm-model-extension: Extend Engine Models from Host App
|
|
8
|
+
|
|
9
|
+
Add custom behavior to SourceMonitor engine models without monkey-patching.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- Adding associations, scopes, or methods to `Source`, `Item`, or other engine models
|
|
14
|
+
- Adding custom validations to engine models
|
|
15
|
+
- Changing the database table name prefix
|
|
16
|
+
- Understanding how `ModelExtensions.register` works
|
|
17
|
+
- Debugging model extension issues
|
|
18
|
+
|
|
19
|
+
## Extension Mechanism
|
|
20
|
+
|
|
21
|
+
SourceMonitor uses `ModelExtensions.register` to apply host-defined concerns and validations to engine models at load time. When `SourceMonitor.configure` runs, it calls `ModelExtensions.reload!` to re-apply all extensions.
|
|
22
|
+
|
|
23
|
+
### Flow
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
1. Host app defines concern modules and validations
|
|
27
|
+
2. config/initializers/source_monitor.rb registers them:
|
|
28
|
+
config.models.source.include_concern "MyApp::SourceExtension"
|
|
29
|
+
config.models.source.validate :custom_check
|
|
30
|
+
3. SourceMonitor.configure { |c| ... } runs
|
|
31
|
+
4. ModelExtensions.reload! applies all concerns and validations
|
|
32
|
+
5. Engine models now have the extended behavior
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Available Extension Points
|
|
36
|
+
|
|
37
|
+
### Extendable Models
|
|
38
|
+
|
|
39
|
+
| Config Accessor | Engine Model | DB Table |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `config.models.source` | `SourceMonitor::Source` | `sourcemon_sources` |
|
|
42
|
+
| `config.models.item` | `SourceMonitor::Item` | `sourcemon_items` |
|
|
43
|
+
| `config.models.fetch_log` | `SourceMonitor::FetchLog` | `sourcemon_fetch_logs` |
|
|
44
|
+
| `config.models.scrape_log` | `SourceMonitor::ScrapeLog` | `sourcemon_scrape_logs` |
|
|
45
|
+
| `config.models.health_check_log` | `SourceMonitor::HealthCheckLog` | `sourcemon_health_check_logs` |
|
|
46
|
+
| `config.models.item_content` | `SourceMonitor::ItemContent` | `sourcemon_item_contents` |
|
|
47
|
+
| `config.models.log_entry` | `SourceMonitor::LogEntry` | `sourcemon_log_entries` |
|
|
48
|
+
|
|
49
|
+
### Table Name Prefix
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
config.models.table_name_prefix = "sm_" # Changes all tables from sourcemon_* to sm_*
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Default: `"sourcemon_"`
|
|
56
|
+
|
|
57
|
+
### Including Concerns
|
|
58
|
+
|
|
59
|
+
Three forms supported:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# 1. String (lazy constantization -- recommended for autoloaded modules)
|
|
63
|
+
config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
|
|
64
|
+
|
|
65
|
+
# 2. Module reference (immediate)
|
|
66
|
+
config.models.source.include_concern MyApp::SourceMonitor::SourceExtensions
|
|
67
|
+
|
|
68
|
+
# 3. Anonymous block (creates Module.new)
|
|
69
|
+
config.models.source.include_concern do
|
|
70
|
+
has_many :tags, dependent: :destroy, foreign_key: :source_id
|
|
71
|
+
scope :tagged, ->(tag) { joins(:tags).where(tags: { name: tag }) }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Concerns are deduplicated by signature -- including the same concern twice is safe.
|
|
76
|
+
|
|
77
|
+
### Adding Validations
|
|
78
|
+
|
|
79
|
+
Two forms:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# 1. Symbol -- method name (must be defined in a concern or the model)
|
|
83
|
+
config.models.source.validate :enforce_custom_rules
|
|
84
|
+
|
|
85
|
+
# 2. Callable (proc/lambda) -- receives the record
|
|
86
|
+
config.models.source.validate ->(record) {
|
|
87
|
+
record.errors.add(:url, "must be HTTPS") unless record.url&.start_with?("https://")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# With validation options
|
|
91
|
+
config.models.source.validate :check_plan_limits, on: :create
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Creating a Host Extension
|
|
95
|
+
|
|
96
|
+
### Step 1: Define the Concern
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# app/models/concerns/my_app/source_monitor/source_extensions.rb
|
|
100
|
+
module MyApp
|
|
101
|
+
module SourceMonitor
|
|
102
|
+
module SourceExtensions
|
|
103
|
+
extend ActiveSupport::Concern
|
|
104
|
+
|
|
105
|
+
included do
|
|
106
|
+
# Associations
|
|
107
|
+
has_many :source_tags, class_name: "MyApp::SourceTag",
|
|
108
|
+
foreign_key: :source_monitor_source_id, dependent: :destroy
|
|
109
|
+
|
|
110
|
+
# Scopes
|
|
111
|
+
scope :by_team, ->(team_id) { where(team_id: team_id) }
|
|
112
|
+
scope :premium, -> { where(premium: true) }
|
|
113
|
+
|
|
114
|
+
# Callbacks
|
|
115
|
+
after_create :notify_team
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Instance methods
|
|
119
|
+
def team_name
|
|
120
|
+
team&.name || "Unassigned"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def notify_team
|
|
126
|
+
TeamNotifier.source_added(self) if team_id.present?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Step 2: Register in Configuration
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# config/initializers/source_monitor.rb
|
|
137
|
+
SourceMonitor.configure do |config|
|
|
138
|
+
config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
|
|
139
|
+
config.models.source.validate :validate_team_assignment
|
|
140
|
+
|
|
141
|
+
config.models.item.include_concern "MyApp::SourceMonitor::ItemExtensions"
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Step 3: Add Database Columns (if needed)
|
|
146
|
+
|
|
147
|
+
If your extension requires new columns on engine tables, create a migration in the host app:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# db/migrate/YYYYMMDDHHMMSS_add_team_to_sourcemon_sources.rb
|
|
151
|
+
class AddTeamToSourcemonSources < ActiveRecord::Migration[8.0]
|
|
152
|
+
def change
|
|
153
|
+
add_column :sourcemon_sources, :team_id, :bigint
|
|
154
|
+
add_index :sourcemon_sources, :team_id
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## How ModelExtensions Works Internally
|
|
160
|
+
|
|
161
|
+
### Registration (`ModelExtensions.register`)
|
|
162
|
+
|
|
163
|
+
Called by each engine model during class loading:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# In SourceMonitor::Source (simplified)
|
|
167
|
+
SourceMonitor::ModelExtensions.register(self, :source)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This:
|
|
171
|
+
1. Looks up the `ModelDefinition` for the given key
|
|
172
|
+
2. Sets `table_name` based on `table_name_prefix + base_table`
|
|
173
|
+
3. Includes all registered concerns (deduped by signature)
|
|
174
|
+
4. Applies all registered validations
|
|
175
|
+
|
|
176
|
+
### Reload (`ModelExtensions.reload!`)
|
|
177
|
+
|
|
178
|
+
Called by `SourceMonitor.configure` after the block runs. Re-applies all extensions to all registered models. Safe to call multiple times.
|
|
179
|
+
|
|
180
|
+
### Concern Deduplication
|
|
181
|
+
|
|
182
|
+
Concerns are tracked by signature:
|
|
183
|
+
- Named module: `[:module, object_id]`
|
|
184
|
+
- String constant: `[:constant, "MyApp::SourceExtensions"]`
|
|
185
|
+
- Anonymous block: `[:anonymous_module, block.object_id]`
|
|
186
|
+
|
|
187
|
+
### Validation Management
|
|
188
|
+
|
|
189
|
+
Extension validations are tracked separately from model-native validations. On reload:
|
|
190
|
+
1. Previous extension validations are removed
|
|
191
|
+
2. New extension validations are applied
|
|
192
|
+
3. Model-native validations are untouched
|
|
193
|
+
|
|
194
|
+
## Limitations and Gotchas
|
|
195
|
+
|
|
196
|
+
1. **Table name prefix is global** -- changing it affects all engine tables. Must match existing migration table names or you need to rename tables.
|
|
197
|
+
|
|
198
|
+
2. **Concern order matters** -- concerns are included in registration order. If concern B depends on an association from concern A, register A first.
|
|
199
|
+
|
|
200
|
+
3. **Anonymous blocks create new modules** -- each `configure` call with a block creates a new anonymous module. In development with code reloading, this is fine because `reload!` re-applies everything.
|
|
201
|
+
|
|
202
|
+
4. **Validations with symbols** require the method to exist on the model. Define it in a concern and register the concern before the validation.
|
|
203
|
+
|
|
204
|
+
5. **Foreign keys** -- when adding associations to engine models, use explicit `foreign_key` and `class_name` options to avoid namespace confusion.
|
|
205
|
+
|
|
206
|
+
6. **Engine table names** -- always reference tables by their prefixed name (e.g., `sourcemon_sources`), not the model name.
|
|
207
|
+
|
|
208
|
+
## Key Source Files
|
|
209
|
+
|
|
210
|
+
| File | Purpose |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `lib/source_monitor/model_extensions.rb` | Registration, reload, apply logic |
|
|
213
|
+
| `lib/source_monitor/configuration/models.rb` | Models config with MODEL_KEYS |
|
|
214
|
+
| `lib/source_monitor/configuration/model_definition.rb` | Per-model concern + validation storage |
|
|
215
|
+
| `lib/source_monitor/configuration/validation_definition.rb` | Validation wrapper |
|
|
216
|
+
| `lib/source_monitor.rb` | `configure` and `reset_configuration!` |
|
|
217
|
+
|
|
218
|
+
## References
|
|
219
|
+
|
|
220
|
+
- `reference/extension-api.md` -- Detailed API reference
|
|
221
|
+
- `docs/configuration.md` -- Configuration documentation (Model Extensions section)
|
|
222
|
+
|
|
223
|
+
## Testing
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
require "test_helper"
|
|
227
|
+
|
|
228
|
+
module TestExtensions
|
|
229
|
+
extend ActiveSupport::Concern
|
|
230
|
+
|
|
231
|
+
included do
|
|
232
|
+
scope :test_scope, -> { where.not(url: nil) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_method
|
|
236
|
+
"extended"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class ModelExtensionTest < ActiveSupport::TestCase
|
|
241
|
+
setup do
|
|
242
|
+
SourceMonitor.reset_configuration!
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
test "include_concern adds methods to source" do
|
|
246
|
+
SourceMonitor.configure do |config|
|
|
247
|
+
config.models.source.include_concern TestExtensions
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
source = create_source!
|
|
251
|
+
assert_equal "extended", source.test_method
|
|
252
|
+
assert_respond_to SourceMonitor::Source, :test_scope
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test "validate adds custom validation" do
|
|
256
|
+
SourceMonitor.configure do |config|
|
|
257
|
+
config.models.source.validate ->(record) {
|
|
258
|
+
record.errors.add(:base, "test error")
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
source = SourceMonitor::Source.new
|
|
263
|
+
source.valid?
|
|
264
|
+
assert_includes source.errors[:base], "test error"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
test "table_name_prefix changes table names" do
|
|
268
|
+
SourceMonitor.configure do |config|
|
|
269
|
+
config.models.table_name_prefix = "custom_"
|
|
270
|
+
end
|
|
271
|
+
SourceMonitor::ModelExtensions.reload!
|
|
272
|
+
|
|
273
|
+
assert_equal "custom_sources", SourceMonitor::Source.table_name
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Checklist
|
|
279
|
+
|
|
280
|
+
- [ ] Extension concern defined under `app/models/concerns/`
|
|
281
|
+
- [ ] Concern registered via `config.models.<model>.include_concern`
|
|
282
|
+
- [ ] Custom validations registered via `config.models.<model>.validate`
|
|
283
|
+
- [ ] Foreign keys use explicit `foreign_key:` option
|
|
284
|
+
- [ ] Host migration created for any new columns on engine tables
|
|
285
|
+
- [ ] Tests verify extension behavior
|
|
286
|
+
- [ ] Tests use `SourceMonitor.reset_configuration!` in setup
|
|
287
|
+
- [ ] Extension is idempotent (safe to apply multiple times)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Model Extension API Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for extending SourceMonitor engine models from host applications.
|
|
4
|
+
|
|
5
|
+
Source: `lib/source_monitor/model_extensions.rb`, `lib/source_monitor/configuration/models.rb`, `lib/source_monitor/configuration/model_definition.rb`
|
|
6
|
+
|
|
7
|
+
## Configuration API
|
|
8
|
+
|
|
9
|
+
### `config.models`
|
|
10
|
+
|
|
11
|
+
Class: `SourceMonitor::Configuration::Models`
|
|
12
|
+
|
|
13
|
+
#### `table_name_prefix`
|
|
14
|
+
|
|
15
|
+
| Property | Type | Default |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `table_name_prefix` | String | `"sourcemon_"` |
|
|
18
|
+
|
|
19
|
+
Changes the database table name prefix for all engine models.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
config.models.table_name_prefix = "sm_"
|
|
23
|
+
# sourcemon_sources -> sm_sources
|
|
24
|
+
# sourcemon_items -> sm_items
|
|
25
|
+
# etc.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Model Accessors
|
|
29
|
+
|
|
30
|
+
Each returns a `ModelDefinition` instance:
|
|
31
|
+
|
|
32
|
+
| Accessor | Key | Engine Model |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `config.models.source` | `:source` | `SourceMonitor::Source` |
|
|
35
|
+
| `config.models.item` | `:item` | `SourceMonitor::Item` |
|
|
36
|
+
| `config.models.fetch_log` | `:fetch_log` | `SourceMonitor::FetchLog` |
|
|
37
|
+
| `config.models.scrape_log` | `:scrape_log` | `SourceMonitor::ScrapeLog` |
|
|
38
|
+
| `config.models.health_check_log` | `:health_check_log` | `SourceMonitor::HealthCheckLog` |
|
|
39
|
+
| `config.models.item_content` | `:item_content` | `SourceMonitor::ItemContent` |
|
|
40
|
+
| `config.models.log_entry` | `:log_entry` | `SourceMonitor::LogEntry` |
|
|
41
|
+
|
|
42
|
+
#### `for(name) -> ModelDefinition`
|
|
43
|
+
|
|
44
|
+
Look up a model definition by key. Raises `ArgumentError` for unknown models.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ModelDefinition API
|
|
49
|
+
|
|
50
|
+
Class: `SourceMonitor::Configuration::ModelDefinition`
|
|
51
|
+
|
|
52
|
+
### `include_concern(concern = nil, &block)`
|
|
53
|
+
|
|
54
|
+
Include a concern module into the engine model.
|
|
55
|
+
|
|
56
|
+
**Three forms:**
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# 1. String constant (lazy -- recommended for autoloaded classes)
|
|
60
|
+
config.models.source.include_concern "MyApp::SourceExtension"
|
|
61
|
+
|
|
62
|
+
# 2. Module reference (immediate)
|
|
63
|
+
config.models.source.include_concern MyApp::SourceExtension
|
|
64
|
+
|
|
65
|
+
# 3. Anonymous block
|
|
66
|
+
config.models.source.include_concern do
|
|
67
|
+
has_many :tags, dependent: :destroy
|
|
68
|
+
scope :tagged, ->(t) { joins(:tags).where(tags: { name: t }) }
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Deduplication:** Concerns are deduplicated by signature:
|
|
73
|
+
|
|
74
|
+
| Form | Signature |
|
|
75
|
+
|---|---|
|
|
76
|
+
| String | `[:constant, "MyApp::SourceExtension"]` |
|
|
77
|
+
| Module | `[:module, <object_id>]` |
|
|
78
|
+
| Block | `[:anonymous_module, <block_object_id>]` |
|
|
79
|
+
|
|
80
|
+
Including the same concern twice (by signature) is a no-op.
|
|
81
|
+
|
|
82
|
+
**Return value:**
|
|
83
|
+
- String: returns the string
|
|
84
|
+
- Module: returns the module
|
|
85
|
+
- Block: returns the anonymous module created from the block
|
|
86
|
+
|
|
87
|
+
### `validate(handler = nil, **options, &block)`
|
|
88
|
+
|
|
89
|
+
Register a validation on the engine model.
|
|
90
|
+
|
|
91
|
+
**Forms:**
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# 1. Symbol -- method name (must exist on the model, typically from a concern)
|
|
95
|
+
config.models.source.validate :check_custom_rules
|
|
96
|
+
|
|
97
|
+
# 2. Callable (proc/lambda)
|
|
98
|
+
config.models.source.validate ->(record) {
|
|
99
|
+
record.errors.add(:url, "must use HTTPS") unless record.url&.start_with?("https://")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# 3. Block
|
|
103
|
+
config.models.source.validate do |record|
|
|
104
|
+
record.errors.add(:base, "invalid") unless record.valid_for_host?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# With options (passed to ActiveModel::Validations.validate)
|
|
108
|
+
config.models.source.validate :check_limits, on: :create
|
|
109
|
+
config.models.source.validate :check_format, if: :needs_format_check?
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Returns:** A `ValidationDefinition` instance.
|
|
113
|
+
|
|
114
|
+
### `validations -> Array<ValidationDefinition>`
|
|
115
|
+
|
|
116
|
+
Read-only list of registered validations.
|
|
117
|
+
|
|
118
|
+
### `each_concern { |signature, module| }`
|
|
119
|
+
|
|
120
|
+
Iterate over registered concerns. Yields signature and resolved module.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## ValidationDefinition
|
|
125
|
+
|
|
126
|
+
Class: `SourceMonitor::Configuration::ValidationDefinition`
|
|
127
|
+
|
|
128
|
+
| Method | Returns | Description |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `handler` | Symbol/Proc | The validation handler |
|
|
131
|
+
| `options` | Hash | Options passed to `validate` (e.g., `on:`, `if:`) |
|
|
132
|
+
| `signature` | Array | Unique identifier for deduplication |
|
|
133
|
+
| `symbol?` | Boolean | True if handler is a Symbol or String |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## ModelExtensions Module
|
|
138
|
+
|
|
139
|
+
Class: `SourceMonitor::ModelExtensions`
|
|
140
|
+
|
|
141
|
+
### `register(model_class, key)`
|
|
142
|
+
|
|
143
|
+
Called by each engine model during class loading to register itself:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Inside SourceMonitor::Source (simplified)
|
|
147
|
+
SourceMonitor::ModelExtensions.register(self, :source)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Actions:
|
|
151
|
+
1. Adds the model to the internal registry
|
|
152
|
+
2. Sets `table_name` using `table_name_prefix + base_table`
|
|
153
|
+
3. Includes all registered concerns (via `apply_concerns`)
|
|
154
|
+
4. Applies all registered validations (via `apply_validations`)
|
|
155
|
+
|
|
156
|
+
### `reload!`
|
|
157
|
+
|
|
158
|
+
Re-applies all extensions to all registered models. Called by:
|
|
159
|
+
- `SourceMonitor.configure` (after the block runs)
|
|
160
|
+
- `SourceMonitor.reset_configuration!`
|
|
161
|
+
|
|
162
|
+
Safe to call multiple times. Idempotent for concerns (deduplicated). Validations are removed and re-applied on each reload.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Internal Mechanics
|
|
167
|
+
|
|
168
|
+
### Table Name Assignment
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
def assign_table_name(entry)
|
|
172
|
+
desired = "#{SourceMonitor.table_name_prefix}#{entry.base_table}"
|
|
173
|
+
model_class.table_name = desired
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`base_table` is derived from the model class name: `Source` -> `sources`, `FetchLog` -> `fetch_logs`.
|
|
178
|
+
|
|
179
|
+
### Concern Application
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
def apply_concerns(model_class, definition)
|
|
183
|
+
applied = model_class.instance_variable_get(:@_source_monitor_extension_concerns) || []
|
|
184
|
+
|
|
185
|
+
definition.each_concern do |signature, mod|
|
|
186
|
+
next if applied.include?(signature)
|
|
187
|
+
model_class.include(mod) unless model_class < mod
|
|
188
|
+
applied << signature
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
model_class.instance_variable_set(:@_source_monitor_extension_concerns, applied)
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Concerns are tracked via an instance variable on the model class. Including the same module twice is prevented both by signature tracking and the `model_class < mod` check.
|
|
196
|
+
|
|
197
|
+
### Validation Application
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
def apply_validations(model_class, definition)
|
|
201
|
+
remove_extension_validations(model_class) # Remove previous extensions
|
|
202
|
+
|
|
203
|
+
definition.validations.each do |validation|
|
|
204
|
+
if validation.symbol?
|
|
205
|
+
model_class.validate(validation.handler, **validation.options)
|
|
206
|
+
else
|
|
207
|
+
callback = proc { |record| validation.handler.call(record) }
|
|
208
|
+
model_class.validate(**validation.options, &callback)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Extension validations are tracked separately. On reload:
|
|
215
|
+
1. Previous extension validations are removed from `_validate_callbacks`
|
|
216
|
+
2. New validations are applied fresh
|
|
217
|
+
3. Model-native validations are never touched
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Concern Definition Internals
|
|
222
|
+
|
|
223
|
+
Class: `SourceMonitor::Configuration::ModelDefinition::ConcernDefinition` (private)
|
|
224
|
+
|
|
225
|
+
### Resolver
|
|
226
|
+
|
|
227
|
+
| Form | Behavior |
|
|
228
|
+
|---|---|
|
|
229
|
+
| Block | Creates `Module.new(&block)`, wraps in lazy resolver |
|
|
230
|
+
| Module | Returns the module directly |
|
|
231
|
+
| String | Calls `constantize` lazily (raises `ArgumentError` on `NameError`) |
|
|
232
|
+
|
|
233
|
+
### Lazy Resolution
|
|
234
|
+
|
|
235
|
+
String-based concerns are not constantized until `resolve` is called. This allows registering concerns for classes that haven't been loaded yet (common with autoloading).
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Patterns and Examples
|
|
240
|
+
|
|
241
|
+
### Adding Associations
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# app/models/concerns/my_app/source_monitor/source_extensions.rb
|
|
245
|
+
module MyApp::SourceMonitor::SourceExtensions
|
|
246
|
+
extend ActiveSupport::Concern
|
|
247
|
+
|
|
248
|
+
included do
|
|
249
|
+
has_many :source_tags,
|
|
250
|
+
class_name: "MyApp::SourceTag",
|
|
251
|
+
foreign_key: :sourcemon_source_id,
|
|
252
|
+
dependent: :destroy
|
|
253
|
+
|
|
254
|
+
has_many :tags, through: :source_tags, class_name: "MyApp::Tag"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Adding Scopes
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
config.models.source.include_concern do
|
|
263
|
+
scope :active_in_team, ->(team_id) {
|
|
264
|
+
where(team_id: team_id).where.not(paused_at: nil)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
scope :with_recent_items, -> {
|
|
268
|
+
where(id: SourceMonitor::Item.where("created_at > ?", 24.hours.ago).select(:source_id))
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Adding Callbacks
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
config.models.item.include_concern do
|
|
277
|
+
after_create :update_source_item_count
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
def update_source_item_count
|
|
282
|
+
source.update_column(:items_count, source.items.count)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Multi-Model Extensions
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
SourceMonitor.configure do |config|
|
|
291
|
+
# Extend sources
|
|
292
|
+
config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
|
|
293
|
+
config.models.source.validate :validate_team_assignment
|
|
294
|
+
|
|
295
|
+
# Extend items
|
|
296
|
+
config.models.item.include_concern "MyApp::SourceMonitor::ItemExtensions"
|
|
297
|
+
config.models.item.validate ->(record) {
|
|
298
|
+
record.errors.add(:title, "too short") if record.title&.length.to_i < 5
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Extend fetch logs
|
|
302
|
+
config.models.fetch_log.include_concern do
|
|
303
|
+
scope :for_team, ->(team_id) {
|
|
304
|
+
joins(:source).where(sourcemon_sources: { team_id: team_id })
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Custom Table Prefix
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
config.models.table_name_prefix = "feeds_"
|
|
314
|
+
# Tables: feeds_sources, feeds_items, feeds_fetch_logs, etc.
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Requires matching migration table names. If changing prefix on an existing install, you must rename tables.
|