dry-types 1.0.0 → 1.2.1

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