dry-types 0.15.0 → 1.0.0

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