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,176 @@
|
|
|
1
|
+
# Code Quality: YAGNI — You Ain't Gonna Need It
|
|
2
|
+
|
|
3
|
+
## Core Principle
|
|
4
|
+
|
|
5
|
+
Don't build it until you need it. Every line of code is a liability — it must be tested, maintained, and understood. Code that exists "in case we need it later" is code that costs you now and may never pay off.
|
|
6
|
+
|
|
7
|
+
## The Three Questions
|
|
8
|
+
|
|
9
|
+
Before adding abstraction, ask:
|
|
10
|
+
|
|
11
|
+
1. **Do I need this right now, or might I need it later?** If "later," don't build it.
|
|
12
|
+
2. **Am I building for the problem I have, or the problem I imagine?** Solve the real problem.
|
|
13
|
+
3. **What's the cost of adding this later when I actually need it?** Usually low. Build then.
|
|
14
|
+
|
|
15
|
+
## Premature Abstraction
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# YAGNI VIOLATION: Building a plugin system for 1 payment provider
|
|
19
|
+
class PaymentProcessorFactory
|
|
20
|
+
REGISTRY = {}
|
|
21
|
+
|
|
22
|
+
def self.register(name, klass)
|
|
23
|
+
REGISTRY[name] = klass
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.build(name, **config)
|
|
27
|
+
klass = REGISTRY.fetch(name)
|
|
28
|
+
klass.new(**config)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class PaymentProcessor
|
|
33
|
+
def charge(amount, token) = raise NotImplementedError
|
|
34
|
+
def refund(transaction_id, amount) = raise NotImplementedError
|
|
35
|
+
def void(transaction_id) = raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class StripeProcessor < PaymentProcessor
|
|
39
|
+
def charge(amount, token)
|
|
40
|
+
# Stripe-specific code
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def refund(transaction_id, amount)
|
|
44
|
+
# Stripe-specific code
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def void(transaction_id)
|
|
48
|
+
# Stripe-specific code
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
PaymentProcessorFactory.register(:stripe, StripeProcessor)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
You have ONE payment provider. The factory, the abstract base class, and the registration mechanism add ~40 lines of code that provide zero value today. When you actually need a second provider (if you ever do), adding the abstraction takes 30 minutes.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# RIGHT-SIZED: Direct implementation for the one provider you have
|
|
59
|
+
class Payments::StripeService
|
|
60
|
+
def charge(amount_cents, token)
|
|
61
|
+
charge = Stripe::Charge.create(amount: amount_cents, currency: "usd", source: token)
|
|
62
|
+
Result.new(success: true, transaction_id: charge.id)
|
|
63
|
+
rescue Stripe::CardError => e
|
|
64
|
+
Result.new(success: false, error: e.message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def refund(charge_id, amount_cents)
|
|
68
|
+
Stripe::Refund.create(charge: charge_id, amount: amount_cents)
|
|
69
|
+
Result.new(success: true)
|
|
70
|
+
rescue Stripe::InvalidRequestError => e
|
|
71
|
+
Result.new(success: false, error: e.message)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Speculative Generality Examples
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# YAGNI: Config class for 2 settings
|
|
80
|
+
class Configuration
|
|
81
|
+
include Singleton
|
|
82
|
+
attr_accessor :settings
|
|
83
|
+
|
|
84
|
+
def initialize
|
|
85
|
+
@settings = {}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def get(key, default: nil)
|
|
89
|
+
settings.dig(*key.to_s.split(".")) || default
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def set(key, value)
|
|
93
|
+
keys = key.to_s.split(".")
|
|
94
|
+
hash = keys[0..-2].reduce(settings) { |h, k| h[k] ||= {} }
|
|
95
|
+
hash[keys.last] = value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# You only have: API key and model name. Just use ENV.
|
|
100
|
+
# ENV["ANTHROPIC_API_KEY"] and ENV["MODEL_NAME"] are simpler.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# YAGNI: Abstract base class with one subclass
|
|
105
|
+
class BaseExporter
|
|
106
|
+
def export(data)
|
|
107
|
+
header + body(data) + footer
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def header = raise NotImplementedError
|
|
111
|
+
def body(data) = raise NotImplementedError
|
|
112
|
+
def footer = raise NotImplementedError
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class CsvExporter < BaseExporter
|
|
116
|
+
def header = "id,name,total\n"
|
|
117
|
+
def body(data) = data.map { |r| "#{r.id},#{r.name},#{r.total}" }.join("\n")
|
|
118
|
+
def footer = ""
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# One exporter doesn't need a base class. Just write CsvExporter directly.
|
|
122
|
+
# When you need PdfExporter, THEN extract the common interface.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## The Rule of Three
|
|
126
|
+
|
|
127
|
+
Don't abstract until you have three concrete examples:
|
|
128
|
+
1. **First time:** Just write the code.
|
|
129
|
+
2. **Second time:** Notice the duplication but tolerate it. Maybe add a comment "similar to X."
|
|
130
|
+
3. **Third time:** Now extract. You have three examples to inform the right abstraction.
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# First time: inline
|
|
134
|
+
class OrdersController
|
|
135
|
+
def export
|
|
136
|
+
csv = orders.map { |o| [o.reference, o.total].join(",") }.join("\n")
|
|
137
|
+
send_data csv, filename: "orders.csv"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Second time: notice similarity, tolerate it
|
|
142
|
+
class InvoicesController
|
|
143
|
+
def export
|
|
144
|
+
csv = invoices.map { |i| [i.number, i.amount].join(",") }.join("\n")
|
|
145
|
+
send_data csv, filename: "invoices.csv"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Third time: NOW extract
|
|
150
|
+
class CsvExporter
|
|
151
|
+
def self.call(records, columns:, filename:)
|
|
152
|
+
csv = records.map { |r| columns.map { |c| r.public_send(c) }.join(",") }.join("\n")
|
|
153
|
+
{ data: csv, filename: filename }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## When YAGNI Doesn't Apply
|
|
159
|
+
|
|
160
|
+
- **Known requirements.** If the spec says "support Stripe and PayPal at launch," build the abstraction. That's not speculative — it's a stated requirement.
|
|
161
|
+
- **Architecture boundaries.** Even with one provider, wrapping external APIs behind an adapter is good practice. The adapter isn't speculative — it isolates you from API changes.
|
|
162
|
+
- **Security and data integrity.** Don't skip input validation because "we'll add it later." Security isn't optional.
|
|
163
|
+
- **Testing infrastructure.** Investing in test helpers, factories, and shared examples pays off immediately — not speculatively.
|
|
164
|
+
|
|
165
|
+
## The Cost of Abstraction
|
|
166
|
+
|
|
167
|
+
Every abstraction has a cost:
|
|
168
|
+
- **Indirection:** Readers must navigate to another file to understand behavior.
|
|
169
|
+
- **Maintenance:** The abstract interface must be kept in sync with all implementations.
|
|
170
|
+
- **Constraint:** Future implementations are shaped by the abstraction, which was designed without knowledge of their needs.
|
|
171
|
+
|
|
172
|
+
Premature abstraction is worse than no abstraction because it constrains future design based on incomplete information. The right abstraction, built with knowledge of 3+ concrete cases, enables extension. The wrong abstraction, built speculatively, requires fighting against it.
|
|
173
|
+
|
|
174
|
+
## Practical Test
|
|
175
|
+
|
|
176
|
+
Ask: "If I delete this abstraction and inline the code, does anything get worse?" If no — the abstraction isn't earning its keep. Delete it.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Design Pattern: Adapter
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Convert the interface of one class into another interface that clients expect. Adapters let classes work together that couldn't otherwise because of incompatible interfaces. In Rails, adapters are essential at system boundaries — wrapping external APIs, gems, and services behind a consistent internal interface.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Your app's internal interface — what your code expects
|
|
9
|
+
# Contract: .embed(texts) → Array<Array<Float>>
|
|
10
|
+
|
|
11
|
+
# Adapter for the Rubyn embedding service (FastAPI sidecar)
|
|
12
|
+
class Embeddings::RubynAdapter
|
|
13
|
+
def initialize(base_url:)
|
|
14
|
+
@base_url = base_url
|
|
15
|
+
@conn = Faraday.new(url: base_url) do |f|
|
|
16
|
+
f.request :json
|
|
17
|
+
f.response :json
|
|
18
|
+
f.adapter Faraday.default_adapter
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def embed(texts)
|
|
23
|
+
response = @conn.post("/embed", { texts: texts, prefix: "passage" })
|
|
24
|
+
response.body["embeddings"]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Adapter for OpenAI's embedding API (different URL, auth, response format)
|
|
29
|
+
class Embeddings::OpenAiAdapter
|
|
30
|
+
def initialize(api_key:)
|
|
31
|
+
@conn = Faraday.new(url: "https://api.openai.com") do |f|
|
|
32
|
+
f.request :json
|
|
33
|
+
f.response :json
|
|
34
|
+
f.headers["Authorization"] = "Bearer #{api_key}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def embed(texts)
|
|
39
|
+
response = @conn.post("/v1/embeddings", {
|
|
40
|
+
model: "text-embedding-3-small",
|
|
41
|
+
input: texts
|
|
42
|
+
})
|
|
43
|
+
# OpenAI returns { data: [{ embedding: [...] }, ...] }
|
|
44
|
+
# We normalize to Array<Array<Float>> to match our interface
|
|
45
|
+
response.body["data"]
|
|
46
|
+
.sort_by { |d| d["index"] }
|
|
47
|
+
.map { |d| d["embedding"] }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Adapter for a local model via ONNX Runtime (completely different mechanism)
|
|
52
|
+
class Embeddings::LocalOnnxAdapter
|
|
53
|
+
def initialize(model_path:)
|
|
54
|
+
@session = OnnxRuntime::InferenceSession.new(model_path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def embed(texts)
|
|
58
|
+
inputs = texts.map { |text| tokenize(text) }
|
|
59
|
+
outputs = @session.run(nil, { input_ids: inputs })
|
|
60
|
+
outputs.first # Already Array<Array<Float>>
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def tokenize(text)
|
|
66
|
+
# Tokenization logic
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Your service code doesn't know or care which adapter it uses
|
|
71
|
+
class Codebase::IndexService
|
|
72
|
+
def initialize(embedder:)
|
|
73
|
+
@embedder = embedder # Any adapter works
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def call(project, files)
|
|
77
|
+
files.each_slice(10) do |batch|
|
|
78
|
+
contents = batch.map(&:last)
|
|
79
|
+
vectors = @embedder.embed(contents) # Same interface, any provider
|
|
80
|
+
batch.zip(vectors).each do |(path, _), vector|
|
|
81
|
+
project.code_embeddings.upsert(
|
|
82
|
+
{ file_path: path, embedding: vector },
|
|
83
|
+
unique_by: [:project_id, :file_path]
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Wire up in config
|
|
91
|
+
embedder = Embeddings::RubynAdapter.new(base_url: ENV["EMBEDDING_SERVICE_URL"])
|
|
92
|
+
# OR: Embeddings::OpenAiAdapter.new(api_key: ENV["OPENAI_API_KEY"])
|
|
93
|
+
# OR: Embeddings::LocalOnnxAdapter.new(model_path: "models/code-embed.onnx")
|
|
94
|
+
|
|
95
|
+
Codebase::IndexService.new(embedder: embedder).call(project, files)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Adapting a gem's interface to your domain:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# The Stripe gem returns Stripe::Charge objects with their own structure.
|
|
102
|
+
# Your app works with a consistent PaymentResult.
|
|
103
|
+
|
|
104
|
+
PaymentResult = Data.define(:success, :transaction_id, :amount_cents, :error)
|
|
105
|
+
|
|
106
|
+
class Payments::StripeAdapter
|
|
107
|
+
def charge(amount_cents:, token:, description:)
|
|
108
|
+
charge = Stripe::Charge.create(
|
|
109
|
+
amount: amount_cents,
|
|
110
|
+
currency: "usd",
|
|
111
|
+
source: token,
|
|
112
|
+
description: description
|
|
113
|
+
)
|
|
114
|
+
PaymentResult.new(
|
|
115
|
+
success: true,
|
|
116
|
+
transaction_id: charge.id,
|
|
117
|
+
amount_cents: charge.amount,
|
|
118
|
+
error: nil
|
|
119
|
+
)
|
|
120
|
+
rescue Stripe::CardError => e
|
|
121
|
+
PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: e.message)
|
|
122
|
+
rescue Stripe::StripeError => e
|
|
123
|
+
PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: "Payment service error")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Payments::BraintreeAdapter
|
|
128
|
+
def charge(amount_cents:, token:, description:)
|
|
129
|
+
result = Braintree::Transaction.sale(
|
|
130
|
+
amount: (amount_cents / 100.0).round(2),
|
|
131
|
+
payment_method_nonce: token,
|
|
132
|
+
options: { submit_for_settlement: true }
|
|
133
|
+
)
|
|
134
|
+
if result.success?
|
|
135
|
+
PaymentResult.new(
|
|
136
|
+
success: true,
|
|
137
|
+
transaction_id: result.transaction.id,
|
|
138
|
+
amount_cents: (result.transaction.amount * 100).to_i,
|
|
139
|
+
error: nil
|
|
140
|
+
)
|
|
141
|
+
else
|
|
142
|
+
PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: result.message)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Why This Is Good
|
|
149
|
+
|
|
150
|
+
- **Unified interface across providers.** `embedder.embed(texts)` works identically whether the backend is your Python sidecar, OpenAI, or a local ONNX model. Business logic never sees provider-specific details.
|
|
151
|
+
- **Provider-specific complexity is contained.** OpenAI's response format (`{ data: [{ embedding, index }] }`) is normalized inside the adapter. Nobody else in the codebase deals with that structure.
|
|
152
|
+
- **Swappable at configuration time.** Switching from OpenAI to your own model means changing one line in an initializer. No business logic changes, no test changes.
|
|
153
|
+
- **Error normalization.** Each adapter catches its own exceptions (Stripe::CardError, Braintree errors) and returns a consistent `PaymentResult`. The caller never rescues provider-specific errors.
|
|
154
|
+
- **Gem upgrades are isolated.** If Stripe changes their API, only `StripeAdapter` changes. Every other class in your app is insulated.
|
|
155
|
+
|
|
156
|
+
## When To Apply
|
|
157
|
+
|
|
158
|
+
- **Every external API integration.** Wrap third-party APIs in adapters from day one. Even if you'll never switch providers, the adapter isolates your code from their API changes.
|
|
159
|
+
- **When migrating between providers.** Build the new adapter, test it, swap the configuration. Both adapters coexist during migration.
|
|
160
|
+
- **Normalizing inconsistent interfaces.** Two gems that do similar things with different method names and return types — adapt them to one internal interface.
|
|
161
|
+
|
|
162
|
+
## When NOT To Apply
|
|
163
|
+
|
|
164
|
+
- **Internal classes with consistent interfaces.** You don't need an adapter between your own service objects if they already share an interface.
|
|
165
|
+
- **Don't over-abstract stable gems.** If you use Devise and will always use Devise, wrapping every Devise method in an adapter is pointless friction.
|
|
166
|
+
- **Single-use, simple integrations.** A one-off API call in a rake task doesn't need a full adapter class.
|
|
167
|
+
|
|
168
|
+
## Edge Cases
|
|
169
|
+
|
|
170
|
+
**Adapter + Decorator composition:**
|
|
171
|
+
Adapters normalize the interface. Decorators add cross-cutting behavior. They compose naturally:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
embedder = Embeddings::RubynAdapter.new(base_url: url) # Normalize interface
|
|
175
|
+
embedder = Embeddings::RetryDecorator.new(embedder) # Add retry
|
|
176
|
+
embedder = Embeddings::LoggingDecorator.new(embedder) # Add logging
|
|
177
|
+
# Result: logged, retried, normalized embedding calls
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Testing adapters:**
|
|
181
|
+
Test each adapter against a shared example that defines the contract:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
RSpec.shared_examples "an embedding adapter" do
|
|
185
|
+
it "returns an array of float arrays" do
|
|
186
|
+
result = subject.embed(["def hello; end"])
|
|
187
|
+
expect(result).to be_an(Array)
|
|
188
|
+
expect(result.first).to all(be_a(Float))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Design Pattern: Bridge
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Separate an abstraction from its implementation so the two can vary independently. In Ruby, this means composing objects rather than inheriting — passing the implementation as a dependency.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# The "abstraction" — what the caller interacts with
|
|
9
|
+
class NotificationSender
|
|
10
|
+
def initialize(transport:, formatter:)
|
|
11
|
+
@transport = transport # HOW to send (email, SMS, push)
|
|
12
|
+
@formatter = formatter # HOW to format (plain, HTML, markdown)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send(user, event)
|
|
16
|
+
message = @formatter.format(event)
|
|
17
|
+
@transport.deliver(user, message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Transports (one dimension of variation)
|
|
22
|
+
class EmailTransport
|
|
23
|
+
def deliver(user, message)
|
|
24
|
+
NotificationMailer.send(to: user.email, body: message).deliver_later
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class SmsTransport
|
|
29
|
+
def deliver(user, message)
|
|
30
|
+
SmsClient.send(user.phone, message.truncate(160))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Formatters (another dimension of variation)
|
|
35
|
+
class PlainFormatter
|
|
36
|
+
def format(event)
|
|
37
|
+
"#{event.title}: #{event.description}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class HtmlFormatter
|
|
42
|
+
def format(event)
|
|
43
|
+
"<h2>#{event.title}</h2><p>#{event.description}</p>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Mix and match independently — 2 transports × 2 formatters = 4 combinations
|
|
48
|
+
# Without Bridge, you'd need: EmailPlainNotifier, EmailHtmlNotifier,
|
|
49
|
+
# SmsPlainNotifier, SmsHtmlNotifier — and N×M more as you add options
|
|
50
|
+
|
|
51
|
+
sender = NotificationSender.new(transport: EmailTransport.new, formatter: HtmlFormatter.new)
|
|
52
|
+
sender.send(user, order_confirmed_event)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**When to use:** When you have two or more dimensions of variation that would otherwise create an explosion of subclasses.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Design Pattern: Memento
|
|
60
|
+
|
|
61
|
+
## Pattern
|
|
62
|
+
|
|
63
|
+
Capture an object's internal state so it can be restored later, without exposing the internals. Useful for undo, versioning, and audit trails.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# Memento — a frozen snapshot of state
|
|
67
|
+
class OrderMemento
|
|
68
|
+
attr_reader :state, :created_at
|
|
69
|
+
|
|
70
|
+
def initialize(order)
|
|
71
|
+
@state = {
|
|
72
|
+
status: order.status,
|
|
73
|
+
total: order.total,
|
|
74
|
+
shipping_address: order.shipping_address,
|
|
75
|
+
discount_amount: order.discount_amount,
|
|
76
|
+
notes: order.notes
|
|
77
|
+
}.freeze
|
|
78
|
+
@created_at = Time.current
|
|
79
|
+
freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Originator — the object that creates and restores from mementos
|
|
84
|
+
class Order < ApplicationRecord
|
|
85
|
+
def save_snapshot
|
|
86
|
+
OrderMemento.new(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def restore_from(memento)
|
|
90
|
+
assign_attributes(memento.state)
|
|
91
|
+
save!
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Caretaker — manages the history of mementos
|
|
96
|
+
class OrderHistory
|
|
97
|
+
def initialize
|
|
98
|
+
@snapshots = []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def push(memento)
|
|
102
|
+
@snapshots.push(memento)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def pop
|
|
106
|
+
@snapshots.pop
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def peek
|
|
110
|
+
@snapshots.last
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def size
|
|
114
|
+
@snapshots.size
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Usage — admin makes changes with undo support
|
|
119
|
+
history = OrderHistory.new
|
|
120
|
+
order = Order.find(params[:id])
|
|
121
|
+
|
|
122
|
+
# Save state before changes
|
|
123
|
+
history.push(order.save_snapshot)
|
|
124
|
+
order.update!(status: :shipped, notes: "Expedited shipping")
|
|
125
|
+
|
|
126
|
+
# Oops, wrong order — undo
|
|
127
|
+
previous = history.pop
|
|
128
|
+
order.restore_from(previous)
|
|
129
|
+
# Order is back to its previous state
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**When to use:** Undo/redo, draft saving, version history, audit trails where you need to restore previous state.
|
|
133
|
+
|
|
134
|
+
**Rails built-in alternative:** The `paper_trail` gem provides automatic versioning with mementos stored in the database.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
# Design Pattern: Visitor
|
|
139
|
+
|
|
140
|
+
## Pattern
|
|
141
|
+
|
|
142
|
+
Separate an algorithm from the objects it operates on. Define operations in visitor objects, and let each element "accept" the visitor. This lets you add new operations without modifying the element classes.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# Elements — domain objects that accept visitors
|
|
146
|
+
class Order < ApplicationRecord
|
|
147
|
+
def accept(visitor)
|
|
148
|
+
visitor.visit_order(self)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class LineItem < ApplicationRecord
|
|
153
|
+
def accept(visitor)
|
|
154
|
+
visitor.visit_line_item(self)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class Discount < ApplicationRecord
|
|
159
|
+
def accept(visitor)
|
|
160
|
+
visitor.visit_discount(self)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Visitor 1: Calculate tax differently per element type
|
|
165
|
+
class TaxCalculatorVisitor
|
|
166
|
+
attr_reader :total_tax
|
|
167
|
+
|
|
168
|
+
def initialize(tax_rate:)
|
|
169
|
+
@tax_rate = tax_rate
|
|
170
|
+
@total_tax = 0
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def visit_order(order)
|
|
174
|
+
order.line_items.each { |item| item.accept(self) }
|
|
175
|
+
order.discounts.each { |discount| discount.accept(self) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def visit_line_item(item)
|
|
179
|
+
@total_tax += (item.quantity * item.unit_price * @tax_rate).round
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def visit_discount(discount)
|
|
183
|
+
@total_tax -= (discount.amount * @tax_rate).round
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Visitor 2: Export elements to different formats
|
|
188
|
+
class CsvExportVisitor
|
|
189
|
+
attr_reader :rows
|
|
190
|
+
|
|
191
|
+
def initialize
|
|
192
|
+
@rows = [%w[type reference amount]]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def visit_order(order)
|
|
196
|
+
@rows << ["order", order.reference, order.total]
|
|
197
|
+
order.line_items.each { |item| item.accept(self) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def visit_line_item(item)
|
|
201
|
+
@rows << ["line_item", item.product.name, item.quantity * item.unit_price]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def visit_discount(discount)
|
|
205
|
+
@rows << ["discount", discount.code, -discount.amount]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def to_csv
|
|
209
|
+
@rows.map { |row| row.join(",") }.join("\n")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Usage — different operations, same elements
|
|
214
|
+
tax_visitor = TaxCalculatorVisitor.new(tax_rate: 0.08)
|
|
215
|
+
order.accept(tax_visitor)
|
|
216
|
+
tax_visitor.total_tax # => calculated tax
|
|
217
|
+
|
|
218
|
+
csv_visitor = CsvExportVisitor.new
|
|
219
|
+
order.accept(csv_visitor)
|
|
220
|
+
csv_visitor.to_csv # => CSV string
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**When to use:** When you need multiple unrelated operations on a set of element types, and you don't want to pollute the element classes with every operation. Common in compilers, report generators, and data exporters.
|
|
224
|
+
|
|
225
|
+
**When NOT to use:** In most Rails apps. Visitor is powerful but heavyweight. If you have 2-3 operations, methods on the models or service objects are simpler. Visitor shines when you have 5+ operations across 5+ element types.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Ruby Idiomatic Alternative to Visitor
|
|
230
|
+
|
|
231
|
+
Ruby's duck typing often makes the Visitor pattern unnecessary. Instead of the formal accept/visit protocol, use polymorphic method dispatch:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
# Simpler Ruby approach — no accept/visit ceremony
|
|
235
|
+
class ReportGenerator
|
|
236
|
+
def generate(elements)
|
|
237
|
+
elements.each do |element|
|
|
238
|
+
case element
|
|
239
|
+
when Order then process_order(element)
|
|
240
|
+
when LineItem then process_line_item(element)
|
|
241
|
+
when Discount then process_discount(element)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
def process_order(order) = # ...
|
|
249
|
+
def process_line_item(item) = # ...
|
|
250
|
+
def process_discount(discount) = # ...
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
This is less "pure" OOP but more idiomatic Ruby. Use the formal Visitor when the element hierarchy is stable but operations change frequently.
|