dry-types 0.15.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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