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,194 @@
1
+ # Ruby: Concurrency
2
+
3
+ ## Pattern
4
+
5
+ Ruby has multiple concurrency primitives: Threads for I/O parallelism, Fibers for cooperative concurrency, and Ractors (Ruby 3+) for true parallelism. Choose the right tool for the workload.
6
+
7
+ ### Threads — Best for I/O-Bound Work
8
+
9
+ ```ruby
10
+ # Parallel HTTP requests — threads shine here because each waits on I/O
11
+ urls = %w[
12
+ https://api.example.com/orders
13
+ https://api.example.com/users
14
+ https://api.example.com/products
15
+ ]
16
+
17
+ results = urls.map do |url|
18
+ Thread.new(url) do |u|
19
+ Faraday.get(u).body
20
+ end
21
+ end.map(&:value) # .value blocks until the thread finishes
22
+
23
+ # Thread pool for controlled concurrency
24
+ require "concurrent-ruby"
25
+
26
+ pool = Concurrent::FixedThreadPool.new(5)
27
+ futures = urls.map do |url|
28
+ Concurrent::Future.execute(executor: pool) do
29
+ Faraday.get(url).body
30
+ end
31
+ end
32
+ results = futures.map(&:value)
33
+ ```
34
+
35
+ ```ruby
36
+ # Thread-safe shared state with Mutex
37
+ class Counter
38
+ def initialize
39
+ @count = 0
40
+ @mutex = Mutex.new
41
+ end
42
+
43
+ def increment
44
+ @mutex.synchronize { @count += 1 }
45
+ end
46
+
47
+ def value
48
+ @mutex.synchronize { @count }
49
+ end
50
+ end
51
+
52
+ counter = Counter.new
53
+ threads = 10.times.map do
54
+ Thread.new { 1000.times { counter.increment } }
55
+ end
56
+ threads.each(&:join)
57
+ counter.value # => 10000 (always correct with Mutex)
58
+ ```
59
+
60
+ ### concurrent-ruby — Production-Grade Concurrency
61
+
62
+ ```ruby
63
+ # Gemfile
64
+ gem "concurrent-ruby"
65
+ ```
66
+
67
+ ```ruby
68
+ # Thread-safe data structures
69
+ require "concurrent"
70
+
71
+ # Atomic values — no Mutex needed
72
+ counter = Concurrent::AtomicFixnum.new(0)
73
+ counter.increment
74
+ counter.value # => 1
75
+
76
+ # Thread-safe hash
77
+ cache = Concurrent::Map.new
78
+ cache["key"] = "value"
79
+ cache.fetch_or_store("key") { expensive_computation }
80
+
81
+ # Promises for async pipelines
82
+ result = Concurrent::Promise.fulfill("data")
83
+ .then { |data| transform(data) }
84
+ .then { |transformed| save(transformed) }
85
+ .rescue { |error| handle_error(error) }
86
+ .value # Blocks until chain completes
87
+ ```
88
+
89
+ ### Fibers — Cooperative Concurrency
90
+
91
+ ```ruby
92
+ # Fibers yield control explicitly — useful for generators and coroutines
93
+ def id_generator
94
+ Fiber.new do
95
+ id = 0
96
+ loop do
97
+ Fiber.yield(id += 1)
98
+ end
99
+ end
100
+ end
101
+
102
+ gen = id_generator
103
+ gen.resume # => 1
104
+ gen.resume # => 2
105
+ gen.resume # => 3
106
+
107
+ # Enumerator (built on Fibers) for lazy sequences
108
+ def fibonacci
109
+ Enumerator.new do |y|
110
+ a, b = 0, 1
111
+ loop do
112
+ y.yield a
113
+ a, b = b, a + b
114
+ end
115
+ end
116
+ end
117
+
118
+ fibonacci.lazy.select(&:odd?).first(10)
119
+ # => [1, 1, 3, 5, 13, 21, 55, 89, 233, 377]
120
+ ```
121
+
122
+ ### Batch Processing with `in_batches` + Threads
123
+
124
+ ```ruby
125
+ # Process large datasets with controlled parallelism
126
+ class BatchProcessor
127
+ def initialize(concurrency: 4)
128
+ @pool = Concurrent::FixedThreadPool.new(concurrency)
129
+ end
130
+
131
+ def process(scope, batch_size: 100, &block)
132
+ futures = []
133
+
134
+ scope.find_in_batches(batch_size: batch_size) do |batch|
135
+ futures << Concurrent::Future.execute(executor: @pool) do
136
+ batch.each { |record| block.call(record) }
137
+ end
138
+ end
139
+
140
+ futures.each(&:value!) # Wait for all, re-raise exceptions
141
+ end
142
+
143
+ def shutdown
144
+ @pool.shutdown
145
+ @pool.wait_for_termination(30)
146
+ end
147
+ end
148
+
149
+ # Usage
150
+ processor = BatchProcessor.new(concurrency: 4)
151
+ processor.process(Order.where(status: :pending)) do |order|
152
+ Orders::ProcessService.call(order)
153
+ end
154
+ processor.shutdown
155
+ ```
156
+
157
+ ## Why This Is Good
158
+
159
+ - **Threads for I/O.** 10 parallel HTTP requests complete in the time of 1 sequential request. Ruby's GVL releases during I/O, enabling true parallelism for network-bound work.
160
+ - **`concurrent-ruby` is production-tested.** Thread pools, atomic values, promises, and thread-safe collections — battle-hardened by millions of Ruby apps.
161
+ - **Mutex for correctness.** Shared mutable state without a Mutex causes race conditions. With a Mutex, operations are atomic and predictable.
162
+ - **Fibers for generators.** Infinite sequences, lazy evaluation, and cooperative multitasking without threads.
163
+
164
+ ## Anti-Pattern
165
+
166
+ Unprotected shared state or over-threading:
167
+
168
+ ```ruby
169
+ # BAD: Race condition — no synchronization
170
+ results = []
171
+ threads = urls.map do |url|
172
+ Thread.new { results << Faraday.get(url).body } # Array#<< is NOT thread-safe
173
+ end
174
+ threads.each(&:join)
175
+ # results may be corrupted, missing items, or raise errors
176
+
177
+ # FIX: Use thread-safe collection or collect from thread return values
178
+ results = urls.map do |url|
179
+ Thread.new { Faraday.get(url).body }
180
+ end.map(&:value)
181
+ ```
182
+
183
+ ## When To Apply
184
+
185
+ - **I/O-bound work** — HTTP requests, file reads, database queries across multiple connections. Threads provide real speedup.
186
+ - **Background processing** — Use `concurrent-ruby` thread pools for in-process parallelism.
187
+ - **Lazy sequences** — Fibers and Enumerators for infinite or expensive sequences that are consumed incrementally.
188
+
189
+ ## When NOT To Apply
190
+
191
+ - **CPU-bound work in MRI Ruby.** The Global VM Lock (GVL) prevents true parallel computation. Threads won't speed up math or data processing. Use Ractors or a separate process (via `parallel` gem).
192
+ - **Simple sequential code.** If the operation takes 50ms, threading adds overhead without meaningful speedup.
193
+ - **Rails request handling.** Puma already manages threads for you. Don't create threads inside controller actions — use background jobs instead.
194
+ - **Avoid more than 10-20 threads.** Thread creation and context switching have overhead. Use a fixed thread pool, not unbounded `Thread.new`.
@@ -0,0 +1,158 @@
1
+ # Ruby: Data, Struct, and OpenStruct
2
+
3
+ ## Pattern
4
+
5
+ Ruby provides three built-in ways to create simple data-holding classes. Choose the right one based on mutability needs and Ruby version.
6
+
7
+ ### Data (Ruby 3.2+) — Immutable Value Objects
8
+
9
+ ```ruby
10
+ # Data.define creates a frozen, immutable value class
11
+ Point = Data.define(:x, :y)
12
+ Money = Data.define(:cents, :currency)
13
+ DateRange = Data.define(:start_date, :end_date)
14
+
15
+ # Creation
16
+ point = Point.new(x: 10, y: 20)
17
+ price = Money.new(cents: 19_99, currency: "USD")
18
+
19
+ # Immutable — frozen by default
20
+ point.x # => 10
21
+ point.frozen? # => true
22
+ point.x = 5 # => FrozenError
23
+
24
+ # Equality by value
25
+ Point.new(x: 1, y: 2) == Point.new(x: 1, y: 2) # => true
26
+
27
+ # Pattern matching
28
+ case price
29
+ in Money[cents: (0..99), currency: "USD"]
30
+ "Under a dollar"
31
+ in Money[cents: (100..), currency: "USD"]
32
+ "A dollar or more"
33
+ end
34
+
35
+ # Add behavior with a block
36
+ Money = Data.define(:cents, :currency) do
37
+ def to_s
38
+ "$#{format('%.2f', cents / 100.0)}"
39
+ end
40
+
41
+ def +(other)
42
+ raise ArgumentError, "Currency mismatch" unless currency == other.currency
43
+ self.class.new(cents: cents + other.cents, currency: currency)
44
+ end
45
+
46
+ def self.zero(currency = "USD")
47
+ new(cents: 0, currency: currency)
48
+ end
49
+ end
50
+
51
+ price = Money.new(cents: 10_00, currency: "USD")
52
+ tax = Money.new(cents: 80, currency: "USD")
53
+ total = price + tax
54
+ total.to_s # => "$10.80"
55
+ ```
56
+
57
+ ### Struct — Mutable Data Containers
58
+
59
+ ```ruby
60
+ # Struct creates a mutable class with attribute accessors
61
+ OrderSummary = Struct.new(:reference, :total, :status, keyword_init: true)
62
+
63
+ summary = OrderSummary.new(reference: "ORD-001", total: 50_00, status: "pending")
64
+ summary.reference # => "ORD-001"
65
+ summary.status = "shipped" # Mutable — can change
66
+
67
+ # Struct supports Enumerable
68
+ summary.to_a # => ["ORD-001", 50_00, "shipped"]
69
+ summary.to_h # => { reference: "ORD-001", total: 50_00, status: "shipped" }
70
+
71
+ # Add methods
72
+ Result = Struct.new(:success, :value, :error, keyword_init: true) do
73
+ def success?
74
+ success == true
75
+ end
76
+
77
+ def failure?
78
+ !success?
79
+ end
80
+ end
81
+
82
+ result = Result.new(success: true, value: order, error: nil)
83
+ result.success? # => true
84
+ ```
85
+
86
+ ### OpenStruct — Dynamic Attributes (Use Sparingly)
87
+
88
+ ```ruby
89
+ # OpenStruct allows any attribute — no predefined structure
90
+ config = OpenStruct.new(api_key: "sk-123", timeout: 30)
91
+ config.api_key # => "sk-123"
92
+ config.new_field = "added dynamically" # Any attribute, any time
93
+ config.new_field # => "added dynamically"
94
+
95
+ # OpenStruct is SLOW — uses method_missing internally
96
+ # ~10x slower than Struct for attribute access
97
+ # ~100x slower than a plain class
98
+ ```
99
+
100
+ ## Decision Tree
101
+
102
+ ```
103
+ Do you need a simple data container?
104
+ ├── Is the data immutable (value object)?
105
+ │ ├── Ruby 3.2+? → Data.define
106
+ │ └── Ruby < 3.2? → Struct.new with .freeze
107
+ ├── Is the data mutable?
108
+ │ └── Struct.new (keyword_init: true)
109
+ ├── Are attributes dynamic/unknown at design time?
110
+ │ └── OpenStruct (but consider a Hash instead)
111
+ └── Does the object need complex behavior?
112
+ └── Write a full class
113
+ ```
114
+
115
+ ## When To Apply
116
+
117
+ - **`Data.define`** for value objects: Money, Email, Coordinates, API responses, Result types. Anywhere immutability and value equality matter.
118
+ - **`Struct`** for simple data transfer objects: search results, service return types, configuration that's built step by step.
119
+ - **OpenStruct** for quick prototyping only. Replace with Struct or Data before code review.
120
+
121
+ ## When NOT To Apply
122
+
123
+ - **ActiveRecord models.** They have their own attribute system. Don't wrap them in Struct/Data.
124
+ - **Complex domain objects.** If the object has 5+ methods of behavior (not just accessors), write a class.
125
+ - **Performance-critical paths.** OpenStruct is slow. In hot loops, use Struct or plain classes.
126
+ - **OpenStruct in production code.** Its dynamic nature makes typos silent (`config.api_ky = "..."` creates a new attribute instead of raising). Struct catches this at construction time.
127
+
128
+ ## Edge Cases
129
+
130
+ **Struct as a base class:**
131
+ ```ruby
132
+ class User < Struct.new(:name, :email, keyword_init: true)
133
+ def greeting
134
+ "Hello, #{name}!"
135
+ end
136
+ end
137
+ ```
138
+ This works but is considered unusual. Prefer `Data.define` with a block or a plain class.
139
+
140
+ **Freezing a Struct for immutability (pre-Ruby 3.2):**
141
+ ```ruby
142
+ Config = Struct.new(:api_key, :timeout, keyword_init: true)
143
+ config = Config.new(api_key: "sk-123", timeout: 30).freeze
144
+ config.api_key = "new" # => FrozenError
145
+ ```
146
+
147
+ **Nested Data objects:**
148
+ ```ruby
149
+ Address = Data.define(:street, :city, :state, :zip)
150
+ Customer = Data.define(:name, :email, :address)
151
+
152
+ customer = Customer.new(
153
+ name: "Alice",
154
+ email: "alice@example.com",
155
+ address: Address.new(street: "123 Main", city: "Austin", state: "TX", zip: "78701")
156
+ )
157
+ customer.address.city # => "Austin"
158
+ ```
@@ -0,0 +1,204 @@
1
+ # Ruby: Debugging and Profiling
2
+
3
+ ## Pattern
4
+
5
+ Use the right debugging tool for the problem: `debug` gem (Ruby 3.1+ built-in) for interactive debugging, logging for production visibility, and profiling tools to find performance bottlenecks.
6
+
7
+ ### Interactive Debugging (debug gem)
8
+
9
+ ```ruby
10
+ # Ruby 3.1+ has `debug` built in — no gem needed
11
+ # Add a breakpoint anywhere:
12
+ def create_order(params)
13
+ order = Order.new(params)
14
+ binding.break # Execution pauses here — inspect variables, step through code
15
+ order.save!
16
+ order
17
+ end
18
+
19
+ # Or use the shorter form
20
+ def process(data)
21
+ debugger # Same as binding.break
22
+ transform(data)
23
+ end
24
+ ```
25
+
26
+ Debug session commands:
27
+ ```
28
+ # In the debug console:
29
+ (rdbg) p order # Print variable
30
+ (rdbg) pp order.errors # Pretty-print
31
+ (rdbg) n # Next line (step over)
32
+ (rdbg) s # Step into method
33
+ (rdbg) c # Continue execution
34
+ (rdbg) info locals # Show all local variables
35
+ (rdbg) bt # Backtrace
36
+ (rdbg) watch @total # Break when @total changes
37
+ (rdbg) break Order#save # Break when Order#save is called
38
+ ```
39
+
40
+ ### Pry (Popular Alternative)
41
+
42
+ ```ruby
43
+ # Gemfile
44
+ gem "pry", group: [:development, :test]
45
+ gem "pry-byebug", group: [:development, :test] # Adds step/next/continue
46
+
47
+ # Usage
48
+ def calculate_total(items)
49
+ subtotal = items.sum(&:price)
50
+ binding.pry # Drops into Pry REPL
51
+ subtotal * 1.08
52
+ end
53
+
54
+ # Pry commands:
55
+ # ls object — list methods
56
+ # cd object — change context into object
57
+ # show-method — show source code of a method
58
+ # whereami — show current location
59
+ # next/step/continue — navigation (with pry-byebug)
60
+ ```
61
+
62
+ ### Logging Best Practices
63
+
64
+ ```ruby
65
+ # Rails logger levels: debug < info < warn < error < fatal
66
+ class Orders::CreateService
67
+ def call(params, user)
68
+ Rails.logger.info("[Orders::CreateService] Starting for user=#{user.id}")
69
+
70
+ order = user.orders.build(params)
71
+ unless order.valid?
72
+ Rails.logger.warn("[Orders::CreateService] Validation failed: #{order.errors.full_messages}")
73
+ return Result.failure(order.errors)
74
+ end
75
+
76
+ order.save!
77
+ Rails.logger.info("[Orders::CreateService] Created order=#{order.id} total=#{order.total}")
78
+
79
+ Result.success(order)
80
+ rescue StandardError => e
81
+ Rails.logger.error("[Orders::CreateService] Failed: #{e.class}: #{e.message}")
82
+ Rails.logger.debug(e.backtrace.first(10).join("\n"))
83
+ raise
84
+ end
85
+ end
86
+
87
+ # Tagged logging — adds context to every log line
88
+ Rails.logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
89
+ Rails.logger.tagged("OrderService", "user:#{user.id}") do
90
+ Rails.logger.info("Processing order")
91
+ # => [OrderService] [user:42] Processing order
92
+ end
93
+
94
+ # Structured logging for production (JSON)
95
+ # Gemfile
96
+ gem "lograge"
97
+
98
+ # config/environments/production.rb
99
+ config.lograge.enabled = true
100
+ config.lograge.formatter = Lograge::Formatters::Json.new
101
+ config.lograge.custom_payload do |controller|
102
+ { user_id: controller.current_user&.id }
103
+ end
104
+ # Output: {"method":"POST","path":"/orders","status":201,"duration":45.2,"user_id":42}
105
+ ```
106
+
107
+ ### Performance Profiling
108
+
109
+ ```ruby
110
+ # Benchmark a block
111
+ require "benchmark"
112
+
113
+ time = Benchmark.measure do
114
+ Order.where(status: :pending).find_each { |o| process(o) }
115
+ end
116
+ puts time # => 0.120000 0.030000 0.150000 ( 0.152345)
117
+
118
+ # Compare approaches
119
+ Benchmark.bm(20) do |x|
120
+ x.report("find_each:") { Order.pending.find_each { |o| o.total } }
121
+ x.report("pluck:") { Order.pending.pluck(:total) }
122
+ x.report("in_batches:") { Order.pending.in_batches.each_record { |o| o.total } }
123
+ end
124
+
125
+ # Memory profiling
126
+ # Gemfile
127
+ gem "memory_profiler", group: :development
128
+
129
+ report = MemoryProfiler.report do
130
+ users = User.all.to_a
131
+ users.map(&:email)
132
+ end
133
+ report.pretty_print
134
+ # Shows: allocated memory, retained memory, allocation by gem/file/location
135
+
136
+ # rack-mini-profiler for web requests
137
+ # Gemfile
138
+ gem "rack-mini-profiler", group: :development
139
+
140
+ # Shows a speed badge on every page with:
141
+ # - Total request time
142
+ # - SQL query count and time
143
+ # - Memory usage
144
+ # - Flamegraph link
145
+ ```
146
+
147
+ ### Finding N+1 Queries
148
+
149
+ ```ruby
150
+ # Bullet gem detects N+1 in development
151
+ # Gemfile
152
+ gem "bullet", group: :development
153
+
154
+ # config/environments/development.rb
155
+ config.after_initialize do
156
+ Bullet.enable = true
157
+ Bullet.alert = true # Browser popup
158
+ Bullet.rails_logger = true # Log to Rails log
159
+ Bullet.add_footer = true # Badge in page footer
160
+ end
161
+
162
+ # strict_loading (Rails 6.1+) — raises on lazy loading
163
+ class Order < ApplicationRecord
164
+ self.strict_loading_by_default = true
165
+ end
166
+
167
+ # Or per-query
168
+ Order.strict_loading.includes(:line_items).each do |order|
169
+ order.line_items # Works — preloaded
170
+ order.user # Raises! Not preloaded
171
+ end
172
+ ```
173
+
174
+ ### Production Debugging
175
+
176
+ ```ruby
177
+ # Rails console in production
178
+ # RAILS_ENV=production rails console
179
+
180
+ # Safe read-only queries
181
+ ActiveRecord::Base.connected_to(role: :reading) do
182
+ Order.where(status: :pending).count
183
+ end
184
+
185
+ # Sandbox mode — rolls back all changes on exit
186
+ # rails console --sandbox
187
+
188
+ # Quick diagnostics
189
+ Rails.logger.level = :debug # Temporarily increase verbosity
190
+ ActiveRecord::Base.logger = Logger.new($stdout) # See all SQL
191
+ ```
192
+
193
+ ## When To Apply
194
+
195
+ - **`debugger`/`binding.pry` for investigation.** When you don't understand why code behaves a certain way, stop execution and inspect state.
196
+ - **Logging for production.** Always log: service entry/exit, errors with context, and performance metrics. Never log: passwords, API keys, or PII.
197
+ - **Profiling before optimizing.** Measure first. The bottleneck is almost never where you think it is.
198
+ - **Bullet/strict_loading in development.** Catch N+1s before they reach production.
199
+
200
+ ## When NOT To Apply
201
+
202
+ - **Don't leave `debugger` calls in committed code.** Use `binding.pry` and `debugger` for local debugging only. CI should catch any that slip through.
203
+ - **Don't log everything.** Debug-level logging for every method call creates noise. Log at the right level: info for normal flow, warn for recoverable issues, error for failures.
204
+ - **Don't optimize without profiling.** "I think this is slow" → profile it → optimize the actual bottleneck.