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,133 @@
|
|
|
1
|
+
# Design Pattern: Facade
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Provide a simplified interface to a complex subsystem. The facade hides the complexity of multiple classes, APIs, or steps behind a single method call. In Rails, service objects often act as facades over multi-step operations.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# The subsystem is complex — embedding client, chunker, database, change detection
|
|
9
|
+
# The facade makes it one call
|
|
10
|
+
|
|
11
|
+
class Codebase::IndexFacade
|
|
12
|
+
def initialize(
|
|
13
|
+
embedding_client: Rails.application.config.x.embedding_client,
|
|
14
|
+
chunker: Codebase::Chunker.new,
|
|
15
|
+
change_detector: Codebase::ChangeDetector.new
|
|
16
|
+
)
|
|
17
|
+
@embedding_client = embedding_client
|
|
18
|
+
@chunker = chunker
|
|
19
|
+
@change_detector = change_detector
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# One method hides 5 subsystem interactions
|
|
23
|
+
def index_project(project, files)
|
|
24
|
+
changed_files = @change_detector.filter_changed(project, files)
|
|
25
|
+
return { indexed: 0, skipped: files.size } if changed_files.empty?
|
|
26
|
+
|
|
27
|
+
changed_files.each_slice(10) do |batch|
|
|
28
|
+
chunks = batch.flat_map { |path, content| @chunker.split(path, content) }
|
|
29
|
+
vectors = @embedding_client.embed(chunks.map(&:text))
|
|
30
|
+
|
|
31
|
+
chunks.zip(vectors).each do |chunk, vector|
|
|
32
|
+
project.code_embeddings.upsert(
|
|
33
|
+
{
|
|
34
|
+
file_path: chunk.path,
|
|
35
|
+
chunk_content: chunk.text,
|
|
36
|
+
chunk_type: chunk.type,
|
|
37
|
+
embedding: vector,
|
|
38
|
+
file_hash: chunk.file_hash,
|
|
39
|
+
last_embedded_at: Time.current
|
|
40
|
+
},
|
|
41
|
+
unique_by: [:project_id, :file_path, :chunk_type, :chunk_content]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
project.update!(last_indexed_at: Time.current)
|
|
47
|
+
{ indexed: changed_files.size, skipped: files.size - changed_files.size }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Caller doesn't know about chunkers, change detectors, or embedding clients
|
|
52
|
+
result = Codebase::IndexFacade.new.index_project(project, files)
|
|
53
|
+
puts "Indexed #{result[:indexed]} files, skipped #{result[:skipped]}"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Another example — wrapping a multi-step onboarding process:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Onboarding::Facade
|
|
60
|
+
def self.call(registration_params)
|
|
61
|
+
new(registration_params).call
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(params)
|
|
65
|
+
@params = params
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def call
|
|
69
|
+
user = create_user
|
|
70
|
+
project = create_default_project(user)
|
|
71
|
+
api_key = generate_api_key(user)
|
|
72
|
+
seed_credits(user)
|
|
73
|
+
send_welcome(user)
|
|
74
|
+
|
|
75
|
+
Result.new(success: true, user: user, project: project, api_key: api_key)
|
|
76
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
77
|
+
Result.new(success: false, error: e.record.errors.full_messages.join(", "))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def create_user
|
|
83
|
+
User.create!(
|
|
84
|
+
email: @params[:email],
|
|
85
|
+
password: @params[:password],
|
|
86
|
+
name: @params[:name]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def create_default_project(user)
|
|
91
|
+
project = Project.create!(name: "My First Project")
|
|
92
|
+
ProjectMembership.create!(user: user, project: project, role: :owner)
|
|
93
|
+
project
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def generate_api_key(user)
|
|
97
|
+
ApiKey.create!(user: user, name: "Default")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def seed_credits(user)
|
|
101
|
+
CreditLedger.create!(user: user, amount: 30, description: "Welcome credits")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def send_welcome(user)
|
|
105
|
+
WelcomeMailer.welcome(user).deliver_later
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Controller is one line
|
|
110
|
+
result = Onboarding::Facade.call(registration_params)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Why This Is Good
|
|
114
|
+
|
|
115
|
+
- **One entry point for a complex operation.** `IndexFacade.new.index_project(project, files)` hides change detection, chunking, embedding, upserting, and timestamp updates behind one call.
|
|
116
|
+
- **Subsystem classes remain independent.** `Chunker`, `ChangeDetector`, and `EmbeddingClient` don't know about each other. The facade coordinates them.
|
|
117
|
+
- **Easy to test at two levels.** Integration test: call the facade and assert the database state. Unit tests: test each subsystem class in isolation.
|
|
118
|
+
- **Callers are decoupled from subsystem changes.** If you replace the chunker algorithm, the facade's interface doesn't change. Callers never know.
|
|
119
|
+
|
|
120
|
+
## When To Apply
|
|
121
|
+
|
|
122
|
+
- **Multi-step operations.** Onboarding, order processing, codebase indexing — anything that coordinates 3+ subsystems.
|
|
123
|
+
- **Complex API integrations.** Wrapping a third-party SDK's 5-step authentication flow behind `Auth::Facade.authenticate(credentials)`.
|
|
124
|
+
- **Simplifying legacy code.** Wrap a messy subsystem in a clean facade before refactoring the internals.
|
|
125
|
+
|
|
126
|
+
## When NOT To Apply
|
|
127
|
+
|
|
128
|
+
- **Single-step operations.** Wrapping `User.create!(params)` in a facade adds a pointless layer.
|
|
129
|
+
- **Don't hide everything.** If callers sometimes need fine-grained control over individual subsystems, expose them alongside the facade — don't force everything through one entry point.
|
|
130
|
+
|
|
131
|
+
## Rails Connection
|
|
132
|
+
|
|
133
|
+
Most Rails service objects ARE facades. `Orders::CreateService` is a facade over validation, persistence, payment charging, and notification. The pattern is so common in Rails that we don't always name it — but recognizing it helps design better services.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Design Pattern: Factory Method
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Define a method that creates objects without specifying the exact class. Let subclasses or configuration determine which class to instantiate. In Ruby, factories are often class methods, configuration hashes, or registry patterns rather than subclass hierarchies.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Factory method as a class method with registration
|
|
9
|
+
class Notifications::Factory
|
|
10
|
+
REGISTRY = {}
|
|
11
|
+
|
|
12
|
+
def self.register(channel, klass)
|
|
13
|
+
REGISTRY[channel.to_sym] = klass
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.build(channel, **options)
|
|
17
|
+
klass = REGISTRY.fetch(channel.to_sym) do
|
|
18
|
+
raise ArgumentError, "Unknown notification channel: #{channel}. Available: #{REGISTRY.keys.join(', ')}"
|
|
19
|
+
end
|
|
20
|
+
klass.new(**options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Notifications::EmailNotifier
|
|
25
|
+
def initialize(from: "noreply@rubyn.ai")
|
|
26
|
+
@from = from
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def deliver(user, message)
|
|
30
|
+
NotificationMailer.notify(to: user.email, from: @from, body: message).deliver_later
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
Notifications::Factory.register(:email, Notifications::EmailNotifier)
|
|
34
|
+
|
|
35
|
+
class Notifications::SmsNotifier
|
|
36
|
+
def initialize(provider: :twilio)
|
|
37
|
+
@provider = provider
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def deliver(user, message)
|
|
41
|
+
SmsClient.new(provider: @provider).send(user.phone, message.truncate(160))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
Notifications::Factory.register(:sms, Notifications::SmsNotifier)
|
|
45
|
+
|
|
46
|
+
# Usage — caller doesn't know or import the concrete class
|
|
47
|
+
notifier = Notifications::Factory.build(:email)
|
|
48
|
+
notifier.deliver(user, "Your order shipped!")
|
|
49
|
+
|
|
50
|
+
notifier = Notifications::Factory.build(:sms, provider: :vonage)
|
|
51
|
+
notifier.deliver(user, "Your order shipped!")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Factory method on a model — named constructors:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class Order < ApplicationRecord
|
|
58
|
+
def self.from_cart(cart, user:)
|
|
59
|
+
new(
|
|
60
|
+
user: user,
|
|
61
|
+
shipping_address: user.default_address,
|
|
62
|
+
line_items: cart.items.map { |item|
|
|
63
|
+
LineItem.new(product: item.product, quantity: item.quantity, unit_price: item.product.price)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.from_api(params)
|
|
69
|
+
new(
|
|
70
|
+
user: User.find(params[:user_id]),
|
|
71
|
+
shipping_address: params[:shipping_address],
|
|
72
|
+
line_items: params[:items].map { |item|
|
|
73
|
+
LineItem.new(product_id: item[:product_id], quantity: item[:quantity])
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.reorder(previous_order)
|
|
79
|
+
from_cart(
|
|
80
|
+
OpenStruct.new(items: previous_order.line_items),
|
|
81
|
+
user: previous_order.user
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Each factory method communicates intent and handles context-specific setup
|
|
87
|
+
order = Order.from_cart(shopping_cart, user: current_user)
|
|
88
|
+
order = Order.from_api(api_params)
|
|
89
|
+
order = Order.reorder(previous_order)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Configuration-driven factory:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# Embedding client factory — environment determines the implementation
|
|
96
|
+
class Embeddings::ClientFactory
|
|
97
|
+
def self.build
|
|
98
|
+
case Rails.env
|
|
99
|
+
when "production"
|
|
100
|
+
Embeddings::HttpClient.new(base_url: ENV.fetch("EMBEDDING_SERVICE_URL"))
|
|
101
|
+
when "test"
|
|
102
|
+
Embeddings::FakeClient.new
|
|
103
|
+
when "development"
|
|
104
|
+
if ENV["EMBEDDING_SERVICE_URL"].present?
|
|
105
|
+
Embeddings::HttpClient.new(base_url: ENV["EMBEDDING_SERVICE_URL"])
|
|
106
|
+
else
|
|
107
|
+
Embeddings::FakeClient.new
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# config/initializers/embeddings.rb
|
|
114
|
+
Rails.application.config.x.embedding_client = Embeddings::ClientFactory.build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Why This Is Good
|
|
118
|
+
|
|
119
|
+
- **Decouples creation from use.** The code that uses a notifier doesn't need to know which concrete class to instantiate. It asks the factory for `:email` and gets back a ready-to-use object.
|
|
120
|
+
- **Named constructors are self-documenting.** `Order.from_cart(cart, user:)` tells you exactly what context the order is being created in. `Order.new(user: user, ...)` doesn't.
|
|
121
|
+
- **New types don't require modifying callers.** Adding a `PushNotifier` means registering it with the factory. Every caller that uses `Factory.build(:push)` works immediately.
|
|
122
|
+
- **Environment-aware factories centralize configuration.** One place decides that production uses the real embedding service and tests use a fake.
|
|
123
|
+
|
|
124
|
+
## When To Apply
|
|
125
|
+
|
|
126
|
+
- **Multiple types with the same interface.** Notification channels, payment processors, export formatters, AI model clients — any family of interchangeable implementations.
|
|
127
|
+
- **Complex construction.** When building an object requires 5+ lines of setup, configuration lookups, or conditional logic, wrap it in a factory method.
|
|
128
|
+
- **Named constructors for models.** When the same model is created from different sources (web form, API, CSV import) with different setup requirements.
|
|
129
|
+
|
|
130
|
+
## When NOT To Apply
|
|
131
|
+
|
|
132
|
+
- **Simple `new` calls.** `User.new(email: email, name: name)` doesn't need a factory. Factories solve complex or polymorphic construction, not all construction.
|
|
133
|
+
- **One implementation.** If there's only one notifier and will only ever be one, skip the factory. Direct instantiation is clearer.
|
|
134
|
+
- **Rails model `.create` with simple params.** Don't wrap `Order.create!(params)` in a factory just for abstraction. Use factories when there's actual construction complexity.
|
|
135
|
+
|
|
136
|
+
## Rails Example
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Service object factory for AI interactions — selects strategy based on request type
|
|
140
|
+
class Ai::ServiceFactory
|
|
141
|
+
SERVICES = {
|
|
142
|
+
"refactor" => Ai::RefactorService,
|
|
143
|
+
"review" => Ai::ReviewService,
|
|
144
|
+
"explain" => Ai::ExplainService,
|
|
145
|
+
"generate" => Ai::GenerateService,
|
|
146
|
+
"debug" => Ai::DebugService
|
|
147
|
+
}.freeze
|
|
148
|
+
|
|
149
|
+
def self.build(request_type, **dependencies)
|
|
150
|
+
service_class = SERVICES.fetch(request_type) do
|
|
151
|
+
raise ArgumentError, "Unknown AI request type: #{request_type}"
|
|
152
|
+
end
|
|
153
|
+
service_class.new(**dependencies)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# API controller uses the factory
|
|
158
|
+
class Api::V1::AiController < Api::V1::BaseController
|
|
159
|
+
def create
|
|
160
|
+
service = Ai::ServiceFactory.build(
|
|
161
|
+
params[:type],
|
|
162
|
+
client: Rails.application.config.x.ai_client,
|
|
163
|
+
pricing: current_pricing_strategy
|
|
164
|
+
)
|
|
165
|
+
result = service.call(params[:prompt], context: build_context)
|
|
166
|
+
render json: result
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Design Pattern: Iterator
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Provide a way to traverse elements of a collection without exposing its underlying structure. Ruby's `Enumerable` module IS the Iterator pattern — include it in any class that defines `each`, and you get 50+ traversal methods for free.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Custom collection with Enumerable — the Ruby way to implement Iterator
|
|
9
|
+
class CodeChunkCollection
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@chunks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(chunk)
|
|
17
|
+
@chunks << chunk
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Define `each` — Enumerable gives you everything else
|
|
22
|
+
def each(&block)
|
|
23
|
+
@chunks.each(&block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Optional: define <=> on elements for sort methods
|
|
27
|
+
def by_relevance(query_embedding)
|
|
28
|
+
sort_by { |chunk| -chunk.similarity_to(query_embedding) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class CodeChunk
|
|
33
|
+
attr_reader :file_path, :content, :embedding, :chunk_type
|
|
34
|
+
|
|
35
|
+
def initialize(file_path:, content:, embedding:, chunk_type:)
|
|
36
|
+
@file_path = file_path
|
|
37
|
+
@content = content
|
|
38
|
+
@embedding = embedding
|
|
39
|
+
@chunk_type = chunk_type
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def similarity_to(other_embedding)
|
|
43
|
+
dot_product(embedding, other_embedding)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def dot_product(a, b)
|
|
49
|
+
a.zip(b).sum { |x, y| x * y }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Usage — all Enumerable methods work automatically
|
|
54
|
+
chunks = CodeChunkCollection.new
|
|
55
|
+
chunks.add(CodeChunk.new(file_path: "app/models/order.rb", content: "class Order...", embedding: [...], chunk_type: "class"))
|
|
56
|
+
chunks.add(CodeChunk.new(file_path: "app/services/create.rb", content: "class Create...", embedding: [...], chunk_type: "class"))
|
|
57
|
+
|
|
58
|
+
# All these work because we included Enumerable and defined each:
|
|
59
|
+
chunks.map(&:file_path) # ["app/models/order.rb", "app/services/create.rb"]
|
|
60
|
+
chunks.select { |c| c.chunk_type == "class" }
|
|
61
|
+
chunks.count # 2
|
|
62
|
+
chunks.any? { |c| c.file_path.include?("models") }
|
|
63
|
+
chunks.flat_map { |c| c.content.lines }
|
|
64
|
+
chunks.group_by(&:chunk_type)
|
|
65
|
+
chunks.min_by { |c| c.file_path.length }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### External Iterator with Enumerator
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# When you need lazy or external iteration control
|
|
72
|
+
class PaginatedApiIterator
|
|
73
|
+
include Enumerable
|
|
74
|
+
|
|
75
|
+
def initialize(client, endpoint, per_page: 100)
|
|
76
|
+
@client = client
|
|
77
|
+
@endpoint = endpoint
|
|
78
|
+
@per_page = per_page
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def each
|
|
82
|
+
page = 1
|
|
83
|
+
loop do
|
|
84
|
+
results = @client.get(@endpoint, page: page, per_page: @per_page)
|
|
85
|
+
break if results.empty?
|
|
86
|
+
|
|
87
|
+
results.each { |item| yield item }
|
|
88
|
+
page += 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Iterate over ALL pages transparently
|
|
94
|
+
iterator = PaginatedApiIterator.new(api_client, "/orders")
|
|
95
|
+
iterator.each { |order| process(order) }
|
|
96
|
+
|
|
97
|
+
# Or use lazily — only fetches pages as needed
|
|
98
|
+
iterator.lazy.select { |o| o["status"] == "pending" }.first(10)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Why This Is Good
|
|
102
|
+
|
|
103
|
+
- **Include `Enumerable`, get 50+ methods.** `map`, `select`, `reduce`, `any?`, `none?`, `group_by`, `sort_by`, `flat_map`, `tally`, `min_by`, `max_by`, `chunk`, `each_slice` — all from defining one `each` method.
|
|
104
|
+
- **Lazy evaluation for large/infinite collections.** `.lazy` chains don't materialize intermediate arrays. Process a 10GB file line by line without loading it into memory.
|
|
105
|
+
- **Uniform interface.** Any Enumerable collection works with any method that accepts an Enumerable. Your custom collection is instantly compatible with the entire Ruby ecosystem.
|
|
106
|
+
|
|
107
|
+
## When To Apply
|
|
108
|
+
|
|
109
|
+
- **Any class that holds a collection.** If your class wraps an array, hash, or tree of objects, include `Enumerable` and define `each`.
|
|
110
|
+
- **Paginated API responses.** Wrap pagination logic in an iterator so callers see a seamless stream of items.
|
|
111
|
+
- **Tree traversal.** Define `each` to walk the tree (depth-first, breadth-first), and all Enumerable methods work on tree nodes.
|
|
112
|
+
|
|
113
|
+
## When NOT To Apply
|
|
114
|
+
|
|
115
|
+
- **You're just wrapping an Array.** If your class is a thin wrapper around `@items`, consider exposing the array directly or using `delegate :each, :map, :select, to: :items` instead of a full Enumerable include.
|
|
116
|
+
- **ActiveRecord already provides this.** `Order.where(status: :pending).each` — ActiveRecord relations are already iterable. Don't wrap them in another iterator.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Design Pattern: Mediator
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Reduce chaotic dependencies between objects by centralizing communication through a mediator. Instead of objects referencing each other directly, they communicate through the mediator, which coordinates the interaction.
|
|
6
|
+
|
|
7
|
+
In Rails, this maps to orchestrator services that coordinate multiple subsystems without those subsystems knowing about each other.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Without Mediator: Objects reference each other directly
|
|
11
|
+
# Order knows about Inventory, Payment, Notification, Analytics
|
|
12
|
+
# Inventory knows about Order, Notification
|
|
13
|
+
# Payment knows about Order, Notification, Analytics
|
|
14
|
+
# Everything is coupled to everything
|
|
15
|
+
|
|
16
|
+
# With Mediator: One orchestrator coordinates everything
|
|
17
|
+
class Orders::CheckoutMediator
|
|
18
|
+
def initialize(
|
|
19
|
+
inventory: Inventory::ReservationService.new,
|
|
20
|
+
payment: Payments::ChargeService.new,
|
|
21
|
+
notification: Notifications::Dispatcher.new,
|
|
22
|
+
analytics: Analytics::Tracker.new
|
|
23
|
+
)
|
|
24
|
+
@inventory = inventory
|
|
25
|
+
@payment = payment
|
|
26
|
+
@notification = notification
|
|
27
|
+
@analytics = analytics
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def checkout(order, payment_method)
|
|
31
|
+
# Step 1: Reserve inventory
|
|
32
|
+
reservation = @inventory.reserve(order.line_items)
|
|
33
|
+
unless reservation.success?
|
|
34
|
+
return Result.new(success: false, error: "Items unavailable: #{reservation.error}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Step 2: Charge payment
|
|
38
|
+
charge = @payment.charge(order.total_cents, payment_method.token)
|
|
39
|
+
unless charge.success?
|
|
40
|
+
@inventory.release(reservation.id) # Compensate
|
|
41
|
+
return Result.new(success: false, error: "Payment failed: #{charge.error}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Step 3: Confirm order
|
|
45
|
+
order.update!(
|
|
46
|
+
status: :confirmed,
|
|
47
|
+
confirmed_at: Time.current,
|
|
48
|
+
payment_transaction_id: charge.transaction_id
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Step 4: Side effects (non-critical)
|
|
52
|
+
@notification.dispatch(order.user, "Order #{order.reference} confirmed!")
|
|
53
|
+
@analytics.track("checkout_completed", order_id: order.id, total: order.total)
|
|
54
|
+
|
|
55
|
+
Result.new(success: true, order: order)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
# Compensate on unexpected failure
|
|
58
|
+
@inventory.release(reservation&.id) if reservation&.success?
|
|
59
|
+
@payment.refund(charge.transaction_id) if charge&.success?
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Each service is independent — none knows about the others
|
|
65
|
+
class Inventory::ReservationService
|
|
66
|
+
def reserve(line_items)
|
|
67
|
+
# Only knows about inventory
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def release(reservation_id)
|
|
71
|
+
# Only knows about inventory
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Payments::ChargeService
|
|
76
|
+
def charge(amount_cents, token)
|
|
77
|
+
# Only knows about payments
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def refund(transaction_id)
|
|
81
|
+
# Only knows about payments
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Why This Is Good
|
|
87
|
+
|
|
88
|
+
- **Subsystems are decoupled.** `Inventory::ReservationService` doesn't know about payments. `Payments::ChargeService` doesn't know about notifications. Each can be developed, tested, and deployed independently.
|
|
89
|
+
- **The workflow is visible in one place.** Open `CheckoutMediator` and read the entire checkout flow: reserve → charge → confirm → notify → track. The orchestration logic is centralized.
|
|
90
|
+
- **Compensation logic is explicit.** If payment fails, inventory is released. If anything unexpected happens, both are rolled back. This saga-style coordination is easy to reason about when it's in one mediator.
|
|
91
|
+
- **Easy to modify the flow.** Adding fraud detection between reserve and charge means adding one step in the mediator. No other services change.
|
|
92
|
+
- **Testable with injected doubles.** Each service is injected, so tests can verify the orchestration without real inventory, payments, or notifications.
|
|
93
|
+
|
|
94
|
+
## Anti-Pattern
|
|
95
|
+
|
|
96
|
+
Objects communicating directly, creating a web of dependencies:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# BAD: Order model orchestrates everything
|
|
100
|
+
class Order < ApplicationRecord
|
|
101
|
+
after_create :reserve_inventory
|
|
102
|
+
after_create :charge_payment
|
|
103
|
+
after_create :send_notification
|
|
104
|
+
after_create :track_analytics
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def reserve_inventory
|
|
109
|
+
Inventory::ReservationService.new.reserve(line_items)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def charge_payment
|
|
113
|
+
result = Payments::ChargeService.new.charge(total_cents, user.default_payment_method.token)
|
|
114
|
+
unless result.success?
|
|
115
|
+
Inventory::ReservationService.new.release(inventory_reservation_id)
|
|
116
|
+
raise "Payment failed"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
# ... Order knows about EVERY subsystem
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## When To Apply
|
|
124
|
+
|
|
125
|
+
- **Multi-step workflows** — checkout, registration, order fulfillment, onboarding. Any flow that touches 3+ subsystems.
|
|
126
|
+
- **When objects are becoming too interconnected.** If Service A calls Service B which calls Service C which calls Service A, you need a mediator to break the cycle.
|
|
127
|
+
- **Saga/compensation patterns.** When steps must be rolled back if later steps fail, a mediator manages the compensation logic.
|
|
128
|
+
|
|
129
|
+
## When NOT To Apply
|
|
130
|
+
|
|
131
|
+
- **Two objects communicating.** A service calling one other service doesn't need a mediator. That's just a method call.
|
|
132
|
+
- **Event-driven communication works better.** If subsystems don't need coordination (just fire-and-forget), use the Observer pattern instead.
|
|
133
|
+
- **The mediator becomes a god object.** If the mediator is 500 lines with 20 dependencies, split it into smaller mediators for sub-workflows.
|