source_monitor 0.2.0 → 0.3.0

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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
data/config/routes.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  SourceMonitor::Engine.routes.draw do
2
4
  get "/health", to: "health#show"
3
5
  get "/dashboard", to: "dashboard#index", as: :dashboard
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddFeedContentReadabilityToSources < ActiveRecord::Migration[8.0]
2
4
  def change
3
5
  add_column :sourcemon_sources, :feed_content_readability_enabled, :boolean, default: false, null: false
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddPerformanceIndexes < ActiveRecord::Migration[8.0]
2
4
  def change
3
5
  # Index for activity rate calculations
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddFetchStatusCheckConstraint < ActiveRecord::Migration[8.0]
2
4
  def up
3
5
  # Add PostgreSQL CHECK constraint to enforce fetch_status enum values at database level
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RefreshFetchStatusConstraint < ActiveRecord::Migration[8.0]
2
4
  ALLOWED_STATUSES = %w[idle queued fetching failed invalid].freeze
3
5
  PREVIOUS_STATUSES = %w[idle queued fetching failed].freeze
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCompositeIndexToLogEntries < ActiveRecord::Migration[8.1]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ add_index :sourcemon_log_entries, [ :started_at, :id ],
8
+ order: { started_at: :desc, id: :desc },
9
+ name: "index_log_entries_on_started_at_desc_id_desc",
10
+ algorithm: :concurrently
11
+
12
+ add_index :sourcemon_log_entries, [ :loggable_type, :started_at, :id ],
13
+ order: { started_at: :desc, id: :desc },
14
+ name: "index_log_entries_on_loggable_type_started_at_id",
15
+ algorithm: :concurrently
16
+ end
17
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SourceMonitor
2
4
  module Assets
3
5
  module Bundler
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SourceMonitor
2
4
  module Assets
3
5
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class AuthenticationSettings
6
+ Handler = Struct.new(:type, :callable) do
7
+ def call(controller)
8
+ return unless callable
9
+
10
+ case type
11
+ when :symbol
12
+ controller.public_send(callable)
13
+ when :callable
14
+ arity = callable.arity
15
+ if arity.zero?
16
+ controller.instance_exec(&callable)
17
+ else
18
+ callable.call(controller)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ attr_reader :authenticate_handler, :authorize_handler
25
+ attr_accessor :current_user_method, :user_signed_in_method
26
+
27
+ def initialize
28
+ reset!
29
+ end
30
+
31
+ def authenticate_with(handler = nil, &block)
32
+ @authenticate_handler = build_handler(handler, &block)
33
+ end
34
+
35
+ def authorize_with(handler = nil, &block)
36
+ @authorize_handler = build_handler(handler, &block)
37
+ end
38
+
39
+ def reset!
40
+ @authenticate_handler = nil
41
+ @authorize_handler = nil
42
+ @current_user_method = nil
43
+ @user_signed_in_method = nil
44
+ end
45
+
46
+ private
47
+
48
+ def build_handler(handler = nil, &block)
49
+ handler ||= block
50
+ return nil unless handler
51
+
52
+ if handler.is_a?(Symbol) || handler.is_a?(String)
53
+ Handler.new(:symbol, handler.to_sym)
54
+ elsif handler.respond_to?(:call)
55
+ Handler.new(:callable, handler)
56
+ else
57
+ raise ArgumentError, "Invalid authentication handler #{handler.inspect}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class Events
6
+ CALLBACK_KEYS = %i[after_item_created after_item_scraped after_fetch_completed].freeze
7
+
8
+ def initialize
9
+ @callbacks = Hash.new { |hash, key| hash[key] = [] }
10
+ @item_processors = []
11
+ end
12
+
13
+ CALLBACK_KEYS.each do |key|
14
+ define_method(key) do |handler = nil, &block|
15
+ register_callback(key, handler, &block)
16
+ end
17
+ end
18
+
19
+ def register_item_processor(processor = nil, &block)
20
+ callable = processor || block
21
+ validate_callable!(callable, :item_processor)
22
+ @item_processors << callable
23
+ callable
24
+ end
25
+
26
+ def callbacks_for(name)
27
+ @callbacks[name.to_sym]&.dup || []
28
+ end
29
+
30
+ def item_processors
31
+ @item_processors.dup
32
+ end
33
+
34
+ def reset!
35
+ @callbacks.clear
36
+ @item_processors.clear
37
+ end
38
+
39
+ private
40
+
41
+ def register_callback(key, handler = nil, &block)
42
+ callable = handler || block
43
+ validate_callable!(callable, key)
44
+ key = key.to_sym
45
+ unless CALLBACK_KEYS.include?(key)
46
+ raise ArgumentError, "Unknown event #{key.inspect}"
47
+ end
48
+
49
+ @callbacks[key] << callable
50
+ callable
51
+ end
52
+
53
+ def validate_callable!(callable, name)
54
+ unless callable.respond_to?(:call)
55
+ raise ArgumentError, "#{name} handler must respond to #call"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class FetchingSettings
6
+ attr_accessor :min_interval_minutes,
7
+ :max_interval_minutes,
8
+ :increase_factor,
9
+ :decrease_factor,
10
+ :failure_increase_factor,
11
+ :jitter_percent
12
+
13
+ def initialize
14
+ reset!
15
+ end
16
+
17
+ def reset!
18
+ @min_interval_minutes = 5
19
+ @max_interval_minutes = 24 * 60
20
+ @increase_factor = 1.25
21
+ @decrease_factor = 0.75
22
+ @failure_increase_factor = 1.5
23
+ @jitter_percent = 0.1
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class HealthSettings
6
+ attr_accessor :window_size,
7
+ :healthy_threshold,
8
+ :warning_threshold,
9
+ :auto_pause_threshold,
10
+ :auto_resume_threshold,
11
+ :auto_pause_cooldown_minutes
12
+
13
+ def initialize
14
+ reset!
15
+ end
16
+
17
+ def reset!
18
+ @window_size = 20
19
+ @healthy_threshold = 0.8
20
+ @warning_threshold = 0.5
21
+ @auto_pause_threshold = 0.2
22
+ @auto_resume_threshold = 0.6
23
+ @auto_pause_cooldown_minutes = 60
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class HTTPSettings
6
+ attr_accessor :timeout,
7
+ :open_timeout,
8
+ :max_redirects,
9
+ :user_agent,
10
+ :proxy,
11
+ :headers,
12
+ :retry_max,
13
+ :retry_interval,
14
+ :retry_interval_randomness,
15
+ :retry_backoff_factor,
16
+ :retry_statuses
17
+
18
+ def initialize
19
+ reset!
20
+ end
21
+
22
+ def reset!
23
+ @timeout = 15
24
+ @open_timeout = 5
25
+ @max_redirects = 5
26
+ @user_agent = default_user_agent
27
+ @proxy = nil
28
+ @headers = {}
29
+ @retry_max = 4
30
+ @retry_interval = 0.5
31
+ @retry_interval_randomness = 0.5
32
+ @retry_backoff_factor = 2
33
+ @retry_statuses = nil
34
+ end
35
+
36
+ private
37
+
38
+ def default_user_agent
39
+ "SourceMonitor/#{SourceMonitor::VERSION}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class ModelDefinition
6
+ attr_reader :validations
7
+
8
+ def initialize
9
+ @concern_definitions = []
10
+ @validations = []
11
+ end
12
+
13
+ def include_concern(concern = nil, &block)
14
+ definition = ConcernDefinition.new(concern, block)
15
+ unless @concern_definitions.any? { |existing| existing.signature == definition.signature }
16
+ @concern_definitions << definition
17
+ end
18
+
19
+ definition.return_value
20
+ end
21
+
22
+ def each_concern
23
+ return enum_for(:each_concern) unless block_given?
24
+
25
+ @concern_definitions.each do |definition|
26
+ yield definition.signature, definition.resolve
27
+ end
28
+ end
29
+
30
+ def validate(handler = nil, **options, &block)
31
+ callable =
32
+ if block
33
+ block
34
+ elsif handler.respond_to?(:call) && !handler.is_a?(Symbol) && !handler.is_a?(String)
35
+ handler
36
+ elsif handler.is_a?(Symbol) || handler.is_a?(String)
37
+ handler.to_sym
38
+ else
39
+ raise ArgumentError, "Invalid validation handler #{handler.inspect}"
40
+ end
41
+
42
+ validation = ValidationDefinition.new(callable, options)
43
+ @validations << validation
44
+ validation
45
+ end
46
+
47
+ private
48
+
49
+ class ConcernDefinition
50
+ attr_reader :signature
51
+
52
+ def initialize(concern, block)
53
+ @resolver = build_resolver(concern, block)
54
+ @signature = build_signature(concern, block)
55
+ @return_value = determine_return_value(concern, block)
56
+ end
57
+
58
+ def resolve
59
+ @resolved ||= @resolver.call
60
+ end
61
+
62
+ def return_value
63
+ @return_value
64
+ end
65
+
66
+ private
67
+
68
+ def build_resolver(concern, block)
69
+ if block
70
+ mod = Module.new(&block)
71
+ -> { mod }
72
+ elsif concern.is_a?(Module)
73
+ -> { concern }
74
+ elsif concern.respond_to?(:to_s)
75
+ constant_name = concern.to_s
76
+ lambda do
77
+ constant_name.constantize
78
+ rescue NameError => error
79
+ raise ArgumentError, error.message
80
+ end
81
+ else
82
+ raise ArgumentError, "Invalid concern #{concern.inspect}"
83
+ end
84
+ end
85
+
86
+ def build_signature(concern, block)
87
+ if block
88
+ [ :anonymous_module, block.object_id ]
89
+ elsif concern.is_a?(Module)
90
+ [ :module, concern.object_id ]
91
+ else
92
+ [ :constant, concern.to_s ]
93
+ end
94
+ end
95
+
96
+ def determine_return_value(concern, block)
97
+ if block
98
+ resolve
99
+ elsif concern.is_a?(Module)
100
+ concern
101
+ else
102
+ concern
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class Models
6
+ MODEL_KEYS = {
7
+ source: :source,
8
+ item: :item,
9
+ fetch_log: :fetch_log,
10
+ scrape_log: :scrape_log,
11
+ health_check_log: :health_check_log,
12
+ item_content: :item_content,
13
+ log_entry: :log_entry
14
+ }.freeze
15
+
16
+ attr_accessor :table_name_prefix
17
+
18
+ def initialize
19
+ @table_name_prefix = "sourcemon_"
20
+ @definitions = MODEL_KEYS.transform_values { ModelDefinition.new }
21
+ end
22
+
23
+ MODEL_KEYS.each do |method_name, key|
24
+ define_method(method_name) { @definitions[key] }
25
+ end
26
+
27
+ def for(name)
28
+ key = name.to_sym
29
+ definition = @definitions[key]
30
+ raise ArgumentError, "Unknown model #{name.inspect}" unless definition
31
+
32
+ definition
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class RealtimeSettings
6
+ VALID_ADAPTERS = %i[solid_cable redis async].freeze
7
+
8
+ attr_reader :adapter, :solid_cable
9
+ attr_accessor :redis_url
10
+
11
+ def initialize
12
+ reset!
13
+ end
14
+
15
+ def adapter=(value)
16
+ value = value&.to_sym
17
+ unless VALID_ADAPTERS.include?(value)
18
+ raise ArgumentError, "Unsupported realtime adapter #{value.inspect}"
19
+ end
20
+
21
+ @adapter = value
22
+ end
23
+
24
+ def reset!
25
+ @solid_cable = SolidCableOptions.new
26
+ @redis_url = nil
27
+ self.adapter = :solid_cable
28
+ end
29
+
30
+ def solid_cable=(options)
31
+ solid_cable.assign(options)
32
+ end
33
+
34
+ def action_cable_config
35
+ case adapter
36
+ when :solid_cable
37
+ solid_cable.to_h.merge(adapter: "solid_cable")
38
+ when :redis
39
+ config = { adapter: "redis" }
40
+ config[:url] = redis_url if redis_url.present?
41
+ config
42
+ when :async
43
+ { adapter: "async" }
44
+ else
45
+ {}
46
+ end
47
+ end
48
+
49
+ class SolidCableOptions
50
+ attr_accessor :polling_interval,
51
+ :message_retention,
52
+ :autotrim,
53
+ :silence_polling,
54
+ :use_skip_locked,
55
+ :trim_batch_size,
56
+ :connects_to
57
+
58
+ def initialize
59
+ reset!
60
+ end
61
+
62
+ def assign(options)
63
+ return unless options.respond_to?(:each)
64
+
65
+ options.each do |key, value|
66
+ setter = "#{key}="
67
+ public_send(setter, value) if respond_to?(setter)
68
+ end
69
+ end
70
+
71
+ def reset!
72
+ @polling_interval = "0.1.seconds"
73
+ @message_retention = "1.day"
74
+ @autotrim = true
75
+ @silence_polling = true
76
+ @use_skip_locked = true
77
+ @trim_batch_size = nil
78
+ @connects_to = nil
79
+ end
80
+
81
+ def to_h
82
+ {
83
+ polling_interval: polling_interval,
84
+ message_retention: message_retention,
85
+ autotrim: autotrim,
86
+ silence_polling: silence_polling,
87
+ use_skip_locked: use_skip_locked,
88
+ trim_batch_size: trim_batch_size,
89
+ connects_to: connects_to
90
+ }.compact
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class RetentionSettings
6
+ attr_accessor :items_retention_days, :max_items
7
+
8
+ def initialize
9
+ @items_retention_days = nil
10
+ @max_items = nil
11
+ @strategy = :destroy
12
+ end
13
+
14
+ def strategy
15
+ @strategy
16
+ end
17
+
18
+ def strategy=(value)
19
+ normalized = normalize_strategy(value)
20
+ @strategy = normalized unless normalized.nil?
21
+ end
22
+
23
+ private
24
+
25
+ def normalize_strategy(value)
26
+ return :destroy if value.nil?
27
+
28
+ if value.respond_to?(:to_sym)
29
+ candidate = value.to_sym
30
+ valid =
31
+ if defined?(SourceMonitor::Items::RetentionPruner::VALID_STRATEGIES)
32
+ SourceMonitor::Items::RetentionPruner::VALID_STRATEGIES
33
+ else
34
+ %i[destroy soft_delete]
35
+ end
36
+
37
+ raise ArgumentError, "Invalid retention strategy #{value.inspect}" unless valid.include?(candidate)
38
+ candidate
39
+ else
40
+ raise ArgumentError, "Invalid retention strategy #{value.inspect}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module SourceMonitor
6
+ class Configuration
7
+ class ScraperRegistry
8
+ include Enumerable
9
+
10
+ def initialize
11
+ @adapters = {}
12
+ end
13
+
14
+ def register(name, adapter)
15
+ key = normalize_name(name)
16
+ @adapters[key] = normalize_adapter(adapter)
17
+ end
18
+
19
+ def unregister(name)
20
+ @adapters.delete(normalize_name(name))
21
+ end
22
+
23
+ def adapter_for(name)
24
+ adapter = @adapters[normalize_name(name)]
25
+ adapter if adapter
26
+ end
27
+
28
+ def each(&block)
29
+ @adapters.each(&block)
30
+ end
31
+
32
+ private
33
+
34
+ def normalize_name(name)
35
+ value = name.to_s
36
+ raise ArgumentError, "Invalid scraper adapter name #{name.inspect}" unless value.match?(/\A[a-z0-9_]+\z/i)
37
+
38
+ value.downcase
39
+ end
40
+
41
+ def normalize_adapter(adapter)
42
+ constant = resolve_adapter(adapter)
43
+
44
+ if defined?(SourceMonitor::Scrapers::Base) && !(constant <= SourceMonitor::Scrapers::Base)
45
+ raise ArgumentError, "Scraper adapters must inherit from SourceMonitor::Scrapers::Base"
46
+ end
47
+
48
+ constant
49
+ end
50
+
51
+ def resolve_adapter(adapter)
52
+ return adapter if adapter.is_a?(Class)
53
+
54
+ if adapter.respond_to?(:to_s)
55
+ constant_name = adapter.to_s
56
+ begin
57
+ return constant_name.constantize
58
+ rescue NameError
59
+ raise ArgumentError, "Unknown scraper adapter constant #{constant_name.inspect}"
60
+ end
61
+ end
62
+
63
+ raise ArgumentError, "Invalid scraper adapter #{adapter.inspect}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class Configuration
5
+ class ScrapingSettings
6
+ attr_accessor :max_in_flight_per_source, :max_bulk_batch_size
7
+
8
+ DEFAULT_MAX_IN_FLIGHT = 25
9
+ DEFAULT_MAX_BULK_BATCH_SIZE = 100
10
+
11
+ def initialize
12
+ reset!
13
+ end
14
+
15
+ def reset!
16
+ @max_in_flight_per_source = DEFAULT_MAX_IN_FLIGHT
17
+ @max_bulk_batch_size = DEFAULT_MAX_BULK_BATCH_SIZE
18
+ end
19
+
20
+ def max_in_flight_per_source=(value)
21
+ @max_in_flight_per_source = normalize_numeric(value)
22
+ end
23
+
24
+ def max_bulk_batch_size=(value)
25
+ @max_bulk_batch_size = normalize_numeric(value)
26
+ end
27
+
28
+ private
29
+
30
+ def normalize_numeric(value)
31
+ return nil if value.nil?
32
+ return nil if value == ""
33
+
34
+ integer = value.respond_to?(:to_i) ? value.to_i : value
35
+ integer.positive? ? integer : nil
36
+ end
37
+ end
38
+ end
39
+ end