evil-client 0.2.3 → 0.3.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +5 -60
  4. data/CHANGELOG.md +50 -0
  5. data/README.md +13 -4
  6. data/evil-client.gemspec +1 -1
  7. data/lib/evil/client.rb +1 -1
  8. data/lib/evil/client/connection.rb +5 -5
  9. data/lib/evil/client/connection/net_http.rb +1 -1
  10. data/lib/evil/client/dsl.rb +1 -10
  11. data/lib/evil/client/dsl/operation.rb +10 -9
  12. data/lib/evil/client/dsl/response.rb +62 -0
  13. data/lib/evil/client/dsl/responses.rb +29 -0
  14. data/lib/evil/client/dsl/scope.rb +1 -8
  15. data/lib/evil/client/dsl/security.rb +1 -1
  16. data/lib/evil/client/model.rb +2 -2
  17. data/lib/evil/client/operation/request.rb +5 -10
  18. data/lib/evil/client/operation/response.rb +16 -17
  19. data/lib/evil/client/operation/response_error.rb +4 -3
  20. data/lib/evil/client/operation/unexpected_response_error.rb +7 -4
  21. data/spec/features/middleware_spec.rb +1 -3
  22. data/spec/features/operation_with_documentation_spec.rb +2 -2
  23. data/spec/features/operation_with_files_spec.rb +1 -1
  24. data/spec/features/operation_with_form_body_spec.rb +3 -3
  25. data/spec/features/operation_with_headers_spec.rb +1 -1
  26. data/spec/features/operation_with_http_method_spec.rb +1 -1
  27. data/spec/features/operation_with_json_body_spec.rb +3 -3
  28. data/spec/features/{operation_with_response_spec.rb → operation_with_nested_responses_spec.rb} +22 -36
  29. data/spec/features/operation_with_path_spec.rb +1 -1
  30. data/spec/features/operation_with_query_spec.rb +1 -1
  31. data/spec/features/operation_with_security_spec.rb +2 -2
  32. data/spec/features/scoping_spec.rb +1 -1
  33. data/spec/unit/evil/client/dsl/operation_spec.rb +156 -15
  34. data/spec/unit/evil/client/dsl/operations_spec.rb +3 -1
  35. data/spec/unit/evil/client/dsl/security_spec.rb +1 -1
  36. data/spec/unit/evil/client/operation/response_spec.rb +11 -9
  37. metadata +8 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d60319bf7209231d32f547d877be87a37a3fda7c
4
- data.tar.gz: 14a555f98f491d5909cb3be0e56de4795de16d06
3
+ metadata.gz: 7709a566e45dfcf398b95933a6d91639fd1ad47d
4
+ data.tar.gz: fa499f5f9856cb8a451bc87c0ff3f44f41ae7733
5
5
  SHA512:
6
- metadata.gz: 83e1ac00f591960aaa8c6f117432034261c99d0c7a7789d89aa40523cfed65d001994921ca84a1623b2101342702b279dc7a75eb01b0fe25980446e4668e3a3a
7
- data.tar.gz: d214fe190c49d2acc66eeb6778d331926c3d517eb3aa5a007141cb464abeb522013f75cde9bae07a3b99a466ef34a39d0fc7380438c6f9ad70d6cbfcbfe8f044
6
+ metadata.gz: 98e0553379fa9a0b2105758cf271f7b7c0816a82d0116519e60f84aeadadc1ba8a25c8d4b53028f4fab5d0deaa1dc3a68da5a9efb01482a803b38ae6c59fc34d
7
+ data.tar.gz: b8f0873e672bbf51531ae4dbcee3948a11b4a16d52cd3c0f06adb50120e4e80186f864b97084cbb16864f9100fbf96e1f0e83908e600276323ca35e465a1b639
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /.rubocop.todo.yml
data/.rubocop.yml CHANGED
@@ -5,94 +5,39 @@ AllCops:
5
5
  StyleGuideCopsOnly: true
6
6
  TargetRubyVersion: 2.3
7
7
 
8
- Lint/HandleExceptions:
9
- Exclude:
10
- - spec/**/*_spec.rb
11
-
12
- Lint/RescueException:
13
- Exclude:
14
- - spec/**/*_spec.rb
15
-
16
- Metrics/LineLength:
17
- Max: 80
18
- Exclude:
19
- - spec/**/*_spec.rb
20
-
21
- Style/AccessorMethodName:
22
- Exclude:
23
- - spec/**/*_spec.rb
24
-
25
8
  Style/Alias:
26
- EnforcedStyle: prefer_alias_method
27
-
28
- Style/AsciiComments:
29
9
  Enabled: false
30
10
 
31
11
  Style/CaseEquality:
32
12
  Enabled: false
33
13
 
34
- Style/ClassAndModuleChildren:
35
- Enabled: false
36
-
37
14
  Style/Documentation:
38
15
  Enabled: false
39
16
 
40
17
  Style/DoubleNegation:
41
18
  Enabled: false
42
19
 
43
- Style/EmptyLinesAroundClassBody:
44
- Enabled: false
45
-
46
- Style/EmptyLinesAroundModuleBody:
47
- Enabled: false
48
-
49
- Style/EmptyLineBetweenDefs:
50
- Enabled: false
51
-
52
- Style/FileName:
20
+ Style/LambdaCall:
53
21
  Enabled: false
54
22
 
55
- Style/Lambda:
56
- Exclude:
57
- - spec/**/*_spec.rb
58
-
59
- Style/LambdaCall:
23
+ Style/MethodMissing:
60
24
  Enabled: false
61
25
 
62
- Style/ModuleFunction:
26
+ Style/NumericPredicate:
63
27
  Enabled: false
64
28
 
65
29
  Style/RaiseArgs:
66
- EnforcedStyle: compact
67
-
68
- Style/Semicolon:
69
- Exclude:
70
- - spec/**/*_spec.rb
71
-
72
- Style/SignalException:
73
- EnforcedStyle: semantic
30
+ Enabled: false
74
31
 
75
- Style/SingleLineBlockParams:
32
+ Style/RescueModifier:
76
33
  Enabled: false
77
34
 
78
35
  Style/SingleLineMethods:
79
36
  Exclude:
80
37
  - spec/**/*_spec.rb
81
38
 
82
- Style/SpaceBeforeFirstArg:
83
- Enabled: false
84
-
85
- Style/SpecialGlobalVars:
86
- Exclude:
87
- - Gemfile
88
- - dry-initializer.gemspec
89
-
90
39
  Style/StringLiterals:
91
40
  EnforcedStyle: double_quotes
92
41
 
93
42
  Style/StringLiteralsInInterpolation:
94
43
  EnforcedStyle: double_quotes
95
-
96
- Style/TrivialAccessors:
97
- Exclude:
98
- - spec/**/*_spec.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # v0.3.0 2016-11-18
2
+
3
+ This version changes the way of processing responses. Instead of dealing
4
+ with raw rake responses, we add opinionated methods to gracefully process
5
+ responses from JSON or plain text.
6
+
7
+ In the next minor versions processors for both "form" and "file" (multipart)
8
+ formats will be added.
9
+
10
+ ## BREAKING CHANGES
11
+ - Method `DSL#response` was redefined with a new signature (nepalez)
12
+
13
+ The method takes 2 _mandatory_ positional params: unique name and
14
+ integer status. This allows to process responses with the same status,
15
+ and different structures, like in the following example, where errors
16
+ are returned with the same status 200 (not 4**) as success:
17
+
18
+ ```ruby
19
+ operation :update_user do
20
+ # ...
21
+ response :success, 200, model: User
22
+ response :error, 200, model: Error
23
+ end
24
+ ```
25
+
26
+ This time response handler will try processing a response using various
27
+ definitions (in order of their declaration) until some suits. The hanlder
28
+ returns `UnexpectedResponseError` in case no definition proves suitable.
29
+
30
+ Names (the first param) are unique. When several definitions use the same name,
31
+ only the last one will be applicable.
32
+
33
+ ## Added
34
+ - Method `DSL#responses` to share options between response definitions (nepalez)
35
+
36
+ ```ruby
37
+ responses format: "json" do
38
+ responses raise: true do
39
+ response :failure, 400
40
+ response :not_found, 404
41
+ end
42
+ end
43
+ ```
44
+
45
+ This is the same as:
46
+
47
+ ```ruby
48
+ response :failure, 400, format: "json", raise: true
49
+ response :not_found, 404, format: "json", raise: true
50
+ ```
data/README.md CHANGED
@@ -90,12 +90,21 @@ class CatsClient < Evil::Client
90
90
  attribute :age, optional: true
91
91
  end
92
92
 
93
- response 200 do |body:, **|
94
- Cat.new JSON.parse(body) # define that the body should be wrapped to cat
93
+ # Parses json response and wraps it into Cat instance with additional
94
+ # parameter
95
+ response 200, format: :json, type: Cat do
96
+ attribute :success
95
97
  end
96
98
 
97
- response 422, raise: true do |body:, **|
98
- JSON.parse(body) # expect 422 to return json data
99
+ # Parses json response, wraps it into model with [#error] and raises
100
+ # an exception where [ResponseError#response] contains the model istance
101
+ response 422, format: :json, raise: true do
102
+ attribute :error
103
+ end
104
+
105
+ # Takes raw body and converts it into the hashie
106
+ response 404, raise: true do |body|
107
+ Hashie::Mash.new error: body
99
108
  end
100
109
  end
101
110
 
data/evil-client.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "evil-client"
3
- gem.version = "0.2.3"
3
+ gem.version = "0.3.0"
4
4
  gem.author = "Andrew Kozin (nepalez)"
5
5
  gem.email = "andrew.kozin@gmail.com"
6
6
  gem.homepage = "https://github.com/evilmartians/evil-client"
data/lib/evil/client.rb CHANGED
@@ -41,7 +41,7 @@ require "rack"
41
41
  # option :id, type: Dry::Types["coercible.int"].constrained(gt: 0)
42
42
  # end
43
43
  #
44
- # response 200, model: Cat
44
+ # response 200, type: Cat
45
45
  # response 400, raise: true
46
46
  # response 422, raise: true do |body:|
47
47
  # JSON.parse(body.first)
@@ -12,11 +12,11 @@ class Evil::Client
12
12
  # @return [Class]
13
13
  #
14
14
  def self.[](name = nil)
15
- key = (name || REGISTRY.keys.first).to_sym
16
-
15
+ keys = REGISTRY.keys
16
+ key = (name || keys.first).to_sym
17
17
  klass = REGISTRY.fetch(key) do
18
- fail ArgumentError.new "Connection '#{key}' is not registered." \
19
- " Use the following keys: #{REGISTRY.keys}"
18
+ raise ArgumentError.new "Connection '#{key}' is not registered." \
19
+ " Use the following keys: #{keys}"
20
20
  end
21
21
 
22
22
  require_relative "connection/#{key}"
@@ -29,7 +29,7 @@ class Evil::Client
29
29
  # @return [Array]
30
30
  #
31
31
  def call(_env)
32
- fail NotImplementedError
32
+ raise NotImplementedError
33
33
  end
34
34
  end
35
35
  end
@@ -46,7 +46,7 @@ class Evil::Client
46
46
  end
47
47
 
48
48
  def build_uri(path, query)
49
- base_uri.merge(path).tap { |uri| uri.query = query }
49
+ base_uri.merge(URI.encode(path)).tap { |uri| uri.query = query }
50
50
  end
51
51
 
52
52
  def handle(response)
@@ -4,15 +4,6 @@ class Evil::Client
4
4
  require_relative "dsl/operations"
5
5
  require_relative "dsl/scope"
6
6
 
7
- # Stack of default middleware before custom midleware and a connection
8
- # This stack cannot be modified
9
- DEFAULT_MIDDLEWARE = Middleware.new do
10
- run Middleware::MergeSecurity
11
- run Middleware::StringifyJson
12
- run Middleware::StringifyQuery
13
- run Middleware::NormalizeHeaders
14
- end
15
-
16
7
  # Adds [#operations] to a specific client's instances
17
8
  def self.extended(klass)
18
9
  klass.include Dry::Initializer.define -> { param :operations }
@@ -106,7 +97,7 @@ class Evil::Client
106
97
 
107
98
  private
108
99
 
109
- BASE_URL = -> (_) { fail NotImplementedError.new "Base url is not defined" }
100
+ BASE_URL = proc { raise NotImplementedError.new "Base url is not defined" }
110
101
 
111
102
  def schema
112
103
  @schema ||= {
@@ -1,6 +1,8 @@
1
1
  module Evil::Client::DSL
2
2
  require_relative "security"
3
3
  require_relative "files"
4
+ require_relative "response"
5
+ require_relative "responses"
4
6
 
5
7
  # Builds a schema for single operation
6
8
  class Operation
@@ -69,13 +71,12 @@ module Evil::Client::DSL
69
71
  @schema[:query] = __model__(options, &block)
70
72
  end
71
73
 
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
74
+ def responses(options = {}, &block)
75
+ Responses.new(self, options, &block)
76
+ end
77
+
78
+ def response(name, status, **options, &block)
79
+ @schema[:responses][name] = Response[status, block: block, **options]
79
80
  end
80
81
 
81
82
  # ==========================================================================
@@ -85,8 +86,8 @@ module Evil::Client::DSL
85
86
  def __valid_format__(format)
86
87
  formats = %w(json form)
87
88
  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}"
89
+ raise ArgumentError.new "Invalid format #{format} for body." \
90
+ " Use one of formats: #{formats}"
90
91
  end
91
92
 
92
93
  def __model__(model: nil, **, &block)
@@ -0,0 +1,62 @@
1
+ module Evil::Client::DSL
2
+ # Builds a schema for response processor
3
+ class Response
4
+ extend Dry::Initializer::Mixin
5
+ param :status
6
+ option :raise, default: proc { false }
7
+ option :format, default: proc {}
8
+ option :model, default: proc { nil }
9
+ option :block, default: proc { nil }
10
+
11
+ def self.[](*args)
12
+ new(*args).to_h
13
+ end
14
+
15
+ def to_h
16
+ { status: status.to_i, coercer: coercer, raise: raise }
17
+ end
18
+
19
+ private
20
+
21
+ def json?
22
+ format.to_s == "json"
23
+ end
24
+
25
+ def arity
26
+ block&.arity
27
+ end
28
+
29
+ def coercer
30
+ handlers = [parser, processor, wrapper, finalizer].compact
31
+ proc { |body| handlers.inject(body) { |obj, handler| handler.call(obj) } }
32
+ end
33
+
34
+ def parser
35
+ proc { |body| JSON.parse(body) } if json?
36
+ end
37
+
38
+ def wrapper
39
+ return unless json?
40
+ proc { |data| Hash === data ? data : { data: data } }
41
+ end
42
+
43
+ def processor
44
+ return unless block&.arity == 1
45
+ block
46
+ end
47
+
48
+ def addon
49
+ return unless arity == 0
50
+ proc { |klass| klass.instance_eval(&block) }
51
+ end
52
+
53
+ def finalizer
54
+ case [model.nil?, addon.nil?]
55
+ when [false, true] then model
56
+ when [false, false] then Class.new(model).tap(&addon)
57
+ when [true, false] then Class.new(Evil::Client::Model).tap(&addon)
58
+ else nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ class Evil::Client::DSL::Responses
2
+ # Add settings shared by several responses
3
+ #
4
+ # @param [#to_sym] name
5
+ # @param [#to_i] status
6
+ # @param [Hash<Symbol, Object>] options
7
+ # @param [Proc] block
8
+ #
9
+ def response(name, status, **options, &block)
10
+ @client.send :response, name, status, @options.merge(options), &block
11
+ end
12
+
13
+ # Add settings shared by several responses
14
+ #
15
+ # @param [Hash<Symbol, Object>] options
16
+ # @param [Proc] block
17
+ #
18
+ def responses(**options, &block)
19
+ self.class.new(@client, @options.merge(options), &block)
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(client, **options, &block)
25
+ @client = client
26
+ @options = options
27
+ instance_eval(&block)
28
+ end
29
+ end
@@ -2,7 +2,7 @@ module Evil::Client::DSL
2
2
  # Provides a namespace for client's top-level DSL
3
3
  class Scope
4
4
  extend Dry::Initializer::Mixin
5
- option :__scope__, default: proc {}
5
+ option :__scope__, default: proc {}, reader: :private
6
6
 
7
7
  # Declares a method that opens new scope inside the current one
8
8
  # An instance of new scope has access to methods of its parent
@@ -20,14 +20,7 @@ module Evil::Client::DSL
20
20
 
21
21
  private
22
22
 
23
- private :__scope__
24
-
25
- def respond_to_missing?(name, *)
26
- __scope__.respond_to? name
27
- end
28
-
29
23
  def method_missing(name, *args)
30
- super unless respond_to? name
31
24
  __scope__.send(name, *args)
32
25
  end
33
26
  end
@@ -51,7 +51,7 @@ module Evil::Client::DSL
51
51
  def __validate__(part)
52
52
  parts = %i(body query headers)
53
53
  return if parts.include? part
54
- fail ArgumentError.new("Wrong part '#{part}'. Use one of parts: #{parts}")
54
+ raise ArgumentError.new "Wrong part '#{part}'. Use one of parts: #{parts}"
55
55
  end
56
56
  end
57
57
  end
@@ -25,8 +25,8 @@ class Evil::Client
25
25
  @attributes ||= []
26
26
  end
27
27
 
28
- def option(name, type = nil, **opts)
29
- super.tap { attributes << name.to_sym }
28
+ def option(name, type = nil, as: nil, **opts)
29
+ super.tap { attributes << (as || name).to_sym }
30
30
  end
31
31
  alias_method :attribute, :option
32
32
  alias_method :param, :option
@@ -13,8 +13,8 @@ class Evil::Client::Operation
13
13
  def build(options)
14
14
  {
15
15
  format: schema[:format],
16
- http_method: http_method,
17
- path: path.call(options),
16
+ http_method: extract(:method),
17
+ path: extract(:path).call(options),
18
18
  security: schema[:security]&.call(options),
19
19
  files: schema[:files]&.call(options),
20
20
  query: schema[:query]&.new(options).to_h,
@@ -29,14 +29,9 @@ class Evil::Client::Operation
29
29
  @key ||= schema[:key]
30
30
  end
31
31
 
32
- def http_method
33
- return schema[:method] if schema[:method]
34
- fail NotImplementedError.new "No method defined for operation '#{key}'"
35
- end
36
-
37
- def path
38
- return schema[:path] if schema[:path]
39
- fail NotImplementedError.new "Path not defined for operation '#{key}'"
32
+ def extract(property)
33
+ return schema[property] if schema[property]
34
+ raise NotImplementedError, "No #{property} defined for operation '#{key}'"
40
35
  end
41
36
  end
42
37
  end
@@ -11,30 +11,29 @@ class Evil::Client::Operation
11
11
  #
12
12
  # @param [Array] array Rack-compatible array of response data
13
13
  # @return [Object]
14
- # @raise [Evil::Client::ResponseError] if it is required by the schema
15
14
  #
16
- def handle(array)
17
- status, header, body = array
18
- response = Rack::Response.new(body, status, header)
15
+ # @raise [Evil::Client::ResponseError] when needed by the schema
16
+ # @raise [Evil::Client::UnexpectedResponseError]
17
+ # when a response cannot be processed
18
+ #
19
+ def handle(response)
20
+ status, _, body = response
21
+ body = body.any? ? body.join("\n") : nil
19
22
 
20
- handler = response_schema(response)
21
- data = handler[:coercer].call response: response,
22
- body: response.body,
23
- header: response.header
23
+ handlers(status).each do |handler|
24
+ data = handler[:coercer][body] rescue next
25
+ raise ResponseError.new(schema, status, data) if handler[:raise]
26
+ return data
27
+ end
24
28
 
25
- handler[:raise] ? fail(ResponseError.new(schema, status, data)) : data
29
+ raise UnexpectedResponseError.new(schema, status, body)
26
30
  end
27
31
 
28
32
  private
29
33
 
30
- def name
31
- @name ||= schema[:name]
32
- end
33
-
34
- def response_schema(response)
35
- schema[:responses].fetch response.status do
36
- fail UnexpectedResponseError.new(schema, response)
37
- end
34
+ def handlers(status)
35
+ schema[:responses].values
36
+ .select { |handler| handler[:status] == status }
38
37
  end
39
38
  end
40
39
  end
@@ -1,11 +1,12 @@
1
1
  class Evil::Client::Operation
2
2
  class ResponseError < RuntimeError
3
- attr_reader :response
3
+ attr_reader :status, :data
4
4
 
5
5
  private
6
6
 
7
- def initialize(schema, status, response)
8
- @response = response
7
+ def initialize(schema, status, data)
8
+ @status = status
9
+ @data = data
9
10
  super "Response to operation '#{schema[:key]}' has http status #{status}"
10
11
  end
11
12
  end
@@ -1,15 +1,18 @@
1
1
  class Evil::Client::Operation
2
2
  class UnexpectedResponseError < RuntimeError
3
- attr_reader :response
3
+ attr_reader :status, :data
4
4
 
5
5
  private
6
6
 
7
- def initialize(schema, response)
8
- @response = response
7
+ def initialize(schema, status, data)
8
+ @status = status
9
+ @data = data
9
10
 
10
11
  message = "Response to operation '#{schema[:key]}'" \
11
- " has unexpected http status #{response.status}."
12
+ " with http status #{status} and body #{data}" \
13
+ " cannot be processed."
12
14
  message << " See #{schema[:doc]} for details." if schema[:doc]
15
+
13
16
  super message
14
17
  end
15
18
  end
@@ -34,9 +34,7 @@ RSpec.describe "middleware" do
34
34
  operation :find do
35
35
  path { "some" }
36
36
  http_method :post
37
- response 200 do |body:, **|
38
- body.first
39
- end
37
+ response :success, 200, format: :plain
40
38
  end
41
39
  end
42
40
 
@@ -25,7 +25,7 @@ RSpec.describe "operation with documentation" do
25
25
  rescue => error
26
26
  expect(error.message).to include "https://docs.example.com/v3/index.html"
27
27
  else
28
- fail
28
+ raise
29
29
  end
30
30
  end
31
31
 
@@ -35,7 +35,7 @@ RSpec.describe "operation with documentation" do
35
35
  rescue => error
36
36
  expect(error.message).to include "https://docs.example.com/v3/findData"
37
37
  else
38
- fail
38
+ raise
39
39
  end
40
40
  end
41
41
  end
@@ -5,7 +5,7 @@ RSpec.describe "operation with files" do
5
5
  operation :example do
6
6
  http_method :get
7
7
  path { "users" }
8
- response 200
8
+ response :success, 200
9
9
 
10
10
  files do |file:, **|
11
11
  add file, type: "text/xml", charset: "utf-16", filename: "foo.xml"
@@ -9,7 +9,7 @@ RSpec.describe "operation with form body" do
9
9
  operation do
10
10
  http_method :get
11
11
  path { "users" }
12
- response 200
12
+ response :success, 200
13
13
  end
14
14
  end
15
15
 
@@ -26,7 +26,7 @@ RSpec.describe "operation with form body" do
26
26
  operation do
27
27
  http_method :get
28
28
  path { "users" }
29
- response 200
29
+ response :success, 200
30
30
 
31
31
  body format: "form" do
32
32
  attribute :foo
@@ -57,7 +57,7 @@ RSpec.describe "operation with form body" do
57
57
  operation do
58
58
  http_method :get
59
59
  path { "users" }
60
- response 200
60
+ response :success, 200
61
61
 
62
62
  body format: "form" do
63
63
  attribute :foo
@@ -12,7 +12,7 @@ RSpec.describe "operation with headers" do
12
12
  attribute :baz, optional: true
13
13
  end
14
14
 
15
- response 200
15
+ response :success, 200
16
16
  end
17
17
 
18
18
  operation :clear_data do
@@ -5,7 +5,7 @@ RSpec.describe "operation with http_method" do
5
5
  operation do |settings|
6
6
  http_method settings.version > 1 ? :post : :get
7
7
  path { "data" }
8
- response 200
8
+ response :success, 200
9
9
  end
10
10
 
11
11
  operation :clear_data do
@@ -9,7 +9,7 @@ RSpec.describe "operation with json body" do
9
9
  operation do
10
10
  http_method :get
11
11
  path { "users" }
12
- response 200
12
+ response :success, 200
13
13
  end
14
14
  end
15
15
 
@@ -26,7 +26,7 @@ RSpec.describe "operation with json body" do
26
26
  operation do
27
27
  http_method :get
28
28
  path { "users" }
29
- response 200
29
+ response :success, 200
30
30
 
31
31
  body do
32
32
  attribute :foo
@@ -55,7 +55,7 @@ RSpec.describe "operation with json body" do
55
55
  operation do
56
56
  http_method :get
57
57
  path { "users" }
58
- response 200
58
+ response :success, 200
59
59
 
60
60
  body do
61
61
  attribute :foo
@@ -1,4 +1,4 @@
1
- RSpec.describe "operation with query" do
1
+ RSpec.describe "operation with nested responses" do
2
2
  # see Test::Client definition in `/spec/support/test_client.rb`
3
3
  before do
4
4
  class Test::User < Evil::Client::Model
@@ -10,18 +10,22 @@ RSpec.describe "operation with query" do
10
10
  http_method :get
11
11
  path { "users" }
12
12
 
13
- response 200
13
+ response :success, 200, format: :plain
14
14
  end
15
15
 
16
16
  operation :example do
17
- response 201 do |body:, header:, response:|
18
- [body, header, response]
19
- end
20
-
21
- response 404, raise: true
22
-
23
- response 422, raise: true do |body:, header:, response:|
24
- [body, header, response]
17
+ responses format: :plain do
18
+ response :created, 201 do |body|
19
+ body.to_sym
20
+ end
21
+
22
+ responses raise: true do
23
+ response :not_found, 404
24
+
25
+ response :error, 422 do |body|
26
+ body.to_sym
27
+ end
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -36,9 +40,7 @@ RSpec.describe "operation with query" do
36
40
  headers: { "Foo" => "BAR" },
37
41
  body: "Hi!"
38
42
 
39
- expect(subject).to be_kind_of Rack::Response
40
- expect(subject.headers).to include "foo" => ["BAR"]
41
- expect(subject.body).to eq ["Hi!"]
43
+ expect(subject).to eq "Hi!"
42
44
  end
43
45
 
44
46
  it "applies block to coerce data" do
@@ -46,13 +48,7 @@ RSpec.describe "operation with query" do
46
48
  headers: { "Foo" => "BAR" },
47
49
  body: "Hi!"
48
50
 
49
- body, headers, response = subject
50
-
51
- expect(response.headers).to include "foo" => ["BAR"]
52
- expect(response.body).to eq ["Hi!"]
53
-
54
- expect(body).to eq response.body
55
- expect(headers).to eq response.headers
51
+ expect(subject).to eq :Hi!
56
52
  end
57
53
 
58
54
  it "raises ResponseError when necessary" do
@@ -63,11 +59,9 @@ RSpec.describe "operation with query" do
63
59
  begin
64
60
  subject
65
61
  rescue Evil::Client::Operation::ResponseError => error
66
- expect(error.response).to be_kind_of Rack::Response
67
- expect(error.response.headers).to include "foo" => ["BAR"]
68
- expect(error.response.body).to eq ["Hi!"]
62
+ expect(error.data).to eq "Hi!"
69
63
  else
70
- fail
64
+ raise
71
65
  end
72
66
  end
73
67
 
@@ -79,15 +73,9 @@ RSpec.describe "operation with query" do
79
73
  begin
80
74
  subject
81
75
  rescue Evil::Client::Operation::ResponseError => error
82
- body, headers, response = error.response
83
-
84
- expect(response.headers).to include "foo" => ["BAR"]
85
- expect(response.body).to eq ["Hi!"]
86
-
87
- expect(body).to eq response.body
88
- expect(headers).to eq response.headers
76
+ expect(error.data).to eq :Hi!
89
77
  else
90
- fail
78
+ raise
91
79
  end
92
80
  end
93
81
 
@@ -99,11 +87,9 @@ RSpec.describe "operation with query" do
99
87
  begin
100
88
  subject
101
89
  rescue Evil::Client::Operation::UnexpectedResponseError => error
102
- expect(error.response).to be_kind_of Rack::Response
103
- expect(error.response.headers).to include "foo" => ["BAR"]
104
- expect(error.response.body).to eq ["Hi!"]
90
+ expect(error.data).to eq "Hi!"
105
91
  else
106
- fail
92
+ raise
107
93
  end
108
94
  end
109
95
  end
@@ -5,7 +5,7 @@ RSpec.describe "operation with path" do
5
5
  operation do
6
6
  http_method :get
7
7
  path { "users" }
8
- response 200
8
+ response :success, 200
9
9
  end
10
10
 
11
11
  operation :find_users
@@ -9,7 +9,7 @@ RSpec.describe "operation with query" do
9
9
  operation do
10
10
  http_method :get
11
11
  path { "users" }
12
- response 200
12
+ response :success, 200
13
13
  end
14
14
 
15
15
  operation :filter do
@@ -5,7 +5,7 @@ RSpec.describe "operation with security" do
5
5
  operation do
6
6
  http_method :get
7
7
  path { "data" }
8
- response 200
8
+ response :success, 200
9
9
  end
10
10
  end
11
11
 
@@ -29,7 +29,7 @@ RSpec.describe "operation with security" do
29
29
  operation do |settings|
30
30
  http_method :get
31
31
  path { "data" }
32
- response 200
32
+ response :success, 200
33
33
 
34
34
  security do
35
35
  basic_auth settings.user, settings.password
@@ -14,7 +14,7 @@ RSpec.describe "scoping" do
14
14
  attribute :name
15
15
  end
16
16
 
17
- response 200
17
+ response :success, 200
18
18
  end
19
19
 
20
20
  scope do
@@ -204,30 +204,171 @@ RSpec.describe Evil::Client::DSL::Operation do
204
204
  end
205
205
 
206
206
  context "with #response" do
207
- let(:block) do
208
- proc do |_|
209
- response 200 do |value|
210
- value.to_sym
207
+ let(:response_schema) { subject[:responses][:success] }
208
+ let(:response_raise) { response_schema[:raise] }
209
+ let(:response_coercer) { response_schema[:coercer] }
210
+
211
+ context "with plain format" do
212
+ let(:body) { "foo" }
213
+ let(:block) { proc { |_| response :success, 200, format: :plain } }
214
+
215
+ it "works" do
216
+ expect(response_coercer.call(body)).to eq "foo"
217
+ end
218
+ end
219
+
220
+ context "with plain format and handler" do
221
+ let(:body) { "foo" }
222
+ let(:block) do
223
+ proc do |_|
224
+ response :success, 200, format: :plain do |body|
225
+ body.to_sym
226
+ end
227
+ end
228
+ end
229
+
230
+ it "applies coercer" do
231
+ expect(response_coercer.call(body)).to eq :foo
232
+ end
233
+ end
234
+
235
+ context "with plain format and type" do
236
+ let(:body) { "0" }
237
+ let(:block) do
238
+ proc do |_|
239
+ response :success,
240
+ 200,
241
+ format: :plain,
242
+ model: Dry::Types["coercible.int"]
243
+ end
244
+ end
245
+
246
+ it "uses type" do
247
+ expect(response_coercer.call(body)).to eq 0
248
+ end
249
+ end
250
+
251
+ context "with plain format, coercer and type" do
252
+ let(:body) { "0" }
253
+ let(:block) do
254
+ proc do |_|
255
+ response :success,
256
+ 200,
257
+ format: :plain,
258
+ model: Dry::Types["coercible.string"] { |val| val.to_i + 1 }
259
+ end
260
+ end
261
+
262
+ it "applies coercer and then type" do
263
+ expect(response_coercer.call(body)).to eq "1"
264
+ end
265
+ end
266
+
267
+ context "with json format" do
268
+ let(:body) { '{"foo":1,"baz":"qux"}' }
269
+ let(:block) { proc { |_| response :success, 200, format: :json } }
270
+
271
+ it "returns parsed body" do
272
+ expect(response_coercer.call(body)).to eq "foo" => 1, "baz" => "qux"
273
+ end
274
+ end
275
+
276
+ context "with json format and handler" do
277
+ let(:body) { '{"foo":1,"baz":"qux"}' }
278
+ let(:block) do
279
+ proc do |_|
280
+ response :success, 200, format: :json do |body|
281
+ body.keys
282
+ end
283
+ end
284
+ end
285
+
286
+ it "returns parsed and handled body" do
287
+ expect(response_coercer.call(body)).to eq data: %w(foo baz)
288
+ end
289
+ end
290
+
291
+ context "with json format and type" do
292
+ before do
293
+ class Test::Foo < Evil::Client::Model
294
+ attribute :foo
295
+ end
296
+ end
297
+
298
+ let(:body) { '{"foo":1,"baz":"qux"}' }
299
+ let(:block) do
300
+ proc do |_|
301
+ response :success, 200, format: :json, model: Test::Foo
302
+ end
303
+ end
304
+
305
+ it "returns parsed and filtered body" do
306
+ expect(response_coercer.call(body)).to eq foo: 1
307
+ end
308
+ end
309
+
310
+ context "with json format, type and handler" do
311
+ before do
312
+ class Test::Foo < Evil::Client::Model
313
+ attribute :foo
314
+ end
315
+ end
316
+
317
+ let(:body) { '{"foo":1,"baz":"qux"}' }
318
+ let(:block) do
319
+ proc do |_|
320
+ response :success, 200, format: :json, model: Test::Foo do |body|
321
+ body["foo"] = body["foo"].to_s
322
+ body
323
+ end
324
+ end
325
+ end
326
+
327
+ it "returns parsed, handled and filtered body" do
328
+ expect(response_coercer.call(body)).to eq Test::Foo.new(foo: "1")
329
+ end
330
+ end
331
+
332
+ context "with json format and filter" do
333
+ before do
334
+ class Test::Foo < Evil::Client::Model
335
+ attribute :foo
211
336
  end
337
+ end
212
338
 
213
- response 404, raise: true do |value|
214
- value.to_s
339
+ let(:body) { '{"foo":1,"baz":"qux"}' }
340
+ let(:block) do
341
+ proc do |_|
342
+ response :success, 200, format: :json do
343
+ attribute :baz
344
+ end
215
345
  end
346
+ end
216
347
 
217
- response 500, raise: true
348
+ it "returns parsed and filtered body" do
349
+ expect(response_coercer.call(body)).to eq baz: "qux"
218
350
  end
219
351
  end
220
352
 
221
- it "defines :responses" do
222
- responses = subject[:responses]
353
+ context "with json format, type and filter" do
354
+ before do
355
+ class Test::Foo < Evil::Client::Model
356
+ attribute :foo
357
+ end
358
+ end
223
359
 
224
- expect(responses[200]).to include raise: false
225
- expect(responses[404]).to include raise: true
226
- expect(responses[500]).to include raise: true
360
+ let(:body) { '{"foo":1,"baz":2}' }
361
+ let(:block) do
362
+ proc do |_|
363
+ response :success, 200, format: :json, model: Test::Foo do
364
+ attribute :baz, Dry::Types["coercible.string"]
365
+ end
366
+ end
367
+ end
227
368
 
228
- expect(responses[200][:coercer]["foo"]).to eq :foo
229
- expect(responses[404][:coercer][:foo]).to eq "foo"
230
- expect(responses[500][:coercer][response: :foo]).to eq :foo
369
+ it "returns parsed and filtered body" do
370
+ expect(response_coercer.call(body)).to eq foo: 1, baz: "2"
371
+ end
231
372
  end
232
373
  end
233
374
  end
@@ -1,6 +1,8 @@
1
1
  describe Evil::Client::DSL::Operations do
2
2
  let(:operations) { described_class.new }
3
- let(:settings) { double(:settings, version: 1, user: "foo", password: "bar") }
3
+ let(:settings) do
4
+ double(:settings, version: 1, user: "foo", password: "bar")
5
+ end
4
6
 
5
7
  before do
6
8
  operations.register(nil) do |settings|
@@ -52,7 +52,7 @@ RSpec.describe Evil::Client::DSL::Security do
52
52
  end
53
53
  end
54
54
 
55
- it "fails" do
55
+ it "raises" do
56
56
  expect { subject }.to raise_error ArgumentError
57
57
  end
58
58
  end
@@ -5,12 +5,14 @@ RSpec.describe Evil::Client::Operation::Response do
5
5
  key: :find_user,
6
6
  doc: "http://example.com/users",
7
7
  responses: {
8
- 200 => {
9
- coercer: proc { |body:, **| body.first.upcase.to_sym },
8
+ success: {
9
+ status: 200,
10
+ coercer: proc { |body| body.upcase.to_sym },
10
11
  raise: false
11
12
  },
12
- 400 => {
13
- coercer: proc { |body:, **| body.first.upcase.to_sym },
13
+ error: {
14
+ status: 400,
15
+ coercer: proc { |body| body.upcase.to_sym },
14
16
  raise: true
15
17
  }
16
18
  }
@@ -35,9 +37,9 @@ RSpec.describe Evil::Client::Operation::Response do
35
37
  subject
36
38
  rescue Evil::Client::Operation::ResponseError => error
37
39
  expect(error.message).to include "find_user"
38
- expect(error.response).to eq :FOO
40
+ expect(error.data).to eq :FOO
39
41
  else
40
- fail
42
+ raise
41
43
  end
42
44
  end
43
45
  end
@@ -51,10 +53,10 @@ RSpec.describe Evil::Client::Operation::Response do
51
53
  rescue Evil::Client::Operation::UnexpectedResponseError => error
52
54
  expect(error.message).to include "find_user"
53
55
  expect(error.message).to include "http://example.com/users"
54
- expect(error.response).to be_a Rack::Response
55
- expect(error.response.status).to eq 404
56
+ expect(error.data).to eq "foo"
57
+ expect(error.status).to eq 404
56
58
  else
57
- fail
59
+ raise
58
60
  end
59
61
  end
60
62
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evil-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kozin (nepalez)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-07 00:00:00.000000000 Z
11
+ date: 2016-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-initializer
@@ -128,12 +128,14 @@ executables: []
128
128
  extensions: []
129
129
  extra_rdoc_files:
130
130
  - README.md
131
+ - CHANGELOG.md
131
132
  files:
132
133
  - ".codeclimate.yml"
133
134
  - ".gitignore"
134
135
  - ".rspec"
135
136
  - ".rubocop.yml"
136
137
  - ".travis.yml"
138
+ - CHANGELOG.md
137
139
  - Gemfile
138
140
  - LICENSE.txt
139
141
  - README.md
@@ -160,6 +162,8 @@ files:
160
162
  - lib/evil/client/dsl/files.rb
161
163
  - lib/evil/client/dsl/operation.rb
162
164
  - lib/evil/client/dsl/operations.rb
165
+ - lib/evil/client/dsl/response.rb
166
+ - lib/evil/client/dsl/responses.rb
163
167
  - lib/evil/client/dsl/scope.rb
164
168
  - lib/evil/client/dsl/security.rb
165
169
  - lib/evil/client/middleware.rb
@@ -186,9 +190,9 @@ files:
186
190
  - spec/features/operation_with_headers_spec.rb
187
191
  - spec/features/operation_with_http_method_spec.rb
188
192
  - spec/features/operation_with_json_body_spec.rb
193
+ - spec/features/operation_with_nested_responses_spec.rb
189
194
  - spec/features/operation_with_path_spec.rb
190
195
  - spec/features/operation_with_query_spec.rb
191
- - spec/features/operation_with_response_spec.rb
192
196
  - spec/features/operation_with_security_spec.rb
193
197
  - spec/features/scoping_spec.rb
194
198
  - spec/spec_helper.rb
@@ -243,9 +247,9 @@ test_files:
243
247
  - spec/features/operation_with_headers_spec.rb
244
248
  - spec/features/operation_with_http_method_spec.rb
245
249
  - spec/features/operation_with_json_body_spec.rb
250
+ - spec/features/operation_with_nested_responses_spec.rb
246
251
  - spec/features/operation_with_path_spec.rb
247
252
  - spec/features/operation_with_query_spec.rb
248
- - spec/features/operation_with_response_spec.rb
249
253
  - spec/features/operation_with_security_spec.rb
250
254
  - spec/features/scoping_spec.rb
251
255
  - spec/spec_helper.rb