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,280 @@
|
|
|
1
|
+
# Rails: Model Concerns
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Use model concerns for genuine, reusable behavior that multiple unrelated models need. A good concern adds one well-defined capability — slugging, soft deleting, auditing, searching. It has a clear contract and works without knowledge of the including model's specific attributes.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# app/models/concerns/searchable.rb
|
|
9
|
+
module Searchable
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
scope :search, ->(query) {
|
|
14
|
+
return all if query.blank?
|
|
15
|
+
|
|
16
|
+
columns = searchable_columns.map { |col| "#{table_name}.#{col}" }
|
|
17
|
+
conditions = columns.map { |col| "#{col} ILIKE :query" }.join(" OR ")
|
|
18
|
+
where(conditions, query: "%#{sanitize_sql_like(query)}%")
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class_methods do
|
|
23
|
+
def searchable_columns
|
|
24
|
+
raise NotImplementedError, "#{name} must define searchable_columns"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Usage in models — each defines what's searchable
|
|
30
|
+
class User < ApplicationRecord
|
|
31
|
+
include Searchable
|
|
32
|
+
|
|
33
|
+
def self.searchable_columns
|
|
34
|
+
%w[name email]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Product < ApplicationRecord
|
|
39
|
+
include Searchable
|
|
40
|
+
|
|
41
|
+
def self.searchable_columns
|
|
42
|
+
%w[name description sku]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Both work identically
|
|
47
|
+
User.search("alice")
|
|
48
|
+
Product.search("widget")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# app/models/concerns/soft_deletable.rb
|
|
53
|
+
module SoftDeletable
|
|
54
|
+
extend ActiveSupport::Concern
|
|
55
|
+
|
|
56
|
+
included do
|
|
57
|
+
scope :kept, -> { where(discarded_at: nil) }
|
|
58
|
+
scope :discarded, -> { where.not(discarded_at: nil) }
|
|
59
|
+
|
|
60
|
+
default_scope { kept }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def discard
|
|
64
|
+
update(discarded_at: Time.current)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def undiscard
|
|
68
|
+
update(discarded_at: nil)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def discarded?
|
|
72
|
+
discarded_at.present?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# app/models/concerns/has_token.rb
|
|
79
|
+
module HasToken
|
|
80
|
+
extend ActiveSupport::Concern
|
|
81
|
+
|
|
82
|
+
included do
|
|
83
|
+
before_create :generate_token
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class_methods do
|
|
87
|
+
def token_column
|
|
88
|
+
:token
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def find_by_token!(token)
|
|
92
|
+
find_by!(token_column => token)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def generate_token
|
|
99
|
+
column = self.class.token_column
|
|
100
|
+
loop do
|
|
101
|
+
self[column] = SecureRandom.urlsafe_base64(32)
|
|
102
|
+
break unless self.class.exists?(column => self[column])
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Why This Is Good
|
|
109
|
+
|
|
110
|
+
- **Genuinely reusable.** `Searchable`, `SoftDeletable`, and `HasToken` work on any model. They don't know or care about order-specific, user-specific, or product-specific logic.
|
|
111
|
+
- **Clear contract.** `Searchable` requires the model to define `searchable_columns`. The concern raises `NotImplementedError` if the model forgets. The contract is explicit and enforced.
|
|
112
|
+
- **Self-contained.** Including `SoftDeletable` gives you scopes, instance methods, and a default scope. The model doesn't need to configure anything — just include and add a `discarded_at` column.
|
|
113
|
+
- **Tested independently.** You can write a shared example that tests the searchable behavior, then include it in User and Product specs. One test verifies the concern works; per-model tests verify the configuration.
|
|
114
|
+
- **Namespace isolation.** The concern defines behavior. The model defines which columns/attributes to apply it to. Neither reaches into the other's internals.
|
|
115
|
+
|
|
116
|
+
## Anti-Pattern
|
|
117
|
+
|
|
118
|
+
Using concerns to split a fat model into multiple files without actually improving the design:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# app/models/concerns/order_calculations.rb
|
|
122
|
+
module OrderCalculations
|
|
123
|
+
extend ActiveSupport::Concern
|
|
124
|
+
|
|
125
|
+
def calculate_subtotal
|
|
126
|
+
line_items.sum { |li| li.quantity * li.unit_price }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def calculate_tax
|
|
130
|
+
subtotal * tax_rate
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def calculate_shipping
|
|
134
|
+
return 0 if subtotal > 100
|
|
135
|
+
line_items.sum(&:weight) * 0.5
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def calculate_total
|
|
139
|
+
calculate_subtotal + calculate_tax + calculate_shipping
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_discount(code)
|
|
143
|
+
discount = Discount.find_by(code: code)
|
|
144
|
+
self.discount_amount = discount&.calculate(calculate_subtotal) || 0
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# app/models/concerns/order_status.rb
|
|
149
|
+
module OrderStatus
|
|
150
|
+
extend ActiveSupport::Concern
|
|
151
|
+
|
|
152
|
+
included do
|
|
153
|
+
enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3, cancelled: 4 }
|
|
154
|
+
|
|
155
|
+
after_update :handle_status_change, if: :saved_change_to_status?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def can_cancel?
|
|
159
|
+
pending? || confirmed?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def can_ship?
|
|
163
|
+
confirmed? && line_items.all?(&:in_stock?)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def handle_status_change
|
|
169
|
+
case status
|
|
170
|
+
when "confirmed" then OrderMailer.confirmed(self).deliver_later
|
|
171
|
+
when "shipped" then OrderMailer.shipped(self).deliver_later
|
|
172
|
+
when "cancelled" then process_cancellation
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def process_cancellation
|
|
177
|
+
line_items.each { |li| li.product.increment!(:stock, li.quantity) }
|
|
178
|
+
RefundService.call(self)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# app/models/order.rb
|
|
183
|
+
class Order < ApplicationRecord
|
|
184
|
+
include OrderCalculations
|
|
185
|
+
include OrderStatus
|
|
186
|
+
include OrderNotifications
|
|
187
|
+
include OrderValidations
|
|
188
|
+
include OrderScopes
|
|
189
|
+
|
|
190
|
+
# Model is now 5 lines but still has 500 lines of responsibility
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Why This Is Bad
|
|
195
|
+
|
|
196
|
+
- **Same responsibilities, different files.** The Order model still has calculations, status management, email sending, inventory management, and refund processing — they're just scattered across 5 files instead of 1. The complexity hasn't been reduced.
|
|
197
|
+
- **Not reusable.** `OrderCalculations` only works for orders. No other model can include it. It's not a shared capability — it's an order-specific feature hidden in a concern.
|
|
198
|
+
- **Harder to navigate.** A developer looking at `Order` sees 5 includes and has to open 5 files to understand what the model does. In a single file, they can scroll. With concerns, they play file hopscotch.
|
|
199
|
+
- **Hidden callbacks.** `OrderStatus` adds an `after_update` callback that sends emails and processes refunds. Including `OrderStatus` in the model gives you no indication that saving an order might trigger a refund.
|
|
200
|
+
- **Business logic in concerns.** `process_cancellation` does inventory management and calls `RefundService`. This belongs in a service object (`Orders::CancelService`), not in a model concern.
|
|
201
|
+
|
|
202
|
+
## When To Apply
|
|
203
|
+
|
|
204
|
+
Use model concerns when ALL of these are true:
|
|
205
|
+
|
|
206
|
+
1. **Multiple unrelated models** need the same behavior (at least 2, ideally 3+)
|
|
207
|
+
2. The behavior is a **capability** ("searchable", "sluggable", "auditable"), not a **feature** ("order calculations")
|
|
208
|
+
3. The concern is **self-contained** — it doesn't need to know the model's specific business logic
|
|
209
|
+
4. The concern has a **clear contract** — the model must provide specific columns or methods, documented explicitly
|
|
210
|
+
|
|
211
|
+
## When NOT To Apply
|
|
212
|
+
|
|
213
|
+
- **Don't use concerns to split a fat model.** If the model is too big, extract service objects, form objects, and query objects. Moving code to a concern file doesn't reduce complexity.
|
|
214
|
+
- **Don't create a concern used by one model.** That's just indirection. Keep the code in the model.
|
|
215
|
+
- **Don't put business logic in concerns.** Calculations, status transitions, payment processing, and notification sending are business logic. They belong in service objects.
|
|
216
|
+
- **Don't put callbacks with side effects in concerns.** If a concern adds `after_create :send_welcome_email`, every model that includes it gets that behavior — possibly unintentionally. Side-effect callbacks belong in service objects.
|
|
217
|
+
|
|
218
|
+
## Edge Cases
|
|
219
|
+
|
|
220
|
+
**Concern needs different configuration per model:**
|
|
221
|
+
Use class methods that the model overrides:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
module Archivable
|
|
225
|
+
extend ActiveSupport::Concern
|
|
226
|
+
|
|
227
|
+
class_methods do
|
|
228
|
+
def archive_after
|
|
229
|
+
30.days # Default
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
included do
|
|
234
|
+
scope :archivable, -> { where(created_at: ..archive_after.ago) }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
class Order < ApplicationRecord
|
|
239
|
+
include Archivable
|
|
240
|
+
|
|
241
|
+
def self.archive_after
|
|
242
|
+
90.days # Override
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Testing concerns:**
|
|
248
|
+
Use shared examples that any including model can run:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
RSpec.shared_examples "a searchable model" do
|
|
252
|
+
describe ".search" do
|
|
253
|
+
it "finds records matching the query" do
|
|
254
|
+
matching = create(described_class.model_name.singular, name: "Rubyn Widget")
|
|
255
|
+
non_matching = create(described_class.model_name.singular, name: "Other Thing")
|
|
256
|
+
|
|
257
|
+
results = described_class.search("rubyn")
|
|
258
|
+
expect(results).to include(matching)
|
|
259
|
+
expect(results).not_to include(non_matching)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "returns all records for blank query" do
|
|
263
|
+
create(described_class.model_name.singular)
|
|
264
|
+
expect(described_class.search("")).to eq(described_class.all)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# In each model spec
|
|
270
|
+
RSpec.describe User do
|
|
271
|
+
it_behaves_like "a searchable model"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
RSpec.describe Product do
|
|
275
|
+
it_behaves_like "a searchable model"
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Concern vs STI (Single Table Inheritance):**
|
|
280
|
+
Use STI when models share a database table and have an "is-a" relationship (AdminUser is a User). Use concerns when models share behavior but have separate tables and no inheritance relationship (both User and Product are searchable, but a User is not a Product).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Rails: Skinny Controllers
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Controllers handle HTTP concerns only: receive params, delegate to a service or model, respond with the appropriate format and status code. Business logic, data transformation, and side effects live elsewhere.
|
|
6
|
+
|
|
7
|
+
A well-structured controller action follows this shape:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/controllers/orders_controller.rb
|
|
11
|
+
class OrdersController < ApplicationController
|
|
12
|
+
before_action :set_order, only: [:show, :update, :destroy]
|
|
13
|
+
|
|
14
|
+
def index
|
|
15
|
+
@orders = Current.user.orders.recent.page(params[:page])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
result = Orders::CreateService.call(order_params, current_user)
|
|
23
|
+
|
|
24
|
+
if result.success?
|
|
25
|
+
redirect_to result.order, notice: "Order placed."
|
|
26
|
+
else
|
|
27
|
+
@order = result.order
|
|
28
|
+
render :new, status: :unprocessable_entity
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
if @order.update(order_params)
|
|
34
|
+
redirect_to @order, notice: "Order updated."
|
|
35
|
+
else
|
|
36
|
+
render :edit, status: :unprocessable_entity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
@order.destroy
|
|
42
|
+
redirect_to orders_path, notice: "Order deleted."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def set_order
|
|
48
|
+
@order = Current.user.orders.find(params[:id])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def order_params
|
|
52
|
+
params.require(:order).permit(:shipping_address, :notes)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Key principles:
|
|
58
|
+
- Each action is 1-5 lines of logic (excluding private methods)
|
|
59
|
+
- `before_action` for shared record loading
|
|
60
|
+
- Private methods only for param filtering and record lookup
|
|
61
|
+
- No business logic, no conditional branching beyond success/failure
|
|
62
|
+
- Delegate complex operations to service objects
|
|
63
|
+
- Use `Current` attributes or scoped queries — never `Order.find(params[:id])` without scoping to the user
|
|
64
|
+
|
|
65
|
+
When an action needs more than simple CRUD, add a new controller rather than a new action:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Instead of orders_controller#cancel, create:
|
|
69
|
+
# app/controllers/order_cancellations_controller.rb
|
|
70
|
+
class OrderCancellationsController < ApplicationController
|
|
71
|
+
def create
|
|
72
|
+
@order = Current.user.orders.find(params[:order_id])
|
|
73
|
+
result = Orders::CancelService.call(@order, current_user)
|
|
74
|
+
|
|
75
|
+
if result.success?
|
|
76
|
+
redirect_to @order, notice: "Order cancelled."
|
|
77
|
+
else
|
|
78
|
+
redirect_to @order, alert: result.error
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# config/routes.rb
|
|
84
|
+
resources :orders do
|
|
85
|
+
resource :cancellation, only: [:create]
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Why This Is Good
|
|
90
|
+
|
|
91
|
+
- **Readable at a glance.** A new developer can open any controller and understand what every endpoint does in seconds. There's no business logic to parse — just HTTP flow.
|
|
92
|
+
- **Testable via request specs.** Thin controllers are tested through HTTP (request specs), which tests the real behavior. No need for brittle controller unit tests.
|
|
93
|
+
- **Consistent across the team.** Every controller follows the same 5-line-action pattern. Code reviews are faster because the shape is predictable.
|
|
94
|
+
- **RESTful by design.** Adding new controllers instead of new actions keeps the app RESTful. `OrderCancellationsController#create` is clearer than `OrdersController#cancel`.
|
|
95
|
+
- **Forces good architecture.** When you can't put logic in the controller, you're forced to find the right home for it — service objects, models, form objects, or query objects.
|
|
96
|
+
|
|
97
|
+
## Anti-Pattern
|
|
98
|
+
|
|
99
|
+
A controller with business logic, conditional branching, direct mailer calls, and inline data transformations:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class OrdersController < ApplicationController
|
|
103
|
+
def create
|
|
104
|
+
@order = Order.new(order_params)
|
|
105
|
+
@order.user = current_user
|
|
106
|
+
|
|
107
|
+
# Business logic in controller
|
|
108
|
+
@order.line_items.each do |item|
|
|
109
|
+
product = Product.find(item.product_id)
|
|
110
|
+
if product.stock < item.quantity
|
|
111
|
+
flash[:alert] = "#{product.name} only has #{product.stock} left"
|
|
112
|
+
render :new and return
|
|
113
|
+
end
|
|
114
|
+
item.unit_price = product.price
|
|
115
|
+
item.total = product.price * item.quantity
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@order.subtotal = @order.line_items.sum(&:total)
|
|
119
|
+
@order.tax = @order.subtotal * 0.08
|
|
120
|
+
@order.total = @order.subtotal + @order.tax
|
|
121
|
+
|
|
122
|
+
if current_user.loyalty_points >= 100
|
|
123
|
+
discount = (@order.total * 0.1).round(2)
|
|
124
|
+
@order.discount = discount
|
|
125
|
+
@order.total -= discount
|
|
126
|
+
current_user.update(loyalty_points: current_user.loyalty_points - 100)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if @order.save
|
|
130
|
+
@order.line_items.each do |item|
|
|
131
|
+
product = Product.find(item.product_id)
|
|
132
|
+
product.update!(stock: product.stock - item.quantity)
|
|
133
|
+
end
|
|
134
|
+
OrderMailer.confirmation(@order).deliver_later
|
|
135
|
+
AdminMailer.new_order(@order).deliver_later if @order.total > 500
|
|
136
|
+
redirect_to @order, notice: "Order placed!"
|
|
137
|
+
else
|
|
138
|
+
render :new
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Why This Is Bad
|
|
145
|
+
|
|
146
|
+
- **50+ lines for one action.** A developer has to read the entire method to understand what creating an order involves. The HTTP concerns (params, render, redirect) are buried among price calculations and stock updates.
|
|
147
|
+
- **Untestable in isolation.** To test order creation you must make HTTP requests, set up products with stock levels, loyalty points, and assert mailer deliveries — all in one test.
|
|
148
|
+
- **Logic is trapped.** When you need to create orders from an API endpoint, a Sidekiq job, or the console, you can't. The logic is locked inside an HTTP controller action.
|
|
149
|
+
- **Multiple responsibilities.** This action validates stock, calculates prices, applies discounts, manages loyalty points, updates inventory, and sends emails. Changing any one of these risks breaking the others.
|
|
150
|
+
- **Missing status codes.** The failure case renders `:new` without `status: :unprocessable_entity`, which breaks Turbo and returns 200 on validation failure.
|
|
151
|
+
|
|
152
|
+
## When To Apply
|
|
153
|
+
|
|
154
|
+
Always. Every Rails controller should follow skinny principles. The question isn't "should this controller be skinny?" — it's "where does the extracted logic go?"
|
|
155
|
+
|
|
156
|
+
- Simple CRUD (save one record, no side effects) → logic stays in the model
|
|
157
|
+
- Complex creation/updates (multiple models, side effects) → service object
|
|
158
|
+
- Complex validations (virtual attributes, multi-model validation) → form object
|
|
159
|
+
- Complex queries (reporting, search, filtering) → query object
|
|
160
|
+
- Shared controller behavior (auth, pagination, error handling) → controller concern
|
|
161
|
+
|
|
162
|
+
## When NOT To Apply
|
|
163
|
+
|
|
164
|
+
There is no case where a fat controller is the right choice. However, there are cases where extracting logic is premature:
|
|
165
|
+
|
|
166
|
+
- A 3-line create action that saves a record and redirects does NOT need a service object. The controller is already skinny.
|
|
167
|
+
- Simple `before_action` callbacks for setting records are fine in the controller. They don't need extraction.
|
|
168
|
+
- Standard `params.require().permit()` belongs in the controller, not in a separate class (unless the params logic itself is complex — then use a form object).
|
|
169
|
+
|
|
170
|
+
## Edge Cases
|
|
171
|
+
|
|
172
|
+
**The action is 8 lines but all the logic is param handling:**
|
|
173
|
+
That's a sign you need a form object, not a service object. If you're transforming, nesting, or conditionally including params, extract to a form object.
|
|
174
|
+
|
|
175
|
+
**You need to return different formats (HTML, JSON, CSV):**
|
|
176
|
+
Use `respond_to` blocks in the controller — format selection IS an HTTP concern. But keep the data preparation in a service or query object.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
def index
|
|
180
|
+
@orders = Orders::SearchQuery.call(search_params)
|
|
181
|
+
respond_to do |format|
|
|
182
|
+
format.html
|
|
183
|
+
format.json { render json: OrderSerializer.new(@orders) }
|
|
184
|
+
format.csv { send_data Orders::CsvExporter.call(@orders), filename: "orders.csv" }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**The team uses `before_action` for everything:**
|
|
190
|
+
Before actions are good for record loading and auth checks. They're bad for business logic. If a before action does more than `set_X` or `authorize_X`, it's hiding complexity in the wrong place.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Rails: Engines
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
A Rails engine is a miniature Rails application that can be mounted inside a host app. Engines package controllers, models, views, routes, and assets into a self-contained, reusable component. Use them for features that are isolated from the host app's domain — admin panels, dev tools, billing dashboards, and embeddable widgets.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Generate a mountable engine
|
|
9
|
+
# rails plugin new rubyn --mountable
|
|
10
|
+
|
|
11
|
+
# lib/rubyn/engine.rb
|
|
12
|
+
module Rubyn
|
|
13
|
+
class Engine < ::Rails::Engine
|
|
14
|
+
isolate_namespace Rubyn
|
|
15
|
+
|
|
16
|
+
# Engine-specific configuration
|
|
17
|
+
config.generators do |g|
|
|
18
|
+
g.test_framework :rspec
|
|
19
|
+
g.assets false # Engine manages its own assets
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Initializers run when the host app boots
|
|
23
|
+
initializer "rubyn.assets" do |app|
|
|
24
|
+
app.config.assets.precompile += %w[rubyn/application.css rubyn/application.js] if app.config.respond_to?(:assets)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Engine routes — completely isolated from host app
|
|
32
|
+
# config/routes.rb (inside the engine)
|
|
33
|
+
Rubyn::Engine.routes.draw do
|
|
34
|
+
root to: "dashboard#show"
|
|
35
|
+
|
|
36
|
+
resources :files, only: [:index, :show]
|
|
37
|
+
resource :agent, only: [:show, :create]
|
|
38
|
+
|
|
39
|
+
namespace :ai do
|
|
40
|
+
post :refactor
|
|
41
|
+
post :review
|
|
42
|
+
post :spec
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
resource :settings, only: [:show, :update]
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Host app mounts the engine
|
|
51
|
+
# config/routes.rb (host app)
|
|
52
|
+
Rails.application.routes.draw do
|
|
53
|
+
mount Rubyn::Engine => "/rubyn" if Rails.env.development?
|
|
54
|
+
|
|
55
|
+
# Host app's own routes
|
|
56
|
+
resources :orders
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Engine Controllers
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# app/controllers/rubyn/application_controller.rb
|
|
64
|
+
module Rubyn
|
|
65
|
+
class ApplicationController < ActionController::Base
|
|
66
|
+
layout "rubyn/application" # Engine's own layout
|
|
67
|
+
|
|
68
|
+
before_action :verify_development_environment
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def verify_development_environment
|
|
73
|
+
head :forbidden unless Rails.env.development?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Engine reads credentials from the user's local config
|
|
77
|
+
def rubyn_api_key
|
|
78
|
+
@rubyn_api_key ||= Rubyn::Config.api_key
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# app/controllers/rubyn/dashboard_controller.rb
|
|
84
|
+
module Rubyn
|
|
85
|
+
class DashboardController < ApplicationController
|
|
86
|
+
def show
|
|
87
|
+
@project_info = Rubyn::ProjectScanner.scan(Rails.root)
|
|
88
|
+
@credit_balance = Rubyn::ApiClient.new(rubyn_api_key).balance
|
|
89
|
+
@recent_activity = Rubyn::ApiClient.new(rubyn_api_key).recent_interactions(limit: 10)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Engine Views (Self-Contained)
|
|
96
|
+
|
|
97
|
+
```erb
|
|
98
|
+
<%# app/views/layouts/rubyn/application.html.erb %>
|
|
99
|
+
<%# Engine has its own layout — doesn't depend on host app's layout %>
|
|
100
|
+
<!DOCTYPE html>
|
|
101
|
+
<html>
|
|
102
|
+
<head>
|
|
103
|
+
<title>Rubyn</title>
|
|
104
|
+
<%= csrf_meta_tags %>
|
|
105
|
+
<%= stylesheet_link_tag "rubyn/application", media: "all" %>
|
|
106
|
+
</head>
|
|
107
|
+
<body class="rubyn-app">
|
|
108
|
+
<nav class="rubyn-nav">
|
|
109
|
+
<%= link_to "Dashboard", rubyn.root_path %>
|
|
110
|
+
<%= link_to "Files", rubyn.files_path %>
|
|
111
|
+
<%= link_to "Agent", rubyn.agent_path %>
|
|
112
|
+
<%= link_to "Settings", rubyn.settings_path %>
|
|
113
|
+
</nav>
|
|
114
|
+
|
|
115
|
+
<main>
|
|
116
|
+
<%= yield %>
|
|
117
|
+
</main>
|
|
118
|
+
|
|
119
|
+
<%= javascript_include_tag "rubyn/application" %>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Namespace Isolation
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# isolate_namespace ensures the engine doesn't pollute the host app
|
|
128
|
+
|
|
129
|
+
# Engine model — lives in rubyn_ prefixed tables
|
|
130
|
+
module Rubyn
|
|
131
|
+
class Interaction < ApplicationRecord
|
|
132
|
+
# Table: rubyn_interactions (not interactions)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Engine routes are namespaced
|
|
137
|
+
rubyn.root_path # => "/rubyn"
|
|
138
|
+
rubyn.files_path # => "/rubyn/files"
|
|
139
|
+
main_app.orders_path # => "/orders" (host app routes)
|
|
140
|
+
|
|
141
|
+
# In engine views, explicitly reference host vs engine routes:
|
|
142
|
+
<%= link_to "Back to app", main_app.root_path %>
|
|
143
|
+
<%= link_to "Dashboard", rubyn.root_path %>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Why This Is Good
|
|
147
|
+
|
|
148
|
+
- **Complete isolation.** The engine has its own namespace, routes, views, assets, and optionally its own database tables. It can't accidentally conflict with the host app's controllers or styles.
|
|
149
|
+
- **Mountable with one line.** `mount Rubyn::Engine => "/rubyn"` — the host app adds one line and gets a full-featured dev dashboard.
|
|
150
|
+
- **Development-only by default.** `if Rails.env.development?` ensures the engine never accidentally runs in production.
|
|
151
|
+
- **Self-contained assets.** The engine ships its own CSS and JavaScript. No dependency on the host app's Tailwind config, asset pipeline, or build tools.
|
|
152
|
+
- **Shareable across projects.** Package the engine as a gem, install it in any Rails project, mount it — instant dev tools.
|
|
153
|
+
|
|
154
|
+
## When To Apply
|
|
155
|
+
|
|
156
|
+
- **Dev tools** — dashboards, profilers, debug panels, AI coding assistants. Features that help developers but shouldn't exist in production.
|
|
157
|
+
- **Admin panels** — self-contained admin interfaces with their own auth, layout, and styles.
|
|
158
|
+
- **Shared features across apps** — authentication, billing, notifications, CMS. Build once, mount in multiple apps.
|
|
159
|
+
- **Rubyn itself** — the mountable web UI is an engine inside the `rubyn` gem.
|
|
160
|
+
|
|
161
|
+
## When NOT To Apply
|
|
162
|
+
|
|
163
|
+
- **Feature that's tightly coupled to the host app's domain.** If the feature needs to share models, validations, and business logic with the host app, it's not a good engine candidate — it's just part of the app.
|
|
164
|
+
- **Simple shared code.** A few utility methods shared across apps should be a gem with modules, not an engine with controllers and views.
|
|
165
|
+
- **One-off features.** Don't engine-ify something used in only one app. Engines add architectural overhead.
|
|
166
|
+
|
|
167
|
+
## Edge Cases
|
|
168
|
+
|
|
169
|
+
**Engine accessing host app's models:**
|
|
170
|
+
```ruby
|
|
171
|
+
# The engine can reference host app models if they exist
|
|
172
|
+
class Rubyn::DashboardController < Rubyn::ApplicationController
|
|
173
|
+
def show
|
|
174
|
+
# Access host app's models — works but creates coupling
|
|
175
|
+
@user_count = ::User.count if defined?(::User)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
Minimize this — the engine should work without knowing the host app's models.
|
|
180
|
+
|
|
181
|
+
**Testing engines:**
|
|
182
|
+
```ruby
|
|
183
|
+
# The engine includes a dummy Rails app for testing
|
|
184
|
+
# test/dummy/ contains a minimal Rails app that mounts the engine
|
|
185
|
+
# spec/dummy/ for RSpec
|
|
186
|
+
|
|
187
|
+
# spec/requests/rubyn/dashboard_spec.rb
|
|
188
|
+
RSpec.describe "Rubyn::Dashboard", type: :request do
|
|
189
|
+
it "shows the dashboard" do
|
|
190
|
+
get rubyn.root_path
|
|
191
|
+
expect(response).to have_http_status(:ok)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Engine migrations:**
|
|
197
|
+
```bash
|
|
198
|
+
# Copy engine migrations to host app
|
|
199
|
+
rails rubyn:install:migrations
|
|
200
|
+
rails db:migrate
|
|
201
|
+
```
|