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,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
|