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.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,317 @@
1
+ # ActiveRecord Type Casting and Serialization Examples
2
+
3
+ # =============================================================================
4
+ # ATTRIBUTE API
5
+ # =============================================================================
6
+
7
+ class Product < ApplicationRecord
8
+ # Override database-inferred types
9
+ attribute :price, :decimal, precision: 8, scale: 2
10
+ attribute :quantity, :integer, default: 0
11
+ attribute :active, :boolean, default: true
12
+
13
+ # JSON attributes
14
+ attribute :metadata, :json, default: {}
15
+
16
+ # Date/time with defaults
17
+ attribute :published_on, :date
18
+ attribute :sale_ends_at, :datetime
19
+ end
20
+
21
+ # Usage
22
+ product = Product.new
23
+ product.price = "19.99" # Cast to BigDecimal
24
+ product.quantity = "5" # Cast to Integer
25
+ product.active = "yes" # Cast to true
26
+ product.metadata = { foo: 1 } # Stored as JSON
27
+
28
+ # =============================================================================
29
+ # CUSTOM TYPES
30
+ # =============================================================================
31
+
32
+ # Money stored as cents in database
33
+ class MoneyType < ActiveRecord::Type::Value
34
+ def cast(value)
35
+ return nil if value.blank?
36
+ case value
37
+ when Money then value
38
+ when Numeric then Money.new(value * 100)
39
+ when String then Money.new(value.to_d * 100)
40
+ end
41
+ end
42
+
43
+ def deserialize(value)
44
+ return nil if value.nil?
45
+ Money.new(value.to_i)
46
+ end
47
+
48
+ def serialize(value)
49
+ return nil if value.nil?
50
+ value.cents
51
+ end
52
+ end
53
+
54
+ # Register globally
55
+ ActiveRecord::Type.register(:money, MoneyType)
56
+
57
+ # Use in model
58
+ class Invoice < ApplicationRecord
59
+ attribute :total, :money
60
+ attribute :tax, :money
61
+ end
62
+
63
+ # =============================================================================
64
+ # VALUE OBJECT TYPE
65
+ # =============================================================================
66
+
67
+ # Email value object
68
+ class EmailAddress
69
+ attr_reader :address
70
+
71
+ def initialize(address)
72
+ @address = address.to_s.downcase.strip
73
+ end
74
+
75
+ def domain
76
+ address.split("@").last
77
+ end
78
+
79
+ def to_s
80
+ address
81
+ end
82
+
83
+ def ==(other)
84
+ address == other.to_s
85
+ end
86
+ end
87
+
88
+ class EmailType < ActiveRecord::Type::Value
89
+ def cast(value)
90
+ return nil if value.blank?
91
+ EmailAddress.new(value)
92
+ end
93
+
94
+ def serialize(value)
95
+ value&.to_s
96
+ end
97
+ end
98
+
99
+ ActiveRecord::Type.register(:email, EmailType)
100
+
101
+ class User < ApplicationRecord
102
+ attribute :email, :email
103
+ end
104
+
105
+ # Usage
106
+ user = User.new(email: " ALICE@Example.COM ")
107
+ user.email.address # => "alice@example.com"
108
+ user.email.domain # => "example.com"
109
+
110
+ # =============================================================================
111
+ # SERIALIZE (Legacy - for text columns)
112
+ # =============================================================================
113
+
114
+ class Setting < ApplicationRecord
115
+ # Rails 7.2+ syntax (keyword argument)
116
+ serialize :preferences, coder: JSON, type: Hash
117
+ serialize :tags, coder: YAML, type: Array
118
+
119
+ # With type enforcement
120
+ serialize :config, coder: JSON, type: Hash
121
+ # Raises ActiveRecord::SerializationTypeMismatch for non-Hash
122
+ end
123
+
124
+ # Usage
125
+ setting = Setting.new
126
+ setting.preferences = { theme: "dark", language: "en" }
127
+ setting.tags = ["important", "featured"]
128
+ setting.save
129
+
130
+ # Database stores: '{"theme":"dark","language":"en"}'
131
+
132
+ # =============================================================================
133
+ # STORE ACCESSOR (Recommended for JSON columns)
134
+ # =============================================================================
135
+
136
+ class User < ApplicationRecord
137
+ # For serialized text column
138
+ store :settings, accessors: [:theme, :language], coder: JSON
139
+
140
+ # For native JSON/JSONB column (PostgreSQL)
141
+ store_accessor :preferences, :notifications, :digest_frequency
142
+
143
+ # With prefix to avoid conflicts
144
+ store_accessor :preferences, :email_enabled, prefix: :pref
145
+ # Creates: pref_email_enabled, pref_email_enabled=
146
+ end
147
+
148
+ # Usage
149
+ user = User.new
150
+ user.theme = "dark"
151
+ user.notifications = true
152
+ user.pref_email_enabled = true
153
+
154
+ user.settings # => {"theme" => "dark"}
155
+
156
+ # Dirty tracking works
157
+ user.theme = "light"
158
+ user.theme_changed? # => true
159
+
160
+ # =============================================================================
161
+ # STORE WITH DEFAULTS
162
+ # =============================================================================
163
+
164
+ class Profile < ApplicationRecord
165
+ store_accessor :settings, :receive_newsletter, :locale
166
+
167
+ after_initialize :set_defaults
168
+
169
+ private
170
+
171
+ def set_defaults
172
+ return unless new_record?
173
+ self.receive_newsletter ||= true
174
+ self.locale ||= "en"
175
+ end
176
+ end
177
+
178
+ # =============================================================================
179
+ # QUERYING JSON COLUMNS (PostgreSQL)
180
+ # =============================================================================
181
+
182
+ # Direct JSON query (PostgreSQL)
183
+ User.where("preferences->>'notifications' = ?", "true")
184
+
185
+ # Using store_accessor doesn't help with queries
186
+ # Must use raw JSON operators
187
+
188
+ # =============================================================================
189
+ # ENUM ATTRIBUTES
190
+ # =============================================================================
191
+
192
+ class Order < ApplicationRecord
193
+ enum :status, {
194
+ pending: 0,
195
+ processing: 1,
196
+ shipped: 2,
197
+ delivered: 3,
198
+ cancelled: 4
199
+ }
200
+
201
+ # With prefix
202
+ enum :payment_status, {
203
+ unpaid: 0,
204
+ paid: 1,
205
+ refunded: 2
206
+ }, prefix: true
207
+ end
208
+
209
+ # Usage
210
+ order = Order.new
211
+ order.status = :pending
212
+ order.pending? # => true
213
+ order.shipped! # Updates and saves
214
+
215
+ # Queries
216
+ Order.pending # Scope for pending orders
217
+ Order.where.not(status: :cancelled)
218
+
219
+ # With prefix
220
+ order.payment_status_paid!
221
+ order.payment_status_unpaid?
222
+
223
+ # =============================================================================
224
+ # BEFORE TYPE CAST
225
+ # =============================================================================
226
+
227
+ user = User.find(1)
228
+
229
+ # Get value before type casting
230
+ user.created_at # => Mon, 01 Jan 2024 00:00:00 UTC
231
+ user.created_at_before_type_cast # => "2024-01-01 00:00:00"
232
+
233
+ # All attributes before type cast
234
+ user.attributes_before_type_cast
235
+ # => {"id" => "1", "name" => "Alice", "created_at" => "2024-01-01 00:00:00"}
236
+
237
+ # =============================================================================
238
+ # TYPE COERCION EDGE CASES
239
+ # =============================================================================
240
+
241
+ class Post < ApplicationRecord
242
+ # Boolean coercion
243
+ attribute :published, :boolean
244
+ end
245
+
246
+ post = Post.new
247
+ post.published = "1" # => true
248
+ post.published = "0" # => false
249
+ post.published = "true" # => true
250
+ post.published = "false" # => false
251
+ post.published = "" # => nil
252
+ post.published = "yes" # => true (in Rails 7+)
253
+
254
+ # Integer coercion
255
+ class Item < ApplicationRecord
256
+ attribute :quantity, :integer
257
+ end
258
+
259
+ item = Item.new
260
+ item.quantity = "10" # => 10
261
+ item.quantity = "10.5" # => 10 (truncated)
262
+ item.quantity = "abc" # => 0
263
+
264
+ # =============================================================================
265
+ # ENCRYPTED ATTRIBUTES (Rails 7+)
266
+ # =============================================================================
267
+
268
+ class User < ApplicationRecord
269
+ encrypts :ssn
270
+ encrypts :email, deterministic: true # Allows querying
271
+
272
+ # With custom key
273
+ encrypts :medical_notes, key: :medical_encryption_key
274
+ end
275
+
276
+ # Usage
277
+ user = User.create(ssn: "123-45-6789")
278
+ # Database stores encrypted value
279
+
280
+ User.find_by(email: "alice@example.com") # Works with deterministic: true
281
+
282
+ # =============================================================================
283
+ # COMPOSED_OF (Value Objects without Custom Types)
284
+ # =============================================================================
285
+
286
+ class Customer < ApplicationRecord
287
+ composed_of :address,
288
+ class_name: "Address",
289
+ mapping: [
290
+ [:address_street, :street],
291
+ [:address_city, :city],
292
+ [:address_zip, :zip]
293
+ ]
294
+ end
295
+
296
+ class Address
297
+ attr_reader :street, :city, :zip
298
+
299
+ def initialize(street:, city:, zip:)
300
+ @street = street
301
+ @city = city
302
+ @zip = zip
303
+ end
304
+
305
+ def full_address
306
+ "#{street}, #{city} #{zip}"
307
+ end
308
+ end
309
+
310
+ # Usage
311
+ customer = Customer.new
312
+ customer.address = Address.new(
313
+ street: "123 Main St",
314
+ city: "Anytown",
315
+ zip: "12345"
316
+ )
317
+ customer.address.full_address # => "123 Main St, Anytown 12345"