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,195 @@
1
+ # Ruby: Hash Patterns
2
+
3
+ ## Pattern
4
+
5
+ Hashes are Ruby's most versatile data structure. Use the right access pattern for safety, the right transformation for clarity, and know when a Hash should become an object.
6
+
7
+ ### Safe Access
8
+
9
+ ```ruby
10
+ config = { database: { host: "localhost", port: 5432 }, redis: { url: "redis://localhost" } }
11
+
12
+ # GOOD: dig for nested access — returns nil instead of raising
13
+ config.dig(:database, :host) # => "localhost"
14
+ config.dig(:database, :timeout) # => nil (missing key)
15
+ config.dig(:missing, :nested) # => nil (missing parent)
16
+
17
+ # GOOD: fetch for required keys — raises if missing, or uses default
18
+ ENV.fetch("DATABASE_URL") # Raises KeyError if missing
19
+ ENV.fetch("OPTIONAL_KEY", "default_value") # Returns default if missing
20
+ ENV.fetch("PORT") { 3000 } # Block for computed default
21
+
22
+ config.fetch(:database) # Raises if :database doesn't exist
23
+ config.fetch(:timeout, 30) # Returns 30 if :timeout doesn't exist
24
+
25
+ # BAD: [] silently returns nil — hides bugs
26
+ config[:databas][:host] # NoMethodError: undefined method `[]' for nil
27
+ # Typo in :databas goes undetected until runtime crash
28
+
29
+ # RULE: Use fetch for required keys, dig for optional nested keys, [] only when nil is an acceptable value
30
+ ```
31
+
32
+ ### Transformation
33
+
34
+ ```ruby
35
+ data = { "user_name" => "Alice", "user_email" => "alice@example.com", "role" => "admin" }
36
+
37
+ # Symbolize keys
38
+ data.symbolize_keys # => { user_name: "Alice", user_email: "alice@example.com", role: "admin" }
39
+ # Rails method — in pure Ruby use: data.transform_keys(&:to_sym)
40
+
41
+ # Transform keys
42
+ data.transform_keys { |k| k.delete_prefix("user_") }
43
+ # => { "name" => "Alice", "email" => "alice@example.com", "role" => "admin" }
44
+
45
+ # Transform values
46
+ prices = { widget: 10_00, gadget: 25_00, gizmo: 50_00 }
47
+ prices.transform_values { |cents| "$#{format('%.2f', cents / 100.0)}" }
48
+ # => { widget: "$10.00", gadget: "$25.00", gizmo: "$50.00" }
49
+
50
+ # Slice — pick specific keys (Rails, or Ruby 2.5+)
51
+ user_params = params.slice(:name, :email, :phone)
52
+
53
+ # Except — remove specific keys (Rails)
54
+ safe_params = params.except(:admin, :role, :password_digest)
55
+
56
+ # Select / reject by key or value
57
+ prices.select { |_, v| v > 20_00 } # => { gadget: 25_00, gizmo: 50_00 }
58
+ prices.reject { |k, _| k == :gizmo } # => { widget: 10_00, gadget: 25_00 }
59
+
60
+ # Filter map (Ruby 2.7+)
61
+ prices.filter_map { |k, v| "#{k}: $#{v / 100.0}" if v > 15_00 }
62
+ # => ["gadget: $25.0", "gizmo: $50.0"]
63
+ ```
64
+
65
+ ### Merging
66
+
67
+ ```ruby
68
+ defaults = { timeout: 30, retries: 3, format: :json }
69
+ overrides = { timeout: 60, debug: true }
70
+
71
+ # merge — right side wins on conflicts
72
+ config = defaults.merge(overrides)
73
+ # => { timeout: 60, retries: 3, format: :json, debug: true }
74
+
75
+ # merge with block — resolve conflicts custom
76
+ counts_a = { orders: 10, users: 5 }
77
+ counts_b = { orders: 3, products: 8 }
78
+ counts_a.merge(counts_b) { |_key, a, b| a + b }
79
+ # => { orders: 13, users: 5, products: 8 }
80
+
81
+ # Deep merge (Rails) — merges nested hashes recursively
82
+ base = { database: { host: "localhost", pool: 5 } }
83
+ override = { database: { pool: 10, timeout: 30 } }
84
+ base.deep_merge(override)
85
+ # => { database: { host: "localhost", pool: 10, timeout: 30 } }
86
+
87
+ # Reverse merge (Rails) — "fill in defaults" — left side wins
88
+ user_options = { theme: "dark" }
89
+ user_options.reverse_merge(theme: "light", locale: "en", per_page: 25)
90
+ # => { theme: "dark", locale: "en", per_page: 25 }
91
+ # User's theme preserved, defaults filled in for missing keys
92
+
93
+ # With duplicate keys — ** (double splat) syntax
94
+ config = { **defaults, **overrides } # Same as defaults.merge(overrides)
95
+ ```
96
+
97
+ ### Building Hashes
98
+
99
+ ```ruby
100
+ users = [user_a, user_b, user_c]
101
+
102
+ # index_by (Rails) — build a lookup hash
103
+ users_by_id = users.index_by(&:id)
104
+ # => { 1 => user_a, 2 => user_b, 3 => user_c }
105
+
106
+ # group_by — group into arrays by key
107
+ users.group_by(&:role)
108
+ # => { "admin" => [user_a], "user" => [user_b, user_c] }
109
+
110
+ # tally (Ruby 2.7+) — count occurrences
111
+ %w[pending pending shipped delivered pending].tally
112
+ # => { "pending" => 3, "shipped" => 1, "delivered" => 1 }
113
+
114
+ # each_with_object — build a hash from iteration
115
+ users.each_with_object({}) do |user, hash|
116
+ hash[user.email] = user.name
117
+ end
118
+
119
+ # to_h with block (Ruby 2.6+)
120
+ users.to_h { |u| [u.id, u.name] }
121
+ # => { 1 => "Alice", 2 => "Bob", 3 => "Charlie" }
122
+
123
+ # zip to build from parallel arrays
124
+ keys = [:name, :email, :role]
125
+ values = ["Alice", "alice@example.com", "admin"]
126
+ keys.zip(values).to_h
127
+ # => { name: "Alice", email: "alice@example.com", role: "admin" }
128
+ ```
129
+
130
+ ### Pattern Matching with Hashes (Ruby 3+)
131
+
132
+ ```ruby
133
+ response = { status: 200, body: { user: { name: "Alice", role: "admin" } } }
134
+
135
+ case response
136
+ in { status: 200, body: { user: { role: "admin" } } }
137
+ puts "Admin user response"
138
+ in { status: 200, body: { user: { name: String => name } } }
139
+ puts "User: #{name}"
140
+ in { status: (400..499) => code }
141
+ puts "Client error: #{code}"
142
+ in { status: (500..) }
143
+ puts "Server error"
144
+ end
145
+
146
+ # Destructuring assignment
147
+ response => { body: { user: { name: } } }
148
+ puts name # => "Alice"
149
+ ```
150
+
151
+ ### When a Hash Should Become an Object
152
+
153
+ ```ruby
154
+ # SMELL: Hash with known, fixed keys passed around everywhere
155
+ def process_order(order_data)
156
+ validate(order_data[:address])
157
+ charge(order_data[:total], order_data[:payment_token])
158
+ notify(order_data[:email])
159
+ end
160
+
161
+ # Accessing order_data[:adress] (typo) returns nil silently
162
+
163
+ # FIX: Use a Data class or Struct
164
+ OrderRequest = Data.define(:address, :total, :payment_token, :email)
165
+
166
+ def process_order(request)
167
+ validate(request.address)
168
+ charge(request.total, request.payment_token)
169
+ notify(request.email)
170
+ end
171
+
172
+ # OrderRequest.new(adress: "...") → ArgumentError: unknown keyword: adress
173
+ # Typos caught at construction time, not buried in runtime nils
174
+ ```
175
+
176
+ ## Why This Is Good
177
+
178
+ - **`fetch` fails loudly on missing keys.** A typo in `config[:databse_url]` returns nil silently and crashes somewhere else. `config.fetch(:database_url)` raises immediately at the point of error.
179
+ - **`dig` handles nested nils gracefully.** No more `config[:database] && config[:database][:host]` chains. One method call.
180
+ - **Transformation methods are functional.** `transform_values`, `select`, `reject` return new hashes without mutating the original.
181
+ - **`index_by` and `tally` replace manual loops.** Building a lookup hash or counting occurrences is one method call, not a 4-line `each_with_object`.
182
+ - **Pattern matching makes hash destructuring readable.** Complex conditional logic on nested hashes becomes a clean `case/in`.
183
+
184
+ ## When To Apply
185
+
186
+ - **`fetch` for ENV variables.** Always. `ENV.fetch("API_KEY")` fails at boot if the key is missing, not at runtime when a request fails.
187
+ - **`dig` for API responses.** External API responses have unpredictable nesting. `response.dig(:data, :attributes, :name)` is safe.
188
+ - **`transform_keys/values` for data normalization.** API responses with string keys, webhook payloads with camelCase — normalize once at the boundary.
189
+ - **`to_h` with a block for building lookups.** Cleaner than `each_with_object` for simple key-value mappings.
190
+
191
+ ## When NOT To Apply
192
+
193
+ - **Don't use `fetch` when nil is a valid value.** If a config key is genuinely optional, use `dig` or `[]` with a nil check.
194
+ - **When the hash should be an object.** If you're passing the same hash shape to 3+ methods, it's a Data class or Struct waiting to happen.
195
+ - **Don't chain too many transformations.** `hash.symbolize_keys.slice(:a, :b).transform_values(&:to_i).merge(defaults)` — if the chain exceeds 3 steps, break it into named intermediate variables.
@@ -0,0 +1,170 @@
1
+ # Ruby: Metaprogramming
2
+
3
+ ## Pattern
4
+
5
+ Metaprogramming is writing code that writes code at runtime. Ruby is famous for it — `attr_accessor`, `has_many`, `validates`, and `scope` are all metaprogramming. Use it sparingly and intentionally: to eliminate repetition in DSLs and frameworks, never to be clever.
6
+
7
+ ### Safe Metaprogramming: `define_method`
8
+
9
+ ```ruby
10
+ # Generating similar methods from data
11
+ class Order < ApplicationRecord
12
+ # Instead of writing 5 nearly identical methods:
13
+ %w[pending confirmed shipped delivered cancelled].each do |status|
14
+ define_method("#{status}?") do
15
+ self.status == status
16
+ end
17
+
18
+ define_method("mark_#{status}!") do
19
+ update!(status: status, "#{status}_at": Time.current)
20
+ end
21
+ end
22
+ end
23
+
24
+ # Usage — these methods exist as if hand-written
25
+ order.pending? # true/false
26
+ order.mark_confirmed! # updates status and confirmed_at
27
+ ```
28
+
29
+ ### Safe Metaprogramming: `class_attribute` and Class Macros
30
+
31
+ ```ruby
32
+ # A class macro like Rails' has_many or validates
33
+ module HasCreditCost
34
+ extend ActiveSupport::Concern
35
+
36
+ class_methods do
37
+ def credit_cost(amount = nil, &block)
38
+ if block
39
+ define_method(:credit_cost) { instance_exec(&block) }
40
+ else
41
+ define_method(:credit_cost) { amount }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ class Ai::RefactorService
48
+ include HasCreditCost
49
+ credit_cost 2
50
+ end
51
+
52
+ class Ai::ReviewService
53
+ include HasCreditCost
54
+ credit_cost { file_content.length > 5000 ? 3 : 1 } # Dynamic cost
55
+ end
56
+
57
+ # Usage
58
+ Ai::RefactorService.new.credit_cost # => 2
59
+ Ai::ReviewService.new.credit_cost # => 1 or 3 depending on content
60
+ ```
61
+
62
+ ### Safe Metaprogramming: `method_missing` with `respond_to_missing?`
63
+
64
+ ```ruby
65
+ # Configuration object with dynamic attribute access
66
+ class Settings
67
+ def initialize(hash)
68
+ @data = hash.transform_keys(&:to_s)
69
+ end
70
+
71
+ def method_missing(name, *args)
72
+ key = name.to_s
73
+ if @data.key?(key)
74
+ value = @data[key]
75
+ value.is_a?(Hash) ? Settings.new(value) : value
76
+ else
77
+ super # CRITICAL: call super for unknown methods
78
+ end
79
+ end
80
+
81
+ def respond_to_missing?(name, include_private = false)
82
+ @data.key?(name.to_s) || super # CRITICAL: implement this
83
+ end
84
+
85
+ def to_h
86
+ @data
87
+ end
88
+ end
89
+
90
+ settings = Settings.new(
91
+ database: { host: "localhost", port: 5432 },
92
+ redis: { url: "redis://localhost:6379" }
93
+ )
94
+
95
+ settings.database.host # => "localhost"
96
+ settings.database.port # => 5432
97
+ settings.redis.url # => "redis://localhost:6379"
98
+ settings.unknown_key # => NoMethodError (falls through to super)
99
+ ```
100
+
101
+ ## Why This Is Good (When Used Correctly)
102
+
103
+ - **Eliminates repetitive code.** 5 status methods generated from an array is DRYer and less error-prone than 5 hand-written methods.
104
+ - **Enables clean DSLs.** `credit_cost 2` at the class level reads like configuration, not code. ActiveRecord's `validates :name, presence: true` is the same pattern.
105
+ - **Dynamic attribute access.** `settings.database.host` is more readable than `settings.dig("database", "host")` for deeply nested configs.
106
+
107
+ ## Anti-Pattern
108
+
109
+ Using metaprogramming to be clever, to avoid typing, or where plain Ruby would be clearer:
110
+
111
+ ```ruby
112
+ # BAD: Metaprogramming where a simple method would do
113
+ class User
114
+ %i[name email phone].each do |attr|
115
+ define_method("display_#{attr}") do
116
+ value = send(attr)
117
+ value.present? ? value : "N/A"
118
+ end
119
+ end
120
+ end
121
+
122
+ # BETTER: Just write the methods
123
+ class User
124
+ def display_name = name.presence || "N/A"
125
+ def display_email = email.presence || "N/A"
126
+ def display_phone = phone.presence || "N/A"
127
+ end
128
+ # 3 lines, immediately readable, greppable, no metaprogramming needed
129
+ ```
130
+
131
+ ```ruby
132
+ # BAD: method_missing without respond_to_missing?
133
+ class MagicHash
134
+ def method_missing(name, *args)
135
+ @data[name.to_s] # Everything silently returns nil for unknown keys
136
+ end
137
+ # Missing respond_to_missing? means is_a?, respond_to?, and inspect lie
138
+ end
139
+
140
+ # BAD: eval with user input (security vulnerability)
141
+ def dynamic_call(method_name, *args)
142
+ eval("object.#{method_name}(#{args.join(',')})") # NEVER do this
143
+ end
144
+
145
+ # SAFE: Use public_send instead
146
+ def dynamic_call(object, method_name, *args)
147
+ object.public_send(method_name, *args)
148
+ end
149
+ ```
150
+
151
+ ## Rules for Safe Metaprogramming
152
+
153
+ 1. **Always implement `respond_to_missing?`** when you implement `method_missing`. Otherwise `respond_to?`, `method`, and debugging tools lie.
154
+ 2. **Always call `super`** in `method_missing` for methods you don't handle. Otherwise all `NoMethodError`s are silently swallowed.
155
+ 3. **Never use `eval` with dynamic input.** Use `define_method`, `public_send`, or `const_get` instead. `eval` is a security hole.
156
+ 4. **Prefer `public_send` over `send`.** `send` bypasses `private` — use `public_send` to respect visibility.
157
+ 5. **Generate methods at load time, not call time.** `define_method` in the class body runs once. `method_missing` runs on every call and is slower.
158
+ 6. **If the generated methods would be fewer than 5, just write them by hand.** Metaprogramming for 3 methods adds complexity that's not worth the 2 lines saved.
159
+
160
+ ## When To Apply
161
+
162
+ - **Framework/library DSLs.** If you're building a gem that others configure (`has_many`, `validates`, `scope`), metaprogramming creates clean APIs.
163
+ - **Code generation from data.** Generating methods from a list of statuses, roles, or feature flags.
164
+ - **When you'd otherwise write 10+ identical methods.** At that point, a loop with `define_method` is legitimately DRYer and safer.
165
+
166
+ ## When NOT To Apply
167
+
168
+ - **Application code.** Business logic should be explicit, greppable, and debuggable. Metaprogramming makes all three harder.
169
+ - **When plain Ruby works.** If you can write 3-5 simple methods instead of metaprogramming, do it. Readable > clever.
170
+ - **Fewer than 5 repetitions.** The Rule of Three applies: don't abstract (or metaprogram) until you have enough examples to justify it.
@@ -0,0 +1,210 @@
1
+ # Ruby: Modules
2
+
3
+ ## Pattern
4
+
5
+ Modules serve two distinct purposes in Ruby: namespacing and behavior sharing. Use namespacing modules to organize related classes. Use mixins (`include`/`extend`/`prepend`) to share behavior across unrelated classes — but only when that behavior is truly reusable and doesn't couple the classes together.
6
+
7
+ **Namespacing — grouping related classes:**
8
+
9
+ ```ruby
10
+ # app/services/orders/create_service.rb
11
+ module Orders
12
+ class CreateService
13
+ def self.call(params, user)
14
+ new(params, user).call
15
+ end
16
+ # ...
17
+ end
18
+ end
19
+
20
+ # app/services/orders/cancel_service.rb
21
+ module Orders
22
+ class CancelService
23
+ def self.call(order, reason:)
24
+ new(order, reason:).call
25
+ end
26
+ # ...
27
+ end
28
+ end
29
+
30
+ # Usage: clear, organized, discoverable
31
+ Orders::CreateService.call(params, user)
32
+ Orders::CancelService.call(order, reason: "customer_request")
33
+ ```
34
+
35
+ **Behavior sharing — reusable capabilities:**
36
+
37
+ ```ruby
38
+ # app/models/concerns/sluggable.rb
39
+ module Sluggable
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ before_validation :generate_slug, on: :create
44
+ validates :slug, presence: true, uniqueness: true
45
+ end
46
+
47
+ def to_param
48
+ slug
49
+ end
50
+
51
+ private
52
+
53
+ def generate_slug
54
+ self.slug ||= name&.parameterize
55
+ end
56
+ end
57
+
58
+ # Used in unrelated models that share the same capability
59
+ class Product < ApplicationRecord
60
+ include Sluggable
61
+ end
62
+
63
+ class Category < ApplicationRecord
64
+ include Sluggable
65
+ end
66
+ ```
67
+
68
+ **`include` vs `extend` vs `prepend`:**
69
+
70
+ ```ruby
71
+ module Logging
72
+ def perform(*args)
73
+ Rails.logger.info("Starting #{self.class.name}")
74
+ result = super # Calls the original method
75
+ Rails.logger.info("Completed #{self.class.name}")
76
+ result
77
+ end
78
+ end
79
+
80
+ # prepend: Inserts BEFORE the class in the method lookup chain
81
+ # The module's method runs first, calls super to reach the class method
82
+ class ImportJob
83
+ prepend Logging
84
+
85
+ def perform(file_path)
86
+ CSV.foreach(file_path) { |row| process(row) }
87
+ end
88
+ end
89
+
90
+ # include: Inserts AFTER the class in the lookup chain
91
+ # Provides methods the class can call, but doesn't wrap existing methods
92
+ class User < ApplicationRecord
93
+ include Sluggable # Adds generate_slug, to_param to instances
94
+ end
95
+
96
+ # extend: Adds methods as CLASS methods, not instance methods
97
+ module Findable
98
+ def find_by_slug(slug)
99
+ find_by!(slug: slug)
100
+ end
101
+ end
102
+
103
+ class Product < ApplicationRecord
104
+ extend Findable # Product.find_by_slug("widget")
105
+ end
106
+ ```
107
+
108
+ ## Why This Is Good
109
+
110
+ - **Namespacing prevents collisions.** `Orders::CreateService` and `Users::CreateService` coexist cleanly. Without namespacing, you'd need `CreateOrderService` and `CreateUserService` — longer names, flatter structure.
111
+ - **Namespacing aids discovery.** `ls app/services/orders/` shows every operation available for orders. The file system mirrors the module structure.
112
+ - **Mixins share behavior without inheritance.** `Sluggable` can be included in Product, Category, and Article without any of them inheriting from a common base class. This avoids fragile inheritance hierarchies.
113
+ - **`prepend` enables clean wrapping.** Adding logging, caching, or instrumentation around a method without modifying the original class. `super` calls the original implementation.
114
+ - **`ActiveSupport::Concern` simplifies Rails mixins.** It handles `included` blocks, class methods, and dependency resolution cleanly.
115
+
116
+ ## Anti-Pattern
117
+
118
+ Using modules as dumping grounds for loosely related methods:
119
+
120
+ ```ruby
121
+ # app/models/concerns/order_helpers.rb
122
+ module OrderHelpers
123
+ extend ActiveSupport::Concern
124
+
125
+ def calculate_total
126
+ line_items.sum { |li| li.quantity * li.unit_price }
127
+ end
128
+
129
+ def apply_discount(code)
130
+ discount = Discount.find_by(code: code)
131
+ self.discount_amount = discount.calculate(total)
132
+ end
133
+
134
+ def send_confirmation
135
+ OrderMailer.confirmation(self).deliver_later
136
+ end
137
+
138
+ def sync_to_warehouse
139
+ WarehouseApi.new.sync(self)
140
+ end
141
+
142
+ def generate_invoice_pdf
143
+ InvoicePdfGenerator.new(self).generate
144
+ end
145
+
146
+ included do
147
+ after_create :send_confirmation
148
+ after_update :sync_to_warehouse, if: :saved_change_to_status?
149
+ end
150
+ end
151
+ ```
152
+
153
+ ## Why This Is Bad
154
+
155
+ - **Junk drawer module.** Calculation, discounts, email, warehouse API, and PDF generation are unrelated responsibilities dumped into one module. The module has no cohesive purpose.
156
+ - **Hidden the fat model problem.** Moving 50 lines from the model into a concern doesn't fix the design — it just hides the bloat in a different file. The model is still doing too much.
157
+ - **Tight coupling.** Any class that includes `OrderHelpers` gets email sending, warehouse syncing, and PDF generation — even if it only needed `calculate_total`.
158
+ - **Callbacks hiding in concerns.** `after_create :send_confirmation` is invisible when reading the model. The concern silently adds behavior that triggers on every create.
159
+ - **Not reusable.** Despite being a module, `OrderHelpers` only works with orders. No other model can include it. It's not actually shared behavior.
160
+
161
+ ## When To Apply
162
+
163
+ **Use namespacing modules when:**
164
+ - You have multiple classes that operate on the same domain concept (`Orders::CreateService`, `Orders::CancelService`, `Orders::SearchQuery`)
165
+ - You want to organize files in a directory structure that mirrors the module hierarchy
166
+ - You need to avoid class name collisions
167
+
168
+ **Use behavior-sharing modules when:**
169
+ - The behavior is genuinely used by 2+ unrelated classes (Sluggable, Searchable, Auditable)
170
+ - The behavior is self-contained — it doesn't depend on the including class having specific methods or attributes (beyond a clear, documented contract)
171
+ - The behavior is about capability ("this object is sluggable") not identity ("this object is an order")
172
+
173
+ **Use `prepend` when:**
174
+ - You need to wrap an existing method with before/after behavior (logging, caching, instrumentation, retry logic)
175
+ - You want `super` to call the original implementation
176
+
177
+ ## When NOT To Apply
178
+
179
+ - **Don't use modules to split a fat model into files.** If your model is 500 lines and you split it into 5 concerns of 100 lines, you still have a 500-line model — it's just harder to read because it's scattered across files.
180
+ - **Don't create a concern for behavior used by only one class.** A concern that's included in exactly one model is just indirection. Keep the methods in the model.
181
+ - **Don't use `extend` when you mean `include`.** Extending adds class methods. Including adds instance methods. Confusing them causes `NoMethodError` at runtime.
182
+ - **Don't use `module_function` in Rails concerns.** It makes methods both instance and module methods, which creates confusing dual interfaces.
183
+
184
+ ## Edge Cases
185
+
186
+ **Concern depends on the including class having specific attributes:**
187
+ Document the contract explicitly. Use a class method or a runtime check:
188
+
189
+ ```ruby
190
+ module Publishable
191
+ extend ActiveSupport::Concern
192
+
193
+ included do
194
+ raise "#{self} must have a published_at column" unless column_names.include?("published_at")
195
+
196
+ scope :published, -> { where.not(published_at: nil) }
197
+ scope :draft, -> { where(published_at: nil) }
198
+ end
199
+
200
+ def publish!
201
+ update!(published_at: Time.current)
202
+ end
203
+ end
204
+ ```
205
+
206
+ **Multiple modules define the same method:**
207
+ Ruby uses the method lookup chain. The last included module wins. Use `prepend` if you need explicit ordering with `super` delegation.
208
+
209
+ **When to use `ActiveSupport::Concern` vs plain modules:**
210
+ Use `Concern` in Rails apps when you need `included` blocks, class methods, or concern dependencies. Use plain modules in pure Ruby, gems, or when the module only adds instance methods.