Skip to content

Using Types

hackartisan edited this page Aug 23, 2022 · 7 revisions

Introduction

The type system for models comes from dry-types and is implemented via dry-struct, which have their own sets of documentation which we highly recommend everyone to read. However, as typing is a core aspect of Valkyrie, we felt it important to include our own little tutorial.

The purpose of types is to allow your models to automatically cast attributes to the appropriate type or even fail to instantiate should you give an inappropriate type. Some examples can be found below.

Built-in Types

Consult the dry-types list of built-in types for the list of standard types, including the typical String, boolean, numeric, datetime, etc. types. When using in Valkyrie, prefix the type with Valkyrie:: (e.g., Types::Integer becomes Valkyrie::Types::Integer).

Optional Types

Types which are marked as optional will become nil if not given a value on instantiation.

class Book < Valkyrie::Resource
  attribute :id, Valkyrie::Types::ID.optional
end

Book.new.id # => nil

Singular Values

There are a variety of types for single values, many of which can be found here. An important aspect to remember about Valkyrie is that while you can define a singular type, they will be stored in the backend as an array and then re-cast to singular. This is so that if you later change your mind and decide that you have multiple creators, you won't have to perform a costly data migration.

class Book < Valkyrie::Resource
  attribute :id, Valkyrie::Types::ID.optional
  attribute :creator, Valkyrie::Types::String
end

book = Book.new(creator: "Harry Potter")
book.creator # => "Harry Potter"
output = persister.save(resource: book) # This saves in your backend as ["Harry Potter"], but will cast to..
output.creator # => "Harry Potter"

Warnings

If you set a value to be multiple, insert data into it, and then change it to singular, when it loads out of the database your "singular" field will still be an array. There are a couple options to prevent this:

  1. Migrate your data - if you convert all your newly singular fields to only have one value, then it will return just the single value the way you expect.

    query_service.find_all.each do |resource|
      resource.creator = Array(resource.creator).first
      persister.save(resource: resource)
    end
  2. Use Strict singular types. When a multiple value gets populated into it, it'll throw an error. You'll have to migrate your data eventually, but you won't have to worry about the rest of your infrastructure doing inadvertent things with data it doesn't expect.

    class Book < Valkyrie::Resource
       attribute :creator, Valkyrie::Types::Strict::String
    end
    Book.new(creator: ["Harry", "Potter"])
    # => Dry::Struct::Error: [Book.new] ["Harry", "Potter"] (Array) has invalid type for :creator violates constraints (type?(String, ["Harry", "Potter"]) failed)

Set & Array

Valkyrie::Types::Set and Valkyrie::Types::Array are provided as options for multi-valued fields. Set will de-duplicate values, and Array allows duplicate values. Both will cast singular values to an array. You can define what kind of members are in an Array or Set via the of operator.

The default type for an attribute declaration is Valkyrie::Types::Set.optional

class Book < Valkyrie::Resource
  attribute :creators, Valkyrie::Types::Set
  attribute :nil_creators, Valkyrie::Types::Set.optional
  attribute :array_creators, Valkyrie::Types::Array
end

book = Book.new(creators: "one", array_creators: ["one", "one"])

book.creators # => ["one"]
book.nil_creators # => nil
book.array_creators # => ["one", "one"]

Ordering Values

Since: 1.2.0

Attributes are unordered by default. Adding ordered: true to an attribute definition will preserve the order of multiple values.

attribute :authors, Valkyrie::Types::Set.meta(ordered: true)