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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +0 -11
  3. data/.gitignore +1 -0
  4. data/.rspec +0 -1
  5. data/.rubocop.yml +22 -19
  6. data/.travis.yml +1 -0
  7. data/CHANGELOG.md +251 -6
  8. data/LICENSE.txt +3 -1
  9. data/README.md +47 -81
  10. data/docs/helpers/body.md +93 -0
  11. data/docs/helpers/connection.md +19 -0
  12. data/docs/helpers/headers.md +72 -0
  13. data/docs/helpers/http_method.md +39 -0
  14. data/docs/helpers/let.md +14 -0
  15. data/docs/helpers/logger.md +24 -0
  16. data/docs/helpers/middleware.md +56 -0
  17. data/docs/helpers/operation.md +103 -0
  18. data/docs/helpers/option.md +50 -0
  19. data/docs/helpers/path.md +37 -0
  20. data/docs/helpers/query.md +59 -0
  21. data/docs/helpers/response.md +40 -0
  22. data/docs/helpers/scope.md +121 -0
  23. data/docs/helpers/security.md +102 -0
  24. data/docs/helpers/validate.md +68 -0
  25. data/docs/index.md +70 -78
  26. data/docs/license.md +5 -1
  27. data/docs/rspec.md +96 -0
  28. data/evil-client.gemspec +10 -8
  29. data/lib/evil/client.rb +126 -72
  30. data/lib/evil/client/builder.rb +47 -0
  31. data/lib/evil/client/builder/operation.rb +40 -0
  32. data/lib/evil/client/builder/scope.rb +31 -0
  33. data/lib/evil/client/chaining.rb +17 -0
  34. data/lib/evil/client/connection.rb +60 -20
  35. data/lib/evil/client/container.rb +66 -0
  36. data/lib/evil/client/container/operation.rb +23 -0
  37. data/lib/evil/client/container/scope.rb +28 -0
  38. data/lib/evil/client/exceptions/definition_error.rb +15 -0
  39. data/lib/evil/client/exceptions/name_error.rb +32 -0
  40. data/lib/evil/client/exceptions/response_error.rb +42 -0
  41. data/lib/evil/client/exceptions/type_error.rb +29 -0
  42. data/lib/evil/client/exceptions/validation_error.rb +27 -0
  43. data/lib/evil/client/formatter.rb +49 -0
  44. data/lib/evil/client/formatter/form.rb +45 -0
  45. data/lib/evil/client/formatter/multipart.rb +33 -0
  46. data/lib/evil/client/formatter/part.rb +66 -0
  47. data/lib/evil/client/formatter/text.rb +21 -0
  48. data/lib/evil/client/resolver.rb +84 -0
  49. data/lib/evil/client/resolver/body.rb +22 -0
  50. data/lib/evil/client/resolver/format.rb +30 -0
  51. data/lib/evil/client/resolver/headers.rb +46 -0
  52. data/lib/evil/client/resolver/http_method.rb +34 -0
  53. data/lib/evil/client/resolver/middleware.rb +36 -0
  54. data/lib/evil/client/resolver/query.rb +39 -0
  55. data/lib/evil/client/resolver/request.rb +96 -0
  56. data/lib/evil/client/resolver/response.rb +26 -0
  57. data/lib/evil/client/resolver/security.rb +113 -0
  58. data/lib/evil/client/resolver/uri.rb +35 -0
  59. data/lib/evil/client/rspec.rb +127 -0
  60. data/lib/evil/client/schema.rb +105 -0
  61. data/lib/evil/client/schema/operation.rb +177 -0
  62. data/lib/evil/client/schema/scope.rb +73 -0
  63. data/lib/evil/client/settings.rb +172 -0
  64. data/lib/evil/client/settings/validator.rb +64 -0
  65. data/mkdocs.yml +21 -15
  66. data/spec/features/custom_connection_spec.rb +17 -0
  67. data/spec/features/operation/middleware_spec.rb +50 -0
  68. data/spec/features/operation/options_spec.rb +71 -0
  69. data/spec/features/operation/request_spec.rb +94 -0
  70. data/spec/features/operation/response_spec.rb +48 -0
  71. data/spec/features/scope/options_spec.rb +52 -0
  72. data/spec/fixtures/locales/en.yml +16 -0
  73. data/spec/fixtures/test_client.rb +76 -0
  74. data/spec/spec_helper.rb +18 -6
  75. data/spec/support/fixtures_helper.rb +7 -0
  76. data/spec/unit/builder/operation_spec.rb +90 -0
  77. data/spec/unit/builder/scope_spec.rb +84 -0
  78. data/spec/unit/client_spec.rb +137 -0
  79. data/spec/unit/connection_spec.rb +78 -0
  80. data/spec/unit/container/operation_spec.rb +81 -0
  81. data/spec/unit/container/scope_spec.rb +61 -0
  82. data/spec/unit/container_spec.rb +107 -0
  83. data/spec/unit/exceptions/definition_error_spec.rb +15 -0
  84. data/spec/unit/exceptions/name_error_spec.rb +77 -0
  85. data/spec/unit/exceptions/response_error_spec.rb +22 -0
  86. data/spec/unit/exceptions/type_error_spec.rb +71 -0
  87. data/spec/unit/exceptions/validation_error_spec.rb +13 -0
  88. data/spec/unit/formatter/form_spec.rb +27 -0
  89. data/spec/unit/formatter/multipart_spec.rb +23 -0
  90. data/spec/unit/formatter/part_spec.rb +49 -0
  91. data/spec/unit/formatter/text_spec.rb +37 -0
  92. data/spec/unit/formatter_spec.rb +46 -0
  93. data/spec/unit/resolver/body_spec.rb +65 -0
  94. data/spec/unit/resolver/format_spec.rb +66 -0
  95. data/spec/unit/resolver/headers_spec.rb +93 -0
  96. data/spec/unit/resolver/http_method_spec.rb +67 -0
  97. data/spec/unit/resolver/middleware_spec.rb +83 -0
  98. data/spec/unit/resolver/query_spec.rb +85 -0
  99. data/spec/unit/resolver/request_spec.rb +121 -0
  100. data/spec/unit/resolver/response_spec.rb +64 -0
  101. data/spec/unit/resolver/security_spec.rb +156 -0
  102. data/spec/unit/resolver/uri_spec.rb +117 -0
  103. data/spec/unit/rspec_spec.rb +342 -0
  104. data/spec/unit/schema/operation_spec.rb +309 -0
  105. data/spec/unit/schema/scope_spec.rb +110 -0
  106. data/spec/unit/schema_spec.rb +157 -0
  107. data/spec/unit/settings/validator_spec.rb +128 -0
  108. data/spec/unit/settings_spec.rb +248 -0
  109. metadata +192 -135
  110. data/docs/base_url.md +0 -38
  111. data/docs/documentation.md +0 -9
  112. data/docs/headers.md +0 -59
  113. data/docs/http_method.md +0 -31
  114. data/docs/model.md +0 -173
  115. data/docs/operation.md +0 -0
  116. data/docs/overview.md +0 -0
  117. data/docs/path.md +0 -48
  118. data/docs/query.md +0 -99
  119. data/docs/responses.md +0 -66
  120. data/docs/security.md +0 -102
  121. data/docs/settings.md +0 -32
  122. data/lib/evil/client/connection/net_http.rb +0 -57
  123. data/lib/evil/client/dsl.rb +0 -127
  124. data/lib/evil/client/dsl/base.rb +0 -26
  125. data/lib/evil/client/dsl/files.rb +0 -37
  126. data/lib/evil/client/dsl/headers.rb +0 -16
  127. data/lib/evil/client/dsl/http_method.rb +0 -24
  128. data/lib/evil/client/dsl/operation.rb +0 -91
  129. data/lib/evil/client/dsl/operations.rb +0 -41
  130. data/lib/evil/client/dsl/path.rb +0 -25
  131. data/lib/evil/client/dsl/query.rb +0 -16
  132. data/lib/evil/client/dsl/response.rb +0 -61
  133. data/lib/evil/client/dsl/responses.rb +0 -29
  134. data/lib/evil/client/dsl/scope.rb +0 -27
  135. data/lib/evil/client/dsl/security.rb +0 -57
  136. data/lib/evil/client/dsl/verifier.rb +0 -35
  137. data/lib/evil/client/middleware.rb +0 -81
  138. data/lib/evil/client/middleware/base.rb +0 -11
  139. data/lib/evil/client/middleware/merge_security.rb +0 -20
  140. data/lib/evil/client/middleware/normalize_headers.rb +0 -17
  141. data/lib/evil/client/middleware/stringify_form.rb +0 -40
  142. data/lib/evil/client/middleware/stringify_json.rb +0 -19
  143. data/lib/evil/client/middleware/stringify_multipart.rb +0 -36
  144. data/lib/evil/client/middleware/stringify_multipart/part.rb +0 -36
  145. data/lib/evil/client/middleware/stringify_query.rb +0 -35
  146. data/lib/evil/client/operation.rb +0 -34
  147. data/lib/evil/client/operation/request.rb +0 -26
  148. data/lib/evil/client/operation/response.rb +0 -39
  149. data/lib/evil/client/operation/response_error.rb +0 -13
  150. data/lib/evil/client/operation/unexpected_response_error.rb +0 -19
  151. data/spec/features/instantiation_spec.rb +0 -68
  152. data/spec/features/middleware_spec.rb +0 -79
  153. data/spec/features/operation_with_documentation_spec.rb +0 -41
  154. data/spec/features/operation_with_files_spec.rb +0 -40
  155. data/spec/features/operation_with_form_body_spec.rb +0 -158
  156. data/spec/features/operation_with_headers_spec.rb +0 -99
  157. data/spec/features/operation_with_http_method_spec.rb +0 -45
  158. data/spec/features/operation_with_json_body_spec.rb +0 -156
  159. data/spec/features/operation_with_nested_responses_spec.rb +0 -95
  160. data/spec/features/operation_with_path_spec.rb +0 -47
  161. data/spec/features/operation_with_query_spec.rb +0 -84
  162. data/spec/features/operation_with_security_spec.rb +0 -228
  163. data/spec/features/scoping_spec.rb +0 -48
  164. data/spec/support/test_client.rb +0 -15
  165. data/spec/unit/evil/client/connection/net_http_spec.rb +0 -38
  166. data/spec/unit/evil/client/dsl/files_spec.rb +0 -37
  167. data/spec/unit/evil/client/dsl/operation_spec.rb +0 -374
  168. data/spec/unit/evil/client/dsl/operations_spec.rb +0 -29
  169. data/spec/unit/evil/client/dsl/scope_spec.rb +0 -32
  170. data/spec/unit/evil/client/dsl/security_spec.rb +0 -135
  171. data/spec/unit/evil/client/middleware/merge_security_spec.rb +0 -32
  172. data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +0 -17
  173. data/spec/unit/evil/client/middleware/stringify_form_spec.rb +0 -63
  174. data/spec/unit/evil/client/middleware/stringify_json_spec.rb +0 -61
  175. data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +0 -59
  176. data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +0 -62
  177. data/spec/unit/evil/client/middleware/stringify_query_spec.rb +0 -40
  178. data/spec/unit/evil/client/middleware_spec.rb +0 -46
  179. data/spec/unit/evil/client/operation/request_spec.rb +0 -49
  180. data/spec/unit/evil/client/operation/response_spec.rb +0 -63
@@ -0,0 +1,17 @@
1
+ class Evil::Client
2
+ #
3
+ # Support chaining of calls for nested scopes/operations
4
+ #
5
+ module Chaining
6
+ private
7
+
8
+ def respond_to_missing?(name, *)
9
+ operations[name] || scopes[name]
10
+ end
11
+
12
+ def method_missing(name, *args, &block)
13
+ return super unless respond_to_missing? name
14
+ (operations[name] || scopes[name]).call(*args)
15
+ end
16
+ end
17
+ end
@@ -1,31 +1,71 @@
1
1
  class Evil::Client
2
- # @abstract Base class for a specific connection to remote uri
3
- class Connection
4
- REGISTRY = { net_http: "NetHTTP" }.freeze
2
+ #
3
+ # Object that sends rack-compatible request environment to remote API,
4
+ # and wraps a response into rack-compatible array of [status, headers, body].
5
+ #
6
+ # @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC
7
+ #
8
+ module Connection
9
+ extend self
5
10
 
6
- extend Dry::Initializer::Mixin
7
- param :base_uri
8
-
9
- # Envokes a specific connection class
11
+ # Makes the request by taking rack env and returning rack response
10
12
  #
11
- # @param [#to_sym] key
12
- # @return [Class]
13
+ # @param [Hash<String, Object>] env Rack environment
14
+ # @return [Array] Rack-compatible response
13
15
  #
14
- def self.[](name = nil)
15
- keys = REGISTRY.keys
16
- key = (name || keys.first).to_sym
17
- klass = REGISTRY.fetch(key) do
18
- raise ArgumentError.new "Connection '#{key}' is not registered." \
19
- " Use the following keys: #{keys}"
16
+ def call(env)
17
+ request = Rack::Request.new(env)
18
+ with_logger_for request do
19
+ open_http_connection_for request do |http|
20
+ res = http.request build_from(request)
21
+ [res.code.to_i, Hash(res.header), Array(res.body)]
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def open_http_connection_for(req)
29
+ Net::HTTP.start req.host, req.port, use_ssl: req.ssl? do |http|
30
+ yield(http)
31
+ end
32
+ end
33
+
34
+ def build_from(request)
35
+ uri = URI request.url
36
+ body = request.body
37
+ type = request.env["REQUEST_METHOD"].capitalize
38
+ headers = request.env["HTTP_Variables"]
39
+
40
+ Net::HTTP.const_get(type).new(uri).tap do |req|
41
+ req.body = body
42
+ headers.each { |key, val| req[key] = val }
20
43
  end
44
+ end
21
45
 
22
- require_relative "connection/#{key}"
23
- const_get klass
46
+ def with_logger_for(request)
47
+ logger = request.logger
48
+ log_request(logger, request)
49
+ yield.tap { |response| log_response(logger, response) }
24
50
  end
25
51
 
26
- # @abstract Sends request to the server and returns rack-compatible response
27
- def call(_env, *)
28
- raise NotImplementedError
52
+ def log_request(logger, request)
53
+ return unless logger
54
+
55
+ logger.info(self) { "sending request:" }
56
+ logger.info(self) { " Url | #{request.url}" }
57
+ logger.info(self) { " Headers | #{request.env['HTTP_Variables']}" }
58
+ logger.info(self) { " Body | #{request.body}" }
59
+ end
60
+
61
+ def log_response(logger, response)
62
+ return unless logger
63
+
64
+ status, headers, body = Array response
65
+ logger.info(self) { "receiving response:" }
66
+ logger.info(self) { " Status | #{status}" }
67
+ logger.info(self) { " Headers | #{headers}" }
68
+ logger.info(self) { " Body | #{body}" }
29
69
  end
30
70
  end
31
71
  end
@@ -0,0 +1,66 @@
1
+ class Evil::Client
2
+ #
3
+ # @abstract
4
+ # Container that carries schema of operation/scope along with its settings
5
+ # and methods to build sub-scope/operation or perform the current operation.
6
+ #
7
+ class Container
8
+ # Loads concrete implementations of the abstract container
9
+ require_relative "container/scope"
10
+ require_relative "container/operation"
11
+
12
+ # The schema containing info about sub-scopes and operations of the scope
13
+ # @return [Evil::Client::Container::ScopeDefinition]
14
+ attr_reader :schema
15
+
16
+ # The settings current scope is initialized with
17
+ # @return [Evil::Client::Settings]
18
+ attr_reader :settings
19
+
20
+ # Options assigned to the [#settings]
21
+ #
22
+ # These are opts given to the [#initializer],
23
+ # processed (via defaults, coercion, renaming) by a constructor of settings.
24
+ #
25
+ # @return [Hash<Symbol, Object>]
26
+ def options
27
+ @options ||= settings.options
28
+ end
29
+
30
+ # The human-friendly representation of the scope instance
31
+ #
32
+ # @example
33
+ # '#<MyClient.scopes[:users] @version=1>'
34
+ #
35
+ # @return [String]
36
+ def to_s
37
+ "#<#{schema} #{options.map { |key, val| "@#{key}=#{val}" }.join(', ')}>"
38
+ end
39
+ alias_method :to_str, :to_s
40
+ alias_method :inspect, :to_s
41
+
42
+ # (Re)sets current logger
43
+ #
44
+ # @param [Logger, nil] logger
45
+ # @return [Logger, nil]
46
+ #
47
+ def logger=(logger)
48
+ settings.logger = logger
49
+ end
50
+
51
+ # Current logger
52
+ #
53
+ # @return [Logger, nil]
54
+ #
55
+ def logger
56
+ settings.logger
57
+ end
58
+
59
+ private
60
+
61
+ def initialize(schema, logger = nil, **opts)
62
+ @schema = schema
63
+ @settings = schema.settings.new(logger, opts)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ class Evil::Client
2
+ #
3
+ # Contains operation schema and settings along with DSL method [#call]
4
+ # to sends a request to API and handle the response.
5
+ #
6
+ class Container::Operation < Container
7
+ # Executes the operation and returns rack-compatible response
8
+ #
9
+ # @return [Array]
10
+ #
11
+ # rubocop: disable Metrics/AbcSize
12
+ def call
13
+ request = Resolver::Request.call(schema, settings)
14
+ middleware = Resolver::Middleware.call(schema, settings)
15
+ connection = schema.client.connection
16
+ stack = middleware.inject(connection) { |app, layer| layer.new app }
17
+ response = stack.call request
18
+
19
+ Resolver::Response.call schema, settings, response
20
+ end
21
+ # rubocop: enable Metrics/AbcSize
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ class Evil::Client
2
+ #
3
+ # Contains schema and settings of some scope along with methods
4
+ # to initialize its sub-[#scopes] and [#operations]
5
+ #
6
+ class Container::Scope < Container
7
+ include Chaining
8
+
9
+ # The collection of named sub-scope constructors
10
+ # @return [Hash<Symbol, Evil::Client::Container::Scope::Builder>]
11
+ def scopes
12
+ @scopes ||= \
13
+ schema.scopes.each_with_object({}) do |(key, sub_schema), obj|
14
+ obj[key] = Builder::Scope.new(sub_schema, settings)
15
+ end
16
+ end
17
+
18
+ # The collection of named operations constructors
19
+ # @return [Hash<Symbol, Evil::Client::Container::Operation::Builder>]
20
+ def operations
21
+ @operations ||= \
22
+ schema.operations.each_with_object({}) do |(key, sub_schema), obj|
23
+ next unless key
24
+ obj[key] = Builder::Operation.new(sub_schema, settings)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ class Evil::Client
2
+ #
3
+ # Exception to be risen when schema definitions cannot be resolved.
4
+ # This is possibly a bug in client definition.
5
+ #
6
+ class DefinitionError < StandardError
7
+ private
8
+
9
+ def initialize(schema, keys, settings, text)
10
+ super "failed to resolve #{keys.join(' ')} from #{schema} schema" \
11
+ " for #{settings}: #{text}. Possibly this means a lack of" \
12
+ " necessary validations in definition of the client."
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ class Evil::Client
2
+ #
3
+ # Exception to be risen when selected name cannot be used in a custom client.
4
+ #
5
+ class NameError < ::NameError
6
+ # Checks whether a name is valid
7
+ #
8
+ # @param [#to_sym] name The name to check
9
+ # @param [Array<Symbol>] forbidden ([]) The list of forbidden names
10
+ # @return [Symbol] if name is valid
11
+ # @raise [self] if name isn't valid
12
+ #
13
+ def self.check!(name, forbidden = [])
14
+ name = name.to_sym
15
+ return name if name[FORMAT] && !forbidden.include?(name)
16
+ raise new(name, forbidden)
17
+ end
18
+
19
+ private
20
+
21
+ def initialize(name, forbidden)
22
+ super "Invalid name :#{name}." \
23
+ " It should contain latin letters in the lower case, digits," \
24
+ " and underscores only; have minimum 2 chars;" \
25
+ " start from a letter; end with either letter or digit." \
26
+ " The following names: '#{forbidden.join("', '")}'" \
27
+ " are already used by Evil::Client."
28
+ end
29
+
30
+ FORMAT = /^[a-z]([a-z\d_])*[a-z\d]$/
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ class Evil::Client
2
+ #
3
+ # Exception to be risen when remote API responded with undefined status
4
+ #
5
+ class ResponseError < RuntimeError
6
+ # @!attribute [r] schema
7
+ # @return [Evil::Client::Container::Operation::Schema] The operation schema
8
+ attr_reader :schema
9
+
10
+ # @!attribute [r] settings
11
+ # @return [Evil::Client::Settings] The settings used by the request
12
+ attr_reader :settings
13
+
14
+ # @!attribute [r] response
15
+ # @return [Array] The rack response to the request
16
+ attr_reader :response
17
+
18
+ # @!attribute [r] settings
19
+ # @return [Integer] The status of the [#response]
20
+ attr_reader :status
21
+
22
+ # @!attribute [r] headers
23
+ # @return [Hash] The hash of the [#response] headers
24
+ attr_reader :headers
25
+
26
+ # @!attribute [r] settings
27
+ # @return [Enumerable] The enumerable object describing the [#response] body
28
+ attr_reader :body
29
+
30
+ private
31
+
32
+ def initialize(schema, settings, response)
33
+ @schema = schema
34
+ @settings = settings
35
+ @response = response
36
+ @status, @headers, @body = Array(response)
37
+
38
+ super "remote API responded to #{@schema}" \
39
+ " with unexpected status #{@status}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ class Evil::Client
2
+ #
3
+ # Exception to be risen when user defines a scope/operation with a name,
4
+ # that has been used by existing operation/scope.
5
+ #
6
+ class TypeError < ::TypeError
7
+ # Checks whether a name can be used to define operation/scope of the schema
8
+ #
9
+ # @param [Evil::Client::Schema::Scope] scope
10
+ # @param [Symbol] name
11
+ # @param [Symbol] type
12
+ # @return [Symbol] nil
13
+ # @raise [self] if name cannot be used
14
+ #
15
+ def self.check!(schema, name, type)
16
+ return if type == :scope && schema.operations[name].nil?
17
+ return if type == :operation && schema.scopes[name].nil?
18
+ raise new(name, type)
19
+ end
20
+
21
+ private
22
+
23
+ def initialize(name, new_type)
24
+ old_type = new_type == :scope ? :operation : :scope
25
+ super "The #{old_type} :#{name} was already defined." \
26
+ " You cannot create #{new_type} with the same name."
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ class Evil::Client
2
+ #
3
+ # Exception to be risen when scope or operation cannot be initialized
4
+ # due to some options or their composition are invalid
5
+ #
6
+ class ValidationError < ArgumentError
7
+ private
8
+
9
+ def initialize(key, scope = nil, **options)
10
+ scope = "evil.client.errors.#{scope}"
11
+ .split(".")
12
+ .map { |part| __underscore__(part) }
13
+
14
+ super key.is_a?(Symbol) ? I18n.t(key, scope: scope, **options) : key
15
+ end
16
+
17
+ def __underscore__(name)
18
+ name.dup.tap do |n|
19
+ n.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
20
+ n.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
21
+ n.gsub!("::", "/")
22
+ n.tr!("-", "_")
23
+ n.downcase!
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ class Evil::Client
2
+ # Utility to format body/query into one of formats: :json, :form, :multipart
3
+ module Formatter
4
+ extend self
5
+
6
+ # Loads concrete formatters called by factory method [#call]
7
+ require_relative "formatter/text"
8
+ require_relative "formatter/form"
9
+ require_relative "formatter/multipart"
10
+
11
+ # Factory that knows how to format source depending on given format
12
+ #
13
+ # @param [Object] source
14
+ # @param [:json, :form, :multipart, :text] format
15
+ # @option opts [String] :boundary The boundary for a multipart body
16
+ # @return [String] formatted body
17
+ #
18
+ def call(source, format, **opts)
19
+ return unless source
20
+ return to_json(source) if format == :json
21
+ return to_yaml(source) if format == :yaml
22
+ return to_form(source) if format == :form
23
+ return to_text(source) if format == :text
24
+ to_multipart(source, opts)
25
+ end
26
+
27
+ private
28
+
29
+ def to_json(source)
30
+ JSON.dump source
31
+ end
32
+
33
+ def to_yaml(source)
34
+ YAML.dump source
35
+ end
36
+
37
+ def to_text(source)
38
+ Text.call source
39
+ end
40
+
41
+ def to_form(source)
42
+ Form.call source
43
+ end
44
+
45
+ def to_multipart(source, opts)
46
+ Multipart.call [source], opts
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module Evil::Client::Formatter
2
+ #
3
+ # Utility module to format body/query as a form
4
+ #
5
+ # @example
6
+ # Evil::Client::Formatter::Form.call foo: { bar: [:baz] }, qux: 1
7
+ # # => "foo[bar][]=baz&qux=1"
8
+ #
9
+ module Form
10
+ extend self
11
+
12
+ # Formats nested hash as a string
13
+ #
14
+ # @param [Hash] source
15
+ # @return [String]
16
+ #
17
+ def call(source)
18
+ case source
19
+ when nil then nil
20
+ when Hash then normalize(source)
21
+ else raise "#{source} is not a hash"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def normalize(value, *keys)
28
+ case value
29
+ when Hash then
30
+ value.flat_map { |key, val| normalize(val, *keys, key) }.join("&")
31
+ when Array then
32
+ value.flat_map { |val| normalize(val, *keys, nil) }.join("&")
33
+ else
34
+ finalize(value, *keys)
35
+ end
36
+ end
37
+
38
+ def finalize(value, key, *keys)
39
+ value = CGI.escape(value.to_s)
40
+ key = CGI.escape(key.to_s)
41
+ keys = keys.map { |k| "[#{CGI.escape(k.to_s)}]" }
42
+ "#{key}#{keys.join}=#{value}"
43
+ end
44
+ end
45
+ end