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
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-service-object
|
|
3
|
+
description: Creates service objects following single-responsibility principle with comprehensive tests. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Service Object Pattern
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Service objects encapsulate business logic:
|
|
12
|
+
- Single responsibility (one public method: `#call`)
|
|
13
|
+
- Easy to test in isolation
|
|
14
|
+
- Reusable across controllers, jobs, rake tasks
|
|
15
|
+
- Clear input/output contract
|
|
16
|
+
- Dependency injection for testability
|
|
17
|
+
|
|
18
|
+
## When to Use Service Objects
|
|
19
|
+
|
|
20
|
+
| Scenario | Use Service Object? |
|
|
21
|
+
|----------|---------------------|
|
|
22
|
+
| Complex business logic spanning multiple models | Yes |
|
|
23
|
+
| Multiple model interactions in one operation | Yes |
|
|
24
|
+
| External API calls | Yes |
|
|
25
|
+
| Logic shared across controllers/jobs | Yes |
|
|
26
|
+
| Operations with side effects (emails, webhooks) | Yes |
|
|
27
|
+
| Simple CRUD operations | **No** (use model) |
|
|
28
|
+
| Single model validation | **No** (use model) |
|
|
29
|
+
| Simple query/filter | **No** (use scope or query object) |
|
|
30
|
+
| View formatting | **No** (use presenter) |
|
|
31
|
+
| Form handling with validations | **No** (use form object) |
|
|
32
|
+
|
|
33
|
+
## When NOT to Use Service Objects
|
|
34
|
+
|
|
35
|
+
**Don't create a service object when:**
|
|
36
|
+
- A model callback does the job (e.g., `after_create :send_welcome_email`)
|
|
37
|
+
- The logic is a single ActiveRecord operation
|
|
38
|
+
- A concern would share the behavior more naturally
|
|
39
|
+
- You're wrapping a single method call (adds indirection for no benefit)
|
|
40
|
+
- The "service" just delegates to one model method
|
|
41
|
+
|
|
42
|
+
**Rule of thumb:** If your service object's `#call` method is under 5 lines and calls one model method, you don't need it.
|
|
43
|
+
|
|
44
|
+
## Workflow Checklist
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Service Object Progress:
|
|
48
|
+
- [ ] Step 1: Define input/output contract
|
|
49
|
+
- [ ] Step 2: Create service test (RED)
|
|
50
|
+
- [ ] Step 3: Run test (fails - no service)
|
|
51
|
+
- [ ] Step 4: Create service file with empty #call
|
|
52
|
+
- [ ] Step 5: Run test (fails - wrong return)
|
|
53
|
+
- [ ] Step 6: Implement #call method
|
|
54
|
+
- [ ] Step 7: Run test (GREEN)
|
|
55
|
+
- [ ] Step 8: Add error case tests
|
|
56
|
+
- [ ] Step 9: Implement error handling
|
|
57
|
+
- [ ] Step 10: Final test run
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Step 1: Define Contract
|
|
61
|
+
|
|
62
|
+
```markdown
|
|
63
|
+
## Service: Orders::CreateService
|
|
64
|
+
|
|
65
|
+
### Purpose
|
|
66
|
+
Creates a new order with inventory validation and payment processing.
|
|
67
|
+
|
|
68
|
+
### Input
|
|
69
|
+
- user: User (required)
|
|
70
|
+
- items: Array<Hash> (required) - [{product_id:, quantity:}]
|
|
71
|
+
- payment_method_id: Integer (optional)
|
|
72
|
+
|
|
73
|
+
### Output (Result object)
|
|
74
|
+
Success: { success?: true, data: Order }
|
|
75
|
+
Failure: { success?: false, error: String, code: Symbol }
|
|
76
|
+
|
|
77
|
+
### Dependencies
|
|
78
|
+
- inventory_service: Checks product availability
|
|
79
|
+
- payment_gateway: Processes payment
|
|
80
|
+
|
|
81
|
+
### Side Effects
|
|
82
|
+
- Creates Order and OrderItem records
|
|
83
|
+
- Decrements inventory
|
|
84
|
+
- Charges payment method
|
|
85
|
+
- Sends confirmation email (async)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Step 2: Service Test
|
|
89
|
+
|
|
90
|
+
Location: `test/services/orders/create_service_test.rb`
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# frozen_string_literal: true
|
|
94
|
+
|
|
95
|
+
require "test_helper"
|
|
96
|
+
|
|
97
|
+
class Orders::CreateServiceTest < ActiveSupport::TestCase
|
|
98
|
+
setup do
|
|
99
|
+
@user = users(:one)
|
|
100
|
+
@product = products(:available)
|
|
101
|
+
@items = [{ product_id: @product.id, quantity: 2 }]
|
|
102
|
+
@service = Orders::CreateService.new
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
test "#call with valid inputs returns success" do
|
|
106
|
+
result = @service.call(user: @user, items: @items)
|
|
107
|
+
|
|
108
|
+
assert result.success?
|
|
109
|
+
assert_instance_of Order, result.data
|
|
110
|
+
assert_equal @user, result.data.user
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
test "#call with valid inputs creates an order" do
|
|
114
|
+
assert_difference("Order.count", 1) do
|
|
115
|
+
@service.call(user: @user, items: @items)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
test "#call with empty items returns failure" do
|
|
120
|
+
result = @service.call(user: @user, items: [])
|
|
121
|
+
|
|
122
|
+
assert result.failure?
|
|
123
|
+
assert_equal "No items provided", result.error
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
test "#call with insufficient inventory returns failure" do
|
|
127
|
+
items = [{ product_id: @product.id, quantity: 999_999 }]
|
|
128
|
+
|
|
129
|
+
result = @service.call(user: @user, items: items)
|
|
130
|
+
|
|
131
|
+
assert result.failure?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
test "#call with insufficient inventory does not create order" do
|
|
135
|
+
items = [{ product_id: @product.id, quantity: 999_999 }]
|
|
136
|
+
|
|
137
|
+
assert_no_difference("Order.count") do
|
|
138
|
+
@service.call(user: @user, items: items)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Step 3-6: Implement Service
|
|
145
|
+
|
|
146
|
+
Location: `app/services/orders/create_service.rb`
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# frozen_string_literal: true
|
|
150
|
+
|
|
151
|
+
module Orders
|
|
152
|
+
class CreateService
|
|
153
|
+
def initialize(inventory_service: InventoryService.new,
|
|
154
|
+
payment_gateway: PaymentGateway.new)
|
|
155
|
+
@inventory_service = inventory_service
|
|
156
|
+
@payment_gateway = payment_gateway
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def call(user:, items:, payment_method_id: nil)
|
|
160
|
+
return failure("No items provided", :empty_items) if items.empty?
|
|
161
|
+
return failure("Insufficient inventory", :insufficient_inventory) unless inventory_available?(items)
|
|
162
|
+
|
|
163
|
+
order = create_order(user, items)
|
|
164
|
+
process_payment(order, payment_method_id) if payment_method_id
|
|
165
|
+
|
|
166
|
+
success(order)
|
|
167
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
168
|
+
failure(e.message, :validation_failed)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
attr_reader :inventory_service, :payment_gateway
|
|
174
|
+
|
|
175
|
+
def inventory_available?(items)
|
|
176
|
+
items.all? do |item|
|
|
177
|
+
inventory_service.available?(item[:product_id], item[:quantity])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def create_order(user, items)
|
|
182
|
+
ActiveRecord::Base.transaction do
|
|
183
|
+
order = Order.create!(user: user, status: :pending)
|
|
184
|
+
|
|
185
|
+
items.each do |item|
|
|
186
|
+
order.order_items.create!(
|
|
187
|
+
product_id: item[:product_id],
|
|
188
|
+
quantity: item[:quantity]
|
|
189
|
+
)
|
|
190
|
+
inventory_service.decrement(item[:product_id], item[:quantity])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
order
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def process_payment(order, payment_method_id)
|
|
198
|
+
payment_gateway.charge(
|
|
199
|
+
amount: order.total,
|
|
200
|
+
payment_method_id: payment_method_id
|
|
201
|
+
)
|
|
202
|
+
order.update!(status: :paid)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def success(data)
|
|
206
|
+
Result.new(success: true, data: data)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def failure(error, code = :unknown)
|
|
210
|
+
Result.new(success: false, error: error, code: code)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Result Object
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# app/services/result.rb
|
|
220
|
+
# frozen_string_literal: true
|
|
221
|
+
|
|
222
|
+
class Result
|
|
223
|
+
attr_reader :data, :error, :code
|
|
224
|
+
|
|
225
|
+
def initialize(success:, data: nil, error: nil, code: nil)
|
|
226
|
+
@success = success
|
|
227
|
+
@data = data
|
|
228
|
+
@error = error
|
|
229
|
+
@code = code
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def success?
|
|
233
|
+
@success
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def failure?
|
|
237
|
+
!@success
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def deconstruct_keys(keys)
|
|
241
|
+
{ success: @success, data: @data, error: @error, code: @code }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Testing with Mocked Dependencies
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
class Orders::CreateServiceTest < ActiveSupport::TestCase
|
|
250
|
+
setup do
|
|
251
|
+
@inventory_service = Minitest::Mock.new
|
|
252
|
+
@payment_gateway = Minitest::Mock.new
|
|
253
|
+
@service = Orders::CreateService.new(
|
|
254
|
+
inventory_service: @inventory_service,
|
|
255
|
+
payment_gateway: @payment_gateway
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
test "calls inventory service to check availability" do
|
|
260
|
+
@inventory_service.expect(:available?, true, [Integer, Integer])
|
|
261
|
+
@inventory_service.expect(:decrement, true, [Integer, Integer])
|
|
262
|
+
|
|
263
|
+
@service.call(user: users(:one), items: [{ product_id: 1, quantity: 2 }])
|
|
264
|
+
|
|
265
|
+
@inventory_service.verify
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Calling Services
|
|
271
|
+
|
|
272
|
+
### From Controllers
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
class OrdersController < ApplicationController
|
|
276
|
+
def create
|
|
277
|
+
result = Orders::CreateService.new.call(
|
|
278
|
+
user: current_user,
|
|
279
|
+
items: order_params[:items],
|
|
280
|
+
payment_method_id: order_params[:payment_method_id]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if result.success?
|
|
284
|
+
redirect_to result.data, notice: "Order created"
|
|
285
|
+
else
|
|
286
|
+
flash.now[:alert] = result.error
|
|
287
|
+
render :new, status: :unprocessable_entity
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### From Jobs
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
class ProcessOrderJob < ApplicationJob
|
|
297
|
+
def perform(user_id, items)
|
|
298
|
+
user = User.find(user_id)
|
|
299
|
+
result = Orders::CreateService.new.call(user: user, items: items)
|
|
300
|
+
|
|
301
|
+
unless result.success?
|
|
302
|
+
Rails.logger.error("Order failed: #{result.error}")
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Directory Structure
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
app/services/
|
|
312
|
+
result.rb
|
|
313
|
+
orders/
|
|
314
|
+
create_service.rb
|
|
315
|
+
cancel_service.rb
|
|
316
|
+
users/
|
|
317
|
+
register_service.rb
|
|
318
|
+
payments/
|
|
319
|
+
charge_service.rb
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Conventions
|
|
323
|
+
|
|
324
|
+
1. **Naming**: `Namespace::VerbNounService` (e.g., `Orders::CreateService`)
|
|
325
|
+
2. **Location**: `app/services/[namespace]/[name]_service.rb`
|
|
326
|
+
3. **Interface**: Single public method `#call`
|
|
327
|
+
4. **Return**: Always return Result object
|
|
328
|
+
5. **Dependencies**: Inject via constructor
|
|
329
|
+
6. **Errors**: Catch and wrap in Result, don't raise
|
|
330
|
+
|
|
331
|
+
## Anti-Patterns to Avoid
|
|
332
|
+
|
|
333
|
+
1. **God service**: Too many responsibilities - split it
|
|
334
|
+
2. **Hidden dependencies**: Using globals instead of injection
|
|
335
|
+
3. **No return contract**: Returning different types
|
|
336
|
+
4. **Raising exceptions**: Use Result objects instead
|
|
337
|
+
5. **Service wrapping one method**: Just call the method directly
|
|
338
|
+
6. **Service with multiple public methods**: Use separate services
|
|
339
|
+
|
|
340
|
+
## Checklist
|
|
341
|
+
|
|
342
|
+
- [ ] Contract defined (input/output/side effects)
|
|
343
|
+
- [ ] Test written first (RED)
|
|
344
|
+
- [ ] Single public method `#call`
|
|
345
|
+
- [ ] Returns Result object consistently
|
|
346
|
+
- [ ] Dependencies injected via constructor
|
|
347
|
+
- [ ] Error cases tested
|
|
348
|
+
- [ ] Transaction wraps multi-model operations
|
|
349
|
+
- [ ] All tests GREEN
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sm-architecture
|
|
3
|
+
description: Provides SourceMonitor engine architecture context. Use when working with engine internals, lib/ module structure, autoload organization, configuration DSL, pipelines, or any structural/organizational code in the source_monitor namespace.
|
|
4
|
+
allowed-tools: Read, Glob, Grep
|
|
5
|
+
user-invocable: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SourceMonitor Architecture
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
SourceMonitor is a Rails 8 mountable engine (`SourceMonitor::Engine`). Code is split between:
|
|
13
|
+
- **`app/`** -- Rails conventions (models, controllers, views, jobs, concerns)
|
|
14
|
+
- **`lib/source_monitor/`** -- Domain logic, configuration, pipelines, utilities
|
|
15
|
+
|
|
16
|
+
The engine uses **Ruby autoload** (not Zeitwerk) for `lib/` modules, with explicit `require` only for critical boot-time modules.
|
|
17
|
+
|
|
18
|
+
## Boot Sequence
|
|
19
|
+
|
|
20
|
+
`lib/source_monitor.rb` loads in this order:
|
|
21
|
+
|
|
22
|
+
1. **Optional gems** (rescue LoadError): `solid_queue`, `solid_cable`, `turbo-rails`, `ransack`
|
|
23
|
+
2. **Table name prefix** setup via `redefine_method`
|
|
24
|
+
3. **Explicit requires** (11 files -- must load at boot):
|
|
25
|
+
- `version`, `engine`, `configuration`, `model_extensions`
|
|
26
|
+
- `events`, `instrumentation`, `metrics`
|
|
27
|
+
- `health`, `realtime`, `feedjira_extensions`
|
|
28
|
+
4. **Autoload declarations** (71 modules) organized by namespace
|
|
29
|
+
|
|
30
|
+
## Engine Configuration
|
|
31
|
+
|
|
32
|
+
`SourceMonitor::Engine` (`lib/source_monitor/engine.rb`):
|
|
33
|
+
- `isolate_namespace SourceMonitor`
|
|
34
|
+
- Table name prefix from `config.models.table_name_prefix`
|
|
35
|
+
- Initializers: assets, metrics subscribers, dashboard streams, jobs/Solid Queue setup
|
|
36
|
+
|
|
37
|
+
## Module Tree
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
SourceMonitor (top-level)
|
|
41
|
+
|-- HTTP # Faraday HTTP client factory
|
|
42
|
+
|-- Scheduler # Fetch scheduling coordinator
|
|
43
|
+
|-- Assets # Asset path helpers
|
|
44
|
+
|
|
|
45
|
+
|-- Analytics/ # Dashboard analytics queries
|
|
46
|
+
| |-- SourceFetchIntervalDistribution
|
|
47
|
+
| |-- SourceActivityRates
|
|
48
|
+
| |-- SourcesIndexMetrics
|
|
49
|
+
|
|
|
50
|
+
|-- Dashboard/ # Dashboard UI support
|
|
51
|
+
| |-- QuickAction, QuickActionsPresenter
|
|
52
|
+
| |-- RecentActivity, RecentActivityPresenter
|
|
53
|
+
| |-- Queries, TurboBroadcaster
|
|
54
|
+
| |-- UpcomingFetchSchedule
|
|
55
|
+
|
|
|
56
|
+
|-- Fetching/ # Feed fetch pipeline
|
|
57
|
+
| |-- FeedFetcher # Main orchestrator
|
|
58
|
+
| | |-- AdaptiveInterval # Interval calculation
|
|
59
|
+
| | |-- SourceUpdater # Source state updates
|
|
60
|
+
| | |-- EntryProcessor # Entry iteration + item creation
|
|
61
|
+
| |-- FetchRunner # Job-level fetch coordinator
|
|
62
|
+
| |-- RetryPolicy # Retry/circuit-breaker decisions
|
|
63
|
+
| |-- StalledFetchReconciler
|
|
64
|
+
| |-- AdvisoryLock # PG advisory locks
|
|
65
|
+
| |-- FetchError (+ subclasses)
|
|
66
|
+
|
|
|
67
|
+
|-- Items/ # Item management
|
|
68
|
+
| |-- ItemCreator # Create/update items from entries
|
|
69
|
+
| | |-- EntryParser # Parse feed entries to attributes
|
|
70
|
+
| | |-- ContentExtractor # Process content through readability
|
|
71
|
+
| |-- RetentionPruner # Age/count-based item cleanup
|
|
72
|
+
| |-- RetentionStrategies/ # Destroy vs SoftDelete
|
|
73
|
+
|
|
|
74
|
+
|-- ImportSessions/ # OPML import support
|
|
75
|
+
| |-- EntryNormalizer
|
|
76
|
+
| |-- HealthCheckBroadcaster
|
|
77
|
+
|
|
|
78
|
+
|-- Jobs/ # Job infrastructure
|
|
79
|
+
| |-- CleanupOptions
|
|
80
|
+
| |-- Visibility # Queue visibility setup
|
|
81
|
+
| |-- SolidQueueMetrics
|
|
82
|
+
| |-- FetchFailureSubscriber
|
|
83
|
+
|
|
|
84
|
+
|-- Logs/ # Unified log system
|
|
85
|
+
| |-- EntrySync # Sync log records to LogEntry
|
|
86
|
+
| |-- FilterSet, Query, TablePresenter
|
|
87
|
+
|
|
|
88
|
+
|-- Models/ # Model concerns
|
|
89
|
+
| |-- Sanitizable # String/hash sanitization
|
|
90
|
+
| |-- UrlNormalizable # URL normalization
|
|
91
|
+
|
|
|
92
|
+
|-- Scrapers/ # Content scraping adapters
|
|
93
|
+
| |-- Base # Scraper interface
|
|
94
|
+
| |-- Readability # Default readability adapter
|
|
95
|
+
| |-- Fetchers/HttpFetcher
|
|
96
|
+
| |-- Parsers/ReadabilityParser
|
|
97
|
+
|
|
|
98
|
+
|-- Scraping/ # Scraping orchestration
|
|
99
|
+
| |-- Enqueuer, Scheduler
|
|
100
|
+
| |-- ItemScraper (+ AdapterResolver, Persistence)
|
|
101
|
+
| |-- BulkSourceScraper, BulkResultPresenter
|
|
102
|
+
| |-- State
|
|
103
|
+
|
|
|
104
|
+
|-- Configuration/ # Configuration DSL (12 settings files)
|
|
105
|
+
| |-- HTTPSettings, FetchingSettings, HealthSettings
|
|
106
|
+
| |-- ScrapingSettings, RealtimeSettings, RetentionSettings
|
|
107
|
+
| |-- AuthenticationSettings, ScraperRegistry
|
|
108
|
+
| |-- Events, ValidationDefinition
|
|
109
|
+
| |-- ModelDefinition, Models
|
|
110
|
+
|
|
|
111
|
+
|-- Security/ # Security layer
|
|
112
|
+
| |-- ParameterSanitizer
|
|
113
|
+
| |-- Authentication
|
|
114
|
+
|
|
|
115
|
+
|-- Setup/ # Install/setup wizard
|
|
116
|
+
| |-- CLI, Workflow, Requirements
|
|
117
|
+
| |-- Detectors, DependencyChecker
|
|
118
|
+
| |-- GemfileEditor, BundleInstaller, NodeInstaller
|
|
119
|
+
| |-- InstallGenerator, MigrationInstaller, InitializerPatcher
|
|
120
|
+
| |-- Verification/ (Result, Runner, Printer, etc.)
|
|
121
|
+
|
|
|
122
|
+
|-- Pagination/Paginator
|
|
123
|
+
|-- Release/ (Changelog, Runner)
|
|
124
|
+
|-- Sources/ (Params, TurboStreamPresenter)
|
|
125
|
+
|-- TurboStreams/StreamResponder
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Key Architectural Patterns
|
|
129
|
+
|
|
130
|
+
### 1. Configuration DSL
|
|
131
|
+
|
|
132
|
+
The `Configuration` class composes 12 settings objects:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
SourceMonitor.configure do |config|
|
|
136
|
+
config.http.timeout = 30
|
|
137
|
+
config.fetching.min_interval_minutes = 5
|
|
138
|
+
config.health.auto_pause_threshold = 0.3
|
|
139
|
+
config.retention.strategy = :soft_delete
|
|
140
|
+
config.scraping.concurrency = 3
|
|
141
|
+
config.models.table_name_prefix = "sm_"
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each settings class is a standalone PORO with defaults. Configuration is resettable via `reset_configuration!`.
|
|
146
|
+
|
|
147
|
+
### 2. ModelExtensions Registry
|
|
148
|
+
|
|
149
|
+
Models register themselves at class load time:
|
|
150
|
+
```ruby
|
|
151
|
+
SourceMonitor::ModelExtensions.register(self, :source)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This enables:
|
|
155
|
+
- Dynamic table name prefixing
|
|
156
|
+
- Host-app concern injection
|
|
157
|
+
- Host-app validation injection
|
|
158
|
+
- Full reload on config change
|
|
159
|
+
|
|
160
|
+
### 3. Event System
|
|
161
|
+
|
|
162
|
+
Three lifecycle events dispatched through `SourceMonitor::Events`:
|
|
163
|
+
|
|
164
|
+
| Event | Fired When | Payload |
|
|
165
|
+
|-------|-----------|---------|
|
|
166
|
+
| `after_item_created` | New item saved | ItemCreatedEvent |
|
|
167
|
+
| `after_item_scraped` | Scrape completed | ItemScrapedEvent |
|
|
168
|
+
| `after_fetch_completed` | Fetch finished | FetchCompletedEvent |
|
|
169
|
+
|
|
170
|
+
Plus `item_processors` -- callbacks run for every item (created or updated).
|
|
171
|
+
|
|
172
|
+
Events are registered via `config.events` and dispatched with error isolation per handler.
|
|
173
|
+
|
|
174
|
+
### 4. Instrumentation (ActiveSupport::Notifications)
|
|
175
|
+
|
|
176
|
+
| Event | Purpose |
|
|
177
|
+
|-------|---------|
|
|
178
|
+
| `source_monitor.fetch.start` | Fetch beginning |
|
|
179
|
+
| `source_monitor.fetch.finish` | Fetch completed |
|
|
180
|
+
| `source_monitor.items.duplicate` | Duplicate item detected |
|
|
181
|
+
| `source_monitor.items.retention` | Retention pruning |
|
|
182
|
+
|
|
183
|
+
`Metrics` module subscribes to these and maintains counters/gauges.
|
|
184
|
+
|
|
185
|
+
### 5. Pipeline Architecture
|
|
186
|
+
|
|
187
|
+
**Fetch Pipeline:**
|
|
188
|
+
```
|
|
189
|
+
FetchRunner
|
|
190
|
+
-> AdvisoryLock (PG lock per source)
|
|
191
|
+
-> FeedFetcher.call
|
|
192
|
+
-> HTTP request (Faraday)
|
|
193
|
+
-> Parse feed (Feedjira)
|
|
194
|
+
-> EntryProcessor.process_feed_entries
|
|
195
|
+
-> ItemCreator.call (per entry)
|
|
196
|
+
-> EntryParser.parse
|
|
197
|
+
-> ContentExtractor.process_feed_content
|
|
198
|
+
-> Events.run_item_processors
|
|
199
|
+
-> Events.after_item_created
|
|
200
|
+
-> SourceUpdater.update_source_for_success
|
|
201
|
+
-> AdaptiveInterval.apply_adaptive_interval!
|
|
202
|
+
-> SourceUpdater.create_fetch_log
|
|
203
|
+
-> Events.after_fetch_completed
|
|
204
|
+
-> Completion handlers (retention, follow-up scraping)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Scrape Pipeline:**
|
|
208
|
+
```
|
|
209
|
+
Scraping::Enqueuer
|
|
210
|
+
-> ItemScraper
|
|
211
|
+
-> AdapterResolver (select scraper)
|
|
212
|
+
-> Scrapers::Base subclass
|
|
213
|
+
-> Fetchers::HttpFetcher
|
|
214
|
+
-> Parsers::ReadabilityParser
|
|
215
|
+
-> Persistence (save to ItemContent)
|
|
216
|
+
-> Events.after_item_scraped
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 6. Health Monitoring
|
|
220
|
+
|
|
221
|
+
Health module hooks into `after_fetch_completed`:
|
|
222
|
+
- `SourceHealthMonitor` -- calculates rolling success rate
|
|
223
|
+
- `SourceHealthCheck` -- HTTP health probe
|
|
224
|
+
- Auto-pause sources below threshold
|
|
225
|
+
- `SourceHealthReset` -- manual health reset
|
|
226
|
+
|
|
227
|
+
## References
|
|
228
|
+
|
|
229
|
+
- [Module Map](reference/module-map.md) -- Full module tree with responsibilities
|
|
230
|
+
- [Extraction Patterns](reference/extraction-patterns.md) -- Refactoring patterns from Phase 3/4
|
|
231
|
+
- Main entry: `lib/source_monitor.rb`
|
|
232
|
+
- Engine: `lib/source_monitor/engine.rb`
|
|
233
|
+
- Configuration: `lib/source_monitor/configuration.rb` + `lib/source_monitor/configuration/*.rb`
|