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,215 @@
|
|
|
1
|
+
# RSpec: Test Suite Performance
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
A fast test suite runs in under 60 seconds for 1,000 specs. Achieve this by minimizing database hits, choosing the cheapest factory strategy per test, avoiding unnecessary setup, and profiling regularly.
|
|
6
|
+
|
|
7
|
+
Core strategies ranked by impact:
|
|
8
|
+
|
|
9
|
+
**1. Use `build_stubbed` wherever possible**
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# FAST: Zero database hits
|
|
13
|
+
let(:user) { build_stubbed(:user) }
|
|
14
|
+
let(:order) { build_stubbed(:order, user: user, total: 100) }
|
|
15
|
+
|
|
16
|
+
it "calculates discount" do
|
|
17
|
+
expect(order.discounted_total).to eq(90)
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**2. Use `let` (lazy) instead of `let!` (eager)**
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# FAST: Only creates records that are actually referenced
|
|
25
|
+
let(:user) { create(:user) }
|
|
26
|
+
let(:order) { create(:order, user: user) }
|
|
27
|
+
|
|
28
|
+
# Only the examples that call `order` pay for its creation
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**3. Minimize factory association chains**
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# BAD: Creates user, company, plan, 3 line items, 3 products, 3 categories
|
|
35
|
+
let(:order) { create(:order, :with_line_items) }
|
|
36
|
+
|
|
37
|
+
# GOOD: Only what this test needs
|
|
38
|
+
let(:order) { build_stubbed(:order, total: 100) }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**4. Use `before(:all)` / `before_all` for truly shared expensive setup**
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# With the test-prof gem's before_all (transaction-safe)
|
|
45
|
+
before_all do
|
|
46
|
+
@reference_data = create(:pricing_table_with_100_rows)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Standard before(:all) — use with caution, not wrapped in transaction
|
|
50
|
+
before(:all) do
|
|
51
|
+
@admin = create(:user, :admin)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**5. Profile your test suite to find the bottlenecks**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Find the 10 slowest examples
|
|
59
|
+
bundle exec rspec --profile 10
|
|
60
|
+
|
|
61
|
+
# Find the slowest factories (requires test-prof gem)
|
|
62
|
+
FPROF=1 bundle exec rspec
|
|
63
|
+
|
|
64
|
+
# Find examples that make the most DB queries
|
|
65
|
+
EVENT_PROF=sql.active_record bundle exec rspec
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**6. Use `aggregate_failures` to reduce example count**
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# SLOW: 4 separate examples, each with their own setup
|
|
72
|
+
it "has a reference" do
|
|
73
|
+
expect(order.reference).to be_present
|
|
74
|
+
end
|
|
75
|
+
it "has a status" do
|
|
76
|
+
expect(order.status).to eq("pending")
|
|
77
|
+
end
|
|
78
|
+
it "belongs to a user" do
|
|
79
|
+
expect(order.user).to eq(user)
|
|
80
|
+
end
|
|
81
|
+
it "has a created_at" do
|
|
82
|
+
expect(order.created_at).to be_present
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# FAST: 1 example, same assertions, same error detail on failure
|
|
86
|
+
it "has the expected attributes", :aggregate_failures do
|
|
87
|
+
expect(order.reference).to be_present
|
|
88
|
+
expect(order.status).to eq("pending")
|
|
89
|
+
expect(order.user).to eq(user)
|
|
90
|
+
expect(order.created_at).to be_present
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**7. Parallelize with `parallel_tests`**
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Gemfile
|
|
98
|
+
gem 'parallel_tests', group: :development
|
|
99
|
+
|
|
100
|
+
# Run on 4 cores
|
|
101
|
+
bundle exec parallel_rspec -n 4
|
|
102
|
+
|
|
103
|
+
# Database setup for parallel
|
|
104
|
+
rake parallel:setup
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Why This Is Good
|
|
108
|
+
|
|
109
|
+
- **Developer productivity.** A 30-second test suite gets run after every change. A 10-minute suite gets run before commits, maybe. A 30-minute suite gets run in CI only. Fast feedback loops produce better code.
|
|
110
|
+
- **CI costs.** CI minutes cost money. A test suite that runs in 60 seconds vs 10 minutes is a 10x cost difference over thousands of builds per month.
|
|
111
|
+
- **Developer morale.** Nobody enjoys waiting. A slow test suite creates friction that makes developers skip tests, run subsets, or push untested code.
|
|
112
|
+
- **Catch failures faster.** When tests run in 30 seconds, you run them after every change and catch bugs immediately. When they take 10 minutes, you batch changes and debugging becomes harder.
|
|
113
|
+
|
|
114
|
+
## Anti-Pattern
|
|
115
|
+
|
|
116
|
+
A test suite where every example creates a full object graph and runs expensive setup:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
RSpec.describe Order do
|
|
120
|
+
let!(:company) { create(:company) }
|
|
121
|
+
let!(:admin) { create(:user, :admin, company: company) }
|
|
122
|
+
let!(:user) { create(:user, company: company) }
|
|
123
|
+
let!(:product_a) { create(:product, :with_inventory, company: company) }
|
|
124
|
+
let!(:product_b) { create(:product, :with_inventory, company: company) }
|
|
125
|
+
let!(:order) { create(:order, :with_line_items, user: user, item_count: 3) }
|
|
126
|
+
|
|
127
|
+
describe "#formatted_total" do
|
|
128
|
+
it "returns a currency string" do
|
|
129
|
+
# 15+ records created for a string formatting test
|
|
130
|
+
expect(order.formatted_total).to match(/\$\d+\.\d{2}/)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe "#pending?" do
|
|
135
|
+
it "returns true when status is pending" do
|
|
136
|
+
# 15+ records created to test a boolean comparison
|
|
137
|
+
expect(order.pending?).to be true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Why This Is Bad
|
|
144
|
+
|
|
145
|
+
- **15+ INSERT statements per example.** With `let!`, every record is created before every example. 20 examples in this file = 300+ INSERTs.
|
|
146
|
+
- **90% of the setup is irrelevant.** `formatted_total` needs a number, not a company, admin, products, and inventory records.
|
|
147
|
+
- **Compounds across the suite.** If 50 spec files follow this pattern, the test suite creates 15,000+ unnecessary records per run.
|
|
148
|
+
- **Factory chains hide the cost.** `create(:order, :with_line_items)` looks like one record but cascades into order + user + company + 3 line items + 3 products = 9 INSERTs.
|
|
149
|
+
|
|
150
|
+
## When To Apply
|
|
151
|
+
|
|
152
|
+
Always. Test performance should be a continuous concern, not an afterthought.
|
|
153
|
+
|
|
154
|
+
**Before writing any spec, ask:**
|
|
155
|
+
1. Does this test need the database at all? → `build_stubbed`
|
|
156
|
+
2. Does it need the database but not the full object graph? → `create` with minimal attributes
|
|
157
|
+
3. Does it need a complex setup? → isolate the setup, share it via traits or `before_all`
|
|
158
|
+
|
|
159
|
+
**Regular maintenance:**
|
|
160
|
+
- Run `--profile 10` monthly to catch slow tests
|
|
161
|
+
- Install `test-prof` and run factory profiling when the suite slows down
|
|
162
|
+
- Set a CI budget: if the suite exceeds 2 minutes, investigate before adding more tests
|
|
163
|
+
|
|
164
|
+
## When NOT To Apply
|
|
165
|
+
|
|
166
|
+
- **Don't prematurely optimize.** A 10-spec file that runs in 2 seconds doesn't need profiling or `build_stubbed` rewrites. Focus on the slow files first.
|
|
167
|
+
- **Don't sacrifice readability for speed.** If using `create` makes a test dramatically clearer than a `build_stubbed` with 5 `allow` stubs, use `create`. Clarity wins when the speed difference is negligible.
|
|
168
|
+
- **System/feature tests are inherently slower.** They launch a browser and interact with full pages. Optimize them by having fewer of them (test critical paths only) rather than by cutting database setup.
|
|
169
|
+
|
|
170
|
+
## Edge Cases
|
|
171
|
+
|
|
172
|
+
**DatabaseCleaner strategies:**
|
|
173
|
+
Use `:transaction` strategy (default with RSpec Rails) for unit and request specs. Use `:truncation` or `:deletion` only for system specs that need JavaScript (and thus run outside the test transaction).
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# spec/support/database_cleaner.rb
|
|
177
|
+
RSpec.configure do |config|
|
|
178
|
+
config.use_transactional_fixtures = true
|
|
179
|
+
|
|
180
|
+
config.before(:each, type: :system) do
|
|
181
|
+
DatabaseCleaner.strategy = :truncation
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
config.after(:each, type: :system) do
|
|
185
|
+
DatabaseCleaner.clean
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Shared test data across a describe block:**
|
|
191
|
+
`test-prof`'s `before_all` creates data once per describe block (wrapped in a transaction), not once per example. This is safe and dramatically faster than `let!` for read-only reference data.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
# Requires test-prof gem
|
|
195
|
+
before_all do
|
|
196
|
+
@products = create_list(:product, 50)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "searches products" do
|
|
200
|
+
results = Product.search("widget")
|
|
201
|
+
expect(results).to include(@products.first)
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Stubbing Time:**
|
|
206
|
+
Use `travel_to` instead of creating records with specific timestamps, then querying by date:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
it "returns recent orders" do
|
|
210
|
+
travel_to(2.days.ago) { create(:order) }
|
|
211
|
+
recent = create(:order)
|
|
212
|
+
|
|
213
|
+
expect(Order.recent).to eq([recent])
|
|
214
|
+
end
|
|
215
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Ruby: Blocks, Procs, and Lambdas
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Blocks are Ruby's most powerful feature — closures that capture their surrounding context and can be passed to methods. Understanding blocks, procs, and lambdas is essential for writing idiomatic Ruby.
|
|
6
|
+
|
|
7
|
+
### Blocks
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# A block is code between do/end or { } passed to a method
|
|
11
|
+
[1, 2, 3].each { |n| puts n }
|
|
12
|
+
|
|
13
|
+
[1, 2, 3].each do |n|
|
|
14
|
+
puts n
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Convention: { } for single-line, do/end for multi-line
|
|
18
|
+
|
|
19
|
+
# yield calls the block from inside the method
|
|
20
|
+
def with_logging
|
|
21
|
+
Rails.logger.info("Starting")
|
|
22
|
+
result = yield
|
|
23
|
+
Rails.logger.info("Completed")
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
with_logging { Order.create!(params) }
|
|
28
|
+
|
|
29
|
+
# block_given? checks if a block was passed
|
|
30
|
+
def find_or_default(collection, default: nil)
|
|
31
|
+
result = collection.find { |item| yield(item) }
|
|
32
|
+
result || default
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Blocks for Resource Management
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# The block pattern guarantees cleanup — Ruby's most important idiom
|
|
40
|
+
File.open("data.csv") do |file|
|
|
41
|
+
file.each_line { |line| process(line) }
|
|
42
|
+
end
|
|
43
|
+
# File is automatically closed when the block exits, even on exception
|
|
44
|
+
|
|
45
|
+
# Build your own resource-managing methods
|
|
46
|
+
class DatabaseConnection
|
|
47
|
+
def self.with_connection
|
|
48
|
+
conn = checkout
|
|
49
|
+
yield conn
|
|
50
|
+
ensure
|
|
51
|
+
checkin(conn)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
DatabaseConnection.with_connection do |conn|
|
|
56
|
+
conn.execute("SELECT * FROM orders")
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Procs and Lambdas
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Proc: a block saved as an object
|
|
64
|
+
doubler = Proc.new { |n| n * 2 }
|
|
65
|
+
doubler.call(5) # => 10
|
|
66
|
+
doubler.(5) # => 10 (shorthand)
|
|
67
|
+
|
|
68
|
+
# Lambda: a stricter proc (checks arity, return scoping)
|
|
69
|
+
doubler = ->(n) { n * 2 }
|
|
70
|
+
doubler.call(5) # => 10
|
|
71
|
+
|
|
72
|
+
# Symbol-to-proc: converts a method name to a proc
|
|
73
|
+
["alice", "bob"].map(&:upcase) # => ["ALICE", "BOB"]
|
|
74
|
+
# Equivalent to: .map { |s| s.upcase }
|
|
75
|
+
|
|
76
|
+
# Method objects as procs
|
|
77
|
+
def double(n) = n * 2
|
|
78
|
+
[1, 2, 3].map(&method(:double)) # => [2, 4, 6]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Proc vs Lambda Differences
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# 1. Arity: Lambda checks argument count, Proc doesn't
|
|
85
|
+
my_lambda = ->(a, b) { a + b }
|
|
86
|
+
my_lambda.call(1) # ArgumentError: wrong number of arguments (given 1, expected 2)
|
|
87
|
+
|
|
88
|
+
my_proc = Proc.new { |a, b| (a || 0) + (b || 0) }
|
|
89
|
+
my_proc.call(1) # => 1 (b is nil, no error)
|
|
90
|
+
|
|
91
|
+
# 2. Return: Lambda returns to its caller, Proc returns from the enclosing method
|
|
92
|
+
def test_lambda
|
|
93
|
+
l = -> { return "from lambda" }
|
|
94
|
+
l.call
|
|
95
|
+
"after lambda" # This line executes
|
|
96
|
+
end
|
|
97
|
+
test_lambda # => "after lambda"
|
|
98
|
+
|
|
99
|
+
def test_proc
|
|
100
|
+
p = Proc.new { return "from proc" }
|
|
101
|
+
p.call
|
|
102
|
+
"after proc" # This line NEVER executes
|
|
103
|
+
end
|
|
104
|
+
test_proc # => "from proc"
|
|
105
|
+
|
|
106
|
+
# RULE: Use lambdas. They behave predictably.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Practical Patterns
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# Strategy via lambda
|
|
113
|
+
PRICING = {
|
|
114
|
+
standard: ->(amount) { amount },
|
|
115
|
+
premium: ->(amount) { amount * 0.9 },
|
|
116
|
+
vip: ->(amount) { amount * 0.8 }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def calculate_price(amount, tier:)
|
|
120
|
+
PRICING.fetch(tier, PRICING[:standard]).call(amount)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Callbacks / hooks
|
|
124
|
+
class Pipeline
|
|
125
|
+
def initialize
|
|
126
|
+
@before_hooks = []
|
|
127
|
+
@after_hooks = []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def before(&block)
|
|
131
|
+
@before_hooks << block
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def after(&block)
|
|
135
|
+
@after_hooks << block
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def execute(data)
|
|
139
|
+
@before_hooks.each { |hook| hook.call(data) }
|
|
140
|
+
result = yield(data)
|
|
141
|
+
@after_hooks.each { |hook| hook.call(result) }
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
pipeline = Pipeline.new
|
|
147
|
+
pipeline.before { |data| puts "Processing: #{data}" }
|
|
148
|
+
pipeline.after { |result| puts "Done: #{result}" }
|
|
149
|
+
pipeline.execute("order-123") { |data| "Processed #{data}" }
|
|
150
|
+
|
|
151
|
+
# Filtering with lambdas
|
|
152
|
+
active = ->(user) { user.active? }
|
|
153
|
+
premium = ->(user) { user.plan == "pro" }
|
|
154
|
+
recent = ->(user) { user.created_at > 30.days.ago }
|
|
155
|
+
|
|
156
|
+
filters = [active, premium, recent]
|
|
157
|
+
users.select { |user| filters.all? { |f| f.call(user) } }
|
|
158
|
+
|
|
159
|
+
# Configuration DSLs
|
|
160
|
+
class Router
|
|
161
|
+
def initialize(&block)
|
|
162
|
+
@routes = {}
|
|
163
|
+
instance_eval(&block) if block
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def get(path, &handler)
|
|
167
|
+
@routes[[:get, path]] = handler
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def post(path, &handler)
|
|
171
|
+
@routes[[:post, path]] = handler
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
router = Router.new do
|
|
176
|
+
get "/health" do
|
|
177
|
+
{ status: "ok" }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
post "/orders" do
|
|
181
|
+
Order.create!(params)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Why This Is Good
|
|
187
|
+
|
|
188
|
+
- **Blocks enable resource safety.** `File.open { }` guarantees cleanup. `ActiveRecord::Base.transaction { }` guarantees rollback on failure. This is more reliable than try/finally patterns.
|
|
189
|
+
- **Lambdas are first-class functions.** Store them in hashes, pass them as arguments, compose them. Ruby's functional programming capabilities are built on lambdas.
|
|
190
|
+
- **Symbol-to-proc is concise and expressive.** `.map(&:name)` is instantly readable by any Rubyist. It's not just shorter — it's clearer.
|
|
191
|
+
- **DSLs via `instance_eval`.** Blocks with `instance_eval` enable clean configuration DSLs (like Rails routes, RSpec, Sinatra).
|
|
192
|
+
|
|
193
|
+
## When To Apply
|
|
194
|
+
|
|
195
|
+
- **Resource management** — always use blocks for open/close, start/stop, begin/end patterns.
|
|
196
|
+
- **Callbacks and hooks** — pass blocks to register behavior that runs at specific points.
|
|
197
|
+
- **Strategy selection** — lambdas in a hash for lightweight strategies that don't need a full class.
|
|
198
|
+
- **Iteration and transformation** — blocks with Enumerable methods are the heart of Ruby.
|
|
199
|
+
|
|
200
|
+
## When NOT To Apply
|
|
201
|
+
|
|
202
|
+
- **Complex logic in a block.** If a block is longer than 5-7 lines, extract it into a method or a class. Blocks should be concise.
|
|
203
|
+
- **Procs for business logic.** Use lambdas, not procs. Proc's return behavior is surprising and error-prone.
|
|
204
|
+
- **Deep `instance_eval` nesting.** More than one level of `instance_eval` becomes hard to reason about. Keep DSLs shallow.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Ruby: Class Design
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Design classes with a single responsibility, a clear public interface, and minimal exposure of internal state. Use `attr_reader` by default, only use `attr_accessor` when mutation is intentional, and freeze objects when immutability is desirable.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# GOOD: Clear responsibility, minimal interface, protected internals
|
|
9
|
+
class Money
|
|
10
|
+
attr_reader :amount_cents, :currency
|
|
11
|
+
|
|
12
|
+
def initialize(amount_cents, currency = "USD")
|
|
13
|
+
@amount_cents = Integer(amount_cents)
|
|
14
|
+
@currency = currency.to_s.upcase.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_f
|
|
19
|
+
amount_cents / 100.0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
format("%.2f %s", to_f, currency)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def +(other)
|
|
27
|
+
raise ArgumentError, "Currency mismatch" unless currency == other.currency
|
|
28
|
+
|
|
29
|
+
self.class.new(amount_cents + other.amount_cents, currency)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def >(other)
|
|
33
|
+
raise ArgumentError, "Currency mismatch" unless currency == other.currency
|
|
34
|
+
|
|
35
|
+
amount_cents > other.amount_cents
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ==(other)
|
|
39
|
+
other.is_a?(self.class) &&
|
|
40
|
+
amount_cents == other.amount_cents &&
|
|
41
|
+
currency == other.currency
|
|
42
|
+
end
|
|
43
|
+
alias_method :eql?, :==
|
|
44
|
+
|
|
45
|
+
def hash
|
|
46
|
+
[amount_cents, currency].hash
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Constructor patterns:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Named constructor for clarity
|
|
55
|
+
class Order
|
|
56
|
+
def self.from_cart(cart, user)
|
|
57
|
+
new(
|
|
58
|
+
user: user,
|
|
59
|
+
line_items: cart.items.map { |item| LineItem.new(product: item.product, quantity: item.quantity) },
|
|
60
|
+
shipping_address: user.default_address
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(user:, line_items:, shipping_address:)
|
|
65
|
+
@user = user
|
|
66
|
+
@line_items = line_items
|
|
67
|
+
@shipping_address = shipping_address
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Usage reads like English
|
|
72
|
+
order = Order.from_cart(cart, current_user)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Why This Is Good
|
|
76
|
+
|
|
77
|
+
- **`attr_reader` protects state.** External code can read `money.amount_cents` but can't set it. State changes only happen through explicit methods with names that communicate intent.
|
|
78
|
+
- **`freeze` enforces immutability.** A frozen Money object can't be accidentally mutated. Operations like `+` return new instances. This eliminates an entire class of bugs where shared references are modified in place.
|
|
79
|
+
- **Named constructors improve readability.** `Order.from_cart(cart, user)` is clearer than `Order.new(user: user, line_items: cart.items.map { ... })`. The constructor name describes the context.
|
|
80
|
+
- **`==` and `hash` make objects work in collections.** Two Money objects with the same amount and currency are equal, can be used as hash keys, and work with `uniq`, `include?`, and Set operations.
|
|
81
|
+
- **Keyword arguments in constructors.** `new(user:, line_items:, shipping_address:)` is self-documenting. You can't accidentally swap argument order.
|
|
82
|
+
|
|
83
|
+
## Anti-Pattern
|
|
84
|
+
|
|
85
|
+
Classes with `attr_accessor` on everything, no encapsulation, and public state mutation:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class Order
|
|
89
|
+
attr_accessor :user, :items, :status, :total, :tax, :discount,
|
|
90
|
+
:shipping_address, :billing_address, :notes,
|
|
91
|
+
:created_at, :updated_at
|
|
92
|
+
|
|
93
|
+
def initialize
|
|
94
|
+
@items = []
|
|
95
|
+
@status = "pending"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# External code reaches in and mutates freely
|
|
100
|
+
order = Order.new
|
|
101
|
+
order.user = current_user
|
|
102
|
+
order.items << line_item
|
|
103
|
+
order.items << another_item
|
|
104
|
+
order.total = order.items.sum(&:price)
|
|
105
|
+
order.tax = order.total * 0.08
|
|
106
|
+
order.total = order.total + order.tax
|
|
107
|
+
order.status = "confirmed"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Why This Is Bad
|
|
111
|
+
|
|
112
|
+
- **No encapsulation.** Any code anywhere can set `order.status = "shipped"` without any validation or side-effect management. The object can't protect its own invariants.
|
|
113
|
+
- **Scattered logic.** Total calculation happens outside the class. Tax calculation happens outside. The object is a passive data bag that external code manipulates.
|
|
114
|
+
- **Impossible to refactor.** Renaming `@total` to `@amount` requires finding every `order.total =` call across the entire codebase. With a method, you change one place.
|
|
115
|
+
- **No constructor contract.** `Order.new` creates an incomplete, invalid object. The caller must remember to set user, items, total, and tax in the correct order. Missing any step produces a broken object silently.
|
|
116
|
+
|
|
117
|
+
## When To Apply
|
|
118
|
+
|
|
119
|
+
- **Every class you write.** Single responsibility and minimal public interface aren't optional patterns — they're baseline class design.
|
|
120
|
+
- **Value objects** (Money, DateRange, Coordinate, EmailAddress) should always be frozen and immutable.
|
|
121
|
+
- **Service objects and domain objects** should use `attr_reader` for dependencies and `private` for implementation details.
|
|
122
|
+
- **Use keyword arguments** when a constructor has more than 2 parameters, or when the parameters are the same type (two strings, two integers) and could be confused.
|
|
123
|
+
|
|
124
|
+
## When NOT To Apply
|
|
125
|
+
|
|
126
|
+
- **ActiveRecord models** follow their own conventions. `attr_accessor` for virtual attributes is normal in Rails models. Don't fight the framework.
|
|
127
|
+
- **Structs and Data objects** use `attr_reader` automatically. You don't need to define them manually.
|
|
128
|
+
- **Configuration objects** that are built incrementally (builder pattern) may need `attr_writer` during construction, then frozen after.
|
|
129
|
+
|
|
130
|
+
## Edge Cases
|
|
131
|
+
|
|
132
|
+
**You need a mutable object but want controlled mutation:**
|
|
133
|
+
Use explicit setter methods with validation instead of `attr_accessor`:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class Account
|
|
137
|
+
attr_reader :balance
|
|
138
|
+
|
|
139
|
+
def deposit(amount)
|
|
140
|
+
raise ArgumentError, "Amount must be positive" unless amount > 0
|
|
141
|
+
@balance += amount
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def withdraw(amount)
|
|
145
|
+
raise InsufficientFunds if amount > @balance
|
|
146
|
+
@balance -= amount
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Too many constructor arguments (more than 4-5):**
|
|
152
|
+
Consider a parameter object, a builder, or breaking the class into smaller collaborators. A constructor with 8 keyword arguments is a sign the class has too many responsibilities.
|
|
153
|
+
|
|
154
|
+
**Inheritance vs composition:**
|
|
155
|
+
Default to composition. If you're inheriting just to share code, use a module instead. Inherit only when there's a genuine "is-a" relationship and you want polymorphism.
|