evil-client 0.3.3 → 1.0.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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +0 -11
- data/.gitignore +1 -0
- data/.rspec +0 -1
- data/.rubocop.yml +22 -19
- data/.travis.yml +1 -0
- data/CHANGELOG.md +251 -6
- data/LICENSE.txt +3 -1
- data/README.md +47 -81
- data/docs/helpers/body.md +93 -0
- data/docs/helpers/connection.md +19 -0
- data/docs/helpers/headers.md +72 -0
- data/docs/helpers/http_method.md +39 -0
- data/docs/helpers/let.md +14 -0
- data/docs/helpers/logger.md +24 -0
- data/docs/helpers/middleware.md +56 -0
- data/docs/helpers/operation.md +103 -0
- data/docs/helpers/option.md +50 -0
- data/docs/helpers/path.md +37 -0
- data/docs/helpers/query.md +59 -0
- data/docs/helpers/response.md +40 -0
- data/docs/helpers/scope.md +121 -0
- data/docs/helpers/security.md +102 -0
- data/docs/helpers/validate.md +68 -0
- data/docs/index.md +70 -78
- data/docs/license.md +5 -1
- data/docs/rspec.md +96 -0
- data/evil-client.gemspec +10 -8
- data/lib/evil/client.rb +126 -72
- data/lib/evil/client/builder.rb +47 -0
- data/lib/evil/client/builder/operation.rb +40 -0
- data/lib/evil/client/builder/scope.rb +31 -0
- data/lib/evil/client/chaining.rb +17 -0
- data/lib/evil/client/connection.rb +60 -20
- data/lib/evil/client/container.rb +66 -0
- data/lib/evil/client/container/operation.rb +23 -0
- data/lib/evil/client/container/scope.rb +28 -0
- data/lib/evil/client/exceptions/definition_error.rb +15 -0
- data/lib/evil/client/exceptions/name_error.rb +32 -0
- data/lib/evil/client/exceptions/response_error.rb +42 -0
- data/lib/evil/client/exceptions/type_error.rb +29 -0
- data/lib/evil/client/exceptions/validation_error.rb +27 -0
- data/lib/evil/client/formatter.rb +49 -0
- data/lib/evil/client/formatter/form.rb +45 -0
- data/lib/evil/client/formatter/multipart.rb +33 -0
- data/lib/evil/client/formatter/part.rb +66 -0
- data/lib/evil/client/formatter/text.rb +21 -0
- data/lib/evil/client/resolver.rb +84 -0
- data/lib/evil/client/resolver/body.rb +22 -0
- data/lib/evil/client/resolver/format.rb +30 -0
- data/lib/evil/client/resolver/headers.rb +46 -0
- data/lib/evil/client/resolver/http_method.rb +34 -0
- data/lib/evil/client/resolver/middleware.rb +36 -0
- data/lib/evil/client/resolver/query.rb +39 -0
- data/lib/evil/client/resolver/request.rb +96 -0
- data/lib/evil/client/resolver/response.rb +26 -0
- data/lib/evil/client/resolver/security.rb +113 -0
- data/lib/evil/client/resolver/uri.rb +35 -0
- data/lib/evil/client/rspec.rb +127 -0
- data/lib/evil/client/schema.rb +105 -0
- data/lib/evil/client/schema/operation.rb +177 -0
- data/lib/evil/client/schema/scope.rb +73 -0
- data/lib/evil/client/settings.rb +172 -0
- data/lib/evil/client/settings/validator.rb +64 -0
- data/mkdocs.yml +21 -15
- data/spec/features/custom_connection_spec.rb +17 -0
- data/spec/features/operation/middleware_spec.rb +50 -0
- data/spec/features/operation/options_spec.rb +71 -0
- data/spec/features/operation/request_spec.rb +94 -0
- data/spec/features/operation/response_spec.rb +48 -0
- data/spec/features/scope/options_spec.rb +52 -0
- data/spec/fixtures/locales/en.yml +16 -0
- data/spec/fixtures/test_client.rb +76 -0
- data/spec/spec_helper.rb +18 -6
- data/spec/support/fixtures_helper.rb +7 -0
- data/spec/unit/builder/operation_spec.rb +90 -0
- data/spec/unit/builder/scope_spec.rb +84 -0
- data/spec/unit/client_spec.rb +137 -0
- data/spec/unit/connection_spec.rb +78 -0
- data/spec/unit/container/operation_spec.rb +81 -0
- data/spec/unit/container/scope_spec.rb +61 -0
- data/spec/unit/container_spec.rb +107 -0
- data/spec/unit/exceptions/definition_error_spec.rb +15 -0
- data/spec/unit/exceptions/name_error_spec.rb +77 -0
- data/spec/unit/exceptions/response_error_spec.rb +22 -0
- data/spec/unit/exceptions/type_error_spec.rb +71 -0
- data/spec/unit/exceptions/validation_error_spec.rb +13 -0
- data/spec/unit/formatter/form_spec.rb +27 -0
- data/spec/unit/formatter/multipart_spec.rb +23 -0
- data/spec/unit/formatter/part_spec.rb +49 -0
- data/spec/unit/formatter/text_spec.rb +37 -0
- data/spec/unit/formatter_spec.rb +46 -0
- data/spec/unit/resolver/body_spec.rb +65 -0
- data/spec/unit/resolver/format_spec.rb +66 -0
- data/spec/unit/resolver/headers_spec.rb +93 -0
- data/spec/unit/resolver/http_method_spec.rb +67 -0
- data/spec/unit/resolver/middleware_spec.rb +83 -0
- data/spec/unit/resolver/query_spec.rb +85 -0
- data/spec/unit/resolver/request_spec.rb +121 -0
- data/spec/unit/resolver/response_spec.rb +64 -0
- data/spec/unit/resolver/security_spec.rb +156 -0
- data/spec/unit/resolver/uri_spec.rb +117 -0
- data/spec/unit/rspec_spec.rb +342 -0
- data/spec/unit/schema/operation_spec.rb +309 -0
- data/spec/unit/schema/scope_spec.rb +110 -0
- data/spec/unit/schema_spec.rb +157 -0
- data/spec/unit/settings/validator_spec.rb +128 -0
- data/spec/unit/settings_spec.rb +248 -0
- metadata +192 -135
- data/docs/base_url.md +0 -38
- data/docs/documentation.md +0 -9
- data/docs/headers.md +0 -59
- data/docs/http_method.md +0 -31
- data/docs/model.md +0 -173
- data/docs/operation.md +0 -0
- data/docs/overview.md +0 -0
- data/docs/path.md +0 -48
- data/docs/query.md +0 -99
- data/docs/responses.md +0 -66
- data/docs/security.md +0 -102
- data/docs/settings.md +0 -32
- data/lib/evil/client/connection/net_http.rb +0 -57
- data/lib/evil/client/dsl.rb +0 -127
- data/lib/evil/client/dsl/base.rb +0 -26
- data/lib/evil/client/dsl/files.rb +0 -37
- data/lib/evil/client/dsl/headers.rb +0 -16
- data/lib/evil/client/dsl/http_method.rb +0 -24
- data/lib/evil/client/dsl/operation.rb +0 -91
- data/lib/evil/client/dsl/operations.rb +0 -41
- data/lib/evil/client/dsl/path.rb +0 -25
- data/lib/evil/client/dsl/query.rb +0 -16
- data/lib/evil/client/dsl/response.rb +0 -61
- data/lib/evil/client/dsl/responses.rb +0 -29
- data/lib/evil/client/dsl/scope.rb +0 -27
- data/lib/evil/client/dsl/security.rb +0 -57
- data/lib/evil/client/dsl/verifier.rb +0 -35
- data/lib/evil/client/middleware.rb +0 -81
- data/lib/evil/client/middleware/base.rb +0 -11
- data/lib/evil/client/middleware/merge_security.rb +0 -20
- data/lib/evil/client/middleware/normalize_headers.rb +0 -17
- data/lib/evil/client/middleware/stringify_form.rb +0 -40
- data/lib/evil/client/middleware/stringify_json.rb +0 -19
- data/lib/evil/client/middleware/stringify_multipart.rb +0 -36
- data/lib/evil/client/middleware/stringify_multipart/part.rb +0 -36
- data/lib/evil/client/middleware/stringify_query.rb +0 -35
- data/lib/evil/client/operation.rb +0 -34
- data/lib/evil/client/operation/request.rb +0 -26
- data/lib/evil/client/operation/response.rb +0 -39
- data/lib/evil/client/operation/response_error.rb +0 -13
- data/lib/evil/client/operation/unexpected_response_error.rb +0 -19
- data/spec/features/instantiation_spec.rb +0 -68
- data/spec/features/middleware_spec.rb +0 -79
- data/spec/features/operation_with_documentation_spec.rb +0 -41
- data/spec/features/operation_with_files_spec.rb +0 -40
- data/spec/features/operation_with_form_body_spec.rb +0 -158
- data/spec/features/operation_with_headers_spec.rb +0 -99
- data/spec/features/operation_with_http_method_spec.rb +0 -45
- data/spec/features/operation_with_json_body_spec.rb +0 -156
- data/spec/features/operation_with_nested_responses_spec.rb +0 -95
- data/spec/features/operation_with_path_spec.rb +0 -47
- data/spec/features/operation_with_query_spec.rb +0 -84
- data/spec/features/operation_with_security_spec.rb +0 -228
- data/spec/features/scoping_spec.rb +0 -48
- data/spec/support/test_client.rb +0 -15
- data/spec/unit/evil/client/connection/net_http_spec.rb +0 -38
- data/spec/unit/evil/client/dsl/files_spec.rb +0 -37
- data/spec/unit/evil/client/dsl/operation_spec.rb +0 -374
- data/spec/unit/evil/client/dsl/operations_spec.rb +0 -29
- data/spec/unit/evil/client/dsl/scope_spec.rb +0 -32
- data/spec/unit/evil/client/dsl/security_spec.rb +0 -135
- data/spec/unit/evil/client/middleware/merge_security_spec.rb +0 -32
- data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +0 -17
- data/spec/unit/evil/client/middleware/stringify_form_spec.rb +0 -63
- data/spec/unit/evil/client/middleware/stringify_json_spec.rb +0 -61
- data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +0 -59
- data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +0 -62
- data/spec/unit/evil/client/middleware/stringify_query_spec.rb +0 -40
- data/spec/unit/evil/client/middleware_spec.rb +0 -46
- data/spec/unit/evil/client/operation/request_spec.rb +0 -49
- data/spec/unit/evil/client/operation/response_spec.rb +0 -63
@@ -0,0 +1,37 @@
|
|
1
|
+
Use `path` helpers to define operation's path for every operation.
|
2
|
+
|
3
|
+
As a rule, you should start from defining root path at the very root of your client, and then complement it at any level of scoping:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class CatsClient < Evil::Client
|
7
|
+
# ...
|
8
|
+
option :domain
|
9
|
+
path { "https://#{domain}.example.com" }
|
10
|
+
# ...
|
11
|
+
|
12
|
+
scope :cats do
|
13
|
+
path "cats" # relative to the root's one
|
14
|
+
|
15
|
+
operation :fetch do
|
16
|
+
option :id # relative to the cats' one
|
17
|
+
path { id }
|
18
|
+
# ...
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
Above declaration will send fetch requests to "https://{domain}.example.com/cats/{id}", for example:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
CatsClient.new(domain: "domestic").cats.fetch(37)
|
28
|
+
# "https://domestic.example.com/cats/37"
|
29
|
+
```
|
30
|
+
|
31
|
+
In practice it is the structure of API paths that will "dictate" the structure of your client's scopes.
|
32
|
+
|
33
|
+
The path should be defined for every operation, otherwise calling it will cause an exception.
|
34
|
+
|
35
|
+
**Notice**. You havent include query to the path. Use [query] helper method instead to define it.
|
36
|
+
|
37
|
+
[query]:
|
@@ -0,0 +1,59 @@
|
|
1
|
+
Use `query` helper to add some data to the request query. The helper should provide a hash, either nested or not.
|
2
|
+
|
3
|
+
```ruby
|
4
|
+
class CatsClient < Evil::Client
|
5
|
+
# ...
|
6
|
+
operation :cats do
|
7
|
+
# ...
|
8
|
+
operation :fetch do
|
9
|
+
option :language, default: proc { "en_US" }
|
10
|
+
# ...
|
11
|
+
query { { language: language } }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Later at the runtime it will include query to the fetch request "..?language=en_US"
|
17
|
+
CatsClient.new.cats.fetch
|
18
|
+
```
|
19
|
+
|
20
|
+
When you add query in nested scopes/operations, it updates upper-level definitions, using deep merge when possible (both queries should have compatible structures):
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class CatsClient < Evil::Client
|
24
|
+
option :language, default: proc { "en_US" }
|
25
|
+
query { { accept: { language: language } } }
|
26
|
+
|
27
|
+
operation :cats do
|
28
|
+
option :charset, default: proc { "utf-8" }
|
29
|
+
query { { accept: { charset: charset } } }
|
30
|
+
# ...
|
31
|
+
operation :fetch do
|
32
|
+
# ...
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Later at the runtime it will include query to any cats operation:
|
38
|
+
# ...?accept[language]=en_US&accept[charset]=utf-8
|
39
|
+
CatsClient.new.cats.fetch
|
40
|
+
```
|
41
|
+
|
42
|
+
**Remember** that like [headers], the final query affected by [security][security] (for example, it can define `basic_auth`) settings.
|
43
|
+
|
44
|
+
When you define both query and security settings at the same time, the priority will be given to security. This isn't depend on where (root scope or its sub-scopes) security and query parts are defined. Security settings will always be written over the same query.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class CatsAPI < Evil::Client
|
48
|
+
security { key_auth :basic_auth, "Bar" }
|
49
|
+
|
50
|
+
cats do
|
51
|
+
query { { basic_auth: "Foo" } }
|
52
|
+
# will send requests to ...?basic_auth=Bar
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
[rfc-3986]: https://tools.ietf.org/html/rfc3986
|
58
|
+
[security]:
|
59
|
+
[headers]:
|
@@ -0,0 +1,40 @@
|
|
1
|
+
For every operation you have to describe all expected responses and how they should be processed.
|
2
|
+
|
3
|
+
Use the `response` method with expected http response status(es):
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class CatsAPI < Evil::Client
|
7
|
+
response 200, 201, 404, 422, 500
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
These definitions are inherited by all subscopes/operations. You can reload them later for every separate status. The definition tells a client to accept responses with given statuses, and to return [rack-compatible response][rack response] as is.
|
12
|
+
|
13
|
+
Using a block, you can handle the response in a way you need. For example, the following code will extract and parse json body only.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
response 200 do |(_status, _headers, *body)|
|
17
|
+
JSON.parse(body.first) if body.any?
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
**Remember** that in rack responses body is always wrapped to array (enumerable structure).
|
22
|
+
|
23
|
+
Do you best to either wrap a response to your domain model, or raise a specific exception:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
response 200 do |(_status, _headers, *body)|
|
27
|
+
Cat.new(JSON.parse(body.first)) if body.any?
|
28
|
+
end
|
29
|
+
|
30
|
+
response 400, 422 do |_status, *|
|
31
|
+
raise "#{status}: Record invalid"
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
When you use client-specific [middleware], the `response` block will receive the result already processed by the whole middleware stack. The helper will serve a final step of its handling. Its result wouldn't be processed further in any way.
|
36
|
+
|
37
|
+
If a remote API will respond with a status, not defined for the operation, the `Evil::Client::ResponseError` will be risen. The exception carries both the response, and all its parts (status, headers, and body).
|
38
|
+
|
39
|
+
[rack response]: http://www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Response
|
40
|
+
[middleware]:
|
@@ -0,0 +1,121 @@
|
|
1
|
+
Evil clients use nested scopes to collect definitions and [options], used by single [operations].
|
2
|
+
|
3
|
+
## Root Scope
|
4
|
+
|
5
|
+
Options and definitions for a root scope should be made in a body of client class:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class CatsClient < Evil::Client
|
9
|
+
option :user, proc(&:to_s)
|
10
|
+
option :password, proc(&:to_s)
|
11
|
+
option :subdomain, proc(&:to_s)
|
12
|
+
|
13
|
+
validate(:valid_subdomain) { %w[wild domestic].include? subdomain }
|
14
|
+
|
15
|
+
path { "https://#{subdomain}.example.com/" }
|
16
|
+
http_method "get"
|
17
|
+
security { basic_auth(user, password) }
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
These options should be assigned to client instance (all undefined ones will be ignored by the initializer).
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
client = CatsClient.new user: "andy", password: "foo", subdomain: "wild"
|
25
|
+
client.options # => { user: "andy", password: "foo", subdomain: "wild" }
|
26
|
+
```
|
27
|
+
|
28
|
+
You're free to made definitions on the root level, or leave them to subscopes and concrete operations. The only recommendation is to define base [path] here for all subscopes to share it. It's well worth it to define [security] settings at the root level as well.
|
29
|
+
|
30
|
+
## Subscopes
|
31
|
+
|
32
|
+
Then you can define named subscope with additional options and definitions. The root options, validators and definitions are inherited by subscopes:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class CatsClient < Evil::Client
|
36
|
+
# ...
|
37
|
+
|
38
|
+
scope :cats do
|
39
|
+
# scope-specific options
|
40
|
+
option :version, proc(&:to_i)
|
41
|
+
|
42
|
+
validate(:supported_version) { version < 5 }
|
43
|
+
|
44
|
+
# scope-specific redefinition of the root settings
|
45
|
+
http_method { version.zero? ? :get : :post }
|
46
|
+
path { "cats/v#{version}" }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
cats = client.cats(version: 3)
|
53
|
+
cats.options # => { user: "andy", password: "foo", subdomain: "wild", version: 3 }
|
54
|
+
```
|
55
|
+
|
56
|
+
You can define any number of subscopes at any level of nesting. Every next level will inherit a previous one. There is no difference in DSL for various labels (with a single exclusion for [connection] -- you can assign it to client as a whole, not to a nested scope).
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class CatsClient < Evil::Client
|
60
|
+
# ...
|
61
|
+
scope :cats do
|
62
|
+
# ...
|
63
|
+
|
64
|
+
scope :video do
|
65
|
+
# ...
|
66
|
+
end
|
67
|
+
|
68
|
+
scope :books do
|
69
|
+
# ...
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Inside a scope you can define the operation -- endpoints to remote API. Any operation belongs to the containing scope and inherits its options, validators and shared definitions (and can reload any).
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class CatsClient < Evil::Client
|
79
|
+
# ...
|
80
|
+
scope :cats do
|
81
|
+
# ...
|
82
|
+
operation :fetch do
|
83
|
+
# ...
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Instantiation
|
90
|
+
|
91
|
+
There're several ways to instantiate the scope with options.
|
92
|
+
|
93
|
+
A verbose (explicit) style:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
client.scopes[:cats].new(version: 3)
|
97
|
+
```
|
98
|
+
|
99
|
+
...and a bit shorter version (`call` is an alias for `new`):
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
client.scopes[:cats].(version: 3)
|
103
|
+
```
|
104
|
+
|
105
|
+
...and even shorter (`[]` is an alias for `new` as well):
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
client.scopes[:cats][version: 3]
|
109
|
+
```
|
110
|
+
|
111
|
+
Or just use the name of the scope as a method:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
client.cats(version: 3)
|
115
|
+
```
|
116
|
+
|
117
|
+
[operations]:
|
118
|
+
[connection]:
|
119
|
+
[path]:
|
120
|
+
[security]:
|
121
|
+
[options]:
|
@@ -0,0 +1,102 @@
|
|
1
|
+
Use `security` declaration for the authorization schema.
|
2
|
+
|
3
|
+
Whether you use a block or not, the result should be hash with keys `:query` or/and `:headers`.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class CatsAPI < Evil::Client
|
7
|
+
option :token
|
8
|
+
|
9
|
+
security { { headers: { Authentication: token } } }
|
10
|
+
end
|
11
|
+
```
|
12
|
+
|
13
|
+
Inside the block we support 3 helper methods as well:
|
14
|
+
|
15
|
+
* `basic_auth`
|
16
|
+
* `token_auth`
|
17
|
+
* `key_auth`
|
18
|
+
|
19
|
+
## Basic Authentication
|
20
|
+
|
21
|
+
Use `basic_auth(login, password)` to define [basic authentication following RFC-7617][basic_auth]:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class CatsAPI < Evil::Client
|
25
|
+
option :login
|
26
|
+
option :password
|
27
|
+
|
28
|
+
security { basic_auth(login, password) }
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
This declaration with add a header `"Authentication" => "Basic {encoded token}"` to every request. The header is added independenlty of declaration for other [headers][headers].
|
33
|
+
|
34
|
+
## Token Authentication
|
35
|
+
|
36
|
+
The command `token_auth(token, **options)` allows you to insert a customizable token to any part of the request. Unlike `basic_auth`, you need to provide the token (build, encrypt etc.) by hand.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class CatsAPI < Evil::Client
|
40
|
+
option :token
|
41
|
+
|
42
|
+
security { token_auth(token) }
|
43
|
+
# ...
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
By default the token is added to `"Authentication" => {token}` header of the request. You can prepend it with a necessary prefix. For example, you can define a [Bearer token authentication following RFC-6750][bearer]:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class CatsAPI < Evil::Client
|
51
|
+
option :token
|
52
|
+
|
53
|
+
security { token_auth(token, prefix: "Bearer") }
|
54
|
+
# ...
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Instead of headers, you can send a token in a query. In this case the token will be sent under `access_key` without any prefix:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class CatsAPI < Evil::Client
|
62
|
+
option :token
|
63
|
+
|
64
|
+
security { token_auth(token, inside: :query) }
|
65
|
+
# ...
|
66
|
+
end
|
67
|
+
|
68
|
+
# will send a request to a path "..?access_key={token}"
|
69
|
+
```
|
70
|
+
|
71
|
+
## Authentication Using Arbitrary Key
|
72
|
+
|
73
|
+
Another option is to authenticate requests with an arbitrary key. This time key-value pair will be added to the selected part (either `headers` or `query`) of the request:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class CatsAPI < Evil::Client
|
77
|
+
option :token
|
78
|
+
|
79
|
+
security { key_auth :Authentication, token }
|
80
|
+
# ...
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
When a root setting is reloaded inside a subscope or operation, it totally reload previous declaration. If you need to combine root-level settings with operation-level ones, use either [headers] or a [query].
|
85
|
+
|
86
|
+
**Important**: When you define both headers/query, and security settings at the same time, the priority will be given to security. This isn't depend on where (root scope or its sub-scopes) security and headers/query parts are defined. Security settings will always be written over the same headers/query.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class CatsAPI < Evil::Client
|
90
|
+
security { key_auth :Authentication, "Bar" }
|
91
|
+
|
92
|
+
scope :cats do
|
93
|
+
headers { { Authentication: "Foo" } }
|
94
|
+
# will set "Authentication" => "Bar" (not "Foo")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
[basic_auth]: https://tools.ietf.org/html/rfc7617
|
100
|
+
[bearer]: https://tools.ietf.org/html/rfc6750
|
101
|
+
[headers]:
|
102
|
+
[query]:
|
@@ -0,0 +1,68 @@
|
|
1
|
+
Use `validate` helper to check interconnection between several options.
|
2
|
+
|
3
|
+
The helper takes name (unique for a current scope) and a block. Validation fails when a block returns falsey value.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class CatsAPI < Evil::Client
|
7
|
+
option :token, optional: true
|
8
|
+
option :user, optional: true
|
9
|
+
option :password, optional: true
|
10
|
+
|
11
|
+
# All requests should be made with either token or user/password
|
12
|
+
# This is required by any request
|
13
|
+
validate(:valid_credentials) { token ^ password }
|
14
|
+
validate(:password_given) { user ^ !password }
|
15
|
+
|
16
|
+
scope :cats do
|
17
|
+
option :version, proc(&:to_i)
|
18
|
+
|
19
|
+
# Check that operation of cats scope include token after API v1
|
20
|
+
# This doesn't affect other subscopes of CatsAPI root scope
|
21
|
+
validate(:token_based) { token || version.zero? }
|
22
|
+
end
|
23
|
+
|
24
|
+
# ...
|
25
|
+
end
|
26
|
+
|
27
|
+
CatsAPI.new password: "foo" # raises Evil::Client::ValidationError
|
28
|
+
```
|
29
|
+
|
30
|
+
The error message is translated using i18n gem. You should provide translations for a corresponding scope:
|
31
|
+
|
32
|
+
```yaml
|
33
|
+
# config/locales/evil-client.en.yml
|
34
|
+
---
|
35
|
+
en:
|
36
|
+
evil:
|
37
|
+
client:
|
38
|
+
errors:
|
39
|
+
cats_api:
|
40
|
+
valid_credentials: "Provide either a token or a password"
|
41
|
+
password_given: "User and password should accompany one another"
|
42
|
+
cats:
|
43
|
+
token_based: "The token is required for operations with cats in API v1+"
|
44
|
+
```
|
45
|
+
|
46
|
+
The root scope for error messages is `{locale}.evil.client.errors.{class_name}` as shown above.
|
47
|
+
|
48
|
+
Remember, that you can initialize client with some valid options, and then reload that options in a nested subscope/operation. All validations defined from the root of the client will be used for any set of options. See the example:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
client = CatsAPI.new token: "foo"
|
52
|
+
# valid
|
53
|
+
|
54
|
+
cats = client.cats(version: 0)
|
55
|
+
# valid
|
56
|
+
|
57
|
+
cats.fetch id: 3
|
58
|
+
# valid
|
59
|
+
|
60
|
+
cats.fetch id: 3, token: nil
|
61
|
+
# fails due to 'valid_credentials' is broken
|
62
|
+
|
63
|
+
cats.fetch id: 3, token: nil, user: "andy", password: "qux"
|
64
|
+
# valid
|
65
|
+
|
66
|
+
cats.fetch id: 3, token: nil, user: "andy", password: "qux", version: 1
|
67
|
+
# fails due to 'cats.token_based' is broken
|
68
|
+
```
|
data/docs/index.md
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
Human-friendly DSL for writing HTTP(s) clients in Ruby
|
2
2
|
|
3
|
-
|
3
|
+
<a href="https://evilmartians.com/">
|
4
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
4
5
|
|
5
|
-
|
6
|
+
[![Gem Version][gem-badger]][gem]
|
7
|
+
[![Build Status][travis-badger]][travis]
|
8
|
+
[![Dependency Status][gemnasium-badger]][gemnasium]
|
9
|
+
[![Code Climate][codeclimate-badger]][codeclimate]
|
10
|
+
[![Inline docs][inch-badger]][inch]
|
6
11
|
|
7
|
-
|
12
|
+
## Intro
|
8
13
|
|
9
|
-
The gem stands away from mutable states and monkey patching when possible. To support multithreading
|
14
|
+
The gem allows writing http(s) clients in a way inspired by [Swagger][swagger] specifications. It stands away from mutable states and monkey patching when possible. To support multithreading all instances are immutable (though not frozen to avoid performance loss).
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
The gem requires ruby 2.2+ and was tested under MRI and JRuby 9+.
|
14
|
-
|
15
|
-
# Installation
|
16
|
+
## Installation
|
16
17
|
|
17
18
|
Add this line to your application's Gemfile:
|
18
19
|
|
@@ -32,96 +33,87 @@ Or install it yourself as:
|
|
32
33
|
$ gem install evil-client
|
33
34
|
```
|
34
35
|
|
35
|
-
|
36
|
+
## Synopsis
|
36
37
|
|
37
|
-
The following example gives an idea of how a client to remote API looks like when written on top of `Evil::Client
|
38
|
+
The following example gives an idea of how a client to remote API looks like when written on top of `Evil::Client`.
|
38
39
|
|
39
40
|
```ruby
|
40
41
|
require "evil-client"
|
41
|
-
require "dry-types"
|
42
42
|
|
43
43
|
class CatsClient < Evil::Client
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
44
|
+
# Define options for the client's initializer
|
45
|
+
option :domain, proc(&:to_s)
|
46
|
+
option :user, proc(&:to_s)
|
47
|
+
option :password, proc(&:to_s)
|
64
48
|
|
65
49
|
# Definitions shared by all operations
|
66
|
-
|
67
|
-
|
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
|
50
|
+
path { "https://#{domain}.example.com/api" }
|
51
|
+
security { basic_auth settings.user, settings.password }
|
81
52
|
|
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
53
|
scope :cats do
|
93
|
-
|
94
|
-
|
95
|
-
|
54
|
+
# Scope-specific definitions
|
55
|
+
option :version, default: proc { 1 }
|
56
|
+
path { "v#{version}" } # subpath added to root path
|
57
|
+
|
58
|
+
# Operation-specific definitions to update a cat by id
|
59
|
+
operation :update do
|
60
|
+
option :id, proc(&:to_i)
|
61
|
+
option :name, optional: true
|
62
|
+
option :color, optional: true
|
63
|
+
option :age, optional: true
|
64
|
+
|
65
|
+
let(:data) { options.select { |key, _| %i(name color age).include? key } }
|
66
|
+
validate(:data_present) { !data.empty? }
|
67
|
+
|
68
|
+
path { "cats/#{id}" } # added to root path
|
69
|
+
http_method :patch # you can use plain syntax instead of a block
|
70
|
+
format "json"
|
71
|
+
body { options.reject { |key, _val| key == :id } }
|
72
|
+
|
73
|
+
# Parses json response and wraps it into Cat instance with additional
|
74
|
+
# parameter
|
75
|
+
response 200 do |(status, headers, body)|
|
76
|
+
# Suppose you define a model for cats
|
77
|
+
Cat.new JSON.parse(body)
|
96
78
|
end
|
79
|
+
|
80
|
+
# Parses json response, wraps it into model with [#error] and raises
|
81
|
+
# an exception where [ResponseError#response] contains the model istance
|
82
|
+
response(400, 422) { |(status, *)| raise "#{status}: Record invalid" }
|
97
83
|
end
|
98
84
|
end
|
99
85
|
end
|
100
86
|
|
101
|
-
# Instantiate a client with concrete settings
|
102
|
-
cat_client = CatClient.new "awesome-cats",
|
103
|
-
|
104
|
-
user: "cat_lover",
|
87
|
+
# Instantiate a client with a concrete settings
|
88
|
+
cat_client = CatClient.new domain: "awesome-cats",
|
89
|
+
user: "cat_lover",
|
105
90
|
password: "purr"
|
106
91
|
|
107
|
-
# Use low-level DSL to send requests
|
108
|
-
cat_client.
|
109
|
-
|
110
|
-
|
111
|
-
color: "tabby"
|
92
|
+
# Use verbose low-level DSL to send requests
|
93
|
+
cat_client.scopes[:cats].new(version: 2)
|
94
|
+
.operations[:update].new(id: 4, age: 10, color: "tabby")
|
95
|
+
.call # sends request
|
112
96
|
|
113
97
|
# Use top-level DSL for the same request
|
114
|
-
cat_client.cats
|
98
|
+
cat_client.cats(version: 2).update(id: 4, age: 10, color: "tabby")
|
115
99
|
|
116
|
-
# Both the methods send `PATCH https://
|
100
|
+
# Both the methods send `PATCH https://awesome-cats.example.com/api/v2/cats/4`
|
117
101
|
# with a specified body and headers (authorization via basic_auth)
|
118
102
|
```
|
119
103
|
|
120
|
-
|
104
|
+
## License
|
105
|
+
|
106
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
107
|
+
|
108
|
+
[codeclimate-badger]: https://img.shields.io/codeclimate/github/evilmartians/evil-client.svg?style=flat
|
109
|
+
[codeclimate]: https://codeclimate.com/github/evilmartians/evil-client
|
121
110
|
[dry-initializer]: http://dry-rb.org/gems/dry-initializer
|
122
|
-
[
|
123
|
-
[
|
124
|
-
[
|
125
|
-
[
|
126
|
-
[
|
127
|
-
[
|
111
|
+
[gem-badger]: https://img.shields.io/gem/v/evil-client.svg?style=flat
|
112
|
+
[gem]: https://rubygems.org/gems/evil-client
|
113
|
+
[gemnasium-badger]: https://img.shields.io/gemnasium/evilmartians/evil-client.svg?style=flat
|
114
|
+
[gemnasium]: https://gemnasium.com/evilmartians/evil-client
|
115
|
+
[inch-badger]: http://inch-ci.org/github/evilmartians/evil-client.svg
|
116
|
+
[inch]: https://inch-ci.org/github/evilmartians/evil-client
|
117
|
+
[swagger]: http://swagger.io
|
118
|
+
[travis-badger]: https://img.shields.io/travis/evilmartians/evil-client/master.svg?style=flat
|
119
|
+
[travis]: https://travis-ci.org/evilmartians/evil-client
|