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,69 @@
1
+ ---
2
+ title: Enum
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ In many cases you may want to define an enum. For example, in a blog application a post may have a finite list of statuses. Apart from accessing the current status value, it is useful to have all possible values accessible too. Furthermore, an enum can be a map from, e.g., strings to integers. This is useful for mapping externally-provided integer values to human-readable strings without explicit conversions, see examples.
8
+
9
+ ``` ruby
10
+ require 'dry-types'
11
+ require 'dry-struct'
12
+
13
+ module Types
14
+ include Dry.Types()
15
+ end
16
+
17
+ class Post < Dry::Struct
18
+ Statuses = Types::String.enum('draft', 'published', 'archived')
19
+
20
+ attribute :title, Types::String
21
+ attribute :body, Types::String
22
+ attribute :status, Statuses
23
+ end
24
+
25
+ # enum values are frozen, let's be paranoid, doesn't hurt and have potential to
26
+ # eliminate silly bugs
27
+ Post::Statuses.values.frozen? # => true
28
+ Post::Statuses.values.all?(&:frozen?) # => true
29
+
30
+ Post::Statuses['draft'] # => "draft"
31
+
32
+ # it'll raise if something silly was passed in
33
+ Post::Statuses['something silly']
34
+ # => Dry::Types::ConstraintError: "something silly" violates constraints
35
+
36
+ # nil is considered as something silly too
37
+ Post::Statuses[nil]
38
+ # => Dry::Types::ConstraintError: nil violates constraints
39
+ ```
40
+
41
+ Note that if you want to define an enum type with a default, you must call `.default` *before* calling `.enum`, not the other way around:
42
+
43
+ ```ruby
44
+ # this is the correct usage:
45
+ Dry::Types::String.default('red').enum('blue', 'green', 'red')
46
+
47
+ # this will raise an error:
48
+ Dry::Types::String.enum('blue', 'green', 'red').default('red')
49
+ ```
50
+
51
+ ### Mappings
52
+
53
+ A classic example is mapping integers coming from somewhere (API/database/etc) to something more understandable:
54
+
55
+ ```ruby
56
+ class Cell < Dry::Struct
57
+ attribute :state, Types::String.enum('locked' => 0, 'open' => 1)
58
+ end
59
+
60
+
61
+ Cell.new(state: 'locked')
62
+ # => #<Cell state="locked">
63
+
64
+ # Integers are accepted too
65
+ Cell.new(state: 0)
66
+ # => #<Cell state="locked">
67
+ Cell.new(state: 1)
68
+ # => #<Cell state="open">
69
+ ```
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Getting Started
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ ### Using `Dry::Types` in Your Application
8
+
9
+ 1. Make `Dry::Types` available to the application by creating a namespace that includes `Dry::Types`:
10
+
11
+ ```ruby
12
+ module Types
13
+ include Dry.Types()
14
+ end
15
+ ```
16
+
17
+ 2. Reload the environment, & type `Types::Coercible::String` in the ruby console to confirm it worked:
18
+
19
+ ``` ruby
20
+ Types::Coercible::String
21
+ # => #<Dry::Types::Constructor type=#<Dry::Types::Definition primitive=String options={}>>
22
+ ```
23
+
24
+ ### Creating Your First Type
25
+
26
+ 1. Define a struct's types by passing the name & type to the `attribute` method:
27
+
28
+ ```ruby
29
+ class User < Dry::Struct
30
+ attribute :name, Types::String
31
+ end
32
+ ```
33
+
34
+ 2. Define [Custom Types](/gems/dry-types/1.0/custom-types) in the `Types` module, then pass the name & type to `attribute`:
35
+
36
+ ```ruby
37
+ module Types
38
+ include Dry.Types()
39
+
40
+ Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
41
+ Age = Integer.constrained(gt: 18)
42
+ end
43
+ class User < Dry::Struct
44
+ attribute :name, Types::String
45
+ attribute :email, Types::Email
46
+ attribute :age, Types::Age
47
+ end
48
+ ```
49
+
50
+ 3. Use a `Dry::Struct` as a type:
51
+
52
+ ```ruby
53
+ class Message < Dry::Struct
54
+ attribute :body, Types::String
55
+ attribute :to, User
56
+ end
57
+ ```
@@ -0,0 +1,169 @@
1
+ ---
2
+ title: Hash Schemas
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ It is possible to define a type for a hash with a known set of keys and corresponding value types. Let's say you want to describe a hash containing the name and the age of a user:
8
+
9
+ ```ruby
10
+ # using simple kernel coercions
11
+ user_hash = Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
12
+
13
+ user_hash[name: 'Jane', age: '21']
14
+ # => { name: 'Jane', age: 21 }
15
+ # :name left untouched and :age was coerced to Integer
16
+ ```
17
+
18
+ If a value doesn't conform to the type, an error is raised:
19
+
20
+ ```ruby
21
+ user_hash[name: :Jane, age: '21']
22
+ # => Dry::Types::SchemaError: :Jane (Symbol) has invalid type
23
+ # for :name violates constraints (type?(String, :Jane) failed)
24
+ ```
25
+
26
+ All keys are required by default:
27
+
28
+ ```ruby
29
+ user_hash[name: 'Jane']
30
+ # => Dry::Types::MissingKeyError: :age is missing in Hash input
31
+ ```
32
+
33
+ Extra keys are omitted by default:
34
+
35
+ ```ruby
36
+ user_hash[name: 'Jane', age: '21', city: 'London']
37
+ # => { name: 'Jane', age: 21 }
38
+ ```
39
+
40
+ ### Default values
41
+
42
+ Default types are **only** evaluated if the corresponding key is missing in the input:
43
+
44
+ ```ruby
45
+ user_hash = Types::Hash.schema(
46
+ name: Types::String,
47
+ age: Types::Integer.default(18)
48
+ )
49
+ user_hash[name: 'Jane']
50
+ # => { name: 'Jane', age: 18 }
51
+
52
+ # nil violates the constraint
53
+ user_hash[name: 'Jane', age: nil]
54
+ # => Dry::Types::SchemaError: nil (NilClass) has invalid type
55
+ # for :age violates constraints (type?(Integer, nil) failed)
56
+ ```
57
+
58
+ In order to evaluate default types on `nil`, wrap your type with a constructor and map `nil` to `Dry::Types::Undefined`:
59
+
60
+ ```ruby
61
+ user_hash = Types::Hash.schema(
62
+ name: Types::String,
63
+ age: Types::Integer.
64
+ default(18).
65
+ constructor { |value|
66
+ value.nil? ? Dry::Types::Undefined : value
67
+ }
68
+ )
69
+
70
+ user_hash[name: 'Jane', age: nil]
71
+ # => { name: 'Jane', age: 18 }
72
+ ```
73
+
74
+ The process of converting types to constructors like that can be automated, see "Type transformations" below.
75
+
76
+ ### Optional keys
77
+
78
+ By default, all keys are required to present in the input. You can mark a key as optional by adding `?` to its name:
79
+
80
+ ```ruby
81
+ user_hash = Types::Hash.schema(name: Types::String, age?: Types::Integer)
82
+
83
+ user_hash[name: 'Jane']
84
+ # => { name: 'Jane' }
85
+ ```
86
+
87
+ ### Extra keys
88
+
89
+ All keys not declared in the schema are silently ignored. This behavior can be changed by calling `.strict` on the schema:
90
+
91
+ ```ruby
92
+ user_hash = Types::Hash.schema(name: Types::String).strict
93
+ user_hash[name: 'Jane', age: 21]
94
+ # => Dry::Types::UnknownKeysError: unexpected keys [:age] in Hash input
95
+ ```
96
+
97
+ ### Transforming input keys
98
+
99
+ Keys are supposed to be symbols but you can attach a key tranformation to a schema, e.g. for converting strings into symbols:
100
+
101
+ ```ruby
102
+ user_hash = Types::Hash.schema(name: Types::String).with_key_transform(&:to_sym)
103
+ user_hash['name' => 'Jane']
104
+
105
+ # => { name: 'Jane' }
106
+ ```
107
+
108
+ ### Inheritance
109
+
110
+ Hash schemas can be inherited in a sense you can define a new schema based on an existing one. Declared keys will be merged, key and type transformations will be preserved. The `strict` option is also passed to the new schema if present.
111
+
112
+ ```ruby
113
+ # Building an empty base schema
114
+ StrictSymbolizingHash = Types::Hash.schema({}).strict.with_key_transform(&:to_sym)
115
+
116
+ user_hash = StrictSymbolizingHash.schema(
117
+ name: Types::String
118
+ )
119
+
120
+ user_hash['name' => 'Jane']
121
+ # => { name: 'Jane' }
122
+
123
+ user_hash['name' => 'Jane', 'city' => 'London']
124
+ # => Dry::Types::UnknownKeysError: unexpected keys [:city] in Hash input
125
+ ```
126
+
127
+ ### Transforming types
128
+
129
+ A schema can transform types with a block. For example, the following code makes all keys optional:
130
+
131
+ ```ruby
132
+ user_hash = Types::Hash.with_type_transform { |type| type.required(false) }.schema(
133
+ name: Types::String,
134
+ age: Types::Integer
135
+ )
136
+
137
+ user_hash[name: 'Jane']
138
+ # => { name: 'Jane' }
139
+ user_hash[{}]
140
+ # => {}
141
+ ```
142
+
143
+ Type transformations work perfectly with inheritance, you don't have to define same rules more than once:
144
+
145
+ ```ruby
146
+ SymbolizeAndOptionalSchema = Types::Hash.
147
+ .schema({})
148
+ .with_key_transform(&:to_sym)
149
+ .with_type_transform { |type| type.required(false) }
150
+
151
+ user_hash = SymbolizeAndOptionalSchema.schema(
152
+ name: Types::String,
153
+ age: Types::Integer
154
+ )
155
+
156
+ user_hash['name' => 'Jane']
157
+ ```
158
+
159
+ You can check key name by calling `.name` on the type argument:
160
+
161
+ ```ruby
162
+ Types::Hash.with_type_transform do |key|
163
+ if key.name.to_s.end_with?('_at')
164
+ key.constructor { |v| Time.iso8601(v) }
165
+ else
166
+ key
167
+ end
168
+ end
169
+ ```
@@ -0,0 +1,155 @@
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
+ ---
19
+
20
+ `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).
21
+
22
+ ### Example usage
23
+
24
+ ```ruby
25
+ require 'dry-types'
26
+ require 'dry-struct'
27
+
28
+ module Types
29
+ include Dry.Types()
30
+ end
31
+
32
+ User = Dry.Struct(name: Types::String, age: Types::Integer)
33
+
34
+ User.new(name: 'Bob', age: 35)
35
+ # => #<User name="Bob" age=35>
36
+ ```
37
+
38
+ See [Built-in Types](/gems/dry-types/1.0/built-in-types/) for a full list of available types.
39
+
40
+ 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:
41
+
42
+ - `Strict` types will raise an error if passed an attribute of the wrong type:
43
+
44
+ ```ruby
45
+ class User < Dry::Struct
46
+ attribute :name, Types::Strict::String
47
+ attribute :age, Types::Strict::Integer
48
+ end
49
+
50
+ User.new(name: 'Bob', age: '18')
51
+ # => Dry::Struct::Error: [User.new] "18" (String) has invalid type for :age
52
+ ```
53
+
54
+ - `Coercible` types will attempt to convert an attribute to the correct class
55
+ using Ruby's built-in coercion methods:
56
+
57
+ ```ruby
58
+ class User < Dry::Struct
59
+ attribute :name, Types::Coercible::String
60
+ attribute :age, Types::Coercible::Integer
61
+ end
62
+
63
+ User.new(name: 'Bob', age: '18')
64
+ # => #<User name="Bob" age=18>
65
+ User.new(name: 'Bob', age: 'not coercible')
66
+ # => ArgumentError: invalid value for Integer(): "not coercible"
67
+ ```
68
+
69
+ - Use `.optional` to denote that an attribute can be `nil` (see [Optional Values](/gems/dry-types/1.0/optional-values)):
70
+
71
+ ```ruby
72
+ class User < Dry::Struct
73
+ attribute :name, Types::String
74
+ attribute :age, Types::Integer.optional
75
+ end
76
+
77
+ User.new(name: 'Bob', age: nil)
78
+ # => #<User name="Bob" age=nil>
79
+ # name is not optional:
80
+ User.new(name: nil, age: 18)
81
+ # => Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
82
+ # keys must still be present:
83
+ User.new(name: 'Bob')
84
+ # => Dry::Struct::Error: [User.new] :age is missing in Hash input
85
+ ```
86
+
87
+ - Add custom constraints (see [Constraints](/gems/dry-types/1.0/constraints.html)):
88
+
89
+ ```ruby
90
+ class User < Dry::Struct
91
+ attribute :name, Types::Strict::String
92
+ attribute :age, Types::Strict::Integer.constrained(gteq: 18)
93
+ end
94
+
95
+ User.new(name: 'Bob', age: 17)
96
+ # => Dry::Struct::Error: [User.new] 17 (Fixnum) has invalid type for :age
97
+ ```
98
+
99
+ - Add custom metadata to a type:
100
+
101
+ ```ruby
102
+ class User < Dry::Struct
103
+ attribute :name, Types::String
104
+ attribute :age, Types::Integer.meta(info: 'extra info about age')
105
+ end
106
+ ```
107
+
108
+ - Pass values directly to `Dry::Types` without creating an object using `[]`:
109
+
110
+ ```ruby
111
+ Types::Strict::String["foo"]
112
+ # => "foo"
113
+ Types::Strict::String["10000"]
114
+ # => "10000"
115
+ Types::Coercible::String[10000]
116
+ # => "10000"
117
+ Types::Strict::String[10000]
118
+ # Dry::Types::ConstraintError: 1000 violates constraints
119
+ ```
120
+
121
+ ### Features
122
+
123
+ * Support for [constrained types](/gems/dry-types/1.0/constraints)
124
+ * Support for [optional values](/gems/dry-types/1.0/optional-values)
125
+ * Support for [default values](/gems/dry-types/1.0/default-values)
126
+ * Support for [sum types](/gems/dry-types/1.0/sum)
127
+ * Support for [enums](/gems/dry-types/1.0/enum)
128
+ * Support for [hash type with type schemas](/gems/dry-types/1.0/hash-schemas)
129
+ * Support for [array type with members](/gems/dry-types/1.0/array-with-member)
130
+ * Support for arbitrary meta information
131
+ * Support for typed struct objects via [dry-struct](/gems/dry-struct)
132
+ * Types are [categorized](/gems/dry-types/1.0/built-in-types), which is especially important for optimized and dedicated coercion logic
133
+ * Types are composable and reusable objects
134
+ * No const-missing magic and complicated const lookups
135
+ * Roughly 6-10 x faster than Virtus
136
+
137
+ ### Use cases
138
+
139
+ `dry-types` is suitable for many use-cases, for example:
140
+
141
+ * Value coercions
142
+ * Processing arrays
143
+ * Processing hashes with explicit schemas
144
+ * Defining various domain-specific information shared between multiple parts of your application
145
+ * Annotating objects
146
+
147
+ ### Other gems using dry-types
148
+
149
+ `dry-types` is often used as a low-level abstraction. The following gems use it already:
150
+
151
+ * [dry-struct](/gems/dry-struct)
152
+ * [dry-initializer](/gems/dry-initializer)
153
+ * [Hanami](http://hanamirb.org)
154
+ * [rom-rb](http://rom-rb.org)
155
+ * [Trailblazer](http://trailblazer.to)