dry-types 0.15.0 → 1.2.0

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