Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace constructor_type with types/keys transformations #64

Merged
merged 2 commits into from
Jan 14, 2018

Conversation

flash-gordon
Copy link
Member

This removes constructor_type in favor of these two methods:

  1. Struct.transform_types—transforms all new attribute types with an arbitrary block (uses Dry::Types::Schema#with_type_transform).
  2. Struct.transform_keys—transforms input keys with a block (uses Dry::Types::Hash#with_key_transform).

See dry-rb/dry-types#231

For unknown keys it is possible to make the underlying schema strict:

class Book < Dry::Struct
  input input.strict

  attribute :title, Types::Strict::String
end

Book.new(title: "The Old Man and the Sea", foo: :bar) 
# => Dry::Struct::Error ([Book.new] unexpected keys [:foo] in Hash input)

You always can create a base class for strict structs:

StrictStruct = Class.new(Dry::Struct) { input input.strict }

Also, individual attributes can be omitted with adding omittable: true to theirs meta:

class Book < Dry::Struct
  attribute :title, Types::Strict::String
  attribute :year, Types::Strict::Int.meta(omittable: true)
end
Book.new(title: "The Old Man and the Sea")
# => #<Book title="The Old Man and the Sea" year=nil>

You can combine it with tranform_types for getting a struct not raising an error on missing keys:

class Book < Dry::Struct
  transform_types { |t| t.meta(omittable: true) }

  attribute :title, Types::Strict::String
  attribute :year, Types::Strict::Int
end
Book.new # => #<Book title=nil year=nil>

P.S. Note that in the new version of dry-types default types in hash schema are only used/evaluated when a key is missing, not when the value is nil:

class Book < Dry::Struct
  attribute :title, Types::Strict::String
  attribute :year, Types::Strict::Int.default { Time.now.year }
end
Book.new(title: "The Old Man and the Sea")
# => #<Book title="The Old Man and the Sea" year=2018>
# BUT
Book.new(title: "The Old Man and the Sea", year: nil)
# => Dry::Struct::Error ([Book.new] nil (NilClass) has invalid type for :year violates constraints (type?(Integer, nil) failed))

If you want to evaluate default value on nil, use .constructor:

class Book < Dry::Struct
  attribute :title, Types::Strict::String
  attribute :year, Types::Strict::Int.
                     default { Time.now.year }.
                     constructor { |v| v.nil? ? Dry::Types::Undefined : v }
end
# => #<Book title="The Old Man and the Sea" year=2018>

Undefined is treated as a missing value, that's how it works internally.

Remember, you can automate "constructing" with transform_types:

class DefaultOnNilStruct < Dry::Stuct
  nil_2_undef = -> v { v.nil? ? Dry::Types::Undefined : v }
  transform_types { |t| t.default? ? t.constructor(nil_2_undef) : t }
end

class Book < DefaultOnNilStruct
  attribute :title, Types::Strict::String
  attribute :year, Types::Strict::Int.default { Time.now.year }
end
# => #<Book title="The Old Man and the Sea" year=2018>

All manipulations now can be done via `input`, although adding a couple
helpers (transform keys/transform types) would be useful.
@GustavoCaso
Copy link
Member

Awesome work @flash-gordon ❤️.

Just I minor suggestion, what about adding a macro strict that would avoid user the need to type input input.strict that way it would like something:

class Book < Dry::Struct
  strict

  attribute :title, Types::Strict::String
end

WDYT?

@flash-gordon
Copy link
Member Author

@GustavoCaso dunno, this depends on how often it will be used. Maybe instead we could have Dry::Struct::Strict so that it would require less typing.

@GustavoCaso
Copy link
Member

@flash-gordon I really like more your idea ❤️

@solnic
Copy link
Member

solnic commented Jan 13, 2018

Yeah input input.strict looks bad, but this is a very minor problem at this point. It's more important to get desired functionality in place on top of new dry-types APIs. We can follow-up with nicer DSLs (or new sub-classes) later in separate PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants