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
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
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
- "Map"
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 [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 = nil)
32
+ if !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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/deprecations'
2
4
  require 'dry/types/builder_methods'
3
5
 
@@ -6,12 +8,15 @@ module Dry
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
21
  def initialize(registry, *args)
17
22
  @registry = registry
@@ -26,7 +31,7 @@ module Dry
26
31
  base.instance_exec(const_get(:Nominal, false)) do |nominal|
27
32
  extend Dry::Core::Deprecations[:'dry-types']
28
33
  const_set(:Definition, nominal)
29
- deprecate_constant(:Definition, message: "Nominal")
34
+ deprecate_constant(:Definition, message: 'Nominal')
30
35
  end
31
36
  end
32
37
  end
@@ -88,8 +93,8 @@ module Dry
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,13 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/deprecations'
2
4
  require 'dry/types/builder'
3
5
  require 'dry/types/result'
4
6
  require 'dry/types/options'
7
+ require 'dry/types/meta'
5
8
 
6
9
  module Dry
7
10
  module Types
11
+ # Nominal types define a primitive class and do not apply any constructors or constraints
12
+ #
13
+ # Use these types for annotations and the base for building more complex types on top of them.
14
+ #
15
+ # @api public
8
16
  class Nominal
9
17
  include Type
10
18
  include Options
19
+ include Meta
11
20
  include Builder
12
21
  include Printable
13
22
  include Dry::Equalizer(:primitive, :options, :meta, inspect: false)
@@ -16,7 +25,10 @@ module Dry
16
25
  attr_reader :primitive
17
26
 
18
27
  # @param [Class] primitive
28
+ #
19
29
  # @return [Type]
30
+ #
31
+ # @api private
20
32
  def self.[](primitive)
21
33
  if primitive == ::Array
22
34
  Types::Array
@@ -27,8 +39,12 @@ module Dry
27
39
  end
28
40
  end
29
41
 
42
+ ALWAYS = proc { true }
43
+
30
44
  # @param [Type,Class] primitive
31
45
  # @param [Hash] options
46
+ #
47
+ # @api private
32
48
  def initialize(primitive, **options)
33
49
  super
34
50
  @primitive = primitive
@@ -36,81 +52,156 @@ module Dry
36
52
  end
37
53
 
38
54
  # @return [String]
55
+ #
56
+ # @api public
39
57
  def name
40
58
  primitive.name
41
59
  end
42
60
 
43
61
  # @return [false]
62
+ #
63
+ # @api public
44
64
  def default?
45
65
  false
46
66
  end
47
67
 
48
68
  # @return [false]
69
+ #
70
+ # @api public
49
71
  def constrained?
50
72
  false
51
73
  end
52
74
 
53
75
  # @return [false]
76
+ #
77
+ # @api public
54
78
  def optional?
55
79
  false
56
80
  end
57
81
 
58
82
  # @param [BasicObject] input
83
+ #
84
+ # @return [BasicObject]
85
+ #
86
+ # @api private
87
+ def call_unsafe(input)
88
+ input
89
+ end
90
+
91
+ # @param [BasicObject] input
92
+ #
59
93
  # @return [BasicObject]
60
- def call(input)
94
+ #
95
+ # @api private
96
+ def call_safe(input)
61
97
  input
62
98
  end
63
- alias_method :[], :call
64
99
 
65
100
  # @param [Object] input
66
101
  # @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']
112
203
  Definition = Nominal
113
- deprecate_constant(:Definition, message: "Nominal")
204
+ deprecate_constant(:Definition, message: 'Nominal')
114
205
  end
115
206
  end
116
207