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
@@ -0,0 +1,37 @@
|
|
1
|
+
module Evil::Client::DSL
|
2
|
+
# Nested definition for attached files
|
3
|
+
class Files
|
4
|
+
# Builds a final upload schema from request options
|
5
|
+
#
|
6
|
+
# @param [Hash<Symbol, Object>] options
|
7
|
+
# @return [Hash<Symbol, Object>]
|
8
|
+
#
|
9
|
+
def call(**options)
|
10
|
+
@mutex.synchronize do
|
11
|
+
@schema = []
|
12
|
+
instance_exec(options, &@block)
|
13
|
+
@schema
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def initialize(&block)
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
# ==========================================================================
|
25
|
+
# Helper methods that mutate files @schema
|
26
|
+
# ==========================================================================
|
27
|
+
|
28
|
+
def add(data, type: "text/plain", charset: "utf-8", filename: nil, **)
|
29
|
+
@schema << {
|
30
|
+
file: data.respond_to?(:read) ? data : StringIO.new(data),
|
31
|
+
type: MIME::Types[type].first,
|
32
|
+
charset: charset,
|
33
|
+
filename: filename
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Evil::Client::DSL
|
2
|
+
require_relative "security"
|
3
|
+
require_relative "files"
|
4
|
+
|
5
|
+
# Builds a schema for single operation
|
6
|
+
class Operation
|
7
|
+
attr_reader :schema
|
8
|
+
|
9
|
+
# Builds a schema for a single operation
|
10
|
+
#
|
11
|
+
# @param [Object] settings
|
12
|
+
# @param [Proc] block A block of definitions (should accept settings)
|
13
|
+
# @return [Hash<Symbol, Object>]
|
14
|
+
#
|
15
|
+
def finalize(settings)
|
16
|
+
@mutex.synchronize do
|
17
|
+
@schema = @default.dup
|
18
|
+
instance_exec(settings, &@block) if @block
|
19
|
+
@schema[:middleware]&.finalize(settings)
|
20
|
+
@schema
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def initialize(key, block)
|
27
|
+
@mutex = Mutex.new
|
28
|
+
@block = block
|
29
|
+
@default = { key: key, responses: {} }
|
30
|
+
end
|
31
|
+
|
32
|
+
# ==========================================================================
|
33
|
+
# Helper methods that mutate a @schema
|
34
|
+
# ==========================================================================
|
35
|
+
|
36
|
+
def documentation(value)
|
37
|
+
@schema[:doc] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def http_method(value)
|
41
|
+
@schema[:method] = value.to_s.downcase
|
42
|
+
end
|
43
|
+
|
44
|
+
def path
|
45
|
+
@schema[:path] = ->(**opts) { yield(opts).gsub(%r{\A/+|/+\z}, "") }
|
46
|
+
end
|
47
|
+
|
48
|
+
def security(&block)
|
49
|
+
@schema[:security] = Security.new(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def files(&block)
|
53
|
+
@schema[:files] = Files.new(&block)
|
54
|
+
@schema[:format] = "multipart"
|
55
|
+
@schema.delete :body
|
56
|
+
end
|
57
|
+
|
58
|
+
def body(format: "json", **options, &block)
|
59
|
+
@schema[:body] = __model__(options, &block)
|
60
|
+
@schema[:format] = __valid_format__(format)
|
61
|
+
@schema.delete :files
|
62
|
+
end
|
63
|
+
|
64
|
+
def headers(**options, &block)
|
65
|
+
@schema[:headers] = __model__(options, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def query(**options, &block)
|
69
|
+
@schema[:query] = __model__(options, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def response(*statuses, raise: false, &block)
|
73
|
+
statuses.each do |status|
|
74
|
+
@schema[:responses][status] = {
|
75
|
+
raise: raise,
|
76
|
+
coercer: block || proc { |response:, **| response }
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# ==========================================================================
|
82
|
+
# Utilities for helpers TODO: extract to a separate module
|
83
|
+
# ==========================================================================
|
84
|
+
|
85
|
+
def __valid_format__(format)
|
86
|
+
formats = %w(json form)
|
87
|
+
return format.to_s if formats.include? format.to_s
|
88
|
+
fail ArgumentError.new "Invalid format #{format} for body." \
|
89
|
+
" Use one of formats: #{formats}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def __model__(model: nil, **, &block)
|
93
|
+
if model && block
|
94
|
+
Class.new(model, &block)
|
95
|
+
elsif block
|
96
|
+
Class.new(Evil::Client::Model, &block)
|
97
|
+
elsif model
|
98
|
+
model
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Evil::Client::DSL
|
2
|
+
require_relative "operation"
|
3
|
+
|
4
|
+
# Container for operations definitions
|
5
|
+
# Applies settings to definitions and returns a final schema
|
6
|
+
class Operations
|
7
|
+
# Adds block definition as a named operation
|
8
|
+
#
|
9
|
+
# @param [#to_sym] key
|
10
|
+
# @param [Proc] block
|
11
|
+
# @return [self]
|
12
|
+
#
|
13
|
+
def register(key, &block)
|
14
|
+
@schema[key] = Operation.new(key, block)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Applies settings to all definitions and returns a final schema
|
19
|
+
#
|
20
|
+
# @param [Object] settings
|
21
|
+
# @return [Hash<Symbol, Object>]
|
22
|
+
#
|
23
|
+
def finalize(settings)
|
24
|
+
default = @schema[nil].finalize(settings)
|
25
|
+
custom = @schema.select { |key| key }
|
26
|
+
|
27
|
+
custom.each_with_object({}) do |(key, operation), hash|
|
28
|
+
custom = operation.finalize(settings)
|
29
|
+
hash[key] = default.merge(custom)
|
30
|
+
hash[key][:format] ||= "json"
|
31
|
+
hash[key][:responses] = default[:responses].merge(custom[:responses])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@schema = { nil => Operation.new(nil, nil) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Evil::Client::DSL
|
2
|
+
# Provides a namespace for client's top-level DSL
|
3
|
+
class Scope
|
4
|
+
extend Dry::Initializer::Mixin
|
5
|
+
option :__scope__, default: proc {}
|
6
|
+
|
7
|
+
# Declares a method that opens new scope inside the current one
|
8
|
+
# An instance of new scope has access to methods of its parent
|
9
|
+
#
|
10
|
+
# @param [#to_sym] name (:[]) The name of the new scope
|
11
|
+
# @return [self]
|
12
|
+
#
|
13
|
+
def self.scope(name = :[], &block)
|
14
|
+
klass = Class.new(Scope, &block)
|
15
|
+
define_method(name) do |*args, **options|
|
16
|
+
klass.new(*args, __scope__: self, **options)
|
17
|
+
end
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
private :__scope__
|
24
|
+
|
25
|
+
def respond_to_missing?(name, *)
|
26
|
+
__scope__.respond_to? name
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(name, *args)
|
30
|
+
super unless respond_to? name
|
31
|
+
__scope__.send(name, *args)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Evil::Client::DSL
|
2
|
+
# Nested definition for a security schemas
|
3
|
+
class Security
|
4
|
+
# Builds final security schema dependent on request options
|
5
|
+
#
|
6
|
+
# @param [Hash<Symbol, Object>] options
|
7
|
+
# @return [Hash<Symbol, Object>]
|
8
|
+
#
|
9
|
+
def call(**options)
|
10
|
+
@mutex.synchronize do
|
11
|
+
@schema = {}
|
12
|
+
instance_exec(options, &@block)
|
13
|
+
@schema
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def initialize(&block)
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
# ==========================================================================
|
25
|
+
# Helper methods that mutate a security @schema
|
26
|
+
# ==========================================================================
|
27
|
+
|
28
|
+
# @see [https://tools.ietf.org/html/rfc7617]
|
29
|
+
def basic_auth(user, password)
|
30
|
+
token = Base64.encode64("#{user}:#{password}").delete("\n")
|
31
|
+
token_auth(token, prefix: "Basic")
|
32
|
+
end
|
33
|
+
|
34
|
+
def token_auth(token, using: :headers, prefix: nil)
|
35
|
+
if using == :headers
|
36
|
+
prefixed_token = [prefix&.to_s&.capitalize, token].compact.join(" ")
|
37
|
+
key_auth("authorization", prefixed_token, using: :headers)
|
38
|
+
else
|
39
|
+
key_auth("access_token", token, using: using)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def key_auth(key, value, using: :headers)
|
44
|
+
__validate__ using
|
45
|
+
@schema[using] ||= {}
|
46
|
+
@schema[using][key.to_s] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
# ==========================================================================
|
50
|
+
|
51
|
+
def __validate__(part)
|
52
|
+
parts = %i(body query headers)
|
53
|
+
return if parts.include? part
|
54
|
+
fail ArgumentError.new("Wrong part '#{part}'. Use one of parts: #{parts}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Evil::Client
|
2
|
+
# Builds and carries stack of middleware parameterized by settings
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# # during client definition
|
6
|
+
# middleware = Evil::Client::Middleware.new do |settings|
|
7
|
+
# run CustomMiddleware if settings.version > 1
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# # during client instantiation
|
11
|
+
# stack = middleware.finalize(settings)
|
12
|
+
# conn = stack.wrap(connection)
|
13
|
+
#
|
14
|
+
# # during runtime to make a request
|
15
|
+
# conn.call request
|
16
|
+
#
|
17
|
+
class Middleware
|
18
|
+
class << self
|
19
|
+
require_relative "middleware/base"
|
20
|
+
require_relative "middleware/merge_security"
|
21
|
+
require_relative "middleware/normalize_headers"
|
22
|
+
require_relative "middleware/stringify_json"
|
23
|
+
require_relative "middleware/stringify_multipart"
|
24
|
+
require_relative "middleware/stringify_query"
|
25
|
+
require_relative "middleware/stringify_form"
|
26
|
+
|
27
|
+
# Middleware to be added on top of full stack (before custom ones)
|
28
|
+
def prepend
|
29
|
+
new do
|
30
|
+
run NormalizeHeaders
|
31
|
+
run MergeSecurity
|
32
|
+
end.finalize
|
33
|
+
end
|
34
|
+
|
35
|
+
# Middleware to be added on bottom of full stack
|
36
|
+
# (between custom stack and connection)
|
37
|
+
def append
|
38
|
+
new do
|
39
|
+
run StringifyQuery
|
40
|
+
run StringifyJson
|
41
|
+
run StringifyForm
|
42
|
+
run StringifyMultipart
|
43
|
+
end.finalize
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Applies client settings to build stack of middleware
|
48
|
+
#
|
49
|
+
# @param [Object] settings
|
50
|
+
# @return [self]
|
51
|
+
#
|
52
|
+
def finalize(settings = nil)
|
53
|
+
@mutex.synchronize do
|
54
|
+
@stack = []
|
55
|
+
instance_exec(settings, &@block) if @block
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Wraps the connection instance to the current stack of middleware
|
61
|
+
#
|
62
|
+
# @param [#call] connection
|
63
|
+
# @return [#call]
|
64
|
+
#
|
65
|
+
def call(other)
|
66
|
+
@stack.reverse.inject(other) { |a, e| e.new(a) }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def initialize(&block)
|
72
|
+
@mutex = Mutex.new
|
73
|
+
@block = block
|
74
|
+
end
|
75
|
+
|
76
|
+
def run(klass)
|
77
|
+
@stack << klass
|
78
|
+
self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Evil::Client::Middleware
|
2
|
+
class MergeSecurity < Base
|
3
|
+
private
|
4
|
+
|
5
|
+
def build(env)
|
6
|
+
env.dup.tap do |hash|
|
7
|
+
security = hash.delete(:security).to_h
|
8
|
+
%i(headers body query).each do |key|
|
9
|
+
next unless security[key]
|
10
|
+
hash[key] ||= {}
|
11
|
+
hash[key].update security[key]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Evil::Client::Middleware
|
2
|
+
class NormalizeHeaders < Base
|
3
|
+
private
|
4
|
+
|
5
|
+
def build(env)
|
6
|
+
headers = Hash(env[:headers]).each_with_object({}) do |(key, val), hash|
|
7
|
+
hash[key.to_s.downcase] = val.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
env.merge headers: headers
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Evil::Client::Middleware
|
2
|
+
class StringifyForm < Base
|
3
|
+
private
|
4
|
+
|
5
|
+
def build(env)
|
6
|
+
return env unless env[:format] == "form"
|
7
|
+
return env if env&.fetch(:body, nil).to_h.empty?
|
8
|
+
|
9
|
+
env.dup.tap do |hash|
|
10
|
+
hash[:headers] ||= {}
|
11
|
+
hash[:headers]["content-type"] = "application/x-www-form-urlencoded"
|
12
|
+
hash[:body_string] = env[:body]
|
13
|
+
.flat_map { |key, val| normalize(val, key) }
|
14
|
+
.flat_map { |item| stringify(item) }
|
15
|
+
.join("&")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def stringify(hash)
|
20
|
+
hash.map do |keys, val|
|
21
|
+
"#{keys.first}#{keys[1..-1].map { |key| "[#{key}]" }.join}=#{val}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def normalize(value, *keys)
|
26
|
+
case value
|
27
|
+
when Hash then
|
28
|
+
value.flat_map { |key, val| normalize(val, *keys, key) }
|
29
|
+
when Array then
|
30
|
+
value.flat_map { |val| normalize(val, *keys, nil) }
|
31
|
+
else
|
32
|
+
[{ keys.map { |key| CGI.escape(key.to_s) } => CGI.escape(value.to_s) }]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|