evil-client 0.2.1
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 +7 -0
- data/.codeclimate.yml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +6 -0
- data/docs/base_url.md +38 -0
- data/docs/documentation.md +9 -0
- data/docs/headers.md +59 -0
- data/docs/http_method.md +31 -0
- data/docs/index.md +127 -0
- data/docs/license.md +19 -0
- data/docs/model.md +173 -0
- data/docs/operation.md +0 -0
- data/docs/overview.md +0 -0
- data/docs/path.md +48 -0
- data/docs/query.md +99 -0
- data/docs/responses.md +66 -0
- data/docs/security.md +102 -0
- data/docs/settings.md +32 -0
- data/evil-client.gemspec +25 -0
- data/lib/evil/client.rb +97 -0
- data/lib/evil/client/connection.rb +35 -0
- data/lib/evil/client/connection/net_http.rb +57 -0
- data/lib/evil/client/dsl.rb +110 -0
- data/lib/evil/client/dsl/files.rb +37 -0
- data/lib/evil/client/dsl/operation.rb +102 -0
- data/lib/evil/client/dsl/operations.rb +41 -0
- data/lib/evil/client/dsl/scope.rb +34 -0
- data/lib/evil/client/dsl/security.rb +57 -0
- data/lib/evil/client/middleware.rb +81 -0
- data/lib/evil/client/middleware/base.rb +15 -0
- data/lib/evil/client/middleware/merge_security.rb +16 -0
- data/lib/evil/client/middleware/normalize_headers.rb +13 -0
- data/lib/evil/client/middleware/stringify_form.rb +36 -0
- data/lib/evil/client/middleware/stringify_json.rb +15 -0
- data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
- data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
- data/lib/evil/client/middleware/stringify_query.rb +31 -0
- data/lib/evil/client/model.rb +65 -0
- data/lib/evil/client/operation.rb +34 -0
- data/lib/evil/client/operation/request.rb +42 -0
- data/lib/evil/client/operation/response.rb +40 -0
- data/lib/evil/client/operation/response_error.rb +12 -0
- data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
- data/mkdocs.yml +21 -0
- data/spec/features/instantiation_spec.rb +68 -0
- data/spec/features/middleware_spec.rb +75 -0
- data/spec/features/operation_with_documentation_spec.rb +41 -0
- data/spec/features/operation_with_files_spec.rb +40 -0
- data/spec/features/operation_with_form_body_spec.rb +158 -0
- data/spec/features/operation_with_headers_spec.rb +99 -0
- data/spec/features/operation_with_http_method_spec.rb +45 -0
- data/spec/features/operation_with_json_body_spec.rb +156 -0
- data/spec/features/operation_with_path_spec.rb +47 -0
- data/spec/features/operation_with_query_spec.rb +84 -0
- data/spec/features/operation_with_response_spec.rb +109 -0
- data/spec/features/operation_with_security_spec.rb +228 -0
- data/spec/features/scoping_spec.rb +48 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/test_client.rb +15 -0
- data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
- data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
- data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
- data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
- data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
- data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
- data/spec/unit/evil/client/dsl_spec.rb +57 -0
- data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
- data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
- data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
- data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
- data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
- data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
- data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
- data/spec/unit/evil/client/middleware_spec.rb +46 -0
- data/spec/unit/evil/client/model_spec.rb +100 -0
- data/spec/unit/evil/client/operation/request_spec.rb +49 -0
- data/spec/unit/evil/client/operation/response_spec.rb +61 -0
- metadata +271 -0
data/docs/responses.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
For every operation you have to describe all expected responses and how they should be processed.
|
2
|
+
|
3
|
+
Use the `response` method with anexpected http response status(es):
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
operation :find_cat do
|
7
|
+
# ...
|
8
|
+
response 200, 201
|
9
|
+
end
|
10
|
+
```
|
11
|
+
|
12
|
+
This definition tells a client to accept responses with given statuses, and to return an instance of `Rack::Response`.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
client.operations[:find_cat].call
|
16
|
+
# => #<Rack::Response @code=200 ...>
|
17
|
+
```
|
18
|
+
|
19
|
+
## Data Coersion
|
20
|
+
|
21
|
+
Instead of returning a raw rack response, you can coerce it using a block. The block will take 3 options, namely the response, its body and headers:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
operation :find_cat do |settings| # remember that you have access to settings
|
25
|
+
# ...
|
26
|
+
response 200 do |response:, body:, headers:|
|
27
|
+
JSON.parse(body) if settings.format == "json"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# later at a runtime
|
32
|
+
client.operations[:find_cat].call
|
33
|
+
# => { name: "Bastet", age: 10 }
|
34
|
+
```
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
## Raising Exceptions
|
39
|
+
|
40
|
+
When processing responces with error statuses you may need to raise an exception instead of returning values. Do this using option `raise: true`
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
operation :find_cat do
|
44
|
+
# ...
|
45
|
+
response 422, raise: true
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
This time the operation will raise a `Evil::Client::ResponseError` (inherited from the `RuntimeError`). The exception carries a rack response:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
begin
|
53
|
+
client.operations[:find_cat].call
|
54
|
+
rescue Evil::Client::ResponseError => error
|
55
|
+
error.response
|
56
|
+
# => #<Rack::Response @code=422 ...>
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
Like before, you can add a block to handle the response. In this case an exception will carry a result of the block.
|
61
|
+
|
62
|
+
## Unexpected Responses
|
63
|
+
|
64
|
+
In case the server responded with undefined status, the operation raises `Evil::Client::UnexpectedResponseError` (inherited from the `RuntimeError`) that carries a rack response just like the `Evil::Client::ResponseError` before.
|
65
|
+
|
66
|
+
Notice that you can declare default responses using anonymous `operation {}` syntax. Only those responces that are declared neither by default, nor for a specific operation, will cause unexpected response behaviour.
|
data/docs/security.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
Use `security` declaration for the authorization schema. Inside the block you have access to 3 methods:
|
2
|
+
* `basic_auth`
|
3
|
+
* `token_auth`
|
4
|
+
* `key_auth`
|
5
|
+
|
6
|
+
## Basic Authentication
|
7
|
+
|
8
|
+
Use `basic_auth(login, password)` to define [basic authentication following RFC-7617][basic_auth]:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
operation :find_cat do |settings|
|
12
|
+
security do
|
13
|
+
basic_auth settings.login, settings.password
|
14
|
+
end
|
15
|
+
end
|
16
|
+
```
|
17
|
+
|
18
|
+
This declaration with add a header `"Authentication" => "Basic {encoded token}"` to every request. The header is added independenlty of declaration for other [headers][headers].
|
19
|
+
|
20
|
+
## Token Authentication
|
21
|
+
|
22
|
+
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.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
operation :find_cat do |settings|
|
26
|
+
security do
|
27
|
+
token_auth settings.token
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
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]:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
operation :find_cat do |settings|
|
36
|
+
security do
|
37
|
+
token_auth settings.token, prefix: "Bearer"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Instead of headers, you can send a token in either request body, or a query. In this case the token will be sent under `access_key` ignoring a prefix:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
operation :find_cat do |settings|
|
46
|
+
path { "/cats" }
|
47
|
+
security do
|
48
|
+
token_auth settings.token, using: :query
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# will send a request to "../cats?access_key={token}"
|
53
|
+
```
|
54
|
+
|
55
|
+
## Authentication Using Arbitrary Key
|
56
|
+
|
57
|
+
The most customizeable option is to authenticate requests with an arbitrary key. This time key-value pair will be added to the selected part (`headers`, `body`, or `query`) of the request:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
operation :find_cat do |settings|
|
61
|
+
path { "/cats" }
|
62
|
+
security do
|
63
|
+
key_auth :accss_key, settings.token, using: :query
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
## Authentication Using Several Schemes
|
69
|
+
|
70
|
+
You can define several schemes for the same request. All of them will be applied at once:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
operation :find_cat do |settings|
|
74
|
+
security do
|
75
|
+
basic_auth settings.login, settings.password
|
76
|
+
token_auth settings.token, using: :query
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
Moreover, you can declare shared authentication by default, and either update, or reload it for a specific operation:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
operation do |settings|
|
85
|
+
security { basic_auth settings.login, settings.password }
|
86
|
+
end
|
87
|
+
|
88
|
+
operation :find_cat do |settings|
|
89
|
+
security { token_auth settings.token, using: :query } # added to default security
|
90
|
+
end
|
91
|
+
|
92
|
+
operation :find_cats do |settings|
|
93
|
+
security { token_auth settings.token } # reloads default "Authentication" header
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
|
98
|
+
[basic_auth]: https://tools.ietf.org/html/rfc7617
|
99
|
+
[bearer]: https://tools.ietf.org/html/rfc6750
|
100
|
+
[headers]:
|
101
|
+
[body]:
|
102
|
+
[query]:
|
data/docs/settings.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Use `settings` to parameterize an instance of the client.
|
2
|
+
|
3
|
+
Inside the block you can define both `param`s and `option`s for a client constructor. See [dry-initializer docs][dry-initializer] for detailed description of the methods' syntax.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
require "evil-client"
|
7
|
+
require "dry-types"
|
8
|
+
|
9
|
+
class CatsClient < Evil::Client
|
10
|
+
settings do
|
11
|
+
param :roor_url
|
12
|
+
option :version, type: Dry::Types["coercible.int"], default: proc { 1 }
|
13
|
+
option :login, type: Dry::Types["strict.string"] # required
|
14
|
+
option :password, type: Dry::Types["strict.string"] # required
|
15
|
+
end
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Now you can initialize a client:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
client = CatsClient.new "https://cats.example.com",
|
23
|
+
login: "cats_lover",
|
24
|
+
password: "purr"
|
25
|
+
```
|
26
|
+
|
27
|
+
A container with assigned settings will be passed to blocks declaring [base_url][base_url], [connection][connection], and [operations][operation].
|
28
|
+
|
29
|
+
[base_url]:
|
30
|
+
[connection]:
|
31
|
+
[operation]:
|
32
|
+
[dry-initializer]: http://dry-rb.org/gems/dry-initializer
|
data/evil-client.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = "evil-client"
|
3
|
+
gem.version = "0.2.1"
|
4
|
+
gem.author = "Andrew Kozin (nepalez)"
|
5
|
+
gem.email = "andrew.kozin@gmail.com"
|
6
|
+
gem.homepage = "https://github.com/evilmartians/evil-client"
|
7
|
+
gem.summary = "Human-friendly DSL for building HTTP(s) clients in Ruby"
|
8
|
+
gem.license = "MIT"
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
11
|
+
gem.test_files = gem.files.grep(/^spec/)
|
12
|
+
gem.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
|
13
|
+
|
14
|
+
gem.required_ruby_version = ">= 2.3"
|
15
|
+
|
16
|
+
gem.add_runtime_dependency "dry-initializer", "~> 0.7.0"
|
17
|
+
gem.add_runtime_dependency "mime-types", "~> 3.0"
|
18
|
+
gem.add_runtime_dependency "rack"
|
19
|
+
|
20
|
+
gem.add_development_dependency "dry-types", "~> 0.9"
|
21
|
+
gem.add_development_dependency "rspec", "~> 3.0"
|
22
|
+
gem.add_development_dependency "rake", "~> 11"
|
23
|
+
gem.add_development_dependency "webmock", "~> 2.1"
|
24
|
+
gem.add_development_dependency "rubocop", "~> 0.44"
|
25
|
+
end
|
data/lib/evil/client.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require "dry-initializer"
|
2
|
+
require "mime-types"
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
# Absctract base class for clients to remote APIs
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
# @example
|
9
|
+
# class MyClient < Evil::Client
|
10
|
+
# # declare settings for the client's constructor
|
11
|
+
# # the settings parameterize the rest of the client's definitions
|
12
|
+
# settings do
|
13
|
+
# option :version, type: Dry::Types["strict.int"].default(1)
|
14
|
+
# option :user, type: Dry::Types["strict.string"]
|
15
|
+
# option :token, type: Dry::Types["strict.string"]
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # define base url of the server
|
19
|
+
# base_url do |settings|
|
20
|
+
# "https://my_api.com/v#{settings.version}"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # define connection and its middleware stack from bottom to top
|
24
|
+
# connection :net_http do |settings|
|
25
|
+
# run AddCustomRequestId
|
26
|
+
# run EncryptToken if settings.token
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # definitions shared by all operations (can be reloaded later)
|
30
|
+
# operation do |settings|
|
31
|
+
# type { :json }
|
32
|
+
# security { basic_auth "foo", "bar" }
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # operation-specific definitions
|
36
|
+
# operation :find_cat do |settings|
|
37
|
+
# http_method :get
|
38
|
+
# path { "#{settings.url}/cats/find/#{id}" }
|
39
|
+
#
|
40
|
+
# query do
|
41
|
+
# option :id, type: Dry::Types["coercible.int"].constrained(gt: 0)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# response 200, model: Cat
|
45
|
+
# response 400, raise: true
|
46
|
+
# response 422, raise: true do |body:|
|
47
|
+
# JSON.parse(body.first)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # top-level DSL for operation
|
52
|
+
# scope :users do
|
53
|
+
# scope do # named `:[]` by default
|
54
|
+
# param :id, type: Dry::Types["strict.int"]
|
55
|
+
#
|
56
|
+
# def get
|
57
|
+
# operations[:find_users].call(id: id)
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # Initialize a client with a corresponding settings
|
64
|
+
# client = MyClient.new user: "andrew", token: "f982j23"
|
65
|
+
#
|
66
|
+
# # Use low-level DSL for searching a user
|
67
|
+
# client.operations[:find_user].call(id: 1)
|
68
|
+
#
|
69
|
+
# # Use top-level DSL for the same operation
|
70
|
+
# client.users[1].get
|
71
|
+
#
|
72
|
+
module Evil
|
73
|
+
class Client
|
74
|
+
require_relative "client/model"
|
75
|
+
require_relative "client/connection"
|
76
|
+
require_relative "client/middleware"
|
77
|
+
require_relative "client/operation"
|
78
|
+
require_relative "client/dsl"
|
79
|
+
|
80
|
+
extend DSL
|
81
|
+
include Dry::Initializer.define -> { param :operations }
|
82
|
+
|
83
|
+
# Builds a client instance with custom settings
|
84
|
+
def self.new(*settings)
|
85
|
+
super finalize(*settings)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def initialize(schema)
|
91
|
+
@operations = \
|
92
|
+
schema[:operations].each_with_object({}) do |(key, val), hash|
|
93
|
+
hash[key] = Operation.new val, schema[:connection]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
# @abstract Base class for a specific connection to remote uri
|
3
|
+
class Connection
|
4
|
+
REGISTRY = { net_http: "NetHTTP" }.freeze
|
5
|
+
|
6
|
+
extend Dry::Initializer::Mixin
|
7
|
+
param :base_uri
|
8
|
+
|
9
|
+
# Envokes a specific connection class
|
10
|
+
#
|
11
|
+
# @param [#to_sym] key
|
12
|
+
# @return [Class]
|
13
|
+
#
|
14
|
+
def self.[](name = nil)
|
15
|
+
key = (name || REGISTRY.keys.first).to_sym
|
16
|
+
|
17
|
+
klass = REGISTRY.fetch(key) do
|
18
|
+
fail ArgumentError.new "Connection '#{key}' is not registered." \
|
19
|
+
" Use the following keys: #{REGISTRY.keys}"
|
20
|
+
end
|
21
|
+
|
22
|
+
require_relative "connection/#{key}"
|
23
|
+
const_get klass
|
24
|
+
end
|
25
|
+
|
26
|
+
# @abstract Sends request to the server and returns rack-compatible response
|
27
|
+
#
|
28
|
+
# @param [Hash] env
|
29
|
+
# @return [Array]
|
30
|
+
#
|
31
|
+
def call(_env)
|
32
|
+
fail NotImplementedError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
|
4
|
+
class Evil::Client
|
5
|
+
class Connection
|
6
|
+
# Net::HTTP based implementation of [Evil::Client::Connection]
|
7
|
+
class NetHTTP < Connection
|
8
|
+
# Sends a request to the remote uri,
|
9
|
+
# and returns rack-compatible response
|
10
|
+
#
|
11
|
+
# @param [Hash] env Middleware environment with keys:
|
12
|
+
# :http_method, :path, :query_string, :body_string, :headers
|
13
|
+
# @return [Array] Rack-compatible response [status, body, headers]
|
14
|
+
#
|
15
|
+
def call(env)
|
16
|
+
request = build_request(env)
|
17
|
+
Net::HTTP.start base_uri.host, base_uri.port, opts do |http|
|
18
|
+
handle http.request(request)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def opts
|
25
|
+
@opts ||= {}.tap { |hash| hash[:use_ssl] = base_uri.scheme == "https" }
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_request(env)
|
29
|
+
type, path, query, body, headers = parse_env(env)
|
30
|
+
|
31
|
+
sender = build_sender(type)
|
32
|
+
uri = build_uri(path, query)
|
33
|
+
|
34
|
+
sender.new(uri).tap do |request|
|
35
|
+
request.body = body
|
36
|
+
headers.each { |key, value| request[key] = value }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_env(env)
|
41
|
+
env.values_at :http_method, :path, :query_string, :body_string, :headers
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_sender(type)
|
45
|
+
Net::HTTP.const_get type.capitalize
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_uri(path, query)
|
49
|
+
base_uri.merge(path).tap { |uri| uri.query = query }
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle(response)
|
53
|
+
[response.code.to_i, Hash(response.header), Array(response.body)]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
# Defines a DSL to customize class-level settings of the specific client
|
3
|
+
module DSL
|
4
|
+
require_relative "dsl/operations"
|
5
|
+
require_relative "dsl/scope"
|
6
|
+
|
7
|
+
# Stack of default middleware before custom midleware and a connection
|
8
|
+
# This stack cannot be modified
|
9
|
+
DEFAULT_MIDDLEWARE = Middleware.new do
|
10
|
+
run Middleware::MergeSecurity
|
11
|
+
run Middleware::StringifyJson
|
12
|
+
run Middleware::StringifyQuery
|
13
|
+
run Middleware::NormalizeHeaders
|
14
|
+
end
|
15
|
+
|
16
|
+
# Helper to define params and options a for a client's constructor
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# class MyClient < Evil::Client
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyClient.new "https://foo.com", user: "bar", token: "baz"
|
23
|
+
#
|
24
|
+
# @param [Proc] block
|
25
|
+
# @return [self]
|
26
|
+
#
|
27
|
+
def settings(&block)
|
28
|
+
return self unless block
|
29
|
+
schema[:settings] = Class.new { include Dry::Initializer.define(&block) }
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helper to define base url of the server
|
34
|
+
#
|
35
|
+
# @param [#to_s] value
|
36
|
+
# @return [self]
|
37
|
+
#
|
38
|
+
def base_url(&block)
|
39
|
+
return self unless block
|
40
|
+
schema[:base_url] = block
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Helper specify a connection to be used by a client
|
45
|
+
#
|
46
|
+
# @param [#to_sym] type (nil)
|
47
|
+
# The specific type of connection. Uses NetHTTP by default.
|
48
|
+
# @return [self]
|
49
|
+
#
|
50
|
+
def connection(type = nil, &block)
|
51
|
+
schema[:connection] = Connection[type]
|
52
|
+
schema[:middleware] = Middleware.new(&block)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Helper to declare operation, either default or specific
|
57
|
+
#
|
58
|
+
# @param [#to_sym] name (nil)
|
59
|
+
# @param [Proc] block
|
60
|
+
# @return [self]
|
61
|
+
#
|
62
|
+
def operation(name = nil, &block)
|
63
|
+
schema[:operations].register(name, &block)
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper to define scopes of the client's top-level DSL
|
68
|
+
#
|
69
|
+
# @param [#to_sym] name (:[])
|
70
|
+
# @param [Proc] block
|
71
|
+
# @return [self]
|
72
|
+
#
|
73
|
+
def scope(name = :[], &block)
|
74
|
+
klass = Class.new(Scope, &block)
|
75
|
+
define_method(name) do |*args, **options|
|
76
|
+
klass.new(*args, __scope__: self, **options)
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Takes constructor arguments and builds a final schema for the instance
|
82
|
+
#
|
83
|
+
# @param [Object] *args
|
84
|
+
# @return [Hash<Symbol, Object>]
|
85
|
+
#
|
86
|
+
def finalize(*args)
|
87
|
+
settings = schema[:settings].new(*args)
|
88
|
+
uri = URI(schema[:base_url].call(settings))
|
89
|
+
client = schema[:connection].new(uri)
|
90
|
+
middleware = schema[:middleware].finalize(settings)
|
91
|
+
stack = Middleware.prepend.(middleware.(Middleware.append.(client)))
|
92
|
+
|
93
|
+
{ connection: stack, operations: schema[:operations].finalize(settings) }
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
BASE_URL = -> (_) { fail NotImplementedError.new "Base url not defined" }
|
99
|
+
|
100
|
+
def schema
|
101
|
+
@schema ||= {
|
102
|
+
settings: Class.new,
|
103
|
+
base_url: BASE_URL,
|
104
|
+
connection: Connection[nil],
|
105
|
+
middleware: Middleware.new,
|
106
|
+
operations: Operations.new
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|