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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +0 -11
  3. data/.gitignore +1 -0
  4. data/.rspec +0 -1
  5. data/.rubocop.yml +22 -19
  6. data/.travis.yml +1 -0
  7. data/CHANGELOG.md +251 -6
  8. data/LICENSE.txt +3 -1
  9. data/README.md +47 -81
  10. data/docs/helpers/body.md +93 -0
  11. data/docs/helpers/connection.md +19 -0
  12. data/docs/helpers/headers.md +72 -0
  13. data/docs/helpers/http_method.md +39 -0
  14. data/docs/helpers/let.md +14 -0
  15. data/docs/helpers/logger.md +24 -0
  16. data/docs/helpers/middleware.md +56 -0
  17. data/docs/helpers/operation.md +103 -0
  18. data/docs/helpers/option.md +50 -0
  19. data/docs/helpers/path.md +37 -0
  20. data/docs/helpers/query.md +59 -0
  21. data/docs/helpers/response.md +40 -0
  22. data/docs/helpers/scope.md +121 -0
  23. data/docs/helpers/security.md +102 -0
  24. data/docs/helpers/validate.md +68 -0
  25. data/docs/index.md +70 -78
  26. data/docs/license.md +5 -1
  27. data/docs/rspec.md +96 -0
  28. data/evil-client.gemspec +10 -8
  29. data/lib/evil/client.rb +126 -72
  30. data/lib/evil/client/builder.rb +47 -0
  31. data/lib/evil/client/builder/operation.rb +40 -0
  32. data/lib/evil/client/builder/scope.rb +31 -0
  33. data/lib/evil/client/chaining.rb +17 -0
  34. data/lib/evil/client/connection.rb +60 -20
  35. data/lib/evil/client/container.rb +66 -0
  36. data/lib/evil/client/container/operation.rb +23 -0
  37. data/lib/evil/client/container/scope.rb +28 -0
  38. data/lib/evil/client/exceptions/definition_error.rb +15 -0
  39. data/lib/evil/client/exceptions/name_error.rb +32 -0
  40. data/lib/evil/client/exceptions/response_error.rb +42 -0
  41. data/lib/evil/client/exceptions/type_error.rb +29 -0
  42. data/lib/evil/client/exceptions/validation_error.rb +27 -0
  43. data/lib/evil/client/formatter.rb +49 -0
  44. data/lib/evil/client/formatter/form.rb +45 -0
  45. data/lib/evil/client/formatter/multipart.rb +33 -0
  46. data/lib/evil/client/formatter/part.rb +66 -0
  47. data/lib/evil/client/formatter/text.rb +21 -0
  48. data/lib/evil/client/resolver.rb +84 -0
  49. data/lib/evil/client/resolver/body.rb +22 -0
  50. data/lib/evil/client/resolver/format.rb +30 -0
  51. data/lib/evil/client/resolver/headers.rb +46 -0
  52. data/lib/evil/client/resolver/http_method.rb +34 -0
  53. data/lib/evil/client/resolver/middleware.rb +36 -0
  54. data/lib/evil/client/resolver/query.rb +39 -0
  55. data/lib/evil/client/resolver/request.rb +96 -0
  56. data/lib/evil/client/resolver/response.rb +26 -0
  57. data/lib/evil/client/resolver/security.rb +113 -0
  58. data/lib/evil/client/resolver/uri.rb +35 -0
  59. data/lib/evil/client/rspec.rb +127 -0
  60. data/lib/evil/client/schema.rb +105 -0
  61. data/lib/evil/client/schema/operation.rb +177 -0
  62. data/lib/evil/client/schema/scope.rb +73 -0
  63. data/lib/evil/client/settings.rb +172 -0
  64. data/lib/evil/client/settings/validator.rb +64 -0
  65. data/mkdocs.yml +21 -15
  66. data/spec/features/custom_connection_spec.rb +17 -0
  67. data/spec/features/operation/middleware_spec.rb +50 -0
  68. data/spec/features/operation/options_spec.rb +71 -0
  69. data/spec/features/operation/request_spec.rb +94 -0
  70. data/spec/features/operation/response_spec.rb +48 -0
  71. data/spec/features/scope/options_spec.rb +52 -0
  72. data/spec/fixtures/locales/en.yml +16 -0
  73. data/spec/fixtures/test_client.rb +76 -0
  74. data/spec/spec_helper.rb +18 -6
  75. data/spec/support/fixtures_helper.rb +7 -0
  76. data/spec/unit/builder/operation_spec.rb +90 -0
  77. data/spec/unit/builder/scope_spec.rb +84 -0
  78. data/spec/unit/client_spec.rb +137 -0
  79. data/spec/unit/connection_spec.rb +78 -0
  80. data/spec/unit/container/operation_spec.rb +81 -0
  81. data/spec/unit/container/scope_spec.rb +61 -0
  82. data/spec/unit/container_spec.rb +107 -0
  83. data/spec/unit/exceptions/definition_error_spec.rb +15 -0
  84. data/spec/unit/exceptions/name_error_spec.rb +77 -0
  85. data/spec/unit/exceptions/response_error_spec.rb +22 -0
  86. data/spec/unit/exceptions/type_error_spec.rb +71 -0
  87. data/spec/unit/exceptions/validation_error_spec.rb +13 -0
  88. data/spec/unit/formatter/form_spec.rb +27 -0
  89. data/spec/unit/formatter/multipart_spec.rb +23 -0
  90. data/spec/unit/formatter/part_spec.rb +49 -0
  91. data/spec/unit/formatter/text_spec.rb +37 -0
  92. data/spec/unit/formatter_spec.rb +46 -0
  93. data/spec/unit/resolver/body_spec.rb +65 -0
  94. data/spec/unit/resolver/format_spec.rb +66 -0
  95. data/spec/unit/resolver/headers_spec.rb +93 -0
  96. data/spec/unit/resolver/http_method_spec.rb +67 -0
  97. data/spec/unit/resolver/middleware_spec.rb +83 -0
  98. data/spec/unit/resolver/query_spec.rb +85 -0
  99. data/spec/unit/resolver/request_spec.rb +121 -0
  100. data/spec/unit/resolver/response_spec.rb +64 -0
  101. data/spec/unit/resolver/security_spec.rb +156 -0
  102. data/spec/unit/resolver/uri_spec.rb +117 -0
  103. data/spec/unit/rspec_spec.rb +342 -0
  104. data/spec/unit/schema/operation_spec.rb +309 -0
  105. data/spec/unit/schema/scope_spec.rb +110 -0
  106. data/spec/unit/schema_spec.rb +157 -0
  107. data/spec/unit/settings/validator_spec.rb +128 -0
  108. data/spec/unit/settings_spec.rb +248 -0
  109. metadata +192 -135
  110. data/docs/base_url.md +0 -38
  111. data/docs/documentation.md +0 -9
  112. data/docs/headers.md +0 -59
  113. data/docs/http_method.md +0 -31
  114. data/docs/model.md +0 -173
  115. data/docs/operation.md +0 -0
  116. data/docs/overview.md +0 -0
  117. data/docs/path.md +0 -48
  118. data/docs/query.md +0 -99
  119. data/docs/responses.md +0 -66
  120. data/docs/security.md +0 -102
  121. data/docs/settings.md +0 -32
  122. data/lib/evil/client/connection/net_http.rb +0 -57
  123. data/lib/evil/client/dsl.rb +0 -127
  124. data/lib/evil/client/dsl/base.rb +0 -26
  125. data/lib/evil/client/dsl/files.rb +0 -37
  126. data/lib/evil/client/dsl/headers.rb +0 -16
  127. data/lib/evil/client/dsl/http_method.rb +0 -24
  128. data/lib/evil/client/dsl/operation.rb +0 -91
  129. data/lib/evil/client/dsl/operations.rb +0 -41
  130. data/lib/evil/client/dsl/path.rb +0 -25
  131. data/lib/evil/client/dsl/query.rb +0 -16
  132. data/lib/evil/client/dsl/response.rb +0 -61
  133. data/lib/evil/client/dsl/responses.rb +0 -29
  134. data/lib/evil/client/dsl/scope.rb +0 -27
  135. data/lib/evil/client/dsl/security.rb +0 -57
  136. data/lib/evil/client/dsl/verifier.rb +0 -35
  137. data/lib/evil/client/middleware.rb +0 -81
  138. data/lib/evil/client/middleware/base.rb +0 -11
  139. data/lib/evil/client/middleware/merge_security.rb +0 -20
  140. data/lib/evil/client/middleware/normalize_headers.rb +0 -17
  141. data/lib/evil/client/middleware/stringify_form.rb +0 -40
  142. data/lib/evil/client/middleware/stringify_json.rb +0 -19
  143. data/lib/evil/client/middleware/stringify_multipart.rb +0 -36
  144. data/lib/evil/client/middleware/stringify_multipart/part.rb +0 -36
  145. data/lib/evil/client/middleware/stringify_query.rb +0 -35
  146. data/lib/evil/client/operation.rb +0 -34
  147. data/lib/evil/client/operation/request.rb +0 -26
  148. data/lib/evil/client/operation/response.rb +0 -39
  149. data/lib/evil/client/operation/response_error.rb +0 -13
  150. data/lib/evil/client/operation/unexpected_response_error.rb +0 -19
  151. data/spec/features/instantiation_spec.rb +0 -68
  152. data/spec/features/middleware_spec.rb +0 -79
  153. data/spec/features/operation_with_documentation_spec.rb +0 -41
  154. data/spec/features/operation_with_files_spec.rb +0 -40
  155. data/spec/features/operation_with_form_body_spec.rb +0 -158
  156. data/spec/features/operation_with_headers_spec.rb +0 -99
  157. data/spec/features/operation_with_http_method_spec.rb +0 -45
  158. data/spec/features/operation_with_json_body_spec.rb +0 -156
  159. data/spec/features/operation_with_nested_responses_spec.rb +0 -95
  160. data/spec/features/operation_with_path_spec.rb +0 -47
  161. data/spec/features/operation_with_query_spec.rb +0 -84
  162. data/spec/features/operation_with_security_spec.rb +0 -228
  163. data/spec/features/scoping_spec.rb +0 -48
  164. data/spec/support/test_client.rb +0 -15
  165. data/spec/unit/evil/client/connection/net_http_spec.rb +0 -38
  166. data/spec/unit/evil/client/dsl/files_spec.rb +0 -37
  167. data/spec/unit/evil/client/dsl/operation_spec.rb +0 -374
  168. data/spec/unit/evil/client/dsl/operations_spec.rb +0 -29
  169. data/spec/unit/evil/client/dsl/scope_spec.rb +0 -32
  170. data/spec/unit/evil/client/dsl/security_spec.rb +0 -135
  171. data/spec/unit/evil/client/middleware/merge_security_spec.rb +0 -32
  172. data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +0 -17
  173. data/spec/unit/evil/client/middleware/stringify_form_spec.rb +0 -63
  174. data/spec/unit/evil/client/middleware/stringify_json_spec.rb +0 -61
  175. data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +0 -59
  176. data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +0 -62
  177. data/spec/unit/evil/client/middleware/stringify_query_spec.rb +0 -40
  178. data/spec/unit/evil/client/middleware_spec.rb +0 -46
  179. data/spec/unit/evil/client/operation/request_spec.rb +0 -49
  180. 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
- [![Logo][evilmartians-logo]][evilmartians]
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
- # About
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
- 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.
12
+ ## Intro
8
13
 
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.
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
- 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
+ ## 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
- # Example
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` using [dry-types][dry-types]-based contracts.
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
- # describe a client-specific model of cat (the furry pinnacle of evolution)
45
- class Cat < Evil::Struct
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
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
- 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
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
- scope do |id|
94
- def find(**data)
95
- operations[:update_cat].call(id: id, **data)
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", # domain
103
- version: 1,
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.operations[:update_cat].call id: 4,
109
- age: 10,
110
- name: "Agamemnon",
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[4].call(age: 10, name: "Agamemnon", color: "tabby")
98
+ cat_client.cats(version: 2).update(id: 4, age: 10, color: "tabby")
115
99
 
116
- # Both the methods send `PATCH https://awesom-cats.example.com/api/v1/cats/7`
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
- [swagger]: http://swagger.io
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
- [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]:
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