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