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
@@ -0,0 +1,156 @@
1
+ ---
2
+ title: Introduction
3
+ layout: gem-single
4
+ type: gem
5
+ name: dry-types
6
+ sections:
7
+ - getting-started
8
+ - built-in-types
9
+ - optional-values
10
+ - default-values
11
+ - sum
12
+ - constraints
13
+ - hash-schemas
14
+ - array-with-member
15
+ - enum
16
+ - map
17
+ - custom-types
18
+ - extensions
19
+ ---
20
+
21
+ `dry-types` is a simple and extendable type system for Ruby; useful for value coercions, applying constraints, defining complex structs or value objects and more. It was created as a successor to [Virtus](https://github.com/solnic/virtus).
22
+
23
+ ### Example usage
24
+
25
+ ```ruby
26
+ require 'dry-types'
27
+ require 'dry-struct'
28
+
29
+ module Types
30
+ include Dry.Types()
31
+ end
32
+
33
+ User = Dry.Struct(name: Types::String, age: Types::Integer)
34
+
35
+ User.new(name: 'Bob', age: 35)
36
+ # => #<User name="Bob" age=35>
37
+ ```
38
+
39
+ See [Built-in Types](docs::built-in-types/) for a full list of available types.
40
+
41
+ By themselves, the basic type definitions like `Types::String` and `Types::Integer` don't do anything except provide documentation about which type an attribute is expected to have. However, there are many more advanced possibilities:
42
+
43
+ - `Strict` types will raise an error if passed an attribute of the wrong type:
44
+
45
+ ```ruby
46
+ class User < Dry::Struct
47
+ attribute :name, Types::Strict::String
48
+ attribute :age, Types::Strict::Integer
49
+ end
50
+
51
+ User.new(name: 'Bob', age: '18')
52
+ # => Dry::Struct::Error: [User.new] "18" (String) has invalid type for :age
53
+ ```
54
+
55
+ - `Coercible` types will attempt to convert an attribute to the correct class
56
+ using Ruby's built-in coercion methods:
57
+
58
+ ```ruby
59
+ class User < Dry::Struct
60
+ attribute :name, Types::Coercible::String
61
+ attribute :age, Types::Coercible::Integer
62
+ end
63
+
64
+ User.new(name: 'Bob', age: '18')
65
+ # => #<User name="Bob" age=18>
66
+ User.new(name: 'Bob', age: 'not coercible')
67
+ # => ArgumentError: invalid value for Integer(): "not coercible"
68
+ ```
69
+
70
+ - Use `.optional` to denote that an attribute can be `nil` (see [Optional Values](docs::optional-values)):
71
+
72
+ ```ruby
73
+ class User < Dry::Struct
74
+ attribute :name, Types::String
75
+ attribute :age, Types::Integer.optional
76
+ end
77
+
78
+ User.new(name: 'Bob', age: nil)
79
+ # => #<User name="Bob" age=nil>
80
+ # name is not optional:
81
+ User.new(name: nil, age: 18)
82
+ # => Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
83
+ # keys must still be present:
84
+ User.new(name: 'Bob')
85
+ # => Dry::Struct::Error: [User.new] :age is missing in Hash input
86
+ ```
87
+
88
+ - Add custom constraints (see [Constraints](docs::constraints.html)):
89
+
90
+ ```ruby
91
+ class User < Dry::Struct
92
+ attribute :name, Types::Strict::String
93
+ attribute :age, Types::Strict::Integer.constrained(gteq: 18)
94
+ end
95
+
96
+ User.new(name: 'Bob', age: 17)
97
+ # => Dry::Struct::Error: [User.new] 17 (Fixnum) has invalid type for :age
98
+ ```
99
+
100
+ - Add custom metadata to a type:
101
+
102
+ ```ruby
103
+ class User < Dry::Struct
104
+ attribute :name, Types::String
105
+ attribute :age, Types::Integer.meta(info: 'extra info about age')
106
+ end
107
+ ```
108
+
109
+ - Pass values directly to `Dry::Types` without creating an object using `[]`:
110
+
111
+ ```ruby
112
+ Types::Strict::String["foo"]
113
+ # => "foo"
114
+ Types::Strict::String["10000"]
115
+ # => "10000"
116
+ Types::Coercible::String[10000]
117
+ # => "10000"
118
+ Types::Strict::String[10000]
119
+ # Dry::Types::ConstraintError: 1000 violates constraints
120
+ ```
121
+
122
+ ### Features
123
+
124
+ * Support for [constrained types](docs::constraints)
125
+ * Support for [optional values](docs::optional-values)
126
+ * Support for [default values](docs::default-values)
127
+ * Support for [sum types](docs::sum)
128
+ * Support for [enums](docs::enum)
129
+ * Support for [hash type with type schemas](docs::hash-schemas)
130
+ * Support for [array type with members](docs::array-with-member)
131
+ * Support for arbitrary meta information
132
+ * Support for typed struct objects via [dry-struct](/gems/dry-struct)
133
+ * Types are [categorized](docs::built-in-types), which is especially important for optimized and dedicated coercion logic
134
+ * Types are composable and reusable objects
135
+ * No const-missing magic and complicated const lookups
136
+ * Roughly 6-10 x faster than Virtus
137
+
138
+ ### Use cases
139
+
140
+ `dry-types` is suitable for many use-cases, for example:
141
+
142
+ * Value coercions
143
+ * Processing arrays
144
+ * Processing hashes with explicit schemas
145
+ * Defining various domain-specific information shared between multiple parts of your application
146
+ * Annotating objects
147
+
148
+ ### Other gems using dry-types
149
+
150
+ `dry-types` is often used as a low-level abstraction. The following gems use it already:
151
+
152
+ * [dry-struct](/gems/dry-struct)
153
+ * [dry-initializer](/gems/dry-initializer)
154
+ * [Hanami](http://hanamirb.org)
155
+ * [rom-rb](http://rom-rb.org)
156
+ * [Trailblazer](http://trailblazer.to)
@@ -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,35 @@
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
+ See [Maybe](docs::extensions/maybe) extension for another approach to handling optional values by returning a [_Monad_](/gems/dry-monads/) object.
@@ -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,47 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'dry/types/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = "dry-types"
8
+ spec.name = 'dry-types'
9
9
  spec.version = Dry::Types::VERSION.dup
10
- spec.authors = ["Piotr Solnica"]
11
- spec.email = ["piotr.solnica@gmail.com"]
10
+ spec.authors = ['Piotr Solnica']
11
+ spec.email = ['piotr.solnica@gmail.com']
12
12
  spec.license = 'MIT'
13
13
 
14
14
  spec.summary = 'Type system for Ruby supporting coercions, constraints and complex types like structs, value objects, enums etc.'
15
15
  spec.description = spec.summary
16
- spec.homepage = "https://github.com/dry-rb/dry-types"
16
+ spec.homepage = 'https://github.com/dry-rb/dry-types'
17
17
 
18
18
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
19
19
  # delete this section to allow pushing this gem to any host.
20
20
  if spec.respond_to?(:metadata)
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"
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'
25
25
  else
26
- 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.'
27
27
  end
28
28
 
29
29
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - ['bin/console', 'bin/setup']
30
- spec.bindir = "exe"
30
+ spec.bindir = 'exe'
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
- spec.required_ruby_version = ">= 2.4.0"
32
+ spec.require_paths = ['lib']
33
+ spec.required_ruby_version = '>= 2.4.0'
34
34
 
35
35
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
36
- spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
37
- spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
38
36
  spec.add_runtime_dependency 'dry-container', '~> 0.3'
37
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
39
38
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2', '>= 0.2.2'
40
- spec.add_runtime_dependency 'dry-logic', '~> 1.0'
39
+ spec.add_runtime_dependency 'dry-inflector', '~> 0.1', '>= 0.1.2'
40
+ spec.add_runtime_dependency 'dry-logic', '~> 1.0', '>= 1.0.2'
41
41
 
42
- spec.add_development_dependency "bundler"
43
- spec.add_development_dependency "rake", "~> 11.0"
44
- spec.add_development_dependency "rspec", "~> 3.3"
42
+ spec.add_development_dependency 'bundler'
45
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'
46
46
  spec.add_development_dependency 'yard', '~> 0.9.5'
47
47
  end
data/lib/dry/types.rb CHANGED
@@ -33,7 +33,7 @@ module Dry
33
33
  extend Dry::Core::Deprecations[:'dry-types']
34
34
  include Dry::Core::Constants
35
35
 
36
- TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
36
+ TYPE_SPEC_REGEX = /(.+)<(.+)>/.freeze
37
37
 
38
38
  # @see Dry.Types
39
39
  def self.module(*namespaces, default: :nominal, **aliases)
@@ -45,7 +45,7 @@ module Dry
45
45
 
46
46
  # @api private
47
47
  def self.included(*)
48
- raise RuntimeError, "Import Dry.Types, not Dry::Types"
48
+ raise 'Import Dry.Types, not Dry::Types'
49
49
  end
50
50
 
51
51
  # Return container with registered built-in type objects
@@ -89,7 +89,7 @@ module Dry
89
89
  def self.[](name)
90
90
  type_map.fetch_or_store(name) do
91
91
  case name
92
- when String
92
+ when ::String
93
93
  result = name.match(TYPE_SPEC_REGEX)
94
94
 
95
95
  if result
@@ -98,7 +98,12 @@ module Dry
98
98
  else
99
99
  container[name]
100
100
  end
101
- 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
+
102
107
  type_name = identifier(name)
103
108
 
104
109
  if container.key?(type_name)
data/lib/dry/types/any.rb CHANGED
@@ -15,7 +15,7 @@ module Dry
15
15
 
16
16
  # @api private
17
17
  def initialize(**options)
18
- super(::Object, options)
18
+ super(::Object, **options)
19
19
  end
20
20
 
21
21
  # @return [String]
@@ -30,7 +30,7 @@ module Dry
30
30
  # @return [Type]
31
31
  #
32
32
  # @api public
33
- def with(new_options)
33
+ def with(**new_options)
34
34
  self.class.new(**options, meta: @meta, **new_options)
35
35
  end
36
36
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/types/array/member'
4
+ require 'dry/types/array/constructor'
4
5
 
5
6
  module Dry
6
7
  module Types
@@ -24,6 +25,11 @@ module Dry
24
25
 
25
26
  Array::Member.new(primitive, **options, member: member)
26
27
  end
28
+
29
+ # @api private
30
+ def constructor_type
31
+ ::Dry::Types::Array::Constructor
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/types/constructor'
4
+
5
+ module Dry
6
+ module Types
7
+ # @api public
8
+ class Array < Nominal
9
+ # @api private
10
+ class Constructor < ::Dry::Types::Constructor
11
+ # @api private
12
+ def constructor_type
13
+ ::Dry::Types::Array::Constructor
14
+ end
15
+
16
+ # @return [Lax]
17
+ #
18
+ # @api public
19
+ def lax
20
+ Lax.new(type.lax.constructor(fn, meta: meta))
21
+ end
22
+
23
+ # @see Dry::Types::Array#of
24
+ #
25
+ # @api public
26
+ def of(member)
27
+ type.of(member).constructor(fn, meta: meta)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry/types/array/constructor'
4
+
3
5
  module Dry
4
6
  module Types
5
7
  class Array < Nominal
@@ -16,7 +18,7 @@ module Dry
16
18
  # @option options [Type] :member
17
19
  #
18
20
  # @api private
19
- def initialize(primitive, options = {})
21
+ def initialize(primitive, **options)
20
22
  @member = options.fetch(:member)
21
23
  super
22
24
  end
@@ -87,7 +89,7 @@ module Dry
87
89
  block ? yield(failure) : failure
88
90
  end
89
91
  else
90
- failure = failure(input, "#{input} is not an array")
92
+ failure = failure(input, CoercionError.new("#{input} is not an array"))
91
93
  block ? yield(failure) : failure
92
94
  end
93
95
  end
@@ -98,7 +100,7 @@ module Dry
98
100
  #
99
101
  # @api public
100
102
  def lax
101
- Lax.new(Member.new(primitive, { **options, member: member.lax }))
103
+ Lax.new(Member.new(primitive, **options, member: member.lax, meta: meta))
102
104
  end
103
105
 
104
106
  # @see Nominal#to_ast
@@ -111,6 +113,11 @@ module Dry
111
113
  [:array, [member, meta ? self.meta : EMPTY_HASH]]
112
114
  end
113
115
  end
116
+
117
+ # @api private
118
+ def constructor_type
119
+ ::Dry::Types::Array::Constructor
120
+ end
114
121
  end
115
122
  end
116
123
  end