evil-client 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +7 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +98 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +144 -0
  10. data/Rakefile +6 -0
  11. data/docs/base_url.md +38 -0
  12. data/docs/documentation.md +9 -0
  13. data/docs/headers.md +59 -0
  14. data/docs/http_method.md +31 -0
  15. data/docs/index.md +127 -0
  16. data/docs/license.md +19 -0
  17. data/docs/model.md +173 -0
  18. data/docs/operation.md +0 -0
  19. data/docs/overview.md +0 -0
  20. data/docs/path.md +48 -0
  21. data/docs/query.md +99 -0
  22. data/docs/responses.md +66 -0
  23. data/docs/security.md +102 -0
  24. data/docs/settings.md +32 -0
  25. data/evil-client.gemspec +25 -0
  26. data/lib/evil/client.rb +97 -0
  27. data/lib/evil/client/connection.rb +35 -0
  28. data/lib/evil/client/connection/net_http.rb +57 -0
  29. data/lib/evil/client/dsl.rb +110 -0
  30. data/lib/evil/client/dsl/files.rb +37 -0
  31. data/lib/evil/client/dsl/operation.rb +102 -0
  32. data/lib/evil/client/dsl/operations.rb +41 -0
  33. data/lib/evil/client/dsl/scope.rb +34 -0
  34. data/lib/evil/client/dsl/security.rb +57 -0
  35. data/lib/evil/client/middleware.rb +81 -0
  36. data/lib/evil/client/middleware/base.rb +15 -0
  37. data/lib/evil/client/middleware/merge_security.rb +16 -0
  38. data/lib/evil/client/middleware/normalize_headers.rb +13 -0
  39. data/lib/evil/client/middleware/stringify_form.rb +36 -0
  40. data/lib/evil/client/middleware/stringify_json.rb +15 -0
  41. data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
  42. data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
  43. data/lib/evil/client/middleware/stringify_query.rb +31 -0
  44. data/lib/evil/client/model.rb +65 -0
  45. data/lib/evil/client/operation.rb +34 -0
  46. data/lib/evil/client/operation/request.rb +42 -0
  47. data/lib/evil/client/operation/response.rb +40 -0
  48. data/lib/evil/client/operation/response_error.rb +12 -0
  49. data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
  50. data/mkdocs.yml +21 -0
  51. data/spec/features/instantiation_spec.rb +68 -0
  52. data/spec/features/middleware_spec.rb +75 -0
  53. data/spec/features/operation_with_documentation_spec.rb +41 -0
  54. data/spec/features/operation_with_files_spec.rb +40 -0
  55. data/spec/features/operation_with_form_body_spec.rb +158 -0
  56. data/spec/features/operation_with_headers_spec.rb +99 -0
  57. data/spec/features/operation_with_http_method_spec.rb +45 -0
  58. data/spec/features/operation_with_json_body_spec.rb +156 -0
  59. data/spec/features/operation_with_path_spec.rb +47 -0
  60. data/spec/features/operation_with_query_spec.rb +84 -0
  61. data/spec/features/operation_with_response_spec.rb +109 -0
  62. data/spec/features/operation_with_security_spec.rb +228 -0
  63. data/spec/features/scoping_spec.rb +48 -0
  64. data/spec/spec_helper.rb +23 -0
  65. data/spec/support/test_client.rb +15 -0
  66. data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
  67. data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
  68. data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
  69. data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
  70. data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
  71. data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
  72. data/spec/unit/evil/client/dsl_spec.rb +57 -0
  73. data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
  74. data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
  75. data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
  76. data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
  77. data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
  78. data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
  79. data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
  80. data/spec/unit/evil/client/middleware_spec.rb +46 -0
  81. data/spec/unit/evil/client/model_spec.rb +100 -0
  82. data/spec/unit/evil/client/operation/request_spec.rb +49 -0
  83. data/spec/unit/evil/client/operation/response_spec.rb +61 -0
  84. metadata +271 -0
@@ -0,0 +1,31 @@
1
+ Use `http_method` to define it for the current operation.
2
+
3
+ ```ruby
4
+ operation :find_cat do
5
+ http_method :get
6
+ end
7
+ ```
8
+
9
+ As usual, you have access to current settings. This can be useful to make the method dependent from either a version, or other variation of the api.
10
+
11
+ ```ruby
12
+ operation :find_cat do |settings|
13
+ http_method settings.version > 2 ? :post : :get
14
+ end
15
+ ```
16
+
17
+ You can also set a default method for all operations. It can be reloaded later:
18
+
19
+ ```ruby
20
+ operation do
21
+ http_method :get
22
+ end
23
+
24
+ operation :find_cat do
25
+ # sends requests via get
26
+ end
27
+
28
+ operation :update_cat do
29
+ http_method :patch
30
+ end
31
+ ```
data/docs/index.md ADDED
@@ -0,0 +1,127 @@
1
+ Human-friendly DSL for writing HTTP(s) clients in Ruby
2
+
3
+ [![Logo][evilmartians-logo]][evilmartians]
4
+
5
+ # About
6
+
7
+ The gem allows writing http(s) clients in a way close to [Swagger][swagger] specifications. Like in Swagger, you describe models and operations in domain-specific terms. In addition, the gem supports [settings][settings] and [scopes][scopes] for instantiating clients and sending requests in idiomatic Ruby.
8
+
9
+ The gem stands away from mutable states and monkey patching when possible. To support multithreading, all instances are immutable (though not frozen to avoid performance loss). The gem's DSL is built on top of [dry-initializer][dry-initializer] gem, and supposes heavy usage of [dry-types][dry-types] system of contracts.
10
+
11
+ For now the top-level DSL supports clients to **json** and **form data** APIs. Because of high variance of XML-based APIs, building their clients require more efforts on a middleware level, which is discussed in the [corresponding topic][xml].
12
+
13
+ The gem requires ruby 2.2+ and was tested under MRI and JRuby 9+.
14
+
15
+ # Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'evil-client'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```shell
26
+ $ bundle
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```shell
32
+ $ gem install evil-client
33
+ ```
34
+
35
+ # Example
36
+
37
+ The following example gives an idea of how a client to remote API looks like when written on top of `Evil::Client` using [dry-types][dry-types]-based contracts.
38
+
39
+ ```ruby
40
+ require "evil-client"
41
+ require "dry-types"
42
+
43
+ class CatsClient < Evil::Client
44
+ # describe a client-specific model of cat (the furry pinnacle of evolution)
45
+ class Cat < Evil::Client::Model
46
+ attribute :name, type: Dry::Types["strict.string"], optional: true
47
+ attribute :color, type: Dry::Types["strict.string"]
48
+ attribute :age, type: Dry::Types["coercible.int"], default: proc { 0 }
49
+ end
50
+
51
+ # Define settings the client initialized with
52
+ # The settings parameterizes operations when necessary
53
+ settings do
54
+ param :domain, type: Dry::Types["strict.string"] # required!
55
+ option :version, type: Dry::Types["coercible.int"], default: proc { 0 }
56
+ option :user, type: Dry::Types["strict.string"] # required!
57
+ option :password, type: Dry::Types["strict.string"] # required!
58
+ end
59
+
60
+ # Define a base url using settings
61
+ base_url do |settings|
62
+ "https://#{settings.domain}.example.com/api/v#{settings.version}/"
63
+ end
64
+
65
+ # Definitions shared by all operations
66
+ operation do |settings|
67
+ security { basic_auth settings.user, settings.password }
68
+ end
69
+
70
+ # Operation-specific definition to update a cat by id
71
+ # This provides low-level DSL `operations[:update_cat].call`
72
+ operation :update_cat do |settings|
73
+ http_method :patch
74
+ path { |id:, **| "cats/#{id}" } # id will be taken from request parameters
75
+
76
+ body format: "json" do
77
+ attribute :name, optional: true
78
+ attribute :color, optional: true
79
+ attribute :age, optional: true
80
+ end
81
+
82
+ response 200 do |body:, **|
83
+ Cat.new JSON.parse(body) # define that the body should be wrapped to cat
84
+ end
85
+
86
+ response 422, raise: true do |body:, **|
87
+ JSON.parse(body) # expect 422 to return json data
88
+ end
89
+ end
90
+
91
+ # Add top-level DSL
92
+ scope :cats do
93
+ scope do |id|
94
+ def find(**data)
95
+ operations[:update_cat].call(id: id, **data)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Instantiate a client with concrete settings
102
+ cat_client = CatClient.new "awesome-cats", # domain
103
+ version: 1,
104
+ user: "cat_lover",
105
+ password: "purr"
106
+
107
+ # Use low-level DSL to send requests
108
+ cat_client.operations[:update_cat].call id: 4,
109
+ age: 10,
110
+ name: "Agamemnon",
111
+ color: "tabby"
112
+
113
+ # Use top-level DSL for the same request
114
+ cat_client.cats[4].call(age: 10, name: "Agamemnon", color: "tabby")
115
+
116
+ # Both the methods send `PATCH https://awesom-cats.example.com/api/v1/cats/7`
117
+ # with a specified body and headers (authorization via basic_auth)
118
+ ```
119
+
120
+ [swagger]: http://swagger.io
121
+ [dry-initializer]: http://dry-rb.org/gems/dry-initializer
122
+ [dry-types]: http://dry-rb.org/gems/dry-types
123
+ [evilmartians]: https://evilmartians.com
124
+ [evilmartians-logo]: https://evilmartians.com/badges/sponsored-by-evil-martians.svg
125
+ [settings]:
126
+ [scopes]:
127
+ [xml]:
data/docs/license.md ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2016 Andrew Kozin (aka nepalez), andrew.kozin@gmail.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/docs/model.md ADDED
@@ -0,0 +1,173 @@
1
+ Models are simple nested structures, based on [dry-initializer][dry-initializer] and [dry-types][dry-types].
2
+
3
+ They are needed to prepare and validate nested bodies and queries, as well as wrap and validate responses.
4
+
5
+ # Model Definition
6
+
7
+ To define a model create a subclass of `Evil::Client::Model` and define its attributes.
8
+
9
+ ```ruby
10
+ class Cat < Evil::Client::Model
11
+ attribute :name, type: Dry::Types["strict.string"], optional: true
12
+ attribute :age, type: Dry::Types["coercible.int"], default: proc { 0 }
13
+ attribute :color, type: Dry::Types["strict.string"]
14
+ end
15
+ ```
16
+
17
+ The method `attribute` is just an alias of [dry-initializer `option`][dry-initializer]. Because model's constructor takes options only, not params, `param` is reloaded as another alias of `option`. You can use any method you like.
18
+
19
+ To initialize an instance send a hash of options to the constructor:
20
+
21
+ ```ruby
22
+ cat = Cat.new(name: "Navuxodonosor II", age: "15", color: "black")
23
+ cat.name # => "Navuxodonosor II"
24
+ cat.age # => 15
25
+ cat.color # => "black"
26
+ ```
27
+
28
+ You can build a model from another one (it just returns the object back):
29
+
30
+ ```ruby
31
+ Cat.new Cat.new(name: "Navuxodonosor II", age: 15, color: "black")
32
+ ```
33
+
34
+ or from a hash with string keys:
35
+
36
+ ```ruby
37
+ Cat.new("name" => "Navuxodonosor II", "age" => 15, "color" => "black")
38
+ ```
39
+
40
+ You can use other (nested) models in type definitions:
41
+
42
+ ```ruby
43
+ class CatPack < Evil::Client::Model
44
+ attribute :cats, type: Dry::Types["array"].member(Cat)
45
+ end
46
+
47
+ CatPack.new cats: [{ name: "Navuxodonosor II", age: 15, color: "black" }]
48
+ ```
49
+
50
+ Models can be converted back to hashes with **symbolic** keys:
51
+
52
+ ```ruby
53
+ cat = Cat.new(name: "Navuxodonosor II", age: "15", color: "black")
54
+ cat.to_h # => { name: "Navuxodonosor II", age: "15", color: "black" }
55
+ ```
56
+
57
+ The model ignores all options it doesn't know about, and applies constraints to known ones only.
58
+
59
+ ```ruby
60
+ # Cats don't care about your expectations
61
+ cat = Cat.new(name: "Navuxodonosor II", age: "15", color: "black", expectation: "hunting")
62
+ cat.to_h # => { name: "Navuxodonosor II", age: "15", color: "black" }
63
+ ```
64
+
65
+ If all you need is data filtering, just use a shortcut `.[]`:
66
+
67
+ ```ruby
68
+ Cat[name: "Navuxodonosor II", age: "15", color: "black", expectation: "hunting"]
69
+ # => { name: "Navuxodonosor II", age: "15", color: "black" }
70
+ ```
71
+
72
+ # Model Usage
73
+
74
+ You can use models in definitions of request [body][body], [query][query], and [headers][headers]...
75
+
76
+ ```ruby
77
+ operation :create_cat do
78
+ method :post
79
+ path { "cats" }
80
+ body model: Cat
81
+ end
82
+ ```
83
+
84
+ ...and in [response][response] processing:
85
+
86
+ ```ruby
87
+ operation :create_cat do
88
+ # ...
89
+ response 201 do |body:, **|
90
+ Cat[JSON.parse(body)]
91
+ end
92
+ end
93
+ ```
94
+
95
+ # Distinction of Models from Dry::Struct
96
+
97
+ Models are like ~~onions~~ structures, defined in [`dry-struct`][dry-struct]. Both models and structures support hash arguments, type constraints, nested data, and backward hashification via `to_h`. You can check [dry-struct documentation] to make an impression of how it works.
98
+
99
+ Nethertheless, there is an important difference between the implementations of nested structures.
100
+
101
+ ## Undefined Values vs nils
102
+
103
+ The main reason to define gem-specific model is the following. In `Dry::Struct` both the `optional` and `default` properties belong to value type constraint. The gem does not draw the line between attributes that are not set, and those that are set to `nil`.
104
+
105
+ To the contrary, in [dry-initializer][dry-initializer] and `Evil::Client::Model` both `optional` and `default` properties describe not a value type by itself, but its relation to the model. An attribute value can be set to `nil`, or been kept in undefined state.
106
+
107
+ Let's see the difference on the example of `StructCat` and `ModelCat`:
108
+
109
+ ```ruby
110
+ class StructCat < Dry::Struct
111
+ attribute :name, type: Dry::Types["strict.string"].optional
112
+ attribute :age, type: Dry::Types["coercible.int"].default(0)
113
+ end
114
+
115
+ class ModelCat < Evil::Client::Model
116
+ attribute :name, type: Dry::Types["strict.string"], optional: true
117
+ attribute :age, type: Dry::Types["coercible.int"], default: proc { 0 }
118
+ end
119
+
120
+ struct_cat = StructCat.new
121
+ struct_cat.name # => nil
122
+ struct_cat.age # => 0
123
+ struct_cat.to_h # => { name: nil, age: 0 }
124
+
125
+ model_cat = ModelCat.new
126
+ model_cat.name # => #<Dry::Initializer::UNDEFINED>
127
+ model_cat.age # => 0
128
+ model_cat.to_h # => { age: 0 }
129
+
130
+ model_cat = ModelCat.new name: nil
131
+ model_cat.name # => nil
132
+ model_cat.age # => 0
133
+ model_cat.to_h # => { name: nil, age: 0 }
134
+ ```
135
+
136
+ Notice that in a model hashification ignores undefined attributes. This is important to filter arguments of a request. In PUT/PATCH requests (update server-side data) there is a difference between values not changed, and those that are explicitly reset to `nil`.
137
+
138
+ ## Tolerance to Unknown Options
139
+
140
+ A model's constructor ignores unknown options, so you can safely sent any ones:
141
+
142
+ ```ruby
143
+ # Oh, no, this is a cat, not a bat!
144
+ model_cat = ModelCat.new name: "Abraham", flying_distance: "5 miles"
145
+
146
+ # so we simply ignore flying_distance
147
+ model_cat.to_h # => { name: "Abraham", age: 0 }
148
+ ```
149
+
150
+ This behaviour allows us to slice only necessary arguments for [body][body], [query][query], and [headers][headers] of a request.
151
+
152
+ ## Stringified Keys in Constructor
153
+
154
+ Ahother difference between structs and models is that models can take hashes with both symbolic and string keys.
155
+
156
+ This addition is useful when processing [responses][response]:
157
+
158
+ ```ruby
159
+ # This works even though JSON#parse returns a hash with string keys
160
+ ModelCat.new JSON.parse('{"age":4}')
161
+ ```
162
+
163
+ ## Equality
164
+
165
+ Models, whose methods `to_h` returns equal hashes, are counted as equal.
166
+
167
+ [dry-initializer]: http://dry-rb.org/gems/dry-initializer
168
+ [dry-struct]: http://dry-rb.org/gems/dry-struct
169
+ [dry-types]: http://dry-rb.org/gems/dry-types
170
+ [body]:
171
+ [headers]:
172
+ [query]:
173
+ [response]:
data/docs/operation.md ADDED
File without changes
data/docs/overview.md ADDED
File without changes
data/docs/path.md ADDED
@@ -0,0 +1,48 @@
1
+ Use `path` to define operation's path that is relative to the [base url][base_url].
2
+
3
+ ```ruby
4
+ operation :find_cats do
5
+ path { "cats" }
6
+ end
7
+ ```
8
+
9
+ Notice that a value should be wrapped into the block. This is necessary to build paths dependent on arguments of the request. The following definition inserts a mandatory id from options:
10
+
11
+ ```ruby
12
+ operation :find_cat do
13
+ path { |id:, **| "cats/#{id}" }
14
+ end
15
+
16
+ # later at a runtime
17
+ client.operations[:find_cat].call id: 98 # sends to "/cats/98"
18
+ ```
19
+
20
+ As usual, you have access to current settings. This can be useful to add client tokens to paths when necessary:
21
+
22
+ ```ruby
23
+ operation :find_cats do |settings|
24
+ path { "cats/#{settings.token}" }
25
+ end
26
+ ```
27
+
28
+ ## Default Path
29
+
30
+ You can set a default path for all operations. Use it to DRY clients whose operations differs not by endpoints, but, for example, by parameters ([query][query], [body][body]) of various requests:
31
+
32
+ ```ruby
33
+ operation do
34
+ path { "cats" }
35
+ end
36
+
37
+ operation :find_cats do
38
+ # sends requests to "/cats"
39
+ end
40
+
41
+ operation :find_details do
42
+ path { "cats/details" } # reloads default setting
43
+ end
44
+ ```
45
+
46
+ [base_url]:
47
+ [query]:
48
+ [body]:
data/docs/query.md ADDED
@@ -0,0 +1,99 @@
1
+ Use `query` to add some data to the request query. The syntax is pretty the same as for [body][body] and [headers][headers].
2
+
3
+ ```ruby
4
+ operation :find_cat do |settings|
5
+ # ...
6
+ path { "cats" }
7
+ query do
8
+ attribute :token, default: proc { settings.token }
9
+ attribute :id
10
+ end
11
+ end
12
+
13
+ # Later at the runtime it will send a request to "../cats?id=4&token=foo"
14
+ client.operations[:find_cat].call id: 4, token: "foo"
15
+ ```
16
+
17
+ ## Nested Data Representation
18
+
19
+ Nested data are represented in a query following Rails convention:
20
+
21
+ ```ruby
22
+ client.operations[:find_cat].call id: [{ key: 4 }], token: ["foo"]
23
+ # "/cats?id[][key]=4&token[]=foo"
24
+ ```
25
+
26
+ Non-unicode symbols are encoded as defined in [RFC-3986][rfc-3986]
27
+
28
+ ## Model-Based Queries
29
+
30
+ Use [models][model] to provide validation of query data:
31
+
32
+ ```ruby
33
+ class Cat < Evil::Client::Model
34
+ attribute :name, type: Dry::Types["strict.string"], optional: true
35
+ attribute :age, type: Dry::Types["strict.int"], default: proc { 0 }
36
+ attribute :color, type: Dry::Types["strict.string"]
37
+ end
38
+ ```
39
+
40
+ You can either restrict `type` of an attribute:
41
+
42
+ ```ruby
43
+ operation :create_cat do
44
+ query do
45
+ attribute :cat, type: Cat
46
+ end
47
+ end
48
+
49
+ # Later at runtime will send "...?cat[color]=tabby&cat[age]=0"
50
+ client.operations[:create_cat].call cat: { color: "tabby" }
51
+ ```
52
+
53
+ ...or use in for the query as a whole under the `model` key:
54
+
55
+ ```ruby
56
+ operation :create_cat do
57
+ query model: Cat
58
+ end
59
+
60
+ # Later at runtime will send "...?color=tabby&age=0"
61
+ client.operations[:create_cat].call color: "tabby"
62
+ ```
63
+
64
+ In the last case you can define additional attributes (this redefinition is local, it don't affect a model by itself):
65
+
66
+ ```ruby
67
+ operation :create_cat do
68
+ query model: Cat do
69
+ attribute :mood, default: proc { "sleeping" }
70
+ end
71
+ end
72
+
73
+ # Later at runtime will send "...?color=tabby&age=0&mood=sleeping"
74
+ client.operations[:create_cat].call color: "tabby"
75
+ ```
76
+
77
+ **Be careful!** You cannot reload existing attributes (this will cause an exception).
78
+
79
+ In operations that update remote data you can skip some attributes (mark them `optional`). If you need to check responses strictly (to require all the necessary attributes), you should provide different models.
80
+
81
+ ```ruby
82
+ # Requires remote server to return consistent beasts
83
+ class Cat
84
+ attribute :id, type: Dry::Types["strict.int"].constrained(gt: 0)
85
+ attribute :age, type: Dry::Types["strict.int"]
86
+ attribute :name, type: Dry::Types["strict.string"]
87
+ end
88
+
89
+ # Allows updating attributes when necessary
90
+ class CatUpdate
91
+ attribute :age, type: Dry::Types["coercible.int"], optional: true
92
+ attribute :name, type: Dry::Types["coercible.string"], optional: true
93
+ end
94
+ ```
95
+
96
+ [rfc-3986]: https://tools.ietf.org/html/rfc3986
97
+ [body]:
98
+ [headers]:
99
+ [model]: