rubyn-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,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.