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
@@ -0,0 +1,311 @@
1
+ ---
2
+ name: active-storage-setup
3
+ description: Configures Active Storage for file uploads with variants and direct uploads. Use when adding file uploads, image attachments, document storage, generating thumbnails, or when user mentions Active Storage, file upload, attachments, or image processing.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Active Storage Setup for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Active Storage handles file uploads in Rails:
12
+ - Cloud storage (S3, GCS, Azure) or local disk
13
+ - Image variants (thumbnails, resizing)
14
+ - Direct uploads from browser
15
+ - Polymorphic attachments
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ bin/rails active_storage:install
21
+ bin/rails db:migrate
22
+ bundle add image_processing
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ```yaml
28
+ # config/storage.yml
29
+ local:
30
+ service: Disk
31
+ root: <%= Rails.root.join("storage") %>
32
+
33
+ test:
34
+ service: Disk
35
+ root: <%= Rails.root.join("tmp/storage") %>
36
+
37
+ amazon:
38
+ service: S3
39
+ access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
40
+ secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
41
+ region: eu-west-1
42
+ bucket: <%= Rails.application.credentials.dig(:aws, :bucket) %>
43
+ ```
44
+
45
+ ```ruby
46
+ # config/environments/development.rb
47
+ config.active_storage.service = :local
48
+
49
+ # config/environments/production.rb
50
+ config.active_storage.service = :amazon
51
+ ```
52
+
53
+ ## Model Attachments
54
+
55
+ ### Single Attachment
56
+
57
+ ```ruby
58
+ class User < ApplicationRecord
59
+ has_one_attached :avatar do |attachable|
60
+ attachable.variant :thumb, resize_to_limit: [100, 100]
61
+ attachable.variant :medium, resize_to_limit: [300, 300]
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### Multiple Attachments
67
+
68
+ ```ruby
69
+ class Event < ApplicationRecord
70
+ has_many_attached :photos
71
+ has_many_attached :documents
72
+ end
73
+ ```
74
+
75
+ ## Validations
76
+
77
+ ### Manual Validation
78
+
79
+ ```ruby
80
+ class User < ApplicationRecord
81
+ has_one_attached :avatar
82
+
83
+ validate :acceptable_avatar
84
+
85
+ private
86
+
87
+ def acceptable_avatar
88
+ return unless avatar.attached?
89
+
90
+ unless avatar.blob.byte_size <= 5.megabytes
91
+ errors.add(:avatar, "is too large (max 5MB)")
92
+ end
93
+
94
+ acceptable_types = ["image/jpeg", "image/png", "image/webp"]
95
+ unless acceptable_types.include?(avatar.content_type)
96
+ errors.add(:avatar, "must be a JPEG, PNG, or WebP")
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### With active_storage_validations Gem
103
+
104
+ ```ruby
105
+ gem "active_storage_validations"
106
+
107
+ class User < ApplicationRecord
108
+ has_one_attached :avatar
109
+
110
+ validates :avatar,
111
+ content_type: ["image/png", "image/jpeg", "image/webp"],
112
+ size: { less_than: 5.megabytes }
113
+ end
114
+ ```
115
+
116
+ ## Image Variants
117
+
118
+ ```ruby
119
+ # Resize to fit (maintains aspect ratio)
120
+ resize_to_limit: [300, 300]
121
+
122
+ # Resize and crop to exact dimensions
123
+ resize_to_fill: [300, 300]
124
+
125
+ # With format conversion
126
+ resize_to_limit: [300, 300], format: :webp, saver: { quality: 80 }
127
+ ```
128
+
129
+ ### Using in Views
130
+
131
+ ```erb
132
+ <% if user.avatar.attached? %>
133
+ <%= image_tag user.avatar.variant(:thumb), alt: user.name %>
134
+ <% else %>
135
+ <%= image_tag "default-avatar.png", alt: "Default" %>
136
+ <% end %>
137
+ ```
138
+
139
+ ## Testing Attachments (Minitest)
140
+
141
+ ### Model Test
142
+
143
+ ```ruby
144
+ # test/models/user_test.rb
145
+ require "test_helper"
146
+
147
+ class UserAttachmentTest < ActiveSupport::TestCase
148
+ setup do
149
+ @user = users(:one)
150
+ end
151
+
152
+ test "attaches an avatar" do
153
+ @user.avatar.attach(
154
+ io: File.open(Rails.root.join("test/fixtures/files/avatar.jpg")),
155
+ filename: "avatar.jpg",
156
+ content_type: "image/jpeg"
157
+ )
158
+
159
+ assert @user.avatar.attached?
160
+ end
161
+
162
+ test "rejects oversized avatar" do
163
+ @user.avatar.attach(
164
+ io: StringIO.new("x" * 6.megabytes),
165
+ filename: "large.jpg",
166
+ content_type: "image/jpeg"
167
+ )
168
+
169
+ assert_not @user.valid?
170
+ assert_includes @user.errors[:avatar], "is too large (max 5MB)"
171
+ end
172
+
173
+ test "rejects invalid content type" do
174
+ @user.avatar.attach(
175
+ io: File.open(Rails.root.join("test/fixtures/files/document.pdf")),
176
+ filename: "doc.pdf",
177
+ content_type: "application/pdf"
178
+ )
179
+
180
+ assert_not @user.valid?
181
+ assert @user.errors[:avatar].any?
182
+ end
183
+ end
184
+ ```
185
+
186
+ ### Controller Test
187
+
188
+ ```ruby
189
+ # test/controllers/users_controller_test.rb
190
+ require "test_helper"
191
+
192
+ class UsersUploadTest < ActionDispatch::IntegrationTest
193
+ setup do
194
+ @user = users(:one)
195
+ sign_in @user
196
+ end
197
+
198
+ test "uploads avatar" do
199
+ avatar = fixture_file_upload("avatar.jpg", "image/jpeg")
200
+
201
+ patch user_path(@user), params: { user: { avatar: avatar } }
202
+
203
+ assert @user.reload.avatar.attached?
204
+ end
205
+
206
+ test "removes avatar" do
207
+ @user.avatar.attach(
208
+ io: File.open(Rails.root.join("test/fixtures/files/avatar.jpg")),
209
+ filename: "avatar.jpg",
210
+ content_type: "image/jpeg"
211
+ )
212
+
213
+ delete remove_avatar_user_path(@user)
214
+
215
+ assert_not @user.reload.avatar.attached?
216
+ end
217
+ end
218
+ ```
219
+
220
+ ### Fixtures Setup
221
+
222
+ Place test files in `test/fixtures/files/`:
223
+ ```
224
+ test/fixtures/files/
225
+ ├── avatar.jpg
226
+ ├── document.pdf
227
+ └── photo.png
228
+ ```
229
+
230
+ ## Controller Handling
231
+
232
+ ```ruby
233
+ class UsersController < ApplicationController
234
+ def update
235
+ if @user.update(user_params)
236
+ redirect_to @user, notice: "Profile updated"
237
+ else
238
+ render :edit, status: :unprocessable_entity
239
+ end
240
+ end
241
+
242
+ def remove_avatar
243
+ @user.avatar.purge
244
+ redirect_to edit_user_path(@user), notice: "Avatar removed"
245
+ end
246
+
247
+ private
248
+
249
+ def user_params
250
+ params.require(:user).permit(:name, :email, :avatar)
251
+ end
252
+ end
253
+ ```
254
+
255
+ ### Multiple Uploads
256
+
257
+ ```ruby
258
+ def event_params
259
+ params.require(:event).permit(:name, photos: [], documents: [])
260
+ end
261
+ ```
262
+
263
+ ## Forms
264
+
265
+ ```erb
266
+ <%= form_with model: @user do |f| %>
267
+ <div>
268
+ <%= f.label :avatar %>
269
+ <%= f.file_field :avatar, accept: "image/png,image/jpeg,image/webp" %>
270
+
271
+ <% if @user.avatar.attached? %>
272
+ <%= image_tag @user.avatar.variant(:thumb), class: "rounded mt-2" %>
273
+ <% end %>
274
+ </div>
275
+ <%= f.submit %>
276
+ <% end %>
277
+ ```
278
+
279
+ ## Direct Uploads
280
+
281
+ ```javascript
282
+ // app/javascript/application.js
283
+ import * as ActiveStorage from "@rails/activestorage"
284
+ ActiveStorage.start()
285
+ ```
286
+
287
+ ```erb
288
+ <%= f.file_field :photos, multiple: true, direct_upload: true %>
289
+ ```
290
+
291
+ ## Performance Tips
292
+
293
+ ```ruby
294
+ # Prevent N+1 on attachments
295
+ User.with_attached_avatar.limit(10)
296
+
297
+ # Multiple attachments
298
+ Event.with_attached_photos.with_attached_documents
299
+ ```
300
+
301
+ ## Checklist
302
+
303
+ - [ ] Active Storage installed and migrated
304
+ - [ ] Storage service configured
305
+ - [ ] Image processing gem added
306
+ - [ ] Attachment added to model
307
+ - [ ] Validations added (type, size)
308
+ - [ ] Variants defined
309
+ - [ ] Controller permits attachment params
310
+ - [ ] Tests written for attachments
311
+ - [ ] All tests GREEN
@@ -0,0 +1,294 @@
1
+ ---
2
+ name: api-versioning
3
+ description: Implements RESTful API design with versioning and request tests. Use when building APIs, adding API endpoints, versioning APIs, or when user mentions REST, JSON API, or API design.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # API Versioning for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Well-structured APIs need versioning for backwards compatibility and clear organization.
12
+
13
+ **Recommended**: URL Path versioning (`/api/v1/users`)
14
+
15
+ ## Quick Setup
16
+
17
+ ### Routes
18
+
19
+ ```ruby
20
+ # config/routes.rb
21
+ Rails.application.routes.draw do
22
+ namespace :api do
23
+ namespace :v1 do
24
+ resources :users, only: [:index, :show, :create, :update, :destroy]
25
+ resources :events, only: [:index, :show, :create]
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ### Directory Structure
32
+
33
+ ```
34
+ app/controllers/
35
+ ├── api/
36
+ │ ├── base_controller.rb
37
+ │ ├── v1/
38
+ │ │ ├── base_controller.rb
39
+ │ │ ├── users_controller.rb
40
+ │ │ └── events_controller.rb
41
+ │ └── v2/
42
+ │ ├── base_controller.rb
43
+ │ └── users_controller.rb
44
+ ```
45
+
46
+ ### Base Controller
47
+
48
+ ```ruby
49
+ # app/controllers/api/base_controller.rb
50
+ module Api
51
+ class BaseController < ApplicationController
52
+ skip_before_action :verify_authenticity_token
53
+
54
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
55
+ rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
56
+ rescue_from ActionController::ParameterMissing, with: :bad_request
57
+
58
+ private
59
+
60
+ def not_found(exception)
61
+ render json: { error: exception.message }, status: :not_found
62
+ end
63
+
64
+ def unprocessable_entity(exception)
65
+ render json: { errors: exception.record.errors }, status: :unprocessable_entity
66
+ end
67
+
68
+ def bad_request(exception)
69
+ render json: { error: exception.message }, status: :bad_request
70
+ end
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### Version Base Controller
76
+
77
+ ```ruby
78
+ # app/controllers/api/v1/base_controller.rb
79
+ module Api
80
+ module V1
81
+ class BaseController < Api::BaseController
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### Resource Controller
88
+
89
+ ```ruby
90
+ # app/controllers/api/v1/users_controller.rb
91
+ module Api
92
+ module V1
93
+ class UsersController < BaseController
94
+ before_action :set_user, only: [:show, :update, :destroy]
95
+
96
+ def index
97
+ @users = User.page(params[:page]).per(25)
98
+ render json: { data: @users, meta: pagination_meta(@users) }
99
+ end
100
+
101
+ def show
102
+ render json: { data: @user }
103
+ end
104
+
105
+ def create
106
+ @user = User.create!(user_params)
107
+ render json: { data: @user }, status: :created
108
+ end
109
+
110
+ def update
111
+ @user.update!(user_params)
112
+ render json: { data: @user }
113
+ end
114
+
115
+ def destroy
116
+ @user.destroy
117
+ head :no_content
118
+ end
119
+
120
+ private
121
+
122
+ def set_user
123
+ @user = User.find(params[:id])
124
+ end
125
+
126
+ def user_params
127
+ params.require(:user).permit(:name, :email)
128
+ end
129
+
130
+ def pagination_meta(collection)
131
+ {
132
+ current_page: collection.current_page,
133
+ total_pages: collection.total_pages,
134
+ total_count: collection.total_count
135
+ }
136
+ end
137
+ end
138
+ end
139
+ end
140
+ ```
141
+
142
+ ## API Authentication
143
+
144
+ ### Bearer Token Auth
145
+
146
+ ```ruby
147
+ # app/controllers/api/base_controller.rb
148
+ module Api
149
+ class BaseController < ApplicationController
150
+ before_action :authenticate_api_user!
151
+
152
+ private
153
+
154
+ def authenticate_api_user!
155
+ token = request.headers["Authorization"]&.split(" ")&.last
156
+ @current_api_user = Session.find_by(token: token)&.user
157
+
158
+ render json: { error: "Unauthorized" }, status: :unauthorized unless @current_api_user
159
+ end
160
+
161
+ def current_api_user
162
+ @current_api_user
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ ## Response Format
169
+
170
+ ```json
171
+ // Success (single)
172
+ { "data": { "id": 1, "name": "John", "email": "john@example.com" } }
173
+
174
+ // Success (collection)
175
+ { "data": [...], "meta": { "current_page": 1, "total_pages": 10 } }
176
+
177
+ // Error
178
+ { "error": "Record not found" }
179
+
180
+ // Validation errors
181
+ { "errors": { "email": ["has already been taken"] } }
182
+ ```
183
+
184
+ ## Testing APIs (Minitest)
185
+
186
+ ### Request Test Template
187
+
188
+ ```ruby
189
+ # test/controllers/api/v1/users_controller_test.rb
190
+ require "test_helper"
191
+
192
+ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
193
+ setup do
194
+ @user = users(:one)
195
+ @headers = {
196
+ "Accept" => "application/json",
197
+ "Content-Type" => "application/json",
198
+ "Authorization" => "Bearer #{api_token_for(@user)}"
199
+ }
200
+ end
201
+
202
+ # -- index --
203
+ test "GET /api/v1/users returns all users" do
204
+ get "/api/v1/users", headers: @headers
205
+
206
+ assert_response :success
207
+ data = json_response["data"]
208
+ assert_kind_of Array, data
209
+ end
210
+
211
+ # -- show --
212
+ test "GET /api/v1/users/:id returns the user" do
213
+ get "/api/v1/users/#{@user.id}", headers: @headers
214
+
215
+ assert_response :success
216
+ assert_equal @user.id, json_response["data"]["id"]
217
+ end
218
+
219
+ test "GET /api/v1/users/:id returns 404 for missing user" do
220
+ get "/api/v1/users/999999", headers: @headers
221
+
222
+ assert_response :not_found
223
+ assert json_response["error"].present?
224
+ end
225
+
226
+ # -- create --
227
+ test "POST /api/v1/users creates a user" do
228
+ params = { user: { name: "New User", email: "new@example.com" } }
229
+
230
+ assert_difference("User.count", 1) do
231
+ post "/api/v1/users", params: params.to_json, headers: @headers
232
+ end
233
+
234
+ assert_response :created
235
+ end
236
+
237
+ test "POST /api/v1/users with invalid params returns errors" do
238
+ params = { user: { name: "", email: "" } }
239
+
240
+ assert_no_difference("User.count") do
241
+ post "/api/v1/users", params: params.to_json, headers: @headers
242
+ end
243
+
244
+ assert_response :unprocessable_entity
245
+ assert json_response["errors"].present?
246
+ end
247
+
248
+ # -- update --
249
+ test "PATCH /api/v1/users/:id updates the user" do
250
+ params = { user: { name: "Updated" } }
251
+
252
+ patch "/api/v1/users/#{@user.id}", params: params.to_json, headers: @headers
253
+
254
+ assert_response :success
255
+ assert_equal "Updated", @user.reload.name
256
+ end
257
+
258
+ # -- destroy --
259
+ test "DELETE /api/v1/users/:id destroys the user" do
260
+ assert_difference("User.count", -1) do
261
+ delete "/api/v1/users/#{@user.id}", headers: @headers
262
+ end
263
+
264
+ assert_response :no_content
265
+ end
266
+
267
+ # -- authentication --
268
+ test "returns 401 without token" do
269
+ get "/api/v1/users", headers: { "Accept" => "application/json" }
270
+
271
+ assert_response :unauthorized
272
+ end
273
+
274
+ private
275
+
276
+ def json_response
277
+ JSON.parse(response.body)
278
+ end
279
+
280
+ def api_token_for(user)
281
+ user.sessions.create!.token
282
+ end
283
+ end
284
+ ```
285
+
286
+ ## Checklist
287
+
288
+ - [ ] Routes namespaced under `api/v1`
289
+ - [ ] Base controller with error handling
290
+ - [ ] Authentication configured
291
+ - [ ] Standard response format
292
+ - [ ] Request tests written
293
+ - [ ] 404/422/401 error cases tested
294
+ - [ ] All tests GREEN