Git Product home page Git Product logo

dry-struct's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dry-struct's Issues

Crash during initialization when using `.constrained` with `.constructor`

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>'

Surprising behaviour of subclassed structs when used as type in another struct

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

No error raises when invalid data is use

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

equality of structs

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 

[feature] Ability to stir/treat an attribute value before coercion and assignment

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

Anonymous struct doesn't work because of dry-container

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

Proposal: Dry::Struct.map

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.

Custom types

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.

How to return an object in json to be a hash and not a string hash

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 ?

Error message for constrained, using format, string in struct is misleading

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

New version

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?

A way to define high-level constraints

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 }

Use meta on self defined objects

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 Dry::Types::Map does not work with map values

Description

Calling to_h on a Dry::Types::Map wont call to_h on the values of the Map.

Example:

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}>}

Notes

Using master branch for dry-types and dry-struct

Got stack level too deep (SystemStackError) when use attribute name hash

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>'

`#[]` vs public_send "regression"

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.

Proposal: Add `.with` method to ValueObjects

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

Violated constraint error message looks irrelevant

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.

Sum types not working as expected

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.

Missing exception for invalid attribute with symbolized constructor type

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.

Struct gets coerced into **kwargs (when *args is also present)

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

Undefined method `default?` for String

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

Allow attributes override, or at least constraint

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.

Different Dry::Types.module instances share the same container

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

default and optional types are not working

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.

#new not working with transform_types

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

Incorrect behaviour of .attribute_names class method

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)?

Inappropriate exception when coercible int not present

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?

Attribute :id has already been defined

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.

Introspective Functions / Testing?

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?

Need a constructor type to omit optional attributes and support `nil` for defaults

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_defaultsdoesn'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)?

Resolving default values on 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.

Array elements are shared between instances

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?

Schema constructor_type gives you implicit nil defaults

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.

Nested attributes of the structure

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.

inconsistent behavior when combining default and constructor

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.

Custom hashify logic

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.

Confusing behavior with inheritance and attr readers

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

Dry::Struct::Value no longer works in 0.2.0

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.

Constructor should delegate to original struct

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

Unable to declare attribute with name matching constant name outside struct class

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"

Adding computed/dynamic attributes

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

next if instance_methods.include?(key)
would allow me to define my own getter, but since things like 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

@attributes.fetch(name)
to be:

@attributes.fetch(name) { send(name) if respond_to?(name) }

but I wanted to check in with the maintainers before attempting this.

Thanks!

Poor exception message on struct instantiation when at least one attribute is required but none are provided

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.