dry-rb

Single purpose libraries for the Rubyist soul

Utility Gems

Dependency Management Gems

Data Gems

Our focus for today

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

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

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

Presentation available at:

https://michaelherold.github.io/dryrb_presentation

Want to chat?