dry-types 0.15.0 → 1.0.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +18 -2
  4. data/.travis.yml +4 -5
  5. data/.yardopts +6 -2
  6. data/CHANGELOG.md +69 -1
  7. data/Gemfile +3 -0
  8. data/README.md +2 -1
  9. data/Rakefile +2 -0
  10. data/benchmarks/hash_schemas.rb +2 -0
  11. data/benchmarks/lax_schema.rb +16 -0
  12. data/benchmarks/profile_invalid_input.rb +15 -0
  13. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  14. data/benchmarks/profile_valid_input.rb +15 -0
  15. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  16. data/benchmarks/setup.rb +17 -0
  17. data/dry-types.gemspec +4 -2
  18. data/lib/dry-types.rb +2 -0
  19. data/lib/dry/types.rb +51 -13
  20. data/lib/dry/types/any.rb +21 -10
  21. data/lib/dry/types/array.rb +11 -1
  22. data/lib/dry/types/array/member.rb +65 -13
  23. data/lib/dry/types/builder.rb +48 -4
  24. data/lib/dry/types/builder_methods.rb +9 -8
  25. data/lib/dry/types/coercions.rb +71 -19
  26. data/lib/dry/types/coercions/json.rb +22 -3
  27. data/lib/dry/types/coercions/params.rb +98 -30
  28. data/lib/dry/types/compiler.rb +35 -12
  29. data/lib/dry/types/constrained.rb +73 -27
  30. data/lib/dry/types/constrained/coercible.rb +36 -6
  31. data/lib/dry/types/constraints.rb +15 -1
  32. data/lib/dry/types/constructor.rb +90 -43
  33. data/lib/dry/types/constructor/function.rb +201 -0
  34. data/lib/dry/types/container.rb +5 -0
  35. data/lib/dry/types/core.rb +7 -5
  36. data/lib/dry/types/decorator.rb +36 -9
  37. data/lib/dry/types/default.rb +48 -16
  38. data/lib/dry/types/enum.rb +30 -16
  39. data/lib/dry/types/errors.rb +73 -7
  40. data/lib/dry/types/extensions.rb +2 -0
  41. data/lib/dry/types/extensions/maybe.rb +43 -4
  42. data/lib/dry/types/fn_container.rb +5 -0
  43. data/lib/dry/types/hash.rb +22 -3
  44. data/lib/dry/types/hash/constructor.rb +13 -0
  45. data/lib/dry/types/inflector.rb +2 -0
  46. data/lib/dry/types/json.rb +4 -6
  47. data/lib/dry/types/{safe.rb → lax.rb} +34 -17
  48. data/lib/dry/types/map.rb +63 -29
  49. data/lib/dry/types/meta.rb +51 -0
  50. data/lib/dry/types/module.rb +7 -2
  51. data/lib/dry/types/nominal.rb +105 -13
  52. data/lib/dry/types/options.rb +12 -25
  53. data/lib/dry/types/params.rb +5 -3
  54. data/lib/dry/types/printable.rb +5 -1
  55. data/lib/dry/types/printer.rb +58 -57
  56. data/lib/dry/types/result.rb +26 -0
  57. data/lib/dry/types/schema.rb +169 -66
  58. data/lib/dry/types/schema/key.rb +34 -39
  59. data/lib/dry/types/spec/types.rb +41 -1
  60. data/lib/dry/types/sum.rb +70 -21
  61. data/lib/dry/types/type.rb +49 -0
  62. data/lib/dry/types/version.rb +3 -1
  63. metadata +14 -12
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'bigdecimal'
3
5
  require 'bigdecimal/util'
@@ -6,14 +8,31 @@ require 'time'
6
8
  module Dry
7
9
  module Types
8
10
  module Coercions
11
+ # JSON-specific coercions
12
+ #
13
+ # @api public
9
14
  module JSON
10
15
  extend Coercions
11
16
 
12
17
  # @param [#to_d, Object] input
18
+ #
13
19
  # @return [BigDecimal,nil]
14
- def self.to_decimal(input)
15
- return if input.nil?
16
- input.to_d unless empty_str?(input)
20
+ #
21
+ # @raise CoercionError
22
+ #
23
+ # @api public
24
+ def self.to_decimal(input, &block)
25
+ if input.is_a?(::Float)
26
+ input.to_d
27
+ else
28
+ BigDecimal(input)
29
+ end
30
+ rescue ArgumentError, TypeError
31
+ if block_given?
32
+ yield
33
+ else
34
+ raise CoercionError, "#{input} cannot be coerced to decimal"
35
+ end
17
36
  end
18
37
  end
19
38
  end
@@ -1,80 +1,148 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bigdecimal'
2
4
  require 'bigdecimal/util'
3
5
 
4
6
  module Dry
5
7
  module Types
6
8
  module Coercions
9
+ # Params-specific coercions
10
+ #
11
+ # @api public
7
12
  module Params
8
13
  TRUE_VALUES = %w[1 on On ON t true True TRUE T y yes Yes YES Y].freeze
9
14
  FALSE_VALUES = %w[0 off Off OFF f false False FALSE F n no No NO N].freeze
10
- BOOLEAN_MAP = ::Hash[TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])].freeze
15
+ BOOLEAN_MAP = ::Hash[
16
+ TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])
17
+ ].freeze
11
18
 
12
19
  extend Coercions
13
20
 
14
21
  # @param [String, Object] input
22
+ #
15
23
  # @return [Boolean,Object]
24
+ #
16
25
  # @see TRUE_VALUES
17
26
  # @see FALSE_VALUES
18
- def self.to_true(input)
19
- BOOLEAN_MAP.fetch(input.to_s, input)
27
+ #
28
+ # @raise CoercionError
29
+ #
30
+ # @api public
31
+ def self.to_true(input, &_block)
32
+ BOOLEAN_MAP.fetch(input.to_s) do
33
+ if block_given?
34
+ yield
35
+ else
36
+ raise CoercionError, "#{input} cannot be coerced to true"
37
+ end
38
+ end
20
39
  end
21
40
 
22
41
  # @param [String, Object] input
42
+ #
23
43
  # @return [Boolean,Object]
44
+ #
24
45
  # @see TRUE_VALUES
25
46
  # @see FALSE_VALUES
26
- def self.to_false(input)
27
- BOOLEAN_MAP.fetch(input.to_s, input)
47
+ #
48
+ # @raise CoercionError
49
+ #
50
+ # @api public
51
+ def self.to_false(input, &_block)
52
+ BOOLEAN_MAP.fetch(input.to_s) do
53
+ if block_given?
54
+ yield
55
+ else
56
+ raise CoercionError, "#{input} cannot be coerced to false"
57
+ end
58
+ end
28
59
  end
29
60
 
30
61
  # @param [#to_int, #to_i, Object] input
62
+ #
31
63
  # @return [Integer, nil, Object]
32
- def self.to_int(input)
33
- if empty_str?(input)
34
- nil
35
- elsif input.is_a? String
64
+ #
65
+ # @raise CoercionError
66
+ #
67
+ # @api public
68
+ def self.to_int(input, &block)
69
+ if input.is_a? String
36
70
  Integer(input, 10)
37
71
  else
38
72
  Integer(input)
39
73
  end
40
- rescue ArgumentError, TypeError
41
- input
74
+ rescue ArgumentError, TypeError => error
75
+ CoercionError.handle(error, &block)
42
76
  end
43
77
 
44
78
  # @param [#to_f, Object] input
79
+ #
45
80
  # @return [Float, nil, Object]
46
- def self.to_float(input)
47
- if empty_str?(input)
48
- nil
49
- else
50
- Float(input)
51
- end
52
- rescue ArgumentError, TypeError
53
- input
81
+ #
82
+ # @raise CoercionError
83
+ #
84
+ # @api public
85
+ def self.to_float(input, &block)
86
+ Float(input)
87
+ rescue ArgumentError, TypeError => error
88
+ CoercionError.handle(error, &block)
54
89
  end
55
90
 
56
91
  # @param [#to_d, Object] input
92
+ #
57
93
  # @return [BigDecimal, nil, Object]
58
- def self.to_decimal(input)
59
- result = to_float(input)
60
-
61
- if result.instance_of?(Float)
62
- input.to_d
63
- else
64
- result
94
+ #
95
+ # @raise CoercionError
96
+ #
97
+ # @api public
98
+ def self.to_decimal(input, &block)
99
+ to_float(input) do
100
+ if block_given?
101
+ return yield
102
+ else
103
+ raise CoercionError, "#{input.inspect} cannot be coerced to decimal"
104
+ end
65
105
  end
106
+
107
+ input.to_d
66
108
  end
67
109
 
68
110
  # @param [Array, String, Object] input
111
+ #
69
112
  # @return [Array, Object]
70
- def self.to_ary(input)
71
- empty_str?(input) ? [] : input
113
+ #
114
+ # @raise CoercionError
115
+ #
116
+ # @api public
117
+ def self.to_ary(input, &_block)
118
+ if empty_str?(input)
119
+ []
120
+ elsif input.is_a?(::Array)
121
+ input
122
+ elsif block_given?
123
+ yield
124
+ else
125
+ raise CoercionError, "#{input.inspect} cannot be coerced to array"
126
+ end
72
127
  end
73
128
 
74
129
  # @param [Hash, String, Object] input
130
+ #
75
131
  # @return [Hash, Object]
76
- def self.to_hash(input)
77
- empty_str?(input) ? {} : input
132
+ #
133
+ # @raise CoercionError
134
+ #
135
+ # @api public
136
+ def self.to_hash(input, &_block)
137
+ if empty_str?(input)
138
+ {}
139
+ elsif input.is_a?(::Hash)
140
+ input
141
+ elsif block_given?
142
+ yield
143
+ else
144
+ raise CoercionError, "#{input.inspect} cannot be coerced to hash"
145
+ end
78
146
  end
79
147
  end
80
148
  end
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/deprecations'
4
+
1
5
  module Dry
2
6
  module Types
7
+ # @api private
3
8
  class Compiler
9
+ extend ::Dry::Core::Deprecations[:'dry-types']
10
+
4
11
  attr_reader :registry
5
12
 
6
13
  def initialize(registry)
@@ -13,29 +20,29 @@ module Dry
13
20
 
14
21
  def visit(node)
15
22
  type, body = node
16
- send(:"visit_#{ type }", body)
23
+ send(:"visit_#{type}", body)
17
24
  end
18
25
 
19
26
  def visit_constrained(node)
20
- nominal, rule, meta = node
21
- Types::Constrained.new(visit(nominal), rule: visit_rule(rule)).meta(meta)
27
+ nominal, rule = node
28
+ type = visit(nominal)
29
+ type.constrained_type.new(type, rule: visit_rule(rule))
22
30
  end
23
31
 
24
32
  def visit_constructor(node)
25
- nominal, fn_register_name, meta = node
26
- fn = Dry::Types::FnContainer[fn_register_name]
33
+ nominal, fn = node
27
34
  primitive = visit(nominal)
28
- Types::Constructor.new(primitive, meta: meta, fn: fn)
35
+ primitive.constructor(compile_fn(fn))
29
36
  end
30
37
 
31
- def visit_safe(node)
32
- ast, meta = node
33
- Types::Safe.new(visit(ast), meta: meta)
38
+ def visit_lax(node)
39
+ Types::Lax.new(visit(node))
34
40
  end
41
+ deprecate(:visit_safe, :visit_lax)
35
42
 
36
43
  def visit_nominal(node)
37
44
  type, meta = node
38
- nominal_name = "nominal.#{ Types.identifier(type) }"
45
+ nominal_name = "nominal.#{Types.identifier(type)}"
39
46
 
40
47
  if registry.registered?(nominal_name)
41
48
  registry[nominal_name].meta(meta)
@@ -95,8 +102,8 @@ module Dry
95
102
  end
96
103
 
97
104
  def visit_enum(node)
98
- type, mapping, meta = node
99
- Enum.new(visit(type), mapping: mapping, meta: meta)
105
+ type, mapping = node
106
+ Enum.new(visit(type), mapping: mapping)
100
107
  end
101
108
 
102
109
  def visit_map(node)
@@ -107,6 +114,22 @@ module Dry
107
114
  def visit_any(meta)
108
115
  registry['any'].meta(meta)
109
116
  end
117
+
118
+ def compile_fn(fn)
119
+ type, *node = fn
120
+
121
+ case type
122
+ when :id
123
+ Dry::Types::FnContainer[node.fetch(0)]
124
+ when :callable
125
+ node.fetch(0)
126
+ when :method
127
+ target, method = node
128
+ target.method(method)
129
+ else
130
+ raise ArgumentError, "Cannot build callable from #{fn.inspect}"
131
+ end
132
+ end
110
133
  end
111
134
  end
112
135
  end
@@ -1,94 +1,140 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/decorator'
2
4
  require 'dry/types/constraints'
3
5
  require 'dry/types/constrained/coercible'
4
6
 
5
7
  module Dry
6
8
  module Types
9
+ # Constrained types apply rules to the input
10
+ #
11
+ # @api public
7
12
  class Constrained
8
13
  include Type
9
14
  include Decorator
10
15
  include Builder
11
16
  include Printable
12
- include Dry::Equalizer(:type, :options, :rule, :meta, inspect: false)
17
+ include Dry::Equalizer(:type, :rule, inspect: false)
13
18
 
14
19
  # @return [Dry::Logic::Rule]
15
20
  attr_reader :rule
16
21
 
17
22
  # @param [Type] type
23
+ #
18
24
  # @param [Hash] options
25
+ #
26
+ # @api public
19
27
  def initialize(type, options)
20
28
  super
21
29
  @rule = options.fetch(:rule)
22
30
  end
23
31
 
24
- # @param [Object] input
32
+ # @api private
33
+ #
25
34
  # @return [Object]
26
- # @raise [ConstraintError]
27
- def call(input)
28
- try(input) { |result|
35
+ #
36
+ # @api public
37
+ def call_unsafe(input)
38
+ result = rule.(input)
39
+
40
+ if result.success?
41
+ type.call_unsafe(input)
42
+ else
29
43
  raise ConstraintError.new(result, input)
30
- }.input
44
+ end
31
45
  end
32
- alias_method :[], :call
33
-
34
- # @param [Object] input
35
- # @param [#call,nil] block
36
- # @yieldparam [Failure] failure
37
- # @yieldreturn [Result]
38
- # @return [Logic::Result, Result]
39
- # @return [Object] if block given and try fails
46
+
47
+ # @api private
48
+ #
49
+ # @return [Object]
50
+ #
51
+ # @api public
52
+ def call_safe(input, &block)
53
+ if rule[input]
54
+ type.call_safe(input, &block)
55
+ else
56
+ yield
57
+ end
58
+ end
59
+
60
+ # Safe coercion attempt. It is similar to #call with a
61
+ # block given but returns a Result instance with metadata
62
+ # about errors (if any).
63
+ #
64
+ # @overload try(input)
65
+ # @param [Object] input
66
+ # @return [Logic::Result]
67
+ #
68
+ # @overload try(input)
69
+ # @param [Object] input
70
+ # @yieldparam [Failure] failure
71
+ # @yieldreturn [Object]
72
+ # @return [Object]
73
+ #
74
+ # @api public
40
75
  def try(input, &block)
41
76
  result = rule.(input)
42
77
 
43
78
  if result.success?
44
79
  type.try(input, &block)
45
80
  else
46
- failure = failure(input, result)
47
- block ? yield(failure) : failure
81
+ failure = failure(input, ConstraintError.new(result, input))
82
+ block_given? ? yield(failure) : failure
48
83
  end
49
84
  end
50
85
 
51
- # @param [Object] value
52
- # @return [Boolean]
53
- def valid?(value)
54
- rule.(value).success? && type.valid?(value)
55
- end
56
-
57
86
  # @param [Hash] options
58
87
  # The options hash provided to {Types.Rule} and combined
59
88
  # using {&} with previous {#rule}
89
+ #
60
90
  # @return [Constrained]
91
+ #
61
92
  # @see Dry::Logic::Operators#and
93
+ #
94
+ # @api public
62
95
  def constrained(options)
63
96
  with(rule: rule & Types.Rule(options))
64
97
  end
65
98
 
66
99
  # @return [true]
100
+ #
101
+ # @api public
67
102
  def constrained?
68
103
  true
69
104
  end
70
105
 
71
106
  # @param [Object] value
107
+ #
72
108
  # @return [Boolean]
109
+ #
110
+ # @api public
73
111
  def ===(value)
74
112
  valid?(value)
75
113
  end
76
114
 
77
- # @api public
115
+ # Build lax type. Constraints are not applicable to lax types hence unwrapping
78
116
  #
117
+ # @return [Lax]
118
+ # @api public
119
+ def lax
120
+ type.lax
121
+ end
122
+
79
123
  # @see Nominal#to_ast
124
+ # @api public
80
125
  def to_ast(meta: true)
81
- [:constrained, [type.to_ast(meta: meta),
82
- rule.to_ast,
83
- meta ? self.meta : EMPTY_HASH]]
126
+ [:constrained, [type.to_ast(meta: meta), rule.to_ast]]
84
127
  end
85
128
 
86
129
  private
87
130
 
88
131
  # @param [Object] response
132
+ #
89
133
  # @return [Boolean]
134
+ #
135
+ # @api private
90
136
  def decorate?(response)
91
- super || response.kind_of?(Constructor)
137
+ super || response.is_a?(Constructor)
92
138
  end
93
139
  end
94
140
  end