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
@@ -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
|