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,432 @@
1
+ # ActiveRecord Database Constraints vs Model Validations Examples
2
+
3
+ # =============================================================================
4
+ # THE RACE CONDITION PROBLEM
5
+ # =============================================================================
6
+
7
+ # SCENARIO: Two requests create users with same email simultaneously
8
+ #
9
+ # Request 1: SELECT COUNT(*) FROM users WHERE email = 'alice@example.com' → 0
10
+ # Request 2: SELECT COUNT(*) FROM users WHERE email = 'alice@example.com' → 0
11
+ # Request 1: Validation passes, proceeds to save
12
+ # Request 2: Validation passes, proceeds to save
13
+ # Request 1: INSERT INTO users (email) VALUES ('alice@example.com') → Success!
14
+ # Request 2: INSERT INTO users (email) VALUES ('alice@example.com') → Success! DUPLICATE!
15
+
16
+ # =============================================================================
17
+ # THE SOLUTION: DATABASE CONSTRAINTS + MODEL VALIDATIONS
18
+ # =============================================================================
19
+
20
+ # Migration - Data integrity layer
21
+ class CreateUsers < ActiveRecord::Migration[7.2]
22
+ def change
23
+ create_table :users do |t|
24
+ t.string :email, null: false
25
+ t.string :username, null: false
26
+ t.string :encrypted_password, null: false
27
+ t.integer :age
28
+ t.string :role, null: false, default: "member"
29
+ t.timestamps
30
+ end
31
+
32
+ # Unique indexes prevent race conditions
33
+ add_index :users, :email, unique: true
34
+ add_index :users, :username, unique: true
35
+
36
+ # Check constraints for domain rules (Rails 6.1+)
37
+ add_check_constraint :users, "age >= 0", name: "users_age_non_negative"
38
+ add_check_constraint :users, "role IN ('admin', 'moderator', 'member')",
39
+ name: "users_role_valid"
40
+ end
41
+ end
42
+
43
+ # Model - User experience layer
44
+ class User < ApplicationRecord
45
+ # Validations provide user-friendly error messages
46
+ validates :email,
47
+ presence: true,
48
+ uniqueness: { case_sensitive: false },
49
+ format: { with: URI::MailTo::EMAIL_REGEXP }
50
+
51
+ validates :username,
52
+ presence: true,
53
+ uniqueness: { case_sensitive: false },
54
+ length: { in: 3..30 },
55
+ format: {
56
+ with: /\A[a-z0-9_]+\z/,
57
+ message: "only allows lowercase letters, numbers, and underscores"
58
+ }
59
+
60
+ validates :age,
61
+ numericality: { greater_than_or_equal_to: 0, only_integer: true },
62
+ allow_nil: true
63
+
64
+ validates :role,
65
+ inclusion: { in: %w[admin moderator member] }
66
+ end
67
+
68
+ # =============================================================================
69
+ # HANDLING DATABASE CONSTRAINT VIOLATIONS
70
+ # =============================================================================
71
+
72
+ class UsersController < ApplicationController
73
+ def create
74
+ @user = User.new(user_params)
75
+
76
+ if @user.save
77
+ redirect_to @user, notice: "User created successfully"
78
+ else
79
+ render :new, status: :unprocessable_entity
80
+ end
81
+ rescue ActiveRecord::RecordNotUnique => e
82
+ # Handle race condition - database caught a duplicate
83
+ if e.message.include?("email")
84
+ @user.errors.add(:email, "has already been taken")
85
+ elsif e.message.include?("username")
86
+ @user.errors.add(:username, "has already been taken")
87
+ else
88
+ @user.errors.add(:base, "A duplicate record already exists")
89
+ end
90
+ render :new, status: :unprocessable_entity
91
+ end
92
+ end
93
+
94
+ # =============================================================================
95
+ # ALTERNATIVE: RESCUE IN MODEL
96
+ # =============================================================================
97
+
98
+ class User < ApplicationRecord
99
+ # ... validations ...
100
+
101
+ def save_with_race_condition_handling
102
+ save
103
+ rescue ActiveRecord::RecordNotUnique => e
104
+ handle_duplicate_error(e)
105
+ false
106
+ end
107
+
108
+ private
109
+
110
+ def handle_duplicate_error(exception)
111
+ if exception.message.include?("email")
112
+ errors.add(:email, "has already been taken")
113
+ elsif exception.message.include?("username")
114
+ errors.add(:username, "has already been taken")
115
+ else
116
+ errors.add(:base, "A duplicate record already exists")
117
+ end
118
+ end
119
+ end
120
+
121
+ # =============================================================================
122
+ # create_or_find_by - IDEMPOTENT OPERATIONS
123
+ # =============================================================================
124
+
125
+ class ApiController < ApplicationController
126
+ # For idempotent API endpoints where duplicates are acceptable
127
+ def find_or_create_user
128
+ # find_or_create_by - tries to find first, then creates
129
+ # Still has race condition potential
130
+ user = User.find_or_create_by(email: params[:email]) do |u|
131
+ u.name = params[:name]
132
+ end
133
+
134
+ render json: user
135
+ end
136
+
137
+ # create_or_find_by - tries to create first, rescues if exists
138
+ # Better for race conditions, but requires unique index
139
+ def create_or_find_user
140
+ user = User.create_or_find_by(email: params[:email]) do |u|
141
+ u.name = params[:name]
142
+ end
143
+
144
+ render json: user
145
+ end
146
+ end
147
+
148
+ # =============================================================================
149
+ # COMPOSITE UNIQUE CONSTRAINTS
150
+ # =============================================================================
151
+
152
+ class CreateAppliedCoupons < ActiveRecord::Migration[7.2]
153
+ def change
154
+ create_table :applied_coupons do |t|
155
+ t.references :account, null: false, foreign_key: true
156
+ t.references :coupon, null: false, foreign_key: true
157
+ t.timestamps
158
+ end
159
+
160
+ # User can only apply each coupon once per account
161
+ add_index :applied_coupons, [:account_id, :coupon_id], unique: true
162
+ end
163
+ end
164
+
165
+ class AppliedCoupon < ApplicationRecord
166
+ belongs_to :account
167
+ belongs_to :coupon
168
+
169
+ validates :coupon_id, uniqueness: { scope: :account_id }
170
+ end
171
+
172
+ class CouponsController < ApplicationController
173
+ def apply
174
+ @applied = AppliedCoupon.new(account: current_account, coupon: @coupon)
175
+
176
+ if @applied.save
177
+ render json: { success: true }
178
+ else
179
+ render json: { errors: @applied.errors }, status: :unprocessable_entity
180
+ end
181
+ rescue ActiveRecord::RecordNotUnique
182
+ render json: { error: "Coupon already applied" }, status: :unprocessable_entity
183
+ end
184
+ end
185
+
186
+ # =============================================================================
187
+ # FOREIGN KEY CONSTRAINTS
188
+ # =============================================================================
189
+
190
+ class CreateOrders < ActiveRecord::Migration[7.2]
191
+ def change
192
+ create_table :orders do |t|
193
+ t.references :user, null: false, foreign_key: true
194
+ t.references :shipping_address, foreign_key: { to_table: :addresses }
195
+ t.timestamps
196
+ end
197
+ end
198
+ end
199
+
200
+ class Order < ApplicationRecord
201
+ belongs_to :user
202
+ belongs_to :shipping_address, class_name: "Address", optional: true
203
+
204
+ validates :user, presence: true
205
+
206
+ # No need to validate shipping_address existence -
207
+ # foreign key ensures referential integrity
208
+ end
209
+
210
+ # Handling foreign key violations
211
+ class OrdersController < ApplicationController
212
+ def create
213
+ @order = Order.new(order_params)
214
+ @order.save!
215
+ redirect_to @order
216
+ rescue ActiveRecord::InvalidForeignKey => e
217
+ @order.errors.add(:base, "Referenced record no longer exists")
218
+ render :new, status: :unprocessable_entity
219
+ end
220
+ end
221
+
222
+ # =============================================================================
223
+ # CHECK CONSTRAINTS (Rails 6.1+)
224
+ # =============================================================================
225
+
226
+ class CreateProducts < ActiveRecord::Migration[7.2]
227
+ def change
228
+ create_table :products do |t|
229
+ t.string :name, null: false
230
+ t.decimal :price, precision: 10, scale: 2, null: false
231
+ t.integer :quantity, null: false, default: 0
232
+ t.string :status, null: false, default: "draft"
233
+ t.timestamps
234
+ end
235
+
236
+ # Ensure price is positive
237
+ add_check_constraint :products, "price >= 0", name: "products_price_positive"
238
+
239
+ # Ensure quantity is non-negative
240
+ add_check_constraint :products, "quantity >= 0", name: "products_quantity_non_negative"
241
+
242
+ # Ensure valid status
243
+ add_check_constraint :products, "status IN ('draft', 'active', 'discontinued')",
244
+ name: "products_status_valid"
245
+ end
246
+ end
247
+
248
+ class Product < ApplicationRecord
249
+ validates :price, numericality: { greater_than_or_equal_to: 0 }
250
+ validates :quantity, numericality: { greater_than_or_equal_to: 0 }
251
+ validates :status, inclusion: { in: %w[draft active discontinued] }
252
+ end
253
+
254
+ # =============================================================================
255
+ # DECISION FRAMEWORK
256
+ # =============================================================================
257
+
258
+ # Question 1: Am I preventing bad data from being written?
259
+ # YES → Use database constraint
260
+ #
261
+ # Question 2: Am I preventing user-fixable errors?
262
+ # YES → Use model validation
263
+
264
+ class DecisionExample < ApplicationRecord
265
+ # Email uniqueness:
266
+ # Q1: Yes, duplicate emails are bad data → DB unique index
267
+ # Q2: Yes, user can change email → Model validation
268
+ # RESULT: Use BOTH
269
+
270
+ # Price must be positive:
271
+ # Q1: Yes, negative prices are bad data → DB check constraint
272
+ # Q2: Yes, user can fix price → Model validation
273
+ # RESULT: Use BOTH
274
+
275
+ # Encrypted password format:
276
+ # Q1: Yes, invalid format is bad data → Maybe DB constraint
277
+ # Q2: No, user doesn't control encrypted value → NO model validation
278
+ # RESULT: DB only (if needed), handle as app error
279
+
280
+ # Bio length maximum:
281
+ # Q1: Debatable, depends on system → Maybe DB constraint
282
+ # Q2: Yes, user can shorten bio → Model validation
283
+ # RESULT: Model validation sufficient, DB optional
284
+ end
285
+
286
+ # =============================================================================
287
+ # WHAT DATABASE CONSTRAINTS PROTECT AGAINST
288
+ # =============================================================================
289
+
290
+ # Methods that SKIP validations (DB constraints still apply):
291
+ class BypassExamples < ApplicationRecord
292
+ def demonstrate_bypass
293
+ # These all skip validations:
294
+ update_attribute(:email, "invalid") # Skips validations
295
+ update_column(:email, "invalid") # Skips validations AND callbacks
296
+ update_columns(email: "invalid", age: -1) # Skips validations AND callbacks
297
+ User.update_all(email: "invalid") # Bulk update, no validations
298
+
299
+ # Bulk inserts skip everything:
300
+ User.insert_all([{ email: "test@example.com" }])
301
+ User.upsert_all([{ email: "test@example.com" }])
302
+
303
+ # delete vs destroy:
304
+ user.delete # Skips callbacks
305
+ User.delete_all # Bulk delete, no callbacks
306
+
307
+ # Raw SQL:
308
+ ActiveRecord::Base.connection.execute("INSERT INTO users ...")
309
+ end
310
+ end
311
+
312
+ # Database constraints protect against ALL of these!
313
+
314
+ # =============================================================================
315
+ # NOT NULL CONSTRAINTS
316
+ # =============================================================================
317
+
318
+ class CreateAccounts < ActiveRecord::Migration[7.2]
319
+ def change
320
+ create_table :accounts do |t|
321
+ # Critical fields - null: false in DB
322
+ t.string :name, null: false
323
+ t.string :email, null: false
324
+
325
+ # Optional fields - allow null
326
+ t.string :phone
327
+ t.text :bio
328
+
329
+ t.timestamps
330
+ end
331
+ end
332
+ end
333
+
334
+ class Account < ApplicationRecord
335
+ # Match DB constraints for user-friendly errors
336
+ validates :name, presence: true
337
+ validates :email, presence: true
338
+
339
+ # Optional fields - no presence validation
340
+ end
341
+
342
+ # =============================================================================
343
+ # FULL PATTERN: CRITICAL USER MODEL
344
+ # =============================================================================
345
+
346
+ # Migration
347
+ class CreateCriticalUsers < ActiveRecord::Migration[7.2]
348
+ def change
349
+ create_table :critical_users do |t|
350
+ # Required fields
351
+ t.string :email, null: false
352
+ t.string :username, null: false
353
+ t.string :encrypted_password, null: false
354
+
355
+ # Optional but constrained
356
+ t.integer :age
357
+ t.decimal :balance, precision: 10, scale: 2, default: 0
358
+
359
+ # Status with valid values
360
+ t.string :status, null: false, default: "pending"
361
+
362
+ t.timestamps
363
+ end
364
+
365
+ # Unique constraints
366
+ add_index :critical_users, :email, unique: true
367
+ add_index :critical_users, :username, unique: true
368
+
369
+ # Check constraints
370
+ add_check_constraint :critical_users, "age IS NULL OR age >= 0",
371
+ name: "critical_users_age_valid"
372
+ add_check_constraint :critical_users, "balance >= 0",
373
+ name: "critical_users_balance_non_negative"
374
+ add_check_constraint :critical_users,
375
+ "status IN ('pending', 'active', 'suspended', 'deleted')",
376
+ name: "critical_users_status_valid"
377
+ end
378
+ end
379
+
380
+ # Model
381
+ class CriticalUser < ApplicationRecord
382
+ # User-facing validations with friendly messages
383
+ validates :email,
384
+ presence: true,
385
+ uniqueness: { case_sensitive: false, message: "is already registered" },
386
+ format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email" }
387
+
388
+ validates :username,
389
+ presence: true,
390
+ uniqueness: { case_sensitive: false },
391
+ length: { in: 3..30, message: "must be between 3 and 30 characters" },
392
+ format: {
393
+ with: /\A[a-z0-9_]+\z/,
394
+ message: "can only contain lowercase letters, numbers, and underscores"
395
+ }
396
+
397
+ validates :age,
398
+ numericality: {
399
+ greater_than_or_equal_to: 0,
400
+ only_integer: true,
401
+ message: "must be a positive number"
402
+ },
403
+ allow_nil: true
404
+
405
+ validates :balance,
406
+ numericality: {
407
+ greater_than_or_equal_to: 0,
408
+ message: "cannot be negative"
409
+ }
410
+
411
+ validates :status,
412
+ inclusion: {
413
+ in: %w[pending active suspended deleted],
414
+ message: "is not a valid status"
415
+ }
416
+
417
+ # Internal validation (not user-controlled)
418
+ validates :encrypted_password, presence: true
419
+
420
+ # Handle race conditions gracefully
421
+ def self.create_safely(attributes)
422
+ create!(attributes)
423
+ rescue ActiveRecord::RecordNotUnique => e
424
+ user = new(attributes)
425
+ if e.message.include?("email")
426
+ user.errors.add(:email, "is already registered")
427
+ elsif e.message.include?("username")
428
+ user.errors.add(:username, "is already taken")
429
+ end
430
+ user
431
+ end
432
+ end