dry-rb/dry-structメモ
2021/08/14時点、v1.4.0で確認。
参照:https://dry-rb.org/gems/dry-struct/master/
dry-structの導入
gem 'dry-struct'
dry-typesはdry-structが依存しているので、自動的にインストールされる。
基本的な使い方
# dry-typesを導入
module Types
include Dry.Types()
end
class Book < Dry::Struct
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
def to_s
"タイトル:#{name}/値段:#{price}円"
end
end
book = Book.new(name: 'よくわかるRuby', price: 2800)
book.name # => "よくわかるRuby"
book.to_s # => "タイトル:よくわかるRuby/値段:2800円"
attribute 属性名, 型
で属性を宣言する。型の性質についてはdry-typesを参照。
ValueObject的な振る舞いをする
dry-structの属性は変更する方法がない。厳密にはbook.attributes[:price] = 100
とすれば変更は可能ではあるが動作保証外のやり方だろう。
等価比較もできる。
book = Book.new(name: 'よくわかるRuby', price: 2800)
book2 = Book.new(name: 'よくわかるRuby', price: 2800)
book3 = Book.new(name: 'よくわからないRuby', price: 2800)
book == book2 # => true
book == book3 # => false
シンボル以外のキーを受け取る
デフォルトではコンストラクタの引数はキーがシンボルのハッシュしか受け付けない。
Book.new('name' => 'よくわかるRuby', 'price' => 2800)
# => Dry::Struct::Error ([Book.new] :name is missing in Hash input)
しかし、transform_keys(&:to_sym)
を追加すると問題なくインスタンス化できるようになる。
class Book < Dry::Struct
transform_keys(&:to_sym)
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
end
# 文字列key
Book.new('name' => 'よくわかるRuby', 'price' => 2800)
# => #<Book name="よくわかるRuby" price=2800>
# シンボルkey
Book.new(name: 'よくわかるRuby', price: 2800)
# => #<Book name="よくわかるRuby" price=2800>
こうすると、デフォルトでは渡せなかったControllerのparamsも渡すことができるようになる。
# ControllerのparamsはActionController::Parametersのインスタンス
params = ActionController::Parameters.new(name: 'よくわかるRuby', price: 2800)
# デフォルトではエラーになるが`transform_keys(&:to_sym)`を
# 指定しているのでインスタンス化できる
Book.new(params.permit(:name, :price))
# => #<Book name="よくわかるRuby" price=2800>
余分なキーを受け入れるかどうか
デフォルトでは余分なキーが指定されていたとしても単に無視される。
Book.new(name: 'よくわかるRuby', price: 2800, publisher: 'ABC出版')
# => #<Book name="よくわかるRuby" price=2800>
以下のようにschema schema.strict
を使うと余分なキーが指定されていた場合、例外が返されるようになる。
class Book < Dry::Struct
schema schema.strict
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
end
Book.new(name: 'よくわかるRuby', price: 2800, publisher: 'ABC出版')
# => Dry::Struct::Error ([Book.new] unexpected keys [:publisher] in Hash input)
属性の欠損を受け入れるかどうか
デフォルトではすべての属性についてコンストラクタで指定しなければならない。
Book.new(name: 'よくわかるRuby')
# => Dry::Struct::Error ([Book.new] :price is missing in Hash input)
以下のように、?
をつけると未指定も許容されるようになる。
class Book < Dry::Struct
# 属性名に?をつけるケース。
# ドキュメントには載っていないので`attribute?`の方がよいだろう
attribute :name?, Types::String
# `attribute?`を使うケース。ドキュメントに記載されている方法
attribute? :price, Types::Coercible::Integer
end
Book.new(name: 'よくわかるRuby') # => #<Book name="よくわかるRuby" price=nil>
Book.new(price: 2800) # => #<Book name=nil price=2800>
Book.new # => #<Book name=nil price=nil>
デフォルト値をつける
以下のようにするとデフォルト値を指定できる。
class Book < Dry::Struct
attribute :name, Types::String.default('名称未定'.freeze)
attribute :price, Types::Coercible::Integer
end
Book.new(price: 2800) # => #<Book name="名称未定" price=2800>
# しかし、nilを指定された場合はデフォルト値にはならない
Book.new(price: 2800, name: nil)
# => Dry::Struct::Error ([Book.new] nil (NilClass) has invalid type for :name violates constraints (type?(String, nil) failed))
もし、nil
が指定された場合にもデフォルト値に置き換えたい場合は、transform_typesを使った方法で対処しなければいけない。
ネスト
構造体をネストさせることができる。
class Book < Dry::Struct
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
attribute :author do
attribute :first_name, Types::String
attribute :last_name, Types::String
end
end
book = Book.new(name: 'よくわかるRuby', price: 2800, author: { first_name: '太郎', last_name: '山田' })
# => #<Book name="よくわかるRuby" price=2800 author=#<Book::Author first_name="太郎" last_name="山田">>
book.author # => #<Book::Author first_name="太郎" last_name="山田">
デフォルトではネストした属性(上記ならauthor
)のクラス(上記ならBook::Author
)はDry::Struct
を継承したものになる。別のクラスを継承させたい場合、attribute
の第二引数に指定する。
class Book < Dry::Struct
attribute :author, MyStruct do
# ...
end
end
ネストした構造体は配列にすることもできる。
class Book < Dry::Struct
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
attribute :authors, Types::Array do
attribute :first_name, Types::String
attribute :last_name, Types::String
end
end
Book.new(name: 'よくわかるRuby', price: 2800, authors: [{ first_name: '太郎', last_name: '山田' }])
# => #<Book name="よくわかるRuby" price=2800 authors=[#<Book::Author first_name="太郎" last_name="山田">]>
ネストした構造体のデフォルト値
attribute
メソッドのネストで表現することはできず、以下のようにしなければならない。
class Book < Dry::Struct
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
class Author < Dry::Struct
attribute :first_name, Types::String
attribute :last_name, Types::String
end
attribute :author, Author.default { Author.new(first_name: '権兵衛', last_name: '名無しの') }
end
Book.new(name: 'よくわかるRuby', price: 2800)
# => #<Book name="よくわかるRuby" price=2800 author=#<Book::Author first_name="権兵衛" last_name="名無しの">>
構造体の合成
他の構造体で定義されている属性をあたかも自分の構造体で定義された属性であるかのように合成することができる。
class Product < Dry::Struct
attribute :name, Types::String
attribute :price, Types::Coercible::Integer
end
class Name < Dry::Struct
attribute :first_name, Types::String
attribute :last_name, Types::String
end
class Book < Dry::Struct
# Product構造体から属性を合成
attributes_from Product
attribute :author do
# ネストした構造体でも合成できる
attributes_from Name
end
end
Book.new(name: 'よくわかるRuby', price: 2800, author: { first_name: '太郎', last_name: '山田' })
# => #<Book name="よくわかるRuby" price=2800 author=#<Book::Author first_name="太郎" last_name="山田">>
ベースクラスを作っておく
class BaseStruct < Dry::Struct
# 文字列キーでもエラーにならないようにする
transform_keys(&:to_sym)
# 余計なキー指定を拒否する
schema schema.strict
module Types
include Dry.Types()
end
# include NilToDefault
# の形で使う
# 値にnilが指定された場合、
# default指定がある属性ならデフォルト値に置き換える
module NilToDefault
def self.included(mod)
mod.transform_types do |type|
if type.default?
type.constructor do |value|
value.nil? ? Dry::Types::Undefined : value
end
else
type
end
end
end
end
end
class Book < BaseStruct
attribute :name, Types::String
# ...
end