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
@@ -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
+ ```