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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- 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.
|