Recipes
Symbolize input keys
require 'dry-struct'
module Types
include Dry.Types()
end
class User < Dry::Struct
transform_keys(&:to_sym)
attribute :name, Types::String
end
User.new('name' => 'Jane')
# => #<User name="Jane">
Tolerance to extra keys
Structs ignore extra keys by default. This can be changed by replacing the constructor.
class User < Dry::Struct
# This does the trick
schema schema.strict
attribute :name, Types::String
end
User.new(name: 'Jane', age: 21)
# => Dry::Struct::Error ([User.new] unexpected keys [:age] in Hash input)
Tolerance to missing keys
You can mark certain keys as optional by calling attribute?
.
class User < Dry::Struct
attribute :name, Types::String
attribute? :age, Types::Integer
end
user = User.new(name: 'Jane')
# => #<User name="Jane" age=nil>
user.age
# => nil
In the example above nil
violates the type constraint so be careful with attribute?
.
Default values
Instead of violating constraints you can assign default values to attributes:
class User < Dry::Struct
attribute :name, Types::String
attribute :age, Types::Integer.default(18)
end
User.new(name: 'Jane')
# => #<User name="Jane" age=18>
Resolving default values on nil
nil
as a value isn't replaced with a default value for default types. You may use transform_types
to turn all types into constructors which map nil
to Dry::Types::Undefined
which in order triggers default values.
class User < Dry::Struct
transform_types do |type|
if type.default?
type.constructor do |value|
value.nil? ? Dry::Types::Undefined : value
end
else
type
end
end
attribute :name, Types::String
attribute :age, Types::Integer.default(18)
end
User.new(name: 'Jane')
# => #<User name="Jane" age=18>
User.new(name: 'Jane', age: nil)
# => #<User name="Jane" age=18>
Creating a custom struct class
You can combine examples from this page to create a custom-purposed base struct class and the reuse it your application or gem
class MyStruct < Dry::Struct
# throw an error when unknown keys provided
schema schema.strict
# convert string keys to symbols
transform_keys(&:to_sym)
# resolve default types on nil
transform_types do |type|
if type.default?
type.constructor do |value|
value.nil? ? Dry::Types::Undefined : value
end
else
type
end
end
end
Set default value for a nested hash
class Foo < Dry::Struct
attribute :bar do
attribute :nested, Types::Integer
end
end
class Foo < Dry::Struct
class Bar < Dry::Struct
attribute :nested, Types::Integer
end
attribute :bar, Bar.default { Bar.new(nested: 1) }
end
Composing structs
You can compose other struct attributes as if they had been defined in place.
class Address < Dry::Struct
attribute :city, Types::String
attribute :country, Types::String
end
class User < Dry::Struct
attribute :name, Types::String
attributes_from Address
end
User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
Composition can happen within a nested attribute:
class User < Dry::Struct
attribute :name, Types::String
attribute :address do
attributes_from Address
end
end