dry-types 0.15.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +18 -2
  7. data/.travis.yml +10 -5
  8. data/.yardopts +6 -2
  9. data/CHANGELOG.md +186 -3
  10. data/Gemfile +11 -5
  11. data/README.md +4 -3
  12. data/Rakefile +4 -2
  13. data/benchmarks/hash_schemas.rb +10 -6
  14. data/benchmarks/lax_schema.rb +15 -0
  15. data/benchmarks/profile_invalid_input.rb +15 -0
  16. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  17. data/benchmarks/profile_valid_input.rb +15 -0
  18. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  19. data/benchmarks/setup.rb +17 -0
  20. data/docsite/source/array-with-member.html.md +13 -0
  21. data/docsite/source/built-in-types.html.md +116 -0
  22. data/docsite/source/constraints.html.md +31 -0
  23. data/docsite/source/custom-types.html.md +93 -0
  24. data/docsite/source/default-values.html.md +91 -0
  25. data/docsite/source/enum.html.md +69 -0
  26. data/docsite/source/getting-started.html.md +57 -0
  27. data/docsite/source/hash-schemas.html.md +169 -0
  28. data/docsite/source/index.html.md +155 -0
  29. data/docsite/source/map.html.md +17 -0
  30. data/docsite/source/optional-values.html.md +96 -0
  31. data/docsite/source/sum.html.md +21 -0
  32. data/dry-types.gemspec +21 -19
  33. data/lib/dry-types.rb +2 -0
  34. data/lib/dry/types.rb +60 -17
  35. data/lib/dry/types/any.rb +21 -10
  36. data/lib/dry/types/array.rb +17 -1
  37. data/lib/dry/types/array/constructor.rb +32 -0
  38. data/lib/dry/types/array/member.rb +72 -13
  39. data/lib/dry/types/builder.rb +49 -5
  40. data/lib/dry/types/builder_methods.rb +43 -16
  41. data/lib/dry/types/coercions.rb +84 -19
  42. data/lib/dry/types/coercions/json.rb +22 -3
  43. data/lib/dry/types/coercions/params.rb +98 -30
  44. data/lib/dry/types/compiler.rb +35 -12
  45. data/lib/dry/types/constrained.rb +78 -27
  46. data/lib/dry/types/constrained/coercible.rb +36 -6
  47. data/lib/dry/types/constraints.rb +15 -1
  48. data/lib/dry/types/constructor.rb +77 -62
  49. data/lib/dry/types/constructor/function.rb +200 -0
  50. data/lib/dry/types/container.rb +5 -0
  51. data/lib/dry/types/core.rb +35 -14
  52. data/lib/dry/types/decorator.rb +37 -10
  53. data/lib/dry/types/default.rb +48 -16
  54. data/lib/dry/types/enum.rb +31 -16
  55. data/lib/dry/types/errors.rb +73 -7
  56. data/lib/dry/types/extensions.rb +6 -0
  57. data/lib/dry/types/extensions/maybe.rb +52 -5
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/fn_container.rb +5 -0
  60. data/lib/dry/types/hash.rb +32 -14
  61. data/lib/dry/types/hash/constructor.rb +16 -3
  62. data/lib/dry/types/inflector.rb +2 -0
  63. data/lib/dry/types/json.rb +7 -5
  64. data/lib/dry/types/{safe.rb → lax.rb} +33 -16
  65. data/lib/dry/types/map.rb +70 -32
  66. data/lib/dry/types/meta.rb +51 -0
  67. data/lib/dry/types/module.rb +10 -5
  68. data/lib/dry/types/nominal.rb +105 -14
  69. data/lib/dry/types/options.rb +12 -25
  70. data/lib/dry/types/params.rb +14 -3
  71. data/lib/dry/types/predicate_inferrer.rb +197 -0
  72. data/lib/dry/types/predicate_registry.rb +34 -0
  73. data/lib/dry/types/primitive_inferrer.rb +97 -0
  74. data/lib/dry/types/printable.rb +5 -1
  75. data/lib/dry/types/printer.rb +70 -64
  76. data/lib/dry/types/result.rb +26 -0
  77. data/lib/dry/types/schema.rb +177 -80
  78. data/lib/dry/types/schema/key.rb +48 -35
  79. data/lib/dry/types/spec/types.rb +43 -6
  80. data/lib/dry/types/sum.rb +70 -21
  81. data/lib/dry/types/type.rb +49 -0
  82. data/lib/dry/types/version.rb +3 -1
  83. metadata +91 -62
@@ -1,50 +1,115 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Common coercion functions used by the built-in `Params` and `JSON` types
6
+ #
7
+ # @api public
3
8
  module Coercions
4
9
  include Dry::Core::Constants
5
10
 
6
11
  # @param [String, Object] input
7
- # @return [nil] if the input is an empty string
8
- # @return [Object] otherwise the input object is returned
9
- def to_nil(input)
10
- input unless empty_str?(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
11
26
  end
12
27
 
13
28
  # @param [#to_str, Object] input
29
+ #
14
30
  # @return [Date, Object]
31
+ #
15
32
  # @see Date.parse
16
- def to_date(input)
17
- return input unless input.respond_to?(:to_str)
18
- Date.parse(input)
19
- rescue ArgumentError, RangeError
20
- input
33
+ #
34
+ # @api public
35
+ def to_date(input, &block)
36
+ if input.respond_to?(:to_str)
37
+ begin
38
+ ::Date.parse(input)
39
+ rescue ArgumentError, RangeError => e
40
+ CoercionError.handle(e, &block)
41
+ end
42
+ elsif block_given?
43
+ yield
44
+ else
45
+ raise CoercionError, "#{input.inspect} is not a string"
46
+ end
21
47
  end
22
48
 
23
49
  # @param [#to_str, Object] input
50
+ #
24
51
  # @return [DateTime, Object]
52
+ #
25
53
  # @see DateTime.parse
26
- def to_date_time(input)
27
- return input unless input.respond_to?(:to_str)
28
- DateTime.parse(input)
29
- rescue ArgumentError
30
- input
54
+ #
55
+ # @api public
56
+ def to_date_time(input, &block)
57
+ if input.respond_to?(:to_str)
58
+ begin
59
+ ::DateTime.parse(input)
60
+ rescue ArgumentError => e
61
+ CoercionError.handle(e, &block)
62
+ end
63
+ elsif block_given?
64
+ yield
65
+ else
66
+ raise CoercionError, "#{input.inspect} is not a string"
67
+ end
31
68
  end
32
69
 
33
70
  # @param [#to_str, Object] input
71
+ #
34
72
  # @return [Time, Object]
73
+ #
35
74
  # @see Time.parse
36
- def to_time(input)
37
- return input unless input.respond_to?(:to_str)
38
- Time.parse(input)
39
- rescue ArgumentError
40
- input
75
+ #
76
+ # @api public
77
+ def to_time(input, &block)
78
+ if input.respond_to?(:to_str)
79
+ begin
80
+ ::Time.parse(input)
81
+ rescue ArgumentError => e
82
+ CoercionError.handle(e, &block)
83
+ end
84
+ elsif block_given?
85
+ yield
86
+ else
87
+ raise CoercionError, "#{input.inspect} is not a string"
88
+ end
89
+ end
90
+
91
+ # @param [#to_sym, Object] input
92
+ #
93
+ # @return [Symbol, Object]
94
+ #
95
+ # @raise CoercionError
96
+ #
97
+ # @api public
98
+ def to_symbol(input, &block)
99
+ input.to_sym
100
+ rescue NoMethodError => e
101
+ CoercionError.handle(e, &block)
41
102
  end
42
103
 
43
104
  private
44
105
 
45
106
  # Checks whether String is empty
107
+ #
46
108
  # @param [String, Object] value
109
+ #
47
110
  # @return [Boolean]
111
+ #
112
+ # @api private
48
113
  def empty_str?(value)
49
114
  EMPTY_STRING.eql?(value)
50
115
  end
@@ -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 => e
75
+ CoercionError.handle(e, &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 => e
88
+ CoercionError.handle(e, &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,145 @@
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]]
127
+ end
128
+
129
+ # @api private
130
+ def constructor_type
131
+ type.constructor_type
84
132
  end
85
133
 
86
134
  private
87
135
 
88
136
  # @param [Object] response
137
+ #
89
138
  # @return [Boolean]
139
+ #
140
+ # @api private
90
141
  def decorate?(response)
91
- super || response.kind_of?(Constructor)
142
+ super || response.is_a?(Constructor)
92
143
  end
93
144
  end
94
145
  end