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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $LOAD_PATH.unshift('lib')
2
4
 
3
5
  require 'bundler/setup'
@@ -5,11 +7,13 @@ require 'dry-types'
5
7
 
6
8
  module SchemaBench
7
9
  def self.hash_schema(type)
8
- Dry::Types['nominal.hash'].public_send(type,
9
- email: Dry::Types['nominal.string'],
10
- age: Dry::Types['params.integer'],
11
- admin: Dry::Types['params.bool'],
12
- address: Dry::Types['nominal.hash'].public_send(type,
10
+ Dry::Types['nominal.hash'].public_send(
11
+ type,
12
+ email: Dry::Types['nominal.string'],
13
+ age: Dry::Types['params.integer'],
14
+ admin: Dry::Types['params.bool'],
15
+ address: Dry::Types['nominal.hash'].public_send(
16
+ type,
13
17
  city: Dry::Types['nominal.string'],
14
18
  street: Dry::Types['nominal.string']
15
19
  )
@@ -29,7 +33,7 @@ module SchemaBench
29
33
  age: '20',
30
34
  admin: '1',
31
35
  address: { city: 'NYC', street: 'Street 1/2' }
32
- }
36
+ }.freeze
33
37
  end
34
38
 
35
39
  require 'benchmark/ips'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup'
4
+
5
+ schema = Dry::Types['params.hash'].schema(
6
+ email?: 'string',
7
+ age?: 'params.integer'
8
+ ).lax
9
+
10
+ params = { email: 'jane@doe.org', age: '19' }
11
+
12
+ Benchmark.ips do |x|
13
+ x.report("valid input") { schema.(params) }
14
+ x.compare!
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup'
4
+
5
+ INVALID_INPUT = {
6
+ name: :John,
7
+ age: '20',
8
+ email: nil
9
+ }.freeze
10
+
11
+ profile do
12
+ 10_000.times do
13
+ PersonSchema.(INVALID_INPUT)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup'
4
+
5
+ Schema = Dry::Types['params.hash'].schema(
6
+ email?: 'string',
7
+ age?: 'coercible.integer'
8
+ ).lax
9
+
10
+ ValidInput = { email: 'jane@doe.org', age: '19' }.freeze
11
+
12
+ profile do
13
+ 10_000.times do
14
+ Schema.(ValidInput)
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup'
4
+
5
+ VALID_INPUT = {
6
+ name: 'John',
7
+ age: 20,
8
+ email: 'john@doe.com'
9
+ }.freeze
10
+
11
+ profile do
12
+ 10_000.times do
13
+ PersonSchema.(VALID_INPUT)
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup'
4
+
5
+ VALID_INPUT = {
6
+ name: 'John',
7
+ age: 20,
8
+ email: 'john@doe.com'
9
+ }
10
+
11
+ INVALID_INPUT = {
12
+ name: :John,
13
+ age: '20',
14
+ email: nil
15
+ }
16
+
17
+ Benchmark.ips do |x|
18
+ x.report("valid input") { PersonSchema.(VALID_INPUT) }
19
+ x.report("invalid input") { PersonSchema.(INVALID_INPUT) }
20
+ x.compare!
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark/ips'
4
+ require 'hotch'
5
+ ENV['HOTCH_VIEWER'] ||= 'open'
6
+
7
+ require 'dry/types'
8
+
9
+ PersonSchema = Dry::Types['hash'].schema(
10
+ name: 'string',
11
+ age: 'integer',
12
+ email: 'string'
13
+ ).lax
14
+
15
+ def profile(&block)
16
+ Hotch(filter: 'Dry', &block)
17
+ end
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Array With Member
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ The built-in array type supports defining the member's type:
8
+
9
+ ``` ruby
10
+ PostStatuses = Types::Array.of(Types::Coercible::String)
11
+
12
+ PostStatuses[[:foo, :bar]] # ["foo", "bar"]
13
+ ```
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: Built-in Types
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ Built-in types are grouped under 6 categories:
8
+
9
+ - `nominal` - base type definitions with a primitive class and options
10
+ - `strict` - constrained types with a primitive type check applied to input
11
+ - `coercible` - types with constructors using kernel coercions
12
+ - `params` - types with constructors performing non-strict coercions specific to HTTP parameters
13
+ - `json` - types with constructors performing non-strict coercions specific to JSON
14
+ - `maybe` - types accepting either nil or a specific primitive type
15
+
16
+ ### Categories
17
+
18
+ Assuming you included `Dry::Types` ([see instructions](/gems/dry-types/1.0/getting-started)) in a module called `Types`:
19
+
20
+ * Nominal types:
21
+ - `Types::Nominal::Any`
22
+ - `Types::Nominal::Nil`
23
+ - `Types::Nominal::Symbol`
24
+ - `Types::Nominal::Class`
25
+ - `Types::Nominal::True`
26
+ - `Types::Nominal::False`
27
+ - `Types::Nominal::Bool`
28
+ - `Types::Nominal::Integer`
29
+ - `Types::Nominal::Float`
30
+ - `Types::Nominal::Decimal`
31
+ - `Types::Nominal::String`
32
+ - `Types::Nominal::Date`
33
+ - `Types::Nominal::DateTime`
34
+ - `Types::Nominal::Time`
35
+ - `Types::Nominal::Array`
36
+ - `Types::Nominal::Hash`
37
+
38
+ * `Strict` types will raise an error if passed a value of the wrong type:
39
+ - `Types::Strict::Nil`
40
+ - `Types::Strict::Symbol`
41
+ - `Types::Strict::Class`
42
+ - `Types::Strict::True`
43
+ - `Types::Strict::False`
44
+ - `Types::Strict::Bool`
45
+ - `Types::Strict::Integer`
46
+ - `Types::Strict::Float`
47
+ - `Types::Strict::Decimal`
48
+ - `Types::Strict::String`
49
+ - `Types::Strict::Date`
50
+ - `Types::Strict::DateTime`
51
+ - `Types::Strict::Time`
52
+ - `Types::Strict::Array`
53
+ - `Types::Strict::Hash`
54
+
55
+ > All types in the `strict` category are [constrained](/gems/dry-types/1.0/constraints) by a type-check that is applied to make sure that the input is an instance of the primitive:
56
+
57
+ ``` ruby
58
+ Types::Strict::Integer[1] # => 1
59
+ Types::Strict::Integer['1'] # => raises Dry::Types::ConstraintError
60
+ ```
61
+
62
+ * `Coercible` types will attempt to cast values to the correct class using kernel coercion methods:
63
+ - `Types::Coercible::String`
64
+ - `Types::Coercible::Integer`
65
+ - `Types::Coercible::Float`
66
+ - `Types::Coercible::Decimal`
67
+ - `Types::Coercible::Array`
68
+ - `Types::Coercible::Hash`
69
+
70
+ * Types suitable for `Params` param processing with coercions:
71
+ - `Types::Params::Nil`
72
+ - `Types::Params::Date`
73
+ - `Types::Params::DateTime`
74
+ - `Types::Params::Time`
75
+ - `Types::Params::True`
76
+ - `Types::Params::False`
77
+ - `Types::Params::Bool`
78
+ - `Types::Params::Integer`
79
+ - `Types::Params::Float`
80
+ - `Types::Params::Decimal`
81
+ - `Types::Params::Array`
82
+ - `Types::Params::Hash`
83
+
84
+ * Types suitable for `JSON` processing with coercions:
85
+ - `Types::JSON::Nil`
86
+ - `Types::JSON::Date`
87
+ - `Types::JSON::DateTime`
88
+ - `Types::JSON::Time`
89
+ - `Types::JSON::Decimal`
90
+ - `Types::JSON::Array`
91
+ - `Types::JSON::Hash`
92
+
93
+ * `Maybe` strict types:
94
+ - `Types::Maybe::Strict::Class`
95
+ - `Types::Maybe::Strict::String`
96
+ - `Types::Maybe::Strict::Symbol`
97
+ - `Types::Maybe::Strict::True`
98
+ - `Types::Maybe::Strict::False`
99
+ - `Types::Maybe::Strict::Integer`
100
+ - `Types::Maybe::Strict::Float`
101
+ - `Types::Maybe::Strict::Decimal`
102
+ - `Types::Maybe::Strict::Date`
103
+ - `Types::Maybe::Strict::DateTime`
104
+ - `Types::Maybe::Strict::Time`
105
+ - `Types::Maybe::Strict::Array`
106
+ - `Types::Maybe::Strict::Hash`
107
+
108
+ * `Maybe` coercible types:
109
+ - `Types::Maybe::Coercible::String`
110
+ - `Types::Maybe::Coercible::Integer`
111
+ - `Types::Maybe::Coercible::Float`
112
+ - `Types::Maybe::Coercible::Decimal`
113
+ - `Types::Maybe::Coercible::Array`
114
+ - `Types::Maybe::Coercible::Hash`
115
+
116
+ > `Maybe` types are not available by default - they must be loaded using `Dry::Types.load_extensions(:maybe)`. See [Optional Values](/gems/dry-types/1.0/optional-values) for more information.
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Constraints
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ You can create constrained types that will use validation rules to check that the input is not violating any of the configured constraints. You can treat it as a lower level guarantee that you're not instantiating objects that are broken.
8
+
9
+ All types support the constraints API, but not all constraints are suitable for a particular primitive, it's up to you to set up constraints that make sense.
10
+
11
+ Under the hood it uses [`dry-logic`](/gems/dry-logic) and all of its predicates are supported.
12
+
13
+ ``` ruby
14
+ string = Types::String.constrained(min_size: 3)
15
+
16
+ string['foo']
17
+ # => "foo"
18
+
19
+ string['fo']
20
+ # => Dry::Types::ConstraintError: "fo" violates constraints
21
+
22
+ email = Types::String.constrained(
23
+ format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
24
+ )
25
+
26
+ email["jane@doe.org"]
27
+ # => "jane@doe.org"
28
+
29
+ email["jane"]
30
+ # => Dry::Types::ConstraintError: "jane" violates constraints
31
+ ```
@@ -0,0 +1,93 @@
1
+ ---
2
+ title: Custom Types
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ There are a bunch of helpers for building your own types based on existing classes and values. These helpers are automatically defined if you're imported types in a module.
8
+
9
+ ### `Types.Instance`
10
+
11
+ `Types.Instance` builds a type that checks if a value has the given class.
12
+
13
+ ```ruby
14
+ range_type = Types.Instance(Range)
15
+ range_type[1..2] # => 1..2
16
+ ```
17
+
18
+ ### `Types.Value`
19
+
20
+ `Types.Value` builds a type that checks a value for equality (using `==`).
21
+
22
+ ```ruby
23
+ valid = Types.Value('valid')
24
+ valid['valid'] # => 'valid'
25
+ valid['invalid']
26
+ # => Dry::Types::ConstraintError: "invalid" violates constraints (eql?("valid", "invalid") failed)
27
+ ```
28
+
29
+ ### `Types.Constant`
30
+
31
+ `Types.Constant` builds a type that checks a value for identity (using `equal?`).
32
+
33
+ ```ruby
34
+ valid = Types.Constant(:valid)
35
+ valid[:valid] # => :valid
36
+ valid[:invalid]
37
+ # => Dry::Types::ConstraintError: :invalid violates constraints (is?(:valid, :invalid) failed)
38
+ ```
39
+
40
+ ### `Types.Constructor`
41
+
42
+ `Types.Constructor` builds a new constructor type for the given class. By default uses the `new` method as a constructor.
43
+
44
+ ```ruby
45
+ user_type = Types.Constructor(User)
46
+
47
+ # It is equivalent to User.new(name: 'John')
48
+ user_type[name: 'John']
49
+
50
+ # Using a block
51
+ user_type = Types.Constructor(User) { |values| User.new(values) }
52
+ ```
53
+
54
+ ### `Types.Nominal`
55
+
56
+ `Types.Nominal` wraps the given class with a simple definition without any behavior attached.
57
+
58
+ ```ruby
59
+ int = Types.Nominal(Integer)
60
+ int[1] # => 1
61
+
62
+ # The type doesn't have any checks
63
+ int['one'] # => 'one'
64
+ ```
65
+
66
+ ### `Types.Hash`
67
+
68
+ `Types.Hash` builds a new hash schema.
69
+
70
+ ```ruby
71
+ # In the full form
72
+ Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
73
+
74
+ # Using Types.Hash()
75
+ Types.Hash(:permissive, name: Types::String, age: Types::Coercible::Integer)
76
+ ```
77
+
78
+ ### `Types.Array`
79
+
80
+ `Types.Array` is a shortcut for `Types::Array.of`
81
+
82
+ ```ruby
83
+ ListOfStrings = Types.Array(Types::String)
84
+ ```
85
+
86
+ ### `Types.Interface`
87
+
88
+ `Types.Interface` builds a type that checks a value responds to given methods.
89
+
90
+ ```ruby
91
+ Callable = Types.Interface(:call)
92
+ Contact = Types.Interface(:name, :phone)
93
+ ```
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: Default Values
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ A type with a default value will return the configured value when the input is not defined:
8
+
9
+ ``` ruby
10
+ PostStatus = Types::String.default('draft')
11
+
12
+ PostStatus[] # "draft"
13
+ PostStatus["published"] # "published"
14
+ PostStatus[true] # raises ConstraintError
15
+ ```
16
+
17
+ It works with a callable value:
18
+
19
+ ``` ruby
20
+ CallableDateTime = Types::DateTime.default { DateTime.now }
21
+
22
+ CallableDateTime[]
23
+ # => #<DateTime: 2017-05-06T00:43:06+03:00 ((2457879j,78186s,649279000n),+10800s,2299161j)>
24
+ CallableDateTime[]
25
+ # => #<DateTime: 2017-05-06T00:43:07+03:00 ((2457879j,78187s,635494000n),+10800s,2299161j)>
26
+ ```
27
+
28
+ `Dry::Types::Undefined` can be passed explicitly as a missing value:
29
+
30
+ ```ruby
31
+ PostStatus = Types::String.default('draft')
32
+
33
+ PostStatus[Dry::Types::Undefined] # "draft"
34
+ ```
35
+
36
+ It also receives the type constructor as an argument:
37
+
38
+ ```ruby
39
+ CallableDateTime = Types::DateTime.constructor(&:to_datetime).default { |type| type[Time.now] }
40
+
41
+ CallableDateTime[Time.now]
42
+ # => #<DateTime: 2017-05-06T01:13:06+03:00 ((2457879j,79986s,63464000n),+10800s,2299161j)>
43
+ CallableDateTime[Date.today]
44
+ # => #<DateTime: 2017-05-06T00:00:00+00:00 ((2457880j,0s,0n),+0s,2299161j)>
45
+ CallableDateTime[]
46
+ # => #<DateTime: 2017-05-06T01:13:06+03:00 ((2457879j,79986s,63503000n),+10800s,2299161j)>
47
+ ```
48
+
49
+ **Be careful:** types will return the **same instance** of the default value every time. This may cause problems if you mutate the returned value after receiving it:
50
+
51
+ ```ruby
52
+ default_0 = PostStatus.()
53
+ # => "draft"
54
+ default_1 = PostStatus.()
55
+ # => "draft"
56
+
57
+ # Both variables point to the same string:
58
+ default_0.object_id == default_1.object_id
59
+ # => true
60
+
61
+ # Mutating the string will change the default value of type:
62
+ default_0 << '_mutated'
63
+ PostStatus.(nil)
64
+ # => "draft_mutated" # not "draft"
65
+ ```
66
+
67
+ You can guard against these kind of errors by calling `freeze` when setting the default:
68
+
69
+ ```ruby
70
+ PostStatus = Types::Params::String.default('draft'.freeze)
71
+ default = PostStatus.()
72
+ default << 'attempt to mutate default'
73
+ # => RuntimeError: can't modify frozen string
74
+
75
+ # If you really want to mutate it, call `dup` on it first:
76
+ default = default.dup
77
+ default << "this time it'll work"
78
+ ```
79
+
80
+ **Warning on using with constrained types**: If the value passed to the `.default` block does not match the type constraints, this will not throw an exception, because it is not passed to the constructor and will be used as is.
81
+
82
+ ```ruby
83
+ CallableDateTime = Types::DateTime.constructor(&:to_datetime).default { Time.now }
84
+
85
+ CallableDateTime[Time.now]
86
+ # => #<DateTime: 2017-05-06T00:50:09+03:00 ((2457879j,78609s,839588000n),+10800s,2299161j)>
87
+ CallableDateTime[Date.today]
88
+ # => #<DateTime: 2017-05-06T00:00:00+00:00 ((2457880j,0s,0n),+0s,2299161j)>
89
+ CallableDateTime[]
90
+ # => 2017-05-06 00:50:15 +0300
91
+ ```