anima-core 0.3.0 → 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 +4 -0
- data/README.md +219 -25
- 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 +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -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 -9
- 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 +4 -0
- 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/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 +8 -7
- 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 +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- 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/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- 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 +284 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# ActiveRecord Dirty Tracking Examples
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# BEFORE SAVE - Pending Changes
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
user = User.find(1)
|
|
8
|
+
user.name # => "Alice"
|
|
9
|
+
|
|
10
|
+
# Make a change
|
|
11
|
+
user.name = "Bob"
|
|
12
|
+
|
|
13
|
+
# Check if anything changed
|
|
14
|
+
user.changed? # => true
|
|
15
|
+
user.changes # => {"name" => ["Alice", "Bob"]}
|
|
16
|
+
|
|
17
|
+
# Check specific attribute
|
|
18
|
+
user.name_changed? # => true
|
|
19
|
+
user.name_was # => "Alice" (original value)
|
|
20
|
+
user.name_change # => ["Alice", "Bob"]
|
|
21
|
+
|
|
22
|
+
# Check what will be saved
|
|
23
|
+
user.will_save_change_to_name? # => true
|
|
24
|
+
user.changes_to_save # => {"name" => ["Alice", "Bob"]}
|
|
25
|
+
user.name_in_database # => "Alice"
|
|
26
|
+
|
|
27
|
+
# Check with options
|
|
28
|
+
user.name_changed?(from: "Alice") # => true
|
|
29
|
+
user.name_changed?(to: "Bob") # => true
|
|
30
|
+
user.name_changed?(from: "Alice", to: "Bob") # => true
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# AFTER SAVE - Previous Changes
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
user.save
|
|
37
|
+
|
|
38
|
+
# Now check previous changes (what just happened)
|
|
39
|
+
user.saved_change_to_name? # => true
|
|
40
|
+
user.saved_change_to_name # => ["Alice", "Bob"]
|
|
41
|
+
user.name_before_last_save # => "Alice"
|
|
42
|
+
user.name_previously_was # => "Alice"
|
|
43
|
+
user.previous_changes # => {"name" => ["Alice", "Bob"], "updated_at" => [...]}
|
|
44
|
+
|
|
45
|
+
# Pending changes are now empty
|
|
46
|
+
user.changed? # => false
|
|
47
|
+
user.changes # => {}
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# REVERTING CHANGES
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
user.name = "Changed"
|
|
54
|
+
user.email = "changed@example.com"
|
|
55
|
+
|
|
56
|
+
# Revert single attribute
|
|
57
|
+
user.restore_name!
|
|
58
|
+
user.name # => "Bob" (restored)
|
|
59
|
+
|
|
60
|
+
# Revert all changes
|
|
61
|
+
user.restore_attributes
|
|
62
|
+
user.email # => original value
|
|
63
|
+
|
|
64
|
+
# Reload from database (clears all dirty state)
|
|
65
|
+
user.name = "Something"
|
|
66
|
+
user.reload
|
|
67
|
+
user.changed? # => false
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# USING IN CALLBACKS
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
class User < ApplicationRecord
|
|
74
|
+
after_save :notify_if_email_changed
|
|
75
|
+
after_save :log_role_change
|
|
76
|
+
before_save :set_confirmation_token, if: :will_save_change_to_email?
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def notify_if_email_changed
|
|
81
|
+
if saved_change_to_email?
|
|
82
|
+
old_email, new_email = saved_change_to_email
|
|
83
|
+
UserMailer.email_changed(self, old_email).deliver_later
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def log_role_change
|
|
88
|
+
if saved_change_to_role?
|
|
89
|
+
AuditLog.create!(
|
|
90
|
+
user: self,
|
|
91
|
+
action: "role_changed",
|
|
92
|
+
old_value: role_before_last_save,
|
|
93
|
+
new_value: role
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_confirmation_token
|
|
99
|
+
self.confirmation_token = SecureRandom.urlsafe_base64
|
|
100
|
+
self.confirmed_at = nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# =============================================================================
|
|
105
|
+
# CONDITIONAL UPDATES
|
|
106
|
+
# =============================================================================
|
|
107
|
+
|
|
108
|
+
class Order < ApplicationRecord
|
|
109
|
+
after_save :recalculate_if_items_changed
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def recalculate_if_items_changed
|
|
114
|
+
# Only recalculate if total-affecting fields changed
|
|
115
|
+
if saved_change_to_discount? || saved_change_to_shipping?
|
|
116
|
+
recalculate_total!
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# TRACKING SPECIFIC CHANGES
|
|
123
|
+
# =============================================================================
|
|
124
|
+
|
|
125
|
+
class Profile < ApplicationRecord
|
|
126
|
+
SENSITIVE_FIELDS = %w[email phone_number ssn].freeze
|
|
127
|
+
|
|
128
|
+
after_save :audit_sensitive_changes
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def audit_sensitive_changes
|
|
133
|
+
changed_sensitive = previous_changes.keys & SENSITIVE_FIELDS
|
|
134
|
+
|
|
135
|
+
changed_sensitive.each do |field|
|
|
136
|
+
old_val, new_val = previous_changes[field]
|
|
137
|
+
SecurityAudit.log(
|
|
138
|
+
user: self,
|
|
139
|
+
field:,
|
|
140
|
+
changed_from: mask_value(old_val),
|
|
141
|
+
changed_to: mask_value(new_val)
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def mask_value(value)
|
|
147
|
+
return nil if value.nil?
|
|
148
|
+
"***#{value.to_s.last(4)}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# =============================================================================
|
|
153
|
+
# CHANGED_ATTRIBUTES VS CHANGES
|
|
154
|
+
# =============================================================================
|
|
155
|
+
|
|
156
|
+
user.name = "New"
|
|
157
|
+
user.email = "new@example.com"
|
|
158
|
+
|
|
159
|
+
# changed_attribute_names_to_save - just the names
|
|
160
|
+
user.changed_attribute_names_to_save # => ["name", "email"]
|
|
161
|
+
|
|
162
|
+
# attributes_in_database - original values hash
|
|
163
|
+
user.attributes_in_database # => {"name" => "Old", "email" => "old@example.com", ...}
|
|
164
|
+
|
|
165
|
+
# =============================================================================
|
|
166
|
+
# ASSOCIATION CHANGES
|
|
167
|
+
# =============================================================================
|
|
168
|
+
|
|
169
|
+
# Foreign key changes are tracked
|
|
170
|
+
post = Post.find(1)
|
|
171
|
+
post.author_id = 5
|
|
172
|
+
post.author_id_changed? # => true
|
|
173
|
+
post.author_id_was # => previous author_id
|
|
174
|
+
|
|
175
|
+
# =============================================================================
|
|
176
|
+
# IN-PLACE MODIFICATION (Rails 7+)
|
|
177
|
+
# =============================================================================
|
|
178
|
+
|
|
179
|
+
# Rails 7+ automatically detects in-place changes
|
|
180
|
+
user.tags = ["ruby"]
|
|
181
|
+
user.tags << "rails"
|
|
182
|
+
user.tags_changed? # => true (automatic detection)
|
|
183
|
+
|
|
184
|
+
# Previously required:
|
|
185
|
+
# user.tags_will_change! # No longer needed in Rails 7+
|
|
186
|
+
# user.tags << "rails"
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# SKIPPING DIRTY TRACKING
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
# These methods bypass dirty tracking entirely
|
|
193
|
+
user.update_column(:login_count, 5) # No callbacks, no dirty tracking
|
|
194
|
+
user.update_columns(login_count: 5) # No callbacks, no dirty tracking
|
|
195
|
+
|
|
196
|
+
# Check changes BEFORE using these if needed
|
|
197
|
+
if user.login_count_changed?
|
|
198
|
+
# ... do something
|
|
199
|
+
end
|
|
200
|
+
user.update_column(:login_count, user.login_count)
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# PERFORMANCE CONSIDERATIONS
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
# Dirty tracking adds memory overhead
|
|
207
|
+
# For bulk operations, use update_all to bypass
|
|
208
|
+
|
|
209
|
+
# BAD - instantiates models with dirty tracking
|
|
210
|
+
User.where(status: :pending).each do |user|
|
|
211
|
+
user.update(status: :active)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# GOOD - direct SQL, no instantiation
|
|
215
|
+
User.where(status: :pending).update_all(status: :active)
|
|
216
|
+
|
|
217
|
+
# For read-only operations, use pluck
|
|
218
|
+
emails = User.pluck(:email) # No model instantiation
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# ActiveRecord Single Table Inheritance (STI) Examples
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# BASIC STI SETUP
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
# Migration
|
|
8
|
+
class CreateVehicles < ActiveRecord::Migration[7.2]
|
|
9
|
+
def change
|
|
10
|
+
create_table :vehicles do |t|
|
|
11
|
+
t.string :type, null: false # Required for STI
|
|
12
|
+
t.string :make
|
|
13
|
+
t.string :model
|
|
14
|
+
t.integer :year
|
|
15
|
+
t.integer :wheels
|
|
16
|
+
t.float :cargo_capacity
|
|
17
|
+
t.timestamps
|
|
18
|
+
|
|
19
|
+
t.index :type
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Models
|
|
25
|
+
class Vehicle < ApplicationRecord
|
|
26
|
+
validates :make, :model, presence: true
|
|
27
|
+
|
|
28
|
+
def description
|
|
29
|
+
"#{year} #{make} #{model}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Car < Vehicle
|
|
34
|
+
validates :wheels, inclusion: { in: [4] }
|
|
35
|
+
|
|
36
|
+
def vehicle_type
|
|
37
|
+
"Automobile"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Truck < Vehicle
|
|
42
|
+
validates :wheels, inclusion: { in: [4, 6, 8, 10, 18] }
|
|
43
|
+
validates :cargo_capacity, presence: true
|
|
44
|
+
|
|
45
|
+
def vehicle_type
|
|
46
|
+
"Commercial Vehicle"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Motorcycle < Vehicle
|
|
51
|
+
validates :wheels, inclusion: { in: [2, 3] }
|
|
52
|
+
|
|
53
|
+
def vehicle_type
|
|
54
|
+
"Two-wheeler"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# =============================================================================
|
|
59
|
+
# STI USAGE
|
|
60
|
+
# =============================================================================
|
|
61
|
+
|
|
62
|
+
# Creating records
|
|
63
|
+
car = Car.create!(make: "Toyota", model: "Camry", year: 2024, wheels: 4)
|
|
64
|
+
car.type # => "Car"
|
|
65
|
+
|
|
66
|
+
truck = Truck.create!(
|
|
67
|
+
make: "Ford",
|
|
68
|
+
model: "F-150",
|
|
69
|
+
year: 2024,
|
|
70
|
+
wheels: 4,
|
|
71
|
+
cargo_capacity: 1500
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Queries automatically filter by type
|
|
75
|
+
Car.all
|
|
76
|
+
# SELECT * FROM vehicles WHERE type = 'Car'
|
|
77
|
+
|
|
78
|
+
Truck.count
|
|
79
|
+
# SELECT COUNT(*) FROM vehicles WHERE type = 'Truck'
|
|
80
|
+
|
|
81
|
+
# Base class queries all types
|
|
82
|
+
Vehicle.all
|
|
83
|
+
# SELECT * FROM vehicles
|
|
84
|
+
|
|
85
|
+
# Type-based queries
|
|
86
|
+
Vehicle.where(type: "Car")
|
|
87
|
+
Vehicle.where(type: ["Car", "Truck"])
|
|
88
|
+
|
|
89
|
+
# =============================================================================
|
|
90
|
+
# STI WITH NAMESPACED MODELS
|
|
91
|
+
# =============================================================================
|
|
92
|
+
|
|
93
|
+
module Inventory
|
|
94
|
+
class Item < ApplicationRecord
|
|
95
|
+
self.table_name = "inventory_items"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class PhysicalItem < Item
|
|
99
|
+
# type = "Inventory::PhysicalItem" (full class name by default)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class DigitalItem < Item
|
|
103
|
+
# type = "Inventory::DigitalItem"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# To store short type names
|
|
108
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
109
|
+
self.store_full_class_name = false
|
|
110
|
+
# Now type = "PhysicalItem" instead of "Inventory::PhysicalItem"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# STI WITH SHARED SCOPES
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
class Vehicle < ApplicationRecord
|
|
118
|
+
scope :recent, -> { where("created_at > ?", 1.year.ago) }
|
|
119
|
+
scope :by_make, ->(make) { where(make:) }
|
|
120
|
+
scope :vintage, -> { where("year < ?", 1990) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Scopes work on subclasses
|
|
124
|
+
Car.recent.by_make("Honda")
|
|
125
|
+
# SELECT * FROM vehicles WHERE type = 'Car' AND created_at > ... AND make = 'Honda'
|
|
126
|
+
|
|
127
|
+
# =============================================================================
|
|
128
|
+
# STI WITH CALLBACKS
|
|
129
|
+
# =============================================================================
|
|
130
|
+
|
|
131
|
+
class Vehicle < ApplicationRecord
|
|
132
|
+
before_save :normalize_make
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def normalize_make
|
|
137
|
+
self.make = make.titleize if make.present?
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class Car < Vehicle
|
|
142
|
+
before_create :set_default_wheels
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def set_default_wheels
|
|
147
|
+
self.wheels ||= 4
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class Truck < Vehicle
|
|
152
|
+
after_create :notify_fleet_manager
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def notify_fleet_manager
|
|
157
|
+
FleetManager.new_truck_added(self)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# =============================================================================
|
|
162
|
+
# STI ANTI-PATTERN: SPARSE TABLES
|
|
163
|
+
# =============================================================================
|
|
164
|
+
|
|
165
|
+
# BAD - Too many type-specific columns
|
|
166
|
+
class Vehicle < ApplicationRecord
|
|
167
|
+
# columns: type, make, model, wheels,
|
|
168
|
+
# wing_span, max_altitude, # Airplane only
|
|
169
|
+
# displacement, fuel_type, # Motorcycle only
|
|
170
|
+
# towing_capacity, bed_length # Truck only
|
|
171
|
+
# Results in lots of NULL values
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# GOOD - Use delegated types or separate tables
|
|
175
|
+
# See delegated_types.rb for alternative
|
|
176
|
+
|
|
177
|
+
# =============================================================================
|
|
178
|
+
# ALTERNATIVE: DELEGATED TYPES (Rails 6.1+)
|
|
179
|
+
# =============================================================================
|
|
180
|
+
|
|
181
|
+
# Migration
|
|
182
|
+
class CreateEntries < ActiveRecord::Migration[7.2]
|
|
183
|
+
def change
|
|
184
|
+
create_table :entries do |t|
|
|
185
|
+
t.string :entryable_type, null: false
|
|
186
|
+
t.bigint :entryable_id, null: false
|
|
187
|
+
t.string :title
|
|
188
|
+
t.datetime :published_at
|
|
189
|
+
t.timestamps
|
|
190
|
+
|
|
191
|
+
t.index [:entryable_type, :entryable_id]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
create_table :messages do |t|
|
|
195
|
+
t.text :body
|
|
196
|
+
t.timestamps
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
create_table :comments do |t|
|
|
200
|
+
t.text :content
|
|
201
|
+
t.bigint :parent_id
|
|
202
|
+
t.timestamps
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Models
|
|
208
|
+
class Entry < ApplicationRecord
|
|
209
|
+
delegated_type :entryable, types: %w[Message Comment], dependent: :destroy
|
|
210
|
+
delegate :body, to: :entryable, allow_nil: true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class Message < ApplicationRecord
|
|
214
|
+
has_one :entry, as: :entryable, touch: true
|
|
215
|
+
validates :body, presence: true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
class Comment < ApplicationRecord
|
|
219
|
+
has_one :entry, as: :entryable, touch: true
|
|
220
|
+
belongs_to :parent, class_name: "Comment", optional: true
|
|
221
|
+
validates :content, presence: true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Usage
|
|
225
|
+
message = Message.create!(body: "Hello world")
|
|
226
|
+
entry = Entry.create!(title: "First Post", entryable: message)
|
|
227
|
+
|
|
228
|
+
entry.message? # => true
|
|
229
|
+
entry.comment? # => false
|
|
230
|
+
entry.entryable # => Message instance
|
|
231
|
+
|
|
232
|
+
Entry.messages # Scope for message entries
|
|
233
|
+
Entry.comments # Scope for comment entries
|
|
234
|
+
|
|
235
|
+
# =============================================================================
|
|
236
|
+
# ALTERNATIVE: SEPARATE TABLES WITH CONCERNS
|
|
237
|
+
# =============================================================================
|
|
238
|
+
|
|
239
|
+
module Vehicular
|
|
240
|
+
extend ActiveSupport::Concern
|
|
241
|
+
|
|
242
|
+
included do
|
|
243
|
+
validates :make, :model, :year, presence: true
|
|
244
|
+
scope :recent, -> { where("year >= ?", 5.years.ago.year) }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def description
|
|
248
|
+
"#{year} #{make} #{model}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def age
|
|
252
|
+
Time.current.year - year
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
class Car < ApplicationRecord
|
|
257
|
+
include Vehicular
|
|
258
|
+
validates :doors, inclusion: { in: 2..5 }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class Motorcycle < ApplicationRecord
|
|
262
|
+
include Vehicular
|
|
263
|
+
validates :engine_cc, presence: true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class Boat < ApplicationRecord
|
|
267
|
+
include Vehicular
|
|
268
|
+
validates :length_feet, presence: true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# =============================================================================
|
|
272
|
+
# STI FACTORY PATTERN
|
|
273
|
+
# =============================================================================
|
|
274
|
+
|
|
275
|
+
class Vehicle < ApplicationRecord
|
|
276
|
+
def self.build_by_type(type, **attributes)
|
|
277
|
+
case type.to_s.downcase
|
|
278
|
+
when "car" then Car.new(attributes)
|
|
279
|
+
when "truck" then Truck.new(attributes)
|
|
280
|
+
when "motorcycle" then Motorcycle.new(attributes)
|
|
281
|
+
else raise ArgumentError, "Unknown vehicle type: #{type}"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Usage
|
|
287
|
+
vehicle = Vehicle.build_by_type("car", make: "Honda", model: "Civic")
|
|
288
|
+
vehicle.class # => Car
|
|
289
|
+
|
|
290
|
+
# =============================================================================
|
|
291
|
+
# QUERYING ACROSS STI HIERARCHY
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
# All vehicles of specific types
|
|
295
|
+
Vehicle.where(type: [Car.name, Truck.name])
|
|
296
|
+
|
|
297
|
+
# Using subclasses
|
|
298
|
+
Vehicle.where(type: Vehicle.subclasses.map(&:name))
|
|
299
|
+
|
|
300
|
+
# Exclude specific type
|
|
301
|
+
Vehicle.where.not(type: "Motorcycle")
|
|
302
|
+
|
|
303
|
+
# Polymorphic queries (if vehicle is used polymorphically elsewhere)
|
|
304
|
+
class Insurance < ApplicationRecord
|
|
305
|
+
belongs_to :insurable, polymorphic: true
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Note: This saves "Vehicle" as insurable_type, not "Car"
|
|
309
|
+
# Be careful with STI + polymorphic associations
|
|
310
|
+
car = Car.find(1)
|
|
311
|
+
Insurance.create!(insurable: car)
|
|
312
|
+
# insurable_type = "Vehicle" (base class) - may cause issues
|
|
313
|
+
|
|
314
|
+
# Workaround: store full type
|
|
315
|
+
class Insurance < ApplicationRecord
|
|
316
|
+
belongs_to :insurable, polymorphic: true
|
|
317
|
+
|
|
318
|
+
before_save :store_actual_type
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def store_actual_type
|
|
323
|
+
self.insurable_type = insurable.class.name
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# TESTING STI MODELS
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
# FactoryBot
|
|
332
|
+
FactoryBot.define do
|
|
333
|
+
factory :vehicle do
|
|
334
|
+
make { "Generic" }
|
|
335
|
+
model { "Model" }
|
|
336
|
+
year { 2024 }
|
|
337
|
+
|
|
338
|
+
factory :car, class: "Car" do
|
|
339
|
+
make { "Toyota" }
|
|
340
|
+
model { "Camry" }
|
|
341
|
+
wheels { 4 }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
factory :truck, class: "Truck" do
|
|
345
|
+
make { "Ford" }
|
|
346
|
+
model { "F-150" }
|
|
347
|
+
wheels { 4 }
|
|
348
|
+
cargo_capacity { 1500 }
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
factory :motorcycle, class: "Motorcycle" do
|
|
352
|
+
make { "Harley-Davidson" }
|
|
353
|
+
model { "Sportster" }
|
|
354
|
+
wheels { 2 }
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# RSpec
|
|
360
|
+
RSpec.describe Car do
|
|
361
|
+
it "inherits from Vehicle" do
|
|
362
|
+
expect(Car.superclass).to eq(Vehicle)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it "has correct type" do
|
|
366
|
+
car = create(:car)
|
|
367
|
+
expect(car.type).to eq("Car")
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "queries only cars" do
|
|
371
|
+
create(:car)
|
|
372
|
+
create(:truck)
|
|
373
|
+
|
|
374
|
+
expect(Car.count).to eq(1)
|
|
375
|
+
expect(Vehicle.count).to eq(2)
|
|
376
|
+
end
|
|
377
|
+
end
|