dry-types 0.15.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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