evil-client 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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