dry-types 0.15.0 → 1.5.1

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +547 -161
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +27 -30
  6. data/lib/dry/types/any.rb +23 -12
  7. data/lib/dry/types/array/constructor.rb +32 -0
  8. data/lib/dry/types/array/member.rb +74 -15
  9. data/lib/dry/types/array.rb +18 -2
  10. data/lib/dry/types/builder.rb +118 -22
  11. data/lib/dry/types/builder_methods.rb +46 -16
  12. data/lib/dry/types/coercions/json.rb +43 -7
  13. data/lib/dry/types/coercions/params.rb +117 -32
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/compiler.rb +44 -21
  16. data/lib/dry/types/constrained/coercible.rb +36 -6
  17. data/lib/dry/types/constrained.rb +79 -31
  18. data/lib/dry/types/constraints.rb +18 -4
  19. data/lib/dry/types/constructor/function.rb +216 -0
  20. data/lib/dry/types/constructor/wrapper.rb +94 -0
  21. data/lib/dry/types/constructor.rb +110 -61
  22. data/lib/dry/types/container.rb +6 -1
  23. data/lib/dry/types/core.rb +34 -11
  24. data/lib/dry/types/decorator.rb +38 -17
  25. data/lib/dry/types/default.rb +61 -16
  26. data/lib/dry/types/enum.rb +36 -20
  27. data/lib/dry/types/errors.rb +74 -8
  28. data/lib/dry/types/extensions/maybe.rb +65 -17
  29. data/lib/dry/types/extensions/monads.rb +29 -0
  30. data/lib/dry/types/extensions.rb +7 -1
  31. data/lib/dry/types/fn_container.rb +6 -1
  32. data/lib/dry/types/hash/constructor.rb +17 -4
  33. data/lib/dry/types/hash.rb +32 -20
  34. data/lib/dry/types/inflector.rb +3 -1
  35. data/lib/dry/types/json.rb +18 -16
  36. data/lib/dry/types/lax.rb +75 -0
  37. data/lib/dry/types/map.rb +70 -32
  38. data/lib/dry/types/meta.rb +51 -0
  39. data/lib/dry/types/module.rb +16 -11
  40. data/lib/dry/types/nominal.rb +113 -22
  41. data/lib/dry/types/options.rb +12 -25
  42. data/lib/dry/types/params.rb +39 -25
  43. data/lib/dry/types/predicate_inferrer.rb +238 -0
  44. data/lib/dry/types/predicate_registry.rb +34 -0
  45. data/lib/dry/types/primitive_inferrer.rb +97 -0
  46. data/lib/dry/types/printable.rb +5 -1
  47. data/lib/dry/types/printer.rb +63 -57
  48. data/lib/dry/types/result.rb +29 -3
  49. data/lib/dry/types/schema/key.rb +62 -36
  50. data/lib/dry/types/schema.rb +201 -91
  51. data/lib/dry/types/spec/types.rb +99 -37
  52. data/lib/dry/types/sum.rb +75 -25
  53. data/lib/dry/types/type.rb +49 -0
  54. data/lib/dry/types/version.rb +3 -1
  55. data/lib/dry/types.rb +106 -48
  56. data/lib/dry-types.rb +3 -1
  57. metadata +55 -78
  58. data/.codeclimate.yml +0 -15
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.rubocop.yml +0 -43
  62. data/.travis.yml +0 -28
  63. data/.yardopts +0 -5
  64. data/CONTRIBUTING.md +0 -29
  65. data/Gemfile +0 -23
  66. data/Rakefile +0 -20
  67. data/benchmarks/hash_schemas.rb +0 -51
  68. data/lib/dry/types/safe.rb +0 -61
  69. data/log/.gitkeep +0 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/types/decorator"
5
+
6
+ module Dry
7
+ module Types
8
+ # Lax types rescue from type-related errors when constructors fail
9
+ #
10
+ # @api public
11
+ class Lax
12
+ include Type
13
+ include Decorator
14
+ include Builder
15
+ include Printable
16
+ include Dry::Equalizer(:type, inspect: false, immutable: true)
17
+
18
+ undef :options, :constructor, :<<, :>>, :prepend, :append
19
+
20
+ # @param [Object] input
21
+ #
22
+ # @return [Object]
23
+ #
24
+ # @api public
25
+ def call(input)
26
+ type.call_safe(input) { |output = input| output }
27
+ end
28
+ alias_method :[], :call
29
+ alias_method :call_safe, :call
30
+ alias_method :call_unsafe, :call
31
+
32
+ # @param [Object] input
33
+ # @param [#call,nil] block
34
+ #
35
+ # @yieldparam [Failure] failure
36
+ # @yieldreturn [Result]
37
+ #
38
+ # @return [Result,Logic::Result]
39
+ #
40
+ # @api public
41
+ def try(input, &block)
42
+ type.try(input, &block)
43
+ end
44
+
45
+ # @see Nominal#to_ast
46
+ #
47
+ # @api public
48
+ def to_ast(meta: true)
49
+ [:lax, type.to_ast(meta: meta)]
50
+ end
51
+
52
+ # @return [Lax]
53
+ #
54
+ # @api public
55
+ def lax
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ # @param [Object, Dry::Types::Constructor] response
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ # @api private
66
+ def decorate?(response)
67
+ super || response.is_a?(type.constructor_type)
68
+ end
69
+ end
70
+
71
+ extend ::Dry::Core::Deprecations[:'dry-types']
72
+ Safe = Lax
73
+ deprecate_constant(:Safe)
74
+ end
75
+ end
data/lib/dry/types/map.rb CHANGED
@@ -1,96 +1,134 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Homogeneous mapping. It describes a hash with unknown keys that match a certain type.
6
+ #
7
+ # @example
8
+ # type = Dry::Types['hash'].map(
9
+ # Dry::Types['integer'].constrained(gteq: 1, lteq: 10),
10
+ # Dry::Types['string']
11
+ # )
12
+ #
13
+ # type.(1 => 'right')
14
+ # # => {1 => 'right'}
15
+ #
16
+ # type.('1' => 'wrong')
17
+ # # Dry::Types::MapError: "1" violates constraints (type?(Integer, "1") AND gteq?(1, "1") AND lteq?(10, "1") failed)
18
+ #
19
+ # type.(11 => 'wrong')
20
+ # # Dry::Types::MapError: 11 violates constraints (lteq?(10, 11) failed)
21
+ #
22
+ # @api public
3
23
  class Map < Nominal
4
- def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
24
+ def initialize(_primitive, key_type: Types["any"], value_type: Types["any"], meta: EMPTY_HASH)
5
25
  super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
6
- validate_options!
7
26
  end
8
27
 
9
28
  # @return [Type]
29
+ #
30
+ # @api public
10
31
  def key_type
11
32
  options[:key_type]
12
33
  end
13
34
 
14
35
  # @return [Type]
36
+ #
37
+ # @api public
15
38
  def value_type
16
39
  options[:value_type]
17
40
  end
18
41
 
19
42
  # @return [String]
43
+ #
44
+ # @api public
20
45
  def name
21
46
  "Map"
22
47
  end
23
48
 
24
49
  # @param [Hash] hash
50
+ #
25
51
  # @return [Hash]
26
- def call(hash)
27
- try(hash) do |failure|
28
- raise MapError, failure.error.join("\n")
29
- end.input
52
+ #
53
+ # @api private
54
+ def call_unsafe(hash)
55
+ try(hash) { |failure|
56
+ raise MapError, failure.error.message
57
+ }.input
30
58
  end
31
- alias_method :[], :call
32
59
 
33
60
  # @param [Hash] hash
34
- # @return [Boolean]
35
- def valid?(hash)
36
- coerce(hash).success?
61
+ #
62
+ # @return [Hash]
63
+ #
64
+ # @api private
65
+ def call_safe(hash)
66
+ try(hash) { return yield }.input
37
67
  end
38
- alias_method :===, :valid?
39
68
 
40
69
  # @param [Hash] hash
70
+ #
41
71
  # @return [Result]
72
+ #
73
+ # @api public
42
74
  def try(hash)
43
75
  result = coerce(hash)
44
76
  return result if result.success? || !block_given?
77
+
45
78
  yield(result)
46
79
  end
47
80
 
48
81
  # @param meta [Boolean] Whether to dump the meta to the AST
82
+ #
49
83
  # @return [Array] An AST representation
84
+ #
85
+ # @api public
50
86
  def to_ast(meta: true)
51
87
  [:map,
52
- [key_type.to_ast(meta: true), value_type.to_ast(meta: true),
88
+ [key_type.to_ast(meta: true),
89
+ value_type.to_ast(meta: true),
53
90
  meta ? self.meta : EMPTY_HASH]]
54
91
  end
55
92
 
56
93
  # @return [Boolean]
94
+ #
95
+ # @api public
57
96
  def constrained?
58
97
  value_type.constrained?
59
98
  end
60
99
 
61
100
  private
62
101
 
102
+ # @api private
63
103
  def coerce(input)
64
- return failure(
65
- input, "#{input.inspect} must be an instance of #{primitive}"
66
- ) unless primitive?(input)
104
+ unless primitive?(input)
105
+ return failure(
106
+ input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
107
+ )
108
+ end
67
109
 
68
- output, failures = {}, []
110
+ output = {}
111
+ failures = []
112
+
113
+ input.each do |k, v|
114
+ res_k = key_type.try(k)
115
+ res_v = value_type.try(v)
69
116
 
70
- input.each do |k,v|
71
- res_k = options[:key_type].try(k)
72
- res_v = options[:value_type].try(v)
73
117
  if res_k.failure?
74
- failures << "input key #{k.inspect} is invalid: #{res_k.error}"
118
+ failures << res_k.error
75
119
  elsif output.key?(res_k.input)
76
- failures << "duplicate coerced hash key #{res_k.input.inspect}"
120
+ failures << CoercionError.new("duplicate coerced hash key #{res_k.input.inspect}")
77
121
  elsif res_v.failure?
78
- failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
122
+ failures << res_v.error
79
123
  else
80
124
  output[res_k.input] = res_v.input
81
125
  end
82
126
  end
83
127
 
84
- return success(output) if failures.empty?
85
-
86
- failure(input, failures)
87
- end
88
-
89
- def validate_options!
90
- %i(key_type value_type).each do |opt|
91
- type = send(opt)
92
- next if type.is_a?(Type)
93
- raise MapError, ":#{opt} must be a #{Type}, got: #{type.inspect}"
128
+ if failures.empty?
129
+ success(output)
130
+ else
131
+ failure(input, MultipleError.new(failures))
94
132
  end
95
133
  end
96
134
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # Storage for meta-data
6
+ #
7
+ # @api public
8
+ module Meta
9
+ def initialize(*args, meta: EMPTY_HASH, **options)
10
+ super(*args, **options)
11
+ @meta = meta.freeze
12
+ end
13
+
14
+ # @param options [Hash] new_options
15
+ #
16
+ # @return [Type]
17
+ #
18
+ # @api public
19
+ def with(**options)
20
+ super(meta: @meta, **options)
21
+ end
22
+
23
+ # @overload meta
24
+ # @return [Hash] metadata associated with type
25
+ #
26
+ # @overload meta(data)
27
+ # @param [Hash] new metadata to merge into existing metadata
28
+ # @return [Type] new type with added metadata
29
+ #
30
+ # @api public
31
+ def meta(data = Undefined)
32
+ if Undefined.equal?(data)
33
+ @meta
34
+ elsif data.empty?
35
+ self
36
+ else
37
+ with(meta: @meta.merge(data))
38
+ end
39
+ end
40
+
41
+ # Resets meta
42
+ #
43
+ # @return [Dry::Types::Type]
44
+ #
45
+ # @api public
46
+ def pristine
47
+ with(meta: EMPTY_HASH)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,22 +1,27 @@
1
- require 'dry/core/deprecations'
2
- require 'dry/types/builder_methods'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/types/builder_methods"
3
5
 
4
6
  module Dry
5
7
  module Types
6
8
  # Export types registered in a container as module constants.
7
9
  # @example
8
10
  # module Types
9
- # include Dry::Types.module(:strict, :coercible, :nominal, default: :strict)
11
+ # include Dry::Types(:strict, :coercible, :nominal, default: :strict)
10
12
  # end
11
- # # Types.constants
13
+ #
14
+ # Types.constants
12
15
  # # => [:Class, :Strict, :Symbol, :Integer, :Float, :String, :Array, :Hash,
13
16
  # # :Decimal, :Nil, :True, :False, :Bool, :Date, :Nominal, :DateTime, :Range,
14
17
  # # :Coercible, :Time]
18
+ #
19
+ # @api public
15
20
  class Module < ::Module
16
- def initialize(registry, *args)
21
+ def initialize(registry, *args, **kwargs)
17
22
  @registry = registry
18
- check_parameters(*args)
19
- constants = type_constants(*args)
23
+ check_parameters(*args, **kwargs)
24
+ constants = type_constants(*args, **kwargs)
20
25
  define_constants(constants)
21
26
  extend(BuilderMethods)
22
27
 
@@ -64,7 +69,7 @@ module Dry
64
69
  def registry_tree
65
70
  @registry_tree ||= @registry.keys.each_with_object({}) { |key, tree|
66
71
  type = @registry[key]
67
- *modules, const_name = key.split('.').map { |part|
72
+ *modules, const_name = key.split(".").map { |part|
68
73
  Inflector.camelize(part).to_sym
69
74
  }
70
75
  next if modules.empty?
@@ -82,14 +87,14 @@ module Dry
82
87
  referenced.concat(aliases.keys)
83
88
 
84
89
  known = @registry.keys.map { |k|
85
- ns, *path = k.split('.')
90
+ ns, *path = k.split(".")
86
91
  ns.to_sym unless path.empty?
87
92
  }.compact.uniq
88
93
 
89
94
  (referenced.uniq - known).each do |name|
90
95
  raise ArgumentError,
91
- "#{ name.inspect } is not a known type namespace. "\
92
- "Supported options are #{ known.map(&:inspect).join(', ') }"
96
+ "#{name.inspect} is not a known type namespace. "\
97
+ "Supported options are #{known.map(&:inspect).join(", ")}"
93
98
  end
94
99
  end
95
100
 
@@ -1,22 +1,35 @@
1
- require 'dry/core/deprecations'
2
- require 'dry/types/builder'
3
- require 'dry/types/result'
4
- require 'dry/types/options'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/core/equalizer"
5
+ require "dry/types/builder"
6
+ require "dry/types/result"
7
+ require "dry/types/options"
8
+ require "dry/types/meta"
5
9
 
6
10
  module Dry
7
11
  module Types
12
+ # Nominal types define a primitive class and do not apply any constructors or constraints
13
+ #
14
+ # Use these types for annotations and the base for building more complex types on top of them.
15
+ #
16
+ # @api public
8
17
  class Nominal
9
18
  include Type
10
19
  include Options
20
+ include Meta
11
21
  include Builder
12
22
  include Printable
13
- include Dry::Equalizer(:primitive, :options, :meta, inspect: false)
23
+ include Dry::Equalizer(:primitive, :options, :meta, inspect: false, immutable: true)
14
24
 
15
25
  # @return [Class]
16
26
  attr_reader :primitive
17
27
 
18
28
  # @param [Class] primitive
29
+ #
19
30
  # @return [Type]
31
+ #
32
+ # @api private
20
33
  def self.[](primitive)
21
34
  if primitive == ::Array
22
35
  Types::Array
@@ -27,8 +40,12 @@ module Dry
27
40
  end
28
41
  end
29
42
 
43
+ ALWAYS = proc { true }
44
+
30
45
  # @param [Type,Class] primitive
31
46
  # @param [Hash] options
47
+ #
48
+ # @api private
32
49
  def initialize(primitive, **options)
33
50
  super
34
51
  @primitive = primitive
@@ -36,76 +53,150 @@ module Dry
36
53
  end
37
54
 
38
55
  # @return [String]
56
+ #
57
+ # @api public
39
58
  def name
40
59
  primitive.name
41
60
  end
42
61
 
43
62
  # @return [false]
63
+ #
64
+ # @api public
44
65
  def default?
45
66
  false
46
67
  end
47
68
 
48
69
  # @return [false]
70
+ #
71
+ # @api public
49
72
  def constrained?
50
73
  false
51
74
  end
52
75
 
53
76
  # @return [false]
77
+ #
78
+ # @api public
54
79
  def optional?
55
80
  false
56
81
  end
57
82
 
58
83
  # @param [BasicObject] input
84
+ #
85
+ # @return [BasicObject]
86
+ #
87
+ # @api private
88
+ def call_unsafe(input)
89
+ input
90
+ end
91
+
92
+ # @param [BasicObject] input
93
+ #
59
94
  # @return [BasicObject]
60
- def call(input)
95
+ #
96
+ # @api private
97
+ def call_safe(input)
61
98
  input
62
99
  end
63
- alias_method :[], :call
64
100
 
65
101
  # @param [Object] input
66
- # @param [#call,nil] block
102
+ #
67
103
  # @yieldparam [Failure] failure
68
104
  # @yieldreturn [Result]
105
+ #
69
106
  # @return [Result,Logic::Result] when a block is not provided
70
107
  # @return [nil] otherwise
71
- def try(input, &block)
72
- if valid?(input)
73
- success(input)
74
- else
75
- failure = failure(input, "#{input.inspect} must be an instance of #{primitive}")
76
- block ? yield(failure) : failure
77
- end
108
+ #
109
+ # @api public
110
+ def try(input)
111
+ success(input)
78
112
  end
79
113
 
80
114
  # @param (see Dry::Types::Success#initialize)
115
+ #
81
116
  # @return [Result::Success]
117
+ #
118
+ # @api public
82
119
  def success(input)
83
120
  Result::Success.new(input)
84
121
  end
85
122
 
86
123
  # @param (see Failure#initialize)
124
+ #
87
125
  # @return [Result::Failure]
126
+ #
127
+ # @api public
88
128
  def failure(input, error)
129
+ raise ArgumentError, "error must be a CoercionError" unless error.is_a?(CoercionError)
130
+
89
131
  Result::Failure.new(input, error)
90
132
  end
91
133
 
92
134
  # Checks whether value is of a #primitive class
135
+ #
93
136
  # @param [Object] value
137
+ #
94
138
  # @return [Boolean]
139
+ #
140
+ # @api public
95
141
  def primitive?(value)
96
142
  value.is_a?(primitive)
97
143
  end
98
- alias_method :valid?, :primitive?
99
- alias_method :===, :primitive?
144
+
145
+ # @api private
146
+ def coerce(input, &_block)
147
+ if primitive?(input)
148
+ input
149
+ elsif block_given?
150
+ yield
151
+ else
152
+ raise CoercionError, "#{input.inspect} must be an instance of #{primitive}"
153
+ end
154
+ end
155
+
156
+ # @api private
157
+ def try_coerce(input)
158
+ result = success(input)
159
+
160
+ coerce(input) do
161
+ result = failure(
162
+ input,
163
+ CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
164
+ )
165
+ end
166
+
167
+ if block_given?
168
+ yield(result)
169
+ else
170
+ result
171
+ end
172
+ end
100
173
 
101
174
  # Return AST representation of a type nominal
102
175
  #
103
- # @api public
104
- #
105
176
  # @return [Array]
177
+ #
178
+ # @api public
106
179
  def to_ast(meta: true)
107
180
  [:nominal, [primitive, meta ? self.meta : EMPTY_HASH]]
108
181
  end
182
+
183
+ # Return self. Nominal types are lax by definition
184
+ #
185
+ # @return [Nominal]
186
+ #
187
+ # @api public
188
+ def lax
189
+ self
190
+ end
191
+
192
+ # Wrap the type with a proc
193
+ #
194
+ # @return [Proc]
195
+ #
196
+ # @api public
197
+ def to_proc
198
+ ALWAYS
199
+ end
109
200
  end
110
201
 
111
202
  extend Dry::Core::Deprecations[:'dry-types']
@@ -114,6 +205,6 @@ module Dry
114
205
  end
115
206
  end
116
207
 
117
- require 'dry/types/array'
118
- require 'dry/types/hash'
119
- require 'dry/types/map'
208
+ require "dry/types/array"
209
+ require "dry/types/hash"
210
+ require "dry/types/map"
@@ -1,42 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Common API for types with options
6
+ #
7
+ # @api private
3
8
  module Options
4
9
  # @return [Hash]
5
10
  attr_reader :options
6
11
 
7
12
  # @see Nominal#initialize
8
- def initialize(*args, meta: EMPTY_HASH, **options)
13
+ #
14
+ # @api private
15
+ def initialize(*args, **options)
9
16
  @__args__ = args.freeze
10
17
  @options = options.freeze
11
- @meta = meta.freeze
12
18
  end
13
19
 
14
20
  # @param [Hash] new_options
21
+ #
15
22
  # @return [Type]
16
- def with(**new_options)
17
- self.class.new(*@__args__, **options, meta: @meta, **new_options)
18
- end
19
-
20
- # @overload meta
21
- # @return [Hash] metadata associated with type
22
23
  #
23
- # @overload meta(data)
24
- # @param [Hash] new metadata to merge into existing metadata
25
- # @return [Type] new type with added metadata
26
- def meta(data = nil)
27
- if !data
28
- @meta
29
- elsif data.empty?
30
- self
31
- else
32
- with(meta: @meta.merge(data))
33
- end
34
- end
35
-
36
- # Resets meta
37
- # @return [Dry::Types::Type]
38
- def pristine
39
- with(meta: EMPTY_HASH)
24
+ # @api private
25
+ def with(**new_options)
26
+ self.class.new(*@__args__, **options, **new_options)
40
27
  end
41
28
  end
42
29
  end