dry-types 1.0.0 → 1.2.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -5
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +76 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +1 -1
  10. data/.rspec +3 -1
  11. data/.rubocop.yml +89 -0
  12. data/CHANGELOG.md +127 -3
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +2 -2
  15. data/Gemfile +12 -6
  16. data/LICENSE +17 -17
  17. data/README.md +2 -2
  18. data/Rakefile +2 -2
  19. data/benchmarks/hash_schemas.rb +8 -6
  20. data/benchmarks/lax_schema.rb +0 -1
  21. data/benchmarks/profile_invalid_input.rb +1 -1
  22. data/benchmarks/profile_lax_schema_valid.rb +1 -1
  23. data/benchmarks/profile_valid_input.rb +1 -1
  24. data/docsite/source/array-with-member.html.md +13 -0
  25. data/docsite/source/built-in-types.html.md +116 -0
  26. data/docsite/source/constraints.html.md +31 -0
  27. data/docsite/source/custom-types.html.md +93 -0
  28. data/docsite/source/default-values.html.md +91 -0
  29. data/docsite/source/enum.html.md +69 -0
  30. data/docsite/source/extensions.html.md +15 -0
  31. data/docsite/source/extensions/maybe.html.md +57 -0
  32. data/docsite/source/extensions/monads.html.md +61 -0
  33. data/docsite/source/getting-started.html.md +57 -0
  34. data/docsite/source/hash-schemas.html.md +169 -0
  35. data/docsite/source/index.html.md +156 -0
  36. data/docsite/source/map.html.md +17 -0
  37. data/docsite/source/optional-values.html.md +35 -0
  38. data/docsite/source/sum.html.md +21 -0
  39. data/dry-types.gemspec +19 -19
  40. data/lib/dry/types.rb +9 -4
  41. data/lib/dry/types/any.rb +2 -2
  42. data/lib/dry/types/array.rb +6 -0
  43. data/lib/dry/types/array/constructor.rb +32 -0
  44. data/lib/dry/types/array/member.rb +10 -3
  45. data/lib/dry/types/builder.rb +2 -2
  46. data/lib/dry/types/builder_methods.rb +34 -8
  47. data/lib/dry/types/coercions.rb +19 -6
  48. data/lib/dry/types/coercions/params.rb +4 -4
  49. data/lib/dry/types/compiler.rb +2 -2
  50. data/lib/dry/types/constrained.rb +6 -1
  51. data/lib/dry/types/constructor.rb +10 -42
  52. data/lib/dry/types/constructor/function.rb +4 -5
  53. data/lib/dry/types/core.rb +27 -8
  54. data/lib/dry/types/decorator.rb +3 -2
  55. data/lib/dry/types/enum.rb +2 -1
  56. data/lib/dry/types/extensions.rb +4 -0
  57. data/lib/dry/types/extensions/maybe.rb +9 -1
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/hash.rb +11 -12
  60. data/lib/dry/types/hash/constructor.rb +5 -5
  61. data/lib/dry/types/json.rb +4 -0
  62. data/lib/dry/types/lax.rb +4 -4
  63. data/lib/dry/types/map.rb +8 -4
  64. data/lib/dry/types/meta.rb +1 -1
  65. data/lib/dry/types/module.rb +6 -6
  66. data/lib/dry/types/nominal.rb +3 -4
  67. data/lib/dry/types/params.rb +9 -0
  68. data/lib/dry/types/predicate_inferrer.rb +197 -0
  69. data/lib/dry/types/predicate_registry.rb +34 -0
  70. data/lib/dry/types/primitive_inferrer.rb +97 -0
  71. data/lib/dry/types/printer.rb +17 -12
  72. data/lib/dry/types/schema.rb +16 -22
  73. data/lib/dry/types/schema/key.rb +19 -1
  74. data/lib/dry/types/spec/types.rb +6 -7
  75. data/lib/dry/types/sum.rb +2 -2
  76. data/lib/dry/types/version.rb +1 -1
  77. metadata +67 -35
  78. data/.travis.yml +0 -27
@@ -11,6 +11,7 @@ module Dry
11
11
  include Type
12
12
  include Dry::Equalizer(:type, :mapping, inspect: false)
13
13
  include Decorator
14
+ include Builder
14
15
 
15
16
  # @return [Array]
16
17
  attr_reader :values
@@ -26,7 +27,7 @@ module Dry
26
27
  # @option options [Array] :values
27
28
  #
28
29
  # @api private
29
- def initialize(type, options)
30
+ def initialize(type, **options)
30
31
  super
31
32
  @mapping = options.fetch(:mapping).freeze
32
33
  @values = @mapping.keys.freeze
@@ -3,3 +3,7 @@
3
3
  Dry::Types.register_extension(:maybe) do
4
4
  require 'dry/types/extensions/maybe'
5
5
  end
6
+
7
+ Dry::Types.register_extension(:monads) do
8
+ require 'dry/types/extensions/monads'
9
+ end
@@ -79,7 +79,7 @@ module Dry
79
79
  # @api public
80
80
  def default(value)
81
81
  if value.nil?
82
- raise ArgumentError, "nil cannot be used as a default of a maybe type"
82
+ raise ArgumentError, 'nil cannot be used as a default of a maybe type'
83
83
  else
84
84
  super
85
85
  end
@@ -97,6 +97,14 @@ module Dry
97
97
  end
98
98
  end
99
99
 
100
+ # @api private
101
+ class Schema::Key
102
+ # @api private
103
+ def maybe
104
+ __new__(type.maybe)
105
+ end
106
+ end
107
+
100
108
  # @api private
101
109
  class Printer
102
110
  MAPPING[Maybe] = :visit_maybe
@@ -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
@@ -50,8 +50,8 @@ module Dry
50
50
 
51
51
  # @api private
52
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"
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'
55
55
  end
56
56
  alias_method :permissive, :weak
57
57
  alias_method :strict, :weak
@@ -69,9 +69,7 @@ module Dry
69
69
  def with_type_transform(proc = nil, &block)
70
70
  fn = proc || block
71
71
 
72
- if fn.nil?
73
- raise ArgumentError, "a block or callable argument is required"
74
- end
72
+ raise ArgumentError, 'a block or callable argument is required' if fn.nil?
75
73
 
76
74
  handle = Dry::Types::FnContainer.register(fn)
77
75
  with(type_transform_fn: handle)
@@ -97,11 +95,11 @@ module Dry
97
95
  #
98
96
  # @api public
99
97
  def to_ast(meta: true)
100
- if RUBY_VERSION >= "2.5"
101
- opts = options.slice(:type_transform_fn)
102
- else
103
- opts = options.select { |k, _| k == :type_transform_fn }
104
- end
98
+ opts = if RUBY_VERSION >= '2.5'
99
+ options.slice(:type_transform_fn)
100
+ else
101
+ options.select { |k, _| k == :type_transform_fn }
102
+ end
105
103
 
106
104
  [:hash, [opts, meta ? self.meta : EMPTY_HASH]]
107
105
  end
@@ -115,7 +113,7 @@ module Dry
115
113
 
116
114
  type_map.map do |map_key, type|
117
115
  name, options = key_name(map_key)
118
- key = Schema::Key.new(resolve_type(type), name, options)
116
+ key = Schema::Key.new(resolve_type(type), name, **options)
119
117
  type_transform.(key)
120
118
  end
121
119
  end
@@ -123,7 +121,8 @@ module Dry
123
121
  # @api private
124
122
  def resolve_type(type)
125
123
  case type
126
- when String, Class then Types[type]
124
+ when Type then type
125
+ when ::Class, ::String then Types[type]
127
126
  else type
128
127
  end
129
128
  end
@@ -21,11 +21,11 @@ module Dry
21
21
  type.lax.constructor(fn, meta: meta)
22
22
  end
23
23
 
24
- private
25
-
26
- # @api private
27
- def composable?(value)
28
- super && !value.is_a?(Schema::Key)
24
+ # @see Dry::Types::Array#of
25
+ #
26
+ # @api public
27
+ def schema(*args)
28
+ type.schema(*args).constructor(fn, meta: meta)
29
29
  end
30
30
  end
31
31
  end
@@ -24,6 +24,10 @@ module Dry
24
24
  self['nominal.decimal'].constructor(Coercions::JSON.method(:to_decimal))
25
25
  end
26
26
 
27
+ register('json.symbol') do
28
+ self['nominal.symbol'].constructor(Coercions::JSON.method(:to_symbol))
29
+ end
30
+
27
31
  register('json.array') { self['array'] }
28
32
 
29
33
  register('json.hash') { self['hash'] }
data/lib/dry/types/lax.rb CHANGED
@@ -15,7 +15,7 @@ module Dry
15
15
  include Printable
16
16
  include Dry::Equalizer(:type, inspect: false)
17
17
 
18
- private :options, :constructor
18
+ undef :options, :constructor
19
19
 
20
20
  # @param [Object] input
21
21
  #
@@ -40,8 +40,8 @@ module Dry
40
40
  # @api public
41
41
  def try(input, &block)
42
42
  type.try(input, &block)
43
- rescue CoercionError => error
44
- result = failure(input, error.message)
43
+ rescue CoercionError => e
44
+ result = failure(input, e.message)
45
45
  block ? yield(result) : result
46
46
  end
47
47
 
@@ -67,7 +67,7 @@ module Dry
67
67
  #
68
68
  # @api private
69
69
  def decorate?(response)
70
- super || response.is_a?(constructor_type)
70
+ super || response.is_a?(type.constructor_type)
71
71
  end
72
72
  end
73
73
 
data/lib/dry/types/map.rb CHANGED
@@ -74,6 +74,7 @@ module Dry
74
74
  def try(hash)
75
75
  result = coerce(hash)
76
76
  return result if result.success? || !block_given?
77
+
77
78
  yield(result)
78
79
  end
79
80
 
@@ -100,11 +101,14 @@ module Dry
100
101
 
101
102
  # @api private
102
103
  def coerce(input)
103
- return failure(
104
- input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
105
- ) 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
106
109
 
107
- output, failures = {}, []
110
+ output = {}
111
+ failures = []
108
112
 
109
113
  input.each do |k, v|
110
114
  res_k = key_type.try(k)
@@ -16,7 +16,7 @@ module Dry
16
16
  # @return [Type]
17
17
  #
18
18
  # @api public
19
- def with(options)
19
+ def with(**options)
20
20
  super(meta: @meta, **options)
21
21
  end
22
22
 
@@ -18,10 +18,10 @@ module Dry
18
18
  #
19
19
  # @api public
20
20
  class Module < ::Module
21
- def initialize(registry, *args)
21
+ def initialize(registry, *args, **kwargs)
22
22
  @registry = registry
23
- check_parameters(*args)
24
- constants = type_constants(*args)
23
+ check_parameters(*args, **kwargs)
24
+ constants = type_constants(*args, **kwargs)
25
25
  define_constants(constants)
26
26
  extend(BuilderMethods)
27
27
 
@@ -31,7 +31,7 @@ module Dry
31
31
  base.instance_exec(const_get(:Nominal, false)) do |nominal|
32
32
  extend Dry::Core::Deprecations[:'dry-types']
33
33
  const_set(:Definition, nominal)
34
- deprecate_constant(:Definition, message: "Nominal")
34
+ deprecate_constant(:Definition, message: 'Nominal')
35
35
  end
36
36
  end
37
37
  end
@@ -93,8 +93,8 @@ module Dry
93
93
 
94
94
  (referenced.uniq - known).each do |name|
95
95
  raise ArgumentError,
96
- "#{ name.inspect } is not a known type namespace. "\
97
- "Supported options are #{ known.map(&:inspect).join(', ') }"
96
+ "#{name.inspect} is not a known type namespace. "\
97
+ "Supported options are #{known.map(&:inspect).join(', ')}"
98
98
  end
99
99
  end
100
100
 
@@ -126,9 +126,8 @@ module Dry
126
126
  #
127
127
  # @api public
128
128
  def failure(input, error)
129
- unless error.is_a?(CoercionError)
130
- raise ArgumentError, "error must be a CoercionError"
131
- end
129
+ raise ArgumentError, 'error must be a CoercionError' unless error.is_a?(CoercionError)
130
+
132
131
  Result::Failure.new(input, error)
133
132
  end
134
133
 
@@ -202,7 +201,7 @@ module Dry
202
201
 
203
202
  extend Dry::Core::Deprecations[:'dry-types']
204
203
  Definition = Nominal
205
- deprecate_constant(:Definition, message: "Nominal")
204
+ deprecate_constant(:Definition, message: 'Nominal')
206
205
  end
207
206
  end
208
207
 
@@ -51,5 +51,14 @@ module Dry
51
51
  register('params.hash') do
52
52
  self['nominal.hash'].constructor(Coercions::Params.method(:to_hash))
53
53
  end
54
+
55
+ register('params.symbol') do
56
+ self['nominal.symbol'].constructor(Coercions::Params.method(:to_symbol))
57
+ end
58
+
59
+ COERCIBLE.each_key do |name|
60
+ next if name.equal?(:string)
61
+ register("optional.params.#{name}", self['params.nil'] | self["params.#{name}"])
62
+ end
54
63
  end
55
64
  end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/cache'
4
+ require 'dry/types/predicate_registry'
5
+
6
+ module Dry
7
+ module Types
8
+ # PredicateInferrer returns the list of predicates used by a type.
9
+ #
10
+ # @api public
11
+ class PredicateInferrer
12
+ extend Core::Cache
13
+
14
+ TYPE_TO_PREDICATE = {
15
+ DateTime => :date_time?,
16
+ FalseClass => :false?,
17
+ Integer => :int?,
18
+ NilClass => :nil?,
19
+ String => :str?,
20
+ TrueClass => :true?,
21
+ BigDecimal => :decimal?
22
+ }.freeze
23
+
24
+ REDUCED_TYPES = {
25
+ [[[:true?], [:false?]]] => %i[bool?]
26
+ }.freeze
27
+
28
+ HASH = %i[hash?].freeze
29
+
30
+ ARRAY = %i[array?].freeze
31
+
32
+ NIL = %i[nil?].freeze
33
+
34
+ # Compiler reduces type AST into a list of predicates
35
+ #
36
+ # @api private
37
+ class Compiler
38
+ # @return [PredicateRegistry]
39
+ # @api private
40
+ attr_reader :registry
41
+
42
+ # @api private
43
+ def initialize(registry)
44
+ @registry = registry
45
+ end
46
+
47
+ # @api private
48
+ def infer_predicate(type)
49
+ [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
50
+ end
51
+
52
+ # @api private
53
+ def visit(node)
54
+ meth, rest = node
55
+ public_send(:"visit_#{meth}", rest)
56
+ end
57
+
58
+ # @api private
59
+ def visit_nominal(node)
60
+ type = node[0]
61
+ predicate = infer_predicate(type)
62
+
63
+ if registry.key?(predicate[0])
64
+ predicate
65
+ else
66
+ [type?: type]
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def visit_hash(_)
72
+ HASH
73
+ end
74
+
75
+ # @api private
76
+ def visit_array(_)
77
+ ARRAY
78
+ end
79
+
80
+ # @api private
81
+ def visit_lax(node)
82
+ visit(node)
83
+ end
84
+
85
+ # @api private
86
+ def visit_constructor(node)
87
+ other, * = node
88
+ visit(other)
89
+ end
90
+
91
+ # @api private
92
+ def visit_enum(node)
93
+ other, * = node
94
+ visit(other)
95
+ end
96
+
97
+ # @api private
98
+ def visit_sum(node)
99
+ left_node, right_node, = node
100
+ left = visit(left_node)
101
+ right = visit(right_node)
102
+
103
+ if left.eql?(NIL)
104
+ right
105
+ else
106
+ [[left, right]]
107
+ end
108
+ end
109
+
110
+ # @api private
111
+ def visit_constrained(node)
112
+ other, rules = node
113
+ predicates = visit(rules)
114
+
115
+ if predicates.empty?
116
+ visit(other)
117
+ else
118
+ [*visit(other), *merge_predicates(predicates)]
119
+ end
120
+ end
121
+
122
+ # @api private
123
+ def visit_any(_)
124
+ EMPTY_ARRAY
125
+ end
126
+
127
+ # @api private
128
+ def visit_and(node)
129
+ left, right = node
130
+ visit(left) + visit(right)
131
+ end
132
+
133
+ # @api private
134
+ def visit_predicate(node)
135
+ pred, args = node
136
+
137
+ if pred.equal?(:type?)
138
+ EMPTY_ARRAY
139
+ elsif registry.key?(pred)
140
+ *curried, _ = args
141
+ values = curried.map { |_, v| v }
142
+
143
+ if values.empty?
144
+ [pred]
145
+ else
146
+ [pred => values[0]]
147
+ end
148
+ else
149
+ EMPTY_ARRAY
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # @api private
156
+ def merge_predicates(nodes)
157
+ preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
158
+ if predicate.is_a?(::Hash)
159
+ h.update(predicate)
160
+ else
161
+ ps << predicate
162
+ end
163
+ end
164
+
165
+ merged.empty? ? preds : [*preds, merged]
166
+ end
167
+ end
168
+
169
+ # @return [Compiler]
170
+ # @api private
171
+ attr_reader :compiler
172
+
173
+ # @api private
174
+ def initialize(registry = PredicateRegistry.new)
175
+ @compiler = Compiler.new(registry)
176
+ end
177
+
178
+ # Infer predicate identifier from the provided type
179
+ #
180
+ # @param [Type] type
181
+ # @return [Symbol]
182
+ #
183
+ # @api private
184
+ def [](type)
185
+ self.class.fetch_or_store(type) do
186
+ predicates = compiler.visit(type.to_ast)
187
+
188
+ if predicates.is_a?(::Hash)
189
+ predicates
190
+ else
191
+ REDUCED_TYPES[predicates] || predicates
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end