rubyn-code 0.1.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 (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,160 @@
1
+ # Rails: ActionCable (WebSockets)
2
+
3
+ ## Pattern
4
+
5
+ ActionCable integrates WebSockets into Rails for real-time features — live chat, notifications, live updates, and collaborative editing. Use channels for bi-directional communication and Turbo Streams for server-pushed HTML updates.
6
+
7
+ ```ruby
8
+ # app/channels/application_cable/connection.rb
9
+ module ApplicationCable
10
+ class Connection < ActionCable::Connection::Base
11
+ identified_by :current_user
12
+
13
+ def connect
14
+ self.current_user = find_verified_user
15
+ end
16
+
17
+ private
18
+
19
+ def find_verified_user
20
+ if (user = User.find_by(id: cookies.encrypted[:user_id]))
21
+ user
22
+ else
23
+ reject_unauthorized_connection
24
+ end
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ```ruby
31
+ # app/channels/order_updates_channel.rb
32
+ class OrderUpdatesChannel < ApplicationCable::Channel
33
+ def subscribed
34
+ order = current_user.orders.find(params[:order_id])
35
+ stream_for order
36
+ end
37
+
38
+ def unsubscribed
39
+ # Cleanup when client disconnects
40
+ end
41
+ end
42
+
43
+ # Broadcasting from anywhere in the app
44
+ OrderUpdatesChannel.broadcast_to(order, {
45
+ type: "status_changed",
46
+ status: order.status,
47
+ updated_at: order.updated_at.iso8601
48
+ })
49
+ ```
50
+
51
+ ### Turbo Streams over ActionCable (The Rails 7+ Way)
52
+
53
+ ```ruby
54
+ # Model broadcasts — simplest approach
55
+ class Order < ApplicationRecord
56
+ after_create_commit -> { broadcast_prepend_to "orders", target: "orders_list" }
57
+ after_update_commit -> { broadcast_replace_to "orders" }
58
+ after_destroy_commit -> { broadcast_remove_to "orders" }
59
+ end
60
+
61
+ # Or broadcast from a service object (preferred — keeps model clean)
62
+ class Orders::ShipService
63
+ def call(order)
64
+ order.update!(status: :shipped, shipped_at: Time.current)
65
+
66
+ # Push update to all subscribers
67
+ Turbo::StreamsChannel.broadcast_replace_to(
68
+ "order_#{order.id}",
69
+ target: "order_#{order.id}",
70
+ partial: "orders/order",
71
+ locals: { order: order }
72
+ )
73
+
74
+ # Push to the orders list page too
75
+ Turbo::StreamsChannel.broadcast_replace_to(
76
+ "orders",
77
+ target: "order_#{order.id}",
78
+ partial: "orders/order_row",
79
+ locals: { order: order }
80
+ )
81
+ end
82
+ end
83
+ ```
84
+
85
+ ```erb
86
+ <%# View — subscribe to updates %>
87
+ <%= turbo_stream_from "orders" %>
88
+
89
+ <div id="orders_list">
90
+ <%= render @orders %>
91
+ </div>
92
+
93
+ <%# Individual order page %>
94
+ <%= turbo_stream_from "order_#{@order.id}" %>
95
+
96
+ <div id="order_<%= @order.id %>">
97
+ <%= render @order %>
98
+ </div>
99
+ ```
100
+
101
+ ### Custom Channel for Interactive Features
102
+
103
+ ```ruby
104
+ # app/channels/notifications_channel.rb
105
+ class NotificationsChannel < ApplicationCable::Channel
106
+ def subscribed
107
+ stream_for current_user
108
+ end
109
+ end
110
+
111
+ # Send notifications from anywhere
112
+ class NotificationService
113
+ def self.push(user, message:, type: :info)
114
+ NotificationsChannel.broadcast_to(user, {
115
+ type: type,
116
+ message: message,
117
+ timestamp: Time.current.iso8601
118
+ })
119
+ end
120
+ end
121
+
122
+ # Usage
123
+ NotificationService.push(user, message: "Your order shipped!", type: :success)
124
+ ```
125
+
126
+ ```javascript
127
+ // app/javascript/channels/notifications_channel.js
128
+ import consumer from "./consumer"
129
+
130
+ consumer.subscriptions.create("NotificationsChannel", {
131
+ received(data) {
132
+ const toast = document.createElement("div")
133
+ toast.className = `toast toast-${data.type}`
134
+ toast.textContent = data.message
135
+ document.getElementById("notifications").appendChild(toast)
136
+
137
+ setTimeout(() => toast.remove(), 5000)
138
+ }
139
+ })
140
+ ```
141
+
142
+ ## Why This Is Good
143
+
144
+ - **Turbo Streams over ActionCable is zero-JavaScript real-time.** Server pushes HTML, Turbo applies it. No custom JS for most use cases.
145
+ - **`broadcast_to` uses the model as the channel key.** `stream_for order` and `broadcast_to(order, ...)` — the channel routing is automatic and scoped.
146
+ - **Authentication via cookies.** The WebSocket connection inherits the user's session. No separate auth token needed for web apps.
147
+ - **Scales with Redis.** In production, ActionCable uses Redis as the pub/sub backend. Multiple app servers share the same broadcast channel.
148
+
149
+ ## When To Apply
150
+
151
+ - **Live updates** — order status changes, dashboard metrics, admin activity feeds.
152
+ - **Notifications** — real-time toasts, badge counts, alert banners.
153
+ - **Collaborative features** — shared editing, presence indicators, live cursors.
154
+ - **Turbo Stream broadcasts** — the simplest path. Use this before building custom channels.
155
+
156
+ ## When NOT To Apply
157
+
158
+ - **Polling works fine.** If data changes once per minute and freshness isn't critical, a 30-second poll is simpler than WebSockets.
159
+ - **API-only apps without a frontend.** Use webhooks or SSE instead.
160
+ - **High-frequency data streams** (stock tickers, game state at 60fps). ActionCable adds overhead per message — consider a dedicated WebSocket server.
@@ -0,0 +1,174 @@
1
+ # Rails: ActiveRecord Best Practices
2
+
3
+ ## Pattern
4
+
5
+ Use scopes for reusable query fragments, `find_by` over `where.first`, `exists?` over loading records to check presence, and `pluck` when you only need column values. Keep models focused on data access and validation, not business logic.
6
+
7
+ ```ruby
8
+ class Order < ApplicationRecord
9
+ # Scopes: named, chainable, readable
10
+ scope :recent, -> { where(created_at: 30.days.ago..) }
11
+ scope :pending, -> { where(status: :pending) }
12
+ scope :shipped, -> { where(status: :shipped) }
13
+ scope :for_user, ->(user) { where(user: user) }
14
+ scope :high_value, -> { where("total >= ?", 200) }
15
+ scope :by_newest, -> { order(created_at: :desc) }
16
+
17
+ # Scopes compose naturally
18
+ # Order.for_user(user).pending.recent.by_newest
19
+
20
+ # Efficient existence checks
21
+ def self.any_pending_for?(user)
22
+ for_user(user).pending.exists?
23
+ end
24
+
25
+ # Efficient counting
26
+ def self.total_revenue
27
+ sum(:total)
28
+ end
29
+
30
+ # Efficient value extraction
31
+ def self.recent_emails
32
+ recent.joins(:user).pluck("users.email")
33
+ end
34
+ end
35
+ ```
36
+
37
+ ```ruby
38
+ # CORRECT: Efficient queries
39
+ user = User.find_by(email: "alice@example.com") # Returns nil if not found
40
+ user = User.find_by!(email: "alice@example.com") # Raises RecordNotFound
41
+
42
+ order_exists = Order.where(user: user).exists? # SELECT 1 ... LIMIT 1
43
+ order_count = user.orders.pending.count # SELECT COUNT(*)
44
+ totals = Order.pending.pluck(:total) # SELECT total — returns array of values
45
+
46
+ # Batch processing for large datasets
47
+ Order.pending.find_each(batch_size: 500) do |order|
48
+ Orders::ProcessService.call(order)
49
+ end
50
+
51
+ # Bulk operations without instantiating records
52
+ Order.where(status: :draft, created_at: ..30.days.ago).delete_all
53
+ Order.pending.update_all(status: :cancelled, cancelled_at: Time.current)
54
+
55
+ # insert_all for bulk creation (Rails 6+)
56
+ Order.insert_all([
57
+ { user_id: 1, total: 100, status: :pending, created_at: Time.current, updated_at: Time.current },
58
+ { user_id: 2, total: 200, status: :pending, created_at: Time.current, updated_at: Time.current }
59
+ ])
60
+ ```
61
+
62
+ ## Why This Is Good
63
+
64
+ - **Scopes are chainable and composable.** `Order.pending.recent.high_value` reads like a sentence and generates a single SQL query. Each scope is a reusable building block.
65
+ - **`exists?` runs `SELECT 1 LIMIT 1`.** It doesn't load records into memory. Checking if a user has pending orders costs one lightweight query regardless of how many orders exist.
66
+ - **`pluck` skips model instantiation.** `Order.pluck(:total)` returns `[100, 200, 300]` without creating Order objects. For 10,000 records, this is dramatically faster and uses a fraction of the memory.
67
+ - **`find_each` prevents memory bloat.** Loading 100,000 orders with `.all.each` allocates all of them simultaneously. `find_each` loads 1,000 at a time (configurable) and GCs between batches.
68
+ - **`update_all` and `delete_all` execute single SQL statements.** No callbacks, no instantiation, no N individual UPDATE queries. For bulk operations on thousands of records, this is orders of magnitude faster.
69
+
70
+ ## Anti-Pattern
71
+
72
+ Loading full records when you only need a check, a count, or a column value:
73
+
74
+ ```ruby
75
+ # BAD: Loads ALL orders into memory to check if any exist
76
+ if user.orders.where(status: :pending).to_a.any?
77
+ # ...
78
+ end
79
+
80
+ # BAD: Loads ALL records to count them
81
+ total = Order.where(status: :pending).to_a.length
82
+
83
+ # BAD: Loads full AR objects to get one column
84
+ emails = User.where(active: true).map(&:email)
85
+
86
+ # BAD: where().first instead of find_by
87
+ user = User.where(email: "alice@example.com").first
88
+
89
+ # BAD: Processing large datasets without batching
90
+ Order.all.each do |order|
91
+ order.recalculate_total!
92
+ end
93
+
94
+ # BAD: N individual updates
95
+ Order.pending.each do |order|
96
+ order.update(status: :cancelled)
97
+ end
98
+
99
+ # BAD: default_scope — almost always a mistake
100
+ class Order < ApplicationRecord
101
+ default_scope { where(deleted: false) }
102
+ end
103
+ ```
104
+
105
+ ## Why This Is Bad
106
+
107
+ - **`.to_a.any?` loads every matching record.** 5,000 pending orders? That's 5,000 ActiveRecord objects instantiated, then thrown away after checking `any?`. `exists?` does the same check with zero objects loaded.
108
+ - **`.to_a.length` vs `.count`.** Loading 10,000 records to count them uses ~100MB of memory. `COUNT(*)` uses zero Ruby memory and returns instantly.
109
+ - **`.map(&:email)` instantiates every User.** For 50,000 users, that's 50,000 ActiveRecord objects in memory. `pluck(:email)` returns a simple array of strings with no model instantiation.
110
+ - **`.where().first` generates `ORDER BY id LIMIT 1`.** `find_by` generates `LIMIT 1` without the sort. On large tables without an index on the filter column, the sort is expensive.
111
+ - **Iterating without batching** loads the entire result set into memory at once. For large tables this can exhaust available RAM.
112
+ - **N individual updates** execute N separate UPDATE statements. Updating 1,000 orders takes 1,000 round trips to the database. `update_all` does it in one.
113
+ - **`default_scope` poisons every query.** Every `Order.find`, `Order.count`, `Order.joins` silently includes `WHERE deleted = false`. Forgetting to `unscope` it causes subtle bugs. Soft deletes should use explicit scopes or gems like `discard`.
114
+
115
+ ## When To Apply
116
+
117
+ - **Every ActiveRecord query should be as efficient as possible.** Use the cheapest operation that satisfies the need: `exists?` > `count` > `pluck` > `select` > loading full records.
118
+ - **Scopes for any query used in more than one place.** If two controllers filter by pending status, define `scope :pending`.
119
+ - **`find_each` for any iteration over more than 100 records.**
120
+ - **`update_all`/`delete_all` for bulk operations** where you don't need callbacks or validations.
121
+
122
+ ## When NOT To Apply
123
+
124
+ - **Small datasets where clarity wins.** If you have 10 records and `.map(&:name)` is more readable than `.pluck(:name)` in context, the performance difference is negligible.
125
+ - **When you need callbacks to fire.** `update_all` skips callbacks and validations. If the model's `after_update` callback must run, iterate and save individually (but consider whether the callback should be a service object instead).
126
+ - **Don't over-scope.** A scope used in exactly one place adds indirection without reuse benefit. An inline `where` is fine for one-off queries.
127
+
128
+ ## Edge Cases
129
+
130
+ **Scopes vs class methods:**
131
+ Scopes always return a relation (even when the condition is nil). Class methods can return nil, breaking chains.
132
+
133
+ ```ruby
134
+ # Scope: always chainable even when condition is nil
135
+ scope :by_status, ->(status) { where(status: status) if status.present? }
136
+
137
+ # Class method: can break the chain if it returns nil
138
+ def self.by_status(status)
139
+ return none unless status.present? # Must return a relation, not nil
140
+ where(status: status)
141
+ end
142
+ ```
143
+
144
+ **`select` vs `pluck`:**
145
+ `select` returns ActiveRecord objects with limited attributes. `pluck` returns raw arrays. Use `select` when you need methods on the model. Use `pluck` when you just need values.
146
+
147
+ ```ruby
148
+ Order.select(:id, :total).each { |o| o.total } # AR objects, can call methods
149
+ Order.pluck(:id, :total) # [[1, 100], [2, 200]] — raw arrays
150
+ ```
151
+
152
+ **Counter caches for frequently counted associations:**
153
+
154
+ ```ruby
155
+ # Migration
156
+ add_column :users, :orders_count, :integer, default: 0
157
+
158
+ # Model
159
+ class Order < ApplicationRecord
160
+ belongs_to :user, counter_cache: true
161
+ end
162
+
163
+ # Now user.orders_count is a column read, not a COUNT(*) query
164
+ ```
165
+
166
+ **`find_or_create_by` race conditions:**
167
+ Use `create_or_find_by` (Rails 6+) with a unique database constraint to handle concurrency:
168
+
169
+ ```ruby
170
+ # Safe under concurrency with a unique index on email
171
+ user = User.create_or_find_by(email: "alice@example.com") do |u|
172
+ u.name = "Alice"
173
+ end
174
+ ```
@@ -0,0 +1,242 @@
1
+ # Rails: Active Storage
2
+
3
+ ## Pattern
4
+
5
+ Active Storage handles file uploads in Rails — attaching files to models, processing variants (thumbnails, resizes), and storing them on local disk, S3, GCS, or Azure. Configure it once, use it through a clean model API.
6
+
7
+ ```ruby
8
+ # Setup
9
+ # rails active_storage:install
10
+ # rails db:migrate
11
+
12
+ # config/storage.yml
13
+ local:
14
+ service: Disk
15
+ root: <%= Rails.root.join("storage") %>
16
+
17
+ amazon:
18
+ service: S3
19
+ access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>
20
+ secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
21
+ region: us-east-1
22
+ bucket: rubyn-uploads
23
+
24
+ # config/environments/development.rb
25
+ config.active_storage.service = :local
26
+
27
+ # config/environments/production.rb
28
+ config.active_storage.service = :amazon
29
+ ```
30
+
31
+ ### Model Attachments
32
+
33
+ ```ruby
34
+ class User < ApplicationRecord
35
+ has_one_attached :avatar
36
+ has_one_attached :resume
37
+
38
+ # Validations (use activestorage-validator gem or custom)
39
+ validate :avatar_format
40
+
41
+ private
42
+
43
+ def avatar_format
44
+ return unless avatar.attached?
45
+
46
+ unless avatar.content_type.in?(%w[image/png image/jpeg image/webp])
47
+ errors.add(:avatar, "must be PNG, JPEG, or WebP")
48
+ end
49
+
50
+ if avatar.byte_size > 5.megabytes
51
+ errors.add(:avatar, "must be under 5MB")
52
+ end
53
+ end
54
+ end
55
+
56
+ class Order < ApplicationRecord
57
+ has_many_attached :documents # Multiple files
58
+
59
+ has_one_attached :invoice_pdf
60
+ end
61
+ ```
62
+
63
+ ### Controller and Form
64
+
65
+ ```ruby
66
+ class UsersController < ApplicationController
67
+ def update
68
+ @user = current_user
69
+
70
+ if @user.update(user_params)
71
+ redirect_to @user, notice: "Profile updated."
72
+ else
73
+ render :edit, status: :unprocessable_entity
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def user_params
80
+ params.require(:user).permit(:name, :email, :avatar)
81
+ end
82
+ end
83
+ ```
84
+
85
+ ```erb
86
+ <%# Form — standard file field, nothing special %>
87
+ <%= form_with model: @user do |f| %>
88
+ <%= f.file_field :avatar, accept: "image/png,image/jpeg,image/webp" %>
89
+
90
+ <% if @user.avatar.attached? %>
91
+ <%= image_tag @user.avatar.variant(resize_to_limit: [200, 200]) %>
92
+ <%= button_to "Remove", purge_avatar_user_path(@user), method: :delete %>
93
+ <% end %>
94
+
95
+ <%= f.submit "Save" %>
96
+ <% end %>
97
+ ```
98
+
99
+ ### Variants (Image Processing)
100
+
101
+ ```ruby
102
+ # Requires: gem "image_processing", "~> 1.2"
103
+
104
+ class User < ApplicationRecord
105
+ has_one_attached :avatar do |attachable|
106
+ attachable.variant :thumb, resize_to_fill: [100, 100]
107
+ attachable.variant :medium, resize_to_limit: [300, 300]
108
+ attachable.variant :large, resize_to_limit: [800, 800]
109
+ end
110
+ end
111
+
112
+ # Usage in views
113
+ <%= image_tag @user.avatar.variant(:thumb) %>
114
+ <%= image_tag @user.avatar.variant(:medium) %>
115
+
116
+ # Custom one-off variant
117
+ <%= image_tag @user.avatar.variant(resize_to_limit: [150, 150], format: :webp) %>
118
+
119
+ # Check before rendering
120
+ <% if @user.avatar.attached? %>
121
+ <%= image_tag @user.avatar.variant(:thumb) %>
122
+ <% else %>
123
+ <%= image_tag "default_avatar.png" %>
124
+ <% end %>
125
+ ```
126
+
127
+ ### Direct Uploads (Client-Side)
128
+
129
+ ```javascript
130
+ // app/javascript/application.js
131
+ import * as ActiveStorage from "@rails/activestorage"
132
+ ActiveStorage.start()
133
+ ```
134
+
135
+ ```erb
136
+ <%# Direct upload — file goes straight to storage, not through your server %>
137
+ <%= form.file_field :avatar, direct_upload: true %>
138
+ ```
139
+
140
+ Direct uploads send the file directly to S3/GCS from the browser. Your server only receives the signed blob ID, not the file bytes. This keeps your web server fast and avoids upload timeouts.
141
+
142
+ ### Service Objects for Complex Uploads
143
+
144
+ ```ruby
145
+ # When upload involves processing, validation, or multiple steps
146
+ class Documents::UploadService
147
+ def self.call(order, file)
148
+ new(order, file).call
149
+ end
150
+
151
+ def initialize(order, file)
152
+ @order = order
153
+ @file = file
154
+ end
155
+
156
+ def call
157
+ validate_file!
158
+ @order.documents.attach(@file)
159
+ process_document(@order.documents.last)
160
+ Result.new(success: true)
161
+ rescue ActiveStorage::IntegrityError => e
162
+ Result.new(success: false, error: "File corrupted: #{e.message}")
163
+ rescue DocumentTooLargeError => e
164
+ Result.new(success: false, error: e.message)
165
+ end
166
+
167
+ private
168
+
169
+ def validate_file!
170
+ raise DocumentTooLargeError, "File exceeds 25MB" if @file.size > 25.megabytes
171
+ end
172
+
173
+ def process_document(attachment)
174
+ # Extract text, generate preview, scan for viruses — async
175
+ DocumentProcessingJob.perform_later(attachment.id)
176
+ end
177
+ end
178
+ ```
179
+
180
+ ## Why This Is Good
181
+
182
+ - **One API for every storage backend.** Develop with local disk, deploy with S3. Change one line in config, not your code.
183
+ - **Variants are lazy.** `variant(:thumb)` doesn't process the image until it's first requested. After that, the processed variant is cached.
184
+ - **Direct uploads offload your server.** Large files go straight to S3 from the browser. Your Rails app never touches the bytes.
185
+ - **Attachment validations on the model.** File type and size checks happen before save, with standard error messages on the model.
186
+ - **Named variants are reusable.** Define `:thumb`, `:medium`, `:large` once on the model, use them everywhere in views.
187
+
188
+ ## Anti-Pattern
189
+
190
+ ```ruby
191
+ # BAD: Processing uploads in the controller
192
+ def create
193
+ file = params[:document]
194
+ File.open(Rails.root.join("uploads", file.original_filename), "wb") do |f|
195
+ f.write(file.read)
196
+ end
197
+ # Manual file management, no cleanup, no variants, no cloud storage
198
+
199
+ # BAD: Synchronous processing on upload
200
+ @user.avatar.attach(params[:avatar])
201
+ ImageOptimizer.new(@user.avatar).optimize! # Blocks the request for 5 seconds
202
+ ThumbnailGenerator.new(@user.avatar).generate! # Another 3 seconds
203
+ end
204
+ ```
205
+
206
+ ## When To Apply
207
+
208
+ - **Every file upload in a Rails app.** Active Storage replaces CarrierWave, Paperclip, and Shrine for most use cases.
209
+ - **User avatars, document uploads, image galleries.** Standard Active Storage with variants.
210
+ - **Large files (>10MB).** Use direct uploads to avoid tying up web workers.
211
+
212
+ ## When NOT To Apply
213
+
214
+ - **Extremely complex image processing pipelines.** If you need 20+ variant types, watermarking, face detection — consider Shrine or a dedicated image service.
215
+ - **Non-Rails apps.** Active Storage is Rails-only. Use Shrine or direct S3 SDK calls for Sinatra/plain Ruby.
216
+ - **Temporary file processing.** If you're processing a CSV and discarding it, don't attach it to a model. Just use `Tempfile`.
217
+
218
+ ## Edge Cases
219
+
220
+ **Purging attachments:**
221
+ ```ruby
222
+ @user.avatar.purge # Deletes synchronously
223
+ @user.avatar.purge_later # Deletes via background job (preferred)
224
+ ```
225
+
226
+ **Preloading to avoid N+1:**
227
+ ```ruby
228
+ # BAD: N+1 on avatars
229
+ users.each { |u| image_tag u.avatar } # Each avatar is a separate query
230
+
231
+ # GOOD: Preload
232
+ users = User.with_attached_avatar
233
+ ```
234
+
235
+ **Attaching from a URL:**
236
+ ```ruby
237
+ @user.avatar.attach(
238
+ io: URI.open("https://example.com/photo.jpg"),
239
+ filename: "photo.jpg",
240
+ content_type: "image/jpeg"
241
+ )
242
+ ```