Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Do You Need That Validation? Let Me Call You Back About It

1,443 views

Published on

Rails apps start nice and cute. Fast forward a year and business logic and view logic are entangled in our validations and callbacks - getting in our way at every turn. Wasn’t this supposed to be easy?

Let’s explore different approaches to improve the situation and untangle the web.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Do You Need That Validation? Let Me Call You Back About It

  1. 1. Thanks for making it!
  2. 2. The magic hook
  3. 3. Event Form
  4. 4. Repetitive
  5. 5. Much better!
  6. 6. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  7. 7. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  8. 8. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  9. 9. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Datetime fields
  10. 10. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  11. 11. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  12. 12. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  13. 13. event = Event.new( name: "Ruby On Ice", location: "Tegernsee", date: "24.02.2019", crew_arrives_at: "6:45", performers_arrive_at: "9:30", open_at: "9:30", starts_at: "10:00", ends_at: "16:00" ) event.valid?
  14. 14. #<Event: name: "Ruby On Ice", location: "Tegernsee", date: Sun, 24 Feb 2019, crew_arrives_at: Sun, 24 Feb 2019 6:45:00, performers_arrive_at: Sun, 24 Feb 2019 9:00:00, open_at: Sun, 24 Feb 2019 9:00:00, starts_at: Sun, 24 Feb 2019 9:30:00, ends_at: Sun, 24 Feb 2019 20:00:00> It works!
  15. 15. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end
  16. 16. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end
  17. 17. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end it "works" do p event.ends_at # Wed, 01 Jan 2042 15:45:00 event.save! p event.ends_at # Sun, 24 Feb 2019 15:45:00 end Have fun debugging!
  18. 18. let(:event) do create :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end it "retrieves the right events" do query = FutureEvents.new expect(query.call(23.years)).to include(event) end Have fun debugging!
  19. 19. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  20. 20. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Smell
  21. 21. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date = date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Why does the model clean up
  22. 22. Validations
  23. 23. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? gitlab/user
  24. 24. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? gitlab/user
  25. 25. Why the effort?
  26. 26. Practice open?
  27. 27. Practice open? No overlap?
  28. 28. Practice open? No overlap? Right skills?
  29. 29. Practice open? No overlap? Right skills? Patient can be contacted?
  30. 30. Practice open? No overlap? Right skills? Patient can be contacted? Associated models
  31. 31. Practice open? No overlap? Right skills? Patient can be contacted? Associated models ...
  32. 32. Expensive Test Setup
  33. 33. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  34. 34. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  35. 35. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } Ways to change?
  36. 36. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  37. 37. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  38. 38. Why are we doing this?
  39. 39. Affordance
  40. 40. Wants to be cuddled
  41. 41. Wants to be fed
  42. 42. class MySolution def do_thing(argument) end end OOP Affordance
  43. 43. Model View Controller Rails Affordance
  44. 44. “Fat Models Skinny Controllers”
  45. 45. 1 or 2 use cases stuck on every model
  46. 46. Controllers Models
  47. 47. Registrations User
  48. 48. Registrations User Users
  49. 49. Registrations User UsersUsers ##. ##. ##.
  50. 50. Controllers Models Service Objects
  51. 51. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##.
  52. 52. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated
  53. 53. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated Validations and Callbacks still mixed
  54. 54. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated Validations and Callbacks still mixed Run all the time by default
  55. 55. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. SignUp ShowIndex ##. ##. ##.
  56. 56. Registrations User ##. SignUpView Controller Service Model
  57. 57. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end A User Model
  58. 58. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end Sign Up / Edit Only
  59. 59. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end View Related
  60. 60. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end WHWWHHYYY???
  61. 61. Registrations User ##. SignUpView Controller Service Model Knowledge?
  62. 62. Registrations User ##. SignUpView Controller Service Model Why Solve it here?
  63. 63. Registrations User ##. SignUpView Controller Service Model Opt Out
  64. 64. Registrations User ##. SignUpView Controller Service Model Could solve here
  65. 65. Registrations User ##. SignUpView Controller Service Model Or here?
  66. 66. Tobi complaining about validations and callbacks You’ve seen:
  67. 67. Do You Need That Validation? Let Me Call You Back About It Tobias Pfeiffer @PragTob pragtob.info
  68. 68. Specifics clutter Model
  69. 69. Specifics clutter Model Hard to get overiew
  70. 70. Specifics clutter Model Hard to get overiew Run all the time
  71. 71. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end ActiveRecord Original
  72. 72. What does Rails offer?
  73. 73. module UserRegistration extend ActiveSupport#:Concern included do validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create end end Concerns
  74. 74. module Copyable def copy_to(destination) Notification.suppress do # Copy logic that creates new # comments that we do not want # triggering notifications. end end end Suppress
  75. 75. class Person < ApplicationRecord validates :email, uniqueness: true, on: :account_setup validates :age, numericality: true, on: :account_setup end Custom Contexts
  76. 76. What’s out there?
  77. 77. Form Objects
  78. 78. Form ObjectsForm ObjectsForm Objects class Registration include ActiveModel#:Model validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password def save if valid? user = BaseUser.new(email: email, password_digest: hash_password) user.save! send_welcome_email true else false end end end
  79. 79. Form ObjectsForm ObjectsPlain ActiveModel class Registration include ActiveModel#:Model validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password def save if valid? user = BaseUser.new(email: email, password_digest: hash_password) user.save! send_welcome_email true else false end end end
  80. 80. Form ObjectsForm ObjectsValidations class Registration include ActiveModel#:Model validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password ###. end
  81. 81. Form ObjectsForm ObjectsAttributes class Registration include ActiveModel#:Model validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password ###. end
  82. 82. Form ObjectsForm ObjectsMap to ActiveRecord class Registration ###. def save if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  83. 83. Form ObjectsForm ObjectsInterface class Registration ###. def save if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  84. 84. Form ObjectsForm ObjectsCallbacks class Registration ###. def save if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  85. 85. def create @user = Registration.new(registration_params) if @user.save # ##. else # ##. end end Same Interface
  86. 86. Inheritance!
  87. 87. Inheritance! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  88. 88. Inheritance! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  89. 89. ActiveType class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  90. 90. class User < ApplicationRecord validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end Original
  91. 91. Almost the same! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  92. 92. Handle STI, Routes etc. class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  93. 93. Changesets
  94. 94. Changesets defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  95. 95. Elixir defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  96. 96. Pipe defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  97. 97. Context defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  98. 98. “strong parameters” defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  99. 99. Validations defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  100. 100. callback defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  101. 101. Mixing concerns defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  102. 102. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } Remember this?
  103. 103. Combinable defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do user |> base_changeset(attrs) |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  104. 104. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Context
  105. 105. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Changeset
  106. 106. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end “after_commit”
  107. 107. defmodule ValidationShowcaseWeb.UserController do def create(conn, %{"user" => user_params}) do case Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully.") |> redirect(to: Routes.user_path(conn, :show, user)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end end Controller
  108. 108. Form ObjectsForm ObjectsSeparate Operations and Validators
  109. 109. trailblazer
  110. 110. class TbRegistrationsController < ApplicationController def create result = Registration#:Create.(params: params) if result.success? redirect_to "/users/", notice: 'User was created.' else @user = result["contract.default"] render "users/new" end end end
  111. 111. class TbRegistrationsController < ApplicationController def create result = Registration#:Create.(params: params) if result.success? redirect_to "/users/", notice: 'User was created.' else @user = result["contract.default"] render "users/new" end end end Operation
  112. 112. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end
  113. 113. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Setup Model
  114. 114. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Setup Form Object
  115. 115. module Registration#:Contract class Create < Reform#:Form include Dry include Reform#:Form#:ActiveModel feature Coercion model :tb_registration property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  116. 116. module Registration#:Contract class Create < Reform#:Form include Dry include Reform#:Form#:ActiveModel feature Coercion model :tb_registration property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  117. 117. module Registration#:Contract class Create < Reform#:Form property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  118. 118. Attributes module Registration#:Contract class Create < Reform#:Form property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  119. 119. dry-validation module Registration#:Contract class Create < Reform#:Form property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size#: 8).confirmation required(:terms).value(:true?) end end end
  120. 120. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end validate
  121. 121. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Callback
  122. 122. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Persist
  123. 123. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant: Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Callback
  124. 124. “Models are persistence-only and solely define associations and scopes. No business code is to be found here. No validations, no callbacks.” trailblazer
  125. 125. The architecture eases keeping the business logic (entities) separated from details such as persistence or validations. hanami/model
  126. 126. Takeaway
  127. 127. I don’t hate Rails
  128. 128. I don’t hate Rails Future in Rails?
  129. 129. I don’t hate Rails Affordances Future in Rails?
  130. 130. I don’t hate Rails Alternatives Affordances Future in Rails?
  131. 131. Form Objects
  132. 132. Inheritance!
  133. 133. Changesets
  134. 134. Form ObjectsForm ObjectsSeparate Operations and Validators
  135. 135. I don’t hate Rails Careful with validations and callbacks Alternatives Affordances Future in Rails?
  136. 136. Thank you!

×