dry-rb
Single purpose libraries for the Rubyist soul
Utility Gems
- dry-equalizer: Object equal testing
- dry-result_matcher: Pattern matching
- dry-transaction: Railway programming for business logic
Dependency Management Gems
- dry-container: IoC container
- dry-auto_inject: Automatic dependency injection
- dry-component: Dependency management system
Data Gems
- dry-logic: Predicate logic
- dry-types: Flexible type system
- dry-validation: Predicate-based data validation
Our focus for today
- dry-types
- dry-validation
dry-types
Flexible type system with many built-in types
dry-types: Built-in Types
Base Types
Types::Array Types::Bool Types::Class
Types::Date Types::DateTime Types::Hash
Types::Nil Types::Symbol Types::Time
dry-types: Built-in Types
Strict Types (with primitive type check)
Types::Strict::Array #=> Array
Types::Strict::Decimal #=> BigDecimal
Types::Strict::Float #=> Float
Types::Strict::Hash #=> Hash
Types::Strict::Int #=> Integer
Types::Strict::String #=> String
dry-types: Built-in Types
Coercible Types (via Kernel coercion)
Types::Coercible::Array[1] #=> [1]
Types::Coercible::Decimal[1] #=> BigDecimal('1.0')
Types::Coercible::Float[1] #=> 1.0
Types::Coercible::Hash[nil] #=> {}
Types::Coercible::Int["1"] #=> 1
Types::Coercible::String[1] #=> "1"
dry-types: Built-in Types
Form Types (for params objects)
Types::Form::DateTime["2016-04-14T18:00:00-05:00"]
Types::Form::Bool["t"] #=> true
Types::Form::Bool["1"] #=> true
Types::Form::Bool["f"] #=> false
Types::Form::Bool["0"] #=> false
dry-types: Built-in Types
Maybe Types (Monads!)
Types::Maybe::Coercible::String[:wat] #=> Some("wat")
Types::Maybe::Coercible::String[nil] #=> None
Types::Maybe::Strict::String[:wat] #=> ConstraintError
Types::Maybe::Strict::String[nil] #=> None
dry-types: Defining Types
Default Values
LastName = Types::Strict::String.default("Awesome")
LastName[nil] #=> "Awesome"
LastName[""] #=> "Awesome"
LastName["Matsumoto"] #=> "Matsumoto"
dry-types: Defining Types
Constraints
HexColor = Types::Strict::String.constrained(
format: /\A\#(?:[\da-f]{6}|[\da-f]{3})\z/i
)
HexColor["#fff"] #=> "#fff"
HexColor["#e5e5e5"] #=> "#e5e5e5"
HexColor["#1234"] #=> ConstraintError
dry-types: Defining Types
Sum Types
HexColor = Types::Strict::String.constrained(...)
RgbColor = Types::Strict::String.constrained(...)
Color = HexColor | RgbColor
Color["#fff"] #=> "#fff"
Color["rgb(239, 239, 239)"] #=> "rgb(239, 239, 239)"
Color["not a color"] #=> ConstraintError
dry-types: Structs
class User < Dry::Types::Struct
attribute :age, Types::Coercible::Int
attribute :color, Color
attribute :name, Types::Maybe::Coercible::String
end
user = User.new(name: nil, age: "29", color: "#ff0")
user.name #=> None
user.age #=> 29
user.color #=> "#ff0"
dry-types: Value Objects
class Point < Dry::Types::Value
attribute :x, Types::Coercible::Int
attribute :y, Types::Coercible::Int
end
origin = Point.new(x: 0, y: 0)
string_origin = Point.new(x: "0", y: "0")
origin == string_origin #=> true
dry-types: Extras
Enums, Hash schemas, and Array members
PostStatus = Types::Strict::String.enum("draft", "published")
PostStatuses = Types::Strict::Array.member(PostStatus)
PostParams = Types::Hash.schema(
title: Types::String,
published_on: Types::DateTime,
status: PostStatus
)
dry-types: Wrap up
- Flexible type system
- Handles strict input an coercible input
- Extremely fast!
dry-validation
Predicate-based data validation
dry-validation: Schemas
PostSchema = Dry::Validation.Schema do
key(:title).required
key(:status).required(inclusion?: ["draft", "published"])
end
PostSchema.(title: "Test Post", status: "draft").success? #=> true
PostSchema.(title: "Test Post", status: "other").success? #=> false
PostSchema.(title: "Test Post").success? #=> false
dry-validation: Built-In Predicates
:empty? :eql? :filled? :format?
:gt? :gteq? :key? :lt?
:lteq? :none? :type?
:inclusion? :exclusion?
:max_size? :min_size?
:size?(int) :size?(range)
dry-validation: Custom Predicates
module PostPredicates
include Dry::Logic::Predicates
predicate(:status?) { inclusion?(%w(draft published)) }
end
PostSchema = Dry::Validation.Schema do
configure { config.predicates = PostPredicates }
key(:status).required(&:status?)
end
dry-validation: Messages
PostSchema.(title: "Test Post", status: "other").messages
#=> {:status => ["must be one of: draft, published"]}
PostSchema.(status: "").messages(full: true)
#=> {:title => ["title is missing"],
#=> :status => ["status must be filled",
#=> "status must be one of: draft, published"]}
The same format as ActiveModel::Model
dry-validation: Custom Messages
UserSchema = Dry::Validation.Schema do
configure do
config.messages = :i18n
config.messages_file = "some/yaml/error.yml"
config.namespace = :user
end
end
dry-validation: Optionals
PersonSchema = Dry::Validation.Schema do
optional(:name).maybe(:str?)
optional(:first_name).maybe(:str?)
optional(:last_name).maybe(:str?)
end
dry-validation: Composite Rules
PersonSchema = Dry::Validation.Schema do
# ...
rule(migrate_name: [:name, :first_name, :last_name]) do
|name, fname, lname|
(name.filled? & (fname.none? & lname.none?)) |
(name.none? & (fname.filled? & lname.filled?))
end
end
dry-validation: Wrap Up
- Very expressive
- Extensible
- Sadly no direct integration to dry-types?
Interactors - Without Checking
class CreateUser
include Interactor
def call
context.user = User.create!(
name: context.name,
email: context.email
)
end
end
Repetitive Checking
class CreateUser
before do
%i(email name).each do |attr|
unless context[attr].present?
context.fail!(message: "missing_#{attr}")
end
end
end
end
interactor-contracts
Expectations
class CreateUser
include Interactor::Contracts
expects do
attr(:email, &:filled?)
attr(:name, &:filled?)
end
end
interactor-contracts
Assurances
class CreateUser
include Interactor::Contracts
assures do
attr(:user, &:filled?)
end
end
interactor-contracts
Future Work
- Reuse schemas between interactors
- Check long business logic chains at startup
- Integrate dry-type checking
Presentation available at:
https://michaelherold.github.io/dryrb_presentation
Want to chat?