evil-client 0.3.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|