anima-core 0.2.1 → 1.0.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 +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# Draper Anti-Patterns and Solutions
|
|
2
|
+
|
|
3
|
+
## Anti-Pattern 1: Fat Decorator
|
|
4
|
+
|
|
5
|
+
### Problem
|
|
6
|
+
|
|
7
|
+
Stuffing all presentation logic into a single decorator creates a maintenance nightmare:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# BAD: 500+ lines, 40+ methods
|
|
11
|
+
class UserDecorator < ApplicationDecorator
|
|
12
|
+
delegate_all
|
|
13
|
+
|
|
14
|
+
# Profile methods
|
|
15
|
+
def full_name; end
|
|
16
|
+
def avatar_tag; end
|
|
17
|
+
def profile_summary; end
|
|
18
|
+
def bio_excerpt; end
|
|
19
|
+
|
|
20
|
+
# Admin methods
|
|
21
|
+
def admin_badge; end
|
|
22
|
+
def permissions_list; end
|
|
23
|
+
def audit_log_link; end
|
|
24
|
+
def role_selector; end
|
|
25
|
+
|
|
26
|
+
# Social methods
|
|
27
|
+
def twitter_link; end
|
|
28
|
+
def facebook_link; end
|
|
29
|
+
def linkedin_badge; end
|
|
30
|
+
|
|
31
|
+
# Notification methods
|
|
32
|
+
def notification_count; end
|
|
33
|
+
def unread_badge; end
|
|
34
|
+
def notification_dropdown; end
|
|
35
|
+
|
|
36
|
+
# Settings methods
|
|
37
|
+
def preferences_form; end
|
|
38
|
+
def privacy_settings; end
|
|
39
|
+
|
|
40
|
+
# ... 30 more methods
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Why It's Bad
|
|
45
|
+
|
|
46
|
+
- **Single Responsibility Violation**: One class handles many contexts
|
|
47
|
+
- **Divergent Change**: Changes to admin views require editing user profile decorator
|
|
48
|
+
- **Hard to Test**: Tests become large and unfocused
|
|
49
|
+
- **Cognitive Overload**: Developers must understand entire decorator to make changes
|
|
50
|
+
|
|
51
|
+
### Solution: Context-Specific Decorators
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# GOOD: Base decorator with shared methods
|
|
55
|
+
class UserDecorator < ApplicationDecorator
|
|
56
|
+
delegate_all
|
|
57
|
+
|
|
58
|
+
def full_name
|
|
59
|
+
"#{first_name} #{last_name}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def avatar_tag(size: :medium)
|
|
63
|
+
h.image_tag(avatar_url(size), alt: full_name, class: "avatar avatar-#{size}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Admin-specific presentation
|
|
68
|
+
class Users::AdminDecorator < UserDecorator
|
|
69
|
+
def admin_badge
|
|
70
|
+
return unless admin?
|
|
71
|
+
h.content_tag(:span, "Admin", class: "badge badge-danger")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def permissions_list
|
|
75
|
+
h.content_tag(:ul) do
|
|
76
|
+
h.safe_join(permissions.map { |p| h.content_tag(:li, p) })
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def audit_log_link
|
|
81
|
+
h.link_to("View Audit Log", h.admin_user_audit_path(object))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Profile page presentation
|
|
86
|
+
class Users::ProfileDecorator < UserDecorator
|
|
87
|
+
def bio_excerpt
|
|
88
|
+
h.truncate(bio, length: 200, separator: ' ')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def social_links
|
|
92
|
+
links = []
|
|
93
|
+
links << twitter_link if twitter_handle.present?
|
|
94
|
+
links << linkedin_link if linkedin_url.present?
|
|
95
|
+
h.safe_join(links, " | ")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Notification-specific presentation
|
|
100
|
+
class Users::NotificationDecorator < UserDecorator
|
|
101
|
+
def unread_count_badge
|
|
102
|
+
count = unread_notifications_count
|
|
103
|
+
return if count.zero?
|
|
104
|
+
|
|
105
|
+
h.content_tag(:span, count, class: "badge badge-primary")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Usage
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Admin panel
|
|
114
|
+
@user = Users::AdminDecorator.decorate(User.find(params[:id]))
|
|
115
|
+
|
|
116
|
+
# Public profile
|
|
117
|
+
@user = Users::ProfileDecorator.decorate(User.find(params[:id]))
|
|
118
|
+
|
|
119
|
+
# Header notification widget
|
|
120
|
+
@user = Users::NotificationDecorator.decorate(current_user)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Anti-Pattern 2: N+1 Queries
|
|
126
|
+
|
|
127
|
+
### Problem
|
|
128
|
+
|
|
129
|
+
Decorating without eager loading causes database performance issues:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Controller
|
|
133
|
+
def index
|
|
134
|
+
@posts = Post.all.decorate # No eager loading!
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Decorator
|
|
138
|
+
class PostDecorator < ApplicationDecorator
|
|
139
|
+
delegate_all
|
|
140
|
+
|
|
141
|
+
def author_name
|
|
142
|
+
author.name # N+1 query for each post!
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def comments_count
|
|
146
|
+
"#{comments.count} comments" # Another N+1!
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Why It's Bad
|
|
152
|
+
|
|
153
|
+
- Each `@post.author_name` triggers a separate query
|
|
154
|
+
- 100 posts = 100+ queries instead of 2
|
|
155
|
+
- Performance degrades linearly with collection size
|
|
156
|
+
|
|
157
|
+
### Solution: Eager Load Before Decorating
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# GOOD: Eager load associations first
|
|
161
|
+
class PostsController < ApplicationController
|
|
162
|
+
def index
|
|
163
|
+
@posts = Post.includes(:author, :comments).all.decorate
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Solution: Use Counter Caches
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# Migration
|
|
172
|
+
add_column :posts, :comments_count, :integer, default: 0, null: false
|
|
173
|
+
|
|
174
|
+
# Model
|
|
175
|
+
class Comment < ApplicationRecord
|
|
176
|
+
belongs_to :post, counter_cache: true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Decorator - no query needed
|
|
180
|
+
class PostDecorator < ApplicationDecorator
|
|
181
|
+
def comments_count_badge
|
|
182
|
+
"#{comments_count} comments" # Uses counter cache
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Solution: Batch Loading for Complex Cases
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class PostsController < ApplicationController
|
|
191
|
+
def index
|
|
192
|
+
@posts = Post.includes(:author)
|
|
193
|
+
.with_attached_images # Active Storage
|
|
194
|
+
.with_rich_text_content # Action Text
|
|
195
|
+
.decorate
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Anti-Pattern 3: Decorating Too Early
|
|
203
|
+
|
|
204
|
+
### Problem
|
|
205
|
+
|
|
206
|
+
Using decorated objects in business logic:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# BAD: Passing decorator to service
|
|
210
|
+
class PublishService
|
|
211
|
+
def call(decorated_post)
|
|
212
|
+
decorated_post.update(published_at: Time.current)
|
|
213
|
+
decorated_post.notify_subscribers # Is this decorator or model method?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Controller
|
|
218
|
+
def publish
|
|
219
|
+
@post = Post.find(params[:id]).decorate
|
|
220
|
+
PublishService.new.call(@post) # Decorated object in service!
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Why It's Bad
|
|
225
|
+
|
|
226
|
+
- Services should work with models, not decorators
|
|
227
|
+
- Confuses presentation and business logic
|
|
228
|
+
- Makes testing harder (do you stub model or decorator?)
|
|
229
|
+
- Decorator methods might be called accidentally in business logic
|
|
230
|
+
|
|
231
|
+
### Solution: Decorate at the Last Moment
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
# GOOD: Services receive models
|
|
235
|
+
class PublishService
|
|
236
|
+
def call(post)
|
|
237
|
+
post.update(published_at: Time.current)
|
|
238
|
+
NotificationJob.perform_later(post.id)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Controller - decorate only before render
|
|
243
|
+
class PostsController < ApplicationController
|
|
244
|
+
def publish
|
|
245
|
+
@post = Post.find(params[:id])
|
|
246
|
+
PublishService.new.call(@post) # Model, not decorator
|
|
247
|
+
|
|
248
|
+
@post = @post.decorate # Decorate right before render
|
|
249
|
+
render :show
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Rule of Thumb
|
|
255
|
+
|
|
256
|
+
> "Decorate at the last moment, right before you render the view."
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Anti-Pattern 4: Business Logic in Decorators
|
|
261
|
+
|
|
262
|
+
### Problem
|
|
263
|
+
|
|
264
|
+
Putting calculations, validations, or state changes in decorators:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
# BAD: Business logic in decorator
|
|
268
|
+
class OrderDecorator < ApplicationDecorator
|
|
269
|
+
delegate_all
|
|
270
|
+
|
|
271
|
+
def apply_discount(code)
|
|
272
|
+
discount = Discount.find_by(code: code)
|
|
273
|
+
object.update(discount_amount: discount.amount) # State change!
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def calculate_tax
|
|
277
|
+
subtotal * tax_rate # Business calculation!
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def valid_for_shipping?
|
|
281
|
+
items.any? && shipping_address.present? # Validation!
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Why It's Bad
|
|
287
|
+
|
|
288
|
+
- Violates separation of concerns
|
|
289
|
+
- Business logic becomes scattered
|
|
290
|
+
- Hard to test business rules in isolation
|
|
291
|
+
- Decorators become coupled to domain logic
|
|
292
|
+
|
|
293
|
+
### Solution: Keep Business Logic in Models/Services
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Model handles business logic
|
|
297
|
+
class Order < ApplicationRecord
|
|
298
|
+
def calculate_tax
|
|
299
|
+
subtotal * tax_rate
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def valid_for_shipping?
|
|
303
|
+
items.any? && shipping_address.present?
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Service handles state changes
|
|
308
|
+
class ApplyDiscountService
|
|
309
|
+
def call(order, code)
|
|
310
|
+
discount = Discount.find_by!(code: code)
|
|
311
|
+
order.update!(discount_amount: discount.amount)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# GOOD: Decorator only formats for display
|
|
316
|
+
class OrderDecorator < ApplicationDecorator
|
|
317
|
+
delegate_all
|
|
318
|
+
|
|
319
|
+
def formatted_tax
|
|
320
|
+
h.number_to_currency(calculate_tax) # Delegates to model
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def shipping_status_badge
|
|
324
|
+
css = valid_for_shipping? ? "success" : "warning"
|
|
325
|
+
text = valid_for_shipping? ? "Ready to Ship" : "Missing Info"
|
|
326
|
+
h.content_tag(:span, text, class: "badge badge-#{css}")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Anti-Pattern 5: Circular Decoration Reference
|
|
334
|
+
|
|
335
|
+
### Problem
|
|
336
|
+
|
|
337
|
+
Models referencing their decorators:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# BAD: Model knows about decorator
|
|
341
|
+
class Post < ApplicationRecord
|
|
342
|
+
def display_title
|
|
343
|
+
PostDecorator.new(self).formatted_title
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def render_preview
|
|
347
|
+
decorator = decorate
|
|
348
|
+
decorator.preview_card
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Why It's Bad
|
|
354
|
+
|
|
355
|
+
- Creates circular dependency (model → decorator → model)
|
|
356
|
+
- Violates unidirectional data flow
|
|
357
|
+
- Makes models dependent on presentation layer
|
|
358
|
+
- Complicates testing
|
|
359
|
+
|
|
360
|
+
### Solution: Keep Models Unaware of Decorators
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# GOOD: Model has no decorator knowledge
|
|
364
|
+
class Post < ApplicationRecord
|
|
365
|
+
def title_with_status
|
|
366
|
+
"#{title} (#{status})" # Pure model method, no formatting
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# View helper or decorator handles presentation
|
|
371
|
+
class PostDecorator < ApplicationDecorator
|
|
372
|
+
delegate_all
|
|
373
|
+
|
|
374
|
+
def formatted_title
|
|
375
|
+
h.content_tag(:h1, title_with_status, class: css_class_for_status)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Anti-Pattern 6: Using Decorators in Background Jobs
|
|
383
|
+
|
|
384
|
+
### Problem
|
|
385
|
+
|
|
386
|
+
Passing decorated objects to background jobs:
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
# BAD: Decorator in job
|
|
390
|
+
class NotificationJob < ApplicationJob
|
|
391
|
+
def perform(decorated_user)
|
|
392
|
+
UserMailer.notification(decorated_user).deliver_now
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Controller
|
|
397
|
+
NotificationJob.perform_later(@user.decorate)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Why It's Bad
|
|
401
|
+
|
|
402
|
+
- Decorators can't be serialized properly
|
|
403
|
+
- View context not available in background
|
|
404
|
+
- Job might fail or behave unexpectedly
|
|
405
|
+
|
|
406
|
+
### Solution: Pass IDs, Decorate in Mailer if Needed
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
# GOOD: Job receives ID
|
|
410
|
+
class NotificationJob < ApplicationJob
|
|
411
|
+
def perform(user_id)
|
|
412
|
+
user = User.find(user_id)
|
|
413
|
+
UserMailer.notification(user).deliver_now
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Mailer can decorate if needed
|
|
418
|
+
class UserMailer < ApplicationMailer
|
|
419
|
+
def notification(user)
|
|
420
|
+
@user = user.decorate
|
|
421
|
+
mail(to: @user.email)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Controller
|
|
426
|
+
NotificationJob.perform_later(@user.id)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Anti-Pattern 7: Inconsistent Decoration
|
|
432
|
+
|
|
433
|
+
### Problem
|
|
434
|
+
|
|
435
|
+
Mixing decorated and undecorated objects in the same context:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# BAD: Inconsistent
|
|
439
|
+
class PostsController < ApplicationController
|
|
440
|
+
def index
|
|
441
|
+
@featured_post = Post.featured.first.decorate
|
|
442
|
+
@recent_posts = Post.recent.limit(5) # Not decorated!
|
|
443
|
+
@popular_posts = Post.popular.decorate
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Why It's Bad
|
|
449
|
+
|
|
450
|
+
- Views must handle both types
|
|
451
|
+
- Leads to bugs when calling decorator methods on raw models
|
|
452
|
+
- Confusing for developers
|
|
453
|
+
|
|
454
|
+
### Solution: Be Consistent
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
# GOOD: All decorated
|
|
458
|
+
class PostsController < ApplicationController
|
|
459
|
+
def index
|
|
460
|
+
@featured_post = Post.featured.first.decorate
|
|
461
|
+
@recent_posts = Post.recent.limit(5).decorate
|
|
462
|
+
@popular_posts = Post.popular.decorate
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Or establish clear naming convention
|
|
467
|
+
class PostsController < ApplicationController
|
|
468
|
+
def index
|
|
469
|
+
@featured_post = Post.featured.first.decorate
|
|
470
|
+
@recent_post_models = Post.recent.limit(5) # Clear it's not decorated
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Anti-Pattern 8: Overriding Model Methods Incorrectly
|
|
478
|
+
|
|
479
|
+
### Problem
|
|
480
|
+
|
|
481
|
+
Overriding model methods without maintaining compatibility:
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
# BAD: Breaks model contract
|
|
485
|
+
class UserDecorator < ApplicationDecorator
|
|
486
|
+
delegate_all
|
|
487
|
+
|
|
488
|
+
def email
|
|
489
|
+
# Original returns string, now returns HTML!
|
|
490
|
+
h.mail_to(object.email, object.email)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Why It's Bad
|
|
496
|
+
|
|
497
|
+
- Code expecting string gets HTML
|
|
498
|
+
- Breaks form helpers that use `email`
|
|
499
|
+
- Violates Liskov Substitution Principle
|
|
500
|
+
|
|
501
|
+
### Solution: Use Distinct Method Names
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
# GOOD: Separate presentation method
|
|
505
|
+
class UserDecorator < ApplicationDecorator
|
|
506
|
+
delegate_all
|
|
507
|
+
|
|
508
|
+
def email_link
|
|
509
|
+
h.mail_to(email, email)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def formatted_email
|
|
513
|
+
h.content_tag(:span, email, class: "email")
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Anti-Pattern 9: Heavy Processing in Decorators
|
|
521
|
+
|
|
522
|
+
### Problem
|
|
523
|
+
|
|
524
|
+
Performing expensive operations in decorator methods:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
# BAD: Expensive operations
|
|
528
|
+
class ReportDecorator < ApplicationDecorator
|
|
529
|
+
delegate_all
|
|
530
|
+
|
|
531
|
+
def summary_chart
|
|
532
|
+
data = object.calculate_statistics # Expensive!
|
|
533
|
+
ChartGenerator.new(data).to_svg # More processing!
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Why It's Bad
|
|
539
|
+
|
|
540
|
+
- Called multiple times per request if accessed multiple times
|
|
541
|
+
- No caching by default
|
|
542
|
+
- View rendering becomes slow
|
|
543
|
+
|
|
544
|
+
### Solution: Memoize or Pre-compute
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
# GOOD: Memoize expensive operations
|
|
548
|
+
class ReportDecorator < ApplicationDecorator
|
|
549
|
+
delegate_all
|
|
550
|
+
|
|
551
|
+
def summary_chart
|
|
552
|
+
@summary_chart ||= generate_chart
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
private
|
|
556
|
+
|
|
557
|
+
def generate_chart
|
|
558
|
+
ChartGenerator.new(object.statistics).to_svg
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Better: Pre-compute in model/service
|
|
563
|
+
class Report < ApplicationRecord
|
|
564
|
+
def statistics
|
|
565
|
+
@statistics ||= StatisticsCalculator.new(self).call
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Anti-Pattern 10: Not Using ApplicationDecorator
|
|
573
|
+
|
|
574
|
+
### Problem
|
|
575
|
+
|
|
576
|
+
Each decorator inherits directly from Draper::Decorator:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
# BAD: No shared base
|
|
580
|
+
class PostDecorator < Draper::Decorator
|
|
581
|
+
def formatted_date
|
|
582
|
+
created_at.strftime("%B %d, %Y")
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
class CommentDecorator < Draper::Decorator
|
|
587
|
+
def formatted_date
|
|
588
|
+
created_at.strftime("%B %d, %Y") # Duplicated!
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Why It's Bad
|
|
594
|
+
|
|
595
|
+
- Code duplication across decorators
|
|
596
|
+
- No place for shared helper methods
|
|
597
|
+
- Inconsistent formatting
|
|
598
|
+
|
|
599
|
+
### Solution: Use ApplicationDecorator
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
# GOOD: Shared base class
|
|
603
|
+
class ApplicationDecorator < Draper::Decorator
|
|
604
|
+
def formatted_date(date = created_at, format: :long)
|
|
605
|
+
return "N/A" if date.blank?
|
|
606
|
+
h.l(date, format:)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def formatted_currency(amount)
|
|
610
|
+
h.number_to_currency(amount)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
class PostDecorator < ApplicationDecorator
|
|
615
|
+
delegate_all
|
|
616
|
+
# formatted_date inherited
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
class CommentDecorator < ApplicationDecorator
|
|
620
|
+
delegate_all
|
|
621
|
+
# formatted_date inherited
|
|
622
|
+
end
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Summary Checklist
|
|
628
|
+
|
|
629
|
+
Before committing decorator code, verify:
|
|
630
|
+
|
|
631
|
+
- [ ] Decorator only contains presentation logic
|
|
632
|
+
- [ ] No business logic, validations, or state changes
|
|
633
|
+
- [ ] Controller eager loads associations before decorating
|
|
634
|
+
- [ ] Decoration happens right before rendering
|
|
635
|
+
- [ ] No circular references between model and decorator
|
|
636
|
+
- [ ] Method names don't override model methods with different return types
|
|
637
|
+
- [ ] Expensive operations are memoized
|
|
638
|
+
- [ ] All related decorators inherit from ApplicationDecorator
|
|
639
|
+
- [ ] Decorator isn't too large (consider splitting if >200 lines)
|
|
640
|
+
- [ ] Background jobs receive IDs, not decorated objects
|