dry-types 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.travis.yml +10 -4
  6. data/CHANGELOG.md +101 -3
  7. data/Gemfile +9 -6
  8. data/README.md +2 -2
  9. data/Rakefile +2 -2
  10. data/benchmarks/hash_schemas.rb +8 -6
  11. data/benchmarks/lax_schema.rb +0 -1
  12. data/benchmarks/profile_invalid_input.rb +1 -1
  13. data/benchmarks/profile_lax_schema_valid.rb +1 -1
  14. data/benchmarks/profile_valid_input.rb +1 -1
  15. data/docsite/source/array-with-member.html.md +13 -0
  16. data/docsite/source/built-in-types.html.md +116 -0
  17. data/docsite/source/constraints.html.md +31 -0
  18. data/docsite/source/custom-types.html.md +93 -0
  19. data/docsite/source/default-values.html.md +91 -0
  20. data/docsite/source/enum.html.md +69 -0
  21. data/docsite/source/getting-started.html.md +57 -0
  22. data/docsite/source/hash-schemas.html.md +169 -0
  23. data/docsite/source/index.html.md +155 -0
  24. data/docsite/source/map.html.md +17 -0
  25. data/docsite/source/optional-values.html.md +96 -0
  26. data/docsite/source/sum.html.md +21 -0
  27. data/dry-types.gemspec +19 -19
  28. data/lib/dry/types.rb +9 -4
  29. data/lib/dry/types/array.rb +6 -0
  30. data/lib/dry/types/array/constructor.rb +32 -0
  31. data/lib/dry/types/array/member.rb +8 -1
  32. data/lib/dry/types/builder.rb +1 -1
  33. data/lib/dry/types/builder_methods.rb +33 -23
  34. data/lib/dry/types/coercions.rb +19 -6
  35. data/lib/dry/types/coercions/params.rb +4 -4
  36. data/lib/dry/types/constrained.rb +5 -0
  37. data/lib/dry/types/constructor.rb +5 -37
  38. data/lib/dry/types/constructor/function.rb +4 -5
  39. data/lib/dry/types/core.rb +27 -8
  40. data/lib/dry/types/decorator.rb +1 -1
  41. data/lib/dry/types/enum.rb +1 -0
  42. data/lib/dry/types/extensions.rb +4 -0
  43. data/lib/dry/types/extensions/maybe.rb +9 -1
  44. data/lib/dry/types/extensions/monads.rb +29 -0
  45. data/lib/dry/types/hash.rb +10 -11
  46. data/lib/dry/types/hash/constructor.rb +5 -5
  47. data/lib/dry/types/json.rb +4 -0
  48. data/lib/dry/types/lax.rb +4 -4
  49. data/lib/dry/types/map.rb +8 -4
  50. data/lib/dry/types/module.rb +3 -3
  51. data/lib/dry/types/nominal.rb +3 -4
  52. data/lib/dry/types/params.rb +9 -0
  53. data/lib/dry/types/predicate_inferrer.rb +197 -0
  54. data/lib/dry/types/predicate_registry.rb +34 -0
  55. data/lib/dry/types/primitive_inferrer.rb +97 -0
  56. data/lib/dry/types/printer.rb +17 -12
  57. data/lib/dry/types/schema.rb +14 -20
  58. data/lib/dry/types/schema/key.rb +19 -1
  59. data/lib/dry/types/spec/types.rb +3 -6
  60. data/lib/dry/types/version.rb +1 -1
  61. metadata +79 -52
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: Introduction
3
+ layout: gem-single
4
+ type: gem
5
+ name: dry-types
6
+ sections:
7
+ - getting-started
8
+ - built-in-types
9
+ - optional-values
10
+ - default-values
11
+ - sum
12
+ - constraints
13
+ - hash-schemas
14
+ - array-with-member
15
+ - enum
16
+ - map
17
+ - custom-types
18
+ ---
19
+
20
+ `dry-types` is a simple and extendable type system for Ruby; useful for value coercions, applying constraints, defining complex structs or value objects and more. It was created as a successor to [Virtus](https://github.com/solnic/virtus).
21
+
22
+ ### Example usage
23
+
24
+ ```ruby
25
+ require 'dry-types'
26
+ require 'dry-struct'
27
+
28
+ module Types
29
+ include Dry.Types()
30
+ end
31
+
32
+ User = Dry.Struct(name: Types::String, age: Types::Integer)
33
+
34
+ User.new(name: 'Bob', age: 35)
35
+ # => #<User name="Bob" age=35>
36
+ ```
37
+
38
+ See [Built-in Types](/gems/dry-types/1.0/built-in-types/) for a full list of available types.
39
+
40
+ By themselves, the basic type definitions like `Types::String` and `Types::Integer` don't do anything except provide documentation about which type an attribute is expected to have. However, there are many more advanced possibilities:
41
+
42
+ - `Strict` types will raise an error if passed an attribute of the wrong type:
43
+
44
+ ```ruby
45
+ class User < Dry::Struct
46
+ attribute :name, Types::Strict::String
47
+ attribute :age, Types::Strict::Integer
48
+ end
49
+
50
+ User.new(name: 'Bob', age: '18')
51
+ # => Dry::Struct::Error: [User.new] "18" (String) has invalid type for :age
52
+ ```
53
+
54
+ - `Coercible` types will attempt to convert an attribute to the correct class
55
+ using Ruby's built-in coercion methods:
56
+
57
+ ```ruby
58
+ class User < Dry::Struct
59
+ attribute :name, Types::Coercible::String
60
+ attribute :age, Types::Coercible::Integer
61
+ end
62
+
63
+ User.new(name: 'Bob', age: '18')
64
+ # => #<User name="Bob" age=18>
65
+ User.new(name: 'Bob', age: 'not coercible')
66
+ # => ArgumentError: invalid value for Integer(): "not coercible"
67
+ ```
68
+
69
+ - Use `.optional` to denote that an attribute can be `nil` (see [Optional Values](/gems/dry-types/1.0/optional-values)):
70
+
71
+ ```ruby
72
+ class User < Dry::Struct
73
+ attribute :name, Types::String
74
+ attribute :age, Types::Integer.optional
75
+ end
76
+
77
+ User.new(name: 'Bob', age: nil)
78
+ # => #<User name="Bob" age=nil>
79
+ # name is not optional:
80
+ User.new(name: nil, age: 18)
81
+ # => Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
82
+ # keys must still be present:
83
+ User.new(name: 'Bob')
84
+ # => Dry::Struct::Error: [User.new] :age is missing in Hash input
85
+ ```
86
+
87
+ - Add custom constraints (see [Constraints](/gems/dry-types/1.0/constraints.html)):
88
+
89
+ ```ruby
90
+ class User < Dry::Struct
91
+ attribute :name, Types::Strict::String
92
+ attribute :age, Types::Strict::Integer.constrained(gteq: 18)
93
+ end
94
+
95
+ User.new(name: 'Bob', age: 17)
96
+ # => Dry::Struct::Error: [User.new] 17 (Fixnum) has invalid type for :age
97
+ ```
98
+
99
+ - Add custom metadata to a type:
100
+
101
+ ```ruby
102
+ class User < Dry::Struct
103
+ attribute :name, Types::String
104
+ attribute :age, Types::Integer.meta(info: 'extra info about age')
105
+ end
106
+ ```
107
+
108
+ - Pass values directly to `Dry::Types` without creating an object using `[]`:
109
+
110
+ ```ruby
111
+ Types::Strict::String["foo"]
112
+ # => "foo"
113
+ Types::Strict::String["10000"]
114
+ # => "10000"
115
+ Types::Coercible::String[10000]
116
+ # => "10000"
117
+ Types::Strict::String[10000]
118
+ # Dry::Types::ConstraintError: 1000 violates constraints
119
+ ```
120
+
121
+ ### Features
122
+
123
+ * Support for [constrained types](/gems/dry-types/1.0/constraints)
124
+ * Support for [optional values](/gems/dry-types/1.0/optional-values)
125
+ * Support for [default values](/gems/dry-types/1.0/default-values)
126
+ * Support for [sum types](/gems/dry-types/1.0/sum)
127
+ * Support for [enums](/gems/dry-types/1.0/enum)
128
+ * Support for [hash type with type schemas](/gems/dry-types/1.0/hash-schemas)
129
+ * Support for [array type with members](/gems/dry-types/1.0/array-with-member)
130
+ * Support for arbitrary meta information
131
+ * Support for typed struct objects via [dry-struct](/gems/dry-struct)
132
+ * Types are [categorized](/gems/dry-types/1.0/built-in-types), which is especially important for optimized and dedicated coercion logic
133
+ * Types are composable and reusable objects
134
+ * No const-missing magic and complicated const lookups
135
+ * Roughly 6-10 x faster than Virtus
136
+
137
+ ### Use cases
138
+
139
+ `dry-types` is suitable for many use-cases, for example:
140
+
141
+ * Value coercions
142
+ * Processing arrays
143
+ * Processing hashes with explicit schemas
144
+ * Defining various domain-specific information shared between multiple parts of your application
145
+ * Annotating objects
146
+
147
+ ### Other gems using dry-types
148
+
149
+ `dry-types` is often used as a low-level abstraction. The following gems use it already:
150
+
151
+ * [dry-struct](/gems/dry-struct)
152
+ * [dry-initializer](/gems/dry-initializer)
153
+ * [Hanami](http://hanamirb.org)
154
+ * [rom-rb](http://rom-rb.org)
155
+ * [Trailblazer](http://trailblazer.to)
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Map
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ `Map` describes a homogeneous hashmap. This means only types of keys and values are known. You can simply imagine a map input as a list of key-value pairs.
8
+
9
+ ```ruby
10
+ int_float_hash = Types::Hash.map(Types::Integer, Types::Float)
11
+ int_float_hash[100 => 300.0, 42 => 70.0]
12
+ # => {100=>300.0, 42=>70.0}
13
+
14
+ # Only accepts mappings of integers to floats
15
+ int_float_hash[name: 'Jane']
16
+ # => Dry::Types::MapError: input key :name is invalid: type?(Integer, :name)
17
+ ```
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Type Attributes
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ Types themselves have optional attributes you can apply to get further functionality.
8
+
9
+ ### Append `.optional` to a _Type_ to allow `nil`
10
+
11
+ By default, nil values raise an error:
12
+
13
+ ``` ruby
14
+ Types::Strict::String[nil]
15
+ # => raises Dry::Types::ConstraintError
16
+ ```
17
+
18
+ Add `.optional` and `nil` values become valid:
19
+
20
+ ```ruby
21
+ optional_string = Types::Strict::String.optional
22
+
23
+ optional_string[nil]
24
+ # => nil
25
+ optional_string['something']
26
+ # => "something"
27
+ optional_string[123]
28
+ # raises Dry::Types::ConstraintError
29
+ ```
30
+
31
+ `Types::String.optional` is just syntactic sugar for `Types::Strict::Nil | Types::Strict::String`.
32
+
33
+ ### Handle optional values using Monads
34
+
35
+ The [dry-monads gem](/gems/dry-monads/) provides another approach to handling optional values by returning a [_Monad_](/gems/dry-monads/) object. This allows you to pass your type to a `Maybe(x)` block that only executes if `x` returns `Some` or `None`.
36
+
37
+ > NOTE: Requires the [dry-monads gem](/gems/dry-monads/) to be loaded.
38
+
39
+ 1. Load the `:maybe` extension in your application.
40
+
41
+ ```ruby
42
+ require 'dry-types'
43
+
44
+ Dry::Types.load_extensions(:maybe)
45
+ module Types
46
+ include Dry.Types()
47
+ end
48
+ ```
49
+
50
+ 2. Append `.maybe` to a _Type_ to return a _Monad_ object
51
+
52
+ ```ruby
53
+ x = Types::Maybe::Strict::Integer[nil]
54
+ Maybe(x) { puts(x) }
55
+
56
+ x = Types::Maybe::Coercible::String[nil]
57
+ Maybe(x) { puts(x) }
58
+
59
+ x = Types::Maybe::Strict::Integer[123]
60
+ Maybe(x) { puts(x) }
61
+
62
+ x = Types::Maybe::Strict::String[123]
63
+ Maybe(x) { puts(x) }
64
+ ```
65
+
66
+ ```ruby
67
+ Types::Maybe::Strict::Integer[nil] # None
68
+ Types::Maybe::Strict::Integer[123] # Some(123)
69
+
70
+ Types::Maybe::Coercible::Float[nil] # None
71
+ Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
72
+
73
+ # 'Maybe' types can also accessed by calling '.maybe' on a regular type:
74
+ Types::Strict::Integer.maybe # equivalent to Types::Maybe::Strict::Integer
75
+ ```
76
+
77
+ You can define your own optional types:
78
+
79
+ ``` ruby
80
+ maybe_string = Types::Strict::String.maybe
81
+
82
+ maybe_string[nil]
83
+ # => None
84
+
85
+ maybe_string[nil].fmap(&:upcase)
86
+ # => None
87
+
88
+ maybe_string['something']
89
+ # => Some('something')
90
+
91
+ maybe_string['something'].fmap(&:upcase)
92
+ # => Some('SOMETHING')
93
+
94
+ maybe_string['something'].fmap(&:upcase).value_or('NOTHING')
95
+ # => "SOMETHING"
96
+ ```
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Sum
3
+ layout: gem-single
4
+ name: dry-types
5
+ order: 7
6
+ ---
7
+
8
+ You can specify sum types using `|` operator, it is an explicit way of defining what the valid types of a value are.
9
+
10
+ For example `dry-types` defines the `Bool` type which is a sum consisting of the `True` and `False` types, expressed as `Types::True | Types::False`.
11
+
12
+ Another common case is defining that something can be either `nil` or something else:
13
+
14
+ ``` ruby
15
+ nil_or_string = Types::Nil | Types::String
16
+
17
+ nil_or_string[nil] # => nil
18
+ nil_or_string["hello"] # => "hello"
19
+
20
+ nil_or_string[123] # raises Dry::Types::ConstraintError
21
+ ```
data/dry-types.gemspec CHANGED
@@ -1,47 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'dry/types/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = "dry-types"
8
+ spec.name = 'dry-types'
9
9
  spec.version = Dry::Types::VERSION.dup
10
- spec.authors = ["Piotr Solnica"]
11
- spec.email = ["piotr.solnica@gmail.com"]
10
+ spec.authors = ['Piotr Solnica']
11
+ spec.email = ['piotr.solnica@gmail.com']
12
12
  spec.license = 'MIT'
13
13
 
14
14
  spec.summary = 'Type system for Ruby supporting coercions, constraints and complex types like structs, value objects, enums etc.'
15
15
  spec.description = spec.summary
16
- spec.homepage = "https://github.com/dry-rb/dry-types"
16
+ spec.homepage = 'https://github.com/dry-rb/dry-types'
17
17
 
18
18
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
19
19
  # delete this section to allow pushing this gem to any host.
20
20
  if spec.respond_to?(:metadata)
21
- spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
- spec.metadata['changelog_uri'] = "https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
23
- spec.metadata['source_code_uri'] = "https://github.com/dry-rb/dry-types"
24
- spec.metadata['bug_tracker_uri'] = "https://github.com/dry-rb/dry-types/issues"
21
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md'
23
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-types'
24
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-types/issues'
25
25
  else
26
- raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
26
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
27
27
  end
28
28
 
29
29
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - ['bin/console', 'bin/setup']
30
- spec.bindir = "exe"
30
+ spec.bindir = 'exe'
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
- spec.required_ruby_version = ">= 2.4.0"
32
+ spec.require_paths = ['lib']
33
+ spec.required_ruby_version = '>= 2.4.0'
34
34
 
35
35
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
36
- spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
37
- spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
38
36
  spec.add_runtime_dependency 'dry-container', '~> 0.3'
37
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
39
38
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2', '>= 0.2.2'
40
- spec.add_runtime_dependency 'dry-logic', '~> 1.0'
39
+ spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
40
+ spec.add_runtime_dependency 'dry-logic', '~> 1.0', '>= 1.0.2'
41
41
 
42
- spec.add_development_dependency "bundler"
43
- spec.add_development_dependency "rake", "~> 11.0"
44
- spec.add_development_dependency "rspec", "~> 3.3"
42
+ spec.add_development_dependency 'bundler'
45
43
  spec.add_development_dependency 'dry-monads', '~> 0.2'
44
+ spec.add_development_dependency 'rake', '~> 11.0'
45
+ spec.add_development_dependency 'rspec', '~> 3.3'
46
46
  spec.add_development_dependency 'yard', '~> 0.9.5'
47
47
  end
data/lib/dry/types.rb CHANGED
@@ -33,7 +33,7 @@ module Dry
33
33
  extend Dry::Core::Deprecations[:'dry-types']
34
34
  include Dry::Core::Constants
35
35
 
36
- TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
36
+ TYPE_SPEC_REGEX = /(.+)<(.+)>/.freeze
37
37
 
38
38
  # @see Dry.Types
39
39
  def self.module(*namespaces, default: :nominal, **aliases)
@@ -45,7 +45,7 @@ module Dry
45
45
 
46
46
  # @api private
47
47
  def self.included(*)
48
- raise RuntimeError, "Import Dry.Types, not Dry::Types"
48
+ raise 'Import Dry.Types, not Dry::Types'
49
49
  end
50
50
 
51
51
  # Return container with registered built-in type objects
@@ -89,7 +89,7 @@ module Dry
89
89
  def self.[](name)
90
90
  type_map.fetch_or_store(name) do
91
91
  case name
92
- when String
92
+ when ::String
93
93
  result = name.match(TYPE_SPEC_REGEX)
94
94
 
95
95
  if result
@@ -98,7 +98,12 @@ module Dry
98
98
  else
99
99
  container[name]
100
100
  end
101
- when Class
101
+ when ::Class
102
+ warn(<<~DEPRECATION)
103
+ Using Dry::Types.[] with a class is deprecated, please use string identifiers: Dry::Types[Integer] -> Dry::Types['integer'].
104
+ If you're using dry-struct this means changing `attribute :counter, Integer` to `attribute :counter, Dry::Types['integer']` or to `attribute :counter, 'integer'`.
105
+ DEPRECATION
106
+
102
107
  type_name = identifier(name)
103
108
 
104
109
  if container.key?(type_name)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/types/array/member'
4
+ require 'dry/types/array/constructor'
4
5
 
5
6
  module Dry
6
7
  module Types
@@ -24,6 +25,11 @@ module Dry
24
25
 
25
26
  Array::Member.new(primitive, **options, member: member)
26
27
  end
28
+
29
+ # @api private
30
+ def constructor_type
31
+ ::Dry::Types::Array::Constructor
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/types/constructor'
4
+
5
+ module Dry
6
+ module Types
7
+ # @api public
8
+ class Array < Nominal
9
+ # @api private
10
+ class Constructor < ::Dry::Types::Constructor
11
+ # @api private
12
+ def constructor_type
13
+ ::Dry::Types::Array::Constructor
14
+ end
15
+
16
+ # @return [Lax]
17
+ #
18
+ # @api public
19
+ def lax
20
+ Lax.new(type.lax.constructor(fn, meta: meta))
21
+ end
22
+
23
+ # @see Dry::Types::Array#of
24
+ #
25
+ # @api public
26
+ def of(member)
27
+ type.of(member).constructor(fn, meta: meta)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end