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,215 @@
1
+ # RSpec: Test Suite Performance
2
+
3
+ ## Pattern
4
+
5
+ A fast test suite runs in under 60 seconds for 1,000 specs. Achieve this by minimizing database hits, choosing the cheapest factory strategy per test, avoiding unnecessary setup, and profiling regularly.
6
+
7
+ Core strategies ranked by impact:
8
+
9
+ **1. Use `build_stubbed` wherever possible**
10
+
11
+ ```ruby
12
+ # FAST: Zero database hits
13
+ let(:user) { build_stubbed(:user) }
14
+ let(:order) { build_stubbed(:order, user: user, total: 100) }
15
+
16
+ it "calculates discount" do
17
+ expect(order.discounted_total).to eq(90)
18
+ end
19
+ ```
20
+
21
+ **2. Use `let` (lazy) instead of `let!` (eager)**
22
+
23
+ ```ruby
24
+ # FAST: Only creates records that are actually referenced
25
+ let(:user) { create(:user) }
26
+ let(:order) { create(:order, user: user) }
27
+
28
+ # Only the examples that call `order` pay for its creation
29
+ ```
30
+
31
+ **3. Minimize factory association chains**
32
+
33
+ ```ruby
34
+ # BAD: Creates user, company, plan, 3 line items, 3 products, 3 categories
35
+ let(:order) { create(:order, :with_line_items) }
36
+
37
+ # GOOD: Only what this test needs
38
+ let(:order) { build_stubbed(:order, total: 100) }
39
+ ```
40
+
41
+ **4. Use `before(:all)` / `before_all` for truly shared expensive setup**
42
+
43
+ ```ruby
44
+ # With the test-prof gem's before_all (transaction-safe)
45
+ before_all do
46
+ @reference_data = create(:pricing_table_with_100_rows)
47
+ end
48
+
49
+ # Standard before(:all) — use with caution, not wrapped in transaction
50
+ before(:all) do
51
+ @admin = create(:user, :admin)
52
+ end
53
+ ```
54
+
55
+ **5. Profile your test suite to find the bottlenecks**
56
+
57
+ ```bash
58
+ # Find the 10 slowest examples
59
+ bundle exec rspec --profile 10
60
+
61
+ # Find the slowest factories (requires test-prof gem)
62
+ FPROF=1 bundle exec rspec
63
+
64
+ # Find examples that make the most DB queries
65
+ EVENT_PROF=sql.active_record bundle exec rspec
66
+ ```
67
+
68
+ **6. Use `aggregate_failures` to reduce example count**
69
+
70
+ ```ruby
71
+ # SLOW: 4 separate examples, each with their own setup
72
+ it "has a reference" do
73
+ expect(order.reference).to be_present
74
+ end
75
+ it "has a status" do
76
+ expect(order.status).to eq("pending")
77
+ end
78
+ it "belongs to a user" do
79
+ expect(order.user).to eq(user)
80
+ end
81
+ it "has a created_at" do
82
+ expect(order.created_at).to be_present
83
+ end
84
+
85
+ # FAST: 1 example, same assertions, same error detail on failure
86
+ it "has the expected attributes", :aggregate_failures do
87
+ expect(order.reference).to be_present
88
+ expect(order.status).to eq("pending")
89
+ expect(order.user).to eq(user)
90
+ expect(order.created_at).to be_present
91
+ end
92
+ ```
93
+
94
+ **7. Parallelize with `parallel_tests`**
95
+
96
+ ```ruby
97
+ # Gemfile
98
+ gem 'parallel_tests', group: :development
99
+
100
+ # Run on 4 cores
101
+ bundle exec parallel_rspec -n 4
102
+
103
+ # Database setup for parallel
104
+ rake parallel:setup
105
+ ```
106
+
107
+ ## Why This Is Good
108
+
109
+ - **Developer productivity.** A 30-second test suite gets run after every change. A 10-minute suite gets run before commits, maybe. A 30-minute suite gets run in CI only. Fast feedback loops produce better code.
110
+ - **CI costs.** CI minutes cost money. A test suite that runs in 60 seconds vs 10 minutes is a 10x cost difference over thousands of builds per month.
111
+ - **Developer morale.** Nobody enjoys waiting. A slow test suite creates friction that makes developers skip tests, run subsets, or push untested code.
112
+ - **Catch failures faster.** When tests run in 30 seconds, you run them after every change and catch bugs immediately. When they take 10 minutes, you batch changes and debugging becomes harder.
113
+
114
+ ## Anti-Pattern
115
+
116
+ A test suite where every example creates a full object graph and runs expensive setup:
117
+
118
+ ```ruby
119
+ RSpec.describe Order do
120
+ let!(:company) { create(:company) }
121
+ let!(:admin) { create(:user, :admin, company: company) }
122
+ let!(:user) { create(:user, company: company) }
123
+ let!(:product_a) { create(:product, :with_inventory, company: company) }
124
+ let!(:product_b) { create(:product, :with_inventory, company: company) }
125
+ let!(:order) { create(:order, :with_line_items, user: user, item_count: 3) }
126
+
127
+ describe "#formatted_total" do
128
+ it "returns a currency string" do
129
+ # 15+ records created for a string formatting test
130
+ expect(order.formatted_total).to match(/\$\d+\.\d{2}/)
131
+ end
132
+ end
133
+
134
+ describe "#pending?" do
135
+ it "returns true when status is pending" do
136
+ # 15+ records created to test a boolean comparison
137
+ expect(order.pending?).to be true
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
143
+ ## Why This Is Bad
144
+
145
+ - **15+ INSERT statements per example.** With `let!`, every record is created before every example. 20 examples in this file = 300+ INSERTs.
146
+ - **90% of the setup is irrelevant.** `formatted_total` needs a number, not a company, admin, products, and inventory records.
147
+ - **Compounds across the suite.** If 50 spec files follow this pattern, the test suite creates 15,000+ unnecessary records per run.
148
+ - **Factory chains hide the cost.** `create(:order, :with_line_items)` looks like one record but cascades into order + user + company + 3 line items + 3 products = 9 INSERTs.
149
+
150
+ ## When To Apply
151
+
152
+ Always. Test performance should be a continuous concern, not an afterthought.
153
+
154
+ **Before writing any spec, ask:**
155
+ 1. Does this test need the database at all? → `build_stubbed`
156
+ 2. Does it need the database but not the full object graph? → `create` with minimal attributes
157
+ 3. Does it need a complex setup? → isolate the setup, share it via traits or `before_all`
158
+
159
+ **Regular maintenance:**
160
+ - Run `--profile 10` monthly to catch slow tests
161
+ - Install `test-prof` and run factory profiling when the suite slows down
162
+ - Set a CI budget: if the suite exceeds 2 minutes, investigate before adding more tests
163
+
164
+ ## When NOT To Apply
165
+
166
+ - **Don't prematurely optimize.** A 10-spec file that runs in 2 seconds doesn't need profiling or `build_stubbed` rewrites. Focus on the slow files first.
167
+ - **Don't sacrifice readability for speed.** If using `create` makes a test dramatically clearer than a `build_stubbed` with 5 `allow` stubs, use `create`. Clarity wins when the speed difference is negligible.
168
+ - **System/feature tests are inherently slower.** They launch a browser and interact with full pages. Optimize them by having fewer of them (test critical paths only) rather than by cutting database setup.
169
+
170
+ ## Edge Cases
171
+
172
+ **DatabaseCleaner strategies:**
173
+ Use `:transaction` strategy (default with RSpec Rails) for unit and request specs. Use `:truncation` or `:deletion` only for system specs that need JavaScript (and thus run outside the test transaction).
174
+
175
+ ```ruby
176
+ # spec/support/database_cleaner.rb
177
+ RSpec.configure do |config|
178
+ config.use_transactional_fixtures = true
179
+
180
+ config.before(:each, type: :system) do
181
+ DatabaseCleaner.strategy = :truncation
182
+ end
183
+
184
+ config.after(:each, type: :system) do
185
+ DatabaseCleaner.clean
186
+ end
187
+ end
188
+ ```
189
+
190
+ **Shared test data across a describe block:**
191
+ `test-prof`'s `before_all` creates data once per describe block (wrapped in a transaction), not once per example. This is safe and dramatically faster than `let!` for read-only reference data.
192
+
193
+ ```ruby
194
+ # Requires test-prof gem
195
+ before_all do
196
+ @products = create_list(:product, 50)
197
+ end
198
+
199
+ it "searches products" do
200
+ results = Product.search("widget")
201
+ expect(results).to include(@products.first)
202
+ end
203
+ ```
204
+
205
+ **Stubbing Time:**
206
+ Use `travel_to` instead of creating records with specific timestamps, then querying by date:
207
+
208
+ ```ruby
209
+ it "returns recent orders" do
210
+ travel_to(2.days.ago) { create(:order) }
211
+ recent = create(:order)
212
+
213
+ expect(Order.recent).to eq([recent])
214
+ end
215
+ ```
@@ -0,0 +1,204 @@
1
+ # Ruby: Blocks, Procs, and Lambdas
2
+
3
+ ## Pattern
4
+
5
+ Blocks are Ruby's most powerful feature — closures that capture their surrounding context and can be passed to methods. Understanding blocks, procs, and lambdas is essential for writing idiomatic Ruby.
6
+
7
+ ### Blocks
8
+
9
+ ```ruby
10
+ # A block is code between do/end or { } passed to a method
11
+ [1, 2, 3].each { |n| puts n }
12
+
13
+ [1, 2, 3].each do |n|
14
+ puts n
15
+ end
16
+
17
+ # Convention: { } for single-line, do/end for multi-line
18
+
19
+ # yield calls the block from inside the method
20
+ def with_logging
21
+ Rails.logger.info("Starting")
22
+ result = yield
23
+ Rails.logger.info("Completed")
24
+ result
25
+ end
26
+
27
+ with_logging { Order.create!(params) }
28
+
29
+ # block_given? checks if a block was passed
30
+ def find_or_default(collection, default: nil)
31
+ result = collection.find { |item| yield(item) }
32
+ result || default
33
+ end
34
+ ```
35
+
36
+ ### Blocks for Resource Management
37
+
38
+ ```ruby
39
+ # The block pattern guarantees cleanup — Ruby's most important idiom
40
+ File.open("data.csv") do |file|
41
+ file.each_line { |line| process(line) }
42
+ end
43
+ # File is automatically closed when the block exits, even on exception
44
+
45
+ # Build your own resource-managing methods
46
+ class DatabaseConnection
47
+ def self.with_connection
48
+ conn = checkout
49
+ yield conn
50
+ ensure
51
+ checkin(conn)
52
+ end
53
+ end
54
+
55
+ DatabaseConnection.with_connection do |conn|
56
+ conn.execute("SELECT * FROM orders")
57
+ end
58
+ ```
59
+
60
+ ### Procs and Lambdas
61
+
62
+ ```ruby
63
+ # Proc: a block saved as an object
64
+ doubler = Proc.new { |n| n * 2 }
65
+ doubler.call(5) # => 10
66
+ doubler.(5) # => 10 (shorthand)
67
+
68
+ # Lambda: a stricter proc (checks arity, return scoping)
69
+ doubler = ->(n) { n * 2 }
70
+ doubler.call(5) # => 10
71
+
72
+ # Symbol-to-proc: converts a method name to a proc
73
+ ["alice", "bob"].map(&:upcase) # => ["ALICE", "BOB"]
74
+ # Equivalent to: .map { |s| s.upcase }
75
+
76
+ # Method objects as procs
77
+ def double(n) = n * 2
78
+ [1, 2, 3].map(&method(:double)) # => [2, 4, 6]
79
+ ```
80
+
81
+ ### Proc vs Lambda Differences
82
+
83
+ ```ruby
84
+ # 1. Arity: Lambda checks argument count, Proc doesn't
85
+ my_lambda = ->(a, b) { a + b }
86
+ my_lambda.call(1) # ArgumentError: wrong number of arguments (given 1, expected 2)
87
+
88
+ my_proc = Proc.new { |a, b| (a || 0) + (b || 0) }
89
+ my_proc.call(1) # => 1 (b is nil, no error)
90
+
91
+ # 2. Return: Lambda returns to its caller, Proc returns from the enclosing method
92
+ def test_lambda
93
+ l = -> { return "from lambda" }
94
+ l.call
95
+ "after lambda" # This line executes
96
+ end
97
+ test_lambda # => "after lambda"
98
+
99
+ def test_proc
100
+ p = Proc.new { return "from proc" }
101
+ p.call
102
+ "after proc" # This line NEVER executes
103
+ end
104
+ test_proc # => "from proc"
105
+
106
+ # RULE: Use lambdas. They behave predictably.
107
+ ```
108
+
109
+ ### Practical Patterns
110
+
111
+ ```ruby
112
+ # Strategy via lambda
113
+ PRICING = {
114
+ standard: ->(amount) { amount },
115
+ premium: ->(amount) { amount * 0.9 },
116
+ vip: ->(amount) { amount * 0.8 }
117
+ }
118
+
119
+ def calculate_price(amount, tier:)
120
+ PRICING.fetch(tier, PRICING[:standard]).call(amount)
121
+ end
122
+
123
+ # Callbacks / hooks
124
+ class Pipeline
125
+ def initialize
126
+ @before_hooks = []
127
+ @after_hooks = []
128
+ end
129
+
130
+ def before(&block)
131
+ @before_hooks << block
132
+ end
133
+
134
+ def after(&block)
135
+ @after_hooks << block
136
+ end
137
+
138
+ def execute(data)
139
+ @before_hooks.each { |hook| hook.call(data) }
140
+ result = yield(data)
141
+ @after_hooks.each { |hook| hook.call(result) }
142
+ result
143
+ end
144
+ end
145
+
146
+ pipeline = Pipeline.new
147
+ pipeline.before { |data| puts "Processing: #{data}" }
148
+ pipeline.after { |result| puts "Done: #{result}" }
149
+ pipeline.execute("order-123") { |data| "Processed #{data}" }
150
+
151
+ # Filtering with lambdas
152
+ active = ->(user) { user.active? }
153
+ premium = ->(user) { user.plan == "pro" }
154
+ recent = ->(user) { user.created_at > 30.days.ago }
155
+
156
+ filters = [active, premium, recent]
157
+ users.select { |user| filters.all? { |f| f.call(user) } }
158
+
159
+ # Configuration DSLs
160
+ class Router
161
+ def initialize(&block)
162
+ @routes = {}
163
+ instance_eval(&block) if block
164
+ end
165
+
166
+ def get(path, &handler)
167
+ @routes[[:get, path]] = handler
168
+ end
169
+
170
+ def post(path, &handler)
171
+ @routes[[:post, path]] = handler
172
+ end
173
+ end
174
+
175
+ router = Router.new do
176
+ get "/health" do
177
+ { status: "ok" }
178
+ end
179
+
180
+ post "/orders" do
181
+ Order.create!(params)
182
+ end
183
+ end
184
+ ```
185
+
186
+ ## Why This Is Good
187
+
188
+ - **Blocks enable resource safety.** `File.open { }` guarantees cleanup. `ActiveRecord::Base.transaction { }` guarantees rollback on failure. This is more reliable than try/finally patterns.
189
+ - **Lambdas are first-class functions.** Store them in hashes, pass them as arguments, compose them. Ruby's functional programming capabilities are built on lambdas.
190
+ - **Symbol-to-proc is concise and expressive.** `.map(&:name)` is instantly readable by any Rubyist. It's not just shorter — it's clearer.
191
+ - **DSLs via `instance_eval`.** Blocks with `instance_eval` enable clean configuration DSLs (like Rails routes, RSpec, Sinatra).
192
+
193
+ ## When To Apply
194
+
195
+ - **Resource management** — always use blocks for open/close, start/stop, begin/end patterns.
196
+ - **Callbacks and hooks** — pass blocks to register behavior that runs at specific points.
197
+ - **Strategy selection** — lambdas in a hash for lightweight strategies that don't need a full class.
198
+ - **Iteration and transformation** — blocks with Enumerable methods are the heart of Ruby.
199
+
200
+ ## When NOT To Apply
201
+
202
+ - **Complex logic in a block.** If a block is longer than 5-7 lines, extract it into a method or a class. Blocks should be concise.
203
+ - **Procs for business logic.** Use lambdas, not procs. Proc's return behavior is surprising and error-prone.
204
+ - **Deep `instance_eval` nesting.** More than one level of `instance_eval` becomes hard to reason about. Keep DSLs shallow.
@@ -0,0 +1,155 @@
1
+ # Ruby: Class Design
2
+
3
+ ## Pattern
4
+
5
+ Design classes with a single responsibility, a clear public interface, and minimal exposure of internal state. Use `attr_reader` by default, only use `attr_accessor` when mutation is intentional, and freeze objects when immutability is desirable.
6
+
7
+ ```ruby
8
+ # GOOD: Clear responsibility, minimal interface, protected internals
9
+ class Money
10
+ attr_reader :amount_cents, :currency
11
+
12
+ def initialize(amount_cents, currency = "USD")
13
+ @amount_cents = Integer(amount_cents)
14
+ @currency = currency.to_s.upcase.freeze
15
+ freeze
16
+ end
17
+
18
+ def to_f
19
+ amount_cents / 100.0
20
+ end
21
+
22
+ def to_s
23
+ format("%.2f %s", to_f, currency)
24
+ end
25
+
26
+ def +(other)
27
+ raise ArgumentError, "Currency mismatch" unless currency == other.currency
28
+
29
+ self.class.new(amount_cents + other.amount_cents, currency)
30
+ end
31
+
32
+ def >(other)
33
+ raise ArgumentError, "Currency mismatch" unless currency == other.currency
34
+
35
+ amount_cents > other.amount_cents
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(self.class) &&
40
+ amount_cents == other.amount_cents &&
41
+ currency == other.currency
42
+ end
43
+ alias_method :eql?, :==
44
+
45
+ def hash
46
+ [amount_cents, currency].hash
47
+ end
48
+ end
49
+ ```
50
+
51
+ Constructor patterns:
52
+
53
+ ```ruby
54
+ # Named constructor for clarity
55
+ class Order
56
+ def self.from_cart(cart, user)
57
+ new(
58
+ user: user,
59
+ line_items: cart.items.map { |item| LineItem.new(product: item.product, quantity: item.quantity) },
60
+ shipping_address: user.default_address
61
+ )
62
+ end
63
+
64
+ def initialize(user:, line_items:, shipping_address:)
65
+ @user = user
66
+ @line_items = line_items
67
+ @shipping_address = shipping_address
68
+ end
69
+ end
70
+
71
+ # Usage reads like English
72
+ order = Order.from_cart(cart, current_user)
73
+ ```
74
+
75
+ ## Why This Is Good
76
+
77
+ - **`attr_reader` protects state.** External code can read `money.amount_cents` but can't set it. State changes only happen through explicit methods with names that communicate intent.
78
+ - **`freeze` enforces immutability.** A frozen Money object can't be accidentally mutated. Operations like `+` return new instances. This eliminates an entire class of bugs where shared references are modified in place.
79
+ - **Named constructors improve readability.** `Order.from_cart(cart, user)` is clearer than `Order.new(user: user, line_items: cart.items.map { ... })`. The constructor name describes the context.
80
+ - **`==` and `hash` make objects work in collections.** Two Money objects with the same amount and currency are equal, can be used as hash keys, and work with `uniq`, `include?`, and Set operations.
81
+ - **Keyword arguments in constructors.** `new(user:, line_items:, shipping_address:)` is self-documenting. You can't accidentally swap argument order.
82
+
83
+ ## Anti-Pattern
84
+
85
+ Classes with `attr_accessor` on everything, no encapsulation, and public state mutation:
86
+
87
+ ```ruby
88
+ class Order
89
+ attr_accessor :user, :items, :status, :total, :tax, :discount,
90
+ :shipping_address, :billing_address, :notes,
91
+ :created_at, :updated_at
92
+
93
+ def initialize
94
+ @items = []
95
+ @status = "pending"
96
+ end
97
+ end
98
+
99
+ # External code reaches in and mutates freely
100
+ order = Order.new
101
+ order.user = current_user
102
+ order.items << line_item
103
+ order.items << another_item
104
+ order.total = order.items.sum(&:price)
105
+ order.tax = order.total * 0.08
106
+ order.total = order.total + order.tax
107
+ order.status = "confirmed"
108
+ ```
109
+
110
+ ## Why This Is Bad
111
+
112
+ - **No encapsulation.** Any code anywhere can set `order.status = "shipped"` without any validation or side-effect management. The object can't protect its own invariants.
113
+ - **Scattered logic.** Total calculation happens outside the class. Tax calculation happens outside. The object is a passive data bag that external code manipulates.
114
+ - **Impossible to refactor.** Renaming `@total` to `@amount` requires finding every `order.total =` call across the entire codebase. With a method, you change one place.
115
+ - **No constructor contract.** `Order.new` creates an incomplete, invalid object. The caller must remember to set user, items, total, and tax in the correct order. Missing any step produces a broken object silently.
116
+
117
+ ## When To Apply
118
+
119
+ - **Every class you write.** Single responsibility and minimal public interface aren't optional patterns — they're baseline class design.
120
+ - **Value objects** (Money, DateRange, Coordinate, EmailAddress) should always be frozen and immutable.
121
+ - **Service objects and domain objects** should use `attr_reader` for dependencies and `private` for implementation details.
122
+ - **Use keyword arguments** when a constructor has more than 2 parameters, or when the parameters are the same type (two strings, two integers) and could be confused.
123
+
124
+ ## When NOT To Apply
125
+
126
+ - **ActiveRecord models** follow their own conventions. `attr_accessor` for virtual attributes is normal in Rails models. Don't fight the framework.
127
+ - **Structs and Data objects** use `attr_reader` automatically. You don't need to define them manually.
128
+ - **Configuration objects** that are built incrementally (builder pattern) may need `attr_writer` during construction, then frozen after.
129
+
130
+ ## Edge Cases
131
+
132
+ **You need a mutable object but want controlled mutation:**
133
+ Use explicit setter methods with validation instead of `attr_accessor`:
134
+
135
+ ```ruby
136
+ class Account
137
+ attr_reader :balance
138
+
139
+ def deposit(amount)
140
+ raise ArgumentError, "Amount must be positive" unless amount > 0
141
+ @balance += amount
142
+ end
143
+
144
+ def withdraw(amount)
145
+ raise InsufficientFunds if amount > @balance
146
+ @balance -= amount
147
+ end
148
+ end
149
+ ```
150
+
151
+ **Too many constructor arguments (more than 4-5):**
152
+ Consider a parameter object, a builder, or breaking the class into smaller collaborators. A constructor with 8 keyword arguments is a sign the class has too many responsibilities.
153
+
154
+ **Inheritance vs composition:**
155
+ Default to composition. If you're inheriting just to share code, use a module instead. Inherit only when there's a genuine "is-a" relationship and you want polymorphism.