Thanks for making it!
The magic hook
Event Form
Repetitive
Much better!
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
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
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
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
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
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
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
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?
#<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!
Do You Need That Validation? Let Me Call You Back About It
let(:event) do
build :event,
ends_at: Time.zone.local(2042, 1, 1, 15, 45)
end
let(:event) do
build :event,
ends_at: Time.zone.local(2042, 1, 1, 15, 45)
end
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!
Do You Need That Validation? Let Me Call You Back About It
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!
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
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
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
Validations
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
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
Why the effort?
Practice open?
Practice open?
No overlap?
Practice open?
No overlap?
Right skills?
Practice open?
No overlap?
Right skills?
Patient can be contacted?
Practice open?
No overlap?
Right skills?
Patient can be contacted?
Associated models
Practice open?
No overlap?
Right skills?
Patient can be contacted?
Associated models
...
Expensive Test Setup
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
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
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?
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
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
Why are we
doing this?
Affordance
Wants to be cuddled
Wants to be fed
class MySolution
def do_thing(argument)
end
end
OOP Affordance
Model
View
Controller
Rails Affordance
“Fat Models
Skinny Controllers”
1 or 2 use cases
stuck on every model
Controllers
Models
Registrations
User
Registrations
User
Users
Registrations
User
UsersUsers ##. ##. ##.
Controllers
Models
Service Objects
Registrations
User
UsersUsers ##. ##. ##.
##. ##. ##. ##. ##.
Registrations
User
UsersUsers ##. ##. ##.
##. ##. ##. ##. ##.
Business Logic Separated
Registrations
User
UsersUsers ##. ##. ##.
##. ##. ##. ##. ##.
Business Logic Separated
Validations and Callbacks still mixed
Registrations
User
UsersUsers ##. ##. ##.
##. ##. ##. ##. ##.
Business Logic Separated
Validations and Callbacks still mixed
Run all the time by default
Registrations
User
UsersUsers ##. ##. ##.
##. ##. ##. ##. ##.
SignUp ShowIndex ##. ##. ##.
Registrations
User
##.
SignUpView
Controller
Service
Model
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
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
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
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???
Registrations
User
##.
SignUpView
Controller
Service
Model
Knowledge?
Registrations
User
##.
SignUpView
Controller
Service
Model
Why Solve it here?
Registrations
User
##.
SignUpView
Controller
Service
Model
Opt Out
Do You Need That Validation? Let Me Call You Back About It
Do You Need That Validation? Let Me Call You Back About It
Registrations
User
##.
SignUpView
Controller
Service
Model
Could solve here
Registrations
User
##.
SignUpView
Controller
Service
Model
Or here?
Do You Need That Validation? Let Me Call You Back About It
Tobi complaining
about validations
and callbacks
You’ve seen:
Do You Need That Validation?
Let Me Call You Back About It
Tobias Pfeiffer
@PragTob
pragtob.info
Do You Need That Validation? Let Me Call You Back About It
Specifics clutter Model
Specifics clutter Model
Hard to get overiew
Specifics clutter Model
Hard to get overiew
Run all the time
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
What does Rails offer?
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
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
class Person < ApplicationRecord
validates :email,
uniqueness: true,
on: :account_setup
validates :age,
numericality: true,
on: :account_setup
end
Custom Contexts
What’s out there?
Form Objects
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
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
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
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
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
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
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
def create
@user = Registration.new(registration_params)
if @user.save
# ##.
else
# ##.
end
end
Same Interface
Inheritance!
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
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
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
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
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
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
Changesets
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
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
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
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
“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
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
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
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
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?
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
defmodule ValidationShowcase.Accounts do
def create_user(attrs  %{}) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
|> send_welcome_email()
end
end
Context
defmodule ValidationShowcase.Accounts do
def create_user(attrs  %{}) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
|> send_welcome_email()
end
end
Changeset
defmodule ValidationShowcase.Accounts do
def create_user(attrs  %{}) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
|> send_welcome_email()
end
end
“after_commit”
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
Form ObjectsForm ObjectsSeparate Operations and Validators
trailblazer
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
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
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
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
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
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
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
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
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
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
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
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
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
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
“Models are persistence-only and
solely define associations and
scopes. No business code is to be
found here. No validations, no
callbacks.”
trailblazer
The architecture eases keeping the
business logic (entities) separated
from details such as persistence or
validations.
hanami/model
Takeaway
I don’t hate Rails
I don’t hate Rails
Future in Rails?
I don’t hate Rails
Affordances
Future in Rails?
I don’t hate Rails
Alternatives
Affordances
Future in Rails?
Form Objects
Inheritance!
Changesets
Form ObjectsForm ObjectsSeparate Operations and Validators
I don’t hate Rails
Careful with validations and callbacks
Alternatives
Affordances
Future in Rails?
Thank you!

More Related Content

PDF
Don't Settle for Poor Names
PDF
[4developers] The saga pattern v3- Robert Pankowiecki
PDF
What Would You Do? With John Quinones
PDF
JS Fest 2019. Glenn Reyes. With great power comes great React hooks!
PPTX
Angular Tutorial Freshers and Experienced
KEY
Zend framework service
KEY
Zend framework service
ODP
Ruby on rails
Don't Settle for Poor Names
[4developers] The saga pattern v3- Robert Pankowiecki
What Would You Do? With John Quinones
JS Fest 2019. Glenn Reyes. With great power comes great React hooks!
Angular Tutorial Freshers and Experienced
Zend framework service
Zend framework service
Ruby on rails

Similar to Do You Need That Validation? Let Me Call You Back About It (20)

PDF
Don't Settle for Poor Names (Or Poor Design)
PDF
Introduction to Zend Framework web services
KEY
Building Web Service Clients with ActiveModel
KEY
Building Web Service Clients with ActiveModel
ODP
Very basic functional design patterns
PDF
Simplify Your Rails Controllers With a Vengeance
PDF
Akka persistence webinar
PDF
JavaScript Refactoring
PPTX
Testing C# and ASP.net using Ruby
PDF
Let it crash - fault tolerance in Elixir/OTP
KEY
Jasmine frontinrio
PDF
Rails 3: Dashing to the Finish
KEY
Working Effectively With Legacy Code
PDF
Hexagonal architecture & Elixir
ODP
Well
PPTX
Introduce cucumber
PDF
WordPress Realtime - WordCamp São Paulo 2015
PPTX
AngularJS, More Than Directives !
PPT
PHP Unit Testing
PDF
Two Trains and Other Refactoring Analogies
Don't Settle for Poor Names (Or Poor Design)
Introduction to Zend Framework web services
Building Web Service Clients with ActiveModel
Building Web Service Clients with ActiveModel
Very basic functional design patterns
Simplify Your Rails Controllers With a Vengeance
Akka persistence webinar
JavaScript Refactoring
Testing C# and ASP.net using Ruby
Let it crash - fault tolerance in Elixir/OTP
Jasmine frontinrio
Rails 3: Dashing to the Finish
Working Effectively With Legacy Code
Hexagonal architecture & Elixir
Well
Introduce cucumber
WordPress Realtime - WordCamp São Paulo 2015
AngularJS, More Than Directives !
PHP Unit Testing
Two Trains and Other Refactoring Analogies
Ad

More from Tobias Pfeiffer (20)

PDF
Going Staff - Keynote @ CodeBEAM EU edition
PDF
Going Staff
PDF
Stories in Open SOurce
PDF
Metaphors are everywhere: Ideas to Improve Software Development
PDF
Stories in Open Source
PDF
Elixir & Phoenix – Fast, Concurrent and Explicit
PDF
Functioning Among Humans
PDF
Functioning Among Humans
PDF
Elixir, your Monolith and You
PDF
Stop Guessing and Start Measuring - Benchmarking in Practice (Lambdadays)
PDF
Where do Rubyists go?
PDF
It's About the Humans, Stupid (Lightning)
PDF
Stop Guessing and Start Measuring - Benchmarking Practice (Poly Version)
PDF
Code, Comments, Concepts, Comprehension – Conclusion?
PDF
How fast is it really? Benchmarking in Practice (Ruby Version)
PDF
How fast ist it really? Benchmarking in practice
PDF
Introducing Elixir the easy way
PDF
Elixir & Phoenix – fast, concurrent and explicit
PDF
What did AlphaGo do to beat the strongest human Go player?
PDF
Elixir & Phoenix – fast, concurrent and explicit
Going Staff - Keynote @ CodeBEAM EU edition
Going Staff
Stories in Open SOurce
Metaphors are everywhere: Ideas to Improve Software Development
Stories in Open Source
Elixir & Phoenix – Fast, Concurrent and Explicit
Functioning Among Humans
Functioning Among Humans
Elixir, your Monolith and You
Stop Guessing and Start Measuring - Benchmarking in Practice (Lambdadays)
Where do Rubyists go?
It's About the Humans, Stupid (Lightning)
Stop Guessing and Start Measuring - Benchmarking Practice (Poly Version)
Code, Comments, Concepts, Comprehension – Conclusion?
How fast is it really? Benchmarking in Practice (Ruby Version)
How fast ist it really? Benchmarking in practice
Introducing Elixir the easy way
Elixir & Phoenix – fast, concurrent and explicit
What did AlphaGo do to beat the strongest human Go player?
Elixir & Phoenix – fast, concurrent and explicit
Ad

Recently uploaded (20)

PDF
Five Habits of High-Impact Board Members
PDF
A review of recent deep learning applications in wood surface defect identifi...
PPT
What is a Computer? Input Devices /output devices
PDF
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
PDF
A contest of sentiment analysis: k-nearest neighbor versus neural network
PDF
A comparative study of natural language inference in Swahili using monolingua...
PPT
Module 1.ppt Iot fundamentals and Architecture
PDF
Developing a website for English-speaking practice to English as a foreign la...
PDF
Unlock new opportunities with location data.pdf
PDF
A Late Bloomer's Guide to GenAI: Ethics, Bias, and Effective Prompting - Boha...
PDF
Hybrid model detection and classification of lung cancer
PPT
Geologic Time for studying geology for geologist
PPTX
Tartificialntelligence_presentation.pptx
PPTX
Benefits of Physical activity for teenagers.pptx
PDF
Hybrid horned lizard optimization algorithm-aquila optimizer for DC motor
PDF
DP Operators-handbook-extract for the Mautical Institute
PDF
Zenith AI: Advanced Artificial Intelligence
PDF
NewMind AI Weekly Chronicles – August ’25 Week III
PDF
Univ-Connecticut-ChatGPT-Presentaion.pdf
PDF
WOOl fibre morphology and structure.pdf for textiles
Five Habits of High-Impact Board Members
A review of recent deep learning applications in wood surface defect identifi...
What is a Computer? Input Devices /output devices
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
A contest of sentiment analysis: k-nearest neighbor versus neural network
A comparative study of natural language inference in Swahili using monolingua...
Module 1.ppt Iot fundamentals and Architecture
Developing a website for English-speaking practice to English as a foreign la...
Unlock new opportunities with location data.pdf
A Late Bloomer's Guide to GenAI: Ethics, Bias, and Effective Prompting - Boha...
Hybrid model detection and classification of lung cancer
Geologic Time for studying geology for geologist
Tartificialntelligence_presentation.pptx
Benefits of Physical activity for teenagers.pptx
Hybrid horned lizard optimization algorithm-aquila optimizer for DC motor
DP Operators-handbook-extract for the Mautical Institute
Zenith AI: Advanced Artificial Intelligence
NewMind AI Weekly Chronicles – August ’25 Week III
Univ-Connecticut-ChatGPT-Presentaion.pdf
WOOl fibre morphology and structure.pdf for textiles

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

  • 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. 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. 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. 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. 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. 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. 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. 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. #<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!
  • 16. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end
  • 17. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45) end
  • 18. 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!
  • 20. 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!
  • 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
  • 22. 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
  • 23. 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
  • 25. 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
  • 26. 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
  • 31. Practice open? No overlap? Right skills? Patient can be contacted?
  • 32. Practice open? No overlap? Right skills? Patient can be contacted? Associated models
  • 33. Practice open? No overlap? Right skills? Patient can be contacted? Associated models ...
  • 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? } gitlab/user
  • 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. 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?
  • 38. 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
  • 39. 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
  • 42. Wants to be cuddled
  • 43. Wants to be fed
  • 47. 1 or 2 use cases stuck on every model
  • 53. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##.
  • 54. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated
  • 55. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated Validations and Callbacks still mixed
  • 56. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. Business Logic Separated Validations and Callbacks still mixed Run all the time by default
  • 57. Registrations User UsersUsers ##. ##. ##. ##. ##. ##. ##. ##. SignUp ShowIndex ##. ##. ##.
  • 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 A User Model
  • 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 Sign Up / Edit Only
  • 61. 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
  • 62. 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???
  • 71. Tobi complaining about validations and callbacks You’ve seen:
  • 72. Do You Need That Validation? Let Me Call You Back About It Tobias Pfeiffer @PragTob pragtob.info
  • 76. Specifics clutter Model Hard to get overiew Run all the time
  • 77. 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
  • 78. What does Rails offer?
  • 79. 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
  • 80. 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
  • 81. class Person < ApplicationRecord validates :email, uniqueness: true, on: :account_setup validates :age, numericality: true, on: :account_setup end Custom Contexts
  • 84. 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
  • 85. 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
  • 86. 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
  • 87. 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
  • 88. 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
  • 89. 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
  • 90. 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
  • 91. def create @user = Registration.new(registration_params) if @user.save # ##. else # ##. end end Same Interface
  • 93. 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
  • 94. 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
  • 95. 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
  • 96. 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
  • 97. 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
  • 98. 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
  • 100. 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
  • 101. 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
  • 102. 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
  • 103. 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
  • 104. “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
  • 105. 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
  • 106. 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
  • 107. 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
  • 108. 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?
  • 109. 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
  • 110. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Context
  • 111. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Changeset
  • 112. defmodule ValidationShowcase.Accounts do def create_user(attrs %{}) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end “after_commit”
  • 113. 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
  • 114. Form ObjectsForm ObjectsSeparate Operations and Validators
  • 116. 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
  • 117. 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
  • 118. 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
  • 119. 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
  • 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 Setup Form Object
  • 121. 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
  • 122. 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
  • 123. 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
  • 124. 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
  • 125. 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
  • 126. 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
  • 127. 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
  • 128. 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
  • 129. 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
  • 130. “Models are persistence-only and solely define associations and scopes. No business code is to be found here. No validations, no callbacks.” trailblazer
  • 131. The architecture eases keeping the business logic (entities) separated from details such as persistence or validations. hanami/model
  • 133. I don’t hate Rails
  • 134. I don’t hate Rails Future in Rails?
  • 135. I don’t hate Rails Affordances Future in Rails?
  • 136. I don’t hate Rails Alternatives Affordances Future in Rails?
  • 140. Form ObjectsForm ObjectsSeparate Operations and Validators
  • 141. I don’t hate Rails Careful with validations and callbacks Alternatives Affordances Future in Rails?