dry-types 0.9.0 → 0.15.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 (61) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +43 -0
  5. data/.travis.yml +15 -14
  6. data/.yardopts +5 -0
  7. data/CHANGELOG.md +494 -88
  8. data/CONTRIBUTING.md +29 -0
  9. data/Gemfile +7 -6
  10. data/README.md +1 -3
  11. data/Rakefile +8 -3
  12. data/benchmarks/hash_schemas.rb +7 -7
  13. data/dry-types.gemspec +11 -9
  14. data/lib/dry/types/any.rb +36 -0
  15. data/lib/dry/types/array/member.rb +29 -4
  16. data/lib/dry/types/array.rb +6 -4
  17. data/lib/dry/types/builder.rb +48 -6
  18. data/lib/dry/types/builder_methods.rb +111 -0
  19. data/lib/dry/types/coercions/json.rb +3 -0
  20. data/lib/dry/types/coercions/{form.rb → params.rb} +23 -3
  21. data/lib/dry/types/coercions.rb +16 -3
  22. data/lib/dry/types/compat.rb +0 -0
  23. data/lib/dry/types/compiler.rb +66 -39
  24. data/lib/dry/types/constrained/coercible.rb +7 -1
  25. data/lib/dry/types/constrained.rb +42 -3
  26. data/lib/dry/types/constraints.rb +3 -0
  27. data/lib/dry/types/constructor.rb +98 -16
  28. data/lib/dry/types/container.rb +2 -0
  29. data/lib/dry/types/core.rb +30 -14
  30. data/lib/dry/types/decorator.rb +31 -5
  31. data/lib/dry/types/default.rb +34 -8
  32. data/lib/dry/types/enum.rb +71 -14
  33. data/lib/dry/types/errors.rb +23 -6
  34. data/lib/dry/types/extensions/maybe.rb +35 -16
  35. data/lib/dry/types/fn_container.rb +34 -0
  36. data/lib/dry/types/hash/constructor.rb +20 -0
  37. data/lib/dry/types/hash.rb +103 -23
  38. data/lib/dry/types/inflector.rb +7 -0
  39. data/lib/dry/types/json.rb +7 -7
  40. data/lib/dry/types/map.rb +98 -0
  41. data/lib/dry/types/module.rb +115 -0
  42. data/lib/dry/types/nominal.rb +119 -0
  43. data/lib/dry/types/options.rb +29 -7
  44. data/lib/dry/types/params.rb +53 -0
  45. data/lib/dry/types/printable.rb +12 -0
  46. data/lib/dry/types/printer.rb +309 -0
  47. data/lib/dry/types/result.rb +12 -2
  48. data/lib/dry/types/safe.rb +27 -1
  49. data/lib/dry/types/schema/key.rb +130 -0
  50. data/lib/dry/types/schema.rb +298 -0
  51. data/lib/dry/types/spec/types.rb +102 -0
  52. data/lib/dry/types/sum.rb +75 -13
  53. data/lib/dry/types/type.rb +6 -0
  54. data/lib/dry/types/version.rb +1 -1
  55. data/lib/dry/types.rb +104 -38
  56. data/log/.gitkeep +0 -0
  57. metadata +81 -50
  58. data/lib/dry/types/definition.rb +0 -79
  59. data/lib/dry/types/form.rb +0 -53
  60. data/lib/dry/types/hash/schema.rb +0 -156
  61. data/lib/spec/dry/types.rb +0 -56
@@ -11,74 +11,101 @@ module Dry
11
11
  visit(ast)
12
12
  end
13
13
 
14
- def visit(node, *args)
15
- send(:"visit_#{node[0]}", node[1], *args)
14
+ def visit(node)
15
+ type, body = node
16
+ send(:"visit_#{ type }", body)
17
+ end
18
+
19
+ def visit_constrained(node)
20
+ nominal, rule, meta = node
21
+ Types::Constrained.new(visit(nominal), rule: visit_rule(rule)).meta(meta)
16
22
  end
17
23
 
18
24
  def visit_constructor(node)
19
- primitive, fn = node
20
- Types::Constructor.new(primitive, &fn)
25
+ nominal, fn_register_name, meta = node
26
+ fn = Dry::Types::FnContainer[fn_register_name]
27
+ primitive = visit(nominal)
28
+ Types::Constructor.new(primitive, meta: meta, fn: fn)
29
+ end
30
+
31
+ def visit_safe(node)
32
+ ast, meta = node
33
+ Types::Safe.new(visit(ast), meta: meta)
21
34
  end
22
35
 
23
- def visit_type(node)
24
- type, args = node
25
- meth = :"visit_#{type.tr('.', '_')}"
36
+ def visit_nominal(node)
37
+ type, meta = node
38
+ nominal_name = "nominal.#{ Types.identifier(type) }"
26
39
 
27
- if respond_to?(meth) && args
28
- send(meth, args)
40
+ if registry.registered?(nominal_name)
41
+ registry[nominal_name].meta(meta)
29
42
  else
30
- registry[type]
43
+ Nominal.new(type, meta: meta)
31
44
  end
32
45
  end
33
46
 
47
+ def visit_rule(node)
48
+ Dry::Types.rule_compiler.([node])[0]
49
+ end
50
+
34
51
  def visit_sum(node)
35
- node.map { |type| visit(type) }.reduce(:|)
52
+ *types, meta = node
53
+ types.map { |type| visit(type) }.reduce(:|).meta(meta)
36
54
  end
37
55
 
38
56
  def visit_array(node)
39
- registry['array'].member(call(node))
57
+ member, meta = node
58
+ member = member.is_a?(Class) ? member : visit(member)
59
+ registry['nominal.array'].of(member).meta(meta)
40
60
  end
41
61
 
42
- def visit_form_array(node)
43
- registry['form.array'].member(call(node))
62
+ def visit_hash(node)
63
+ opts, meta = node
64
+ registry['nominal.hash'].with(opts.merge(meta: meta))
44
65
  end
45
66
 
46
- def visit_json_array(node)
47
- registry['json.array'].member(call(node))
67
+ def visit_schema(node)
68
+ keys, options, meta = node
69
+ registry['nominal.hash'].schema(keys.map { |key| visit(key) }).with(options.merge(meta: meta))
48
70
  end
49
71
 
50
- def visit_hash(node)
51
- constructor, schema = node
52
- merge_with('hash', constructor, schema)
72
+ def visit_json_hash(node)
73
+ keys, meta = node
74
+ registry['json.hash'].schema(keys.map { |key| visit(key) }, meta)
53
75
  end
54
76
 
55
- def visit_form_hash(node)
56
- if node
57
- constructor, schema = node
58
- merge_with('form.hash', constructor, schema)
59
- else
60
- registry['form.hash']
61
- end
77
+ def visit_json_array(node)
78
+ member, meta = node
79
+ registry['json.array'].of(visit(member)).meta(meta)
62
80
  end
63
81
 
64
- def visit_json_hash(node)
65
- if node
66
- constructor, schema = node
67
- merge_with('json.hash', constructor, schema)
68
- else
69
- registry['json.hash']
70
- end
82
+ def visit_params_hash(node)
83
+ keys, meta = node
84
+ registry['params.hash'].schema(keys.map { |key| visit(key) }, meta)
85
+ end
86
+
87
+ def visit_params_array(node)
88
+ member, meta = node
89
+ registry['params.array'].of(visit(member)).meta(meta)
71
90
  end
72
91
 
73
92
  def visit_key(node)
74
- name, types = node
75
- { name => visit(types) }
93
+ name, required, type = node
94
+ Schema::Key.new(visit(type), name, required: required)
95
+ end
96
+
97
+ def visit_enum(node)
98
+ type, mapping, meta = node
99
+ Enum.new(visit(type), mapping: mapping, meta: meta)
100
+ end
101
+
102
+ def visit_map(node)
103
+ key_type, value_type, meta = node
104
+ registry['nominal.hash'].map(visit(key_type), visit(value_type)).meta(meta)
76
105
  end
77
106
 
78
- def merge_with(hash_id, constructor, schema)
79
- registry[hash_id].__send__(
80
- constructor, schema.map { |key| visit(key) }.reduce({}, :merge)
81
- )
107
+ def visit_any(meta)
108
+ registry['any'].meta(meta)
82
109
  end
83
110
  end
84
111
  end
@@ -2,6 +2,11 @@ module Dry
2
2
  module Types
3
3
  class Constrained
4
4
  class Coercible < Constrained
5
+ # @param [Object] input
6
+ # @param [#call,nil] block
7
+ # @yieldparam [Failure] failure
8
+ # @yieldreturn [Result]
9
+ # @return [Result,nil]
5
10
  def try(input, &block)
6
11
  result = type.try(input)
7
12
 
@@ -11,7 +16,8 @@ module Dry
11
16
  if validation.success?
12
17
  result
13
18
  else
14
- block ? yield(validation) : validation
19
+ failure = failure(result.input, validation)
20
+ block ? yield(failure) : failure
15
21
  end
16
22
  else
17
23
  block ? yield(result) : result
@@ -5,24 +5,38 @@ require 'dry/types/constrained/coercible'
5
5
  module Dry
6
6
  module Types
7
7
  class Constrained
8
- include Dry::Equalizer(:type, :options, :rule)
8
+ include Type
9
9
  include Decorator
10
10
  include Builder
11
+ include Printable
12
+ include Dry::Equalizer(:type, :options, :rule, :meta, inspect: false)
11
13
 
14
+ # @return [Dry::Logic::Rule]
12
15
  attr_reader :rule
13
16
 
17
+ # @param [Type] type
18
+ # @param [Hash] options
14
19
  def initialize(type, options)
15
20
  super
16
21
  @rule = options.fetch(:rule)
17
22
  end
18
23
 
24
+ # @param [Object] input
25
+ # @return [Object]
26
+ # @raise [ConstraintError]
19
27
  def call(input)
20
- try(input) do |result|
28
+ try(input) { |result|
21
29
  raise ConstraintError.new(result, input)
22
- end.input
30
+ }.input
23
31
  end
24
32
  alias_method :[], :call
25
33
 
34
+ # @param [Object] input
35
+ # @param [#call,nil] block
36
+ # @yieldparam [Failure] failure
37
+ # @yieldreturn [Result]
38
+ # @return [Logic::Result, Result]
39
+ # @return [Object] if block given and try fails
26
40
  def try(input, &block)
27
41
  result = rule.(input)
28
42
 
@@ -34,20 +48,45 @@ module Dry
34
48
  end
35
49
  end
36
50
 
51
+ # @param [Object] value
52
+ # @return [Boolean]
37
53
  def valid?(value)
38
54
  rule.(value).success? && type.valid?(value)
39
55
  end
40
56
 
57
+ # @param [Hash] options
58
+ # The options hash provided to {Types.Rule} and combined
59
+ # using {&} with previous {#rule}
60
+ # @return [Constrained]
61
+ # @see Dry::Logic::Operators#and
41
62
  def constrained(options)
42
63
  with(rule: rule & Types.Rule(options))
43
64
  end
44
65
 
66
+ # @return [true]
45
67
  def constrained?
46
68
  true
47
69
  end
48
70
 
71
+ # @param [Object] value
72
+ # @return [Boolean]
73
+ def ===(value)
74
+ valid?(value)
75
+ end
76
+
77
+ # @api public
78
+ #
79
+ # @see Nominal#to_ast
80
+ def to_ast(meta: true)
81
+ [:constrained, [type.to_ast(meta: meta),
82
+ rule.to_ast,
83
+ meta ? self.meta : EMPTY_HASH]]
84
+ end
85
+
49
86
  private
50
87
 
88
+ # @param [Object] response
89
+ # @return [Boolean]
51
90
  def decorate?(response)
52
91
  super || response.kind_of?(Constructor)
53
92
  end
@@ -4,12 +4,15 @@ require 'dry/logic/rule/predicate'
4
4
 
5
5
  module Dry
6
6
  module Types
7
+ # @param [Hash] options
8
+ # @return [Dry::Logic::Rule]
7
9
  def self.Rule(options)
8
10
  rule_compiler.(
9
11
  options.map { |key, val| Logic::Rule::Predicate.new(Logic::Predicates[:"#{key}?"]).curry(val).to_ast }
10
12
  ).reduce(:and)
11
13
  end
12
14
 
15
+ # @return [Dry::Logic::RuleCompiler]
13
16
  def self.rule_compiler
14
17
  @rule_compiler ||= Logic::RuleCompiler.new(Logic::Predicates)
15
18
  end
@@ -1,67 +1,145 @@
1
- require 'dry/types/decorator'
1
+ require 'dry/types/fn_container'
2
2
 
3
3
  module Dry
4
4
  module Types
5
- class Constructor < Definition
6
- include Dry::Equalizer(:type)
5
+ class Constructor < Nominal
6
+ include Dry::Equalizer(:type, :options, :meta, inspect: false)
7
7
 
8
+ # @return [#call]
8
9
  attr_reader :fn
9
10
 
11
+ # @return [Type]
10
12
  attr_reader :type
11
13
 
12
- def self.new(input, options = {}, &block)
13
- type = input.is_a?(Builder) ? input : Definition.new(input)
14
- super(type, options, &block)
14
+ undef :constrained?
15
+
16
+ # @param [Builder, Object] input
17
+ # @param [Hash] options
18
+ # @param [#call, nil] block
19
+ def self.new(input, **options, &block)
20
+ type = input.is_a?(Builder) ? input : Nominal.new(input)
21
+ super(type, **options, &block)
15
22
  end
16
23
 
17
- def initialize(type, options = {}, &block)
24
+ # @param [Type] type
25
+ # @param [Hash] options
26
+ # @param [#call, nil] block
27
+ def initialize(type, **options, &block)
18
28
  @type = type
19
29
  @fn = options.fetch(:fn, block)
20
- super
30
+
31
+ raise ArgumentError, 'Missing constructor block' if fn.nil?
32
+
33
+ super(type, **options, fn: fn)
21
34
  end
22
35
 
36
+ # @return [Class]
23
37
  def primitive
24
38
  type.primitive
25
39
  end
26
40
 
41
+ # @return [String]
42
+ def name
43
+ type.name
44
+ end
45
+
46
+ # @return [Boolean]
47
+ def default?
48
+ type.default?
49
+ end
50
+
51
+ # @param [Object] input
52
+ # @return [Object]
27
53
  def call(input)
28
54
  type[fn[input]]
29
55
  end
30
56
  alias_method :[], :call
31
57
 
58
+ # @param [Object] input
59
+ # @param [#call,nil] block
60
+ # @return [Logic::Result, Types::Result]
61
+ # @return [Object] if block given and try fails
32
62
  def try(input, &block)
33
63
  type.try(fn[input], &block)
34
- rescue TypeError => e
64
+ rescue TypeError, ArgumentError => e
35
65
  failure(input, e.message)
36
66
  end
37
67
 
68
+ # @param [#call, nil] new_fn
69
+ # @param [Hash] options
70
+ # @param [#call, nil] block
71
+ # @return [Constructor]
38
72
  def constructor(new_fn = nil, **options, &block)
39
73
  left = new_fn || block
40
74
  right = fn
41
75
 
42
- with(options.merge(fn: -> input { left[right[input]] }))
76
+ with(**options, fn: -> input { left[right[input]] })
43
77
  end
78
+ alias_method :append, :constructor
79
+ alias_method :>>, :constructor
44
80
 
81
+ # @param [Object] value
82
+ # @return [Boolean]
45
83
  def valid?(value)
46
- super && type.valid?(value)
84
+ constructed_value = fn[value]
85
+ rescue NoMethodError, TypeError, ArgumentError
86
+ false
87
+ else
88
+ type.valid?(constructed_value)
47
89
  end
90
+ alias_method :===, :valid?
48
91
 
92
+ # @return [Class]
49
93
  def constrained_type
50
94
  Constrained::Coercible
51
95
  end
52
96
 
97
+ # @api public
98
+ #
99
+ # @see Nominal#to_ast
100
+ def to_ast(meta: true)
101
+ [:constructor, [type.to_ast(meta: meta),
102
+ register_fn(fn),
103
+ meta ? self.meta : EMPTY_HASH]]
104
+ end
105
+
106
+ # @api public
107
+ #
108
+ # @param [#call, nil] new_fn
109
+ # @param [Hash] options
110
+ # @param [#call, nil] block
111
+ # @return [Constructor]
112
+ def prepend(new_fn = nil, **options, &block)
113
+ left = new_fn || block
114
+ right = fn
115
+
116
+ with(**options, fn: -> input { right[left[input]] })
117
+ end
118
+ alias_method :<<, :prepend
119
+
53
120
  private
54
121
 
122
+ def register_fn(fn)
123
+ Dry::Types::FnContainer.register(fn)
124
+ end
125
+
126
+ # @param [Symbol] meth
127
+ # @param [Boolean] include_private
128
+ # @return [Boolean]
55
129
  def respond_to_missing?(meth, include_private = false)
56
130
  super || type.respond_to?(meth)
57
131
  end
58
132
 
59
- def method_missing(meth, *args, &block)
60
- if type.respond_to?(meth)
61
- response = type.__send__(meth, *args, &block)
133
+ # Delegates missing methods to {#type}
134
+ # @param [Symbol] method
135
+ # @param [Array] args
136
+ # @param [#call, nil] block
137
+ def method_missing(method, *args, &block)
138
+ if type.respond_to?(method)
139
+ response = type.__send__(method, *args, &block)
62
140
 
63
- if response.kind_of?(Builder)
64
- self.class.new(response, options)
141
+ if composable?(response)
142
+ response.constructor_type.new(response, options)
65
143
  else
66
144
  response
67
145
  end
@@ -69,6 +147,10 @@ module Dry
69
147
  super
70
148
  end
71
149
  end
150
+
151
+ def composable?(value)
152
+ value.kind_of?(Builder)
153
+ end
72
154
  end
73
155
  end
74
156
  end
@@ -1,3 +1,5 @@
1
+ require 'dry/container'
2
+
1
3
  module Dry
2
4
  module Types
3
5
  class Container
@@ -1,14 +1,18 @@
1
+ require 'dry/types/any'
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Primitives with {Kernel} coercion methods
3
6
  COERCIBLE = {
4
7
  string: String,
5
- int: Integer,
8
+ integer: Integer,
6
9
  float: Float,
7
10
  decimal: BigDecimal,
8
11
  array: ::Array,
9
12
  hash: ::Hash
10
13
  }.freeze
11
14
 
15
+ # Primitives that are non-coercible through {Kernel} methods
12
16
  NON_COERCIBLE = {
13
17
  nil: NilClass,
14
18
  symbol: Symbol,
@@ -17,44 +21,56 @@ module Dry
17
21
  false: FalseClass,
18
22
  date: Date,
19
23
  date_time: DateTime,
20
- time: Time
24
+ time: Time,
25
+ range: Range
21
26
  }.freeze
22
27
 
28
+ # All built-in primitives
23
29
  ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
24
30
 
31
+ # All built-in primitives except {NilClass}
25
32
  NON_NIL = ALL_PRIMITIVES.reject { |name, _| name == :nil }.freeze
26
33
 
27
- # Register built-in types that are non-coercible through kernel methods
34
+ # Register generic types for {ALL_PRIMITIVES}
28
35
  ALL_PRIMITIVES.each do |name, primitive|
29
- register(name.to_s, Definition[primitive].new(primitive))
36
+ type = Nominal[primitive].new(primitive)
37
+ register("nominal.#{name}", type)
30
38
  end
31
39
 
32
- # Register strict built-in types that are non-coercible through kernel methods
40
+ # Register strict types for {ALL_PRIMITIVES}
33
41
  ALL_PRIMITIVES.each do |name, primitive|
34
- register("strict.#{name}", self[name.to_s].constrained(type: primitive))
42
+ type = self["nominal.#{name}"].constrained(type: primitive)
43
+ register(name.to_s, type)
44
+ register("strict.#{name}", type)
35
45
  end
36
46
 
37
- # Register built-in primitive types with kernel coercion methods
47
+ # Register {COERCIBLE} types
38
48
  COERCIBLE.each do |name, primitive|
39
- register("coercible.#{name}", self[name.to_s].constructor(Kernel.method(primitive.name)))
49
+ register("coercible.#{name}", self["nominal.#{name}"].constructor(Kernel.method(primitive.name)))
40
50
  end
41
51
 
42
- # Register non-coercible optional types
52
+ # Register optional strict {NON_NIL} types
43
53
  NON_NIL.each_key do |name|
44
54
  register("optional.strict.#{name}", self["strict.#{name}"].optional)
45
55
  end
46
56
 
47
- # Register coercible optional types
57
+ # Register optional {COERCIBLE} types
48
58
  COERCIBLE.each_key do |name|
49
59
  register("optional.coercible.#{name}", self["coercible.#{name}"].optional)
50
60
  end
51
61
 
52
- # Register :bool since it's common and not a built-in Ruby type :(
53
- register("bool", self["true"] | self["false"])
54
- register("strict.bool", self["strict.true"] | self["strict.false"])
62
+ # Register `:bool` since it's common and not a built-in Ruby type :(
63
+ register("nominal.bool", self["nominal.true"] | self["nominal.false"])
64
+ bool = self["strict.true"] | self["strict.false"]
65
+ register("strict.bool", bool)
66
+ register("bool", bool)
67
+
68
+ register("any", Any)
69
+ register("nominal.any", Any)
70
+ register("strict.any", Any)
55
71
  end
56
72
  end
57
73
 
58
74
  require 'dry/types/coercions'
59
- require 'dry/types/form'
75
+ require 'dry/types/params'
60
76
  require 'dry/types/json'
@@ -5,49 +5,70 @@ module Dry
5
5
  module Decorator
6
6
  include Options
7
7
 
8
+ # @return [Type]
8
9
  attr_reader :type
9
10
 
11
+ # @param [Type] type
10
12
  def initialize(type, *)
11
13
  super
12
14
  @type = type
13
15
  end
14
16
 
15
- def constructor
16
- type.constructor
17
- end
18
-
17
+ # @param [Object] input
18
+ # @param [#call, nil] block
19
+ # @return [Result,Logic::Result]
20
+ # @return [Object] if block given and try fails
19
21
  def try(input, &block)
20
22
  type.try(input, &block)
21
23
  end
22
24
 
25
+ # @param [Object] value
26
+ # @return [Boolean]
23
27
  def valid?(value)
24
28
  type.valid?(value)
25
29
  end
30
+ alias_method :===, :valid?
26
31
 
32
+ # @return [Boolean]
27
33
  def default?
28
34
  type.default?
29
35
  end
30
36
 
37
+ # @return [Boolean]
31
38
  def constrained?
32
39
  type.constrained?
33
40
  end
34
41
 
42
+ # @return [Sum]
43
+ def optional
44
+ Types['strict.nil'] | self
45
+ end
46
+
47
+ # @param [Symbol] meth
48
+ # @param [Boolean] include_private
49
+ # @return [Boolean]
35
50
  def respond_to_missing?(meth, include_private = false)
36
51
  super || type.respond_to?(meth)
37
52
  end
38
53
 
39
54
  private
40
55
 
56
+ # @param [Object] response
57
+ # @return [Boolean]
41
58
  def decorate?(response)
42
59
  response.kind_of?(type.class)
43
60
  end
44
61
 
62
+ # Delegates missing methods to {#type}
63
+ # @param [Symbol] meth
64
+ # @param [Array] args
65
+ # @param [#call, nil] block
45
66
  def method_missing(meth, *args, &block)
46
67
  if type.respond_to?(meth)
47
68
  response = type.__send__(meth, *args, &block)
48
69
 
49
70
  if decorate?(response)
50
- self.class.new(response, options)
71
+ __new__(response)
51
72
  else
52
73
  response
53
74
  end
@@ -55,6 +76,11 @@ module Dry
55
76
  super
56
77
  end
57
78
  end
79
+
80
+ # Replace underlying type
81
+ def __new__(type)
82
+ self.class.new(type, options)
83
+ end
58
84
  end
59
85
  end
60
86
  end