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,177 @@
1
+ # Rails: Query Objects
2
+
3
+ ## Pattern
4
+
5
+ Extract complex database queries into query objects when a scope chain becomes long, requires conditional logic, or involves joins and subqueries that obscure intent. Query objects live in `app/queries/` and return ActiveRecord relations so they remain chainable.
6
+
7
+ ```ruby
8
+ # app/queries/orders/search_query.rb
9
+ module Orders
10
+ class SearchQuery
11
+ def self.call(params)
12
+ new(params).call
13
+ end
14
+
15
+ def initialize(params)
16
+ @params = params
17
+ end
18
+
19
+ def call
20
+ scope = Order.includes(:user, :line_items)
21
+ scope = filter_by_status(scope)
22
+ scope = filter_by_date_range(scope)
23
+ scope = filter_by_total(scope)
24
+ scope = search_by_keyword(scope)
25
+ scope = apply_sorting(scope)
26
+ scope
27
+ end
28
+
29
+ private
30
+
31
+ def filter_by_status(scope)
32
+ return scope unless @params[:status].present?
33
+
34
+ scope.where(status: @params[:status])
35
+ end
36
+
37
+ def filter_by_date_range(scope)
38
+ scope = scope.where(created_at: @params[:from]..) if @params[:from].present?
39
+ scope = scope.where(created_at: ..@params[:to]) if @params[:to].present?
40
+ scope
41
+ end
42
+
43
+ def filter_by_total(scope)
44
+ scope = scope.where("total >= ?", @params[:min_total]) if @params[:min_total].present?
45
+ scope = scope.where("total <= ?", @params[:max_total]) if @params[:max_total].present?
46
+ scope
47
+ end
48
+
49
+ def search_by_keyword(scope)
50
+ return scope unless @params[:q].present?
51
+
52
+ scope.where("orders.reference ILIKE :q OR users.email ILIKE :q", q: "%#{@params[:q]}%")
53
+ .references(:user)
54
+ end
55
+
56
+ def apply_sorting(scope)
57
+ case @params[:sort]
58
+ when "newest" then scope.order(created_at: :desc)
59
+ when "oldest" then scope.order(created_at: :asc)
60
+ when "highest" then scope.order(total: :desc)
61
+ else scope.order(created_at: :desc)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ ```
67
+
68
+ The controller stays minimal:
69
+
70
+ ```ruby
71
+ class OrdersController < ApplicationController
72
+ def index
73
+ @orders = Orders::SearchQuery.call(search_params).page(params[:page])
74
+ end
75
+
76
+ private
77
+
78
+ def search_params
79
+ params.permit(:status, :from, :to, :min_total, :max_total, :q, :sort)
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Why This Is Good
85
+
86
+ - **Returns a relation.** The query object returns an ActiveRecord::Relation, not an array. You can chain `.page()`, `.limit()`, `.count()` on the result. It composes with the rest of Rails.
87
+ - **Each filter is isolated.** Adding a new filter means adding one private method. Removing a filter means removing one method. No risk of breaking other filters.
88
+ - **Testable without HTTP.** Pass in a params hash, assert the SQL or the returned records. Fast, focused tests.
89
+ - **Reusable.** The same query object works in the controller, in an API endpoint, in a CSV export job, and in an admin panel.
90
+ - **Readable intent.** `Orders::SearchQuery.call(params)` communicates what's happening. A 30-line scope chain in a controller does not.
91
+
92
+ ## Anti-Pattern
93
+
94
+ Building complex queries inline in the controller with conditional scope chaining:
95
+
96
+ ```ruby
97
+ class OrdersController < ApplicationController
98
+ def index
99
+ @orders = Order.includes(:user, :line_items)
100
+
101
+ if params[:status].present?
102
+ @orders = @orders.where(status: params[:status])
103
+ end
104
+
105
+ if params[:from].present?
106
+ @orders = @orders.where("created_at >= ?", params[:from])
107
+ end
108
+
109
+ if params[:to].present?
110
+ @orders = @orders.where("created_at <= ?", params[:to])
111
+ end
112
+
113
+ if params[:min_total].present?
114
+ @orders = @orders.where("total >= ?", params[:min_total])
115
+ end
116
+
117
+ if params[:q].present?
118
+ @orders = @orders.joins(:user)
119
+ .where("orders.reference ILIKE :q OR users.email ILIKE :q", q: "%#{params[:q]}%")
120
+ end
121
+
122
+ @orders = case params[:sort]
123
+ when "newest" then @orders.order(created_at: :desc)
124
+ when "oldest" then @orders.order(created_at: :asc)
125
+ when "highest" then @orders.order(total: :desc)
126
+ else @orders.order(created_at: :desc)
127
+ end
128
+
129
+ @orders = @orders.page(params[:page])
130
+ end
131
+ end
132
+ ```
133
+
134
+ ## Why This Is Bad
135
+
136
+ - **30+ lines of query logic in a controller.** The controller's job is HTTP handling, not query construction.
137
+ - **Impossible to reuse.** When the admin panel needs the same search, you copy-paste the entire block. When the API needs it, you copy it again. When the logic changes, you update it in three places.
138
+ - **Difficult to test.** Testing this requires making HTTP requests and asserting HTML or JSON output. You can't test the query logic in isolation.
139
+ - **Grows unbounded.** Every new filter adds another `if` block. Every new sort option adds a `when` clause. The controller action becomes the longest method in the codebase.
140
+
141
+ ## When To Apply
142
+
143
+ Use a query object when ANY of these are true:
144
+
145
+ - A query has **3 or more conditional filters** (status, date range, keyword, price range)
146
+ - The query involves **joins, subqueries, or raw SQL fragments** that obscure what's being queried
147
+ - The **same query logic is needed in multiple places** (web controller, API, admin, background job, export)
148
+ - The query is used for **reporting or analytics** (monthly revenue, user activity, conversion funnels)
149
+ - A model's scope chain is getting **longer than 3 chained scopes** for a single use case
150
+
151
+ ## When NOT To Apply
152
+
153
+ - **Simple, reusable filters belong as scopes on the model.** `Order.recent`, `Order.pending`, `Order.for_user(user)` are fine as scopes. They're short, reusable, and chainable.
154
+ - **Single-condition queries don't need a class.** `Order.where(status: :pending)` in a controller is perfectly fine. Don't extract a query object for one `where` clause.
155
+ - The query is **only used in one place** and is **under 5 lines.** A small inline query in a controller is more readable than navigating to a separate file.
156
+
157
+ ## Edge Cases
158
+
159
+ **Some filters should always be applied (like tenant scoping):**
160
+ Apply those in the constructor or at the top of `call`, not as conditional filters. Tenant scoping is not optional.
161
+
162
+ ```ruby
163
+ def call
164
+ scope = Order.where(company: @company) # Always applied
165
+ scope = filter_by_status(scope) # Conditionally applied
166
+ scope
167
+ end
168
+ ```
169
+
170
+ **The query needs to return raw data (not ActiveRecord objects):**
171
+ Use `.pluck`, `.select`, or `.to_sql` at the call site, not inside the query object. The query object returns a relation; the caller decides how to materialize it.
172
+
173
+ **You need both a count and the results:**
174
+ Return the relation. The caller chains `.count` or `.to_a` as needed. Don't build two methods that run nearly identical queries.
175
+
176
+ **The query is extremely complex (CTEs, window functions):**
177
+ Consider `Arel` for type-safe query construction, or use `.from(Arel.sql(...))` for raw SQL. Wrap it in the query object so the complexity is contained in one place. Add comments explaining the SQL.
@@ -0,0 +1,194 @@
1
+ # Rails: Routing
2
+
3
+ ## Pattern
4
+
5
+ Routes define your application's public API surface. Keep them RESTful, use resources for CRUD, create new controllers instead of custom actions, and use namespaces to organize related endpoints.
6
+
7
+ ### RESTful Resources
8
+
9
+ ```ruby
10
+ # config/routes.rb
11
+ Rails.application.routes.draw do
12
+ # GOOD: Standard RESTful resources
13
+ resources :orders, only: [:index, :show, :create, :update, :destroy]
14
+ resources :products, only: [:index, :show]
15
+
16
+ # GOOD: Nested resources for parent-child relationships
17
+ resources :orders do
18
+ resources :line_items, only: [:create, :destroy]
19
+ resource :shipment, only: [:show, :create] # singular — one shipment per order
20
+ end
21
+ # Generates: /orders/:order_id/line_items
22
+ # /orders/:order_id/shipment
23
+
24
+ # GOOD: Shallow nesting — child resources get their own top-level routes for show/edit/destroy
25
+ resources :projects, shallow: true do
26
+ resources :memberships, only: [:index, :create, :show, :destroy]
27
+ end
28
+ # Generates: /projects/:project_id/memberships (index, create)
29
+ # /memberships/:id (show, destroy)
30
+ end
31
+ ```
32
+
33
+ ### New Controllers Over Custom Actions
34
+
35
+ ```ruby
36
+ # BAD: Custom actions on a resource controller
37
+ resources :orders do
38
+ member do
39
+ post :cancel # POST /orders/:id/cancel
40
+ post :ship # POST /orders/:id/ship
41
+ post :refund # POST /orders/:id/refund
42
+ get :invoice # GET /orders/:id/invoice
43
+ get :tracking # GET /orders/:id/tracking
44
+ end
45
+ end
46
+
47
+ # GOOD: Each verb gets its own resource controller
48
+ resources :orders, only: [:index, :show, :create, :update] do
49
+ resource :cancellation, only: [:create], controller: "order_cancellations"
50
+ resource :shipment, only: [:show, :create], controller: "order_shipments"
51
+ resource :refund, only: [:create], controller: "order_refunds"
52
+ resource :invoice, only: [:show], controller: "order_invoices"
53
+ resource :tracking, only: [:show], controller: "order_trackings"
54
+ end
55
+ ```
56
+
57
+ Each new controller has a single RESTful action. `OrderCancellationsController#create` is clearer than `OrdersController#cancel`, and each controller stays skinny.
58
+
59
+ ### Namespaces, Scopes, and Modules
60
+
61
+ ```ruby
62
+ Rails.application.routes.draw do
63
+ # namespace: adds URL prefix AND module prefix
64
+ namespace :admin do
65
+ resources :users # Admin::UsersController, /admin/users
66
+ resources :orders # Admin::OrdersController, /admin/orders
67
+ root to: "dashboard#show"
68
+ end
69
+
70
+ # namespace for API versioning
71
+ namespace :api do
72
+ namespace :v1 do
73
+ resources :orders # Api::V1::OrdersController, /api/v1/orders
74
+ resources :projects do
75
+ resources :embeddings, only: [:index, :create]
76
+ end
77
+ namespace :ai do
78
+ post :refactor # Api::V1::Ai::RefactorController (if using Grape, mount instead)
79
+ post :review
80
+ post :spec
81
+ end
82
+ end
83
+ end
84
+
85
+ # scope: adds URL prefix but NOT module prefix
86
+ scope "/dashboard" do
87
+ resources :analytics, only: [:index] # AnalyticsController, /dashboard/analytics
88
+ end
89
+
90
+ # module: adds module prefix but NOT URL prefix
91
+ scope module: :public do
92
+ resources :products, only: [:index, :show] # Public::ProductsController, /products
93
+ end
94
+ end
95
+ ```
96
+
97
+ ### Constraints and Advanced Routing
98
+
99
+ ```ruby
100
+ Rails.application.routes.draw do
101
+ # Subdomain constraints
102
+ constraints subdomain: "api" do
103
+ namespace :api, path: "" do # api.rubyn.ai/v1/orders instead of api.rubyn.ai/api/v1/orders
104
+ namespace :v1 do
105
+ resources :orders
106
+ end
107
+ end
108
+ end
109
+
110
+ # Format constraints
111
+ resources :reports, only: [:show], defaults: { format: :json }
112
+
113
+ # Custom constraints
114
+ constraints ->(req) { req.env["HTTP_AUTHORIZATION"].present? } do
115
+ resources :admin_tools
116
+ end
117
+
118
+ # Catch-all for SPA (must be LAST)
119
+ get "*path", to: "application#frontend", constraints: ->(req) { !req.xhr? && req.format.html? }
120
+ end
121
+ ```
122
+
123
+ ### Route Helpers and Path Generation
124
+
125
+ ```ruby
126
+ # Use named routes — never hardcode paths
127
+ redirect_to order_path(@order) # /orders/123
128
+ redirect_to order_line_items_path(@order) # /orders/123/line_items
129
+ redirect_to [:admin, @user] # /admin/users/456
130
+ redirect_to new_order_path # /orders/new
131
+
132
+ # Polymorphic routing
133
+ redirect_to [@order, @line_item] # /orders/123/line_items/789
134
+
135
+ # URL helpers in non-controller contexts
136
+ Rails.application.routes.url_helpers.order_url(order, host: "rubyn.ai")
137
+ ```
138
+
139
+ ## Why This Is Good
140
+
141
+ - **RESTful resources are predictable.** Any Rails developer opening your routes file knows that `resources :orders` means 7 standard actions. Custom actions require reading each one.
142
+ - **New controllers keep actions skinny.** `OrderCancellationsController#create` has one job. `OrdersController#cancel` is a non-RESTful action hiding in a RESTful controller.
143
+ - **Namespaces organize by concern.** Admin routes, API routes, and public routes are clearly separated. Different authentication, different base controllers, different middleware.
144
+ - **Shallow nesting avoids deep URLs.** `/projects/1/memberships/2/permissions/3` is painful. Shallow nesting gives children their own top-level routes after creation.
145
+ - **`only:` keeps it explicit.** `resources :products, only: [:index, :show]` tells you exactly which endpoints exist. No guessing, no unused routes.
146
+
147
+ ## Anti-Pattern
148
+
149
+ ```ruby
150
+ # BAD: Everything on one controller, no nesting discipline
151
+ resources :orders do
152
+ collection do
153
+ get :search
154
+ get :export
155
+ get :report
156
+ end
157
+ member do
158
+ post :cancel
159
+ post :ship
160
+ post :approve
161
+ post :reject
162
+ post :archive
163
+ post :duplicate
164
+ get :pdf
165
+ get :receipt
166
+ end
167
+ end
168
+ # OrdersController now has 15+ actions
169
+ ```
170
+
171
+ ## When To Apply
172
+
173
+ - **Every Rails app.** RESTful routing is the Rails way. It's not optional.
174
+ - **When an action doesn't map to CRUD** — it's a new controller, not a custom action. "Cancel" is creating a cancellation. "Ship" is creating a shipment.
175
+ - **API versioning from day one.** `/api/v1/` costs nothing now and saves everything later.
176
+ - **`only:` on every `resources` call.** Don't generate routes you don't use.
177
+
178
+ ## When NOT To Apply
179
+
180
+ - **Sinatra apps.** Sinatra routes are explicit — no `resources` macro. Just define the routes you need.
181
+ - **Single-action controllers don't need resources.** A health check is `get "/health", to: "health#show"`, not `resources :health`.
182
+ - **Don't over-nest.** Never go deeper than 2 levels. `/orders/:id/line_items` is fine. `/companies/:id/orders/:id/line_items/:id/adjustments` is too deep — use shallow nesting or flatten.
183
+
184
+ ## Edge Cases
185
+
186
+ **Mounting engines and Rack apps:**
187
+
188
+ ```ruby
189
+ mount Rubyn::Engine => "/rubyn" if Rails.env.development?
190
+ mount Sidekiq::Web => "/sidekiq" if Rails.env.development?
191
+ mount ActionCable.server => "/cable"
192
+ ```
193
+
194
+ **Route precedence:** Routes are matched top to bottom. Put specific routes before generic ones, and catch-all routes last.
@@ -0,0 +1,187 @@
1
+ # Rails: ActiveRecord Scopes
2
+
3
+ ## Pattern
4
+
5
+ Scopes are named, reusable query fragments that return `ActiveRecord::Relation`. They chain, compose, and serve as the vocabulary for querying your domain. Design scopes like building blocks — small, focused, and combinable.
6
+
7
+ ```ruby
8
+ class Order < ApplicationRecord
9
+ # Status scopes — named after the state
10
+ scope :pending, -> { where(status: :pending) }
11
+ scope :confirmed, -> { where(status: :confirmed) }
12
+ scope :shipped, -> { where(status: :shipped) }
13
+ scope :completed, -> { where(status: %i[shipped delivered]) }
14
+ scope :active, -> { where.not(status: :cancelled) }
15
+
16
+ # Time scopes — named after the time frame
17
+ scope :recent, -> { where(created_at: 30.days.ago..) }
18
+ scope :today, -> { where(created_at: Date.current.all_day) }
19
+ scope :this_month, -> { where(created_at: Date.current.all_month) }
20
+ scope :before, ->(date) { where(created_at: ...date) }
21
+ scope :after, ->(date) { where(created_at: date..) }
22
+ scope :between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
23
+
24
+ # Relationship scopes — named after the association
25
+ scope :for_user, ->(user) { where(user: user) }
26
+ scope :for_product, ->(product) { joins(:line_items).where(line_items: { product: product }) }
27
+
28
+ # Value scopes — named after what they filter
29
+ scope :high_value, -> { where("total >= ?", 200_00) }
30
+ scope :above, ->(amount) { where("total >= ?", amount) }
31
+ scope :free_shipping, -> { where("total >= ?", 50_00) }
32
+
33
+ # Ordering scopes
34
+ scope :by_newest, -> { order(created_at: :desc) }
35
+ scope :by_total, -> { order(total: :desc) }
36
+ scope :by_status, -> { order(:status) }
37
+
38
+ # Inclusion scopes — preload associations for performance
39
+ scope :with_details, -> { includes(:user, :line_items, line_items: :product) }
40
+ scope :with_user, -> { includes(:user) }
41
+ end
42
+
43
+ # Compose scopes naturally — reads like a sentence
44
+ Order.for_user(current_user).pending.recent.by_newest
45
+ Order.confirmed.high_value.with_details.by_total
46
+ Order.active.this_month.for_product(widget)
47
+ ```
48
+
49
+ ### Scopes with Conditional Logic
50
+
51
+ ```ruby
52
+ class Product < ApplicationRecord
53
+ # Parameterized scope — nil-safe
54
+ scope :in_category, ->(category) { where(category: category) if category.present? }
55
+ scope :cheaper_than, ->(price) { where("price <= ?", price) if price.present? }
56
+ scope :search, ->(query) {
57
+ where("name ILIKE :q OR sku ILIKE :q", q: "%#{sanitize_sql_like(query)}%") if query.present?
58
+ }
59
+
60
+ # Scope that wraps a subquery
61
+ scope :with_recent_orders, -> {
62
+ where(id: LineItem.joins(:order).where(orders: { created_at: 30.days.ago.. }).select(:product_id))
63
+ }
64
+
65
+ # Scope using merge to combine conditions from another model's scope
66
+ scope :ordered_recently, -> {
67
+ joins(:line_items).merge(LineItem.joins(:order).merge(Order.recent)).distinct
68
+ }
69
+ end
70
+
71
+ # Nil-safe scopes chain gracefully — nil params are ignored
72
+ Product.in_category(params[:category]).cheaper_than(params[:max_price]).search(params[:q])
73
+ # If params[:category] is nil, that scope returns `all` — the chain continues
74
+ ```
75
+
76
+ ### Scopes vs Class Methods
77
+
78
+ ```ruby
79
+ class Order < ApplicationRecord
80
+ # SCOPE: Always returns a relation, even when the condition is nil
81
+ scope :by_status, ->(status) { where(status: status) if status.present? }
82
+ # When status is nil: returns `all` (chainable)
83
+
84
+ # CLASS METHOD: Can return nil, breaking the chain
85
+ def self.by_status(status)
86
+ return if status.blank? # Returns nil — .by_newest chained after this explodes
87
+ where(status: status)
88
+ end
89
+
90
+ # FIX: Class method that always returns a relation
91
+ def self.by_status(status)
92
+ return all if status.blank? # Returns scope, not nil
93
+ where(status: status)
94
+ end
95
+ end
96
+ ```
97
+
98
+ **Rule:** Use scopes for simple query fragments. Use class methods when you need complex logic (multiple lines, early returns, error handling) — but always return a relation or `all`/`none`, never `nil`.
99
+
100
+ ## Why This Is Good
101
+
102
+ - **Composable.** Each scope is a LEGO brick. Snap them together in any combination. `Order.pending.recent.high_value` generates one SQL query with three WHERE clauses.
103
+ - **Readable.** `Order.for_user(user).completed.this_month` reads like English. The equivalent raw SQL is harder to scan and impossible to reuse.
104
+ - **Chainable.** Scopes return `ActiveRecord::Relation`, so you can always chain more scopes, `.count`, `.page()`, `.pluck()`, `.exists?` after them.
105
+ - **Nil-safe.** A scope with `if condition.present?` returns `all` when the condition is false — the chain continues without breaking. This makes conditional filtering trivial.
106
+ - **Single source of truth.** "What does 'recent' mean?" is answered in one place — the scope definition. Not scattered across 8 controllers with slightly different `where` clauses.
107
+ - **Preloadable.** Scopes work with `includes`, `preload`, and `eager_load`. Query objects that return arrays don't.
108
+
109
+ ## Anti-Pattern
110
+
111
+ ```ruby
112
+ class Order < ApplicationRecord
113
+ # BAD: default_scope — poisons every query
114
+ default_scope { where(deleted: false) }
115
+ # Every Order.find, Order.count, Order.joins silently adds WHERE deleted = false
116
+ # Forgetting to unscope causes subtle bugs
117
+
118
+ # BAD: Scope that returns an array, not a relation
119
+ scope :totals, -> { pluck(:total) }
120
+ # Can't chain: Order.totals.pending → NoMethodError
121
+
122
+ # BAD: Scope with side effects
123
+ scope :expire_old, -> {
124
+ where(created_at: ...30.days.ago).update_all(status: :expired)
125
+ }
126
+ # Scopes should query, not mutate. This belongs in a service object.
127
+
128
+ # BAD: Overly complex scope that should be a query object
129
+ scope :dashboard_summary, -> {
130
+ select("status, COUNT(*) as count, SUM(total) as revenue")
131
+ .where(created_at: 30.days.ago..)
132
+ .where.not(status: :cancelled)
133
+ .group(:status)
134
+ .having("COUNT(*) > ?", 0)
135
+ .order("revenue DESC")
136
+ }
137
+ end
138
+ ```
139
+
140
+ ## When To Apply
141
+
142
+ - **Every reusable query condition** that's used in 2+ places. If two controllers filter by `pending`, that's a scope.
143
+ - **Parameterized filters.** `scope :for_user, ->(user)` is cleaner than `where(user: user)` repeated everywhere.
144
+ - **Ordering.** `scope :by_newest` is more expressive than `.order(created_at: :desc)` in every controller.
145
+ - **Eager loading bundles.** `scope :with_details` bundles the `includes` for a specific use case.
146
+
147
+ ## When NOT To Apply
148
+
149
+ - **Complex queries with 4+ joins, subqueries, or CTEs.** These belong in a Query Object, not a scope.
150
+ - **Queries with side effects.** Scopes should never `update_all`, send emails, or modify state. They read data.
151
+ - **One-off queries.** If a query is only used in one place and is simple (one `where` clause), inline it. Don't create a scope for everything.
152
+ - **Never use `default_scope`.** It silently affects every query on the model. Use explicit scopes and apply them where needed.
153
+
154
+ ## Edge Cases
155
+
156
+ **Merging scopes across models:**
157
+
158
+ ```ruby
159
+ # merge applies another model's scope in a join
160
+ Order.joins(:user).merge(User.active)
161
+ # WHERE users.active = true
162
+
163
+ # Useful for combining scopes from both sides of a join
164
+ Order.confirmed.joins(:user).merge(User.active).merge(User.pro_plan)
165
+ ```
166
+
167
+ **Scopes on associations:**
168
+
169
+ ```ruby
170
+ class User < ApplicationRecord
171
+ has_many :orders
172
+ has_many :pending_orders, -> { pending }, class_name: "Order"
173
+ has_many :recent_orders, -> { recent.by_newest }, class_name: "Order"
174
+ end
175
+
176
+ user.pending_orders # Preloadable scoped association
177
+ User.includes(:pending_orders) # Works with includes
178
+ ```
179
+
180
+ **`none` scope for empty results:**
181
+
182
+ ```ruby
183
+ def orders_for(user)
184
+ return Order.none unless user&.active? # Returns empty relation, still chainable
185
+ user.orders.active
186
+ end
187
+ ```