dry-types 1.2.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +405 -221
  3. data/LICENSE +1 -1
  4. data/README.md +14 -13
  5. data/dry-types.gemspec +26 -31
  6. data/lib/dry-types.rb +1 -1
  7. data/lib/dry/types.rb +55 -40
  8. data/lib/dry/types/any.rb +2 -2
  9. data/lib/dry/types/array.rb +2 -2
  10. data/lib/dry/types/array/constructor.rb +1 -1
  11. data/lib/dry/types/array/member.rb +1 -1
  12. data/lib/dry/types/builder.rb +70 -18
  13. data/lib/dry/types/builder_methods.rb +6 -3
  14. data/lib/dry/types/coercions.rb +6 -17
  15. data/lib/dry/types/coercions/json.rb +22 -5
  16. data/lib/dry/types/coercions/params.rb +21 -4
  17. data/lib/dry/types/compiler.rb +10 -10
  18. data/lib/dry/types/constrained.rb +6 -10
  19. data/lib/dry/types/constraints.rb +3 -3
  20. data/lib/dry/types/constructor.rb +40 -7
  21. data/lib/dry/types/constructor/function.rb +47 -32
  22. data/lib/dry/types/constructor/wrapper.rb +94 -0
  23. data/lib/dry/types/container.rb +1 -1
  24. data/lib/dry/types/core.rb +15 -13
  25. data/lib/dry/types/decorator.rb +2 -9
  26. data/lib/dry/types/default.rb +15 -3
  27. data/lib/dry/types/enum.rb +4 -4
  28. data/lib/dry/types/errors.rb +6 -6
  29. data/lib/dry/types/extensions.rb +2 -2
  30. data/lib/dry/types/extensions/maybe.rb +17 -17
  31. data/lib/dry/types/extensions/monads.rb +1 -1
  32. data/lib/dry/types/fn_container.rb +1 -1
  33. data/lib/dry/types/hash.rb +9 -15
  34. data/lib/dry/types/hash/constructor.rb +1 -1
  35. data/lib/dry/types/inflector.rb +1 -1
  36. data/lib/dry/types/json.rb +15 -15
  37. data/lib/dry/types/lax.rb +4 -7
  38. data/lib/dry/types/map.rb +2 -2
  39. data/lib/dry/types/meta.rb +3 -3
  40. data/lib/dry/types/module.rb +6 -6
  41. data/lib/dry/types/nominal.rb +11 -12
  42. data/lib/dry/types/params.rb +31 -28
  43. data/lib/dry/types/predicate_inferrer.rb +52 -11
  44. data/lib/dry/types/predicate_registry.rb +1 -1
  45. data/lib/dry/types/primitive_inferrer.rb +1 -1
  46. data/lib/dry/types/printer.rb +25 -25
  47. data/lib/dry/types/result.rb +3 -3
  48. data/lib/dry/types/schema.rb +26 -13
  49. data/lib/dry/types/schema/key.rb +20 -7
  50. data/lib/dry/types/spec/types.rb +65 -42
  51. data/lib/dry/types/sum.rb +6 -6
  52. data/lib/dry/types/type.rb +1 -1
  53. data/lib/dry/types/version.rb +1 -1
  54. metadata +27 -84
  55. data/.codeclimate.yml +0 -12
  56. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  57. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  58. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  59. data/.github/workflows/custom_ci.yml +0 -76
  60. data/.github/workflows/docsite.yml +0 -34
  61. data/.github/workflows/sync_configs.yml +0 -34
  62. data/.gitignore +0 -11
  63. data/.rspec +0 -4
  64. data/.rubocop.yml +0 -89
  65. data/.yardopts +0 -9
  66. data/CODE_OF_CONDUCT.md +0 -13
  67. data/CONTRIBUTING.md +0 -29
  68. data/Gemfile +0 -32
  69. data/Rakefile +0 -22
  70. data/benchmarks/hash_schemas.rb +0 -55
  71. data/benchmarks/lax_schema.rb +0 -15
  72. data/benchmarks/profile_invalid_input.rb +0 -15
  73. data/benchmarks/profile_lax_schema_valid.rb +0 -16
  74. data/benchmarks/profile_valid_input.rb +0 -15
  75. data/benchmarks/schema_valid_vs_invalid.rb +0 -21
  76. data/benchmarks/setup.rb +0 -17
  77. data/docsite/source/array-with-member.html.md +0 -13
  78. data/docsite/source/built-in-types.html.md +0 -116
  79. data/docsite/source/constraints.html.md +0 -31
  80. data/docsite/source/custom-types.html.md +0 -93
  81. data/docsite/source/default-values.html.md +0 -91
  82. data/docsite/source/enum.html.md +0 -69
  83. data/docsite/source/extensions.html.md +0 -15
  84. data/docsite/source/extensions/maybe.html.md +0 -57
  85. data/docsite/source/extensions/monads.html.md +0 -61
  86. data/docsite/source/getting-started.html.md +0 -57
  87. data/docsite/source/hash-schemas.html.md +0 -169
  88. data/docsite/source/index.html.md +0 -156
  89. data/docsite/source/map.html.md +0 -17
  90. data/docsite/source/optional-values.html.md +0 -35
  91. data/docsite/source/sum.html.md +0 -21
@@ -4,7 +4,6 @@ module Dry
4
4
  module Types
5
5
  # Common API for building type objects in a convenient way
6
6
  #
7
- # rubocop:disable Naming/MethodName
8
7
  #
9
8
  # @api public
10
9
  module BuilderMethods
@@ -83,7 +82,11 @@ module Dry
83
82
  # @return [Dry::Types::Type]
84
83
  def Constructor(klass, cons = nil, &block)
85
84
  if klass.is_a?(Type)
86
- klass.constructor(cons || block || klass.method(:new))
85
+ if cons || block
86
+ klass.constructor(cons || block)
87
+ else
88
+ klass
89
+ end
87
90
  else
88
91
  Nominal(klass).constructor(cons || block || klass.method(:new))
89
92
  end
@@ -129,7 +132,7 @@ module Dry
129
132
  #
130
133
  # @return [Dry::Types::Contrained]
131
134
  def Interface(*methods)
132
- methods.reduce(Types['nominal.any']) do |type, method|
135
+ methods.reduce(Types["nominal.any"]) do |type, method|
133
136
  type.constrained(respond_to: method)
134
137
  end
135
138
  end
@@ -8,23 +8,6 @@ module Dry
8
8
  module Coercions
9
9
  include Dry::Core::Constants
10
10
 
11
- # @param [String, Object] input
12
- #
13
- # @return [nil] if the input is an empty string or nil
14
- #
15
- # @raise CoercionError
16
- #
17
- # @api public
18
- def to_nil(input, &_block)
19
- if input.nil? || empty_str?(input)
20
- nil
21
- elsif block_given?
22
- yield
23
- else
24
- raise CoercionError, "#{input.inspect} is not nil"
25
- end
26
- end
27
-
28
11
  # @param [#to_str, Object] input
29
12
  #
30
13
  # @return [Date, Object]
@@ -39,6 +22,8 @@ module Dry
39
22
  rescue ArgumentError, RangeError => e
40
23
  CoercionError.handle(e, &block)
41
24
  end
25
+ elsif input.is_a?(::Date)
26
+ input
42
27
  elsif block_given?
43
28
  yield
44
29
  else
@@ -60,6 +45,8 @@ module Dry
60
45
  rescue ArgumentError => e
61
46
  CoercionError.handle(e, &block)
62
47
  end
48
+ elsif input.is_a?(::DateTime)
49
+ input
63
50
  elsif block_given?
64
51
  yield
65
52
  else
@@ -81,6 +68,8 @@ module Dry
81
68
  rescue ArgumentError => e
82
69
  CoercionError.handle(e, &block)
83
70
  end
71
+ elsif input.is_a?(::Time)
72
+ input
84
73
  elsif block_given?
85
74
  yield
86
75
  else
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'bigdecimal'
5
- require 'bigdecimal/util'
6
- require 'time'
3
+ require "date"
4
+ require "bigdecimal"
5
+ require "bigdecimal/util"
6
+ require "time"
7
7
 
8
8
  module Dry
9
9
  module Types
@@ -14,6 +14,23 @@ module Dry
14
14
  module JSON
15
15
  extend Coercions
16
16
 
17
+ # @param [Object] input
18
+ #
19
+ # @return [nil] if the input is nil
20
+ #
21
+ # @raise CoercionError
22
+ #
23
+ # @api public
24
+ def self.to_nil(input, &_block)
25
+ if input.nil?
26
+ nil
27
+ elsif block_given?
28
+ yield
29
+ else
30
+ raise CoercionError, "#{input.inspect} is not nil"
31
+ end
32
+ end
33
+
17
34
  # @param [#to_d, Object] input
18
35
  #
19
36
  # @return [BigDecimal,nil]
@@ -21,7 +38,7 @@ module Dry
21
38
  # @raise CoercionError
22
39
  #
23
40
  # @api public
24
- def self.to_decimal(input, &block)
41
+ def self.to_decimal(input, &_block)
25
42
  if input.is_a?(::Float)
26
43
  input.to_d
27
44
  else
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bigdecimal'
4
- require 'bigdecimal/util'
3
+ require "bigdecimal"
4
+ require "bigdecimal/util"
5
5
 
6
6
  module Dry
7
7
  module Types
@@ -14,10 +14,27 @@ module Dry
14
14
  FALSE_VALUES = %w[0 off Off OFF f false False FALSE F n no No NO N].freeze
15
15
  BOOLEAN_MAP = ::Hash[
16
16
  TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])
17
- ].freeze
17
+ ].merge(true => true, false => false).freeze
18
18
 
19
19
  extend Coercions
20
20
 
21
+ # @param [Object] input
22
+ #
23
+ # @return [nil] if the input is an empty string or nil
24
+ #
25
+ # @raise CoercionError
26
+ #
27
+ # @api public
28
+ def self.to_nil(input, &_block)
29
+ if input.nil? || empty_str?(input)
30
+ nil
31
+ elsif block_given?
32
+ yield
33
+ else
34
+ raise CoercionError, "#{input.inspect} is not nil"
35
+ end
36
+ end
37
+
21
38
  # @param [String, Object] input
22
39
  #
23
40
  # @return [Boolean,Object]
@@ -95,7 +112,7 @@ module Dry
95
112
  # @raise CoercionError
96
113
  #
97
114
  # @api public
98
- def self.to_decimal(input, &block)
115
+ def self.to_decimal(input, &_block)
99
116
  to_float(input) do
100
117
  if block_given?
101
118
  return yield
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/deprecations'
3
+ require "dry/core/deprecations"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -63,37 +63,37 @@ module Dry
63
63
  def visit_array(node)
64
64
  member, meta = node
65
65
  member = member.is_a?(Class) ? member : visit(member)
66
- registry['nominal.array'].of(member).meta(meta)
66
+ registry["nominal.array"].of(member).meta(meta)
67
67
  end
68
68
 
69
69
  def visit_hash(node)
70
70
  opts, meta = node
71
- registry['nominal.hash'].with(**opts, meta: meta)
71
+ registry["nominal.hash"].with(**opts, meta: meta)
72
72
  end
73
73
 
74
74
  def visit_schema(node)
75
75
  keys, options, meta = node
76
- registry['nominal.hash'].schema(keys.map { |key| visit(key) }).with(**options, meta: meta)
76
+ registry["nominal.hash"].schema(keys.map { |key| visit(key) }).with(**options, meta: meta)
77
77
  end
78
78
 
79
79
  def visit_json_hash(node)
80
80
  keys, meta = node
81
- registry['json.hash'].schema(keys.map { |key| visit(key) }, meta)
81
+ registry["json.hash"].schema(keys.map { |key| visit(key) }, meta)
82
82
  end
83
83
 
84
84
  def visit_json_array(node)
85
85
  member, meta = node
86
- registry['json.array'].of(visit(member)).meta(meta)
86
+ registry["json.array"].of(visit(member)).meta(meta)
87
87
  end
88
88
 
89
89
  def visit_params_hash(node)
90
90
  keys, meta = node
91
- registry['params.hash'].schema(keys.map { |key| visit(key) }, meta)
91
+ registry["params.hash"].schema(keys.map { |key| visit(key) }, meta)
92
92
  end
93
93
 
94
94
  def visit_params_array(node)
95
95
  member, meta = node
96
- registry['params.array'].of(visit(member)).meta(meta)
96
+ registry["params.array"].of(visit(member)).meta(meta)
97
97
  end
98
98
 
99
99
  def visit_key(node)
@@ -108,11 +108,11 @@ module Dry
108
108
 
109
109
  def visit_map(node)
110
110
  key_type, value_type, meta = node
111
- registry['nominal.hash'].map(visit(key_type), visit(value_type)).meta(meta)
111
+ registry["nominal.hash"].map(visit(key_type), visit(value_type)).meta(meta)
112
112
  end
113
113
 
114
114
  def visit_any(meta)
115
- registry['any'].meta(meta)
115
+ registry["any"].meta(meta)
116
116
  end
117
117
 
118
118
  def compile_fn(fn)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/types/decorator'
4
- require 'dry/types/constraints'
5
- require 'dry/types/constrained/coercible'
3
+ require "dry/types/decorator"
4
+ require "dry/types/constraints"
5
+ require "dry/types/constrained/coercible"
6
6
 
7
7
  module Dry
8
8
  module Types
@@ -14,7 +14,7 @@ module Dry
14
14
  include Decorator
15
15
  include Builder
16
16
  include Printable
17
- include Dry::Equalizer(:type, :rule, inspect: false)
17
+ include Dry::Equalizer(:type, :rule, inspect: false, immutable: true)
18
18
 
19
19
  # @return [Dry::Logic::Rule]
20
20
  attr_reader :rule
@@ -29,11 +29,9 @@ module Dry
29
29
  @rule = options.fetch(:rule)
30
30
  end
31
31
 
32
- # @api private
33
- #
34
32
  # @return [Object]
35
33
  #
36
- # @api public
34
+ # @api private
37
35
  def call_unsafe(input)
38
36
  result = rule.(input)
39
37
 
@@ -44,11 +42,9 @@ module Dry
44
42
  end
45
43
  end
46
44
 
47
- # @api private
48
- #
49
45
  # @return [Object]
50
46
  #
51
- # @api public
47
+ # @api private
52
48
  def call_safe(input, &block)
53
49
  if rule[input]
54
50
  type.call_safe(input, &block)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/rule_compiler'
4
- require 'dry/logic/predicates'
5
- require 'dry/logic/rule/predicate'
3
+ require "dry/logic/rule_compiler"
4
+ require "dry/logic/predicates"
5
+ require "dry/logic/rule/predicate"
6
6
 
7
7
  module Dry
8
8
  # Helper methods for constraint types
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/types/fn_container'
4
- require 'dry/types/constructor/function'
3
+ require "dry/types/fn_container"
4
+ require "dry/types/constructor/function"
5
+ require "dry/types/constructor/wrapper"
5
6
 
6
7
  module Dry
7
8
  module Types
@@ -10,7 +11,7 @@ module Dry
10
11
  #
11
12
  # @api public
12
13
  class Constructor < Nominal
13
- include Dry::Equalizer(:type, :options, inspect: false)
14
+ include Dry::Equalizer(:type, :options, inspect: false, immutable: true)
14
15
 
15
16
  # @return [#call]
16
17
  attr_reader :fn
@@ -30,6 +31,32 @@ module Dry
30
31
  super(type, **options, fn: Function[options.fetch(:fn, block)])
31
32
  end
32
33
 
34
+ # @param [Builder, Object] input
35
+ # @param [Hash] options
36
+ # @param [#call, nil] block
37
+ #
38
+ # @api public
39
+ def self.[](type, fn:, **options)
40
+ function = Function[fn]
41
+
42
+ if function.wrapper?
43
+ wrapper_type.new(type, fn: function, **options)
44
+ else
45
+ new(type, fn: function, **options)
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def self.wrapper_type
51
+ @wrapper_type ||= begin
52
+ if self < Wrapper
53
+ self
54
+ else
55
+ const_set(:Wrapping, ::Class.new(self).include(Wrapper))
56
+ end
57
+ end
58
+ end
59
+
33
60
  # Instantiate a new constructor type instance
34
61
  #
35
62
  # @param [Type] type
@@ -85,7 +112,13 @@ module Dry
85
112
  #
86
113
  # @api public
87
114
  def constructor(new_fn = nil, **options, &block)
88
- with(**options, fn: fn >> (new_fn || block))
115
+ next_fn = Function[new_fn || block]
116
+
117
+ if next_fn.wrapper?
118
+ self.class.wrapper_type.new(with(**options), fn: next_fn)
119
+ else
120
+ with(**options, fn: fn >> next_fn)
121
+ end
89
122
  end
90
123
  alias_method :append, :constructor
91
124
  alias_method :>>, :constructor
@@ -123,7 +156,7 @@ module Dry
123
156
  # @return [Lax]
124
157
  # @api public
125
158
  def lax
126
- Lax.new(Constructor.new(type.lax, **options))
159
+ Lax.new(constructor_type[type.lax, **options])
127
160
  end
128
161
 
129
162
  # Wrap the type with a proc
@@ -157,8 +190,8 @@ module Dry
157
190
  if type.respond_to?(method)
158
191
  response = type.public_send(method, *args, &block)
159
192
 
160
- if response.is_a?(Type) && type.class == response.class
161
- response.constructor_type.new(response, **options)
193
+ if response.is_a?(Type) && type.class.equal?(response.class)
194
+ response.constructor_type[response, **options]
162
195
  else
163
196
  response
164
197
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "concurrent/map"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -15,7 +15,7 @@ module Dry
15
15
  class Safe < Function
16
16
  def call(input, &block)
17
17
  @fn.(input, &block)
18
- rescue NoMethodError, TypeError, ArgumentError => e
18
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
19
19
  CoercionError.handle(e, &block)
20
20
  end
21
21
  end
@@ -30,10 +30,14 @@ module Dry
30
30
  #
31
31
  # @return [Function]
32
32
  def self.call_class(method, public, safe)
33
- @cache.fetch_or_store([method, public, safe].hash) do
33
+ @cache.fetch_or_store([method, public, safe]) do
34
34
  if public
35
35
  ::Class.new(PublicCall) do
36
36
  include PublicCall.call_interface(method, safe)
37
+
38
+ define_method(:__to_s__) do
39
+ "#<PublicCall for :#{method}>"
40
+ end
37
41
  end
38
42
  elsif safe
39
43
  PrivateCall
@@ -53,7 +57,7 @@ module Dry
53
57
  #
54
58
  # @return [::Module]
55
59
  def self.call_interface(method, safe)
56
- @interfaces.fetch_or_store([method, safe].hash) do
60
+ @interfaces.fetch_or_store([method, safe]) do
57
61
  ::Module.new do
58
62
  if safe
59
63
  module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
@@ -65,7 +69,7 @@ module Dry
65
69
  module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
66
70
  def call(input, &block)
67
71
  @target.#{method}(input)
68
- rescue NoMethodError, TypeError, ArgumentError => error
72
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => error
69
73
  CoercionError.handle(error, &block)
70
74
  end
71
75
  RUBY
@@ -90,7 +94,7 @@ module Dry
90
94
  class PrivateSafeCall < PrivateCall
91
95
  def call(input, &block)
92
96
  @target.send(@name, input)
93
- rescue NoMethodError, TypeError, ArgumentError => e
97
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
94
98
  CoercionError.handle(e, &block)
95
99
  end
96
100
  end
@@ -116,15 +120,31 @@ module Dry
116
120
  end
117
121
  end
118
122
 
123
+ class Wrapper < Function
124
+ # @return [Object]
125
+ def call(input, type, &block)
126
+ @fn.(input, type, &block)
127
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
128
+ CoercionError.handle(e, &block)
129
+ end
130
+ alias_method :[], :call
131
+
132
+ def arity
133
+ 2
134
+ end
135
+ end
136
+
119
137
  # Choose or build specialized invokation code for a callable
120
138
  #
121
139
  # @param [#call] fn
122
140
  # @return [Function]
123
141
  def self.[](fn)
124
- raise ArgumentError, 'Missing constructor block' if fn.nil?
142
+ raise ::ArgumentError, "Missing constructor block" if fn.nil?
125
143
 
126
144
  if fn.is_a?(Function)
127
145
  fn
146
+ elsif fn.respond_to?(:arity) && fn.arity.equal?(2)
147
+ Wrapper.new(fn)
128
148
  elsif fn.is_a?(::Method)
129
149
  MethodCall[fn, yields_block?(fn)]
130
150
  elsif yields_block?(fn)
@@ -146,7 +166,7 @@ module Dry
146
166
  last_arg.equal?(:block)
147
167
  end
148
168
 
149
- include Dry::Equalizer(:fn)
169
+ include ::Dry::Equalizer(:fn, immutable: true)
150
170
 
151
171
  attr_reader :fn
152
172
 
@@ -160,39 +180,34 @@ module Dry
160
180
  end
161
181
  alias_method :[], :call
162
182
 
183
+ # @return [Integer]
184
+ def arity
185
+ 1
186
+ end
187
+
188
+ def wrapper?
189
+ arity.equal?(2)
190
+ end
191
+
163
192
  # @return [Array]
164
193
  def to_ast
165
194
  if fn.is_a?(::Proc)
166
- [:id, Dry::Types::FnContainer.register(fn)]
195
+ [:id, FnContainer.register(fn)]
167
196
  else
168
197
  [:callable, fn]
169
198
  end
170
199
  end
171
200
 
172
- if RUBY_VERSION >= '2.6'
173
- # @return [Function]
174
- def >>(other)
175
- proc = other.is_a?(::Proc) ? other : other.fn
176
- Function[@fn >> proc]
177
- end
178
-
179
- # @return [Function]
180
- def <<(other)
181
- proc = other.is_a?(::Proc) ? other : other.fn
182
- Function[@fn << proc]
183
- end
184
- else
185
- # @return [Function]
186
- def >>(other)
187
- proc = other.is_a?(::Proc) ? other : other.fn
188
- Function[-> x { proc[@fn[x]] }]
189
- end
201
+ # @return [Function]
202
+ def >>(other)
203
+ f = Function[other]
204
+ Function[-> x, &b { f.(self.(x, &b), &b) }]
205
+ end
190
206
 
191
- # @return [Function]
192
- def <<(other)
193
- proc = other.is_a?(::Proc) ? other : other.fn
194
- Function[-> x { @fn[proc[x]] }]
195
- end
207
+ # @return [Function]
208
+ def <<(other)
209
+ f = Function[other]
210
+ Function[-> x, &b { self.(f.(x, &b), &b) }]
196
211
  end
197
212
  end
198
213
  end