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,177 @@
|
|
|
1
|
+
# Ruby: String Handling
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Strings are everywhere in Ruby. Use the right tool: frozen string literals for performance, heredocs for multi-line, interpolation for building, and `String` methods over regex when possible.
|
|
6
|
+
|
|
7
|
+
### Frozen String Literals
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# frozen_string_literal: true
|
|
11
|
+
|
|
12
|
+
# This magic comment at the top of the file freezes ALL string literals
|
|
13
|
+
# Frozen strings can't be mutated, which prevents bugs and improves performance
|
|
14
|
+
# (Ruby can reuse the same object instead of allocating new ones)
|
|
15
|
+
|
|
16
|
+
name = "Alice"
|
|
17
|
+
name << " Smith" # => FrozenError: can't modify frozen String
|
|
18
|
+
|
|
19
|
+
# When you need a mutable string, use .dup or unary +
|
|
20
|
+
name = +"Alice" # Mutable copy
|
|
21
|
+
name = "Alice".dup # Same thing, more explicit
|
|
22
|
+
name << " Smith" # Works
|
|
23
|
+
|
|
24
|
+
# RECOMMENDATION: Add `# frozen_string_literal: true` to every Ruby file.
|
|
25
|
+
# Rubocop enforces this by default. Rails 8 enables it in new apps.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Interpolation vs Concatenation vs Format
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
user = "Alice"
|
|
32
|
+
count = 3
|
|
33
|
+
|
|
34
|
+
# GOOD: Interpolation — clearest for simple cases
|
|
35
|
+
"Hello, #{user}! You have #{count} orders."
|
|
36
|
+
|
|
37
|
+
# GOOD: Format for precise number formatting
|
|
38
|
+
format("Total: $%.2f", total / 100.0) # => "Total: $19.99"
|
|
39
|
+
format("Order %06d", order_id) # => "Order 000042"
|
|
40
|
+
format("%s has %d orders", user, count) # => "Alice has 3 orders"
|
|
41
|
+
|
|
42
|
+
# BAD: Concatenation — ugly, slow, error-prone with non-strings
|
|
43
|
+
"Hello, " + user + "! You have " + count.to_s + " orders."
|
|
44
|
+
|
|
45
|
+
# BAD: String interpolation for SQL (security vulnerability)
|
|
46
|
+
User.where("name = '#{params[:name]}'") # SQL INJECTION!
|
|
47
|
+
User.where("name = ?", params[:name]) # Safe: parameterized
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Heredocs
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Plain heredoc — preserves indentation literally
|
|
54
|
+
sql = <<-SQL
|
|
55
|
+
SELECT *
|
|
56
|
+
FROM orders
|
|
57
|
+
WHERE status = 'pending'
|
|
58
|
+
SQL
|
|
59
|
+
|
|
60
|
+
# Squiggly heredoc (Ruby 2.3+) — strips leading whitespace based on the least-indented line
|
|
61
|
+
message = <<~MSG
|
|
62
|
+
Hello #{user.name},
|
|
63
|
+
|
|
64
|
+
Your order #{order.reference} has shipped!
|
|
65
|
+
Expected delivery: #{order.estimated_delivery.strftime("%B %d")}.
|
|
66
|
+
|
|
67
|
+
Thanks,
|
|
68
|
+
The Rubyn Team
|
|
69
|
+
MSG
|
|
70
|
+
# Result has no leading spaces — perfect for emails and templates
|
|
71
|
+
|
|
72
|
+
# Frozen heredoc
|
|
73
|
+
QUERY = <<~SQL.freeze
|
|
74
|
+
SELECT users.email, COUNT(orders.id) as order_count
|
|
75
|
+
FROM users
|
|
76
|
+
LEFT JOIN orders ON orders.user_id = users.id
|
|
77
|
+
GROUP BY users.email
|
|
78
|
+
SQL
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Common String Operations
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Presence and blank checks (Rails)
|
|
85
|
+
name = " "
|
|
86
|
+
name.blank? # => true (whitespace only)
|
|
87
|
+
name.present? # => false
|
|
88
|
+
name.presence # => nil (returns nil if blank, self if present)
|
|
89
|
+
|
|
90
|
+
# Use .presence for conditional assignment
|
|
91
|
+
display_name = user.nickname.presence || user.email
|
|
92
|
+
# Instead of: user.nickname.blank? ? user.email : user.nickname
|
|
93
|
+
|
|
94
|
+
# Stripping and squishing
|
|
95
|
+
" hello world ".strip # => "hello world" (leading/trailing only)
|
|
96
|
+
" hello world ".squish # => "hello world" (Rails — collapses internal whitespace too)
|
|
97
|
+
|
|
98
|
+
# Case conversion
|
|
99
|
+
"hello_world".camelize # => "HelloWorld" (Rails)
|
|
100
|
+
"HelloWorld".underscore # => "hello_world" (Rails)
|
|
101
|
+
"hello world".titleize # => "Hello World" (Rails)
|
|
102
|
+
"HELLO".downcase # => "hello"
|
|
103
|
+
"hello".upcase # => "HELLO"
|
|
104
|
+
|
|
105
|
+
# Checking content
|
|
106
|
+
"hello world".include?("world") # => true
|
|
107
|
+
"hello world".start_with?("hello") # => true
|
|
108
|
+
"hello world".end_with?("world") # => true
|
|
109
|
+
"hello world".match?(/\d/) # => false (no digits)
|
|
110
|
+
|
|
111
|
+
# Extracting
|
|
112
|
+
"user@example.com".split("@") # => ["user", "example.com"]
|
|
113
|
+
"ORD-001-2026".split("-", 2) # => ["ORD", "001-2026"] (limit splits)
|
|
114
|
+
"hello world"[0..4] # => "hello"
|
|
115
|
+
"order_12345".delete_prefix("order_") # => "12345" (Ruby 2.5+)
|
|
116
|
+
"file.rb".delete_suffix(".rb") # => "file" (Ruby 2.5+)
|
|
117
|
+
|
|
118
|
+
# Replacing
|
|
119
|
+
"hello world".sub("world", "Ruby") # => "hello Ruby" (first occurrence)
|
|
120
|
+
"aabaa".gsub("a", "x") # => "xxbxx" (all occurrences)
|
|
121
|
+
"hello world".tr("aeiou", "*") # => "h*ll* w*rld" (character-level replace)
|
|
122
|
+
|
|
123
|
+
# Truncation (Rails)
|
|
124
|
+
"A very long description that goes on and on".truncate(30)
|
|
125
|
+
# => "A very long description tha..."
|
|
126
|
+
"A very long description".truncate(20, separator: " ")
|
|
127
|
+
# => "A very long..." (breaks at word boundary)
|
|
128
|
+
|
|
129
|
+
# Parameterize for URLs (Rails)
|
|
130
|
+
"Hello World! It's great.".parameterize # => "hello-world-it-s-great"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### String Building for Performance
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# BAD: Repeated concatenation (creates new string objects each time)
|
|
137
|
+
result = ""
|
|
138
|
+
items.each { |item| result += "#{item.name}: #{item.price}\n" }
|
|
139
|
+
|
|
140
|
+
# GOOD: Array join (one allocation at the end)
|
|
141
|
+
result = items.map { |item| "#{item.name}: #{item.price}" }.join("\n")
|
|
142
|
+
|
|
143
|
+
# GOOD: StringIO for large outputs
|
|
144
|
+
require "stringio"
|
|
145
|
+
buffer = StringIO.new
|
|
146
|
+
items.each { |item| buffer.puts "#{item.name}: #{item.price}" }
|
|
147
|
+
result = buffer.string
|
|
148
|
+
|
|
149
|
+
# GOOD: String interpolation with Array for building
|
|
150
|
+
parts = []
|
|
151
|
+
parts << "Status: #{order.status}"
|
|
152
|
+
parts << "Total: #{order.formatted_total}"
|
|
153
|
+
parts << "Items: #{order.line_items.count}" if order.line_items.loaded?
|
|
154
|
+
parts.join(" | ")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Why This Is Good
|
|
158
|
+
|
|
159
|
+
- **Frozen string literals prevent mutation bugs and improve performance.** A frozen string used as a hash key or constant is allocated once and reused.
|
|
160
|
+
- **Interpolation is the Ruby way.** `"Hello, #{name}"` is cleaner, safer, and faster than concatenation.
|
|
161
|
+
- **Squiggly heredocs keep code clean.** Multi-line strings stay properly indented in the source without extra whitespace in the output.
|
|
162
|
+
- **`.presence` eliminates conditional checks.** One method replaces a blank-check ternary.
|
|
163
|
+
- **Array + join beats repeated concatenation.** Concatenation creates N intermediate string objects. Join creates one.
|
|
164
|
+
|
|
165
|
+
## When To Apply
|
|
166
|
+
|
|
167
|
+
- **`# frozen_string_literal: true`** — every Ruby file, always.
|
|
168
|
+
- **Heredocs** — any string longer than 2 lines.
|
|
169
|
+
- **`.presence`** — any `x.blank? ? default : x` pattern.
|
|
170
|
+
- **`.parameterize`** — any user input going into a URL slug.
|
|
171
|
+
- **`format`** — any numeric formatting (currency, percentages, padded numbers).
|
|
172
|
+
|
|
173
|
+
## When NOT To Apply
|
|
174
|
+
|
|
175
|
+
- **Don't over-optimize string building.** For 5-10 concatenations, readability wins over performance. `join` matters when you have 1,000+ items.
|
|
176
|
+
- **Don't use `.squish` on content that should preserve whitespace** — code blocks, pre-formatted text, etc.
|
|
177
|
+
- **Don't freeze mutable strings.** If you're building a string incrementally, don't freeze the initial value.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Ruby: Bundler and Dependency Management
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Use Bundler to manage dependencies. Pin versions carefully, group gems by environment, audit for vulnerabilities, and keep the Gemfile organized and commented.
|
|
6
|
+
|
|
7
|
+
### Gemfile Organization
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
source "https://rubygems.org"
|
|
11
|
+
ruby "~> 3.3"
|
|
12
|
+
|
|
13
|
+
# ─── Core ───────────────────────────────────────────
|
|
14
|
+
gem "rails", "~> 8.0"
|
|
15
|
+
gem "pg", "~> 1.5"
|
|
16
|
+
gem "redis", "~> 5.0"
|
|
17
|
+
gem "puma", "~> 6.0"
|
|
18
|
+
|
|
19
|
+
# ─── Authentication & Authorization ─────────────────
|
|
20
|
+
gem "devise", "~> 4.9"
|
|
21
|
+
gem "pundit", "~> 2.3"
|
|
22
|
+
|
|
23
|
+
# ─── Background Jobs ────────────────────────────────
|
|
24
|
+
gem "sidekiq", "~> 7.2"
|
|
25
|
+
gem "sidekiq-cron", "~> 1.12"
|
|
26
|
+
|
|
27
|
+
# ─── API ─────────────────────────────────────────────
|
|
28
|
+
gem "grape", "~> 2.0"
|
|
29
|
+
gem "grape-entity", "~> 1.0"
|
|
30
|
+
|
|
31
|
+
# ─── External Services ──────────────────────────────
|
|
32
|
+
gem "faraday", "~> 2.9"
|
|
33
|
+
gem "anthropic", "~> 0.3" # Claude API client
|
|
34
|
+
gem "stripe", "~> 10.0"
|
|
35
|
+
|
|
36
|
+
# ─── Frontend ────────────────────────────────────────
|
|
37
|
+
gem "turbo-rails", "~> 2.0"
|
|
38
|
+
gem "stimulus-rails", "~> 1.3"
|
|
39
|
+
gem "tailwindcss-rails", "~> 2.6"
|
|
40
|
+
|
|
41
|
+
group :development, :test do
|
|
42
|
+
gem "debug" # Built-in Ruby debugger
|
|
43
|
+
gem "dotenv-rails" # Load .env in development
|
|
44
|
+
gem "factory_bot_rails" # Test factories
|
|
45
|
+
gem "faker" # Generate fake data for seeds
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
group :development do
|
|
49
|
+
gem "bullet" # N+1 query detection
|
|
50
|
+
gem "rack-mini-profiler" # Performance profiling
|
|
51
|
+
gem "web-console" # In-browser Rails console
|
|
52
|
+
gem "rubocop-rails-omakase" # Rails-recommended linting
|
|
53
|
+
gem "annotate" # Schema annotations on models
|
|
54
|
+
gem "strong_migrations" # Catch unsafe migrations
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
group :test do
|
|
58
|
+
gem "capybara" # System tests
|
|
59
|
+
gem "selenium-webdriver" # Browser driver
|
|
60
|
+
gem "webmock" # Stub HTTP requests
|
|
61
|
+
gem "simplecov", require: false # Test coverage
|
|
62
|
+
gem "mocha" # Mocking for Minitest
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
group :production do
|
|
66
|
+
# Production-only gems (monitoring, APM)
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Version Constraints
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Pessimistic operator (~>) — the RIGHT default
|
|
74
|
+
gem "rails", "~> 8.0" # >= 8.0.0, < 9.0 (major updates require manual bump)
|
|
75
|
+
gem "pg", "~> 1.5" # >= 1.5.0, < 2.0
|
|
76
|
+
gem "sidekiq", "~> 7.2" # >= 7.2.0, < 8.0
|
|
77
|
+
|
|
78
|
+
# Exact pin — for gems where ANY update could break you
|
|
79
|
+
gem "anthropic", "0.3.2" # Exactly this version
|
|
80
|
+
|
|
81
|
+
# No constraint — AVOID (accepts any version, including breaking changes)
|
|
82
|
+
gem "faraday" # BAD: could jump from 2.x to 3.x on bundle update
|
|
83
|
+
|
|
84
|
+
# Greater than — rarely needed
|
|
85
|
+
gem "ruby", ">= 3.1" # For gemspecs only, not Gemfiles
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Security Auditing
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Check for known vulnerabilities in your dependencies
|
|
92
|
+
gem install bundler-audit
|
|
93
|
+
bundle audit check --update
|
|
94
|
+
|
|
95
|
+
# Output:
|
|
96
|
+
# Name: actionpack
|
|
97
|
+
# Version: 7.0.4
|
|
98
|
+
# CVE: CVE-2023-22795
|
|
99
|
+
# Criticality: High
|
|
100
|
+
# Solution: upgrade to ~> 7.0.4.1
|
|
101
|
+
|
|
102
|
+
# Automate in CI
|
|
103
|
+
# .github/workflows/security.yml
|
|
104
|
+
# - run: bundle audit check --update
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Bundle Commands
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Install dependencies
|
|
111
|
+
bundle install
|
|
112
|
+
|
|
113
|
+
# Update a specific gem
|
|
114
|
+
bundle update sidekiq
|
|
115
|
+
|
|
116
|
+
# Update all gems within constraints
|
|
117
|
+
bundle update
|
|
118
|
+
|
|
119
|
+
# Show where a gem is installed
|
|
120
|
+
bundle show faraday
|
|
121
|
+
|
|
122
|
+
# Show dependency tree
|
|
123
|
+
bundle list
|
|
124
|
+
|
|
125
|
+
# Open a gem's source code in your editor
|
|
126
|
+
bundle open faraday
|
|
127
|
+
|
|
128
|
+
# Check for outdated gems
|
|
129
|
+
bundle outdated
|
|
130
|
+
|
|
131
|
+
# Verify Gemfile.lock matches Gemfile
|
|
132
|
+
bundle check
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Gemfile.lock — Always Commit It
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
# For APPLICATIONS (Rails apps, Sinatra apps):
|
|
139
|
+
# ✅ ALWAYS commit Gemfile.lock
|
|
140
|
+
# Ensures every developer and CI runs the exact same versions
|
|
141
|
+
|
|
142
|
+
# For GEMS (libraries you publish):
|
|
143
|
+
# ❌ Do NOT commit Gemfile.lock
|
|
144
|
+
# Add to .gitignore — consumers use their own lock file
|
|
145
|
+
# The gemspec defines version constraints, not exact versions
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Why This Is Good
|
|
149
|
+
|
|
150
|
+
- **Pessimistic version constraints prevent breaking changes.** `~> 8.0` allows patch and minor updates but blocks major version jumps that could break your app.
|
|
151
|
+
- **Groups keep production lean.** Development gems (bullet, rubocop) don't install on production servers.
|
|
152
|
+
- **Comments explain WHY a gem exists.** Future developers know what `anthropic` does without looking it up.
|
|
153
|
+
- **`bundle audit` catches CVEs.** Known vulnerabilities in dependencies are reported before they reach production.
|
|
154
|
+
- **Gemfile.lock ensures reproducibility.** Every environment runs the exact same gem versions.
|
|
155
|
+
|
|
156
|
+
## Anti-Pattern
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# BAD: No version constraints
|
|
160
|
+
gem "rails"
|
|
161
|
+
gem "pg"
|
|
162
|
+
gem "sidekiq"
|
|
163
|
+
|
|
164
|
+
# BAD: All gems in one ungrouped list
|
|
165
|
+
gem "rails"
|
|
166
|
+
gem "debug" # Dev only — shouldn't install in production
|
|
167
|
+
gem "pg"
|
|
168
|
+
gem "capybara" # Test only
|
|
169
|
+
gem "sidekiq"
|
|
170
|
+
gem "webmock" # Test only
|
|
171
|
+
|
|
172
|
+
# BAD: Not committing Gemfile.lock
|
|
173
|
+
# .gitignore
|
|
174
|
+
Gemfile.lock # DON'T DO THIS for applications
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## When To Apply
|
|
178
|
+
|
|
179
|
+
- **Every Ruby project.** Bundler is non-negotiable. Use it from the first line of code.
|
|
180
|
+
- **Run `bundle audit` in CI.** Every build should check for vulnerabilities.
|
|
181
|
+
- **Update dependencies monthly.** `bundle outdated` → update one gem at a time → run tests → commit.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Ruby: CLI Tools with Thor
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Thor is the CLI framework Rails uses internally. It provides command parsing, subcommands, options, and help generation. Use it for any Ruby CLI tool that has more than 2-3 commands.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# lib/rubyn/cli.rb
|
|
9
|
+
require "thor"
|
|
10
|
+
|
|
11
|
+
module Rubyn
|
|
12
|
+
class CLI < Thor
|
|
13
|
+
desc "init", "Initialize Rubyn in the current project"
|
|
14
|
+
option :api_key, type: :string, desc: "API key (or set RUBYN_API_KEY env var)"
|
|
15
|
+
def init
|
|
16
|
+
api_key = options[:api_key] || ENV["RUBYN_API_KEY"]
|
|
17
|
+
api_key ||= ask("Enter your Rubyn API key:")
|
|
18
|
+
|
|
19
|
+
result = Commands::Init.call(api_key: api_key, project_dir: Dir.pwd)
|
|
20
|
+
|
|
21
|
+
if result.success?
|
|
22
|
+
say "✅ Rubyn initialized.", :green
|
|
23
|
+
say " #{result.project_info}", :cyan
|
|
24
|
+
else
|
|
25
|
+
say "❌ #{result.error}", :red
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "refactor FILE", "Refactor a file toward best practices"
|
|
31
|
+
option :model, type: :string, default: "base", enum: %w[light base pro], desc: "AI model tier"
|
|
32
|
+
option :apply, type: :boolean, default: false, desc: "Auto-apply changes without prompting"
|
|
33
|
+
def refactor(file_path)
|
|
34
|
+
ensure_initialized!
|
|
35
|
+
ensure_file_exists!(file_path)
|
|
36
|
+
|
|
37
|
+
say "🔍 Analyzing #{file_path}...", :cyan
|
|
38
|
+
context = Context::FileResolver.resolve(file_path, project_dir: Dir.pwd)
|
|
39
|
+
say " Loading context: #{context.related_files.map { |f| File.basename(f) }.join(', ')}"
|
|
40
|
+
|
|
41
|
+
result = Commands::Refactor.call(
|
|
42
|
+
file_path: file_path,
|
|
43
|
+
model_tier: options[:model],
|
|
44
|
+
context: context
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
display_streaming_response(result)
|
|
48
|
+
display_credits(result)
|
|
49
|
+
|
|
50
|
+
if options[:apply] || yes?("Apply changes? (y/n)")
|
|
51
|
+
apply_changes(result.changes)
|
|
52
|
+
say "✅ Changes applied.", :green
|
|
53
|
+
else
|
|
54
|
+
say "Changes discarded.", :yellow
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "spec FILE", "Generate tests for a file"
|
|
59
|
+
option :model, type: :string, default: "base", enum: %w[light base pro]
|
|
60
|
+
option :framework, type: :string, enum: %w[rspec minitest], desc: "Override detected test framework"
|
|
61
|
+
def spec(file_path)
|
|
62
|
+
ensure_initialized!
|
|
63
|
+
ensure_file_exists!(file_path)
|
|
64
|
+
|
|
65
|
+
say "🧪 Generating tests for #{file_path}...", :cyan
|
|
66
|
+
result = Commands::Spec.call(file_path: file_path, model_tier: options[:model])
|
|
67
|
+
|
|
68
|
+
display_streaming_response(result)
|
|
69
|
+
display_credits(result)
|
|
70
|
+
|
|
71
|
+
if yes?("Write spec file? (y/n)")
|
|
72
|
+
write_file(result.spec_path, result.spec_content)
|
|
73
|
+
say "✅ Written to #{result.spec_path}", :green
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc "review FILE_OR_DIR", "Review code for anti-patterns"
|
|
78
|
+
option :model, type: :string, default: "base", enum: %w[light base pro]
|
|
79
|
+
def review(path)
|
|
80
|
+
ensure_initialized!
|
|
81
|
+
|
|
82
|
+
files = File.directory?(path) ? Dir.glob("#{path}/**/*.rb") : [path]
|
|
83
|
+
say "🔍 Reviewing #{files.count} file(s)...", :cyan
|
|
84
|
+
|
|
85
|
+
result = Commands::Review.call(files: files, model_tier: options[:model])
|
|
86
|
+
display_streaming_response(result)
|
|
87
|
+
display_credits(result)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc "usage", "Show credit balance and recent activity"
|
|
91
|
+
def usage
|
|
92
|
+
ensure_initialized!
|
|
93
|
+
|
|
94
|
+
result = Commands::Usage.call
|
|
95
|
+
say result.formatted_output
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "version", "Show Rubyn version"
|
|
99
|
+
def version
|
|
100
|
+
say "Rubyn #{Rubyn::VERSION}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc "dashboard", "Open the Rubyn web dashboard"
|
|
104
|
+
def dashboard
|
|
105
|
+
if File.exist?(File.join(Dir.pwd, "config", "routes.rb"))
|
|
106
|
+
say "Dashboard available at http://localhost:3000/rubyn", :cyan
|
|
107
|
+
say "(Make sure your Rails server is running)"
|
|
108
|
+
else
|
|
109
|
+
say "Starting standalone dashboard...", :cyan
|
|
110
|
+
Commands::Dashboard.call(project_dir: Dir.pwd)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def ensure_initialized!
|
|
117
|
+
unless Rubyn::Config.initialized?
|
|
118
|
+
say "❌ Rubyn not initialized. Run `rubyn init` first.", :red
|
|
119
|
+
exit 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ensure_file_exists!(path)
|
|
124
|
+
unless File.exist?(path)
|
|
125
|
+
say "❌ File not found: #{path}", :red
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def display_streaming_response(result)
|
|
131
|
+
# StreamHandler renders content in real-time via SSE
|
|
132
|
+
result.stream.each do |chunk|
|
|
133
|
+
print chunk.text # Streams to terminal as it arrives
|
|
134
|
+
end
|
|
135
|
+
puts
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def display_credits(result)
|
|
139
|
+
say "📊 Used #{result.credits_used} credits (#{result.model_tier}) — #{result.balance_remaining} remaining", :cyan
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_changes(changes)
|
|
143
|
+
changes.each do |change|
|
|
144
|
+
write_file(change.path, change.content)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_file(path, content)
|
|
149
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
150
|
+
File.write(path, content)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### The Executable
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
#!/usr/bin/env ruby
|
|
160
|
+
# exe/rubyn
|
|
161
|
+
|
|
162
|
+
require "rubyn"
|
|
163
|
+
Rubyn::CLI.start(ARGV)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Gemspec Setup
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# rubyn.gemspec
|
|
170
|
+
spec.executables = ["rubyn"]
|
|
171
|
+
spec.bindir = "exe"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Thor Features Used
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Options
|
|
178
|
+
option :verbose, type: :boolean, default: false, aliases: "-v"
|
|
179
|
+
option :output, type: :string, default: "terminal", enum: %w[terminal json file]
|
|
180
|
+
option :count, type: :numeric, default: 10
|
|
181
|
+
|
|
182
|
+
# Ask for input
|
|
183
|
+
name = ask("What is your name?")
|
|
184
|
+
password = ask("Password:", echo: false) # Hidden input
|
|
185
|
+
|
|
186
|
+
# Yes/No confirmation
|
|
187
|
+
if yes?("Are you sure?")
|
|
188
|
+
# proceed
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Colored output
|
|
192
|
+
say "Success!", :green
|
|
193
|
+
say "Warning!", :yellow
|
|
194
|
+
say "Error!", :red
|
|
195
|
+
say "Info", :cyan
|
|
196
|
+
|
|
197
|
+
# Tables
|
|
198
|
+
print_table([
|
|
199
|
+
["Model", "Credits", "Status"],
|
|
200
|
+
["Base", "3", "✅"],
|
|
201
|
+
["Pro", "7", "✅"]
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
# Shell commands
|
|
205
|
+
run("bundle exec rspec #{spec_path}") # Runs and shows output
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Why This Is Good
|
|
209
|
+
|
|
210
|
+
- **Auto-generated help.** `rubyn help`, `rubyn help refactor` — Thor generates help text from `desc` and `option` declarations.
|
|
211
|
+
- **Type-checked options.** `type: :boolean`, `type: :numeric`, `enum: %w[light base pro]` — Thor validates before your code runs.
|
|
212
|
+
- **Consistent interface.** Every CLI tool built with Thor follows the same patterns. Users familiar with Rails generators, Bundler, or any Thor-based CLI feel at home.
|
|
213
|
+
- **Subcommands for free.** Each `desc` + method becomes a subcommand. No routing logic, no argument parsing.
|
|
214
|
+
- **Testable.** `Rubyn::CLI.start(["refactor", "app/controllers/orders_controller.rb", "--model", "pro"])` — invoke CLI commands programmatically in tests.
|
|
215
|
+
|
|
216
|
+
## When To Apply
|
|
217
|
+
|
|
218
|
+
- **Any CLI tool with 3+ commands.** Thor provides structure, help, and option parsing that OptionParser requires you to build manually.
|
|
219
|
+
- **Gem executables.** Thor is the standard for Ruby gem CLIs (Rails, Bundler, Rubocop all use it).
|
|
220
|
+
|
|
221
|
+
## When NOT To Apply
|
|
222
|
+
|
|
223
|
+
- **One-off scripts.** A single-purpose script doesn't need Thor. Use `OptionParser` or just `ARGV`.
|
|
224
|
+
- **Two commands.** If the CLI is literally `tool do_thing` and `tool --version`, Thor is overkill.
|