evil-client 0.3.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +0 -11
- data/.gitignore +1 -0
- data/.rspec +0 -1
- data/.rubocop.yml +22 -19
- data/.travis.yml +1 -0
- data/CHANGELOG.md +251 -6
- data/LICENSE.txt +3 -1
- data/README.md +47 -81
- data/docs/helpers/body.md +93 -0
- data/docs/helpers/connection.md +19 -0
- data/docs/helpers/headers.md +72 -0
- data/docs/helpers/http_method.md +39 -0
- data/docs/helpers/let.md +14 -0
- data/docs/helpers/logger.md +24 -0
- data/docs/helpers/middleware.md +56 -0
- data/docs/helpers/operation.md +103 -0
- data/docs/helpers/option.md +50 -0
- data/docs/helpers/path.md +37 -0
- data/docs/helpers/query.md +59 -0
- data/docs/helpers/response.md +40 -0
- data/docs/helpers/scope.md +121 -0
- data/docs/helpers/security.md +102 -0
- data/docs/helpers/validate.md +68 -0
- data/docs/index.md +70 -78
- data/docs/license.md +5 -1
- data/docs/rspec.md +96 -0
- data/evil-client.gemspec +10 -8
- data/lib/evil/client.rb +126 -72
- data/lib/evil/client/builder.rb +47 -0
- data/lib/evil/client/builder/operation.rb +40 -0
- data/lib/evil/client/builder/scope.rb +31 -0
- data/lib/evil/client/chaining.rb +17 -0
- data/lib/evil/client/connection.rb +60 -20
- data/lib/evil/client/container.rb +66 -0
- data/lib/evil/client/container/operation.rb +23 -0
- data/lib/evil/client/container/scope.rb +28 -0
- data/lib/evil/client/exceptions/definition_error.rb +15 -0
- data/lib/evil/client/exceptions/name_error.rb +32 -0
- data/lib/evil/client/exceptions/response_error.rb +42 -0
- data/lib/evil/client/exceptions/type_error.rb +29 -0
- data/lib/evil/client/exceptions/validation_error.rb +27 -0
- data/lib/evil/client/formatter.rb +49 -0
- data/lib/evil/client/formatter/form.rb +45 -0
- data/lib/evil/client/formatter/multipart.rb +33 -0
- data/lib/evil/client/formatter/part.rb +66 -0
- data/lib/evil/client/formatter/text.rb +21 -0
- data/lib/evil/client/resolver.rb +84 -0
- data/lib/evil/client/resolver/body.rb +22 -0
- data/lib/evil/client/resolver/format.rb +30 -0
- data/lib/evil/client/resolver/headers.rb +46 -0
- data/lib/evil/client/resolver/http_method.rb +34 -0
- data/lib/evil/client/resolver/middleware.rb +36 -0
- data/lib/evil/client/resolver/query.rb +39 -0
- data/lib/evil/client/resolver/request.rb +96 -0
- data/lib/evil/client/resolver/response.rb +26 -0
- data/lib/evil/client/resolver/security.rb +113 -0
- data/lib/evil/client/resolver/uri.rb +35 -0
- data/lib/evil/client/rspec.rb +127 -0
- data/lib/evil/client/schema.rb +105 -0
- data/lib/evil/client/schema/operation.rb +177 -0
- data/lib/evil/client/schema/scope.rb +73 -0
- data/lib/evil/client/settings.rb +172 -0
- data/lib/evil/client/settings/validator.rb +64 -0
- data/mkdocs.yml +21 -15
- data/spec/features/custom_connection_spec.rb +17 -0
- data/spec/features/operation/middleware_spec.rb +50 -0
- data/spec/features/operation/options_spec.rb +71 -0
- data/spec/features/operation/request_spec.rb +94 -0
- data/spec/features/operation/response_spec.rb +48 -0
- data/spec/features/scope/options_spec.rb +52 -0
- data/spec/fixtures/locales/en.yml +16 -0
- data/spec/fixtures/test_client.rb +76 -0
- data/spec/spec_helper.rb +18 -6
- data/spec/support/fixtures_helper.rb +7 -0
- data/spec/unit/builder/operation_spec.rb +90 -0
- data/spec/unit/builder/scope_spec.rb +84 -0
- data/spec/unit/client_spec.rb +137 -0
- data/spec/unit/connection_spec.rb +78 -0
- data/spec/unit/container/operation_spec.rb +81 -0
- data/spec/unit/container/scope_spec.rb +61 -0
- data/spec/unit/container_spec.rb +107 -0
- data/spec/unit/exceptions/definition_error_spec.rb +15 -0
- data/spec/unit/exceptions/name_error_spec.rb +77 -0
- data/spec/unit/exceptions/response_error_spec.rb +22 -0
- data/spec/unit/exceptions/type_error_spec.rb +71 -0
- data/spec/unit/exceptions/validation_error_spec.rb +13 -0
- data/spec/unit/formatter/form_spec.rb +27 -0
- data/spec/unit/formatter/multipart_spec.rb +23 -0
- data/spec/unit/formatter/part_spec.rb +49 -0
- data/spec/unit/formatter/text_spec.rb +37 -0
- data/spec/unit/formatter_spec.rb +46 -0
- data/spec/unit/resolver/body_spec.rb +65 -0
- data/spec/unit/resolver/format_spec.rb +66 -0
- data/spec/unit/resolver/headers_spec.rb +93 -0
- data/spec/unit/resolver/http_method_spec.rb +67 -0
- data/spec/unit/resolver/middleware_spec.rb +83 -0
- data/spec/unit/resolver/query_spec.rb +85 -0
- data/spec/unit/resolver/request_spec.rb +121 -0
- data/spec/unit/resolver/response_spec.rb +64 -0
- data/spec/unit/resolver/security_spec.rb +156 -0
- data/spec/unit/resolver/uri_spec.rb +117 -0
- data/spec/unit/rspec_spec.rb +342 -0
- data/spec/unit/schema/operation_spec.rb +309 -0
- data/spec/unit/schema/scope_spec.rb +110 -0
- data/spec/unit/schema_spec.rb +157 -0
- data/spec/unit/settings/validator_spec.rb +128 -0
- data/spec/unit/settings_spec.rb +248 -0
- metadata +192 -135
- data/docs/base_url.md +0 -38
- data/docs/documentation.md +0 -9
- data/docs/headers.md +0 -59
- data/docs/http_method.md +0 -31
- data/docs/model.md +0 -173
- data/docs/operation.md +0 -0
- data/docs/overview.md +0 -0
- data/docs/path.md +0 -48
- data/docs/query.md +0 -99
- data/docs/responses.md +0 -66
- data/docs/security.md +0 -102
- data/docs/settings.md +0 -32
- data/lib/evil/client/connection/net_http.rb +0 -57
- data/lib/evil/client/dsl.rb +0 -127
- data/lib/evil/client/dsl/base.rb +0 -26
- data/lib/evil/client/dsl/files.rb +0 -37
- data/lib/evil/client/dsl/headers.rb +0 -16
- data/lib/evil/client/dsl/http_method.rb +0 -24
- data/lib/evil/client/dsl/operation.rb +0 -91
- data/lib/evil/client/dsl/operations.rb +0 -41
- data/lib/evil/client/dsl/path.rb +0 -25
- data/lib/evil/client/dsl/query.rb +0 -16
- data/lib/evil/client/dsl/response.rb +0 -61
- data/lib/evil/client/dsl/responses.rb +0 -29
- data/lib/evil/client/dsl/scope.rb +0 -27
- data/lib/evil/client/dsl/security.rb +0 -57
- data/lib/evil/client/dsl/verifier.rb +0 -35
- data/lib/evil/client/middleware.rb +0 -81
- data/lib/evil/client/middleware/base.rb +0 -11
- data/lib/evil/client/middleware/merge_security.rb +0 -20
- data/lib/evil/client/middleware/normalize_headers.rb +0 -17
- data/lib/evil/client/middleware/stringify_form.rb +0 -40
- data/lib/evil/client/middleware/stringify_json.rb +0 -19
- data/lib/evil/client/middleware/stringify_multipart.rb +0 -36
- data/lib/evil/client/middleware/stringify_multipart/part.rb +0 -36
- data/lib/evil/client/middleware/stringify_query.rb +0 -35
- data/lib/evil/client/operation.rb +0 -34
- data/lib/evil/client/operation/request.rb +0 -26
- data/lib/evil/client/operation/response.rb +0 -39
- data/lib/evil/client/operation/response_error.rb +0 -13
- data/lib/evil/client/operation/unexpected_response_error.rb +0 -19
- data/spec/features/instantiation_spec.rb +0 -68
- data/spec/features/middleware_spec.rb +0 -79
- data/spec/features/operation_with_documentation_spec.rb +0 -41
- data/spec/features/operation_with_files_spec.rb +0 -40
- data/spec/features/operation_with_form_body_spec.rb +0 -158
- data/spec/features/operation_with_headers_spec.rb +0 -99
- data/spec/features/operation_with_http_method_spec.rb +0 -45
- data/spec/features/operation_with_json_body_spec.rb +0 -156
- data/spec/features/operation_with_nested_responses_spec.rb +0 -95
- data/spec/features/operation_with_path_spec.rb +0 -47
- data/spec/features/operation_with_query_spec.rb +0 -84
- data/spec/features/operation_with_security_spec.rb +0 -228
- data/spec/features/scoping_spec.rb +0 -48
- data/spec/support/test_client.rb +0 -15
- data/spec/unit/evil/client/connection/net_http_spec.rb +0 -38
- data/spec/unit/evil/client/dsl/files_spec.rb +0 -37
- data/spec/unit/evil/client/dsl/operation_spec.rb +0 -374
- data/spec/unit/evil/client/dsl/operations_spec.rb +0 -29
- data/spec/unit/evil/client/dsl/scope_spec.rb +0 -32
- data/spec/unit/evil/client/dsl/security_spec.rb +0 -135
- data/spec/unit/evil/client/middleware/merge_security_spec.rb +0 -32
- data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +0 -17
- data/spec/unit/evil/client/middleware/stringify_form_spec.rb +0 -63
- data/spec/unit/evil/client/middleware/stringify_json_spec.rb +0 -61
- data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +0 -59
- data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +0 -62
- data/spec/unit/evil/client/middleware/stringify_query_spec.rb +0 -40
- data/spec/unit/evil/client/middleware_spec.rb +0 -46
- data/spec/unit/evil/client/operation/request_spec.rb +0 -49
- data/spec/unit/evil/client/operation/response_spec.rb +0 -63
@@ -0,0 +1,26 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
#
|
3
|
+
# Resolves rack-compatible response from schema for given settings
|
4
|
+
# @private
|
5
|
+
#
|
6
|
+
class Resolver::Response < Resolver
|
7
|
+
private
|
8
|
+
|
9
|
+
def initialize(schema, settings, response)
|
10
|
+
@__response__ = Array response
|
11
|
+
super schema, settings, :responses, @__response__.first.to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
def __call__
|
15
|
+
super do
|
16
|
+
__check_status__
|
17
|
+
instance_exec(*@__response__, &__blocks__.last)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def __check_status__
|
22
|
+
return if __blocks__.any?
|
23
|
+
raise ResponseError.new(@__schema__, @__settings__, @__response__)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
#
|
3
|
+
# Resolves security definitions from operation settings and schema.
|
4
|
+
# Defines helpers for different methods of the authentication.
|
5
|
+
# @private
|
6
|
+
#
|
7
|
+
class Resolver::Security < Resolver
|
8
|
+
# DSL method to provide basic authentication schema
|
9
|
+
# by user name and password
|
10
|
+
#
|
11
|
+
# It provides base64-encoded "user:password" token and adds it
|
12
|
+
# to the "Authorization" header with a "Basic" prefix.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# operation do
|
16
|
+
# option :user
|
17
|
+
# option :password
|
18
|
+
#
|
19
|
+
# security { basic_auth user, password }
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @param [#to_s] user User name
|
23
|
+
# @param [#to_s] password Password
|
24
|
+
# @return [Hash<:headers, Hash<Symbol, String>>]
|
25
|
+
#
|
26
|
+
def basic_auth(user, password)
|
27
|
+
token = Base64.encode64("#{user}:#{password}").delete("\n")
|
28
|
+
token_auth(token, prefix: "Basic")
|
29
|
+
end
|
30
|
+
|
31
|
+
# DSL method to provide token-based authentication schema
|
32
|
+
#
|
33
|
+
# It places the token under either standard "Authorization" header,
|
34
|
+
# or standard "access_token" parameter of body or query.
|
35
|
+
# If you need custom key use [#key_auth] schema instead.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# operation do
|
39
|
+
# option :token
|
40
|
+
# security { token_auth token, prefix: "Bearer" }
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @param [#to_s] token User secret token
|
44
|
+
# @option [#to_s, nil] :prefix The standard prefix to be added before token
|
45
|
+
# @option [:headers, :query] :inside (:headers)
|
46
|
+
# The part of the request for the token
|
47
|
+
# @return [Hash<Symbol, Hash<Symbol, String>>]
|
48
|
+
#
|
49
|
+
def token_auth(token, inside: :headers, prefix: nil)
|
50
|
+
if inside == :headers
|
51
|
+
prefixed_token = [prefix&.to_s&.capitalize, token].compact.join(" ")
|
52
|
+
key_auth("Authorization", prefixed_token, inside: :headers)
|
53
|
+
else
|
54
|
+
key_auth("access_token", token, inside: inside)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# DSL method to provide the key-based authentication schema
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# operation do
|
62
|
+
# option :key
|
63
|
+
# security { key_auth "Authorize", key }
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @param [#to_s] key Name of the parameter
|
67
|
+
# @param [#to_s] value Value of the parameter
|
68
|
+
# @option [:headers, :query] :inside (:headers)
|
69
|
+
# The part of the request for the key-value pair
|
70
|
+
# @return [Hash<Symbol, Hash<Symbol, String>>]
|
71
|
+
#
|
72
|
+
def key_auth(key, value, inside: :headers)
|
73
|
+
{ inside => { key.to_s => value.to_s } }
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def initialize(schema, settings)
|
79
|
+
super schema, settings, :security
|
80
|
+
end
|
81
|
+
|
82
|
+
def __call__
|
83
|
+
super do
|
84
|
+
value = __blocks__.any? ? Hash(instance_exec(&__blocks__.last)) : {}
|
85
|
+
raise __wrong_format_error__(value) unless value.is_a? Hash
|
86
|
+
|
87
|
+
__symbolize_keys__(value).tap do |val|
|
88
|
+
__check_format__(val)
|
89
|
+
__check_values__(val)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def __check_format__(value)
|
95
|
+
data = value.keys - %i[headers query]
|
96
|
+
return if data.empty?
|
97
|
+
|
98
|
+
raise __definition_error__ "#{value.inspect} is not a hash"
|
99
|
+
end
|
100
|
+
|
101
|
+
def __check_values__(value)
|
102
|
+
data = value.reject { |_, val| val.is_a? Hash }
|
103
|
+
return if data.empty?
|
104
|
+
|
105
|
+
message = "inacceptable parts :#{data} of the request"
|
106
|
+
raise __definition_error__(message)
|
107
|
+
end
|
108
|
+
|
109
|
+
def __wrong_value_error__(data)
|
110
|
+
__definition_error__ "inacceptable security settings #{data}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
#
|
3
|
+
# Resolves request URI from operation settings and schema
|
4
|
+
# @private
|
5
|
+
#
|
6
|
+
class Resolver::Uri < Resolver
|
7
|
+
private
|
8
|
+
|
9
|
+
def initialize(schema, settings)
|
10
|
+
super schema, settings, :path
|
11
|
+
end
|
12
|
+
|
13
|
+
def __call__
|
14
|
+
super do
|
15
|
+
parts = __blocks__.map { |block| instance_exec(&block)&.to_s }
|
16
|
+
path = File.join(parts)
|
17
|
+
__uri__(path).tap { |uri| __check__(uri) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def __uri__(path)
|
22
|
+
URI path
|
23
|
+
rescue => error
|
24
|
+
raise __definition_error__(error.message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def __check__(uri)
|
28
|
+
scheme = uri.scheme
|
29
|
+
details = "base url should be defined" unless scheme
|
30
|
+
details ||= "base url should use HTTP(S). '#{scheme}' used instead"
|
31
|
+
|
32
|
+
raise __definition_error__(details) unless scheme&.match(/^https?$/)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
#
|
2
|
+
# Checks that an operation has been performed with given options
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# subject do
|
6
|
+
# MyClient.new(token: "foo").users(version: 3).fetch(token: "bar", id: 42)
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# # call only matcher
|
10
|
+
# expect { subject }.to perform_operation("MyClient.users.fetch")
|
11
|
+
#
|
12
|
+
# # exact matcher
|
13
|
+
# expect { subject }
|
14
|
+
# .to perform_operation("MyClient.users.fetch")
|
15
|
+
# .with_exactly(token: "bar", version: 3, id: 42)
|
16
|
+
#
|
17
|
+
# # partial matcher
|
18
|
+
# expect { subject }
|
19
|
+
# .to perform_operation("MyClient.users.fetch")
|
20
|
+
# .with(token: "bar", version: 3)
|
21
|
+
#
|
22
|
+
# # absence matcher
|
23
|
+
# expect { subject }
|
24
|
+
# .to perform_operation("MyClient.users.fetch")
|
25
|
+
# .without(:user, :password)
|
26
|
+
#
|
27
|
+
# # block syntax
|
28
|
+
# expect { subject }
|
29
|
+
# .to perform_operation("MyClient.users.fetch")
|
30
|
+
# .with { |token:, **| expect(token).to eq "bar" }
|
31
|
+
#
|
32
|
+
# @param [String] name The full name of the operation
|
33
|
+
#
|
34
|
+
RSpec::Matchers.define :perform_operation do |name|
|
35
|
+
supports_block_expectations
|
36
|
+
|
37
|
+
description { "perform operation #{name} " }
|
38
|
+
|
39
|
+
chain :with do |**options|
|
40
|
+
@some_options = options
|
41
|
+
end
|
42
|
+
|
43
|
+
chain :with_exactly do |**options|
|
44
|
+
@exact_options = options
|
45
|
+
end
|
46
|
+
|
47
|
+
chain :without do |*options|
|
48
|
+
@no_options = options.flatten.map(&:to_sym)
|
49
|
+
end
|
50
|
+
|
51
|
+
def full_signature(name)
|
52
|
+
name.dup.tap do |text|
|
53
|
+
text << " with options #{@exact_options}" if @exact_options
|
54
|
+
text << " with options including #{@some_options}" if @some_options
|
55
|
+
text << " without options :#{@no_options.join(', :')}" if @no_options
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
60
|
+
# rubocop: disable Style/InverseMethods
|
61
|
+
def expected_options?(options, check)
|
62
|
+
return if @exact_options && options != @exact_options
|
63
|
+
return if @some_options && !(options >= @some_options)
|
64
|
+
return if (options.keys & @no_options.to_a).any?
|
65
|
+
check.nil? || check.call(options)
|
66
|
+
end
|
67
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
68
|
+
# rubocop: enable Style/InverseMethods
|
69
|
+
|
70
|
+
def stub_resolver
|
71
|
+
resolver = Evil::Client::Resolver::Request
|
72
|
+
|
73
|
+
allow(resolver).to receive(:call) do |schema, settings|
|
74
|
+
register(schema, settings)
|
75
|
+
resolver.new(schema, settings).send(:__call__)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def actual_operations
|
80
|
+
@actual_operations ||= []
|
81
|
+
end
|
82
|
+
|
83
|
+
def register(schema, settings)
|
84
|
+
actual_operations << [schema.to_s, settings.options]
|
85
|
+
end
|
86
|
+
|
87
|
+
def performed(name, check)
|
88
|
+
@performed ||= actual_operations.find do |(key, options)|
|
89
|
+
(key == name) && expected_options?(options, check)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
match do |block|
|
94
|
+
stub_resolver
|
95
|
+
block.call
|
96
|
+
!performed(name, block_arg).nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
match_when_negated do |block|
|
100
|
+
stub_resolver
|
101
|
+
block.call
|
102
|
+
performed(name, block_arg).nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
def describe_expectations(name, perform)
|
106
|
+
"It was expected the operation #{full_signature(name)}" \
|
107
|
+
" #{'NOT ' unless perform}to be performed.\n" \
|
108
|
+
"The following operations has been actually performed:"
|
109
|
+
end
|
110
|
+
|
111
|
+
failure_message do
|
112
|
+
text = describe_expectations(name, true)
|
113
|
+
actual_operations.each.with_index(1) do |(key, opts), index|
|
114
|
+
text << format("\n %02d) #{key} with #{opts}", index)
|
115
|
+
end
|
116
|
+
text
|
117
|
+
end
|
118
|
+
|
119
|
+
failure_message_when_negated do
|
120
|
+
text = describe_expectations(name, false)
|
121
|
+
actual_operations.each.with_index(1) do |(key, opts), index|
|
122
|
+
marker = performed(name, block_arg) == [key, opts] ? "->" : " "
|
123
|
+
text << format("\n#{marker} % 2d) #{key} with #{opts}", index)
|
124
|
+
end
|
125
|
+
text
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
#
|
3
|
+
# @abstract
|
4
|
+
# Base class for mutable containers of client-specific definitions
|
5
|
+
# of nested scopes and operations along with a corresponding [#settings] class
|
6
|
+
# subclassing [Evil::Client::Settings]
|
7
|
+
#
|
8
|
+
# Every concrete container defines its only DSL for scope/operation
|
9
|
+
# definitions.
|
10
|
+
#
|
11
|
+
class Schema
|
12
|
+
# Loads concrete implementations of the abstract schema
|
13
|
+
require_relative "schema/operation"
|
14
|
+
require_relative "schema/scope"
|
15
|
+
|
16
|
+
# The name of current schema which is unique for the existing [#parent],
|
17
|
+
# or equals to client class name without any [#parent] (root scope name).
|
18
|
+
#
|
19
|
+
# @return [String]
|
20
|
+
#
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# Scope schema the operation belongs to
|
24
|
+
#
|
25
|
+
# Only the root schema has no parents.
|
26
|
+
# Its definitions are shared by all operations
|
27
|
+
#
|
28
|
+
# @return [Evil::Client::Schema::Scope, nil]
|
29
|
+
#
|
30
|
+
attr_reader :parent
|
31
|
+
|
32
|
+
# Back reference to client the schema belongs to
|
33
|
+
#
|
34
|
+
# @return [Evil::Client]
|
35
|
+
#
|
36
|
+
attr_reader :client # TODO: add spec
|
37
|
+
|
38
|
+
# The human-friendly representation of the schema
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# "MyClient.users.fetch" # custom operation's schema
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
#
|
45
|
+
def to_s
|
46
|
+
[parent, name].compact.join(".")
|
47
|
+
end
|
48
|
+
alias_method :to_str, :to_s
|
49
|
+
alias_method :inspect, :to_s
|
50
|
+
|
51
|
+
# The settings class inherited from the [#parent]'s one
|
52
|
+
#
|
53
|
+
# @return [Class]
|
54
|
+
#
|
55
|
+
def settings
|
56
|
+
@settings ||= Class.new(parent&.settings || Settings).tap do |klass|
|
57
|
+
klass.send(:instance_variable_set, :@schema, self)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds an option to the [#settings] class
|
62
|
+
#
|
63
|
+
# @param (see Evil::Client::Settings.option)
|
64
|
+
# @option (see Evil::Client::Settings.option)
|
65
|
+
# @return [self]
|
66
|
+
#
|
67
|
+
def option(key, type = nil, **opts)
|
68
|
+
settings.option(key, type, **opts)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds a memoized method to the [#settings] class
|
73
|
+
#
|
74
|
+
# @param (see Evil::Client::Settings.let)
|
75
|
+
# @return [self]
|
76
|
+
#
|
77
|
+
def let(key, &block)
|
78
|
+
settings.let(key, &block)
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Adds validator to the [#settings] class
|
83
|
+
#
|
84
|
+
# @param (see Evil::Client::Settings.validate)
|
85
|
+
# @return [self]
|
86
|
+
#
|
87
|
+
def validate(key, &block)
|
88
|
+
settings.validate(key, &block)
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def initialize(parent, name = nil)
|
95
|
+
if parent.is_a? self.class
|
96
|
+
@parent = parent
|
97
|
+
@client = parent&.client
|
98
|
+
@name = name
|
99
|
+
else
|
100
|
+
@client = parent
|
101
|
+
@name = parent.name
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
#
|
3
|
+
# Mutable container of operation definitions
|
4
|
+
# with DSL to configure both settings and parts of request/response.
|
5
|
+
#
|
6
|
+
class Schema::Operation < Schema
|
7
|
+
# Tells that this is a schema for end route (operation)
|
8
|
+
#
|
9
|
+
# @return [true]
|
10
|
+
#
|
11
|
+
def leaf?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
# Definitions for the current operation
|
16
|
+
#
|
17
|
+
# @return [Hash<Symbol, [Proc, Hash<Integer, Proc>]>]
|
18
|
+
#
|
19
|
+
def definitions
|
20
|
+
@definitions ||= { responses: {} }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Adds path definition to the schema
|
24
|
+
#
|
25
|
+
# Root path should be a valid URL for HTTP(S) protocol
|
26
|
+
#
|
27
|
+
# @param [#to_s, nil] value
|
28
|
+
# @param [Proc] block
|
29
|
+
# @return [self]
|
30
|
+
#
|
31
|
+
def path(value = nil, &block)
|
32
|
+
__define__(:path, value, block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds http method definition to the schema
|
36
|
+
#
|
37
|
+
# @see https://tools.ietf.org/html/rfc7231#section-4
|
38
|
+
# @see https://tools.ietf.org/html/rfc5789#section-2
|
39
|
+
#
|
40
|
+
# @param [#to_s, nil] value Acceptable http_method
|
41
|
+
# @param [Proc] block
|
42
|
+
# @return [self]
|
43
|
+
#
|
44
|
+
def http_method(value = nil, &block)
|
45
|
+
__define__(:http_method, value, block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adds format for request body
|
49
|
+
#
|
50
|
+
# @param [:json, :form, :text, :multipart, nil] value (:json)
|
51
|
+
# @param [Proc] block
|
52
|
+
# @return [self]
|
53
|
+
#
|
54
|
+
def format(value = nil, &block)
|
55
|
+
__define__(:format, value, block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds security definition to the schema
|
59
|
+
#
|
60
|
+
# The definition should be nested hash with a root keys :headers, or :query.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# security { { headers: { "idempotent-token" => "foobar" } } }
|
64
|
+
#
|
65
|
+
# Inside the block we provide several helpers for standard authentication
|
66
|
+
# schemas, namely `basic_auth`, `token_auth`, and `key_auth`. Those are
|
67
|
+
# preferred ways to define a security schema:
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# security { token_auth token, prefix: "Bearer" }
|
71
|
+
#
|
72
|
+
# @param [Hash<[:headers, :query], Hash>, nil]
|
73
|
+
# @param [Proc] block
|
74
|
+
# @return [self]
|
75
|
+
#
|
76
|
+
def security(value = nil, &block)
|
77
|
+
__define__(:security, value, block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Adds request headers definition to the schema
|
81
|
+
#
|
82
|
+
# Headers should be hash of header-value pairs.
|
83
|
+
# Values should be either stringified values or array of stringified values.
|
84
|
+
#
|
85
|
+
# Nested definition will be merged to the root one(s),
|
86
|
+
# so that you can add headers step-by-step from root of the client
|
87
|
+
# to its scopes and operations.
|
88
|
+
#
|
89
|
+
# To reset previous settings you can either set all headers to `nil`,
|
90
|
+
# or assign nil to custom headers. All headers with empty values
|
91
|
+
# will be ignored.
|
92
|
+
#
|
93
|
+
# @param [Hash<#to_s, [#to_s, Array<#to_s>]>, nil] value
|
94
|
+
# @param [Proc] block
|
95
|
+
# @return [self]
|
96
|
+
#
|
97
|
+
def headers(value = nil, &block)
|
98
|
+
__define__(:headers, value, block)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Adds query definition to the schema
|
102
|
+
#
|
103
|
+
# Query should be a nested hash.
|
104
|
+
# Wnen subscope or operation reloads previously defined query,
|
105
|
+
# new definition are merged deeply to older one. You can populate
|
106
|
+
# a query step-by-step from client root to an operation.
|
107
|
+
#
|
108
|
+
# @param [Hash, nil] value
|
109
|
+
# @param [Proc] block
|
110
|
+
# @return [self]
|
111
|
+
#
|
112
|
+
def query(value = nil, &block)
|
113
|
+
__define__(:query, value, block)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Adds body definition to the schema
|
117
|
+
#
|
118
|
+
# It is expected the body to correspond to [#format].
|
119
|
+
#
|
120
|
+
# When a format is :json, the body should be convertable to json
|
121
|
+
# When a format is :text, the body should be stringified
|
122
|
+
# When a format is :form, the body should be a hash
|
123
|
+
# When a format is :multipart, the body can be object or array of objects
|
124
|
+
#
|
125
|
+
# Unlike queries, previous body definitions aren't inherited.
|
126
|
+
# The body defined for root scope can be fully reloaded
|
127
|
+
# at subscope/operation level without any merging.
|
128
|
+
#
|
129
|
+
# @param [Object] value
|
130
|
+
# @param [Proc] block
|
131
|
+
# @return [self]
|
132
|
+
#
|
133
|
+
def body(value = nil, &block)
|
134
|
+
__define__(:body, value, block)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Adds list of middleware to the schema
|
138
|
+
#
|
139
|
+
# New middleware are added to previously defined (by root).
|
140
|
+
# This means the operation-specific middleware will handle the request
|
141
|
+
# after a root-specific one, and will handle the response before
|
142
|
+
# a roog-specific middleware.
|
143
|
+
#
|
144
|
+
# Values should be either a Rack middleware class, or array of
|
145
|
+
# Rack middleware classes.
|
146
|
+
#
|
147
|
+
# @param [Rack::Middleware, <Array<Rack::Middleware>>] value
|
148
|
+
# @param [Proc] block
|
149
|
+
# @return [self]
|
150
|
+
#
|
151
|
+
def middleware(value = nil, &block)
|
152
|
+
__define__(:middleware, value, block)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Adds response handler definition to the schema
|
156
|
+
#
|
157
|
+
# @param [Integer, Array<Integer>] codes List of response codes
|
158
|
+
# @param [Proc] block
|
159
|
+
# @return [self]
|
160
|
+
#
|
161
|
+
def response(*codes, &block)
|
162
|
+
codes.flatten.map(&:to_i).each do |code|
|
163
|
+
definitions[:responses][code] = block || proc { |*response| response }
|
164
|
+
end
|
165
|
+
self
|
166
|
+
end
|
167
|
+
alias_method :responses, :response
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# @private Method to add definitions to the schema
|
172
|
+
def __define__(key, value, block)
|
173
|
+
definitions[key] = block || proc { value }
|
174
|
+
self
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|