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,277 @@
1
+ # ActiveRecord Built-in Validators Examples
2
+
3
+ # =============================================================================
4
+ # PRESENCE
5
+ # =============================================================================
6
+
7
+ class User < ApplicationRecord
8
+ validates :name, presence: true
9
+ validates :email, :username, presence: true # Multiple attributes
10
+ end
11
+
12
+ # Boolean fields - presence doesn't work correctly!
13
+ class Feature < ApplicationRecord
14
+ # WRONG: false.blank? == true, so this fails for false values
15
+ # validates :enabled, presence: true
16
+
17
+ # CORRECT: Use inclusion for boolean fields
18
+ validates :enabled, inclusion: { in: [true, false] }
19
+ end
20
+
21
+ # =============================================================================
22
+ # UNIQUENESS
23
+ # =============================================================================
24
+
25
+ class Account < ApplicationRecord
26
+ # Basic uniqueness
27
+ validates :email, uniqueness: true
28
+
29
+ # Case-insensitive
30
+ validates :username, uniqueness: { case_sensitive: false }
31
+
32
+ # Scoped to another column (per-organization uniqueness)
33
+ validates :employee_id, uniqueness: { scope: :organization_id }
34
+
35
+ # Composite scope
36
+ validates :slug, uniqueness: { scope: [:category_id, :year] }
37
+
38
+ # With conditions
39
+ validates :primary_email, uniqueness: true, if: :primary?
40
+ end
41
+
42
+ # IMPORTANT: Always add database unique index for race condition safety
43
+ # add_index :accounts, :email, unique: true
44
+ # add_index :accounts, [:organization_id, :employee_id], unique: true
45
+
46
+ # =============================================================================
47
+ # FORMAT
48
+ # =============================================================================
49
+
50
+ class Product < ApplicationRecord
51
+ # Basic format with regex
52
+ validates :sku, format: { with: /\A[A-Z]{3}-\d{4}\z/ }
53
+
54
+ # Email format using Ruby's built-in regex
55
+ validates :contact_email, format: { with: URI::MailTo::EMAIL_REGEXP }
56
+
57
+ # With custom message
58
+ validates :code, format: {
59
+ with: /\A[a-z0-9_]+\z/,
60
+ message: "only allows lowercase letters, numbers, and underscores"
61
+ }
62
+
63
+ # SECURITY: Always use \A and \z, not ^ and $
64
+ # ^ and $ match line boundaries, vulnerable to injection:
65
+ # "valid\nmalicious" would pass /^valid$/
66
+ # \A and \z match string boundaries, safe:
67
+ # "valid\nmalicious" would NOT pass /\Avalid\z/
68
+ end
69
+
70
+ # =============================================================================
71
+ # LENGTH
72
+ # =============================================================================
73
+
74
+ class Post < ApplicationRecord
75
+ validates :title, length: { minimum: 5 }
76
+ validates :excerpt, length: { maximum: 200 }
77
+ validates :access_code, length: { is: 8 }
78
+ validates :username, length: { in: 3..20 }
79
+
80
+ # With custom messages
81
+ validates :password, length: {
82
+ minimum: 8,
83
+ maximum: 72,
84
+ too_short: "must have at least %{count} characters",
85
+ too_long: "must have at most %{count} characters"
86
+ }
87
+
88
+ # Note: :maximum alone allows nil by default
89
+ validates :bio, length: { maximum: 500 } # nil is valid
90
+ validates :name, length: { minimum: 1 } # nil is NOT valid
91
+ end
92
+
93
+ # =============================================================================
94
+ # NUMERICALITY
95
+ # =============================================================================
96
+
97
+ class Order < ApplicationRecord
98
+ # Basic numeric validation
99
+ validates :total, numericality: true
100
+
101
+ # Integer only
102
+ validates :quantity, numericality: { only_integer: true }
103
+
104
+ # Comparison operators
105
+ validates :price, numericality: { greater_than: 0 }
106
+ validates :discount_percent, numericality: {
107
+ greater_than_or_equal_to: 0,
108
+ less_than_or_equal_to: 100
109
+ }
110
+
111
+ # Range
112
+ validates :rating, numericality: { in: 1..5 }
113
+
114
+ # Other than
115
+ validates :priority, numericality: { other_than: 0 }
116
+
117
+ # Odd/even
118
+ validates :pair_count, numericality: { even: true }
119
+
120
+ # Allowing nil (for optional fields)
121
+ validates :optional_score, numericality: { greater_than: 0 }, allow_nil: true
122
+ end
123
+
124
+ # =============================================================================
125
+ # INCLUSION / EXCLUSION
126
+ # =============================================================================
127
+
128
+ class Article < ApplicationRecord
129
+ # Inclusion - value must be in list
130
+ validates :status, inclusion: { in: %w[draft published archived] }
131
+
132
+ # With custom message
133
+ validates :size, inclusion: {
134
+ in: %w[S M L XL],
135
+ message: "%{value} is not a valid size"
136
+ }
137
+
138
+ # Using proc for dynamic values
139
+ validates :category, inclusion: {
140
+ in: -> { Category.active.pluck(:name) }
141
+ }
142
+
143
+ # Exclusion - value must NOT be in list
144
+ validates :subdomain, exclusion: {
145
+ in: %w[www admin api],
146
+ message: "%{value} is reserved"
147
+ }
148
+ end
149
+
150
+ # =============================================================================
151
+ # CONFIRMATION
152
+ # =============================================================================
153
+
154
+ class Registration < ApplicationRecord
155
+ # Adds virtual password_confirmation attribute
156
+ validates :password, confirmation: true
157
+
158
+ # IMPORTANT: Confirmation field is optional by default!
159
+ # Add presence validation if confirmation is required
160
+ validates :password_confirmation, presence: true, if: :password_changed?
161
+
162
+ # For email confirmation
163
+ validates :email, confirmation: true
164
+ validates :email_confirmation, presence: true, on: :create
165
+ end
166
+
167
+ # =============================================================================
168
+ # ACCEPTANCE
169
+ # =============================================================================
170
+
171
+ class Signup < ApplicationRecord
172
+ # Virtual attribute, validates checkbox was checked
173
+ validates :terms_of_service, acceptance: true
174
+
175
+ # Custom accepted values
176
+ validates :eula, acceptance: { accept: ["yes", "1", true] }
177
+
178
+ # With message
179
+ validates :age_verification, acceptance: {
180
+ message: "You must confirm you are 18 or older"
181
+ }
182
+ end
183
+
184
+ # =============================================================================
185
+ # COMPARISON
186
+ # =============================================================================
187
+
188
+ class Event < ApplicationRecord
189
+ validates :end_date, comparison: { greater_than: :start_date }
190
+ validates :max_attendees, comparison: { greater_than_or_equal_to: :min_attendees }
191
+ end
192
+
193
+ class User < ApplicationRecord
194
+ validates :password, comparison: { other_than: :username }
195
+ end
196
+
197
+ # =============================================================================
198
+ # ASSOCIATED
199
+ # =============================================================================
200
+
201
+ class Author < ApplicationRecord
202
+ has_many :books
203
+ validates_associated :books # Validates all books when saving author
204
+ end
205
+
206
+ class Book < ApplicationRecord
207
+ belongs_to :author
208
+ validates :author, presence: true # Ensure association exists
209
+
210
+ # WARNING: Do NOT add validates_associated :author here!
211
+ # Bidirectional validates_associated causes infinite recursion
212
+ end
213
+
214
+ # =============================================================================
215
+ # COMBINED VALIDATIONS
216
+ # =============================================================================
217
+
218
+ class User < ApplicationRecord
219
+ validates :email,
220
+ presence: true,
221
+ uniqueness: { case_sensitive: false },
222
+ format: { with: URI::MailTo::EMAIL_REGEXP },
223
+ length: { maximum: 255 }
224
+
225
+ validates :password,
226
+ presence: true,
227
+ length: { minimum: 8, maximum: 72 },
228
+ confirmation: true,
229
+ on: :create
230
+
231
+ validates :password_confirmation,
232
+ presence: true,
233
+ if: :password_required?
234
+
235
+ validates :age,
236
+ numericality: { greater_than_or_equal_to: 13, only_integer: true },
237
+ allow_nil: true
238
+
239
+ validates :username,
240
+ presence: true,
241
+ uniqueness: { case_sensitive: false },
242
+ length: { in: 3..30 },
243
+ format: {
244
+ with: /\A[a-z0-9_]+\z/,
245
+ message: "only allows lowercase letters, numbers, and underscores"
246
+ }
247
+
248
+ private
249
+
250
+ def password_required?
251
+ new_record? || password.present?
252
+ end
253
+ end
254
+
255
+ # =============================================================================
256
+ # COMMON OPTIONS
257
+ # =============================================================================
258
+
259
+ class Item < ApplicationRecord
260
+ # allow_nil - skip validation if value is nil
261
+ validates :optional_code, format: { with: /\A[A-Z]+\z/ }, allow_nil: true
262
+
263
+ # allow_blank - skip validation if value is blank (nil, "", " ")
264
+ validates :notes, length: { minimum: 10 }, allow_blank: true
265
+
266
+ # on - specify validation context
267
+ validates :publish_date, presence: true, on: :publish
268
+
269
+ # if/unless - conditional validation
270
+ validates :reason, presence: true, if: :requires_reason?
271
+
272
+ # message - custom error message
273
+ validates :quantity, numericality: {
274
+ greater_than: 0,
275
+ message: "must be positive"
276
+ }
277
+ end
@@ -0,0 +1,288 @@
1
+ # ActiveRecord Conditional Validations Examples
2
+
3
+ # =============================================================================
4
+ # :if AND :unless OPTIONS
5
+ # =============================================================================
6
+
7
+ class Order < ApplicationRecord
8
+ # Symbol method reference (preferred for readability)
9
+ validates :card_number, presence: true, if: :paid_with_card?
10
+ validates :check_number, presence: true, if: :paid_with_check?
11
+ validates :delivery_address, presence: true, unless: :pickup?
12
+
13
+ # Lambda/Proc for one-liners
14
+ validates :discount_code, presence: true,
15
+ if: -> { promotional_period? && premium_customer? }
16
+
17
+ validates :signature, presence: true,
18
+ unless: -> { total < 100 }
19
+
20
+ # Array of conditions (ALL must be true)
21
+ validates :insurance_number, presence: true,
22
+ if: [:high_value?, :requires_insurance?, :not_insured?]
23
+
24
+ # Mixed array (symbols and procs)
25
+ validates :manager_approval, presence: true,
26
+ if: [:large_order?, -> { created_at > 1.day.ago }]
27
+
28
+ private
29
+
30
+ def paid_with_card?
31
+ payment_method == "card"
32
+ end
33
+
34
+ def paid_with_check?
35
+ payment_method == "check"
36
+ end
37
+
38
+ def pickup?
39
+ delivery_method == "pickup"
40
+ end
41
+
42
+ def promotional_period?
43
+ Time.current.between?(promo_start, promo_end)
44
+ end
45
+
46
+ def premium_customer?
47
+ customer&.premium?
48
+ end
49
+
50
+ def high_value?
51
+ total > 10_000
52
+ end
53
+
54
+ def requires_insurance?
55
+ items.any?(&:fragile?)
56
+ end
57
+
58
+ def not_insured?
59
+ !insured?
60
+ end
61
+
62
+ def large_order?
63
+ items.count > 50
64
+ end
65
+ end
66
+
67
+ # =============================================================================
68
+ # with_options - GROUPING VALIDATIONS
69
+ # =============================================================================
70
+
71
+ class User < ApplicationRecord
72
+ # Group validations by condition
73
+ with_options if: :admin? do |admin|
74
+ admin.validates :password, length: { minimum: 12 }
75
+ admin.validates :two_factor_enabled, inclusion: { in: [true] }
76
+ admin.validates :security_question, presence: true
77
+ end
78
+
79
+ with_options if: :guest? do |guest|
80
+ guest.validates :session_token, presence: true
81
+ guest.validates :expires_at, presence: true
82
+ end
83
+
84
+ # Nested with_options
85
+ with_options if: :active? do |active|
86
+ active.validates :email, presence: true
87
+ active.validates :last_sign_in_at, presence: true
88
+
89
+ active.with_options if: :subscribed? do |subscribed|
90
+ subscribed.validates :subscription_id, presence: true
91
+ subscribed.validates :subscription_expires_at, presence: true
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def admin?
98
+ role == "admin"
99
+ end
100
+
101
+ def guest?
102
+ role == "guest"
103
+ end
104
+
105
+ def active?
106
+ status == "active"
107
+ end
108
+
109
+ def subscribed?
110
+ subscription_status == "active"
111
+ end
112
+ end
113
+
114
+ # =============================================================================
115
+ # DYNAMIC allow_nil AND allow_blank
116
+ # =============================================================================
117
+
118
+ class Profile < ApplicationRecord
119
+ # Static allow_blank
120
+ validates :bio, length: { minimum: 50 }, allow_blank: true
121
+
122
+ # Dynamic allow_blank with Proc
123
+ validates :phone, presence: true,
124
+ allow_blank: -> { signup_step < 3 }
125
+
126
+ # Dynamic allow_nil
127
+ validates :age, numericality: { greater_than: 0 },
128
+ allow_nil: :optional_age_field?
129
+
130
+ private
131
+
132
+ def optional_age_field?
133
+ !requires_age_verification?
134
+ end
135
+ end
136
+
137
+ # =============================================================================
138
+ # CONDITIONAL CALLBACKS WITH VALIDATIONS
139
+ # =============================================================================
140
+
141
+ class Document < ApplicationRecord
142
+ # Normalize before validation, conditionally
143
+ before_validation :normalize_title, if: :title_changed?
144
+ before_validation :set_slug, unless: :slug?
145
+
146
+ validates :title, presence: true
147
+ validates :slug, presence: true, uniqueness: true
148
+
149
+ private
150
+
151
+ def normalize_title
152
+ self.title = title.strip.titleize
153
+ end
154
+
155
+ def set_slug
156
+ self.slug = title.parameterize if title.present?
157
+ end
158
+ end
159
+
160
+ # =============================================================================
161
+ # HALTING VALIDATION
162
+ # =============================================================================
163
+
164
+ class ImportedRecord < ApplicationRecord
165
+ before_validation :check_skip_validation
166
+
167
+ validates :name, presence: true
168
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
169
+
170
+ attr_accessor :skip_validation
171
+
172
+ private
173
+
174
+ def check_skip_validation
175
+ throw(:abort) if skip_validation
176
+ end
177
+ end
178
+
179
+ # Usage:
180
+ # record = ImportedRecord.new(skip_validation: true)
181
+ # record.save # Skips all validations
182
+
183
+ # =============================================================================
184
+ # COMPLEX CONDITIONAL PATTERNS
185
+ # =============================================================================
186
+
187
+ class Subscription < ApplicationRecord
188
+ # Different validations based on plan type
189
+ validates :credit_card, presence: true, if: :paid_plan?
190
+ validates :trial_ends_at, presence: true, if: :trial_plan?
191
+ validates :enterprise_contract_id, presence: true, if: :enterprise_plan?
192
+
193
+ # Inverse conditions
194
+ validates :payment_method, presence: true, unless: :free_plan?
195
+
196
+ # Combining positive and negative conditions
197
+ validates :billing_address, presence: true,
198
+ if: :requires_billing?,
199
+ unless: :digital_only?
200
+
201
+ private
202
+
203
+ def paid_plan?
204
+ %w[basic pro business].include?(plan_type)
205
+ end
206
+
207
+ def trial_plan?
208
+ plan_type == "trial"
209
+ end
210
+
211
+ def enterprise_plan?
212
+ plan_type == "enterprise"
213
+ end
214
+
215
+ def free_plan?
216
+ plan_type == "free"
217
+ end
218
+
219
+ def requires_billing?
220
+ paid_plan? || enterprise_plan?
221
+ end
222
+
223
+ def digital_only?
224
+ products.all?(&:digital?)
225
+ end
226
+ end
227
+
228
+ # =============================================================================
229
+ # CONTEXT-AWARE CONDITIONALS
230
+ # =============================================================================
231
+
232
+ class Article < ApplicationRecord
233
+ # Different rules based on validation context
234
+ validates :title, presence: true
235
+ validates :body, presence: true
236
+
237
+ # Only require these fields when publishing
238
+ validates :meta_description, presence: true,
239
+ if: -> { validation_context == :publish }
240
+ validates :featured_image, presence: true,
241
+ if: -> { validation_context == :publish }
242
+ validates :published_at, presence: true,
243
+ if: -> { validation_context == :publish }
244
+
245
+ def publish!
246
+ self.published_at ||= Time.current
247
+ save!(context: :publish)
248
+ end
249
+ end
250
+
251
+ # =============================================================================
252
+ # ANTI-PATTERNS TO AVOID
253
+ # =============================================================================
254
+
255
+ # ANTI-PATTERN: Overly complex conditionals
256
+ class BadExample < ApplicationRecord
257
+ # WRONG: Too many conditions, hard to understand
258
+ validates :field1, presence: true, if: :a?
259
+ validates :field1, length: { min: 5 }, if: :b?
260
+ validates :field1, format: { with: /.../ }, unless: :c?
261
+ validates :field2, presence: true, if: :a?
262
+ validates :field2, uniqueness: true, if: -> { b? && !d? }
263
+ # ... more scattered conditionals
264
+ end
265
+
266
+ # BETTER: Group related validations or use contexts
267
+ class GoodExample < ApplicationRecord
268
+ # Group by context
269
+ with_options on: :step_one do
270
+ validates :field1, :field2, presence: true
271
+ end
272
+
273
+ with_options on: :step_two do
274
+ validates :field1, length: { minimum: 5 }
275
+ validates :field2, uniqueness: true
276
+ end
277
+
278
+ # Or use form objects for complex multi-step flows
279
+ end
280
+
281
+ # ANTI-PATTERN: String evaluation (deprecated, slow)
282
+ class OldStyleExample < ApplicationRecord
283
+ # WRONG: String evaluation
284
+ # validates :name, presence: true, if: "admin?"
285
+
286
+ # CORRECT: Symbol or proc
287
+ validates :name, presence: true, if: :admin?
288
+ end