dry-types 0.15.0 → 1.0.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +18 -2
  4. data/.travis.yml +4 -5
  5. data/.yardopts +6 -2
  6. data/CHANGELOG.md +69 -1
  7. data/Gemfile +3 -0
  8. data/README.md +2 -1
  9. data/Rakefile +2 -0
  10. data/benchmarks/hash_schemas.rb +2 -0
  11. data/benchmarks/lax_schema.rb +16 -0
  12. data/benchmarks/profile_invalid_input.rb +15 -0
  13. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  14. data/benchmarks/profile_valid_input.rb +15 -0
  15. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  16. data/benchmarks/setup.rb +17 -0
  17. data/dry-types.gemspec +4 -2
  18. data/lib/dry-types.rb +2 -0
  19. data/lib/dry/types.rb +51 -13
  20. data/lib/dry/types/any.rb +21 -10
  21. data/lib/dry/types/array.rb +11 -1
  22. data/lib/dry/types/array/member.rb +65 -13
  23. data/lib/dry/types/builder.rb +48 -4
  24. data/lib/dry/types/builder_methods.rb +9 -8
  25. data/lib/dry/types/coercions.rb +71 -19
  26. data/lib/dry/types/coercions/json.rb +22 -3
  27. data/lib/dry/types/coercions/params.rb +98 -30
  28. data/lib/dry/types/compiler.rb +35 -12
  29. data/lib/dry/types/constrained.rb +73 -27
  30. data/lib/dry/types/constrained/coercible.rb +36 -6
  31. data/lib/dry/types/constraints.rb +15 -1
  32. data/lib/dry/types/constructor.rb +90 -43
  33. data/lib/dry/types/constructor/function.rb +201 -0
  34. data/lib/dry/types/container.rb +5 -0
  35. data/lib/dry/types/core.rb +7 -5
  36. data/lib/dry/types/decorator.rb +36 -9
  37. data/lib/dry/types/default.rb +48 -16
  38. data/lib/dry/types/enum.rb +30 -16
  39. data/lib/dry/types/errors.rb +73 -7
  40. data/lib/dry/types/extensions.rb +2 -0
  41. data/lib/dry/types/extensions/maybe.rb +43 -4
  42. data/lib/dry/types/fn_container.rb +5 -0
  43. data/lib/dry/types/hash.rb +22 -3
  44. data/lib/dry/types/hash/constructor.rb +13 -0
  45. data/lib/dry/types/inflector.rb +2 -0
  46. data/lib/dry/types/json.rb +4 -6
  47. data/lib/dry/types/{safe.rb → lax.rb} +34 -17
  48. data/lib/dry/types/map.rb +63 -29
  49. data/lib/dry/types/meta.rb +51 -0
  50. data/lib/dry/types/module.rb +7 -2
  51. data/lib/dry/types/nominal.rb +105 -13
  52. data/lib/dry/types/options.rb +12 -25
  53. data/lib/dry/types/params.rb +5 -3
  54. data/lib/dry/types/printable.rb +5 -1
  55. data/lib/dry/types/printer.rb +58 -57
  56. data/lib/dry/types/result.rb +26 -0
  57. data/lib/dry/types/schema.rb +169 -66
  58. data/lib/dry/types/schema/key.rb +34 -39
  59. data/lib/dry/types/spec/types.rb +41 -1
  60. data/lib/dry/types/sum.rb +70 -21
  61. data/lib/dry/types/type.rb +49 -0
  62. data/lib/dry/types/version.rb +3 -1
  63. metadata +14 -12
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Dry::Types.register_extension(:maybe) do
2
4
  require 'dry/types/extensions/maybe'
3
5
  end
@@ -1,31 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/monads/maybe'
2
4
  require 'dry/types/decorator'
3
5
 
4
6
  module Dry
5
7
  module Types
8
+ # Maybe extension provides Maybe types where values are wrapped using `Either` monad
9
+ #
10
+ # @api public
6
11
  class Maybe
7
12
  include Type
8
13
  include Dry::Equalizer(:type, :options, inspect: false)
9
14
  include Decorator
10
15
  include Builder
16
+ include Printable
11
17
  include Dry::Monads::Maybe::Mixin
12
18
 
13
19
  # @param [Dry::Monads::Maybe, Object] input
20
+ #
21
+ # @return [Dry::Monads::Maybe]
22
+ #
23
+ # @api private
24
+ def call_unsafe(input = Undefined)
25
+ case input
26
+ when Dry::Monads::Maybe
27
+ input
28
+ when Undefined
29
+ None()
30
+ else
31
+ Maybe(type.call_unsafe(input))
32
+ end
33
+ end
34
+
35
+ # @param [Dry::Monads::Maybe, Object] input
36
+ #
14
37
  # @return [Dry::Monads::Maybe]
15
- def call(input = Undefined)
38
+ #
39
+ # @api private
40
+ def call_safe(input = Undefined, &block)
16
41
  case input
17
42
  when Dry::Monads::Maybe
18
43
  input
19
44
  when Undefined
20
45
  None()
21
46
  else
22
- Maybe(type[input])
47
+ Maybe(type.call_safe(input, &block))
23
48
  end
24
49
  end
25
- alias_method :[], :call
26
50
 
27
51
  # @param [Object] input
52
+ #
28
53
  # @return [Result::Success]
54
+ #
55
+ # @api public
29
56
  def try(input = Undefined)
30
57
  res = if input.equal?(Undefined)
31
58
  None()
@@ -37,13 +64,19 @@ module Dry
37
64
  end
38
65
 
39
66
  # @return [true]
67
+ #
68
+ # @api public
40
69
  def default?
41
70
  true
42
71
  end
43
72
 
44
73
  # @param [Object] value
74
+ #
45
75
  # @see Dry::Types::Builder#default
76
+ #
46
77
  # @raise [ArgumentError] if nil provided as default value
78
+ #
79
+ # @api public
47
80
  def default(value)
48
81
  if value.nil?
49
82
  raise ArgumentError, "nil cannot be used as a default of a maybe type"
@@ -54,18 +87,24 @@ module Dry
54
87
  end
55
88
 
56
89
  module Builder
90
+ # Turn a type into a maybe type
91
+ #
57
92
  # @return [Maybe]
93
+ #
94
+ # @api public
58
95
  def maybe
59
96
  Maybe.new(Types['strict.nil'] | self)
60
97
  end
61
98
  end
62
99
 
100
+ # @api private
63
101
  class Printer
64
102
  MAPPING[Maybe] = :visit_maybe
65
103
 
104
+ # @api private
66
105
  def visit_maybe(maybe)
67
106
  visit(maybe.type) do |type|
68
- yield "Maybe<#{ type }>"
107
+ yield "Maybe<#{type}>"
69
108
  end
70
109
  end
71
110
  end
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
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,18 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/hash/constructor'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Hash types can be used to define maps and schemas
8
+ #
9
+ # @api public
5
10
  class Hash < Nominal
6
11
  NOT_REQUIRED = { required: false }.freeze
7
12
 
8
- # @overload schmea(type_map, meta = EMPTY_HASH)
13
+ # @overload schema(type_map, meta = EMPTY_HASH)
9
14
  # @param [{Symbol => Dry::Types::Nominal}] type_map
10
15
  # @param [Hash] meta
11
16
  # @return [Dry::Types::Schema]
17
+ #
12
18
  # @overload schema(keys)
13
19
  # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
14
20
  # @param [Hash] meta
15
21
  # @return [Dry::Types::Schema]
22
+ #
23
+ # @api public
16
24
  def schema(keys_or_map, meta = EMPTY_HASH)
17
25
  if keys_or_map.is_a?(::Array)
18
26
  keys = keys_or_map
@@ -27,7 +35,10 @@ module Dry
27
35
  #
28
36
  # @param [Type] key_type
29
37
  # @param [Type] value_type
38
+ #
30
39
  # @return [Map]
40
+ #
41
+ # @api public
31
42
  def map(key_type, value_type)
32
43
  Map.new(
33
44
  primitive,
@@ -37,8 +48,7 @@ module Dry
37
48
  )
38
49
  end
39
50
 
40
- # @param [{Symbol => Nominal}] type_map
41
- # @return [Schema]
51
+ # @api private
42
52
  def weak(*)
43
53
  raise "Support for old hash schemas was removed, please refer to the CHANGELOG "\
44
54
  "on how to proceed with the new API https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
@@ -49,9 +59,13 @@ module Dry
49
59
  alias_method :symbolized, :weak
50
60
 
51
61
  # Injects a type transformation function for building schemas
62
+ #
52
63
  # @param [#call,nil] proc
53
64
  # @param [#call,nil] block
65
+ #
54
66
  # @return [Hash]
67
+ #
68
+ # @api public
55
69
  def with_type_transform(proc = nil, &block)
56
70
  fn = proc || block
57
71
 
@@ -69,14 +83,19 @@ module Dry
69
83
  end
70
84
 
71
85
  # Whether the type transforms types of schemas created by {Dry::Types::Hash#schema}
86
+ #
72
87
  # @return [Boolean]
88
+ #
73
89
  # @api public
74
90
  def transform_types?
75
91
  !options[:type_transform_fn].nil?
76
92
  end
77
93
 
78
94
  # @param meta [Boolean] Whether to dump the meta to the AST
95
+ #
79
96
  # @return [Array] An AST representation
97
+ #
98
+ # @api public
80
99
  def to_ast(meta: true)
81
100
  if RUBY_VERSION >= "2.5"
82
101
  opts = options.slice(:type_transform_fn)
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/constructor'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Hash type exposes additional APIs for working with schema hashes
8
+ #
9
+ # @api public
5
10
  class Hash < Nominal
6
11
  class Constructor < ::Dry::Types::Constructor
7
12
  # @api private
@@ -9,8 +14,16 @@ module Dry
9
14
  ::Dry::Types::Hash::Constructor
10
15
  end
11
16
 
17
+ # @return [Lax]
18
+ #
19
+ # @api public
20
+ def lax
21
+ type.lax.constructor(fn, meta: meta)
22
+ end
23
+
12
24
  private
13
25
 
26
+ # @api private
14
27
  def composable?(value)
15
28
  super && !value.is_a?(Schema::Key)
16
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/inflector'
2
4
 
3
5
  module Dry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/coercions/json'
2
4
 
3
5
  module Dry
@@ -22,12 +24,8 @@ module Dry
22
24
  self['nominal.decimal'].constructor(Coercions::JSON.method(:to_decimal))
23
25
  end
24
26
 
25
- register('json.array') do
26
- self['nominal.array'].safe
27
- end
27
+ register('json.array') { self['array'] }
28
28
 
29
- register('json.hash') do
30
- self['nominal.hash'].safe
31
- end
29
+ register('json.hash') { self['hash'] }
32
30
  end
33
31
  end
@@ -1,61 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/deprecations'
1
4
  require 'dry/types/decorator'
2
5
 
3
6
  module Dry
4
7
  module Types
5
- class Safe
8
+ # Lax types rescue from type-related errors when constructors fail
9
+ #
10
+ # @api public
11
+ class Lax
6
12
  include Type
7
13
  include Decorator
8
14
  include Builder
9
15
  include Printable
10
16
  include Dry::Equalizer(:type, inspect: false)
11
17
 
12
- private :options, :meta
18
+ private :options, :constructor
13
19
 
14
20
  # @param [Object] input
21
+ #
15
22
  # @return [Object]
23
+ #
24
+ # @api public
16
25
  def call(input)
17
- result = try(input)
18
-
19
- if result.respond_to?(:input)
20
- result.input
21
- else
22
- input
23
- end
26
+ type.call_safe(input) { |output = input| output }
24
27
  end
25
28
  alias_method :[], :call
29
+ alias_method :call_safe, :call
30
+ alias_method :call_unsafe, :call
26
31
 
27
32
  # @param [Object] input
28
33
  # @param [#call,nil] block
34
+ #
29
35
  # @yieldparam [Failure] failure
30
36
  # @yieldreturn [Result]
37
+ #
31
38
  # @return [Result,Logic::Result]
39
+ #
40
+ # @api public
32
41
  def try(input, &block)
33
42
  type.try(input, &block)
34
- rescue TypeError, ArgumentError => e
35
- result = failure(input, e.message)
43
+ rescue CoercionError => error
44
+ result = failure(input, error.message)
36
45
  block ? yield(result) : result
37
46
  end
38
47
 
39
- # @api public
40
- #
41
48
  # @see Nominal#to_ast
49
+ #
50
+ # @api public
42
51
  def to_ast(meta: true)
43
- [:safe, [type.to_ast(meta: meta), EMPTY_HASH]]
52
+ [:lax, type.to_ast(meta: meta)]
44
53
  end
45
54
 
55
+ # @return [Lax]
56
+ #
46
57
  # @api public
47
- # @return [Safe]
48
- def safe
58
+ def lax
49
59
  self
50
60
  end
51
61
 
52
62
  private
53
63
 
54
64
  # @param [Object, Dry::Types::Constructor] response
65
+ #
55
66
  # @return [Boolean]
67
+ #
68
+ # @api private
56
69
  def decorate?(response)
57
- super || response.kind_of?(Constructor)
70
+ super || response.is_a?(constructor_type)
58
71
  end
59
72
  end
73
+
74
+ extend ::Dry::Core::Deprecations[:'dry-types']
75
+ Safe = Lax
76
+ deprecate_constant(:Safe)
60
77
  end
61
78
  end
@@ -1,44 +1,76 @@
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?
@@ -46,51 +78,53 @@ module Dry
46
78
  end
47
79
 
48
80
  # @param meta [Boolean] Whether to dump the meta to the AST
81
+ #
49
82
  # @return [Array] An AST representation
83
+ #
84
+ # @api public
50
85
  def to_ast(meta: true)
51
86
  [:map,
52
- [key_type.to_ast(meta: true), value_type.to_ast(meta: true),
87
+ [key_type.to_ast(meta: true),
88
+ value_type.to_ast(meta: true),
53
89
  meta ? self.meta : EMPTY_HASH]]
54
90
  end
55
91
 
56
92
  # @return [Boolean]
93
+ #
94
+ # @api public
57
95
  def constrained?
58
96
  value_type.constrained?
59
97
  end
60
98
 
61
99
  private
62
100
 
101
+ # @api private
63
102
  def coerce(input)
64
103
  return failure(
65
- input, "#{input.inspect} must be an instance of #{primitive}"
104
+ input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
66
105
  ) unless primitive?(input)
67
106
 
68
107
  output, failures = {}, []
69
108
 
70
- input.each do |k,v|
71
- res_k = options[:key_type].try(k)
72
- res_v = options[:value_type].try(v)
109
+ input.each do |k, v|
110
+ res_k = key_type.try(k)
111
+ res_v = value_type.try(v)
112
+
73
113
  if res_k.failure?
74
- failures << "input key #{k.inspect} is invalid: #{res_k.error}"
114
+ failures << res_k.error
75
115
  elsif output.key?(res_k.input)
76
- failures << "duplicate coerced hash key #{res_k.input.inspect}"
116
+ failures << CoercionError.new("duplicate coerced hash key #{res_k.input.inspect}")
77
117
  elsif res_v.failure?
78
- failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
118
+ failures << res_v.error
79
119
  else
80
120
  output[res_k.input] = res_v.input
81
121
  end
82
122
  end
83
123
 
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}"
124
+ if failures.empty?
125
+ success(output)
126
+ else
127
+ failure(input, MultipleError.new(failures))
94
128
  end
95
129
  end
96
130
  end