dry-types 0.14.1 → 1.5.1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +631 -134
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +27 -30
  6. data/lib/dry/types/any.rb +32 -12
  7. data/lib/dry/types/array/constructor.rb +32 -0
  8. data/lib/dry/types/array/member.rb +75 -16
  9. data/lib/dry/types/array.rb +19 -6
  10. data/lib/dry/types/builder.rb +131 -15
  11. data/lib/dry/types/builder_methods.rb +49 -20
  12. data/lib/dry/types/coercions/json.rb +43 -7
  13. data/lib/dry/types/coercions/params.rb +118 -31
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/compat.rb +0 -2
  16. data/lib/dry/types/compiler.rb +56 -41
  17. data/lib/dry/types/constrained/coercible.rb +36 -6
  18. data/lib/dry/types/constrained.rb +81 -32
  19. data/lib/dry/types/constraints.rb +18 -4
  20. data/lib/dry/types/constructor/function.rb +216 -0
  21. data/lib/dry/types/constructor/wrapper.rb +94 -0
  22. data/lib/dry/types/constructor.rb +126 -56
  23. data/lib/dry/types/container.rb +7 -0
  24. data/lib/dry/types/core.rb +54 -21
  25. data/lib/dry/types/decorator.rb +38 -17
  26. data/lib/dry/types/default.rb +61 -16
  27. data/lib/dry/types/enum.rb +43 -20
  28. data/lib/dry/types/errors.rb +75 -9
  29. data/lib/dry/types/extensions/maybe.rb +74 -16
  30. data/lib/dry/types/extensions/monads.rb +29 -0
  31. data/lib/dry/types/extensions.rb +7 -1
  32. data/lib/dry/types/fn_container.rb +6 -1
  33. data/lib/dry/types/hash/constructor.rb +33 -0
  34. data/lib/dry/types/hash.rb +86 -67
  35. data/lib/dry/types/inflector.rb +3 -1
  36. data/lib/dry/types/json.rb +18 -16
  37. data/lib/dry/types/lax.rb +75 -0
  38. data/lib/dry/types/map.rb +76 -33
  39. data/lib/dry/types/meta.rb +51 -0
  40. data/lib/dry/types/module.rb +120 -0
  41. data/lib/dry/types/nominal.rb +210 -0
  42. data/lib/dry/types/options.rb +13 -26
  43. data/lib/dry/types/params.rb +39 -25
  44. data/lib/dry/types/predicate_inferrer.rb +238 -0
  45. data/lib/dry/types/predicate_registry.rb +34 -0
  46. data/lib/dry/types/primitive_inferrer.rb +97 -0
  47. data/lib/dry/types/printable.rb +16 -0
  48. data/lib/dry/types/printer.rb +315 -0
  49. data/lib/dry/types/result.rb +29 -3
  50. data/lib/dry/types/schema/key.rb +156 -0
  51. data/lib/dry/types/schema.rb +408 -0
  52. data/lib/dry/types/spec/types.rb +103 -33
  53. data/lib/dry/types/sum.rb +84 -35
  54. data/lib/dry/types/type.rb +49 -0
  55. data/lib/dry/types/version.rb +3 -1
  56. data/lib/dry/types.rb +156 -76
  57. data/lib/dry-types.rb +3 -1
  58. metadata +65 -78
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.travis.yml +0 -27
  62. data/.yardopts +0 -5
  63. data/CONTRIBUTING.md +0 -29
  64. data/Gemfile +0 -24
  65. data/Rakefile +0 -20
  66. data/benchmarks/hash_schemas.rb +0 -51
  67. data/lib/dry/types/compat/form_types.rb +0 -27
  68. data/lib/dry/types/compat/int.rb +0 -14
  69. data/lib/dry/types/definition.rb +0 -113
  70. data/lib/dry/types/hash/schema.rb +0 -199
  71. data/lib/dry/types/hash/schema_builder.rb +0 -75
  72. data/lib/dry/types/safe.rb +0 -59
  73. data/log/.gitkeep +0 -0
@@ -1,33 +1,35 @@
1
- require 'dry/types/coercions/json'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/coercions/json"
2
4
 
3
5
  module Dry
4
6
  module Types
5
- register('json.nil') do
6
- self['nil'].constructor(Coercions::JSON.method(:to_nil))
7
+ register("json.nil") do
8
+ self["nominal.nil"].constructor(Coercions::JSON.method(:to_nil))
7
9
  end
8
10
 
9
- register('json.date') do
10
- self['date'].constructor(Coercions::JSON.method(:to_date))
11
+ register("json.date") do
12
+ self["nominal.date"].constructor(Coercions::JSON.method(:to_date))
11
13
  end
12
14
 
13
- register('json.date_time') do
14
- self['date_time'].constructor(Coercions::JSON.method(:to_date_time))
15
+ register("json.date_time") do
16
+ self["nominal.date_time"].constructor(Coercions::JSON.method(:to_date_time))
15
17
  end
16
18
 
17
- register('json.time') do
18
- self['time'].constructor(Coercions::JSON.method(:to_time))
19
+ register("json.time") do
20
+ self["nominal.time"].constructor(Coercions::JSON.method(:to_time))
19
21
  end
20
22
 
21
- register('json.decimal') do
22
- self['decimal'].constructor(Coercions::JSON.method(:to_decimal))
23
+ register("json.decimal") do
24
+ self["nominal.decimal"].constructor(Coercions::JSON.method(:to_decimal))
23
25
  end
24
26
 
25
- register('json.array') do
26
- self['array'].safe
27
+ register("json.symbol") do
28
+ self["nominal.symbol"].constructor(Coercions::JSON.method(:to_symbol))
27
29
  end
28
30
 
29
- register('json.hash') do
30
- self['hash'].safe
31
- end
31
+ register("json.array") { self["array"] }
32
+
33
+ register("json.hash") { self["hash"] }
32
34
  end
33
35
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/types/decorator"
5
+
6
+ module Dry
7
+ module Types
8
+ # Lax types rescue from type-related errors when constructors fail
9
+ #
10
+ # @api public
11
+ class Lax
12
+ include Type
13
+ include Decorator
14
+ include Builder
15
+ include Printable
16
+ include Dry::Equalizer(:type, inspect: false, immutable: true)
17
+
18
+ undef :options, :constructor, :<<, :>>, :prepend, :append
19
+
20
+ # @param [Object] input
21
+ #
22
+ # @return [Object]
23
+ #
24
+ # @api public
25
+ def call(input)
26
+ type.call_safe(input) { |output = input| output }
27
+ end
28
+ alias_method :[], :call
29
+ alias_method :call_safe, :call
30
+ alias_method :call_unsafe, :call
31
+
32
+ # @param [Object] input
33
+ # @param [#call,nil] block
34
+ #
35
+ # @yieldparam [Failure] failure
36
+ # @yieldreturn [Result]
37
+ #
38
+ # @return [Result,Logic::Result]
39
+ #
40
+ # @api public
41
+ def try(input, &block)
42
+ type.try(input, &block)
43
+ end
44
+
45
+ # @see Nominal#to_ast
46
+ #
47
+ # @api public
48
+ def to_ast(meta: true)
49
+ [:lax, type.to_ast(meta: meta)]
50
+ end
51
+
52
+ # @return [Lax]
53
+ #
54
+ # @api public
55
+ def lax
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ # @param [Object, Dry::Types::Constructor] response
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ # @api private
66
+ def decorate?(response)
67
+ super || response.is_a?(type.constructor_type)
68
+ end
69
+ end
70
+
71
+ extend ::Dry::Core::Deprecations[:'dry-types']
72
+ Safe = Lax
73
+ deprecate_constant(:Safe)
74
+ end
75
+ end
data/lib/dry/types/map.rb CHANGED
@@ -1,91 +1,134 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
- class Map < Definition
4
- def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
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
23
+ class Map < Nominal
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
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
 
93
+ # @return [Boolean]
94
+ #
95
+ # @api public
96
+ def constrained?
97
+ value_type.constrained?
98
+ end
99
+
56
100
  private
57
101
 
102
+ # @api private
58
103
  def coerce(input)
59
- return failure(
60
- input, "#{input.inspect} must be an instance of #{primitive}"
61
- ) 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
109
+
110
+ output = {}
111
+ failures = []
62
112
 
63
- output, failures = {}, []
113
+ input.each do |k, v|
114
+ res_k = key_type.try(k)
115
+ res_v = value_type.try(v)
64
116
 
65
- input.each do |k,v|
66
- res_k = options[:key_type].try(k)
67
- res_v = options[:value_type].try(v)
68
117
  if res_k.failure?
69
- failures << "input key #{k.inspect} is invalid: #{res_k.error}"
118
+ failures << res_k.error
70
119
  elsif output.key?(res_k.input)
71
- failures << "duplicate coerced hash key #{res_k.input.inspect}"
120
+ failures << CoercionError.new("duplicate coerced hash key #{res_k.input.inspect}")
72
121
  elsif res_v.failure?
73
- failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
122
+ failures << res_v.error
74
123
  else
75
124
  output[res_k.input] = res_v.input
76
125
  end
77
126
  end
78
127
 
79
- return success(output) if failures.empty?
80
-
81
- failure(input, failures)
82
- end
83
-
84
- def validate_options!
85
- %i(key_type value_type).each do |opt|
86
- type = send(opt)
87
- next if type.is_a?(Type)
88
- 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))
89
132
  end
90
133
  end
91
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 options [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 = Undefined)
32
+ if Undefined.equal?(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
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/types/builder_methods"
5
+
6
+ module Dry
7
+ module Types
8
+ # Export types registered in a container as module constants.
9
+ # @example
10
+ # module Types
11
+ # include Dry::Types(:strict, :coercible, :nominal, default: :strict)
12
+ # end
13
+ #
14
+ # Types.constants
15
+ # # => [:Class, :Strict, :Symbol, :Integer, :Float, :String, :Array, :Hash,
16
+ # # :Decimal, :Nil, :True, :False, :Bool, :Date, :Nominal, :DateTime, :Range,
17
+ # # :Coercible, :Time]
18
+ #
19
+ # @api public
20
+ class Module < ::Module
21
+ def initialize(registry, *args, **kwargs)
22
+ @registry = registry
23
+ check_parameters(*args, **kwargs)
24
+ constants = type_constants(*args, **kwargs)
25
+ define_constants(constants)
26
+ extend(BuilderMethods)
27
+
28
+ if constants.key?(:Nominal)
29
+ singleton_class.send(:define_method, :included) do |base|
30
+ super(base)
31
+ base.instance_exec(const_get(:Nominal, false)) do |nominal|
32
+ extend Dry::Core::Deprecations[:'dry-types']
33
+ const_set(:Definition, nominal)
34
+ deprecate_constant(:Definition, message: "Nominal")
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # @api private
41
+ def type_constants(*namespaces, default: Undefined, **aliases)
42
+ if namespaces.empty? && aliases.empty? && Undefined.equal?(default)
43
+ default_ns = :Strict
44
+ elsif Undefined.equal?(default)
45
+ default_ns = Undefined
46
+ else
47
+ default_ns = Inflector.camelize(default).to_sym
48
+ end
49
+
50
+ tree = registry_tree
51
+
52
+ if namespaces.empty? && aliases.empty?
53
+ modules = tree.select { |_, v| v.is_a?(::Hash) }.map(&:first)
54
+ else
55
+ modules = (namespaces + aliases.keys).map { |n| Inflector.camelize(n).to_sym }
56
+ end
57
+
58
+ tree.each_with_object({}) do |(key, value), constants|
59
+ if modules.include?(key)
60
+ name = aliases.fetch(Inflector.underscore(key).to_sym, key)
61
+ constants[name] = value
62
+ end
63
+
64
+ constants.update(value) if key == default_ns
65
+ end
66
+ end
67
+
68
+ # @api private
69
+ def registry_tree
70
+ @registry_tree ||= @registry.keys.each_with_object({}) { |key, tree|
71
+ type = @registry[key]
72
+ *modules, const_name = key.split(".").map { |part|
73
+ Inflector.camelize(part).to_sym
74
+ }
75
+ next if modules.empty?
76
+
77
+ modules.reduce(tree) { |br, name| br[name] ||= {} }[const_name] = type
78
+ }.freeze
79
+ end
80
+
81
+ private
82
+
83
+ # @api private
84
+ def check_parameters(*namespaces, default: Undefined, **aliases)
85
+ referenced = namespaces.dup
86
+ referenced << default unless false.equal?(default) || Undefined.equal?(default)
87
+ referenced.concat(aliases.keys)
88
+
89
+ known = @registry.keys.map { |k|
90
+ ns, *path = k.split(".")
91
+ ns.to_sym unless path.empty?
92
+ }.compact.uniq
93
+
94
+ (referenced.uniq - known).each do |name|
95
+ raise ArgumentError,
96
+ "#{name.inspect} is not a known type namespace. "\
97
+ "Supported options are #{known.map(&:inspect).join(", ")}"
98
+ end
99
+ end
100
+
101
+ # @api private
102
+ def define_constants(constants, mod = self)
103
+ constants.each do |name, value|
104
+ case value
105
+ when ::Hash
106
+ if mod.const_defined?(name, false)
107
+ define_constants(value, mod.const_get(name, false))
108
+ else
109
+ m = ::Module.new
110
+ mod.const_set(name, m)
111
+ define_constants(value, m)
112
+ end
113
+ else
114
+ mod.const_set(name, value)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/core/equalizer"
5
+ require "dry/types/builder"
6
+ require "dry/types/result"
7
+ require "dry/types/options"
8
+ require "dry/types/meta"
9
+
10
+ module Dry
11
+ module Types
12
+ # Nominal types define a primitive class and do not apply any constructors or constraints
13
+ #
14
+ # Use these types for annotations and the base for building more complex types on top of them.
15
+ #
16
+ # @api public
17
+ class Nominal
18
+ include Type
19
+ include Options
20
+ include Meta
21
+ include Builder
22
+ include Printable
23
+ include Dry::Equalizer(:primitive, :options, :meta, inspect: false, immutable: true)
24
+
25
+ # @return [Class]
26
+ attr_reader :primitive
27
+
28
+ # @param [Class] primitive
29
+ #
30
+ # @return [Type]
31
+ #
32
+ # @api private
33
+ def self.[](primitive)
34
+ if primitive == ::Array
35
+ Types::Array
36
+ elsif primitive == ::Hash
37
+ Types::Hash
38
+ else
39
+ self
40
+ end
41
+ end
42
+
43
+ ALWAYS = proc { true }
44
+
45
+ # @param [Type,Class] primitive
46
+ # @param [Hash] options
47
+ #
48
+ # @api private
49
+ def initialize(primitive, **options)
50
+ super
51
+ @primitive = primitive
52
+ freeze
53
+ end
54
+
55
+ # @return [String]
56
+ #
57
+ # @api public
58
+ def name
59
+ primitive.name
60
+ end
61
+
62
+ # @return [false]
63
+ #
64
+ # @api public
65
+ def default?
66
+ false
67
+ end
68
+
69
+ # @return [false]
70
+ #
71
+ # @api public
72
+ def constrained?
73
+ false
74
+ end
75
+
76
+ # @return [false]
77
+ #
78
+ # @api public
79
+ def optional?
80
+ false
81
+ end
82
+
83
+ # @param [BasicObject] input
84
+ #
85
+ # @return [BasicObject]
86
+ #
87
+ # @api private
88
+ def call_unsafe(input)
89
+ input
90
+ end
91
+
92
+ # @param [BasicObject] input
93
+ #
94
+ # @return [BasicObject]
95
+ #
96
+ # @api private
97
+ def call_safe(input)
98
+ input
99
+ end
100
+
101
+ # @param [Object] input
102
+ #
103
+ # @yieldparam [Failure] failure
104
+ # @yieldreturn [Result]
105
+ #
106
+ # @return [Result,Logic::Result] when a block is not provided
107
+ # @return [nil] otherwise
108
+ #
109
+ # @api public
110
+ def try(input)
111
+ success(input)
112
+ end
113
+
114
+ # @param (see Dry::Types::Success#initialize)
115
+ #
116
+ # @return [Result::Success]
117
+ #
118
+ # @api public
119
+ def success(input)
120
+ Result::Success.new(input)
121
+ end
122
+
123
+ # @param (see Failure#initialize)
124
+ #
125
+ # @return [Result::Failure]
126
+ #
127
+ # @api public
128
+ def failure(input, error)
129
+ raise ArgumentError, "error must be a CoercionError" unless error.is_a?(CoercionError)
130
+
131
+ Result::Failure.new(input, error)
132
+ end
133
+
134
+ # Checks whether value is of a #primitive class
135
+ #
136
+ # @param [Object] value
137
+ #
138
+ # @return [Boolean]
139
+ #
140
+ # @api public
141
+ def primitive?(value)
142
+ value.is_a?(primitive)
143
+ end
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
173
+
174
+ # Return AST representation of a type nominal
175
+ #
176
+ # @return [Array]
177
+ #
178
+ # @api public
179
+ def to_ast(meta: true)
180
+ [:nominal, [primitive, meta ? self.meta : EMPTY_HASH]]
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
200
+ end
201
+
202
+ extend Dry::Core::Deprecations[:'dry-types']
203
+ Definition = Nominal
204
+ deprecate_constant(:Definition, message: "Nominal")
205
+ end
206
+ end
207
+
208
+ require "dry/types/array"
209
+ require "dry/types/hash"
210
+ require "dry/types/map"