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
+ # Ruby: Pattern Matching
2
+
3
+ ## Pattern
4
+
5
+ Ruby 3.x introduced structural pattern matching with `case/in`. It destructures arrays, hashes, and objects, binds variables, and replaces complex conditional chains with declarative matching. Use it for API response handling, parsing, and multi-branch logic on complex data.
6
+
7
+ ### Basic Matching
8
+
9
+ ```ruby
10
+ # Match on value
11
+ case status
12
+ in "pending"
13
+ process_pending
14
+ in "shipped"
15
+ process_shipped
16
+ in "delivered" | "completed" # OR pattern
17
+ mark_complete
18
+ in String => unknown_status # Catch-all with binding
19
+ Rails.logger.warn("Unknown status: #{unknown_status}")
20
+ end
21
+
22
+ # Match on type
23
+ case value
24
+ in Integer => n if n.positive?
25
+ "Positive integer: #{n}"
26
+ in Float
27
+ "A float"
28
+ in String
29
+ "A string"
30
+ in nil
31
+ "Nothing"
32
+ end
33
+ ```
34
+
35
+ ### Hash Destructuring
36
+
37
+ ```ruby
38
+ # Parse API responses
39
+ response = { status: 200, body: { user: { name: "Alice", role: "admin", plan: "pro" } } }
40
+
41
+ case response
42
+ in { status: 200, body: { user: { role: "admin", name: String => name } } }
43
+ puts "Admin user: #{name}"
44
+ in { status: 200, body: { user: { plan: "pro", name: String => name } } }
45
+ puts "Pro user: #{name}"
46
+ in { status: 200, body: { user: { name: String => name } } }
47
+ puts "Standard user: #{name}"
48
+ in { status: (400..499) => code }
49
+ puts "Client error: #{code}"
50
+ in { status: (500..) => code }
51
+ puts "Server error: #{code}"
52
+ end
53
+
54
+ # One-line destructuring with =>
55
+ response => { body: { user: { name: } } }
56
+ puts name # => "Alice"
57
+
58
+ # Nested destructuring
59
+ webhook = { event: "order.shipped", data: { order_id: 42, tracking: "1Z999" } }
60
+ webhook => { event: /^order\.(.+)/ => event, data: { order_id: Integer => id } }
61
+ puts "Order #{id}: #{event}"
62
+ ```
63
+
64
+ ### Array Destructuring
65
+
66
+ ```ruby
67
+ # Head and tail
68
+ case [1, 2, 3, 4, 5]
69
+ in [first, *rest]
70
+ puts "First: #{first}, rest: #{rest}"
71
+ # First: 1, rest: [2, 3, 4, 5]
72
+ end
73
+
74
+ # Find pattern — match an element anywhere in the array
75
+ case ["info", "warning", "error: disk full", "info"]
76
+ in [*, /^error: (.+)/ => error_msg, *]
77
+ puts "Found error: #{error_msg}"
78
+ end
79
+
80
+ # Fixed structure
81
+ case [200, "OK", { content_type: "application/json" }]
82
+ in [200, String => msg, Hash => headers]
83
+ puts "Success: #{msg}"
84
+ in [(400..499) => code, String => msg, _]
85
+ puts "Client error #{code}: #{msg}"
86
+ end
87
+ ```
88
+
89
+ ### Pin Operator (Match Against Existing Variables)
90
+
91
+ ```ruby
92
+ expected_status = "shipped"
93
+
94
+ case order
95
+ in { status: ^expected_status } # ^ pins the variable — matches its VALUE, not a new binding
96
+ puts "Order is shipped!"
97
+ in { status: String => actual }
98
+ puts "Expected #{expected_status}, got #{actual}"
99
+ end
100
+
101
+ # Without ^, `expected_status` would be a new binding, not a comparison
102
+ ```
103
+
104
+ ### Guard Conditions
105
+
106
+ ```ruby
107
+ case order
108
+ in { total: Integer => amount } if amount > 100_00
109
+ apply_free_shipping(order)
110
+ in { total: Integer => amount } if amount > 50_00
111
+ apply_discount_shipping(order)
112
+ in { total: Integer }
113
+ apply_standard_shipping(order)
114
+ end
115
+ ```
116
+
117
+ ### Practical Rails Uses
118
+
119
+ ```ruby
120
+ # Webhook handler — clean multi-type dispatch
121
+ class Webhooks::StripeHandler
122
+ def call(event)
123
+ case event
124
+ in { type: "checkout.session.completed", data: { object: { customer: String => customer_id, amount_total: Integer => amount } } }
125
+ process_checkout(customer_id, amount)
126
+ in { type: "invoice.payment_failed", data: { object: { customer: String => customer_id } } }
127
+ handle_payment_failure(customer_id)
128
+ in { type: /^customer\.subscription\./, data: { object: { id: String => sub_id, status: String => status } } }
129
+ update_subscription(sub_id, status)
130
+ in { type: String => type }
131
+ Rails.logger.info("Unhandled webhook: #{type}")
132
+ end
133
+ end
134
+ end
135
+
136
+ # Service result handling
137
+ case Orders::CreateService.call(params, user)
138
+ in { success: true, order: Order => order }
139
+ redirect_to order
140
+ in { success: false, error: String => message }
141
+ flash.now[:alert] = message
142
+ render :new, status: :unprocessable_entity
143
+ end
144
+
145
+ # Config validation at boot
146
+ case Rails.application.credentials.config
147
+ in { anthropic: { api_key: String }, database: { url: String } }
148
+ # All required config present
149
+ in { anthropic: nil | { api_key: nil } }
150
+ raise "Missing Anthropic API key in credentials"
151
+ in { database: nil | { url: nil } }
152
+ raise "Missing database URL in credentials"
153
+ end
154
+ ```
155
+
156
+ ## Why This Is Good
157
+
158
+ - **Declarative over imperative.** `case/in` says WHAT you're looking for. Nested `if/elsif` chains say HOW to check.
159
+ - **Destructuring binds variables inline.** `{ user: { name: String => name } }` both validates the structure AND extracts the value in one expression.
160
+ - **Exhaustive matching catches missing cases.** If no pattern matches, Ruby raises `NoMatchingPatternError`. This catches unhandled types at runtime instead of silently returning nil.
161
+ - **Readable webhook/API handling.** Stripe webhooks have deeply nested JSON. Pattern matching handles them in 3 lines instead of 15.
162
+ - **Pin operator enables dynamic matching.** `^expected_value` matches against a variable's value without rebinding it.
163
+
164
+ ## When To Apply
165
+
166
+ - **Webhook handlers** — matching on event type and extracting nested data from JSON payloads.
167
+ - **API response parsing** — matching on status codes and body structure.
168
+ - **Multi-type dispatch** — when a method receives different shapes of input and must handle each differently.
169
+ - **Config validation** — asserting required structure exists at boot time.
170
+ - **Result object handling** — matching on success/failure with different payloads.
171
+
172
+ ## When NOT To Apply
173
+
174
+ - **Simple equality checks.** `case status when "pending"` is clearer than `case status in "pending"` for flat value matching. Use `case/when` for simple equality, `case/in` for structural matching.
175
+ - **Ruby < 3.0 projects.** Pattern matching is Ruby 3+ only. Check the project's `.ruby-version`.
176
+ - **Performance-critical hot paths.** Pattern matching is slightly slower than direct hash access. For code that runs millions of times, use `dig` / `fetch` directly.
177
+ - **When the team isn't familiar.** Pattern matching is powerful but unfamiliar to many Rubyists. If the team hasn't adopted it, don't introduce it in one file.
@@ -0,0 +1,166 @@
1
+ # Ruby: Regular Expressions
2
+
3
+ ## Pattern
4
+
5
+ Use regex for pattern matching, validation, and extraction — but prefer string methods when they suffice. Keep patterns readable with `x` flag for complex expressions, and use named captures for clarity.
6
+
7
+ ### Matching
8
+
9
+ ```ruby
10
+ # match? — boolean check, fastest (no MatchData allocation)
11
+ "ORD-12345".match?(/\AORD-\d+\z/) # => true
12
+ "hello@example.com".match?(URI::MailTo::EMAIL_REGEXP) # => true
13
+
14
+ # =~ — returns index of match or nil
15
+ "hello world" =~ /world/ # => 6
16
+ "hello world" =~ /xyz/ # => nil
17
+
18
+ # match — returns MatchData object (for captures)
19
+ md = "ORD-12345".match(/\AORD-(\d+)\z/)
20
+ md[1] # => "12345"
21
+
22
+ # String#scan — find all matches
23
+ "Order ORD-001 and ORD-002 shipped".scan(/ORD-\d+/)
24
+ # => ["ORD-001", "ORD-002"]
25
+ ```
26
+
27
+ ### Named Captures
28
+
29
+ ```ruby
30
+ # Named captures make regex self-documenting
31
+ pattern = /\A(?<prefix>ORD|INV)-(?<number>\d{6})-(?<year>\d{4})\z/
32
+ md = "ORD-000042-2026".match(pattern)
33
+ md[:prefix] # => "ORD"
34
+ md[:number] # => "000042"
35
+ md[:year] # => "2026"
36
+
37
+ # Ruby 3.2+ pattern matching with regex
38
+ case "ORD-000042-2026"
39
+ in /\AORD-(?<number>\d+)/ => ref
40
+ puts "Order #{ref}"
41
+ end
42
+
43
+ # Named captures assigned to local variables (magic behavior)
44
+ if /\A(?<name>\w+)@(?<domain>\w+\.\w+)\z/ =~ "alice@example.com"
45
+ puts name # => "alice"
46
+ puts domain # => "example.com"
47
+ end
48
+ ```
49
+
50
+ ### Common Patterns
51
+
52
+ ```ruby
53
+ # Email (use URI::MailTo::EMAIL_REGEXP instead of writing your own)
54
+ URI::MailTo::EMAIL_REGEXP
55
+
56
+ # UUID
57
+ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
58
+
59
+ # Phone (loose US format)
60
+ /\A\+?1?\d{10}\z/
61
+
62
+ # Semantic version
63
+ /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<pre>[a-zA-Z0-9.]+))?\z/
64
+
65
+ # IP address (v4, loose)
66
+ /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/
67
+ # Better: Use IPAddr.new(str) and rescue — regex doesn't validate 0-255 range
68
+
69
+ # Slug (URL-safe)
70
+ /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
71
+ ```
72
+
73
+ ### Verbose Mode for Complex Patterns
74
+
75
+ ```ruby
76
+ # x flag — whitespace and comments ignored, dramatically more readable
77
+ CREDIT_CARD = /\A
78
+ (?<type>
79
+ 4\d{12}(?:\d{3})? # Visa: starts with 4, 13 or 16 digits
80
+ | 5[1-5]\d{14} # Mastercard: starts with 51-55, 16 digits
81
+ | 3[47]\d{13} # Amex: starts with 34 or 37, 15 digits
82
+ | 6(?:011|5\d{2})\d{12} # Discover: starts with 6011 or 65, 16 digits
83
+ )
84
+ \z/x
85
+
86
+ # Without x flag — unreadable
87
+ CREDIT_CARD_UGLY = /\A(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|3[47]\d{13}|6(?:011|5\d{2})\d{12})\z/
88
+ ```
89
+
90
+ ### Substitution
91
+
92
+ ```ruby
93
+ # sub — first occurrence
94
+ "hello world world".sub(/world/, "Ruby") # => "hello Ruby world"
95
+
96
+ # gsub — all occurrences
97
+ "hello world world".gsub(/world/, "Ruby") # => "hello Ruby Ruby"
98
+
99
+ # gsub with block
100
+ "ORD-001 and ORD-002".gsub(/ORD-(\d+)/) { |match| "Order ##{$1}" }
101
+ # => "Order #001 and Order #002"
102
+
103
+ # gsub with hash
104
+ "cat and dog".gsub(/cat|dog/, "cat" => "feline", "dog" => "canine")
105
+ # => "feline and canine"
106
+
107
+ # Remove matching content
108
+ "Hello, World!".gsub(/[^a-zA-Z ]/, "") # => "Hello World"
109
+ ```
110
+
111
+ ### Performance
112
+
113
+ ```ruby
114
+ # Compile regex once with a constant — don't rebuild per call
115
+ EMAIL_PATTERN = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.freeze
116
+
117
+ # BAD: Regex rebuilt on every call
118
+ def valid_email?(email)
119
+ email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
120
+ end
121
+
122
+ # GOOD: Regex compiled once
123
+ def valid_email?(email)
124
+ email.match?(EMAIL_PATTERN)
125
+ end
126
+
127
+ # Prefer match? over =~ when you don't need captures
128
+ "test".match?(/\d/) # Fastest — no MatchData allocated
129
+ "test" =~ /\d/ # Slower — allocates MatchData
130
+ "test".match(/\d/) # Slowest — allocates MatchData object
131
+ ```
132
+
133
+ ## Why This Is Good
134
+
135
+ - **`match?` is fastest.** When you only need true/false, `match?` avoids allocating a MatchData object — 2-3x faster than `=~`.
136
+ - **Named captures are self-documenting.** `md[:year]` is clearer than `md[3]`. The reader doesn't need to count capture groups.
137
+ - **Verbose mode (`x`) makes complex patterns readable.** Comments explain each part. Whitespace groups related sections.
138
+ - **Constants avoid recompilation.** A regex literal in a method body is recompiled on every call. A frozen constant is compiled once.
139
+
140
+ ## Anti-Pattern
141
+
142
+ ```ruby
143
+ # BAD: Regex where a string method would do
144
+ email.match?(/example\.com/)
145
+ email.include?("example.com") # Simpler, faster, clearer
146
+
147
+ "hello world".match?(/\Ahello/)
148
+ "hello world".start_with?("hello") # No regex needed
149
+
150
+ name.gsub(/\s+/, " ")
151
+ name.squeeze(" ") # Collapses repeated spaces without regex
152
+ ```
153
+
154
+ ## When To Apply
155
+
156
+ - **Pattern validation** — emails, phone numbers, UUIDs, reference formats.
157
+ - **Extraction** — pulling structured data from strings (log parsing, URL matching).
158
+ - **Complex substitution** — replacing patterns with computed values.
159
+ - **Named captures** — whenever you have 2+ capture groups.
160
+
161
+ ## When NOT To Apply
162
+
163
+ - **Simple string checks.** `include?`, `start_with?`, `end_with?`, `==` are clearer and faster than regex for exact matches.
164
+ - **HTML/XML parsing.** Use Nokogiri, not regex. Regex can't handle nested structures.
165
+ - **Email validation in production.** Use `URI::MailTo::EMAIL_REGEXP` or better yet, just send a confirmation email — that's the real validation.
166
+ - **Complex parsing.** If the regex exceeds 3 lines even in verbose mode, consider a proper parser (StringScanner, Parslet, or a state machine).
@@ -0,0 +1,200 @@
1
+ # Ruby: Result Objects
2
+
3
+ ## Pattern
4
+
5
+ Instead of returning mixed types (record or nil, true or false) or raising exceptions for expected failures, return a Result object that explicitly represents success or failure. The caller inspects the result instead of rescuing exceptions.
6
+
7
+ ```ruby
8
+ # Simple Result using Data.define (Ruby 3.2+)
9
+ Result = Data.define(:success, :value, :error) do
10
+ def success? = success
11
+ def failure? = !success
12
+
13
+ def self.success(value)
14
+ new(success: true, value: value, error: nil)
15
+ end
16
+
17
+ def self.failure(error)
18
+ new(success: false, value: nil, error: error)
19
+ end
20
+ end
21
+ ```
22
+
23
+ ```ruby
24
+ # Service object using Result
25
+ class Orders::CreateService
26
+ def self.call(params, user)
27
+ order = user.orders.build(params)
28
+
29
+ unless order.valid?
30
+ return Result.failure(order.errors.full_messages.join(", "))
31
+ end
32
+
33
+ ActiveRecord::Base.transaction do
34
+ order.save!
35
+ OrderConfirmationJob.perform_later(order.id)
36
+ end
37
+
38
+ Result.success(order)
39
+ rescue ActiveRecord::RecordInvalid => e
40
+ Result.failure(e.message)
41
+ end
42
+ end
43
+
44
+ # The caller handles both cases explicitly — no rescue needed
45
+ result = Orders::CreateService.call(params, current_user)
46
+
47
+ if result.success?
48
+ redirect_to result.value, notice: "Order created."
49
+ else
50
+ flash.now[:alert] = result.error
51
+ render :new, status: :unprocessable_entity
52
+ end
53
+ ```
54
+
55
+ ### Result with Error Codes
56
+
57
+ ```ruby
58
+ # More structured for API responses
59
+ Result = Data.define(:success, :value, :error, :error_code) do
60
+ def success? = success
61
+ def failure? = !success
62
+
63
+ def self.success(value)
64
+ new(success: true, value: value, error: nil, error_code: nil)
65
+ end
66
+
67
+ def self.failure(error, code: :unknown)
68
+ new(success: false, value: nil, error: error, error_code: code)
69
+ end
70
+ end
71
+
72
+ class Credits::DeductionService
73
+ def self.call(user, amount)
74
+ if user.credit_balance < amount
75
+ return Result.failure("Insufficient credits. Balance: #{user.credit_balance}, needed: #{amount}",
76
+ code: :insufficient_credits)
77
+ end
78
+
79
+ if user.suspended?
80
+ return Result.failure("Account suspended", code: :account_suspended)
81
+ end
82
+
83
+ user.deduct_credits!(amount)
84
+ Result.success(user.credit_balance)
85
+ end
86
+ end
87
+
88
+ # API controller maps error codes to HTTP statuses
89
+ result = Credits::DeductionService.call(current_user, credits_needed)
90
+
91
+ unless result.success?
92
+ status = case result.error_code
93
+ when :insufficient_credits then :payment_required
94
+ when :account_suspended then :forbidden
95
+ else :unprocessable_entity
96
+ end
97
+ render json: { error: result.error }, status: status
98
+ return
99
+ end
100
+ ```
101
+
102
+ ### Result with Struct (Pre-Ruby 3.2)
103
+
104
+ ```ruby
105
+ Result = Struct.new(:success, :value, :error, keyword_init: true) do
106
+ def success? = success
107
+ def failure? = !success
108
+
109
+ def self.success(value)
110
+ new(success: true, value: value)
111
+ end
112
+
113
+ def self.failure(error)
114
+ new(success: false, error: error)
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Chaining Results (Railway-Oriented Programming)
120
+
121
+ ```ruby
122
+ class Orders::CheckoutPipeline
123
+ def self.call(params, user)
124
+ validate(params)
125
+ .then { |p| reserve_inventory(p) }
126
+ .then { |reservation| charge_payment(user, reservation) }
127
+ .then { |charge| create_order(user, params, charge) }
128
+ .then { |order| send_notifications(order) }
129
+ end
130
+
131
+ private
132
+
133
+ def self.validate(params)
134
+ return Result.failure("Missing address") if params[:address].blank?
135
+ Result.success(params)
136
+ end
137
+
138
+ def self.reserve_inventory(params)
139
+ reservation = Inventory::Reserve.call(params[:items])
140
+ reservation.success? ? Result.success(reservation) : Result.failure(reservation.error)
141
+ end
142
+
143
+ # Each step returns Result.success or Result.failure
144
+ # .then only executes if the previous result was success
145
+ end
146
+
147
+ # Add .then to Result
148
+ Result = Data.define(:success, :value, :error) do
149
+ def success? = success
150
+ def failure? = !success
151
+
152
+ def then
153
+ return self if failure?
154
+ yield(value)
155
+ end
156
+
157
+ def self.success(value) = new(success: true, value: value, error: nil)
158
+ def self.failure(error) = new(success: false, value: nil, error: error)
159
+ end
160
+ ```
161
+
162
+ ## Why This Is Good
163
+
164
+ - **Explicit over implicit.** The return type tells you both success and failure are possible. No surprise `nil` returns or unexpected exceptions.
165
+ - **The caller decides how to handle failure.** A controller renders an error page. A background job retries. A CLI prints a message. The service doesn't dictate error handling.
166
+ - **No exceptions for expected failures.** "Insufficient credits" is not exceptional — it's a normal business outcome. Exceptions should be for unexpected failures (database down, network timeout).
167
+ - **Chainable.** `.then` enables railway-oriented programming where the pipeline short-circuits on the first failure.
168
+ - **Testable.** Assert `result.success?` and `result.value` — clean, specific, no `assert_raises` for business logic failures.
169
+
170
+ ## Anti-Pattern
171
+
172
+ Mixed return types or exceptions for flow control:
173
+
174
+ ```ruby
175
+ # BAD: Returns an Order on success, a String on failure
176
+ def create_order(params)
177
+ order = Order.create!(params)
178
+ order
179
+ rescue ActiveRecord::RecordInvalid => e
180
+ e.message # Returns a String — caller must check type
181
+ end
182
+
183
+ # BAD: Raises for expected business failures
184
+ def deduct_credits(user, amount)
185
+ raise InsufficientCredits if user.balance < amount # Expected outcome, not exceptional
186
+ user.deduct!(amount)
187
+ end
188
+ ```
189
+
190
+ ## When To Apply
191
+
192
+ - **Every service object.** Services should return Results, not raise or return mixed types.
193
+ - **Operations with known failure modes.** Payment declined, insufficient credits, validation failed, rate limited — all expected outcomes.
194
+ - **Multi-step workflows.** Each step returns a Result. The pipeline short-circuits on failure.
195
+
196
+ ## When NOT To Apply
197
+
198
+ - **Simple model methods.** `order.total` returns a number. It doesn't need a Result wrapper.
199
+ - **Truly exceptional failures.** Database connection lost, out of memory, unexpected nil — these should raise exceptions. They're not business outcomes.
200
+ - **Single-line lookups.** `User.find(id)` raising `RecordNotFound` is fine — it's the Rails convention and rescued at the controller level.