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,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: activerecord
|
|
3
|
+
description: "ActiveRecord patterns — associations, validations, queries, migrations, eager loading. Activate when working with models, migrations, database schema, N+1 queries, scopes, includes/preload/eager_load, callbacks, or editing files in app/models/ or db/migrate/."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ActiveRecord
|
|
7
|
+
|
|
8
|
+
This skill provides comprehensive guidance for working with ActiveRecord in Rails applications. Use for writing migrations, defining associations, optimizing queries, preventing N+1 issues, implementing validations, and following database best practices.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
### CRUD Operations
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Create
|
|
16
|
+
user = User.create(name: "Alice", email: "alice@example.com")
|
|
17
|
+
user = User.create!(...) # Raises on failure
|
|
18
|
+
|
|
19
|
+
# Read
|
|
20
|
+
User.find(1) # Raises RecordNotFound
|
|
21
|
+
User.find_by(email: "x") # Returns nil if not found
|
|
22
|
+
User.where(active: true) # Returns Relation
|
|
23
|
+
|
|
24
|
+
# Update
|
|
25
|
+
user.update(name: "Bob")
|
|
26
|
+
user.update!(...) # Raises on failure
|
|
27
|
+
|
|
28
|
+
# Delete (callbacks run)
|
|
29
|
+
user.destroy
|
|
30
|
+
|
|
31
|
+
# Delete (no callbacks)
|
|
32
|
+
user.delete
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Key Concepts
|
|
36
|
+
|
|
37
|
+
| Concept | Purpose |
|
|
38
|
+
|---------|---------|
|
|
39
|
+
| `belongs_to` | Child side of association (has foreign key) |
|
|
40
|
+
| `has_many` / `has_one` | Parent side of association |
|
|
41
|
+
| `has_many :through` | Many-to-many via join model |
|
|
42
|
+
| `includes` / `preload` | Eager loading (prevent N+1) |
|
|
43
|
+
| `scope` | Named query builder |
|
|
44
|
+
| `validates` | Model-level data validation |
|
|
45
|
+
| `before_save` / `after_commit` | Lifecycle callbacks |
|
|
46
|
+
|
|
47
|
+
## Eager Loading Decision Tree
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Need to access associated data?
|
|
51
|
+
├── NO → Use `joins` (filtering only)
|
|
52
|
+
└── YES → Need to filter/sort by association?
|
|
53
|
+
├── NO → Use `preload` (separate queries)
|
|
54
|
+
└── YES → Large dataset with many associations?
|
|
55
|
+
├── YES → Use `includes` with `references`
|
|
56
|
+
└── NO → Use `eager_load` (single JOIN)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Quick Comparison
|
|
60
|
+
|
|
61
|
+
| Method | Strategy | Best For |
|
|
62
|
+
|--------|----------|----------|
|
|
63
|
+
| `includes` | Auto-choose | Default choice |
|
|
64
|
+
| `preload` | Separate queries | Large datasets, no filtering |
|
|
65
|
+
| `eager_load` | LEFT OUTER JOIN | Filtering by association |
|
|
66
|
+
| `joins` | INNER JOIN | Filtering only, not accessing data |
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# N+1 problem
|
|
70
|
+
Post.all.each { |p| p.author.name } # 1 + N queries
|
|
71
|
+
|
|
72
|
+
# Solution
|
|
73
|
+
Post.includes(:author).each { |p| p.author.name } # 2 queries
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Validation vs Constraint Decision
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Does the rule ALWAYS apply, regardless of business logic?
|
|
80
|
+
├── Yes → Database constraint
|
|
81
|
+
│ └── Examples: NOT NULL, foreign keys, unique emails
|
|
82
|
+
└── No → Model validation
|
|
83
|
+
└── Examples: Format rules that change, conditional requirements
|
|
84
|
+
|
|
85
|
+
Need helpful user-facing error messages?
|
|
86
|
+
├── Yes → Model validation (possibly WITH constraint)
|
|
87
|
+
└── No → Constraint alone is fine
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Best Practice**: Use both for critical fields:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Migration (data integrity)
|
|
94
|
+
add_index :users, :email, unique: true
|
|
95
|
+
|
|
96
|
+
# Model (user feedback)
|
|
97
|
+
validates :email, presence: true, uniqueness: true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Associations Quick Reference
|
|
101
|
+
|
|
102
|
+
### Basic Types
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class Author < ApplicationRecord
|
|
106
|
+
has_many :books, dependent: :destroy
|
|
107
|
+
has_one :profile
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class Book < ApplicationRecord
|
|
111
|
+
belongs_to :author # Required by default
|
|
112
|
+
belongs_to :publisher, optional: true # Allow NULL
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Through Associations
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class Physician < ApplicationRecord
|
|
120
|
+
has_many :appointments
|
|
121
|
+
has_many :patients, through: :appointments
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class Appointment < ApplicationRecord
|
|
125
|
+
belongs_to :physician
|
|
126
|
+
belongs_to :patient
|
|
127
|
+
# Join model can have attributes
|
|
128
|
+
validates :scheduled_at, presence: true
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Critical Options
|
|
133
|
+
|
|
134
|
+
| Option | Purpose |
|
|
135
|
+
|--------|---------|
|
|
136
|
+
| `inverse_of` | Required with custom foreign_key |
|
|
137
|
+
| `dependent: :destroy` | Cascade delete with callbacks |
|
|
138
|
+
| `counter_cache: true` | Cache association count |
|
|
139
|
+
| `touch: true` | Update parent's updated_at |
|
|
140
|
+
|
|
141
|
+
## Migrations Quick Reference
|
|
142
|
+
|
|
143
|
+
### Safe Patterns
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Always reversible
|
|
147
|
+
add_column :users, :name, :string
|
|
148
|
+
add_index :users, :email, unique: true
|
|
149
|
+
add_reference :orders, :user, foreign_key: true
|
|
150
|
+
|
|
151
|
+
# Concurrent index (no table lock)
|
|
152
|
+
disable_ddl_transaction!
|
|
153
|
+
add_index :users, :email, algorithm: :concurrently
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Must Include Type for Reversibility
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
remove_column :users, :legacy_field, :string # Include type!
|
|
160
|
+
change_column_default :users, :status, from: nil, to: "active"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Callbacks Quick Reference
|
|
164
|
+
|
|
165
|
+
### Order of Execution
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
before_validation → after_validation →
|
|
169
|
+
before_save → around_save → before_create →
|
|
170
|
+
around_create → [INSERT] → after_create →
|
|
171
|
+
after_save → [COMMIT] → after_commit
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Critical Rule: Use after_commit for External Systems
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# WRONG - Race condition!
|
|
178
|
+
after_save :enqueue_processing
|
|
179
|
+
|
|
180
|
+
# CORRECT - Runs after COMMIT
|
|
181
|
+
after_commit :enqueue_processing, on: :create
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Batch Processing
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# BAD - loads all records
|
|
188
|
+
User.all.each { |u| process(u) }
|
|
189
|
+
|
|
190
|
+
# GOOD - processes in batches
|
|
191
|
+
User.find_each { |u| process(u) }
|
|
192
|
+
|
|
193
|
+
# Bulk operations
|
|
194
|
+
User.where(old: true).in_batches.update_all(archived: true)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Best Practices
|
|
198
|
+
|
|
199
|
+
### Do
|
|
200
|
+
|
|
201
|
+
- Add database index for columns used in WHERE, ORDER BY, JOIN
|
|
202
|
+
- Use `includes` to prevent N+1 queries
|
|
203
|
+
- Pair uniqueness validations with unique database indexes
|
|
204
|
+
- Use `find_each` for processing large datasets
|
|
205
|
+
- Use `pluck(:column)` instead of `all.map(&:column)`
|
|
206
|
+
- Define `inverse_of` when using custom `foreign_key`
|
|
207
|
+
- Use `after_commit` for background jobs and external APIs
|
|
208
|
+
- Prefer `has_many :through` over `has_and_belongs_to_many`
|
|
209
|
+
|
|
210
|
+
### Don't
|
|
211
|
+
|
|
212
|
+
- Don't use `default_scope` (causes subtle issues)
|
|
213
|
+
- Don't use `delete` when you need callbacks
|
|
214
|
+
- Don't skip database constraints for critical uniqueness
|
|
215
|
+
- Don't use `update_column` to bypass validations casually
|
|
216
|
+
- Don't reference models in migrations (use raw SQL)
|
|
217
|
+
- Don't edit already-deployed migrations
|
|
218
|
+
- Don't use `after_save` for external system interactions
|
|
219
|
+
|
|
220
|
+
## Anti-Patterns Quick List
|
|
221
|
+
|
|
222
|
+
| Anti-Pattern | Solution |
|
|
223
|
+
|--------------|----------|
|
|
224
|
+
| N+1 queries | Use `includes`, `preload`, or `eager_load` |
|
|
225
|
+
| `User.all.map(&:email)` | Use `User.pluck(:email)` |
|
|
226
|
+
| Uniqueness without index | Add unique database index |
|
|
227
|
+
| `validates :active, presence: true` | Use `inclusion: { in: [true, false] }` for booleans |
|
|
228
|
+
| `after_save` for jobs | Use `after_commit` |
|
|
229
|
+
| Callback hell | Extract to service objects |
|
|
230
|
+
| `default_scope` | Use explicit scopes |
|
|
231
|
+
| `has_and_belongs_to_many` | Use `has_many :through` |
|
|
232
|
+
|
|
233
|
+
## Additional Resources
|
|
234
|
+
|
|
235
|
+
### Reference Files
|
|
236
|
+
|
|
237
|
+
For detailed patterns and complete API references, consult:
|
|
238
|
+
|
|
239
|
+
- **`references/basics.md`** - Conventions, CRUD, dirty tracking, STI, type casting
|
|
240
|
+
- **`references/migrations.md`** - Schema changes, indexes, constraints, safe patterns
|
|
241
|
+
- **`references/validations.md`** - Built-in validators, custom validators, contexts
|
|
242
|
+
- **`references/callbacks.md`** - Lifecycle hooks, transaction callbacks, alternatives
|
|
243
|
+
- **`references/associations.md`** - All association types, inverse_of, dependent options
|
|
244
|
+
- **`references/querying.md`** - Finders, eager loading, scopes, batch processing
|
|
245
|
+
|
|
246
|
+
### Example Files
|
|
247
|
+
|
|
248
|
+
Ready-to-use code patterns in `examples/`:
|
|
249
|
+
|
|
250
|
+
- **`examples/basics/`** - CRUD, dirty tracking, type casting, inheritance
|
|
251
|
+
- **`examples/migrations/`** - Schema changes, indexes, safe patterns, reversibility
|
|
252
|
+
- **`examples/validations/`** - Built-in, conditional, custom, contexts, constraints
|
|
253
|
+
- **`examples/callbacks/`** - Lifecycle, transaction callbacks, conditional, alternatives
|
|
254
|
+
- **`examples/associations/`** - Basic, through, polymorphic, self-referential, extensions
|
|
255
|
+
- **`examples/querying/`** - Finders, eager loading, scopes, batch processing, optimization
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Association Extension Examples
|
|
2
|
+
# Adding custom methods to association proxies
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# Inline Extension
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class Project < ApplicationRecord
|
|
9
|
+
has_many :tasks do
|
|
10
|
+
# Filter methods
|
|
11
|
+
def active
|
|
12
|
+
where(status: "active")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def completed
|
|
16
|
+
where(status: "completed")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def overdue
|
|
20
|
+
active.where("due_date < ?", Date.current)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Calculation methods
|
|
24
|
+
def total_hours
|
|
25
|
+
sum(:estimated_hours)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def completion_percentage
|
|
29
|
+
return 0 if empty?
|
|
30
|
+
|
|
31
|
+
(completed.count.to_f / count * 100).round(2)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Creation helpers
|
|
35
|
+
def create_milestone!(name, due_date)
|
|
36
|
+
create!(name:, due_date:, priority: :high, milestone: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Access owner via proxy_association
|
|
40
|
+
def recent_for_owner
|
|
41
|
+
where("created_at > ?", proxy_association.owner.created_at)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Usage
|
|
47
|
+
project = Project.find(1)
|
|
48
|
+
project.tasks.active
|
|
49
|
+
project.tasks.overdue.count
|
|
50
|
+
project.tasks.total_hours
|
|
51
|
+
project.tasks.completion_percentage
|
|
52
|
+
project.tasks.create_milestone!("Launch", 1.month.from_now)
|
|
53
|
+
|
|
54
|
+
# ============================================
|
|
55
|
+
# Shared Extension Module
|
|
56
|
+
# ============================================
|
|
57
|
+
|
|
58
|
+
module StatusFilter
|
|
59
|
+
def active
|
|
60
|
+
where(status: "active")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def completed
|
|
64
|
+
where(status: "completed")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pending
|
|
68
|
+
where(status: "pending")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def by_status(status)
|
|
72
|
+
where(status:)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
module Orderable
|
|
77
|
+
def by_priority
|
|
78
|
+
order(priority: :desc)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def by_recent
|
|
82
|
+
order(created_at: :desc)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def by_due_date
|
|
86
|
+
order(due_date: :asc)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Project < ApplicationRecord
|
|
91
|
+
has_many :tasks, -> { extending StatusFilter, Orderable }
|
|
92
|
+
has_many :issues, -> { extending StatusFilter, Orderable }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Team < ApplicationRecord
|
|
96
|
+
has_many :members, -> { extending StatusFilter }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Usage
|
|
100
|
+
project.tasks.active.by_priority
|
|
101
|
+
project.issues.pending.by_due_date
|
|
102
|
+
team.members.active
|
|
103
|
+
|
|
104
|
+
# ============================================
|
|
105
|
+
# Extension with Scopes
|
|
106
|
+
# ============================================
|
|
107
|
+
|
|
108
|
+
class Author < ApplicationRecord
|
|
109
|
+
has_many :books, -> { order(published_at: :desc) } do
|
|
110
|
+
def fiction
|
|
111
|
+
where(genre: "fiction")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def nonfiction
|
|
115
|
+
where(genre: "nonfiction")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bestsellers
|
|
119
|
+
where(bestseller: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def published_in(year)
|
|
123
|
+
where("EXTRACT(YEAR FROM published_at) = ?", year)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def search(query)
|
|
127
|
+
where("title ILIKE ?", "%#{query}%")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Chain extensions with other query methods
|
|
133
|
+
author.books.fiction.bestsellers
|
|
134
|
+
author.books.nonfiction.published_in(2024)
|
|
135
|
+
author.books.search("ruby").limit(10)
|
|
136
|
+
|
|
137
|
+
# ============================================
|
|
138
|
+
# Extension Accessing Owner
|
|
139
|
+
# ============================================
|
|
140
|
+
|
|
141
|
+
class User < ApplicationRecord
|
|
142
|
+
has_many :notifications do
|
|
143
|
+
def unread
|
|
144
|
+
where(read_at: nil)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def mark_all_read!
|
|
148
|
+
update_all(read_at: Time.current)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def for_past_week
|
|
152
|
+
where("created_at > ?", 1.week.ago)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Access the user via proxy_association.owner
|
|
156
|
+
def summary
|
|
157
|
+
owner = proxy_association.owner
|
|
158
|
+
{
|
|
159
|
+
total: count,
|
|
160
|
+
unread: unread.count,
|
|
161
|
+
user_name: owner.name
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Usage
|
|
168
|
+
user.notifications.unread.count
|
|
169
|
+
user.notifications.mark_all_read!
|
|
170
|
+
user.notifications.summary
|
|
171
|
+
# => { total: 42, unread: 5, user_name: "Alice" }
|
|
172
|
+
|
|
173
|
+
# ============================================
|
|
174
|
+
# Extension for Through Associations
|
|
175
|
+
# ============================================
|
|
176
|
+
|
|
177
|
+
class Doctor < ApplicationRecord
|
|
178
|
+
has_many :appointments
|
|
179
|
+
has_many :patients, through: :appointments do
|
|
180
|
+
def with_recent_visit
|
|
181
|
+
where("appointments.scheduled_at > ?", 6.months.ago)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def needing_followup
|
|
185
|
+
joins(:appointments)
|
|
186
|
+
.where(appointments: { followup_needed: true })
|
|
187
|
+
.distinct
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Usage
|
|
193
|
+
doctor.patients.with_recent_visit
|
|
194
|
+
doctor.patients.needing_followup.count
|
|
195
|
+
|
|
196
|
+
# ============================================
|
|
197
|
+
# Real-World Example: Versioned Documents
|
|
198
|
+
# ============================================
|
|
199
|
+
|
|
200
|
+
class Document < ApplicationRecord
|
|
201
|
+
has_many :versions, -> { order(version_number: :desc) } do
|
|
202
|
+
def latest
|
|
203
|
+
first
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def published
|
|
207
|
+
where(published: true)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def latest_published
|
|
211
|
+
published.first
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def diff(v1, v2)
|
|
215
|
+
# Return diff between two versions
|
|
216
|
+
version1 = find_by(version_number: v1)
|
|
217
|
+
version2 = find_by(version_number: v2)
|
|
218
|
+
# Implementation...
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def create_new_version!(content, author:)
|
|
222
|
+
last_number = maximum(:version_number) || 0
|
|
223
|
+
create!(
|
|
224
|
+
content:,
|
|
225
|
+
author:,
|
|
226
|
+
version_number: last_number + 1,
|
|
227
|
+
created_at: Time.current
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Usage
|
|
234
|
+
doc.versions.latest
|
|
235
|
+
doc.versions.published.count
|
|
236
|
+
doc.versions.create_new_version!("Updated content", author: current_user)
|
|
237
|
+
|
|
238
|
+
# ============================================
|
|
239
|
+
# Extension vs Scope Decision
|
|
240
|
+
# ============================================
|
|
241
|
+
|
|
242
|
+
# Use SCOPE when:
|
|
243
|
+
# - Query is useful globally (not just via association)
|
|
244
|
+
# - Simple filtering/ordering
|
|
245
|
+
# - Used in multiple associations
|
|
246
|
+
|
|
247
|
+
class Task < ApplicationRecord
|
|
248
|
+
scope :active, -> { where(status: "active") }
|
|
249
|
+
scope :by_priority, -> { order(priority: :desc) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Use EXTENSION when:
|
|
253
|
+
# - Query only makes sense in association context
|
|
254
|
+
# - Need access to owner (proxy_association.owner)
|
|
255
|
+
# - Doing mutations (mark_all_read!, etc.)
|
|
256
|
+
# - Complex business logic specific to relationship
|
|
257
|
+
|
|
258
|
+
class Project < ApplicationRecord
|
|
259
|
+
has_many :tasks do
|
|
260
|
+
# Makes sense only in project context
|
|
261
|
+
def critical_path
|
|
262
|
+
# Complex calculation involving project data
|
|
263
|
+
owner = proxy_association.owner
|
|
264
|
+
active.where("due_date <= ?", owner.deadline)
|
|
265
|
+
.order(priority: :desc)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ============================================
|
|
271
|
+
# Testing Extensions
|
|
272
|
+
# ============================================
|
|
273
|
+
|
|
274
|
+
# spec/models/project_spec.rb
|
|
275
|
+
RSpec.describe Project do
|
|
276
|
+
describe "tasks extension" do
|
|
277
|
+
let(:project) { create(:project) }
|
|
278
|
+
let!(:active_task) { create(:task, project:, status: "active") }
|
|
279
|
+
let!(:completed_task) { create(:task, project:, status: "completed") }
|
|
280
|
+
|
|
281
|
+
describe "#active" do
|
|
282
|
+
it "returns only active tasks" do
|
|
283
|
+
expect(project.tasks.active).to contain_exactly(active_task)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe "#completion_percentage" do
|
|
288
|
+
it "calculates correct percentage" do
|
|
289
|
+
expect(project.tasks.completion_percentage).to eq(50.0)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "returns 0 for empty association" do
|
|
293
|
+
empty_project = create(:project)
|
|
294
|
+
expect(empty_project.tasks.completion_percentage).to eq(0)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Basic Association Examples
|
|
2
|
+
# Demonstrates belongs_to, has_one, has_many relationships
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# belongs_to - Child side (has foreign key)
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class Book < ApplicationRecord
|
|
9
|
+
# Required by default (Rails 5+)
|
|
10
|
+
belongs_to :author
|
|
11
|
+
|
|
12
|
+
# Optional - allows NULL foreign key
|
|
13
|
+
belongs_to :publisher, optional: true
|
|
14
|
+
|
|
15
|
+
# Custom naming
|
|
16
|
+
belongs_to :category,
|
|
17
|
+
class_name: "Genre",
|
|
18
|
+
foreign_key: "genre_id",
|
|
19
|
+
inverse_of: :books # Required with custom FK
|
|
20
|
+
|
|
21
|
+
# With counter cache on parent
|
|
22
|
+
belongs_to :series, counter_cache: true
|
|
23
|
+
|
|
24
|
+
# Touch parent on save
|
|
25
|
+
belongs_to :library, touch: true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Migration for books
|
|
29
|
+
# create_table :books do |t|
|
|
30
|
+
# t.belongs_to :author, null: false, foreign_key: true
|
|
31
|
+
# t.belongs_to :publisher, foreign_key: true
|
|
32
|
+
# t.belongs_to :genre, foreign_key: { to_table: :genres }
|
|
33
|
+
# t.belongs_to :series, foreign_key: true
|
|
34
|
+
# t.belongs_to :library, foreign_key: true
|
|
35
|
+
# t.string :title
|
|
36
|
+
# t.timestamps
|
|
37
|
+
# end
|
|
38
|
+
|
|
39
|
+
# ============================================
|
|
40
|
+
# has_one - Parent side one-to-one
|
|
41
|
+
# ============================================
|
|
42
|
+
|
|
43
|
+
class Supplier < ApplicationRecord
|
|
44
|
+
has_one :account, dependent: :destroy
|
|
45
|
+
|
|
46
|
+
# Optional association
|
|
47
|
+
has_one :profile
|
|
48
|
+
|
|
49
|
+
# Custom naming
|
|
50
|
+
has_one :representative,
|
|
51
|
+
class_name: "Person",
|
|
52
|
+
foreign_key: "company_id",
|
|
53
|
+
inverse_of: :employer
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Migration - enforce true 1:1 at database level
|
|
57
|
+
# create_table :accounts do |t|
|
|
58
|
+
# t.belongs_to :supplier, null: false, index: { unique: true }, foreign_key: true
|
|
59
|
+
# t.decimal :balance
|
|
60
|
+
# t.timestamps
|
|
61
|
+
# end
|
|
62
|
+
|
|
63
|
+
# ============================================
|
|
64
|
+
# has_many - One-to-many
|
|
65
|
+
# ============================================
|
|
66
|
+
|
|
67
|
+
class Author < ApplicationRecord
|
|
68
|
+
has_many :books, dependent: :destroy
|
|
69
|
+
|
|
70
|
+
# Scoped association
|
|
71
|
+
has_many :published_books,
|
|
72
|
+
-> { where(published: true) },
|
|
73
|
+
class_name: "Book"
|
|
74
|
+
|
|
75
|
+
has_many :recent_books,
|
|
76
|
+
-> { order(created_at: :desc).limit(5) },
|
|
77
|
+
class_name: "Book"
|
|
78
|
+
|
|
79
|
+
# Through association
|
|
80
|
+
has_many :publishers, -> { distinct }, through: :books
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# ============================================
|
|
84
|
+
# Usage Examples
|
|
85
|
+
# ============================================
|
|
86
|
+
|
|
87
|
+
# Creating with associations
|
|
88
|
+
author = Author.create!(name: "Jane Austen")
|
|
89
|
+
book = author.books.create!(title: "Pride and Prejudice")
|
|
90
|
+
|
|
91
|
+
# Building without saving
|
|
92
|
+
draft = author.books.build(title: "Work in Progress")
|
|
93
|
+
draft.save
|
|
94
|
+
|
|
95
|
+
# Adding existing record
|
|
96
|
+
existing_book = Book.find(123)
|
|
97
|
+
author.books << existing_book # Saves immediately!
|
|
98
|
+
|
|
99
|
+
# Association queries
|
|
100
|
+
author.books.count # COUNT query
|
|
101
|
+
author.books.size # Uses counter_cache if present
|
|
102
|
+
author.books.empty? # Boolean check
|
|
103
|
+
author.book_ids # Array of IDs
|
|
104
|
+
|
|
105
|
+
# Scoped queries on association
|
|
106
|
+
author.books.where(published: true)
|
|
107
|
+
author.books.order(created_at: :desc)
|
|
108
|
+
author.published_books.first
|
|
109
|
+
|
|
110
|
+
# has_one building
|
|
111
|
+
supplier = Supplier.create!(name: "ACME Corp")
|
|
112
|
+
supplier.create_account!(balance: 1000)
|
|
113
|
+
# or
|
|
114
|
+
supplier.build_account(balance: 500)
|
|
115
|
+
supplier.save
|
|
116
|
+
|
|
117
|
+
# has_one replacement
|
|
118
|
+
supplier.account = Account.new(balance: 2000) # Old account nullified/destroyed
|