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
@@ -75,7 +75,7 @@ module Dry
75
75
  ' Be careful: types will return the same instance of the default'\
76
76
  ' value every time. Call `.freeze` when setting the default'\
77
77
  ' or pass `shared: true` to discard this warning.'\
78
- "\n#{ where }",
78
+ "\n#{where}",
79
79
  tag: :'dry-types'
80
80
  )
81
81
  end
@@ -127,7 +127,7 @@ module Dry
127
127
  #
128
128
  # @api public
129
129
  def constructor(constructor = nil, **options, &block)
130
- constructor_type.new(with(options), fn: constructor || block)
130
+ constructor_type.new(with(**options), fn: constructor || block)
131
131
  end
132
132
  end
133
133
  end
@@ -25,7 +25,7 @@ module Dry
25
25
  #
26
26
  # @return [Dry::Types::Array]
27
27
  def Array(type)
28
- self::Array.of(type)
28
+ Strict(::Array).of(type)
29
29
  end
30
30
 
31
31
  # Build a hash schema
@@ -34,7 +34,7 @@ module Dry
34
34
  #
35
35
  # @return [Dry::Types::Array]
36
36
  def Hash(type_map)
37
- self::Hash.schema(type_map)
37
+ Strict(::Hash).schema(type_map)
38
38
  end
39
39
 
40
40
  # Build a type which values are instances of a given class
@@ -49,7 +49,7 @@ module Dry
49
49
  #
50
50
  # @return [Dry::Types::Type]
51
51
  def Instance(klass)
52
- Nominal.new(klass).constrained(type: klass)
52
+ Nominal(klass).constrained(type: klass)
53
53
  end
54
54
  alias_method :Strict, :Instance
55
55
 
@@ -60,7 +60,7 @@ module Dry
60
60
  #
61
61
  # @return [Dry::Types::Type]
62
62
  def Value(value)
63
- Nominal.new(value.class).constrained(eql: value)
63
+ Nominal(value.class).constrained(eql: value)
64
64
  end
65
65
 
66
66
  # Build a type with a single value
@@ -70,7 +70,7 @@ module Dry
70
70
  #
71
71
  # @return [Dry::Types::Type]
72
72
  def Constant(object)
73
- Nominal.new(object.class).constrained(is: object)
73
+ Nominal(object.class).constrained(is: object)
74
74
  end
75
75
 
76
76
  # Build a constructor type
@@ -82,7 +82,11 @@ module Dry
82
82
  #
83
83
  # @return [Dry::Types::Type]
84
84
  def Constructor(klass, cons = nil, &block)
85
- Nominal.new(klass).constructor(cons || block || klass.method(:new))
85
+ if klass.is_a?(Type)
86
+ klass.constructor(cons || block || klass.method(:new))
87
+ else
88
+ Nominal(klass).constructor(cons || block || klass.method(:new))
89
+ end
86
90
  end
87
91
 
88
92
  # Build a nominal type
@@ -91,7 +95,13 @@ module Dry
91
95
  #
92
96
  # @return [Dry::Types::Type]
93
97
  def Nominal(klass)
94
- Nominal.new(klass)
98
+ if klass <= ::Array
99
+ Array.new(klass)
100
+ elsif klass <= ::Hash
101
+ Hash.new(klass)
102
+ else
103
+ Nominal.new(klass)
104
+ end
95
105
  end
96
106
 
97
107
  # Build a map type
@@ -105,7 +115,23 @@ module Dry
105
115
  #
106
116
  # @return [Dry::Types::Map]
107
117
  def Map(key_type, value_type)
108
- Types['nominal.hash'].map(key_type, value_type)
118
+ Nominal(::Hash).map(key_type, value_type)
119
+ end
120
+
121
+ # Builds a constrained nominal type accepting any value that
122
+ # responds to given methods
123
+ #
124
+ # @example
125
+ # Types::Callable = Types.Interface(:call)
126
+ # Types::Contact = Types.Interface(:name, :address)
127
+ #
128
+ # @param methods [Array<String, Symbol>] Method names
129
+ #
130
+ # @return [Dry::Types::Contrained]
131
+ def Interface(*methods)
132
+ methods.reduce(Types['nominal.any']) do |type, method|
133
+ type.constrained(respond_to: method)
134
+ end
109
135
  end
110
136
  end
111
137
  end
@@ -36,8 +36,8 @@ module Dry
36
36
  if input.respond_to?(:to_str)
37
37
  begin
38
38
  ::Date.parse(input)
39
- rescue ArgumentError, RangeError => error
40
- CoercionError.handle(error, &block)
39
+ rescue ArgumentError, RangeError => e
40
+ CoercionError.handle(e, &block)
41
41
  end
42
42
  elsif block_given?
43
43
  yield
@@ -57,8 +57,8 @@ module Dry
57
57
  if input.respond_to?(:to_str)
58
58
  begin
59
59
  ::DateTime.parse(input)
60
- rescue ArgumentError => error
61
- CoercionError.handle(error, &block)
60
+ rescue ArgumentError => e
61
+ CoercionError.handle(e, &block)
62
62
  end
63
63
  elsif block_given?
64
64
  yield
@@ -78,8 +78,8 @@ module Dry
78
78
  if input.respond_to?(:to_str)
79
79
  begin
80
80
  ::Time.parse(input)
81
- rescue ArgumentError => error
82
- CoercionError.handle(error, &block)
81
+ rescue ArgumentError => e
82
+ CoercionError.handle(e, &block)
83
83
  end
84
84
  elsif block_given?
85
85
  yield
@@ -88,6 +88,19 @@ module Dry
88
88
  end
89
89
  end
90
90
 
91
+ # @param [#to_sym, Object] input
92
+ #
93
+ # @return [Symbol, Object]
94
+ #
95
+ # @raise CoercionError
96
+ #
97
+ # @api public
98
+ def to_symbol(input, &block)
99
+ input.to_sym
100
+ rescue NoMethodError => e
101
+ CoercionError.handle(e, &block)
102
+ end
103
+
91
104
  private
92
105
 
93
106
  # Checks whether String is empty
@@ -71,8 +71,8 @@ module Dry
71
71
  else
72
72
  Integer(input)
73
73
  end
74
- rescue ArgumentError, TypeError => error
75
- CoercionError.handle(error, &block)
74
+ rescue ArgumentError, TypeError => e
75
+ CoercionError.handle(e, &block)
76
76
  end
77
77
 
78
78
  # @param [#to_f, Object] input
@@ -84,8 +84,8 @@ module Dry
84
84
  # @api public
85
85
  def self.to_float(input, &block)
86
86
  Float(input)
87
- rescue ArgumentError, TypeError => error
88
- CoercionError.handle(error, &block)
87
+ rescue ArgumentError, TypeError => e
88
+ CoercionError.handle(e, &block)
89
89
  end
90
90
 
91
91
  # @param [#to_d, Object] input
@@ -68,12 +68,12 @@ module Dry
68
68
 
69
69
  def visit_hash(node)
70
70
  opts, meta = node
71
- registry['nominal.hash'].with(opts.merge(meta: meta))
71
+ registry['nominal.hash'].with(**opts, meta: meta)
72
72
  end
73
73
 
74
74
  def visit_schema(node)
75
75
  keys, options, meta = node
76
- registry['nominal.hash'].schema(keys.map { |key| visit(key) }).with(options.merge(meta: meta))
76
+ registry['nominal.hash'].schema(keys.map { |key| visit(key) }).with(**options, meta: meta)
77
77
  end
78
78
 
79
79
  def visit_json_hash(node)
@@ -24,7 +24,7 @@ module Dry
24
24
  # @param [Hash] options
25
25
  #
26
26
  # @api public
27
- def initialize(type, options)
27
+ def initialize(type, **options)
28
28
  super
29
29
  @rule = options.fetch(:rule)
30
30
  end
@@ -126,6 +126,11 @@ module Dry
126
126
  [:constrained, [type.to_ast(meta: meta), rule.to_ast]]
127
127
  end
128
128
 
129
+ # @api private
130
+ def constructor_type
131
+ type.constructor_type
132
+ end
133
+
129
134
  private
130
135
 
131
136
  # @param [Object] response
@@ -12,15 +12,13 @@ module Dry
12
12
  class Constructor < Nominal
13
13
  include Dry::Equalizer(:type, :options, inspect: false)
14
14
 
15
- private :meta
16
-
17
15
  # @return [#call]
18
16
  attr_reader :fn
19
17
 
20
18
  # @return [Type]
21
19
  attr_reader :type
22
20
 
23
- undef :constrained?
21
+ undef :constrained?, :meta, :optional?, :primitive, :default?, :name
24
22
 
25
23
  # @param [Builder, Object] input
26
24
  # @param [Hash] options
@@ -46,36 +44,11 @@ module Dry
46
44
  super(type, **options, fn: fn)
47
45
  end
48
46
 
49
- # Return the inner type's primitive
50
- #
51
- # @return [Class]
52
- #
53
- # @api public
54
- def primitive
55
- type.primitive
56
- end
57
-
58
- # Return the inner type's name
59
- #
60
- # @return [String]
61
- #
62
- # @api public
63
- def name
64
- type.name
65
- end
66
-
67
- # @return [Boolean]
68
- #
69
- # @api public
70
- def default?
71
- type.default?
72
- end
73
-
74
47
  # @return [Object]
75
48
  #
76
49
  # @api private
77
50
  def call_safe(input)
78
- coerced = fn.(input) { return yield }
51
+ coerced = fn.(input) { |output = input| return yield(output) }
79
52
  type.call_safe(coerced) { |output = coerced| yield(output) }
80
53
  end
81
54
 
@@ -95,8 +68,8 @@ module Dry
95
68
  # @api public
96
69
  def try(input, &block)
97
70
  value = fn.(input)
98
- rescue CoercionError => error
99
- failure = failure(input, error)
71
+ rescue CoercionError => e
72
+ failure = failure(input, e)
100
73
  block_given? ? yield(failure) : failure
101
74
  else
102
75
  type.try(value, &block)
@@ -112,7 +85,7 @@ module Dry
112
85
  #
113
86
  # @api public
114
87
  def constructor(new_fn = nil, **options, &block)
115
- with({**options, fn: fn >> (new_fn || block)})
88
+ with(**options, fn: fn >> (new_fn || block))
116
89
  end
117
90
  alias_method :append, :constructor
118
91
  alias_method :>>, :constructor
@@ -141,7 +114,7 @@ module Dry
141
114
  #
142
115
  # @api public
143
116
  def prepend(new_fn = nil, **options, &block)
144
- with({**options, fn: fn << (new_fn || block)})
117
+ with(**options, fn: fn << (new_fn || block))
145
118
  end
146
119
  alias_method :<<, :prepend
147
120
 
@@ -150,7 +123,7 @@ module Dry
150
123
  # @return [Lax]
151
124
  # @api public
152
125
  def lax
153
- Lax.new(Constructor.new(type.lax, options))
126
+ Lax.new(Constructor.new(type.lax, **options))
154
127
  end
155
128
 
156
129
  # Wrap the type with a proc
@@ -182,10 +155,10 @@ module Dry
182
155
  # @api private
183
156
  def method_missing(method, *args, &block)
184
157
  if type.respond_to?(method)
185
- response = type.__send__(method, *args, &block)
158
+ response = type.public_send(method, *args, &block)
186
159
 
187
- if composable?(response)
188
- response.constructor_type.new(response, options)
160
+ if response.is_a?(Type) && type.class == response.class
161
+ response.constructor_type.new(response, **options)
189
162
  else
190
163
  response
191
164
  end
@@ -193,11 +166,6 @@ module Dry
193
166
  super
194
167
  end
195
168
  end
196
-
197
- # @api private
198
- def composable?(value)
199
- value.is_a?(Builder)
200
- end
201
169
  end
202
170
  end
203
171
  end
@@ -1,4 +1,3 @@
1
-
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'concurrent/map'
@@ -16,8 +15,8 @@ module Dry
16
15
  class Safe < Function
17
16
  def call(input, &block)
18
17
  @fn.(input, &block)
19
- rescue NoMethodError, TypeError, ArgumentError => error
20
- CoercionError.handle(error, &block)
18
+ rescue NoMethodError, TypeError, ArgumentError => e
19
+ CoercionError.handle(e, &block)
21
20
  end
22
21
  end
23
22
 
@@ -91,8 +90,8 @@ module Dry
91
90
  class PrivateSafeCall < PrivateCall
92
91
  def call(input, &block)
93
92
  @target.send(@name, input)
94
- rescue NoMethodError, TypeError, ArgumentError => error
95
- CoercionError.handle(error, &block)
93
+ rescue NoMethodError, TypeError, ArgumentError => e
94
+ CoercionError.handle(e, &block)
96
95
  end
97
96
  end
98
97
 
@@ -5,7 +5,7 @@ require 'dry/types/any'
5
5
  module Dry
6
6
  module Types
7
7
  # Primitives with {Kernel} coercion methods
8
- COERCIBLE = {
8
+ KERNEL_COERCIBLE = {
9
9
  string: String,
10
10
  integer: Integer,
11
11
  float: Float,
@@ -14,10 +14,19 @@ module Dry
14
14
  hash: ::Hash
15
15
  }.freeze
16
16
 
17
- # Primitives that are non-coercible through {Kernel} methods
17
+ # Primitives with coercions through by convention `to_*` methods
18
+ METHOD_COERCIBLE = {
19
+ symbol: Symbol
20
+ }.freeze
21
+
22
+ # By convention methods to coerce {METHOD_COERCIBLE} primitives
23
+ METHOD_COERCIBLE_METHODS = {
24
+ symbol: :to_sym
25
+ }.freeze
26
+
27
+ # Primitives that are non-coercible
18
28
  NON_COERCIBLE = {
19
29
  nil: NilClass,
20
- symbol: Symbol,
21
30
  class: Class,
22
31
  true: TrueClass,
23
32
  false: FalseClass,
@@ -28,7 +37,10 @@ module Dry
28
37
  }.freeze
29
38
 
30
39
  # All built-in primitives
31
- ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
40
+ ALL_PRIMITIVES = [KERNEL_COERCIBLE, METHOD_COERCIBLE, NON_COERCIBLE].reduce(&:merge).freeze
41
+
42
+ # All coercible types
43
+ COERCIBLE = KERNEL_COERCIBLE.merge(METHOD_COERCIBLE).freeze
32
44
 
33
45
  # All built-in primitives except {NilClass}
34
46
  NON_NIL = ALL_PRIMITIVES.reject { |name, _| name == :nil }.freeze
@@ -46,11 +58,18 @@ module Dry
46
58
  register("strict.#{name}", type)
47
59
  end
48
60
 
49
- # Register {COERCIBLE} types
50
- COERCIBLE.each do |name, primitive|
61
+ # Register {KERNEL_COERCIBLE} types
62
+ KERNEL_COERCIBLE.each do |name, primitive|
51
63
  register("coercible.#{name}", self["nominal.#{name}"].constructor(Kernel.method(primitive.name)))
52
64
  end
53
65
 
66
+ # Register {METHOD_COERCIBLE} types
67
+ METHOD_COERCIBLE.each_key do |name|
68
+ register(
69
+ "coercible.#{name}", self["nominal.#{name}"].constructor(&METHOD_COERCIBLE_METHODS[name])
70
+ )
71
+ end
72
+
54
73
  # Register optional strict {NON_NIL} types
55
74
  NON_NIL.each_key do |name|
56
75
  register("optional.strict.#{name}", self["strict.#{name}"].optional)
@@ -64,8 +83,8 @@ module Dry
64
83
  # Register `:bool` since it's common and not a built-in Ruby type :(
65
84
  register('nominal.bool', self['nominal.true'] | self['nominal.false'])
66
85
  bool = self['strict.true'] | self['strict.false']
67
- register("strict.bool", bool)
68
- register("bool", bool)
86
+ register('strict.bool', bool)
87
+ register('bool', bool)
69
88
 
70
89
  register('any', Any)
71
90
  register('nominal.any', Any)
@@ -14,7 +14,7 @@ module Dry
14
14
  attr_reader :type
15
15
 
16
16
  # @param [Type] type
17
- def initialize(type, *)
17
+ def initialize(type, *, **)
18
18
  super
19
19
  @type = type
20
20
  end
@@ -90,7 +90,7 @@ module Dry
90
90
  # @api private
91
91
  def method_missing(meth, *args, &block)
92
92
  if type.respond_to?(meth)
93
- response = type.__send__(meth, *args, &block)
93
+ response = type.public_send(meth, *args, &block)
94
94
 
95
95
  if decorate?(response)
96
96
  __new__(response)
@@ -101,6 +101,7 @@ module Dry
101
101
  super
102
102
  end
103
103
  end
104
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
104
105
 
105
106
  # Replace underlying type
106
107
  #