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,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.
|