Code Monkey home page Code Monkey logo

dry-types's People

Contributors

abinoam avatar actions-user avatar alsemyonov avatar amhol avatar backus avatar bkuhlmann avatar bolshakov avatar cgeorgii avatar cllns avatar coop avatar d-pixie avatar dry-bot avatar emptyflask avatar esparta avatar flash-gordon avatar fran-worley avatar gustavocaso avatar jeremyf avatar joevandyk avatar kirs avatar marshall-lee avatar olleolleolle avatar robhanlon22 avatar roryokane avatar skryukov avatar solnic avatar splattael avatar timriley avatar timstott avatar waiting-for-dev avatar

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

dry-types's Issues

(Types::Form::Date | Types::Strict::Nil) not working for nil?

Using gem version 0.7.1.

I might be misunderstanding |, but I expected it to work as a fallback if the left coercion fails.

Code

(Types::Form::Date | Types::Strict::Nil)[nil]

Result I get

Dry::Types::ConstraintError: nil violates constraints (#<struct Dry::Types::Result::Failure input=nil, error="no implicit conversion of nil into String">)
    from dry-types-0.7.1/lib/dry/types/sum.rb:31:in `block in call'
    from dry-types-0.7.1/lib/dry/types/sum.rb:44:in `try'
    from dry-types-0.7.1/lib/dry/types/sum.rb:30:in `call'

Result I expected

nil

(Types::Form::Decimal | Types::Strict::Nil)[nil] works btw, so it seems to be specific to the Form::Date type.

Shouldn't Kleisli::Maybe::None be understood as nil in some cases?

If I create a Struct type and then take the resulting object and convert it to hash again and pass it to the exact same Struct, shouldn't it be valid? Maybe it's very obvious why they shouldn't, but given my very limited experience with very strict functional code it made sense to open the issue.

Consider this:

class MyStruct < Dry::Data::Struct
  attribute :a,  Types::String
  attribute :b,  Types::String.optional
end

x = MyStruct.new a: 2, b: nil 
#=> {
#  :a => 2,
#  :b => None
#}

y = MyStruct.new x.to_h
#=> {
#  :a => 2,
#  :b => Some(None)  <----- Shouldn't this simply be None?
#}

I would expect the resulting hash for y to be equivalent to x because the meaning on None should mean the same as nil in this case. I understand how they are different because nil is not a "value" but a "wrapper" of no value. However shouldn't the coercion of no value be the same for nil?

This project and related ones like ROM have moved my code to a new level. I appreciate the effort. It's still quite different for me and I'm still getting used to it, so I'd appreciate any assistance in understanding wether this is a bug or not. ;)

Grandchildren of Dry::Types::Struct don't enforce types

Example

require 'dry/types'

class Parent < Dry::Types::Struct
  attribute :foo, 'strict.string'
end

class Child < Parent
end

Parent.new(foo: 'bar') # => #<Parent foo="bar">
Parent.new(foo: 1)     # !> [Parent.new] 1 (Fixnum) has invalid type for :foo (Dry::Types::StructError)
Parent.new             # !> [Parent.new] :foo is missing in Hash input (Dry::Types::StructError)

Child.new(foo: 'bar')  # => #<Child>
Child.new(foo: 1).foo  # => 1
Child.new              # => #<Child>
Child.new.foo          # => nil

Add form coercion for array types when they are blank

This relates to dry-rb/dry-validation#102, in which I outlined that there's no simple way to include empty arrays in HTML form submissions. We've been exploring thus further with our formalist-generated forms, and have come to this arrangement:

When a form is expected to submit an array as one of its fields, but that array is empty, then instead replace it with something like this:

<input type="hidden" name="category_ids">

This means that we now get params like this being included in our form submission for an array-type field with nothing included:

{"category_ids" => ""}

Now, if we had a schema like this:

schema = Dry::Validation.Form do
  key(:category_ids).each(:int?)
end

When we run it with that input, right now we get an error, because there's no specific form coercion rules for arrays:

schema.("category_ids" => "").messages
# => {:category_ids=>["must be an array"]}

Since the empty string is an HTML form's standard "empty value", I'd like to propose that we add a form coercion for this when we expect the field to be an array. In this case, I'd propose that we actually coerce it to nil, like we do for other HTML type coercions here, like int:

def self.to_int(input)
  return if empty_str?(input)

  # ... rest of coercion logic ...
end

By coercing to nil, then we can have the flexibility to use a type in our validation schema with a default value (like an empty array), which is what you suggested in dry-rb/dry-validation#102, @solnic.

Would you be happy to have this coercion added to dry-types? I'll go ahead and build it if so.

Thanks!

struct to struct feature?

One of the problems that I have collided with over the last 10 years of ruby-goodness is incompatible definitions of a point (e.g. location) in the world. Strictly speaking a location is a latitude, longitude, altitude, and a datum such as WGS84. This "type" is sometimes implemented as a class with attributes that intersect the four mentioned with various abbreviations such as lat, lng, long, alt etc.

Sometimes the "type" is implemented as a hash with again the same parade of abbreviations and full attribute names as the keys - sometimes as strings with various cases or symbols also with various cases.

Many times I have had to do one-off conversion methods to convert from one location type to another.

I sense the possibility with Dry::Data::Struct of constructing a family of types that are related - I guess thats why its called a family :)

class Location < Dry::Data::Struct
  attribute :latitude,  "maybe.coercible.float"
  attribute :longitude, "maybe.coercible.float"
  attribute :altitude,  "maybe.coercible.float"
  attrobite :datum,     "coercible.string"
end

class AnotherLocation < Location
  attribute :lat,  renames: :latitude
  attribute :long, renames: :longitude
  attribute :alt,  renames: :altitude
end

class YetAnotherLocation < Location
  attribute :lat,  renames: :latitude,  "coercible.string"
  attribute :lng,  renames: :longitude, "coercible.string"
end

class AndYetAnotherLocation < Location
  attribute :lat,  renames: :latitude,  "coercible.hms_location"
  attribute :lng,  renames: :longitude, "coercible.hms_location"
end

class HmsLocation < Dry::Data::Struct
  attribute :hours,   "maybe.coercible.float"
  attribute :minutes, "maybe.coercible.float"
  attribute :seconds, "maybe.coercible.float"
end

I think I may have talked myself out of the idea. The idea was that a super class can be the way in which two related sub-classes can be coerced;

"symbolized" Hash schema only accepts string keys input

While working on a more tolerant Dry::Data::Struct, I experimented with both Dry::Data::Type::Hash#symbolized and #schema. Turns out the symbolized schema will only apply string keys.

hash = Dry::Data::Type::Hash.symbolized(name: Types::String)

hash['name' => 'hello'] # => {:name => 'hello'}
hash[name: 'hello'] # => {}

I understand the documentation is pretty clear about the purpose of symbolized schemas:

Symbolized hash will turn string key names into symbols

but I was wondering if it might be nice to just #to_sym whatever key passed in (String, Symbol) to both the schema creation and type instantiation so something like that would work:

hash = Dry::Data::Type::Hash.symbolized(
  name: Types::String,
  'age' => Types::Integer
)

hash['name' => 'hello', age: 10] # => {:name => 'hello', :age => 10}

I'd be happy to work on the implementation if that's a desirable feature.

`constructor_type(:schema)` can circumvent type checks

I have some dry type classes that have defaults. I want to be able to omit keys and have them assume the default value so I've been using constructor_type(:schema).

I would expect that no matter what constructor type I use I would still end up with a valid object. When I use the constructor_type(:schema), however, I can initialize objects that don't fit the type constraints of the class.

class Foo < Dry::Types::Struct
  constructor_type(:schema)

  attribute :a, Dry::Types['strict.int']
  attribute :b, Dry::Types['strict.int'].default(17)
end

Foo.new(a: 1)   # => #<Foo a=1 b=17>
Foo.new(a: nil) # => Dry::Types::ConstraintError: nil violates constraints (type?(Integer) failed)
Foo.new(a: '1') # => Dry::Types::ConstraintError: "1" violates constraints (type?(Integer) failed)

All reasonable^ But then...

Foo.new # => #<Foo a=nil b=17>

Wait, what? a should not be able to be nil.

This also gets propagated through other dry types that rely on Foo.

class Bar < Dry::Types::Struct
  attribute :foo, Dry::Types['foo']
end

Bar.new(foo: Foo.new(a: 1)) # => #<Bar foo=#<Foo a=1 b=17>>

Good so far.

Bar.new(foo: nil) # => #<Bar foo=#<Foo thing=nil>>

Not so good...

Optional & default type and nil input

#39 was closed but it seems that we can still have a value that violates type check/coercion rule.

[4] pry(main)> Types::Maybe::Strict::Int.default(nil)[nil]
=> nil

I expected to receive an error on calling default(nil) or None as a result of the entire expression :)

On a large scale I want to have something like this:

class MyOptionalValue < Dry::Types::Value
  constructor_type :schema
  attribute :foo, Types::Maybe::Strict::Int.default(nil)
end

v = MyOptionalValue.new({})
v.foo # => None

I store an empty hash value in DB but I want more strictness for my domain models. I put keys to the database only if they have values.

Optional attributes in Dry::Data::Struct

Would it be possible to have optional attributes when using Dry::Data::Struct? When I try to instantiate an "incomplete" object, I get the strict hash SchemaKey error (well, the StructError since that's caught by the Struct class).

I might not be using this gem right, but some of my models are not "complete" sometimes and it doesn't matter (stuff is lazily loaded later when required.)

A more precise use-case: a MongoDB::Collection model has the attributes :ns (string) and :count (integer). However, sometimes we don't get the count right away due to the fact that it's an extra call to the database.

Would it be desirable to set nil on the attribute if that hash key is not present on instantiation? It would only work with the optional types, and so would be aligned with the basic premise of this gem.

Difficult to use in development with Rails

Hey there, long-time fan of Virtus here. Trying to switch over to Dry::Data.

We're using Rails and it appears I keep stumbling on There is already an item registered with the key errors. I believe that's due to how Rails reload classes and such upon file changes.

The only way I found to circumvent that was to use:

Dry::Data.remove_instance_variable :@container

... to reset the container. However, I had to do that manually in the process.

Any idea how to simplify that?

File type

It would be great to have the built-in File type in dry-data and dry-validation.
Since dry-validation is a solution for request parameters / form validation, File a kind of must have type for forms.

@solnic what's your opinion?

Dry::Types::Struct constructor might be an anti-pattern

Right now Structs are "strict" by default. They error if you omit a key but they don't error if you supply extra keys. Not exactly my definition of "strict" but close enough.

If I want to specify that a field has a default value though I still have to provide key => nil for that field if I want to use the strict type. If I want the default to be applied when the key is omitted then I have to specify constructor_type(:schema). Now I can omit all keys though! Oh boy.

Out of curiosity I played with the three options to see how they behave in different scenarios:

Description :strict :schema :symbolized
Omitting key for default value StructError Sets default value Sets default value
Including extra junk keys Value silently ignored Value silently ignored Value silently ignored
Providing nil for field with default value Sets default value Sets default value Sets default value
Providing nil for field without default StructError ConstraintError ConstraintError
Providing invalid type for coercible field StructError TypeError TypeError
Initializing with empty hash StructError values without defaults set to nil values without defaults set to nil
Providing valid input but it string keys StructError all values set to nil all values set properly

So should we add a few more constructor_types? I don't think so. A wise man once said

a swiss army knife wonโ€™t ever replace a set of proper tools dedicated for specific tasks. Coercion logic varies, it depends on the context, in a web application there are different coercion rules than in a context of loading data from, letโ€™s say, a relational databases. We canโ€™t just dump this logic into one bucket.

I think that is good advice for this situation. The keys in a hash are just as important and potentially complex in terms of coercion rules as the values.


I think a constructor for a Struct should be configurable in the following ways:

I should be able to specify...

  1. how to handle missing keys
  2. how to handle unexpected keys
  3. that a single key should be coerced from a string to a symbol (or vice versa)
  4. that all keys should be coerced from a string to a symbol

I should be able to specify these rules for part of a hash or a whole hash.

Thoughts? I have ideas for the implementation but this is already a long post

Why default to a no-op?

I'm confused by the example:

float["3.2"] # "3.2"

I don't see what float is doing for me, if it's not going to do anything with anything I pass to it. At the very least, I'd expect the default to be for float to do something, with the option of having it perform a no-op.

Register under custom keys (and alias)

I think it would be good to allow types to be registered under custom keys, this way we can register under symbol keys and reduce allocations. Would also be good to allow for aliases (i.e. :int => :integer, :bool => :boolean). This should also allow us to get rid of the const_missing definition and I think the interface will be nicer.

Also, perhaps we should move the registration out of Dry::Data::Struct and make it explicit (i.e. Dry::Data.register(:name, Type, aliases: [:alias1, :alias2]).

This should make the library much more flexible, the obvious detriment is that there will be a bit of setup involved. I think it would also be good to make it so that a user can get a container without any of the built in types, and just create Dry::Data container and register the built-in types.

conflict with active_support :try

The internal use of the method try in combination with method_missing runs into an error as soon as active_support with it's try core_ext is required.

require 'bundler/inline'
gemfile true do
  gem 'dry-types'
  gem 'activesupport', require: false
end

include Dry::Types.module

CustomType = Strict::String.enum("val1", "val2")
CustomStringArray = Strict::Array.member(CustomType)

p CustomStringArray.call(["val1"])

require "active_support/core_ext/object/try"

p CustomStringArray.call(["val1"])

I'm not sure the method_missing cascade is used similar in other scenarios.

Given how common active_support is even outside rails applications this seems like a issue that should be fixed. Without to much insight into the code renaming the try method to something unique or changing how the method_missing cascade is used here might be an option so solve this.

Replace kleisli with dry-monads

It looks like we have some issues with kleisli gem. First of all the author didn't release the new gem version that would include much welcome removal of obsolete blank_slate hack. When I tried to add dry-types to my existing rails project this first thing I saw was StackOverflowError. It if very unfriendly error tbh :) After some time of debugging I figured out that the problem was caused by other gem that uses blankslate (without reason, actually). I absolutely don't blame the author at all, it is OSS so he can do whatever he wants.

The second issue is that kleisli adds some pollution to the global environment. E.g. it adds #Maybe and #Right methods to object so the become a private members of almost any object in runtime. I think it is against our rules :) Also I Maybe/Either methods a lot and found a tricky problem with using them inside a namespaced blocks of dry-container because NamespaceDSL uses SimpleDelegator that inherits from BasicObject and does not delegate to private methods so I got really unexpected NoMethodError.

Third is that I think we do not need most of kleisli classes. I doubt we need more than Maybe + Either. And from the other side we can add some other stuff to a potential monad/fp dependency.

Finally I think the API of kleisli is quite good but not perfect. Twisting it a bit would be really nice.

I propose to create a minor dry-monad/dry-fp dependency so we can solve all that issues. I quickly made a couple of files (untested :)) that can become a new gem:
Maybe: https://gist.github.com/flash-gordon/a6e6c0956b802d387e2fe81d9b7530b7
Either: https://gist.github.com/flash-gordon/446d38a1c7295e1b9ffcbe1302741522

Any thoughts?

Error requiring 'dry-types': undefined method `visit_type?'

Hi Piotr,

Since the gem isn't yet available using gem install dry-types, I cloned the master branch of the git repo, built it locally, and installed from that. After doing a require 'dry-types', however, I get this error:

/Users/jg/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/dry-logic-0.1.4/lib/dry/logic/rule_compiler.rb:18:in `visit': undefined method `visit_type?' for #<Dry::Logic::RuleCompiler:0x007f91552edef0> (N
oMethodError)

Should I be using another method for installation?

Freeze attributes belonging to Dry::Types::Value objects?

Initializing a struct or value object with an existing instance just returns that object:

require 'dry/types'

class Name < Dry::Types::Value
  attribute :first, 'strict.string'
  attribute :last,  'strict.string'
end

name1 = Name.new(first: 'john', last: 'doe') # => #<Name first="john" last="doe">
name2 = Name.new(name1)                      # => #<Name first="john" last="doe">

name1.equal?(name2) # => true

name1.first.reverse!

name1.first # => "nhoj"
name2.first # => "nhoj"

I would say that dry-types should at least freeze each attribute's value. I realize that a Dry::Types::Value instance itself is frozen but its members should probably also be frozen.

Array of structs doesn't work

I thought this should work, but it doesn't...

require 'dry-data'

module Types
end

Dry::Data.configure do |config|
  config.namespace = Types
end

Dry::Data.finalize

class Book < Dry::Data::Value
  attribute :name,      Types::Maybe::Strict::String
end

class Shelf < Dry::Data::Value
  attribute :books,     Types::Strict::Array.member(Book)
end

book1 = Book.new(:name => 'A book name')
book2 = Book.new(:name => 'Another book')

puts Shelf.new(:books => [book1, book2]).inspect
Dry::Data::StructError: [Shelf.new] [#<Book name=Some("A book name")>, #<Book name=Some("Another book")>] (Array) has invalid type for :books
        from /usr/lib/ruby/gems/2.3.0/gems/dry-data-0.5.1/lib/dry/data/struct.rb:36:in `rescue in new'
        from /usr/lib/ruby/gems/2.3.0/gems/dry-data-0.5.1/lib/dry/data/struct.rb:34:in `new'

Enum for integer attributes

Hello,

I was a little bit confused with next scenario:

2.3.0 :001 > require 'dry-data'
 => true
2.3.0 :002 > billing_period = Dry::Data['int'].enum(1, 3, 6, 12)
 => #<Dry::Data::Enum:0x007fe1fc85de88 @type=#<Dry::Data::Constrained:0x007fe1fc85ded8 @type=#<Dry::Data::Type constructor=#<Method: Dry::Data::Type.constructor> options={:primitive=>Integer}>, @options={:rule=>#<Dry::Logic::Rule::Value name=Integer predicate=#<Dry::Logic::Predicate id=:inclusion?>>}, @rule=#<Dry::Logic::Rule::Value name=Integer predicate=#<Dry::Logic::Predicate id=:inclusion?>>>, @options={:values=>[1, 3, 6, 12]}, @values=[1, 3, 6, 12]>
2.3.0 :003 > billing_period[3]
 => 12
2.3.0 :004 >

Here you can see that due to special Fixnum processing actual billing period was treated as an array index. That led me to some strange troubles. My spec builds model then persists it (occasionally with wrong value 12 instead of 3). After that when I try to fetch that record from DB the spec fails with an error "nil violates constraints" because it reads 12 and then tries to construct a value by calling PeriodType[12].

It is quite confusing, isn't it? What do you think? :)

P.S. Piotr I want to thank you very much for all that cool stuff. Great work!

Nikita

Rename it to dry-types!

Hello, @solnic!

I revisited your gem again and now it looks better to me. In fact, I find it somehow ideal because it gives maximum flexibility and the API is pretty good. I cannot believe because the first time I found dry-data very ugly.

So the last thing that bothers me is a name. What's wrong with it? Is it a reference to Haskell data keyword or what? If so then algebraic data types (structs and sum types in your terms) are not a whole picture. dry-data works not only with them but with primitive types too. If it's not a reference to data keyword then what it is? It looks that dry-data is not about data but types. Do you agree with me or maybe I'm missing something?

Code smell? Custom constructor for Strict type can need type-check

Hi there,

As a first-time user of dry-types, I'm reporting a 'code smell' that confused me when I ran into it.

I created a TrimmedString type using Strict::String and a custom constructor which trims the supplied input: TrimmedString = Strict::String.constructor { |value| value.strip }

What I expected:

  • TrimmedString[' Trim me '], the supplied String value is trimmed and returned
  • TrimmedString[3], a 'Dry::Types::ConstraintError" is thrown since input is not a String

For TrimmedString[3], this is not what happened. Instead, the code threw an exception because the strip method was not found on the Fixnum 3. This is the code smell: I didn't expect the custom constructor to be called, since the input is not a String (and I specified Strict::String). The fix is easy enough: TrimmedString = Strict::String.constructor { |value| value.kind_of?(::String) ? value.strip : value }

I realise that I will get things wrong since I'm a new user of dry-types, and am piecing things together from blog posts etc (the docs for dry-types latest are not complete). However, this is something that didn't feel intuitively right, and may trip up others.

Thanks for a nifty library!

Example program

require 'bundler/setup'

require 'dry-types'

module Types
  include Dry::Types.module

  TrimmedString = Strict::String.constructor { |value|
    # Throws exception: undefined method `strip', when supplied Fixnum
    value.strip
    # Replace with type-checked line below to stop exception
    # value.kind_of?(::String) ? value.strip : value
  }

end

puts Types::TrimmedString[' Trim me ']
puts Types::TrimmedString[2]

# Output
Trim me
app/types/wtf.rb:10:in `block in <module:Types>': undefined method `strip' for 2:Fixnum (NoMethodError)
    from /home/vagrant/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/bundler/gems/dry-types-ed7a7f605dfa/lib/dry/types/constructor.rb:28:in `[]'
    from /home/vagrant/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/bundler/gems/dry-types-ed7a7f605dfa/lib/dry/types/constructor.rb:28:in `call'
    from app/types/wtf.rb:18:in `<main>'

Rails hot reloading in development environment causes error in Dry::Container::Error

I have class in my project

class PhoneToken::Generate < Dry::Types::Struct
  ...
end

It works fine until I edit file containing this code and do a request. Rails tries to reload edited file which leads to this error:

Dry::Container::Error - There is already an item registered with the key "phone_token.generate"

The same behavior can be reproduced using reload! in rails console.

Dynamic default values

Not sure if this is a bad idea or not, but here it is anyway.

message_input = Dry::Types['hash'].schema(
  body:       'strict.string',
  from_id:    'strict.int',
  created_at: Dry::Types['strict.time'].default{ Time.now }
)

Documentation Question `Types::Form::String`

This page makes reference to Types::Form::String, but that doesn't appear to exist. However, in chatting with @timriley it doesn't appear to just be a bug, as there's mention of a specific behaviour (that "" would be coerced to nil). Is this a case of the documentation being out-of-date (or perhaps ahead-of-date) or something else? :)

Behavior when a struct type does not specify any attributes

While integrating dry-data into a small project of mine I happened to try to initialize a dry struct before specifying any attributes

>> require 'dry/data'
=> true
>>
>> class User < Dry::Data::Struct
 | end
=> nil
>>
>> params = {}
=> {}
>>
>> User.new(params)
NoMethodError: undefined method `[]' for nil:NilClass
from /Users/johnbackus/.rvm/gems/ruby-2.3.0/gems/dry-data-0.4.2/lib/dry/data/struct.rb:34:in `new'

Personally, I think this should just initialize an object with no members. I'm not sure what you intend the behavior will be in this case though. It could also throw a custom error saying you can't create structs with zero attributes.

I'd be happy to contribute a fix once I know how this case should behave.

Add a better Hash::Schema sub-type suitable for dry-v

Currently we use Types::Safe with Types::Hash::Schema::Symbolized for input-processors. Unfortunately its current behavior has a major issue - when applying, value coercion results in a type error, the safe type will rescue from that and return original input. This is not acceptable in dry-v context since it results in getting the original input back so we cannot apply validation rules as basic key? checks fails since we don't even get symbolized keys back.

We need two things:

  1. Symbolizing keys
  2. Using Type#try for value coercion and setting original value in the resulting hash if result is a failure

Refs dry-rb/dry-validation#131
Refs dry-rb/dry-validation#132
Refs dry-rb/dry-validation#133

Feature request: make a constrained type its own unique class (subclassing constrained base class)

Hi there,

Would it be possible for a constrained-type to be its own unique class?

So, for a value of constrained-type, value.class == constrained-type-class. constrained-type-class should be a subclass of the base-class it constrains.

This is not currently the case. Instead, value.class == base-class.

Below is an example program showing a constrained-type that would benefit from this change, and a list of motivations for the change.

Example program

# Using dry-types head

require 'bundler/setup'

require 'dry-types'

module Types
  include Dry::Types.module

  # Constrained email type: a string of certain form and size, and normalised
  EmailString = Strict::String.constrained(
      format: /\A[^\s@]+@([^\s@.]+\.)+([^\s@.]+)\z/u, # Must loosely look like email
      max_size: 254 # Maximum possible length of email
  ).constructor { |value|
    value.kind_of?(::String) ? value.strip.downcase : value # Normalisation
  }
end

value = Types::EmailString['   [email protected] ']
puts value
puts value.class

# Expected output
# [email protected]
# String

# Preferred output
# [email protected]
# EmailString (which subclasses String)

Motivation

Motivations for this change:

  • A value of constrained-type can be correctly reasoned about, based on its class. If we know value.class == constrained-type-class:
    • we know that value has validated qualities (of a certain format, size, ..)
    • we know that value has been normalised in a particular way
    • if we use value elsewhere, we don't need to create a new instance of constrained-type[value] just to be sure it's truly of constrained-type: we know it is already
  • If constrained-type-class subclasses base-class, value will still function as the base-class, but a more specialised version
  • It intuitively seems right - when I started dry-types, I assumed a value of constrained-type would have a distinct class

Better error message when missing to specify "type" in struct

class User < Dry::Data::Struct
  attribute :name, 'strict.string'
  attribute :email # <-- whoops I forgot the "type"
end

blows up with something like this:

/vendor/bundle/gems/dry-data-0.0.1/lib/dry/data.rb:42:in block in []': undefined method match' for nil:NilClass (NoMethodError)

which is quite ironic since the README states "(NoMethodError: undefined methodsize' for nil:NilClass` anyone?)" =P

I'm aware this lib is not stable yet ;)

dry-types throwing a large number of warnings every time I run my minitest test suite

On every rake test for my gem (you can see the code here: https://github.com/grempe/tss-rb) I see the following warnings which seems pretty excessive to me.

...
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-configurable-0.1.4/lib/dry/configurable.rb:31: warning: method redefined; discarding old _settings
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-configurable-0.1.4/lib/dry/configurable.rb:31: warning: method redefined; discarding old _settings
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-container-0.3.1/lib/dry/container/mixin.rb:29: warning: method redefined; discarding old _container
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-configurable-0.1.4/lib/dry/configurable.rb:31: warning: method redefined; discarding old _settings
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/options.rb:18: warning: method redefined; discarding old meta
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:56: warning: instance variable @constructor_type not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-configurable-0.1.4/lib/dry/configurable.rb:31: warning: method redefined; discarding old _settings
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-configurable-0.1.4/lib/dry/configurable.rb:31: warning: method redefined; discarding old _settings
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:56: warning: instance variable @constructor_type not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:56: warning: instance variable @constructor_type not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
/usr/local/var/rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/dry-types-0.7.1/lib/dry/types/struct.rb:62: warning: instance variable @schema not initialized
...

Rename `Coercible::*` to `Kernel::*` for clarity

I believe the Coercible types may cause confusion as people may expect complex coercion logic under this category whereas it only provides Kernel type constructor methods (like Kernel.Integer etc.).

Dry::Types::Struct should be allowed to be blank

joevandyk@84979b0 has the failing test.

Test output:

Failures:

  1) Dry::Types::Struct.attribute works on blank structs
     Failure/Error: super(constructor[attributes])

     NoMethodError:
       undefined method `[]' for nil:NilClass
     # ./lib/dry/types/struct.rb:48:in `new'
     # ./spec/dry/types/struct_spec.rb:97:in `block (3 levels) in <top (required)>'


Finished in 0.09924 seconds (files took 0.92103 seconds to load)
173 examples, 1 failure

Decimal doesn't convert Floats

class Sku < Dry::Types::Struct
  attribute :price, Types::Coercible::Decimal
end
p Sku.new(price: 1.2).price
# dry-types-0.6.0/lib/dry/types/constructor.rb:30:in `BigDecimal': can't omit precision for a Float. (ArgumentError)

I think ideally dry-types would convert floats to a string before doing BigDecimal.new(input)

AST compiling failling with an array without member rules

Falling AST:

ast = [
      :type, [
        "hash", [
          :schema, [
            [:key, [:skills, [:type, "array"]]]
          ]
        ]
      ]
    ]

This fails because visit_array tries to define a member rule every time, even when they don't exist.

I did some small tests and this seemed to solve the issue:

def visit_array(node)
  if node
    registry['array'].member(call(node))
  else
    registry['array']
  end
end

But I'm not sure if this would impact other usages of the AST with an array.

This is basically needed for schema validations that are done like that: required(:skills).filled(:array?), in which we just want to ensure that the key has an array, but the values inside of it doesn't matter.

Related: dry-rb/dry-validation#170

Allow defaults to be set on on sum types

If I have a type like this:

Types::Form::Bool.default(false)

I'll get this error:

NoMethodError: undefined method `default' for #<Dry::Data::SumType:0x007f805b9a51b8>

Should it be possible to allow default values for sum types? Seems like in this case at least it'd make sense.

Let me know what you think and I'd be happy to take a stab at implementing this.

Thanks!

Default value can violate constraints

I'm not sure if this is a bug or the intended behavior.

type = Dry::Types["string"].constrained(min_size: 5).default("asd")
type.call(nil)  # => "asd"

type.call("asd")  # => Dry::Types::ConstraintError: "asd" violates constraint

Sum types with structs are not supported

I realized I cannot use | to create sum types with Dry::Types::Struct. The following doesn't work with the latest version from Rubygems:

class DiscreetFilter < Dry::Types::Struct 
     attribute :target_column, Types::Strict::String
     attribute :name, Types::Strict::String
     attribute :type, Types::Strict::String.enum("DISCREET")
     attribute :id, Types::Strict::String
     attribute :active, Types::Bool
     attribute :values, Types::Array.member(Types::DiscreetFilterValues)
  end

  class RangeFilter < Dry::Types::Struct 

    RangeFilterValue = Types::Hash.schema({
      range_max: Types::Coercible::Float,
      range_min: Types::Coercible::Float
    })

    attribute :target_column, Types::Strict::String
    attribute :name, Types::Strict::String
    attribute :type, Types::Strict::String.enum("RANGE")
    attribute :unit, Types::Strict::String
    attribute :id, Types::Strict::String
    attribute :active, Types::Bool
    attribute :values, Types::Array.member(RangeFilterValue)
  end

  #throws: NoMethodError: undefined method `|' for Types::DiscreetFilter:Class
  LineFilter = Types::DiscreetFilter | Types::RangeFilter 

Is there a reason why this shouldn't be added? It seems to me sum types should be supported here to, right?

I also tried naively to do Dry::Types::Sum.new Types::DiscreetFilter, Types::RangeFilter but that doesn't work because there's a try method missing.

Types::Bool.default(false) does not work

For attempting to set a default value false to Types::Bool, I am getting constraint violation because of the following line of code:

https://github.com/dry-rb/dry-types/blob/master/lib/dry/types/builder.rb#L26

because false in there was ignored and block wasn't given. Is this something expected? Or is Bool by default has a default value of false?

So far I worked around it by doing Types::Bool.default { false }, but having to create a proc does drag the performance down a little bit.

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.