dry-types 0.13.2 → 1.5.1

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