We can take data integrity a step further: we can optionally define our own entity internal schema.

Custom schema is optional for SQL databases, while it’s mandatory for entities without a database table, or while using with a non-SQL database.

Custom schema takes precedence over automatic schema. If we use custom schema, we need to manually add all the new columns from the corresponding SQL database table.

Default mode

  1. # lib/bookshelf/entities/user.rb
  2. class User < Hanami::Entity
  3. EMAIL_FORMAT = /\@/
  4. attributes do
  5. attribute :id, Types::Int
  6. attribute :name, Types::String
  7. attribute :email, Types::String.constrained(format: EMAIL_FORMAT)
  8. attribute :age, Types::Int.constrained(gt: 18)
  9. attribute :profile, Types::Entity(Profile)
  10. attribute :codes, Types::Collection(Types::Coercible::Int)
  11. attribute :comments, Types::Collection(Comment)
  12. attribute :created_at, Types::Time
  13. attribute :updated_at, Types::Time
  14. end
  15. end

Let’s instantiate it with proper values:

  1. user = User.new(name: "Luca", age: 35, email: "luca@hanami.test")
  2. user.name # => "Luca"
  3. user.age # => 35
  4. user.email # => "luca@hanami.test"
  5. user.codes # => nil
  6. user.comments # => nil

It can coerce values:

  1. user = User.new(codes: ["123", "456"])
  2. user.codes # => [123, 456]

Other entities can be passed as concrete instance:

  1. user = User.new(comments: [Comment.new(text: "cool")])
  2. user.comments
  3. # => [#<Comment:0x007f966be20c58 @attributes={:text=>"cool"}>]

Or as data:

  1. user = User.new(comments: [{text: "cool"}])
  2. user.comments
  3. # => [#<Comment:0x007f966b689e40 @attributes={:text=>"cool"}>]

It enforces data integrity via exceptions:

  1. User.new(email: "foo") # => TypeError: "foo" (String) has invalid type for :email
  2. User.new(comments: [:foo]) # => TypeError: :foo must be coercible into Comment

Strict mode

  1. # lib/bookshelf/entities/user.rb
  2. class User < Hanami::Entity
  3. EMAIL_FORMAT = /\@/
  4. attributes :strict do
  5. attribute :id, Types::Strict::Int
  6. attribute :name, Types::Strict::String
  7. attribute :email, Types::Strict::String.constrained(format: EMAIL_FORMAT)
  8. attribute :age, Types::Strict::Int.constrained(gt: 18)
  9. end
  10. end

Let’s instantiate it with proper values:

  1. user = User.new(id: 1, name: "Luca", age: 35, email: "luca@hanami.test")
  2. user.id # => 1
  3. user.name # => "Luca"
  4. user.age # => 35
  5. user.email # => "luca@hanami.test"

It cannot be instantiated with missing keys

  1. User.new
  2. # => ArgumentError: :id is missing in Hash input
  1. User.new(id: 1, name: "Luca", age: 35)
  2. # => ArgumentError: :email is missing in Hash input

Or with nil:

  1. User.new(id: 1, name: nil, age: 35, email: "luca@hanami.test")
  2. # => TypeError: nil (NilClass) has invalid type for :name violates constraints (type?(String, nil) failed)

It accepts strict values and it doesn’t attempt to coerce:

  1. User.new(id: "1", name: "Luca", age: 35, email: "luca@hanami.test")
  2. # => TypeError: "1" (String) has invalid type for :id violates constraints (type?(Integer, "1") failed)

It enforces data integrity via exceptions:

  1. User.new(id: 1, name: "Luca", age: 1, email: "luca@hanami.test")
  2. # => TypeError: 1 (Integer) has invalid type for :age violates constraints (gt?(18, 1) failed)
  3. User.new(id: 1, name: "Luca", age: 35, email: "foo")
  4. # => TypeError: "foo" (String) has invalid type for :email violates constraints (format?(/\@/, "foo") failed)

Learn more about data types in the dedicated article.