dry-rb / dry-struct Goto Github PK
View Code? Open in Web Editor NEWTyped struct and value objects
Home Page: https://dry-rb.org/gems/dry-struct
License: MIT License
Typed struct and value objects
Home Page: https://dry-rb.org/gems/dry-struct
License: MIT License
The following example is supposed to raise a dry-rb error, instead it crashes:
require 'dry-struct'
module Types
include Dry::Types.module
end
class Bar < Dry::Struct
attribute :countries, Types::Strict::Array.member(
Types::Coercible::String
.constructor { |s| s.to_s.upcase }
.constrained(included_in: ["AT"])
)
end
Bar.new(countries: ["XX"])
Stacktrace:
/home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/array/member.rb:38:in `map': undefined method `input' for #<Dry::Logic::Result:0x00000002a9c1d8> (NoMethodError)
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/array/member.rb:38:in `try'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/constrained.rb:43:in `try'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/constrained.rb:27:in `call'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:91:in `block in coerce'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:104:in `block in resolve'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:102:in `each'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:102:in `resolve'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:89:in `coerce'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/hash/schema.rb:27:in `call'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-types-0.11.1/lib/dry/types/constructor.rb:47:in `call'
from /home/mau/.rvm/gems/ruby-2.4.1@foo/gems/dry-struct-0.3.1/lib/dry/struct/class_interface.rb:91:in `new'
from lol.rb:15:in `<main>'
From @andreaseger on September 14, 2016 12:31
So given this example:
require 'bundler/inline'
gemfile true do
gem 'dry-types'
end
include Dry::Types.module
class A < Dry::Types::Struct
attribute :a, Strict::Int
end
class AA < A
attribute :aa, Strict::Int
end
class AB < A
attribute :ab, Strict::Int
end
class Foo < Dry::Types::Struct
attribute :bar, A
end
a = A.new(a: 123)
aa = AA.new(a: 123, aa: 234)
ab = AB.new(a: 123, ab: 345)
foo = Foo.new(bar: a)
p [ foo.bar.class,
foo.bar.is_a?(A),
foo.bar.is_a?(AA),
foo.bar.is_a?(AB) ]
# [A, true, false, false]
# everything as expected
foo = Foo.new(bar: aa)
p [ foo.bar.class,
foo.bar.is_a?(A),
foo.bar.is_a?(AA),
foo.bar.is_a?(AB) ]
# [A, true, false, false]
# hmm the type checking worked with subclasses
# but the attribute got somehow coerced? typecasted? type-assigned to it's parent class
foo = Foo.new(bar: ab)
p [ foo.bar.class,
foo.bar.is_a?(A),
foo.bar.is_a?(AA),
foo.bar.is_a?(AB) ]
# [A, true, false, false]
# same as with AA
First of even though I don't remember reading it explicitly using a Dry::Types::Struct
or Value
as Type of an attribute in another Struct
or Value
works (almost) as expected.
The type itself is checked as expected and even stuff like .optional
just works.
What surprised me is how subclassed Struct
/Values
are handled. As can be seen in the above example class A
has two subclasses AA
and AB
. The type checking is done as expected meaning that the subclasses are valid too. But afterwards the attribute is no longer of the same type as the input value. So some form of type-casting/-coersion/-assignment is happening.
Is this a "weird" side-effect from something in the struct code or intended behaviour?
Right now my workaround is to use a sum type with all subtypes instead of the parent type but that only works because right now I have a very limited number of subtypes.
Copied from original issue: dry-rb/dry-types#146
I know from the guides that encourage not to use symbolize
or weak
with dry-struct
but I was looking into other issue with dry-types
and the example in the issue looks like it should raise an exception.
module TestTypes
include Dry::Types.module
end
module Test
class Fail < Dry::Struct
constructor_type :symbolized
attribute :age, TestTypes::Strict::Int
end
end
value = Test::Fail.new("age" => "2.2") #=> <Test::Fail age="2.2">
Looks like the error could be in dry-types
All Struct equality comparisons are by object_id. This probably makes sense for most Class objects, but isn't very useful here:
> s1=Dry.Struct()
=> #<Class:0x00007fe7b80860e0>
> s2=Dry.Struct()
=> #<Class:0x00007fe7b82ee198>
> s1.input == s2.input && s1.meta == s2.meta
=> true
> [s1 == s2, s1.eql?(s2), s1 === s2]
=> [false, false, false]
This is mostly an issue with .meta
, since it is currently implemented by generating a subclass:
> start = Dry.Struct.meta(foo: 'bar')
=> #<Class:0x00007fe7b82ec3e8>
> flip = start.meta(foo: 'woot')
=> #<Class:0x00007fe7b78ed2c0>
> flop = flip.meta(foo: 'bar')
=> #<Class:0x00007fe7b82cc9d0>
> start.input == flop.input && start.meta == flop.meta
=> true
> start == flop
=> false
http://dry-rb.org/gems/dry-types/including-types/
class User < Dry::Types::Struct
attribute :name, Types::String
attribute :email, Types::Email
attribute :age, Types::Age
end
Wrong class name (as of latest version).
I tried to find a way to treat the incoming value of an attribute letting it go to its type coercion and assignment and apparently there isn't way of doing so.
In case I'm mistaken, please let me know. Otherwise, let this be an official request for having such feature.
I thought of something like:
module Types
include Dry::Types.module
end
class Book < Dry::Struct
attribute :title, Types::String, filter: :strip_value
private
def strip_value(value)
value.strip
end
end
I try use anonymous structs based on my simple struct.
And it doesn't work because of error
Dry::Container::Error: Nothing registered with the key "#"
You can quickly reproduce it with:
class MyStruct < Dry::Types::Struct
attribute :my_attr, Types::Strict::String
end
Dry::Types[MyStruct]
# => MyStruct
Dry::Types[Class.new(MyStruct)]
# Dry::Container::Error: Nothing registered with the key "#"
The reason of this is https://github.com/dry-rb/dry-types/blob/master/lib/dry/types/hash.rb#L10, where it casts type, whenever it String or Class
First of all, https://discuss.dry-rb.org/ is down.
A little pattern appeared in my code base. Have to map a collection's items to Dry::Struct objects.
So, I did a module to extend my structs and share this new feature. But in my opinion, I think this could be a common problem and we can add this new little feature to the project.
PS: Another cool feature related to this one is: create another method (or a flag to Dry::Struct.map
) to symbolize string Hash keys.
I tried to migrate from dry-types
to dry-struct
and found out that it does not register custom types. Please consider the following example:
module Foo
class Bar < Dry::Struct
attribute :qux, 'strict.string'
end
class Baz < Dry::Struct
# Fails here: Nothing registered with the key "foo.bar" (Dry::Container::Error)
attribute :bar, 'foo.bar'
end
end
In order to fix this problem, I have to register my custom types explicitly:
Dry::Types.register('foo.bar', Foo::Bar)
I am not sure why this line didn't make it to dry-struct
.
I have a response from an api like this
{
"total": 59198,
"products": [
{
"id": "23423",
"name": {
"German": "s.Oliver BLACK LABE - Damen, Weiß, Größe 44"
},
"brand": "s.Oliver BLACK LABEL",
"image_url": "https://some_url.com",
"category": "Shirts & Tops"
},...
]
}
so i have written a class which is like this:
class SomeService < Dry::Struct
include Dry::Types.module
attribute 'id', String
attribute 'brand', String
attribute 'image_url', String
attribute 'category', String
attribute 'name', Strict::Hash
end
But this is returning the name
object as string
{
"brand": "s.Oliver BLACK LABEL",
"name": "{\"German\"=>\"s.Oliver BLACK LABEL - Damen, Weiß, Größe 44\"}"
}
Any ideas how to do it ?
From @d-Pixie on August 10, 2016 8:14
Using this setup:
require 'dry-types'
module Types
include Dry::Types.module
CountryCode = Types::Strict::String.constrained(format: /\A[A-Z]{2}\z/)
end
class Model < Dry::Struct
attribute :country_code, Types::CountryCode
end
When you use the type on its own you get an appropriate error message in the exception:
p Types::CountryCode['d2sg']
# => Exception: Dry::Types::ConstraintError: "d2sg" violates constraints (format?(/\A[A-Z]{2}\z/) failed)
But when used as an attribute in a struct it gives a misleading message about the values type instead of its format:
model = Model.new( country_code: 'd2sg' )
# => Exception: Dry::Types::StructError: [Model.new] "d2sg" (String) has invalid type for :country_code
I would hope that the context you use a type in should not change its error message, at least not to one that is wrong :)
Copied from original issue: dry-rb/dry-types#120
This isn't affecting functionality, but it affects the readability of my tests. I'm sending a PR shortly that corrects this.
Thanks for this gem - it really helps a project I'm working on.
Hi,
Sorry I would have asked this on gitter chat but it just wasn't workign for me.
I'd like to use the nested attributes that are available on master but a yet in a tagged release. Is there a release scheduled?
There are situations, where domain entity should implement an invariant, that can't be expressed with DRY types (or overhead of such expressing is too high).
For instance, when my contact person might have an email and a phone, but at least one attribute should be set. Or, when I have two entities, and I need a rule for first entity based on second.
Current way is to override constructor, such as
def initialize(params)
error_message = 'at least :email or :phone should be in Hash input'
raise Dry::Struct::Error, error_message unless params[:email] || params[:phone]
super(params)
end
It has downfalls: it calls so early, and any bad input may cause a crash within this code. Also, it's not pretty way. I suspect, there is not always a hash as argument, a different dry-struct might be passed as well.
Another possible solution is to override #input
, but that's hard and fragile as well.
The best solution is to have some ability to set callback after dry-struct is initialized and all internal checks are passed, but before illegal object is initialized and returned to a callee. Something like
class ContactPerson < Dry::Struct
validate -> (instance) { instance.name || instance.email }
end
By default, it's validate -> (_) { true }
From @lal-s on August 18, 2017 15:18
I want to add meta data to my attributes. For example, I have the following classes:
class User < Dry::Struct
attribute :name, Types::String.meta(my_meta: 'meta'
attribute :address, Address
attribute :accounts, Types::Array.member(Account).meta(resource: 'my resource')
end
class Address < Dry::Struct
attribute :street, Types::String
attribute :city, Types::String
end
class Account < Dry::Struct
attribute :account_num, Types::Int
end
I want to be able to add meta data to attribute address
in User
, is there a way to do it?
i.e
attribute :address, Address.meta(my_meta: 'meta')
Probably if I can define it differently eg: attribute :address, Types::Address.meta(my_meta: 'meta')
just so that it lets me use meta on it.
Copied from original issue: dry-rb/dry-types#209
Calling to_h
on a Dry::Types::Map
wont call to_h
on the values of the Map.
module Types
include Dry::Types.module
class LinkObject < Dry::Struct
transform_types { |type| type.meta(omittable: true) }
attribute :href, Types::String
attribute :meta, Types::Hash
end
Link = String | Constructor(LinkObject)
Links = Map(key_type: Symbol, value_type: Link)
end
Types::Links.call({ self: "", reltive: { href: "", meta: { count: 1 } } }).to_h
=> {:self=>"", :reltive=>#<Types::LinkObject href="" meta={:count=>1}>}
Using master branch for dry-types
and dry-struct
The following example is raise stack level too deep (SystemStackError):
require 'dry-struct'
module Types
include Dry::Types.module
end
class Bar < Dry::Struct
attribute :hash, Types::Strict::String
end
bar = Bar.new(hash: 'test')
bar.hash
Stacktrace:
/Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `lambda': stack level too deep (SystemStackError)
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `to_proc'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `map'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `map'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `map'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
... 5943 levels...
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `map'
from /Volumes/Data/mario/.rvm/gems/ruby-2.3.4/gems/dry-equalizer-0.2.0/lib/dry/equalizer.rb:78:in `block in define_hash_method'
from tmp/test.rb:12:in `<main>'
I originally added this as a comment on #66, but I'm also opening an issue so its seen.
Turns out, I was relying on the #public_send
behavior of #[]
for a project of mine. I'm using Dry::Struct to write a presenter library, and I was using it with Rails to generate urls. I had a superclass like this:
class Presenter < Dry::Struct
transform_types { |t| t.meta(omittable: true) }
attribute :url, Types::Url
def url
url_for(self)
end
end
Then, subclasses could either override that method themselves, or when one presenter is calling another it could be passed in if needed, in case of nested routes.
class WidgetCollection < Presenter
attribute :user_id, Types::Integer
attribute :widgets, Types::Array
def url
user_widgets_path(user_id)
end
def widgets
@widgets.map { |widget| WidgetPresenter.new(widget: widget, url: widget_path(user_id: user_id, widget) }
end
end
Because it now uses #fetch
instead of #public_send
, I can no longer access "attributes" that are overridden by explicit methods. I also cannot use Types::URL.default { }
because that block is eval'd in the class context rather than the instance (undefined method 'url_for' for Presenter:Class
).
What I ended up doing is just redefining the #[]
method to use #public_send
in my Presenter superclass, so its not that big a deal.
Like in virtus I comes quit handy when there is a method on the value type to create a new value type based on existing one:
peter = Person.new(first_name: "Peter", last_name: "Hanson")
paul = peter.with(first_name: "Paul")
maybe this is just an alias? like alias_method :with, :new
Here's a class:
class ConstrainedUser < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Strict::Int.constrained(gteq: 18)
end
Here's the violated constraint error message:
[1] pry(main)> user = ConstrainedUser.new(name: 'Sally', age: 17)
Dry::Struct::Error: [ConstrainedUser.new] 17 (Integer) has invalid type for :age
from /Users/mvasin/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/dry-struct-0.3.0/lib/dry/struct/class_interface.rb:109:in `rescue in new'
The 'invalid type' error message looks weird, as 17 is a perfectly valid Integer.
At the same time, dry-types
part looks fine:
[2] pry(main)> Types::Strict::Int.constrained(gteq: 18)[17]
Dry::Types::ConstraintError: 17 violates constraints (gteq?(18, 17) failed)
from /Users/mvasin/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/dry-types-0.10.3/lib/dry/types/constrained.rb:28:in `block in call'
I understand that a violated constraint type value is an invalid type value, so overall the error message is not wrong, but should be more specific.
It would be good if structs supported constructor
method in the same way as dry-types, currently constructor
returns a hash schema type, which may be surprising, especially that structs are supposed to quack like dry-types.
/cc @flash-gordon @backus
Not sure if this should be a dry-types
or dry-struct
issue, but sum types do not seem to enforce the types are actually correct.
require 'dry/struct'
class Foo < Dry::Struct; end
class Bar < Dry::Struct; end
sum_type = Foo | Bar
sum_type[Foo.new] # => #<Foo>
# ok
sum_type[Bar.new] # => #<Foo>
# wait, a Bar is not a Foo
sum_type[nil] # => #<Foo>
# NilClass is certainly not a Foo
sum_type[:thing] rescue $! # => #<TypeError: can't convert Symbol into Hash>
# i guess everything is coerced to a hash by default? a bit of a cryptic error message.
sum_type[hash: :thing] rescue $! # => #<Foo>
# { hash: : thing } is definitely not a Foo either
sum_type[1] rescue $! # => #<TypeError: can't convert Fixnum into Hash>
# slightly confusing, but similar to :thing
but, if I manually constrain the types, they work as expected:
sum_type = Foo.constrained(type: Foo) | Bar.constrained(type: Bar)
sum_type[Foo.new] # => #<Foo>
sum_type[Bar.new] # => #<Bar>
sum_type[nil] rescue $! # => #<Dry::Types::ConstraintError: nil violates constraints (type?(Bar, nil) failed)>
sum_type[:thing] rescue $! # => #<Dry::Types::ConstraintError: :thing violates constraints (type?(Bar, :thing) failed)>
sum_type[hash: :thing] rescue $! # => #<Dry::Types::ConstraintError: {:hash=>:thing} violates constraints (type?(Bar, {:hash=>:thing}) failed)>
sum_type[1] rescue $! # => #<Dry::Types::ConstraintError: 1 violates constraints (type?(Bar, 1) failed)>
Is this the expected behavior? If so, I think it's fairly surprising. I was hoping the first snippet of code would behave like the second after the 0.1.1
release since the release notes mention:
Make Dry::Struct act as a constrained type. This fixes the behavior of sum types containing structs.
When using constructor type :symbolized
, I don't get any error messages for invalid attributes. A simple example:
module Types
include Dry::Types.module
end
class User < Dry::Struct
constructor_type(:symbolized)
attribute :age, Types::Int.constrained(gt: 18)
end
User.new(age: 17)
=> #<User age=17>
I'm expecting some kind of error message when I set age
to 17
in above example. With constructor_type(:schema)
, it works:
class User < Dry::Struct
constructor_type(:schema)
attribute :age, Types::Int.constrained(gt: 18)
end
User.new(age: 17)
Dry::Struct::Error: [User.new] 17 (Fixnum) has invalid type for :age
Is this expected behavior or is this a bug? I'm not quite sure how the different constructor types work. I can't find any documentation other than the discussion in dry-rb/dry-types#72, which I guess might be outdated now when dry-struct
is a separate gem.
Why would we ever define Dry::Struct#to_hash
method? Its purposed for implicit coercions like in the following example. We should define #to_h
only, I guess.
Example:
[1] pry(main)> HueBridge.new.lights.last
=> #<HueBridge::Light id=15 name="Sofa" on=true brightness=144 hue=7676 saturation=199>
[2] pry(main)> a = HueBridge.new.lights.last
=> #<HueBridge::Light id=15 name="Sofa" on=true brightness=144 hue=7676 saturation=199>
[3] pry(main)> def b(*c, **d)
[3] pry(main)* puts "args"
[3] pry(main)* p c
[3] pry(main)* puts "kwargs"
[3] pry(main)* puts d
[3] pry(main)* end
=> :b
[4] pry(main)> b(1, a)
args
[1]
kwargs
{:id=>15, :name=>"Sofa", :on=>true, :brightness=>144, :hue=>7676, :saturation=>199}
=> nil
[5] pry(main)> b(a, 1)
args
[#<HueBridge::Light id=15 name="Sofa" on=true brightness=144 hue=7676 saturation=199>, 1]
kwargs
{}
=> nil
From @backus on June 20, 2016 23:3
class StrictUser < Dry::Types::Struct
constructor_type(:strict)
attribute :name, 'strict.string'
attribute :gender, Dry::Types['strict.string'].default('male')
end
StrictUser.new # => #<NoMethodError: undefined method `default?' for "strict.string":String>
works if you replace strict.string
with Dry::Types['strict.string']
Copied from original issue: dry-rb/dry-types#100
In DDD it may be needed to override attribute in a sub-struct.
For example, I have a Customer
with optional #address
, but in Order
I need Customer
with mandatory #address
.
I have an option to define class CustomerWithAddress < Customer
with overwritten #address
attribute, but with current implementation it's impossible to do it.
Also, we can think about better specific DSL for such things, like Customer.constrained(required_attribute: :address)
.
In this case, we need to have ability to revert .optional
from Dry::Type.
Tell me your idea/opinion, and I can try to make PR.
From @Kukunin on August 28, 2016 7:56
It's a little bit unobvious, that Types1 = Dry::Types.module
and Types2 = Dry::Types.module
shares the same container for type registering. I don't know, if it's bad or no, but I have some side-effect of this.
I build application, where I extract logic to a separate gems. And those gems use their own Dry::Types.module
instance.
With that said, in my main umbrella application I get this warning on every application load:
/.../2.3.0/gems/dry-types-0.8.1/lib/dry/types.rb:88: warning: already initialized constant Shift::Domain::Customer::PersonName
/.../2.3.0/bundler/gems/.-ab82fa16b459/shift-domain/lib/shift/domain/customer.rb:9: warning: previous definition of PersonName was here
/.../2.3.0/gems/dry-types-0.8.1/lib/dry/types.rb:88: warning: already initialized constant Shift::Domain::ContactPerson::EmailContactMethod
/.../2.3.0/bundler/gems/.-ab82fa16b459/shift-domain/lib/shift/domain/contact_person.rb:4: warning: previous definition of EmailContactMethod was here
/.../2.3.0/gems/dry-types-0.8.1/lib/dry/types.rb:88: warning: already initialized constant Shift::Domain::ContactPerson::PhoneContactMethod
/.../2.3.0/bundler/gems/.-ab82fa16b459/shift-domain/lib/shift/domain/contact_person.rb:9: warning: previous definition of PhoneContactMethod was here
And every new Dry::Types.module
gives the next pack of these errors
BTW, I have a way more such structs, I don't know, why it gives warnings only about these ones.
Definition is like this:
class ContactPerson < Dry::Types::Struct
class EmailContactMethod < Dry::Types::Struct
attribute :type, Types::Symbol.constrained(included_in: [:email])
attribute :value, Types::Strict::Email
end
class PhoneContactMethod < Dry::Types::Struct
attribute :type, Types::Symbol.constrained(included_in: [:phone])
attribute :value, Types::Strict::UsaPhone
end
ContactMethod = EmailContactMethod | PhoneContactMethod
ContactMethods = Types::Strict::Array.member(ContactMethod).constrained(min_size: 1)
attribute :name, Types::Strict::FilledString
attribute :contact_methods, ContactMethods
end
Copied from original issue: dry-rb/dry-types#136
class Component::Helpers::Link_to < Dry::Struct
attribute :text, Types::Hash
attribute :path, Types::Strict::String.default('#')
attribute :klass, Types::String.default('button')
end
When I do Component::Helpers::Link_to.new(text: {en: 'dsfsd'}, path: 'asdas')
Dry::Struct::Error: [Component::Helpers::Link_to.new] :klass is missing in Hash input
Same thing happens with optional.
I'm not sure if I'm using transform_types
correctly.
I want to do some type transformations on an array on initalization and later use #new
to create a copy of the struct, but I couldn't get this to work. Am I doing it wrong? It seems to work as expected without transform_type
.
Please let me know if you need more information or if my question is not clear to you. Please find the code and the output below.
Cheers, Simon
# running ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]
require 'bundler/inline'
gemfile do
source "https://rubygems.org"
gem "dry-struct", "0.5.0"
end
module Types
include Dry::Types.module
end
class Tag < Dry::Struct
transform_keys(&:to_sym)
attribute :name, Types::Strict::String
end
class EntityWorking < Dry::Struct
transform_keys(&:to_sym)
attribute :name, Types::Strict::String
attribute :tags, Types::Strict::Array.of(Tag)
end
class EntityFailing < Dry::Struct
transform_keys(&:to_sym)
transform_types do |type, name|
case name
when :tags then type.constructor do |tags|
tags.map { |t| Tag.new(name: t) }
end
else
type
end
end
attribute :name, Types::Strict::String
attribute :tags, Types::Strict::Array.of(Tag)
end
p x = EntityWorking.new("name" => "Franz", tags: [ {name: "red"}])
p x.new(name: "asdf")
p x = EntityFailing.new("name" => "Franz", tags: %w(red))
p x.new(name: "asdf")
#<EntityWorking name="Franz" tags=[#<Tag name="red">]>
#<EntityWorking name="asdf" tags=[#<Tag name="red">]>
#<EntityFailing name="Franz" tags=[#<Tag name="red">]>
/home/simon/.rvm/gems/ruby-2.4.0@guidebook-rails/gems/dry-struct-0.5.0/lib/dry/struct/class_interface.rb:208:in `rescue in new': [EntityFailing.new] [#<Tag name="red">] (Array) has invalid type for :tags violates constraints ([Tag.new] #<Tag name="red"> (Tag) has invalid type for :name violates constraints (type?(String, #<Tag name="red">) failed) failed) (Dry::Struct::Error)
from /home/simon/.rvm/gems/ruby-2.4.0@guidebook-rails/gems/dry-struct-0.5.0/lib/dry/struct/class_interface.rb:202:in `new'
from /home/simon/.rvm/gems/ruby-2.4.0@guidebook-rails/gems/dry-struct-0.5.0/lib/dry/struct/class_interface.rb:218:in `call'
from /home/simon/.rvm/gems/ruby-2.4.0@guidebook-rails/gems/dry-struct-0.5.0/lib/dry/struct.rb:174:in `new'
from dry_problem.rb:46:in `<main>'
shell returned 1
Suppose we have a simple struct and we want to receive the list of attribute names:
class User < Dry::Struct
attribute :name, Dry::Types::Any
attribute :age, Dry::Types::Any
end
User.attribute_names # => [:name, :age]
If you continue to work with this structure after .attribute_names call, the following call of .attribute_names will return the cached value
# Reopen and define new attributes (after .attribute_names call):
class User
attribute :email, Dry::Types::Any
attribute :id, Dry::Types::Any
end
# Current (incorrect):
User.attribute_names # => [:name, :age]
# Expected:
# User.attribute_names # => [:name, :age, :email, :id]
The problem is here:
https://github.com/dry-rb/dry-struct/blob/master/lib/dry/struct/class_interface.rb#L206
module Dry::Struct::ClassInterface
...
# Gets the list of attribute names
#
# @return [Array<Symbol>]
def attribute_names
@attribute_names ||= schema.keys # here
end
...
end
Is it really need to cache schema's keys (object with dynamic behaviour)?
Consider this example:
2.4.0 :021 > class Item < Dry::Struct
2.4.0 :022?> attribute :price, Types::Coercible::Int
2.4.0 :023?> end
=> Item
2.4.0 :024 > i = Item.new
TypeError: can't convert nil into Integer
I would expect an error along these lines instead:
Dry::Struct::Error: [Item.new] :price is missing in Hash input
Is there a way to run the missing check first, before attempting to coerce?
Hi, when I launch my worker, an error append with Dry-struct :
Dry::Struct::RepeatedAttributeError: Attribute :id has already been defined
But nobody can reproduce this bug.
I'm browsing the source, seems like some introspective methods would be useful for testing. In defining a Dry::Struct, and wanting to write unit tests that I have defined them as expected,
I'm writing the following:
class BaseStruct < Dry::Struct
def self.includes_attribute?(attr)
attr_names.include?(attr.to_sym)
end
def self.attr_names
# schema part of Dry::Struct
@attr_names ||= schema.keys
end
...
seems like methods like these would be useful within Dry::Struct itself?
Can we have omitable nested attributes, I couldn't quite figure it out.
I use dry-struct
to build my domain entities and value objects.
Some entities has a lot of optional fields, that I don't want to specify.
I can use constructor_type :schema
, but it allows to 'forget' required key.
I can use strit_with_defaults
and .default(nil)
to all my optional fields, but Types::Strict::String.optional.default(nil)
is too much as for me.
Moreover, strict_with_defaults
doesn't accept nil
value for attributes with default value.
I use default attributes defined in Entity because: a) they are often business rules b) it gives encapsulation. Application still may send nil
value for that attribute, the most obvious example is FactoryGirl
who builds the object. And it's still desirable for Entity to set default value.
Currently, no constructor type fits this needs.
P.S. Does it make sense to consider .optional
attribute as one which has .default(nil)
?
Hi DRY team
dry-struct 0.5.0
In your recipes for dry-struct under the heading "Resolving default values on nil", you state:
nil as a value isn’t replaced with a default value for default types.
I have found that's not the case. Here's an RSpec test:
require 'dry-types'
require 'dry-struct'
RSpec.describe Dry::Struct do
module Types
include Dry::Types.module
end
class User < Dry::Struct
attribute :name, Types::String
attribute :age, Types::Integer.default(18)
end
context 'when the key is missing' do
let(:user) { User.new(name: 'Jane') }
it 'sets the default' do
expect(user.age).to eq 18
end
end
context 'when the key is present but the value is nil' do
let(:user) { User.new(name: 'Jane', age: nil) }
it 'returns nil' do
expect(user.age).to be_nil
end
end
end
The first example passes, but the second fails with:
1) Dry::Struct when the key is present but the value is nil returns nil
Failure/Error: expect(user.age).to be_nil
expected: nil
got: 18
Thanks for looking into this.
Context: rom-rb/rom-sql#115 (comment)
Objects with an array attribute sometimes share the same elements of this array. If the objects have other attributes like id
, then the elements are shared. If not, the elements aren't shared.
Use the following example to reproduce the behaviour.
# types.rb
require "dry-types"
module Foo
module Types
include Dry::Types.module
end
end
# test.rb
require "dry-struct"
require "foo/types"
module Foo
class Test < Dry::Struct
constructor_type :strict_with_defaults
# Works if, :id attribute isn't present
# attribute :id, Types::Coercible::Int
# Fails if, :id attribute is present
attribute :id, Types::Coercible::Int
attribute :linked, Types::Strict::Array.member(Test).default([])
end
end
# test_spec.rb
require "spec_helper"
require "foo/test"
module Foo
describe Test do
describe "initialization" do
let(:a) { described_class.new(id: 4) }
let(:b) { described_class.new(id: 5) }
# works
# let(:a) { described_class.new() }
# let(:b) { described_class.new() }
it "works only without id attribute" do
expect(a.linked).to eq []
expect(b.linked).to eq []
a.linked.push(b)
puts "schema: #{a.class.schema}"
expect(a.linked).to eq [b]
expect(b.linked).to eq []
end
end
end
end
The value
field in the a.class.schema
changes between the execution of the two cases.
Maybe this issue is related to #2.
I'm not sure what the expected behaviour is. Is it allowed to change an array (add/remove elements) after initialization?
require 'dry/struct'
class Foo < Dry::Struct
constructor_type :schema
Types = Dry::Types.module
attribute :bar, Types::Strict::Bool
end
p Foo.new({}).bar
# => nil
I expect this code to fail with missing key as I have provided no default for #bar
and nil
is definitely not Types::Strict::Bool
.
Structs in dry-types
for now do not support nesting like this:
class User < Dry::Types::Struct
attribute :name, Types::Strict::String
attribute :profile do
attribute :email, Types::Coercible::Email # defined somewhere else
attribute :phone, Types::Coercible::Phone # ^^
attribute :preferences do
attribute :food, Types::Array
end
end
end
User.new name: 'Joe',
profile: {
email: '[email protected]',
phone: '1(234)567-8900',
preferences: {
food: ['ice-cream']
}
}
I know, that I could define types for preferences and profile explicitly (and I do), but in many cases it doesn't have much sense, because they are parts of only one aggregate.
dry-struct behaves inconsistently when there is an attribute with both default
and constructor
. Using constructor_type :schema
, if you don't specify value for the attribute, the default is used and is run through the constructor, but if you specify nil
as the attribute value, the default is still used, but is NOT run through the constructor. I think it may be open to discussion whether the defaults should be run through the constructor (for me it makes sense to do it), but I don't think it's right to run them through in one case and not in the other.
I've created a spec for this behavior here.
So I tried to (and mostly succeeded!) at using the ipaddress
gem to encode IP addresses and CIDR blocks into some of my structs. All good. However, IPAddress::IPv4
implements map
to provide every IP address in its CIDR block. This makes sense in context given their requirements--and causes invoking to_h
on my structs to fail with a SystemStackError
.
Maybe the Dry::Struct::Hashify
module should allow us to override the rendering out of a given class? I have this as a monkey-patch in my code right now:
module Dry
class Struct
module Hashify
def self.[](value)
if value.respond_to?(:hashify)
value.hashify
elsif value.respond_to?(:to_hash)
value.to_hash
elsif value.respond_to?(:map)
value.map { |item| self[item] }
else
value
end
end
end
end
end
This allows me to implement hashify
on my IPAddress::IPv4
class and dump out a string (which, since I've registered this as a coercible class that uses IPAddress.parse
, is now in/out capable!). This may not be the best way to do it, so I wanted to drop this here and see if we could come up with a good way forward.
We've found a bug which lets you define an attribute in a superclass after defining a descendant class, which results in attr reader defined, but corresponding attribute is missing in descendant schema.
Here's what I'm talking about:
require 'dry-struct'
class A < Dry::Struct
class B < A
end
attribute :foo, 'string'
end
puts A::B.schema[:foo].inspect
# nil
puts A::B.instance_methods.include?(:foo)
# true
Currently this is waiting on dry-rb/dry-configurable#7 being resolved. Once I can inline the version I can also drop the version_spec
I added in #3.
class Foo < Dry::Struct::Value
attribute :bar, Dry::Types['strict.string']
end
results in
TypeError: no implicit conversion of nil into Hash
from /Users/gollahon/.rvm/gems/ruby-2.4.0/gems/dry-struct-0.2.0/lib/dry/struct/class_interface.rb:70:in `merge'
Seems like here @schema
happens to be nil
.
Incidentally,
class Baz < Dry::Struct
attribute :qux, Dry::Types['strict.string']
end
works fine.
dry-struct v3.1
I ran into an issue where creating a Type with a custom constructor would result in:
#<NoMethodError: undefined method 'default?' for #<Dry::Struct::Constructor type=Helios::SlotTemplateKey>>
when trying create instances of my other dry-struct entities that depend on the type with the custom constructor.
I believe a valid solution to this case would be for the new Dry::Struct::Constructor instance to delegate any incoming calls to the original struct type. Something along the lines of the solution below.
class Dry::Struct::Constructor
def method_missing(method, *args)
if type.respond_to?(method)
self.class.send(:define_method, method) do |*args|
type.send(method, *args)
end
send(method, *args)
else
super
end
end
def respond_to_missing?(method, private=false)
type.respond_to?(method, private) || super
end
end
Example:
class Article
end
class ArticleForm < ::Dry::Struct
module Types
include ::Dry::Types.module
end
attribute :article do
attribute :title, Types::Coercible::String.meta(omittable: true)
end
end
ArticleForm.const_defined?("Article") #=> true
Object.const_defined?("Article") #=> true
ArticleForm.const_defined?("ArticleForm::Article") #=> false
Object.const_defined?("ArticleForm::Article") #=> false
defined?(Article) #=> "constant"
defined?(ArticleForm::Article) #=> nil
We should only care about the existence of ArticleForm::Article
but not ::Article
However Object.const_defined?
is used in
https://github.com/dry-rb/dry-struct/blob/v0.5.0/lib/dry/struct/struct_builder.rb#L63
According to description for const_defined?
It also finds constant in ancestors (mainly Object
), which makes the search also catches ::Article
unexpectedly
So I guess we should be using full namespace like ArticleForm::Article
# Continue from last example
ArticleForm.const_set(:"Article", Class.new(Object))
Article == ArticleForm::Article #=> false
ArticleForm.const_defined?("Article") #=> true
Object.const_defined?("Article") #=> true
ArticleForm.const_defined?("ArticleForm::Article") #=> true
Object.const_defined?("ArticleForm::Article") #=> true
defined?(Article) #=> "constant"
defined?(ArticleForm::Article) #=> "constant"
I would like the ability to create computed attributes on my struct that call a method on self
instead of taking a value in the initializer.
class Person < Dry::Struct
attribute :first_name, Types::Strict::String
attribute :last_name, Types::Strict::String
attribute :full_name, Types::Strict::String
def full_name
"#{first_name} #{last_name}"
end
end
john = Person.new(first_name: "John", last_name: "Doe")
john.to_hash # => { first_name: "John", last_name: "Doe", full_name: "John Doe" }
It doesn't seem like there is (currently) a way to do this. I tried just making the attribute ommittable: true
, since it seemed like
dry-struct/lib/dry/struct/class_interface.rb
Line 138 in d34ec0e
Struct#[]
and Struct#to_hash
use the @attributes
Hash directly, the getter method isn't called.
My initial thought is to create a PR that changes
Line 123 in d34ec0e
@attributes.fetch(name) { send(name) if respond_to?(name) }
but I wanted to check in with the maintainers before attempting this.
Thanks!
I see you've said that error messages will be improved, but I just wanted to note one that I've found particularly unhelpful:
require 'dry-struct'
class Types
include Dry::Types.module
end
class Person < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Strict::Int
attribute :address, Types::String
end
Because at least one attribute is strict, attempting to create a Person without supplying any attributes gives an exception message that's not very helpful:
Person.new
# Dry::Types::ConstraintError: nil violates constraints (type?(String, nil) failed)
If you supply any valid/invalid attribute or a hash however, the message is much more helpful:
Person.new age: 5
Person.new derp: 5
Person.new({})
# Dry::Struct::Error: [Person.new] :name is missing in Hash input
When called with no arguments, a message like that indicating one or all missing attributes (maybe only required ones?) would be much better.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.