dry-types 0.15.0 → 1.2.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 (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