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,168 @@
|
|
|
1
|
+
# Ruby: Enumerable Patterns
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Choose the most expressive Enumerable method for the operation. Ruby provides specific methods for specific transformations — using the right one makes code self-documenting and often more performant.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
users = [user_a, user_b, user_c, user_d]
|
|
9
|
+
|
|
10
|
+
# TRANSFORMING: map when you want a new array of transformed elements
|
|
11
|
+
emails = users.map(&:email)
|
|
12
|
+
# => ["alice@example.com", "bob@example.com", ...]
|
|
13
|
+
|
|
14
|
+
# FILTERING: select/reject for keeping/removing elements
|
|
15
|
+
active_users = users.select(&:active?)
|
|
16
|
+
inactive_users = users.reject(&:active?)
|
|
17
|
+
|
|
18
|
+
# FINDING: find for the first match, detect is an alias
|
|
19
|
+
admin = users.find { |u| u.role == :admin }
|
|
20
|
+
|
|
21
|
+
# CHECKING: any?/all?/none? for boolean questions about the collection
|
|
22
|
+
has_admins = users.any? { |u| u.role == :admin }
|
|
23
|
+
all_confirmed = users.all?(&:confirmed?)
|
|
24
|
+
no_banned = users.none?(&:banned?)
|
|
25
|
+
|
|
26
|
+
# ACCUMULATING: each_with_object for building a new structure
|
|
27
|
+
users_by_role = users.each_with_object({}) do |user, hash|
|
|
28
|
+
(hash[user.role] ||= []) << user
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# COUNTING: tally for frequency counts (Ruby 2.7+)
|
|
32
|
+
role_counts = users.map(&:role).tally
|
|
33
|
+
# => { admin: 1, user: 3 }
|
|
34
|
+
|
|
35
|
+
# GROUPING: group_by for categorizing
|
|
36
|
+
by_plan = users.group_by(&:plan)
|
|
37
|
+
# => { free: [user_a, user_c], pro: [user_b, user_d] }
|
|
38
|
+
|
|
39
|
+
# FLATTENING + TRANSFORMING: flat_map when map would return nested arrays
|
|
40
|
+
all_orders = users.flat_map(&:orders)
|
|
41
|
+
# Instead of: users.map(&:orders).flatten
|
|
42
|
+
|
|
43
|
+
# SORTING: sort_by for sorting by a derived value
|
|
44
|
+
by_name = users.sort_by(&:name)
|
|
45
|
+
by_newest = users.sort_by(&:created_at).reverse
|
|
46
|
+
|
|
47
|
+
# CHUNKING: chunk for grouping consecutive elements
|
|
48
|
+
log_lines.chunk { |line| line.start_with?("ERROR") }.each do |is_error, lines|
|
|
49
|
+
report_errors(lines) if is_error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# INDEXING: index_by (Rails) or to_h for key-value lookup
|
|
53
|
+
users_by_id = users.index_by(&:id)
|
|
54
|
+
# => { 1 => user_a, 2 => user_b, ... }
|
|
55
|
+
|
|
56
|
+
# ZIPPING: zip for pairing elements from two arrays
|
|
57
|
+
names = ["Alice", "Bob"]
|
|
58
|
+
scores = [95, 87]
|
|
59
|
+
paired = names.zip(scores)
|
|
60
|
+
# => [["Alice", 95], ["Bob", 87]]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Why This Is Good
|
|
64
|
+
|
|
65
|
+
- **Self-documenting.** `users.select(&:active?)` reads like English. The method name tells you the intent — filtering. No comments needed.
|
|
66
|
+
- **No intermediate state.** Each method returns a new array (or enumerator). No temporary variables, no mutation, no `<< item` inside a loop.
|
|
67
|
+
- **Chainable.** Methods compose naturally: `users.select(&:active?).sort_by(&:name).map(&:email)` is a pipeline where each step is clear.
|
|
68
|
+
- **Performance.** Specific methods like `any?` short-circuit (stop iterating once the answer is known). `flat_map` avoids creating an intermediate nested array. `tally` is a single pass instead of `group_by` + `transform_values(&:count)`.
|
|
69
|
+
- **Symbol-to-proc shorthand.** `&:method_name` is idiomatic Ruby. Use it whenever the block is a single method call on the yielded element.
|
|
70
|
+
|
|
71
|
+
## Anti-Pattern
|
|
72
|
+
|
|
73
|
+
Using `each` with manual accumulation for everything:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Collecting results manually
|
|
77
|
+
emails = []
|
|
78
|
+
users.each do |user|
|
|
79
|
+
emails << user.email
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Filtering manually
|
|
83
|
+
active_users = []
|
|
84
|
+
users.each do |user|
|
|
85
|
+
if user.active?
|
|
86
|
+
active_users << user
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Building a hash manually
|
|
91
|
+
users_by_role = {}
|
|
92
|
+
users.each do |user|
|
|
93
|
+
if users_by_role[user.role]
|
|
94
|
+
users_by_role[user.role] << user
|
|
95
|
+
else
|
|
96
|
+
users_by_role[user.role] = [user]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Counting manually
|
|
101
|
+
admin_count = 0
|
|
102
|
+
users.each do |user|
|
|
103
|
+
admin_count += 1 if user.role == :admin
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Why This Is Bad
|
|
108
|
+
|
|
109
|
+
- **Verbose.** 4 lines for what `map` does in 1. Multiply this across a codebase and you have thousands of unnecessary lines.
|
|
110
|
+
- **Mutable state.** `emails = []` followed by `emails << ...` is imperative mutation. It's easy to accidentally push into the wrong array, skip the push, or modify the array elsewhere.
|
|
111
|
+
- **Hides intent.** Reading `emails = []` followed by a loop, you have to trace through the loop body to understand "oh, this is collecting emails." With `map(&:email)`, the intent is immediate.
|
|
112
|
+
- **Error-prone.** The manual hash building has a nil check that `group_by` handles automatically. The manual count is off-by-one prone. These bugs don't exist when you use the right method.
|
|
113
|
+
- **Not chainable.** The result is a variable, not a method return. You can't compose it with another operation without assigning to yet another variable.
|
|
114
|
+
|
|
115
|
+
## When To Apply
|
|
116
|
+
|
|
117
|
+
- **Always.** There is no case where manual `each` + accumulation is better than the appropriate Enumerable method. This is idiomatic Ruby — it's expected.
|
|
118
|
+
- **In ActiveRecord contexts** — prefer database operations (`pluck`, `where`, `group`, `count`) over loading records and using Ruby enumerables. But when you have the collection in memory already, use enumerables.
|
|
119
|
+
|
|
120
|
+
## When NOT To Apply
|
|
121
|
+
|
|
122
|
+
- **Don't chain excessively.** `users.select(&:active?).reject(&:banned?).sort_by(&:name).first(10).map(&:email)` is readable. Adding 3 more transformations is not. Break into named intermediate variables or methods if the chain exceeds 4-5 steps.
|
|
123
|
+
- **Don't use enumerables on large database sets.** `User.all.select(&:active?)` loads every user into memory then filters in Ruby. Use `User.where(active: true)` to filter in the database.
|
|
124
|
+
- **`each` is correct for side effects.** When the purpose is to DO something (send emails, update records, log output) rather than COMPUTE something, `each` is the right choice. Don't force `map` when you don't need the return value.
|
|
125
|
+
|
|
126
|
+
## Edge Cases
|
|
127
|
+
|
|
128
|
+
**`reduce` vs `each_with_object`:**
|
|
129
|
+
Use `each_with_object` for building hashes and arrays. Use `reduce` for computing a single value (sum, product). The difference: `reduce` requires you to return the accumulator from every block; `each_with_object` doesn't.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# each_with_object: cleaner for hash building
|
|
133
|
+
users.each_with_object({}) { |u, h| h[u.id] = u.name }
|
|
134
|
+
|
|
135
|
+
# reduce: cleaner for arithmetic
|
|
136
|
+
order.line_items.reduce(0) { |sum, item| sum + item.total }
|
|
137
|
+
|
|
138
|
+
# But for sums, just use .sum
|
|
139
|
+
order.line_items.sum(&:total)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**`map` + `compact` vs `filter_map`:**
|
|
143
|
+
Use `filter_map` (Ruby 2.7+) when the transformation might return nil and you want to skip nils:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Instead of
|
|
147
|
+
users.map { |u| u.profile&.avatar_url }.compact
|
|
148
|
+
|
|
149
|
+
# Use
|
|
150
|
+
users.filter_map { |u| u.profile&.avatar_url }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Lazy enumerables for large/infinite collections:**
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# Process a huge file without loading it all into memory
|
|
157
|
+
File.open("huge.csv").lazy.map { |line| parse(line) }.select(&:valid?).first(100)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**`each_slice` and `each_cons` for batching:**
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# Process in batches of 100
|
|
164
|
+
users.each_slice(100) { |batch| BulkEmailJob.perform_later(batch.map(&:id)) }
|
|
165
|
+
|
|
166
|
+
# Sliding window of 3
|
|
167
|
+
temperatures.each_cons(3) { |window| detect_trend(window) }
|
|
168
|
+
```
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Ruby: Exception Handling
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Rescue specific exceptions. Define custom exception hierarchies for your domain. Never use bare `rescue`. Use `retry` with limits. Always clean up resources in `ensure`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Define a custom exception hierarchy for your domain
|
|
9
|
+
module Rubyn
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class AuthenticationError < Error; end
|
|
13
|
+
class InsufficientCreditsError < Error; end
|
|
14
|
+
class RateLimitError < Error; end
|
|
15
|
+
|
|
16
|
+
class ApiError < Error
|
|
17
|
+
attr_reader :status_code, :response_body
|
|
18
|
+
|
|
19
|
+
def initialize(message, status_code:, response_body: nil)
|
|
20
|
+
@status_code = status_code
|
|
21
|
+
@response_body = response_body
|
|
22
|
+
super(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Rescue specific exceptions with appropriate handling
|
|
30
|
+
class Orders::CreateService
|
|
31
|
+
def call
|
|
32
|
+
order = build_order
|
|
33
|
+
charge_payment(order)
|
|
34
|
+
order.save!
|
|
35
|
+
send_confirmation(order)
|
|
36
|
+
Result.new(success: true, order: order)
|
|
37
|
+
rescue Stripe::CardError => e
|
|
38
|
+
# Specific: payment failed, tell the user
|
|
39
|
+
Result.new(success: false, error: "Payment declined: #{e.message}")
|
|
40
|
+
rescue Stripe::RateLimitError => e
|
|
41
|
+
# Specific: transient, retry makes sense
|
|
42
|
+
retry_or_fail(e)
|
|
43
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
44
|
+
# Specific: validation failed
|
|
45
|
+
Result.new(success: false, error: e.record.errors.full_messages.join(", "))
|
|
46
|
+
rescue Rubyn::InsufficientCreditsError
|
|
47
|
+
# Specific: domain error
|
|
48
|
+
Result.new(success: false, error: "Insufficient credits")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Retry pattern with exponential backoff and limit
|
|
55
|
+
def fetch_with_retry(url, max_retries: 3)
|
|
56
|
+
retries = 0
|
|
57
|
+
begin
|
|
58
|
+
response = Faraday.get(url)
|
|
59
|
+
raise Rubyn::ApiError.new("Server error", status_code: response.status) if response.status >= 500
|
|
60
|
+
response
|
|
61
|
+
rescue Faraday::TimeoutError, Rubyn::ApiError => e
|
|
62
|
+
retries += 1
|
|
63
|
+
raise if retries > max_retries
|
|
64
|
+
|
|
65
|
+
sleep_time = (2**retries) + rand(0.0..0.5) # Exponential backoff with jitter
|
|
66
|
+
Rails.logger.warn("Retry #{retries}/#{max_retries} after #{e.class}: sleeping #{sleep_time}s")
|
|
67
|
+
sleep(sleep_time)
|
|
68
|
+
retry
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Ensure for guaranteed cleanup
|
|
75
|
+
def process_file(path)
|
|
76
|
+
file = File.open(path, "r")
|
|
77
|
+
parse_contents(file.read)
|
|
78
|
+
rescue CSV::MalformedCSVError => e
|
|
79
|
+
Rails.logger.error("Malformed CSV: #{e.message}")
|
|
80
|
+
raise
|
|
81
|
+
ensure
|
|
82
|
+
file&.close
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Better: use block form which handles cleanup automatically
|
|
86
|
+
def process_file(path)
|
|
87
|
+
File.open(path, "r") do |file|
|
|
88
|
+
parse_contents(file.read)
|
|
89
|
+
end
|
|
90
|
+
rescue CSV::MalformedCSVError => e
|
|
91
|
+
Rails.logger.error("Malformed CSV: #{e.message}")
|
|
92
|
+
raise
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Why This Is Good
|
|
97
|
+
|
|
98
|
+
- **Specific rescues handle specific failures.** A `Stripe::CardError` gets a user-facing message. A `Stripe::RateLimitError` gets a retry. A bare `rescue` would handle both the same way — hiding the card error behind a generic "something went wrong."
|
|
99
|
+
- **Custom exceptions communicate domain intent.** `raise Rubyn::InsufficientCreditsError` is meaningful to anyone reading the code. `raise StandardError, "not enough credits"` is generic and uncatchable by type.
|
|
100
|
+
- **Exception hierarchies enable selective catching.** `rescue Rubyn::Error` catches all domain exceptions. `rescue Rubyn::AuthenticationError` catches only auth failures. The hierarchy gives callers the granularity they need.
|
|
101
|
+
- **Retry with backoff prevents cascading failures.** A transient network error triggers a retry with increasing delay, not an immediate failure or an infinite retry loop.
|
|
102
|
+
- **`ensure` guarantees cleanup.** File handles, database connections, and temporary resources are always released, even when an exception occurs.
|
|
103
|
+
|
|
104
|
+
## Anti-Pattern
|
|
105
|
+
|
|
106
|
+
Bare rescue, swallowed exceptions, and rescue-driven flow control:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# BAD: Bare rescue catches EVERYTHING including SyntaxError, NoMemoryError
|
|
110
|
+
def create_order(params)
|
|
111
|
+
order = Order.create!(params)
|
|
112
|
+
charge_payment(order)
|
|
113
|
+
order
|
|
114
|
+
rescue
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# BAD: Rescuing Exception (catches system signals, memory errors)
|
|
119
|
+
begin
|
|
120
|
+
dangerous_operation
|
|
121
|
+
rescue Exception => e
|
|
122
|
+
log(e.message)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# BAD: Using exceptions for flow control
|
|
126
|
+
def find_user(email)
|
|
127
|
+
User.find_by!(email: email)
|
|
128
|
+
rescue ActiveRecord::RecordNotFound
|
|
129
|
+
User.create!(email: email, name: "New User")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# BAD: Swallowing exceptions silently
|
|
133
|
+
def send_notification(user)
|
|
134
|
+
NotificationService.call(user)
|
|
135
|
+
rescue StandardError
|
|
136
|
+
# silently ignore all errors
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Why This Is Bad
|
|
141
|
+
|
|
142
|
+
- **Bare `rescue` catches `StandardError` and all subclasses.** This includes `NoMethodError`, `TypeError`, `NameError` — real bugs in your code that should crash loudly, not be silently swallowed. You're hiding bugs, not handling errors.
|
|
143
|
+
- **Rescuing `Exception` catches signals.** `Interrupt` (Ctrl+C), `SignalException` (kill), `NoMemoryError`, and `SyntaxError` are all subclasses of `Exception`. Rescuing them makes your program unkillable and masks fatal errors.
|
|
144
|
+
- **Exceptions for flow control are slow and misleading.** `find_by!` + `rescue RecordNotFound` is 10-100x slower than `find_by` + `nil?` check. Exceptions should be exceptional — unexpected failures, not expected branches.
|
|
145
|
+
- **Silently swallowed exceptions are invisible bugs.** When `NotificationService.call` fails, nobody knows. The user doesn't get notified, no error is logged, no alert fires. The bug exists silently until someone investigates why notifications stopped.
|
|
146
|
+
|
|
147
|
+
## When To Apply
|
|
148
|
+
|
|
149
|
+
- **Always rescue specific exception classes.** Name the exception class you expect. If you can't name it, you don't understand the failure mode well enough to handle it.
|
|
150
|
+
- **Custom exceptions for domain errors.** If your application has distinct failure modes (insufficient credits, rate limited, invalid API key), define exceptions for them.
|
|
151
|
+
- **Retry for transient failures only.** Network timeouts, rate limits, and temporary server errors are retriable. Validation errors, authentication failures, and business logic violations are not.
|
|
152
|
+
- **`ensure` for any resource that must be cleaned up.** Files, sockets, database connections, temporary directories. Or better — use block form methods that handle cleanup automatically (`File.open { }`, `ActiveRecord::Base.transaction { }`).
|
|
153
|
+
|
|
154
|
+
## When NOT To Apply
|
|
155
|
+
|
|
156
|
+
- **Don't rescue in every method.** Let exceptions propagate to the appropriate handler. A service object should raise; the controller or error middleware catches and renders the appropriate response.
|
|
157
|
+
- **Don't define custom exceptions for one-off cases.** If an exception is only raised in one place and caught in one place, `StandardError` with a message is sufficient. Custom exceptions shine when the same error type is raised or caught in multiple places.
|
|
158
|
+
- **Don't retry non-transient errors.** Retrying a `Stripe::CardError` (card declined) will fail every time. Retrying a `Stripe::RateLimitError` (temporary) makes sense.
|
|
159
|
+
|
|
160
|
+
## Edge Cases
|
|
161
|
+
|
|
162
|
+
**Re-raising after logging:**
|
|
163
|
+
Use `raise` with no arguments to re-raise the current exception after logging it:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
rescue Rubyn::ApiError => e
|
|
167
|
+
Rails.logger.error("API failed: #{e.message}")
|
|
168
|
+
raise # Re-raises the same exception with original backtrace
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Wrapping third-party exceptions:**
|
|
173
|
+
Convert external gem exceptions into your domain exceptions at the boundary:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
def fetch_data
|
|
177
|
+
ExternalApi.get("/data")
|
|
178
|
+
rescue ExternalApi::Timeout => e
|
|
179
|
+
raise Rubyn::ApiError.new("External service timed out", status_code: 504)
|
|
180
|
+
rescue ExternalApi::Unauthorized => e
|
|
181
|
+
raise Rubyn::AuthenticationError, "Invalid external API credentials"
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Multiple rescue clauses — order matters:**
|
|
186
|
+
Ruby checks rescue clauses top to bottom. Put specific exceptions before general ones:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
rescue Stripe::CardError => e # Specific first
|
|
190
|
+
handle_card_error(e)
|
|
191
|
+
rescue Stripe::StripeError => e # General parent second
|
|
192
|
+
handle_stripe_error(e)
|
|
193
|
+
rescue StandardError => e # Catch-all last
|
|
194
|
+
handle_unexpected_error(e)
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Exception in `ensure`:**
|
|
199
|
+
If `ensure` raises an exception, it replaces the original exception. Keep `ensure` blocks simple and safe. Wrap cleanup in its own begin/rescue if it might fail.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Ruby: File I/O
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Use block forms for automatic resource cleanup, choose the right I/O method for the data size, and prefer standard library parsers (CSV, JSON, YAML) over manual parsing.
|
|
6
|
+
|
|
7
|
+
### Reading Files
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# GOOD: Block form — file is automatically closed when block exits
|
|
11
|
+
File.open("data.txt") do |file|
|
|
12
|
+
file.each_line { |line| process(line) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# GOOD: Read entire file at once (small files only — loads into memory)
|
|
16
|
+
content = File.read("config.yml")
|
|
17
|
+
lines = File.readlines("data.txt", chomp: true) # Array of lines, newlines stripped
|
|
18
|
+
|
|
19
|
+
# GOOD: Stream large files line by line (constant memory)
|
|
20
|
+
File.foreach("huge_log.txt") do |line|
|
|
21
|
+
next unless line.include?("ERROR")
|
|
22
|
+
log_error(line)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GOOD: Read with encoding
|
|
26
|
+
content = File.read("data.csv", encoding: "UTF-8")
|
|
27
|
+
|
|
28
|
+
# BAD: Manual open without close
|
|
29
|
+
file = File.open("data.txt")
|
|
30
|
+
content = file.read
|
|
31
|
+
# file.close ← Easy to forget, especially if an exception occurs between open and close
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Writing Files
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# GOOD: Block form for writing
|
|
38
|
+
File.open("output.txt", "w") do |file|
|
|
39
|
+
file.puts "Line 1"
|
|
40
|
+
file.puts "Line 2"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# GOOD: Write entire string at once
|
|
44
|
+
File.write("output.txt", "Hello, world!")
|
|
45
|
+
File.write("log.txt", "New entry\n", mode: "a") # Append mode
|
|
46
|
+
|
|
47
|
+
# GOOD: Atomic write — prevents partial writes on crash (Rails)
|
|
48
|
+
require "fileutils"
|
|
49
|
+
# Write to temp file, then rename (atomic on most filesystems)
|
|
50
|
+
temp_path = "#{path}.tmp"
|
|
51
|
+
File.write(temp_path, content)
|
|
52
|
+
FileUtils.mv(temp_path, path)
|
|
53
|
+
|
|
54
|
+
# Rails provides this built-in:
|
|
55
|
+
File.atomic_write("config/settings.yml") do |file|
|
|
56
|
+
file.write(settings.to_yaml)
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Temporary Files
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
require "tempfile"
|
|
64
|
+
|
|
65
|
+
# GOOD: Tempfile with block — auto-deleted when block exits
|
|
66
|
+
Tempfile.create("report") do |temp|
|
|
67
|
+
temp.write(generate_csv_data)
|
|
68
|
+
temp.rewind
|
|
69
|
+
upload_to_s3(temp)
|
|
70
|
+
end
|
|
71
|
+
# File is deleted here
|
|
72
|
+
|
|
73
|
+
# GOOD: Tempfile with specific extension
|
|
74
|
+
Tempfile.create(["export", ".csv"]) do |temp|
|
|
75
|
+
temp.path # => "/tmp/export20260320-12345.csv"
|
|
76
|
+
CSV.open(temp.path, "w") do |csv|
|
|
77
|
+
csv << ["name", "email"]
|
|
78
|
+
users.each { |u| csv << [u.name, u.email] }
|
|
79
|
+
end
|
|
80
|
+
send_email_with_attachment(temp.path)
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### CSV
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "csv"
|
|
88
|
+
|
|
89
|
+
# Reading
|
|
90
|
+
CSV.foreach("orders.csv", headers: true) do |row|
|
|
91
|
+
Order.create!(
|
|
92
|
+
reference: row["reference"],
|
|
93
|
+
total: row["total"].to_i,
|
|
94
|
+
status: row["status"]
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reading into array of hashes
|
|
99
|
+
data = CSV.read("data.csv", headers: true).map(&:to_h)
|
|
100
|
+
|
|
101
|
+
# Writing
|
|
102
|
+
CSV.open("export.csv", "w") do |csv|
|
|
103
|
+
csv << %w[reference total status created_at]
|
|
104
|
+
orders.each do |order|
|
|
105
|
+
csv << [order.reference, order.total, order.status, order.created_at.iso8601]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generate CSV string (for send_data in controllers)
|
|
110
|
+
csv_string = CSV.generate do |csv|
|
|
111
|
+
csv << %w[name email plan]
|
|
112
|
+
users.each { |u| csv << [u.name, u.email, u.plan] }
|
|
113
|
+
end
|
|
114
|
+
send_data csv_string, filename: "users-#{Date.current}.csv"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### JSON
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
require "json"
|
|
121
|
+
|
|
122
|
+
# Parsing
|
|
123
|
+
data = JSON.parse(File.read("config.json"))
|
|
124
|
+
data = JSON.parse(response.body, symbolize_names: true) # Symbol keys
|
|
125
|
+
|
|
126
|
+
# Generating
|
|
127
|
+
json_string = { name: "Alice", orders: 5 }.to_json
|
|
128
|
+
pretty_json = JSON.pretty_generate({ name: "Alice", orders: 5 })
|
|
129
|
+
|
|
130
|
+
# Safe parsing (handle invalid JSON)
|
|
131
|
+
begin
|
|
132
|
+
data = JSON.parse(input)
|
|
133
|
+
rescue JSON::ParserError => e
|
|
134
|
+
Rails.logger.error("Invalid JSON: #{e.message}")
|
|
135
|
+
data = {}
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### YAML
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
require "yaml"
|
|
143
|
+
|
|
144
|
+
# SAFE: Permitted classes only (Ruby 3.1+ default)
|
|
145
|
+
config = YAML.safe_load_file("config.yml", permitted_classes: [Date, Time, Symbol])
|
|
146
|
+
|
|
147
|
+
# For Rails config files
|
|
148
|
+
config = YAML.safe_load(
|
|
149
|
+
ERB.new(File.read("config/database.yml")).result,
|
|
150
|
+
permitted_classes: [Symbol],
|
|
151
|
+
aliases: true
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Writing
|
|
155
|
+
File.write("output.yml", data.to_yaml)
|
|
156
|
+
|
|
157
|
+
# DANGEROUS: Never use YAML.load on untrusted input — it can execute arbitrary code
|
|
158
|
+
# YAML.load(user_input) ← SECURITY VULNERABILITY
|
|
159
|
+
# YAML.safe_load(user_input) ← SAFE
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Path Handling
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# GOOD: Use Pathname or File.join — never string concatenation for paths
|
|
166
|
+
require "pathname"
|
|
167
|
+
|
|
168
|
+
path = Pathname.new("app/models")
|
|
169
|
+
path / "order.rb" # => #<Pathname:app/models/order.rb>
|
|
170
|
+
path.join("concerns", "sluggable.rb") # => #<Pathname:app/models/concerns/sluggable.rb>
|
|
171
|
+
|
|
172
|
+
File.join("app", "models", "order.rb") # => "app/models/order.rb" (cross-platform)
|
|
173
|
+
|
|
174
|
+
# Useful Pathname methods
|
|
175
|
+
path = Pathname.new("app/models/order.rb")
|
|
176
|
+
path.exist? # => true
|
|
177
|
+
path.extname # => ".rb"
|
|
178
|
+
path.basename # => #<Pathname:order.rb>
|
|
179
|
+
path.dirname # => #<Pathname:app/models>
|
|
180
|
+
path.expand_path # => #<Pathname:/home/user/project/app/models/order.rb>
|
|
181
|
+
|
|
182
|
+
# BAD: String concatenation — breaks on different OS path separators
|
|
183
|
+
"app" + "/" + "models" + "/" + "order.rb"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Directory Operations
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# List files
|
|
190
|
+
Dir.glob("app/models/**/*.rb") # All .rb files recursively
|
|
191
|
+
Dir.glob("spec/**/*_spec.rb") # All spec files
|
|
192
|
+
Dir["app/services/*.rb"] # Shorthand for glob
|
|
193
|
+
|
|
194
|
+
# Create directories
|
|
195
|
+
FileUtils.mkdir_p("app/services/orders") # Creates intermediate dirs
|
|
196
|
+
|
|
197
|
+
# Check existence
|
|
198
|
+
File.exist?("app/models/order.rb")
|
|
199
|
+
File.directory?("app/services")
|
|
200
|
+
File.file?("Gemfile")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Why This Is Good
|
|
204
|
+
|
|
205
|
+
- **Block forms guarantee cleanup.** Files are closed even if exceptions occur. No resource leaks.
|
|
206
|
+
- **`File.foreach` streams.** Processing a 10GB log file uses constant memory, not 10GB.
|
|
207
|
+
- **Standard library parsers handle edge cases.** CSV with quoted commas, JSON with unicode escapes, YAML with anchors — don't parse these manually.
|
|
208
|
+
- **`YAML.safe_load` prevents RCE.** `YAML.load` can execute arbitrary Ruby code from crafted YAML. Always use `safe_load`.
|
|
209
|
+
- **`Pathname` is cross-platform.** No hardcoded `/` separators that break on Windows.
|
|
210
|
+
|
|
211
|
+
## When To Apply
|
|
212
|
+
|
|
213
|
+
- **Always use block forms** for `File.open`, `Tempfile.create`, `CSV.open`.
|
|
214
|
+
- **`File.foreach` for large files** (logs, data imports, CSVs over 1MB).
|
|
215
|
+
- **`File.read` for small files** (config, templates, under 1MB).
|
|
216
|
+
- **`YAML.safe_load` always** — never `YAML.load` on any input.
|
|
217
|
+
- **`JSON.parse` with rescue** — external JSON may be malformed.
|