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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +7 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +98 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +144 -0
  10. data/Rakefile +6 -0
  11. data/docs/base_url.md +38 -0
  12. data/docs/documentation.md +9 -0
  13. data/docs/headers.md +59 -0
  14. data/docs/http_method.md +31 -0
  15. data/docs/index.md +127 -0
  16. data/docs/license.md +19 -0
  17. data/docs/model.md +173 -0
  18. data/docs/operation.md +0 -0
  19. data/docs/overview.md +0 -0
  20. data/docs/path.md +48 -0
  21. data/docs/query.md +99 -0
  22. data/docs/responses.md +66 -0
  23. data/docs/security.md +102 -0
  24. data/docs/settings.md +32 -0
  25. data/evil-client.gemspec +25 -0
  26. data/lib/evil/client.rb +97 -0
  27. data/lib/evil/client/connection.rb +35 -0
  28. data/lib/evil/client/connection/net_http.rb +57 -0
  29. data/lib/evil/client/dsl.rb +110 -0
  30. data/lib/evil/client/dsl/files.rb +37 -0
  31. data/lib/evil/client/dsl/operation.rb +102 -0
  32. data/lib/evil/client/dsl/operations.rb +41 -0
  33. data/lib/evil/client/dsl/scope.rb +34 -0
  34. data/lib/evil/client/dsl/security.rb +57 -0
  35. data/lib/evil/client/middleware.rb +81 -0
  36. data/lib/evil/client/middleware/base.rb +15 -0
  37. data/lib/evil/client/middleware/merge_security.rb +16 -0
  38. data/lib/evil/client/middleware/normalize_headers.rb +13 -0
  39. data/lib/evil/client/middleware/stringify_form.rb +36 -0
  40. data/lib/evil/client/middleware/stringify_json.rb +15 -0
  41. data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
  42. data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
  43. data/lib/evil/client/middleware/stringify_query.rb +31 -0
  44. data/lib/evil/client/model.rb +65 -0
  45. data/lib/evil/client/operation.rb +34 -0
  46. data/lib/evil/client/operation/request.rb +42 -0
  47. data/lib/evil/client/operation/response.rb +40 -0
  48. data/lib/evil/client/operation/response_error.rb +12 -0
  49. data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
  50. data/mkdocs.yml +21 -0
  51. data/spec/features/instantiation_spec.rb +68 -0
  52. data/spec/features/middleware_spec.rb +75 -0
  53. data/spec/features/operation_with_documentation_spec.rb +41 -0
  54. data/spec/features/operation_with_files_spec.rb +40 -0
  55. data/spec/features/operation_with_form_body_spec.rb +158 -0
  56. data/spec/features/operation_with_headers_spec.rb +99 -0
  57. data/spec/features/operation_with_http_method_spec.rb +45 -0
  58. data/spec/features/operation_with_json_body_spec.rb +156 -0
  59. data/spec/features/operation_with_path_spec.rb +47 -0
  60. data/spec/features/operation_with_query_spec.rb +84 -0
  61. data/spec/features/operation_with_response_spec.rb +109 -0
  62. data/spec/features/operation_with_security_spec.rb +228 -0
  63. data/spec/features/scoping_spec.rb +48 -0
  64. data/spec/spec_helper.rb +23 -0
  65. data/spec/support/test_client.rb +15 -0
  66. data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
  67. data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
  68. data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
  69. data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
  70. data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
  71. data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
  72. data/spec/unit/evil/client/dsl_spec.rb +57 -0
  73. data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
  74. data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
  75. data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
  76. data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
  77. data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
  78. data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
  79. data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
  80. data/spec/unit/evil/client/middleware_spec.rb +46 -0
  81. data/spec/unit/evil/client/model_spec.rb +100 -0
  82. data/spec/unit/evil/client/operation/request_spec.rb +49 -0
  83. data/spec/unit/evil/client/operation/response_spec.rb +61 -0
  84. 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,15 @@
1
+ class Evil::Client::Middleware::Base
2
+ def call(env)
3
+ @app.call build(env)
4
+ end
5
+
6
+ private
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def build(env)
13
+ env
14
+ end
15
+ 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