dry-types 1.0.0 → 1.2.1

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