dry-types 0.13.2 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +763 -233
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +28 -28
  6. data/lib/dry-types.rb +3 -1
  7. data/lib/dry/types.rb +156 -76
  8. data/lib/dry/types/any.rb +32 -12
  9. data/lib/dry/types/array.rb +19 -6
  10. data/lib/dry/types/array/constructor.rb +32 -0
  11. data/lib/dry/types/array/member.rb +75 -16
  12. data/lib/dry/types/builder.rb +131 -15
  13. data/lib/dry/types/builder_methods.rb +49 -20
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/coercions/json.rb +43 -7
  16. data/lib/dry/types/coercions/params.rb +118 -31
  17. data/lib/dry/types/compat.rb +0 -2
  18. data/lib/dry/types/compiler.rb +56 -41
  19. data/lib/dry/types/constrained.rb +81 -32
  20. data/lib/dry/types/constrained/coercible.rb +36 -6
  21. data/lib/dry/types/constraints.rb +18 -4
  22. data/lib/dry/types/constructor.rb +127 -54
  23. data/lib/dry/types/constructor/function.rb +216 -0
  24. data/lib/dry/types/constructor/wrapper.rb +94 -0
  25. data/lib/dry/types/container.rb +7 -0
  26. data/lib/dry/types/core.rb +54 -21
  27. data/lib/dry/types/decorator.rb +38 -17
  28. data/lib/dry/types/default.rb +61 -16
  29. data/lib/dry/types/enum.rb +43 -20
  30. data/lib/dry/types/errors.rb +75 -9
  31. data/lib/dry/types/extensions.rb +7 -1
  32. data/lib/dry/types/extensions/maybe.rb +74 -16
  33. data/lib/dry/types/extensions/monads.rb +29 -0
  34. data/lib/dry/types/fn_container.rb +6 -1
  35. data/lib/dry/types/hash.rb +86 -67
  36. data/lib/dry/types/hash/constructor.rb +33 -0
  37. data/lib/dry/types/inflector.rb +3 -1
  38. data/lib/dry/types/json.rb +18 -16
  39. data/lib/dry/types/lax.rb +75 -0
  40. data/lib/dry/types/map.rb +76 -33
  41. data/lib/dry/types/meta.rb +51 -0
  42. data/lib/dry/types/module.rb +120 -0
  43. data/lib/dry/types/nominal.rb +210 -0
  44. data/lib/dry/types/options.rb +13 -26
  45. data/lib/dry/types/params.rb +39 -25
  46. data/lib/dry/types/predicate_inferrer.rb +238 -0
  47. data/lib/dry/types/predicate_registry.rb +34 -0
  48. data/lib/dry/types/primitive_inferrer.rb +97 -0
  49. data/lib/dry/types/printable.rb +16 -0
  50. data/lib/dry/types/printer.rb +315 -0
  51. data/lib/dry/types/result.rb +29 -3
  52. data/lib/dry/types/schema.rb +408 -0
  53. data/lib/dry/types/schema/key.rb +156 -0
  54. data/lib/dry/types/spec/types.rb +103 -33
  55. data/lib/dry/types/sum.rb +84 -35
  56. data/lib/dry/types/type.rb +49 -0
  57. data/lib/dry/types/version.rb +3 -1
  58. metadata +68 -79
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.travis.yml +0 -29
  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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/result"
4
+
5
+ module Dry
6
+ module Types
7
+ # Monad extension for Result
8
+ #
9
+ # @api public
10
+ class Result
11
+ include Dry::Monads::Result::Mixin
12
+
13
+ # Turn result into a monad
14
+ #
15
+ # This makes result objects work with dry-monads (or anything with a compatible interface)
16
+ #
17
+ # @return [Dry::Monads::Success,Dry::Monads::Failure]
18
+ #
19
+ # @api public
20
+ def to_monad
21
+ if success?
22
+ Success(input)
23
+ else
24
+ Failure([error, input])
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,7 +1,12 @@
1
- require 'dry/types/container'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/container"
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Internal container for constructor functions used by the built-in types
8
+ #
9
+ # @api private
5
10
  class FnContainer
6
11
  # @api private
7
12
  def self.container
@@ -1,34 +1,44 @@
1
- require 'dry/types/hash/schema_builder'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/hash/constructor"
2
4
 
3
5
  module Dry
4
6
  module Types
5
- class Hash < Definition
6
- SCHEMA_BUILDER = SchemaBuilder.new.freeze
7
-
8
- # @param [{Symbol => Definition}] type_map
9
- # @param [Symbol] constructor
10
- # @return [Schema]
11
- def schema(type_map, constructor = nil)
12
- member_types = transform_types(type_map)
13
-
14
- if constructor.nil?
15
- Schema.new(primitive, member_types: member_types, **options, meta: meta)
7
+ # Hash types can be used to define maps and schemas
8
+ #
9
+ # @api public
10
+ class Hash < Nominal
11
+ NOT_REQUIRED = {required: false}.freeze
12
+
13
+ # @overload schema(type_map, meta = EMPTY_HASH)
14
+ # @param [{Symbol => Dry::Types::Nominal}] type_map
15
+ # @param [Hash] meta
16
+ # @return [Dry::Types::Schema]
17
+ #
18
+ # @overload schema(keys)
19
+ # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
20
+ # @param [Hash] meta
21
+ # @return [Dry::Types::Schema]
22
+ #
23
+ # @api public
24
+ def schema(keys_or_map, meta = EMPTY_HASH)
25
+ if keys_or_map.is_a?(::Array)
26
+ keys = keys_or_map
16
27
  else
17
- SCHEMA_BUILDER.(
18
- primitive,
19
- **options,
20
- member_types: member_types,
21
- meta: meta,
22
- hash_type: constructor
23
- )
28
+ keys = build_keys(keys_or_map)
24
29
  end
30
+
31
+ Schema.new(primitive, keys: keys, **options, meta: self.meta.merge(meta))
25
32
  end
26
33
 
27
34
  # Build a map type
28
35
  #
29
36
  # @param [Type] key_type
30
37
  # @param [Type] value_type
38
+ #
31
39
  # @return [Map]
40
+ #
41
+ # @api public
32
42
  def map(key_type, value_type)
33
43
  Map.new(
34
44
  primitive,
@@ -38,81 +48,90 @@ module Dry
38
48
  )
39
49
  end
40
50
 
41
- # @param [{Symbol => Definition}] type_map
42
- # @return [Schema]
43
- def weak(type_map)
44
- schema(type_map, :weak)
45
- end
46
-
47
- # @param [{Symbol => Definition}] type_map
48
- # @return [Schema]
49
- def permissive(type_map)
50
- schema(type_map, :permissive)
51
- end
52
-
53
- # @param [{Symbol => Definition}] type_map
54
- # @return [Schema]
55
- def strict(type_map)
56
- schema(type_map, :strict)
57
- end
58
-
59
- # @param [{Symbol => Definition}] type_map
60
- # @return [Schema]
61
- def strict_with_defaults(type_map)
62
- schema(type_map, :strict_with_defaults)
63
- end
64
-
65
- # @param [{Symbol => Definition}] type_map
66
- # @return [Schema]
67
- def symbolized(type_map)
68
- schema(type_map, :symbolized)
69
- end
70
-
71
- # Build a schema from an AST
72
51
  # @api private
73
- # @param [{Symbol => Definition}] member_types
74
- # @return [Schema]
75
- def instantiate(member_types)
76
- SCHEMA_BUILDER.instantiate(primitive, **options, member_types: member_types)
52
+ def weak(*)
53
+ raise "Support for old hash schemas was removed, please refer to the CHANGELOG "\
54
+ "on how to proceed with the new API https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
77
55
  end
56
+ alias_method :permissive, :weak
57
+ alias_method :strict, :weak
58
+ alias_method :strict_with_defaults, :weak
59
+ alias_method :symbolized, :weak
78
60
 
79
61
  # Injects a type transformation function for building schemas
62
+ #
80
63
  # @param [#call,nil] proc
81
64
  # @param [#call,nil] block
65
+ #
82
66
  # @return [Hash]
67
+ #
68
+ # @api public
83
69
  def with_type_transform(proc = nil, &block)
84
70
  fn = proc || block
85
71
 
86
- if fn.nil?
87
- raise ArgumentError, "a block or callable argument is required"
88
- end
72
+ raise ArgumentError, "a block or callable argument is required" if fn.nil?
89
73
 
90
74
  handle = Dry::Types::FnContainer.register(fn)
91
- meta(type_transform_fn: handle)
75
+ with(type_transform_fn: handle)
76
+ end
77
+
78
+ # @api private
79
+ def constructor_type
80
+ ::Dry::Types::Hash::Constructor
81
+ end
82
+
83
+ # Whether the type transforms types of schemas created by {Dry::Types::Hash#schema}
84
+ #
85
+ # @return [Boolean]
86
+ #
87
+ # @api public
88
+ def transform_types?
89
+ !options[:type_transform_fn].nil?
90
+ end
91
+
92
+ # @param meta [Boolean] Whether to dump the meta to the AST
93
+ #
94
+ # @return [Array] An AST representation
95
+ #
96
+ # @api public
97
+ def to_ast(meta: true)
98
+ [:hash, [options.slice(:type_transform_fn), meta ? self.meta : EMPTY_HASH]]
92
99
  end
93
100
 
94
101
  private
95
102
 
96
103
  # @api private
97
- def transform_types(type_map)
98
- type_fn = meta.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
104
+ def build_keys(type_map)
105
+ type_fn = options.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
99
106
  type_transform = Dry::Types::FnContainer[type_fn]
100
107
 
101
- type_map.each_with_object({}) { |(name, type), result|
102
- result[name] = type_transform.(
103
- resolve_type(type),
104
- name
105
- )
106
- }
108
+ type_map.map do |map_key, type|
109
+ name, options = key_name(map_key)
110
+ key = Schema::Key.new(resolve_type(type), name, **options)
111
+ type_transform.(key)
112
+ end
107
113
  end
108
114
 
109
115
  # @api private
110
116
  def resolve_type(type)
111
117
  case type
112
- when String, Class then Types[type]
118
+ when Type then type
119
+ when ::Class, ::String then Types[type]
113
120
  else type
114
121
  end
115
122
  end
123
+
124
+ # @api private
125
+ def key_name(key)
126
+ if key.to_s.end_with?("?")
127
+ [key.to_s.chop.to_sym, NOT_REQUIRED]
128
+ else
129
+ [key, EMPTY_HASH]
130
+ end
131
+ end
116
132
  end
117
133
  end
118
134
  end
135
+
136
+ require "dry/types/schema/key"
137
+ require "dry/types/schema"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/constructor"
4
+
5
+ module Dry
6
+ module Types
7
+ # Hash type exposes additional APIs for working with schema hashes
8
+ #
9
+ # @api public
10
+ class Hash < Nominal
11
+ class Constructor < ::Dry::Types::Constructor
12
+ # @api private
13
+ def constructor_type
14
+ ::Dry::Types::Hash::Constructor
15
+ end
16
+
17
+ # @return [Lax]
18
+ #
19
+ # @api public
20
+ def lax
21
+ type.lax.constructor(fn, meta: meta)
22
+ end
23
+
24
+ # @see Dry::Types::Array#of
25
+ #
26
+ # @api public
27
+ def schema(*args)
28
+ type.schema(*args).constructor(fn, meta: meta)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,4 +1,6 @@
1
- require 'dry/inflector'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
2
4
 
3
5
  module Dry
4
6
  module Types
@@ -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