evil-client 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +6 -0
- data/docs/base_url.md +38 -0
- data/docs/documentation.md +9 -0
- data/docs/headers.md +59 -0
- data/docs/http_method.md +31 -0
- data/docs/index.md +127 -0
- data/docs/license.md +19 -0
- data/docs/model.md +173 -0
- data/docs/operation.md +0 -0
- data/docs/overview.md +0 -0
- data/docs/path.md +48 -0
- data/docs/query.md +99 -0
- data/docs/responses.md +66 -0
- data/docs/security.md +102 -0
- data/docs/settings.md +32 -0
- data/evil-client.gemspec +25 -0
- data/lib/evil/client.rb +97 -0
- data/lib/evil/client/connection.rb +35 -0
- data/lib/evil/client/connection/net_http.rb +57 -0
- data/lib/evil/client/dsl.rb +110 -0
- data/lib/evil/client/dsl/files.rb +37 -0
- data/lib/evil/client/dsl/operation.rb +102 -0
- data/lib/evil/client/dsl/operations.rb +41 -0
- data/lib/evil/client/dsl/scope.rb +34 -0
- data/lib/evil/client/dsl/security.rb +57 -0
- data/lib/evil/client/middleware.rb +81 -0
- data/lib/evil/client/middleware/base.rb +15 -0
- data/lib/evil/client/middleware/merge_security.rb +16 -0
- data/lib/evil/client/middleware/normalize_headers.rb +13 -0
- data/lib/evil/client/middleware/stringify_form.rb +36 -0
- data/lib/evil/client/middleware/stringify_json.rb +15 -0
- data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
- data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
- data/lib/evil/client/middleware/stringify_query.rb +31 -0
- data/lib/evil/client/model.rb +65 -0
- data/lib/evil/client/operation.rb +34 -0
- data/lib/evil/client/operation/request.rb +42 -0
- data/lib/evil/client/operation/response.rb +40 -0
- data/lib/evil/client/operation/response_error.rb +12 -0
- data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
- data/mkdocs.yml +21 -0
- data/spec/features/instantiation_spec.rb +68 -0
- data/spec/features/middleware_spec.rb +75 -0
- data/spec/features/operation_with_documentation_spec.rb +41 -0
- data/spec/features/operation_with_files_spec.rb +40 -0
- data/spec/features/operation_with_form_body_spec.rb +158 -0
- data/spec/features/operation_with_headers_spec.rb +99 -0
- data/spec/features/operation_with_http_method_spec.rb +45 -0
- data/spec/features/operation_with_json_body_spec.rb +156 -0
- data/spec/features/operation_with_path_spec.rb +47 -0
- data/spec/features/operation_with_query_spec.rb +84 -0
- data/spec/features/operation_with_response_spec.rb +109 -0
- data/spec/features/operation_with_security_spec.rb +228 -0
- data/spec/features/scoping_spec.rb +48 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/test_client.rb +15 -0
- data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
- data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
- data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
- data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
- data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
- data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
- data/spec/unit/evil/client/dsl_spec.rb +57 -0
- data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
- data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
- data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
- data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
- data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
- data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
- data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
- data/spec/unit/evil/client/middleware_spec.rb +46 -0
- data/spec/unit/evil/client/model_spec.rb +100 -0
- data/spec/unit/evil/client/operation/request_spec.rb +49 -0
- data/spec/unit/evil/client/operation/response_spec.rb +61 -0
- metadata +271 -0
data/docs/http_method.md
ADDED
@@ -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]:
|