dry-types 0.15.0 → 1.2.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +18 -2
  7. data/.travis.yml +10 -5
  8. data/.yardopts +6 -2
  9. data/CHANGELOG.md +186 -3
  10. data/Gemfile +11 -5
  11. data/README.md +4 -3
  12. data/Rakefile +4 -2
  13. data/benchmarks/hash_schemas.rb +10 -6
  14. data/benchmarks/lax_schema.rb +15 -0
  15. data/benchmarks/profile_invalid_input.rb +15 -0
  16. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  17. data/benchmarks/profile_valid_input.rb +15 -0
  18. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  19. data/benchmarks/setup.rb +17 -0
  20. data/docsite/source/array-with-member.html.md +13 -0
  21. data/docsite/source/built-in-types.html.md +116 -0
  22. data/docsite/source/constraints.html.md +31 -0
  23. data/docsite/source/custom-types.html.md +93 -0
  24. data/docsite/source/default-values.html.md +91 -0
  25. data/docsite/source/enum.html.md +69 -0
  26. data/docsite/source/getting-started.html.md +57 -0
  27. data/docsite/source/hash-schemas.html.md +169 -0
  28. data/docsite/source/index.html.md +155 -0
  29. data/docsite/source/map.html.md +17 -0
  30. data/docsite/source/optional-values.html.md +96 -0
  31. data/docsite/source/sum.html.md +21 -0
  32. data/dry-types.gemspec +21 -19
  33. data/lib/dry-types.rb +2 -0
  34. data/lib/dry/types.rb +60 -17
  35. data/lib/dry/types/any.rb +21 -10
  36. data/lib/dry/types/array.rb +17 -1
  37. data/lib/dry/types/array/constructor.rb +32 -0
  38. data/lib/dry/types/array/member.rb +72 -13
  39. data/lib/dry/types/builder.rb +49 -5
  40. data/lib/dry/types/builder_methods.rb +43 -16
  41. data/lib/dry/types/coercions.rb +84 -19
  42. data/lib/dry/types/coercions/json.rb +22 -3
  43. data/lib/dry/types/coercions/params.rb +98 -30
  44. data/lib/dry/types/compiler.rb +35 -12
  45. data/lib/dry/types/constrained.rb +78 -27
  46. data/lib/dry/types/constrained/coercible.rb +36 -6
  47. data/lib/dry/types/constraints.rb +15 -1
  48. data/lib/dry/types/constructor.rb +77 -62
  49. data/lib/dry/types/constructor/function.rb +200 -0
  50. data/lib/dry/types/container.rb +5 -0
  51. data/lib/dry/types/core.rb +35 -14
  52. data/lib/dry/types/decorator.rb +37 -10
  53. data/lib/dry/types/default.rb +48 -16
  54. data/lib/dry/types/enum.rb +31 -16
  55. data/lib/dry/types/errors.rb +73 -7
  56. data/lib/dry/types/extensions.rb +6 -0
  57. data/lib/dry/types/extensions/maybe.rb +52 -5
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/fn_container.rb +5 -0
  60. data/lib/dry/types/hash.rb +32 -14
  61. data/lib/dry/types/hash/constructor.rb +16 -3
  62. data/lib/dry/types/inflector.rb +2 -0
  63. data/lib/dry/types/json.rb +7 -5
  64. data/lib/dry/types/{safe.rb → lax.rb} +33 -16
  65. data/lib/dry/types/map.rb +70 -32
  66. data/lib/dry/types/meta.rb +51 -0
  67. data/lib/dry/types/module.rb +10 -5
  68. data/lib/dry/types/nominal.rb +105 -14
  69. data/lib/dry/types/options.rb +12 -25
  70. data/lib/dry/types/params.rb +14 -3
  71. data/lib/dry/types/predicate_inferrer.rb +197 -0
  72. data/lib/dry/types/predicate_registry.rb +34 -0
  73. data/lib/dry/types/primitive_inferrer.rb +97 -0
  74. data/lib/dry/types/printable.rb +5 -1
  75. data/lib/dry/types/printer.rb +70 -64
  76. data/lib/dry/types/result.rb +26 -0
  77. data/lib/dry/types/schema.rb +177 -80
  78. data/lib/dry/types/schema/key.rb +48 -35
  79. data/lib/dry/types/spec/types.rb +43 -6
  80. data/lib/dry/types/sum.rb +70 -21
  81. data/lib/dry/types/type.rb +49 -0
  82. data/lib/dry/types/version.rb +3 -1
  83. metadata +91 -62
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Map
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ `Map` describes a homogeneous hashmap. This means only types of keys and values are known. You can simply imagine a map input as a list of key-value pairs.
8
+
9
+ ```ruby
10
+ int_float_hash = Types::Hash.map(Types::Integer, Types::Float)
11
+ int_float_hash[100 => 300.0, 42 => 70.0]
12
+ # => {100=>300.0, 42=>70.0}
13
+
14
+ # Only accepts mappings of integers to floats
15
+ int_float_hash[name: 'Jane']
16
+ # => Dry::Types::MapError: input key :name is invalid: type?(Integer, :name)
17
+ ```
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Type Attributes
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ Types themselves have optional attributes you can apply to get further functionality.
8
+
9
+ ### Append `.optional` to a _Type_ to allow `nil`
10
+
11
+ By default, nil values raise an error:
12
+
13
+ ``` ruby
14
+ Types::Strict::String[nil]
15
+ # => raises Dry::Types::ConstraintError
16
+ ```
17
+
18
+ Add `.optional` and `nil` values become valid:
19
+
20
+ ```ruby
21
+ optional_string = Types::Strict::String.optional
22
+
23
+ optional_string[nil]
24
+ # => nil
25
+ optional_string['something']
26
+ # => "something"
27
+ optional_string[123]
28
+ # raises Dry::Types::ConstraintError
29
+ ```
30
+
31
+ `Types::String.optional` is just syntactic sugar for `Types::Strict::Nil | Types::Strict::String`.
32
+
33
+ ### Handle optional values using Monads
34
+
35
+ The [dry-monads gem](/gems/dry-monads/) provides another approach to handling optional values by returning a [_Monad_](/gems/dry-monads/) object. This allows you to pass your type to a `Maybe(x)` block that only executes if `x` returns `Some` or `None`.
36
+
37
+ > NOTE: Requires the [dry-monads gem](/gems/dry-monads/) to be loaded.
38
+
39
+ 1. Load the `:maybe` extension in your application.
40
+
41
+ ```ruby
42
+ require 'dry-types'
43
+
44
+ Dry::Types.load_extensions(:maybe)
45
+ module Types
46
+ include Dry.Types()
47
+ end
48
+ ```
49
+
50
+ 2. Append `.maybe` to a _Type_ to return a _Monad_ object
51
+
52
+ ```ruby
53
+ x = Types::Maybe::Strict::Integer[nil]
54
+ Maybe(x) { puts(x) }
55
+
56
+ x = Types::Maybe::Coercible::String[nil]
57
+ Maybe(x) { puts(x) }
58
+
59
+ x = Types::Maybe::Strict::Integer[123]
60
+ Maybe(x) { puts(x) }
61
+
62
+ x = Types::Maybe::Strict::String[123]
63
+ Maybe(x) { puts(x) }
64
+ ```
65
+
66
+ ```ruby
67
+ Types::Maybe::Strict::Integer[nil] # None
68
+ Types::Maybe::Strict::Integer[123] # Some(123)
69
+
70
+ Types::Maybe::Coercible::Float[nil] # None
71
+ Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
72
+
73
+ # 'Maybe' types can also accessed by calling '.maybe' on a regular type:
74
+ Types::Strict::Integer.maybe # equivalent to Types::Maybe::Strict::Integer
75
+ ```
76
+
77
+ You can define your own optional types:
78
+
79
+ ``` ruby
80
+ maybe_string = Types::Strict::String.maybe
81
+
82
+ maybe_string[nil]
83
+ # => None
84
+
85
+ maybe_string[nil].fmap(&:upcase)
86
+ # => None
87
+
88
+ maybe_string['something']
89
+ # => Some('something')
90
+
91
+ maybe_string['something'].fmap(&:upcase)
92
+ # => Some('SOMETHING')
93
+
94
+ maybe_string['something'].fmap(&:upcase).value_or('NOTHING')
95
+ # => "SOMETHING"
96
+ ```
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Sum
3
+ layout: gem-single
4
+ name: dry-types
5
+ order: 7
6
+ ---
7
+
8
+ You can specify sum types using `|` operator, it is an explicit way of defining what the valid types of a value are.
9
+
10
+ For example `dry-types` defines the `Bool` type which is a sum consisting of the `True` and `False` types, expressed as `Types::True | Types::False`.
11
+
12
+ Another common case is defining that something can be either `nil` or something else:
13
+
14
+ ``` ruby
15
+ nil_or_string = Types::Nil | Types::String
16
+
17
+ nil_or_string[nil] # => nil
18
+ nil_or_string["hello"] # => "hello"
19
+
20
+ nil_or_string[123] # raises Dry::Types::ConstraintError
21
+ ```
data/dry-types.gemspec CHANGED
@@ -1,45 +1,47 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'dry/types/version'
4
6
 
5
7
  Gem::Specification.new do |spec|
6
- spec.name = "dry-types"
8
+ spec.name = 'dry-types'
7
9
  spec.version = Dry::Types::VERSION.dup
8
- spec.authors = ["Piotr Solnica"]
9
- spec.email = ["piotr.solnica@gmail.com"]
10
+ spec.authors = ['Piotr Solnica']
11
+ spec.email = ['piotr.solnica@gmail.com']
10
12
  spec.license = 'MIT'
11
13
 
12
14
  spec.summary = 'Type system for Ruby supporting coercions, constraints and complex types like structs, value objects, enums etc.'
13
15
  spec.description = spec.summary
14
- spec.homepage = "https://github.com/dry-rb/dry-types"
16
+ spec.homepage = 'https://github.com/dry-rb/dry-types'
15
17
 
16
18
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
19
  # delete this section to allow pushing this gem to any host.
18
20
  if spec.respond_to?(:metadata)
19
- spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
- spec.metadata['changelog_uri'] = "https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
21
- spec.metadata['source_code_uri'] = "https://github.com/dry-rb/dry-types"
22
- spec.metadata['bug_tracker_uri'] = "https://github.com/dry-rb/dry-types/issues"
21
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md'
23
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-types'
24
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-types/issues'
23
25
  else
24
- raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
26
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
25
27
  end
26
28
 
27
29
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - ['bin/console', 'bin/setup']
28
- spec.bindir = "exe"
30
+ spec.bindir = 'exe'
29
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
31
- spec.required_ruby_version = ">= 2.3.0"
32
+ spec.require_paths = ['lib']
33
+ spec.required_ruby_version = '>= 2.4.0'
32
34
 
33
35
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
34
- spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
35
- spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
36
36
  spec.add_runtime_dependency 'dry-container', '~> 0.3'
37
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
37
38
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2', '>= 0.2.2'
38
- spec.add_runtime_dependency 'dry-logic', '~> 0.5', '>= 0.5'
39
+ spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
40
+ spec.add_runtime_dependency 'dry-logic', '~> 1.0', '>= 1.0.2'
39
41
 
40
- spec.add_development_dependency "bundler"
41
- spec.add_development_dependency "rake", "~> 11.0"
42
- spec.add_development_dependency "rspec", "~> 3.3"
42
+ spec.add_development_dependency 'bundler'
43
43
  spec.add_development_dependency 'dry-monads', '~> 0.2'
44
+ spec.add_development_dependency 'rake', '~> 11.0'
45
+ spec.add_development_dependency 'rspec', '~> 3.3'
44
46
  spec.add_development_dependency 'yard', '~> 0.9.5'
45
47
  end
data/lib/dry-types.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types'
data/lib/dry/types.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bigdecimal'
2
4
  require 'date'
3
5
  require 'set'
@@ -22,13 +24,16 @@ require 'dry/types/module'
22
24
  require 'dry/types/errors'
23
25
 
24
26
  module Dry
27
+ # Main library namespace
28
+ #
29
+ # @api public
25
30
  module Types
26
31
  extend Dry::Core::Extensions
27
32
  extend Dry::Core::ClassAttributes
28
33
  extend Dry::Core::Deprecations[:'dry-types']
29
34
  include Dry::Core::Constants
30
35
 
31
- TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
36
+ TYPE_SPEC_REGEX = /(.+)<(.+)>/.freeze
32
37
 
33
38
  # @see Dry.Types
34
39
  def self.module(*namespaces, default: :nominal, **aliases)
@@ -40,34 +45,51 @@ module Dry
40
45
 
41
46
  # @api private
42
47
  def self.included(*)
43
- raise RuntimeError, "Import Dry.Types, not Dry::Types"
48
+ raise 'Import Dry.Types, not Dry::Types'
44
49
  end
45
50
 
51
+ # Return container with registered built-in type objects
52
+ #
46
53
  # @return [Container{String => Nominal}]
54
+ #
55
+ # @api private
47
56
  def self.container
48
57
  @container ||= Container.new
49
58
  end
50
59
 
60
+ # Check if a give type is registered
61
+ #
62
+ # @return [Boolean]
63
+ #
51
64
  # @api private
52
65
  def self.registered?(class_or_identifier)
53
66
  container.key?(identifier(class_or_identifier))
54
67
  end
55
68
 
69
+ # Register a new built-in type
70
+ #
56
71
  # @param [String] name
57
72
  # @param [Type] type
58
73
  # @param [#call,nil] block
74
+ #
59
75
  # @return [Container{String => Nominal}]
76
+ #
60
77
  # @api private
61
78
  def self.register(name, type = nil, &block)
62
79
  container.register(name, type || block.call)
63
80
  end
64
81
 
82
+ # Get a built-in type by its name
83
+ #
65
84
  # @param [String,Class] name
85
+ #
66
86
  # @return [Type,Class]
87
+ #
88
+ # @api public
67
89
  def self.[](name)
68
90
  type_map.fetch_or_store(name) do
69
91
  case name
70
- when String
92
+ when ::String
71
93
  result = name.match(TYPE_SPEC_REGEX)
72
94
 
73
95
  if result
@@ -76,7 +98,12 @@ module Dry
76
98
  else
77
99
  container[name]
78
100
  end
79
- when Class
101
+ when ::Class
102
+ warn(<<~DEPRECATION)
103
+ Using Dry::Types.[] with a class is deprecated, please use string identifiers: Dry::Types[Integer] -> Dry::Types['integer'].
104
+ If you're using dry-struct this means changing `attribute :counter, Integer` to `attribute :counter, Dry::Types['integer']` or to `attribute :counter, 'integer'`.
105
+ DEPRECATION
106
+
80
107
  type_name = identifier(name)
81
108
 
82
109
  if container.key?(type_name)
@@ -88,19 +115,29 @@ module Dry
88
115
  end
89
116
  end
90
117
 
118
+ # Infer a type identifier from the provided class
119
+ #
91
120
  # @param [#to_s] klass
121
+ #
92
122
  # @return [String]
93
123
  def self.identifier(klass)
94
124
  Inflector.underscore(klass).tr('/', '.')
95
125
  end
96
126
 
127
+ # Cached type map
128
+ #
97
129
  # @return [Concurrent::Map]
130
+ #
131
+ # @api private
98
132
  def self.type_map
99
133
  @type_map ||= Concurrent::Map.new
100
134
  end
101
135
 
102
- # List of type keys defined in {Dry::Types.container}
103
- # @return [<String>]
136
+ # List of type keys defined in {Dry::Types.container}
137
+ #
138
+ # @return [String]
139
+ #
140
+ # @api private
104
141
  def self.type_keys
105
142
  container.keys
106
143
  end
@@ -112,8 +149,8 @@ module Dry
112
149
  if container.keys.any? { |key| key.split('.')[0] == underscored }
113
150
  raise NameError,
114
151
  'dry-types does not define constants for default types. '\
115
- 'You can access the predefined types with [], e.g. Dry::Types["strict.integer"] '\
116
- 'or generate a module with types using Dry::Types.module'
152
+ 'You can access the predefined types with [], e.g. Dry::Types["integer"] '\
153
+ 'or generate a module with types using Dry.Types()'
117
154
  else
118
155
  super
119
156
  end
@@ -126,26 +163,26 @@ module Dry
126
163
  #
127
164
  # module Types
128
165
  # # imports all types as constants, uses modules for namespaces
129
- # include Dry::Types.module
166
+ # include Dry::Types()
130
167
  # end
131
- # # nominal types are exported by default
168
+ # # strict types are exported by default
132
169
  # Types::Integer
133
- # # => #<Dry::Types[Nominal<Integer>]>
134
- # Types::Strict::Integer
135
170
  # # => #<Dry::Types[Constrained<Nominal<Integer> rule=[type?(Integer)]>]>
171
+ # Types::Nominal::Integer
172
+ # # => #<Dry::Types[Nominal<Integer>]>
136
173
  #
137
174
  # @example changing default types
138
175
  #
139
176
  # module Types
140
- # include Dry::Types(default: :strict)
177
+ # include Dry::Types(default: :nominal)
141
178
  # end
142
179
  # Types::Integer
143
- # # => #<Dry::Types[Constrained<Nominal<Integer> rule=[type?(Integer)]>]>
180
+ # # => #<Dry::Types[Nominal<Integer>]>
144
181
  #
145
182
  # @example cherry-picking namespaces
146
183
  #
147
184
  # module Types
148
- # include Dry::Types.module(:strict, :coercible)
185
+ # include Dry::Types(:strict, :coercible)
149
186
  # end
150
187
  # # cherry-picking discards default types,
151
188
  # # provide the :default option along with the list of
@@ -154,7 +191,7 @@ module Dry
154
191
  #
155
192
  # @example custom names
156
193
  # module Types
157
- # include Dry::Types.module(coercible: :Kernel)
194
+ # include Dry::Types(coercible: :Kernel)
158
195
  # end
159
196
  # Types::Kernel::Integer
160
197
  # # => #<Dry::Types[Constructor<Nominal<Integer> fn=Kernel.Integer>]>
@@ -162,12 +199,18 @@ module Dry
162
199
  # @param [Array<Symbol>] namespaces List of type namespaces to export
163
200
  # @param [Symbol] default Default namespace to export
164
201
  # @param [Hash{Symbol => Symbol}] aliases Optional renamings, like strict: :Draconian
202
+ #
165
203
  # @return [Dry::Types::Module]
166
204
  #
167
- # @see Dry::types::Module
205
+ # @see Dry::Types::Module
206
+ #
207
+ # @api public
208
+ #
209
+ # rubocop:disable Naming/MethodName
168
210
  def self.Types(*namespaces, default: Types::Undefined, **aliases)
169
211
  Types::Module.new(Types.container, *namespaces, default: default, **aliases)
170
212
  end
213
+ # rubocop:enable Naming/MethodName
171
214
  end
172
215
 
173
216
  require 'dry/types/core' # load built-in types
data/lib/dry/types/any.rb CHANGED
@@ -1,36 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
- Any = Class.new(Nominal) do
5
+ # Any is a nominal type that defines Object as the primitive class
6
+ #
7
+ # This type is useful in places where you can't be specific about the type
8
+ # and anything is acceptable.
9
+ #
10
+ # @api public
11
+ class AnyClass < Nominal
4
12
  def self.name
5
13
  'Any'
6
14
  end
7
15
 
16
+ # @api private
8
17
  def initialize(**options)
9
18
  super(::Object, options)
10
19
  end
11
20
 
12
21
  # @return [String]
22
+ #
23
+ # @api public
13
24
  def name
14
25
  'Any'
15
26
  end
16
27
 
17
- # @param [Object] any input is valid
18
- # @return [true]
19
- def valid?(_)
20
- true
21
- end
22
- alias_method :===, :valid?
23
-
24
28
  # @param [Hash] new_options
29
+ #
25
30
  # @return [Type]
26
- def with(**new_options)
31
+ #
32
+ # @api public
33
+ def with(new_options)
27
34
  self.class.new(**options, meta: @meta, **new_options)
28
35
  end
29
36
 
30
37
  # @return [Array]
38
+ #
39
+ # @api public
31
40
  def to_ast(meta: true)
32
41
  [:any, meta ? self.meta : EMPTY_HASH]
33
42
  end
34
- end.new
43
+ end
44
+
45
+ Any = AnyClass.new
35
46
  end
36
47
  end