evil-client 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2740449772138472d84ec8f0a8f4e5fdeea23af8
4
- data.tar.gz: d165a97dababfa3e40f1838a18d06b61847ff1da
3
+ metadata.gz: 4e0323c33d7f3cf56d4aca84057aef698e57efbb
4
+ data.tar.gz: fde6a5543b7c8775350d18cf805b8efe277d65cb
5
5
  SHA512:
6
- metadata.gz: 4274645076138cbba089ecbcc3c3c4034e50789c7128e06b1abb45e9316c41908cf367174002227ddc51e124bd2963a4b0de3ba7a5be8e3bee345ba34e2cf92b
7
- data.tar.gz: 98cc762b3c67d6688879dd3724a24c19ab78823acb162f9c6ea94e8fd4f53aa1f44834f005df3e99f483128ba2de7e43549823135e71b2e136927ec844deb5bd
6
+ metadata.gz: 4bcaf9be801454e01f7a99d0385f71eafabbe6e98955ea24b3541d0a5a68bfb88e891ca714736354a8bbd3e45c57c4936ee84e2a2f65abd4606b4b45dc53b149
7
+ data.tar.gz: 21161df236b8079fb8511bac24f5a3c87e4054803d18f7811a0d46a68941595ed4a5de59517e856c1e4a9ce8f1ef9022eeebf7e67a7dc4b9154303e684ebb09d
data/.rubocop.yml CHANGED
@@ -15,6 +15,9 @@ Style/Alias:
15
15
  Style/ClassAndModuleChildren:
16
16
  Enabled: false
17
17
 
18
+ Style/DateTime:
19
+ Enabled: false
20
+
18
21
  Style/FileName:
19
22
  Exclude:
20
23
  - lib/evil-client.rb
data/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog], and this project adheres
5
5
  to [Semantic Versioning].
6
6
 
7
+ ## [2.1.0] [WIP]
8
+
9
+ ### Added
10
+
11
+ - Class `Evil::Client::Model` (nepalez)
12
+ Describes standalone model with `.option`, `.let`, and `.validate` extracted
13
+ from `Evil::Client::Settings`.
14
+
15
+ - Module `Evil::Client::Dictionary` (nepalez)
16
+ Describes a yaml dictionary-based collection of items
17
+
18
+ - Helper method `extend` to inject a model into another model, or settings (nepalez)
19
+
20
+ ```ruby
21
+ operation :update_user do
22
+ extend User # takes option-s, let-s, and validate-s from User
23
+ end
24
+ ```
25
+
26
+ - Method to pass response handling to parent scopes by @Envek ([#21](https://github.com/evilmartians/evil-client/pull/21]))
27
+
28
+ Allow to handle specific cases in operations and common cases in parent scopes.
29
+
30
+ ```ruby
31
+ scope :entities do
32
+ operation :create do
33
+ response(409) do |_, _, (data, *)|
34
+ super! unless data["errorCode"] == "201"
35
+ raise YourAPI::AlreadyExists, data["errorMessage"]
36
+ end
37
+ end
38
+
39
+ response(409) do |_, _, (data, *)|
40
+ raise YourAPI::Error, data.dig["errorsMessage"]
41
+ end
42
+ end
43
+ ```
44
+
45
+ ### Fixed
46
+
47
+ - Generation of English error messages in case of using non-English locales
48
+
49
+ ### Changed
50
+
51
+ - Version requirement for tram-policy is limited due to regression in 0.2.4
52
+
53
+ See https://github.com/tram-rb/tram-policy/commit/874c8f61399dbe174c158fec729d16c2b1ffb2fd#r26432444
54
+
55
+
7
56
  ## [2.0.0] [2017-09-02]
8
57
 
9
58
  ### Changed
@@ -381,11 +430,13 @@ formats will be added.
381
430
  response :not_found, 404, format: "json", raise: true
382
431
  ```
383
432
 
384
- [1.1.0]: https://github.com/evilmartians/evil-client/compare/v1.0.0...v1.1.0
385
- [1.0.0]: https://github.com/evilmartians/evil-client/compare/v0.3.3...v1.0.0
386
- [0.3.3]: https://github.com/evilmartians/evil-client/compare/v0.3.2...v0.3.3
387
- [0.3.2]: https://github.com/evilmartians/evil-client/compare/v0.3.1...v0.3.2
388
- [0.3.1]: https://github.com/evilmartians/evil-client/compare/v0.3.0...v0.3.1
389
433
  [Keep a Changelog]: http://keepachangelog.com/
390
434
  [Semantic Versioning]: http://semver.org/
391
435
  [dry-initializer]: http://github.com/dry-rb/dry-initalizer
436
+ [0.3.1]: https://github.com/evilmartians/evil-client/compare/v0.3.0...v0.3.1
437
+ [0.3.2]: https://github.com/evilmartians/evil-client/compare/v0.3.1...v0.3.2
438
+ [0.3.3]: https://github.com/evilmartians/evil-client/compare/v0.3.2...v0.3.3
439
+ [1.0.0]: https://github.com/evilmartians/evil-client/compare/v0.3.3...v1.0.0
440
+ [1.1.0]: https://github.com/evilmartians/evil-client/compare/v1.0.0...v1.1.0
441
+ [2.0.0]: https://github.com/evilmartians/evil-client/compare/v1.1.0...v2.0.0
442
+ [2.1.0]: https://github.com/evilmartians/evil-client/compare/v2.0.0...v2.1.0
@@ -32,6 +32,31 @@ response 400, 422 do |_status, *|
32
32
  end
33
33
  ```
34
34
 
35
+ In case if you want to implement hierarchical processing of errors from more specific to certain operations or scopes to common errors of whole API, you can call `super!` method from response handler when you want to delegate handling to parent scope:
36
+
37
+ ```ruby
38
+ class YourAPI < Evil::Client
39
+ scope :entities do
40
+ operation :create do
41
+ response(409) do |_, _, body|
42
+ data = JSON.parse(body.first)
43
+ case data.dig("errors", 0, "errorId")
44
+ when 35021
45
+ raise YourAPI::AlreadyExists, data.dig("errors", 0, "message")
46
+ else
47
+ super!
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ response(409) do |_, _, body|
54
+ data = JSON.parse(body.first)
55
+ raise EbayAPI::Error, data.dig("errors", 0, "message")
56
+ end
57
+ end
58
+ ```
59
+
35
60
  When you use client-specific [middleware], the `response` block will receive the result already processed by the whole middleware stack. The helper will serve a final step of its handling. Its result wouldn't be processed further in any way.
36
61
 
37
62
  If a remote API will respond with a status, not defined for the operation, the `Evil::Client::ResponseError` will be risen. The exception carries both the response, and all its parts (status, headers, and body).
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 = "2.0.0"
3
+ gem.version = "2.1.0"
4
4
  gem.author = ["Andrew Kozin (nepalez)", "Ravil Bairamgalin (brainopia)"]
5
5
  gem.email = ["andrew.kozin@gmail.com", "nepalez@evilmartians.com"]
6
6
  gem.homepage = "https://github.com/evilmartians/evil-client"
@@ -13,10 +13,10 @@ Gem::Specification.new do |gem|
13
13
 
14
14
  gem.required_ruby_version = "~> 2.3"
15
15
 
16
- gem.add_runtime_dependency "dry-initializer", "~> 2.0.0"
17
- gem.add_runtime_dependency "tram-policy", "~> 0.2.1"
16
+ gem.add_runtime_dependency "dry-initializer", "~> 2.1"
18
17
  gem.add_runtime_dependency "mime-types", "~> 3.1"
19
18
  gem.add_runtime_dependency "rack", "~> 2"
19
+ gem.add_runtime_dependency "tram-policy", "~> 0.2.2", "<= 0.2.3"
20
20
 
21
21
  gem.add_development_dependency "rake", ">= 10"
22
22
  gem.add_development_dependency "rspec", "~> 3.0"
data/lib/evil/client.rb CHANGED
@@ -30,6 +30,7 @@ module Evil
30
30
  require_relative "client/chaining"
31
31
  require_relative "client/options"
32
32
  require_relative "client/policy"
33
+ require_relative "client/model"
33
34
  require_relative "client/settings"
34
35
  require_relative "client/schema"
35
36
  require_relative "client/container"
@@ -37,6 +38,7 @@ module Evil
37
38
  require_relative "client/connection"
38
39
  require_relative "client/formatter"
39
40
  require_relative "client/resolver"
41
+ require_relative "client/dictionary"
40
42
 
41
43
  include Chaining
42
44
 
@@ -59,12 +61,8 @@ module Evil
59
61
  # Sets a custom connection, or resets it to a default one
60
62
  #
61
63
  # @param [#call, nil] connection
62
- # @return [self]
63
64
  #
64
- def connection=(connection)
65
- @connection = connection
66
- self
67
- end
65
+ attr_writer :connection
68
66
 
69
67
  # Schema for the root scope of the client
70
68
  #
@@ -106,7 +104,6 @@ module Evil
106
104
  #
107
105
  def logger=(logger)
108
106
  @scope.logger = logger
109
- self
110
107
  end
111
108
 
112
109
  # Operations defined at the root of the client
@@ -141,6 +138,17 @@ module Evil
141
138
  @scope.options
142
139
  end
143
140
 
141
+ # Human-readable representation of the client
142
+ #
143
+ # @return [String]
144
+ #
145
+ def inspect
146
+ vars = options.map { |k, v| "@#{k}=#{v}" }.join(", ")
147
+ "#<#{self.class}:#{format('0x%014x', object_id)} #{vars}>"
148
+ end
149
+ alias to_s inspect
150
+ alias to_str inspect
151
+
144
152
  private
145
153
 
146
154
  def initialize(**options)
@@ -0,0 +1,77 @@
1
+ class Evil::Client
2
+ # Class-level methods
3
+ module Dictionary
4
+ include Enumerable
5
+
6
+ # Exception to be risen when item cannot be found in a dictionary
7
+ Error = Class.new(ArgumentError)
8
+
9
+ # Raw data for the dictionary items
10
+ # @return [String]
11
+ def raw
12
+ @raw ||= []
13
+ end
14
+
15
+ # List of the dictionary items
16
+ # @return [Array<Evil::Client::Dictionary>]
17
+ def all
18
+ @all ||= raw.map { |item| new(item) }
19
+ end
20
+
21
+ # Iterates by dictionary items
22
+ # @return [Enumerator<Evil::Client::Dictionary>]
23
+ def each
24
+ block_given? ? all.each { |item| yield(item) } : all.to_enum
25
+ end
26
+
27
+ # Calls the item and raises when it is not in the dictionary
28
+ #
29
+ # @param [Evil::Client::Dictionary] item
30
+ # @return [Evil::Client::Dictionary]
31
+ # @raise [Evil::Client::Dictionary::Error]
32
+ #
33
+ def call(item)
34
+ return item if all.include? item
35
+ raise Error, "#{item} is absent in the dictionary #{self}"
36
+ end
37
+
38
+ # Alias for [.call]
39
+ #
40
+ # @param [Evil::Client::Dictionary] item
41
+ # @return [Evil::Client::Dictionary]
42
+ # @raise [Evil::Client::Dictionary::Error]
43
+ #
44
+ def [](item)
45
+ call(item)
46
+ end
47
+
48
+ class << self
49
+ # Loads [#raw] dictionary from YAML config file
50
+ #
51
+ # @param [String] path
52
+ # @return [self]
53
+ #
54
+ def [](path)
55
+ file, paths = path.to_s.split("#")
56
+ list = YAML.load_file(file)
57
+ keys = paths.to_s.split("/").map(&:to_sym)
58
+ @raw = keys.any? ? Hash(list).dig(*keys) : list
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def extended(klass)
65
+ super
66
+ klass.send :instance_variable_set, :@raw, @raw.to_a
67
+ @raw = nil
68
+ end
69
+
70
+ def included(klass)
71
+ super
72
+ klass.send :define_method, :raw, &@raw.method(:to_a)
73
+ @raw = nil
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,126 @@
1
+ class Evil::Client
2
+ #
3
+ # Data structure with validators and memoizers
4
+ #
5
+ class Model
6
+ extend Dry::Initializer
7
+
8
+ @policy = Policy
9
+
10
+ class << self
11
+ # @!method options(key, type = nil, opts = {})
12
+ # Creates or updates the settings' initializer
13
+ #
14
+ # @see [http://dry-rb.org/gems/dry-initializer]
15
+ #
16
+ # @param [#to_sym] key Symbolic name of the option
17
+ # @param [#call] type (nil) Type coercer for the option
18
+ # @option opts [#call] :type Another way to assign type coercer
19
+ # @option opts [#call] :default Proc containing default value
20
+ # @option opts [Boolean] :optional Whether it can be missed
21
+ # @option opts [#to_sym] :as The name of settings variable
22
+ # @option opts [false, :private, :protected] :reader Reader method type
23
+ # @return [self]
24
+ #
25
+ def option(key, type = nil, as: key.to_sym, **opts)
26
+ NameError.check!(as)
27
+ super
28
+ self
29
+ end
30
+ undef_method :param # model initializes with [#options] only
31
+
32
+ # Creates or reloads memoized attribute
33
+ #
34
+ # @param [#to_sym] key The name of the attribute
35
+ # @param [Proc] block The body of new attribute
36
+ # @return [self]
37
+ #
38
+ def let(key, &block)
39
+ NameError.check!(key)
40
+ lets[key.to_sym] = block
41
+
42
+ define_method(key) do
43
+ instance_variable_get(:"@#{key}") ||
44
+ instance_variable_set(:"@#{key}", instance_exec(&block))
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ # Definitions for virtual attributes
51
+ #
52
+ # @return [Hash<Symbol, Proc>]
53
+ #
54
+ def lets
55
+ @lets ||= {}
56
+ end
57
+
58
+ # Policy object for model instances
59
+ #
60
+ # @return [Evil::Client::Policy]
61
+ #
62
+ def policy
63
+ @policy ||= superclass.policy.for(self)
64
+ end
65
+
66
+ # Add validation rule to the [#policy]
67
+ #
68
+ # @param [Proc] block The body of new attribute
69
+ # @return [self]
70
+ #
71
+ def validate(&block)
72
+ policy.validate(&block)
73
+ self
74
+ end
75
+
76
+ # Merges [.option]-s, virtual attributes [.let] and [.validation]-s
77
+ # from another model into the current one.
78
+ #
79
+ # @param [Evil::Client::Model] other
80
+ # @return [self]
81
+ #
82
+ # rubocop: disable Metrics/AbcSize
83
+ def extend(other)
84
+ return super if other.instance_of? Module
85
+
86
+ unless other.ancestors.include? Evil::Client::Model
87
+ raise TypeError, "#{other} is not a subclass of Evil::Client::Model"
88
+ end
89
+
90
+ other.dry_initializer.options.each do |definition|
91
+ option definition.source, definition.options
92
+ end
93
+
94
+ other.lets.each { |key, block| let(key, &block) }
95
+ other.policy.all.each { |validator| policy.local << validator }
96
+ end
97
+ # rubocop: enable Metrics/AbcSize
98
+
99
+ # Model instance constructor
100
+ #
101
+ # @param [Hash] op Model options
102
+ # @return [Evil::Client::Model]
103
+ #
104
+ def new(op = {})
105
+ op = Hash(op).each_with_object({}) { |(k, v), obj| obj[k.to_sym] = v }
106
+ super(op).tap { |item| in_english { policy[item].validate! } }
107
+ rescue StandardError => error
108
+ raise ValidationError, error.message
109
+ end
110
+ alias call new
111
+ alias [] call
112
+
113
+ private
114
+
115
+ def in_english(&block)
116
+ unless I18n.available_locales.include?(:en)
117
+ available_locales = I18n.available_locales
118
+ I18n.available_locales += %i[en]
119
+ end
120
+ I18n.with_locale(:en, &block)
121
+ ensure
122
+ I18n.available_locales = available_locales if available_locales
123
+ end
124
+ end
125
+ end
126
+ end
@@ -4,29 +4,29 @@ class Evil::Client
4
4
  #
5
5
  class Policy < Tram::Policy
6
6
  class << self
7
- # Subclasses itself for a settings class
7
+ # Subclasses itself for a model class
8
8
  #
9
- # @param [Class] settings Settings class to validate
9
+ # @param [Class] model Settings class to validate
10
10
  # @return [Class]
11
11
  #
12
- def for(settings)
12
+ def for(model)
13
13
  Class.new(self).tap do |klass|
14
- klass.send :instance_variable_set, :@settings, settings
14
+ klass.send :instance_variable_set, :@model, model
15
15
  end
16
16
  end
17
17
 
18
- # Reference to the settings class whose instances validates the policy
18
+ # Reference to the model whose instances are validated by the policy
19
19
  #
20
20
  # @return [Class, nil]
21
21
  #
22
- attr_reader :settings
22
+ attr_reader :model
23
23
 
24
- # Delegates the name of the policy to the name of checked settings
24
+ # Delegates the name of the policy to the name of checked model
25
25
  #
26
26
  # @return [String, nil]
27
27
  #
28
28
  def name
29
- "#{settings}.policy"
29
+ "#{model}.policy"
30
30
  end
31
31
  alias_method :to_s, :name
32
32
  alias_method :to_sym, :name
@@ -36,21 +36,21 @@ class Evil::Client
36
36
 
37
37
  def scope
38
38
  @scope ||= %i[evil client errors] << \
39
- Tram::Policy::Inflector.underscore(settings.to_s)
39
+ Tram::Policy::Inflector.underscore(model.to_s)
40
40
  end
41
41
  end
42
42
 
43
43
  # An instance of settings to be checked by the policy
44
- param :settings
44
+ param :model
45
45
 
46
46
  private
47
47
 
48
48
  def respond_to_missing?(name, *)
49
- settings.respond_to?(name)
49
+ model.respond_to?(name)
50
50
  end
51
51
 
52
52
  def method_missing(*args)
53
- respond_to_missing?(*args) ? settings.__send__(*args) : super
53
+ respond_to_missing?(*args) ? model.__send__(*args) : super
54
54
  end
55
55
  end
56
56
  end
@@ -46,7 +46,7 @@ class Evil::Client
46
46
  yield.tap do |obj|
47
47
  logger&.debug(self.class) { "resolved #{self} to #{obj.inspect}" }
48
48
  end
49
- rescue => err
49
+ rescue StandardError => err
50
50
  logger&.error(self.class) { "failed to resolve #{self}: #{err.message}" }
51
51
  raise
52
52
  end
@@ -31,7 +31,7 @@ class Evil::Client
31
31
 
32
32
  left = __stringify_keys__(left)
33
33
  right = __stringify_keys__(right)
34
- right.keys.each { |key| left[key] = __deep_merge__ left[key], right[key] }
34
+ right.each_key { |key| left[key] = __deep_merge__ left[key], right[key] }
35
35
 
36
36
  left
37
37
  end
@@ -6,6 +6,9 @@ class Evil::Client
6
6
  class Resolver::Response < Resolver
7
7
  private
8
8
 
9
+ PROCESSING_DONE = Object.new
10
+ SKIP_RESPONSE = Object.new
11
+
9
12
  def initialize(schema, settings, response)
10
13
  @__response__ = Array response
11
14
  super schema, settings, :responses, @__response__.first.to_i
@@ -13,14 +16,20 @@ class Evil::Client
13
16
 
14
17
  def __call__
15
18
  super do
16
- __check_status__
17
- instance_exec(*@__response__, &__blocks__.last)
19
+ catch(PROCESSING_DONE) do
20
+ __blocks__.reverse_each do |block|
21
+ catch(SKIP_RESPONSE) do
22
+ throw(PROCESSING_DONE, instance_exec(*@__response__, &block))
23
+ end
24
+ end
25
+ # We're here if 1) no blocks or 2) all blocks skipped processing
26
+ raise ResponseError.new(@__schema__, @__settings__, @__response__)
27
+ end
18
28
  end
19
29
  end
20
30
 
21
- def __check_status__
22
- return if __blocks__.any?
23
- raise ResponseError.new(@__schema__, @__settings__, @__response__)
31
+ def super!
32
+ throw SKIP_RESPONSE
24
33
  end
25
34
  end
26
35
  end
@@ -20,7 +20,7 @@ class Evil::Client
20
20
 
21
21
  def __uri__(path)
22
22
  URI path
23
- rescue => error
23
+ rescue StandardError => error
24
24
  raise __definition_error__(error.message)
25
25
  end
26
26
 
@@ -60,8 +60,8 @@ class Evil::Client
60
60
 
61
61
  # Adds an option to the [#settings] class
62
62
  #
63
- # @param (see Evil::Client::Settings.option)
64
- # @option (see Evil::Client::Settings.option)
63
+ # @param (see Evil::Client::Model.option)
64
+ # @option (see Evil::Client::Model.option)
65
65
  # @return [self]
66
66
  #
67
67
  def option(key, type = nil, **opts)
@@ -71,7 +71,7 @@ class Evil::Client
71
71
 
72
72
  # Adds a memoized method to the [#settings] class
73
73
  #
74
- # @param (see Evil::Client::Settings.let)
74
+ # @param (see Evil::Client::Model.let)
75
75
  # @return [self]
76
76
  #
77
77
  def let(key, &block)
@@ -81,7 +81,7 @@ class Evil::Client
81
81
 
82
82
  # Adds validator to the [#settings] class
83
83
  #
84
- # @param (see Evil::Client::Settings.validate)
84
+ # @param (see Evil::Client::Model.validate)
85
85
  # @return [self]
86
86
  #
87
87
  def validate(&block)
@@ -2,11 +2,8 @@ class Evil::Client
2
2
  #
3
3
  # Container for settings assigned to some operation or scope.
4
4
  #
5
- class Settings
5
+ class Settings < Model
6
6
  Names.clean(self) # Remove unnecessary methods from the instance
7
- extend ::Dry::Initializer
8
-
9
- @policy = Policy
10
7
 
11
8
  class << self
12
9
  # Subclasses itself for a given schema
@@ -37,54 +34,6 @@ class Evil::Client
37
34
  alias_method :to_str, :name
38
35
  alias_method :inspect, :name
39
36
 
40
- # Only options can be defined for the settings container
41
- # @private
42
- def param(*args)
43
- option(*args)
44
- end
45
-
46
- # Creates or updates the settings' initializer
47
- #
48
- # @see [http://dry-rb.org/gems/dry-initializer]
49
- #
50
- # @param [#to_sym] key Symbolic name of the option
51
- # @param [#call] type Type coercer for the option
52
- # @option opts [#call] :type Another way to assign type coercer
53
- # @option opts [#call] :default Proc containing default value
54
- # @option opts [Boolean] :optional Whether it can be missed
55
- # @option opts [#to_sym] :as The name of settings variable
56
- # @option opts [false, :private, :protected] :reader Reader method type
57
- # @return [self]
58
- #
59
- def option(key, type = nil, as: key.to_sym, **opts)
60
- NameError.check!(as)
61
- super
62
- self
63
- end
64
-
65
- # Creates or reloads memoized attribute
66
- #
67
- # @param [#to_sym] key The name of the attribute
68
- # @param [Proc] block The body of new attribute
69
- # @return [self]
70
- #
71
- def let(key, &block)
72
- NameError.check!(key)
73
- define_method(key) do
74
- instance_variable_get(:"@#{key}") ||
75
- instance_variable_set(:"@#{key}", instance_exec(&block))
76
- end
77
- self
78
- end
79
-
80
- # Policy class that collects all the necessary validators
81
- #
82
- # @return [Class] a subclass of [Tram::Policy] named after the scope
83
- #
84
- def policy
85
- @policy ||= superclass.policy.for(self)
86
- end
87
-
88
37
  # Add validation rule to the [#policy]
89
38
  #
90
39
  # @param [Proc] block The body of new attribute
@@ -101,22 +50,12 @@ class Evil::Client
101
50
  # @param [Hash<#to_sym, Object>, nil] opts
102
51
  # @return [Evil::Client::Settings]
103
52
  #
104
- def new(logger, opts = {})
105
- logger&.debug(self) { "initializing with options #{opts}..." }
106
- opts = Hash(opts).each_with_object({}) { |(k, v), o| o[k.to_sym] = v }
107
- in_english { super logger, opts }
108
- rescue => error
109
- raise ValidationError, error.message
110
- end
111
-
112
- private
113
-
114
- def in_english(&block)
115
- available_locales = I18n.available_locales
116
- I18n.available_locales = %i[en]
117
- I18n.with_locale(:en, &block)
118
- ensure
119
- I18n.available_locales = available_locales
53
+ def new(logger, op = {})
54
+ logger&.debug(self) { "initializing with options #{op}..." }
55
+ super(op).tap do |item|
56
+ item.logger = logger
57
+ logger&.debug(item) { "initialized" }
58
+ end
120
59
  end
121
60
  end
122
61
 
@@ -160,14 +99,5 @@ class Evil::Client
160
99
  end
161
100
  alias_method :to_str, :inspect
162
101
  alias_method :to_s, :inspect
163
-
164
- private
165
-
166
- def initialize(logger, **options)
167
- super(options)
168
- @logger = logger
169
- self.class.policy[self].validate!
170
- logger&.debug(self) { "initialized" }
171
- end
172
102
  end
173
103
  end
@@ -0,0 +1,4 @@
1
+ # Config file for testing Evil::Client::Dictionary
2
+ ---
3
+ - ONE
4
+ - TWO
@@ -14,3 +14,7 @@ en:
14
14
  users:
15
15
  filter:
16
16
  filter_given: You should define some filter with either name, email, or id
17
+ test/model:
18
+ name_present: "The user has no name"
19
+ test/other:
20
+ empty_name: "name is empty"
@@ -0,0 +1,58 @@
1
+ RSpec.describe Evil::Client::Dictionary do
2
+ shared_examples :a_dictionary do |scope|
3
+ before do
4
+ class Test::Dictionary < String
5
+ def initialize(value)
6
+ super(value.downcase)
7
+ end
8
+ end
9
+ end
10
+
11
+ let(:klass) { Test::Dictionary }
12
+
13
+ describe "#all (#{scope})" do
14
+ subject { klass.all }
15
+
16
+ it "returns all items from a dictionary" do
17
+ expect(subject).to eq %w[one two]
18
+ end
19
+ end
20
+
21
+ describe "#each (#{scope})" do
22
+ subject { klass.map(&:reverse) }
23
+
24
+ it "iterates over dictionary items" do
25
+ expect(subject).to eq %w[eno owt]
26
+ end
27
+ end
28
+
29
+ describe "#call (#{scope})" do
30
+ it "returns dictionary item" do
31
+ expect(klass.call("one")).to eq "one"
32
+ end
33
+
34
+ it "raises when the item not in the dictionary" do
35
+ expect { klass.call "ONE" }
36
+ .to raise_error Evil::Client::Dictionary::Error
37
+ end
38
+ end
39
+ end
40
+
41
+ it_behaves_like :a_dictionary, "when class extended by the module" do
42
+ before do
43
+ class Test::Dictionary < String
44
+ extend Evil::Client::Dictionary["spec/fixtures/config.yml"]
45
+ end
46
+ end
47
+ end
48
+
49
+ it_behaves_like :a_dictionary, "when singleton class includes the module" do
50
+ before do
51
+ class Test::Dictionary < String
52
+ class << self
53
+ include Evil::Client::Dictionary["spec/fixtures/config.yml"]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,133 @@
1
+ RSpec.describe Evil::Client::Model do
2
+ before { class Test::Model < described_class; end }
3
+
4
+ let(:model) { klass.new(options) }
5
+ let(:klass) { Test::Model }
6
+ let(:options) { { "id" => 42, "name" => "Andrew" } }
7
+ let(:dsl_methods) do
8
+ %i[options datetime logger scope basic_auth key_auth token_auth]
9
+ end
10
+
11
+ describe ".policy" do
12
+ subject { klass.policy }
13
+
14
+ it "subclasses Evil::Client::Policy" do
15
+ expect(subject.superclass).to eq described_class.policy
16
+ expect(described_class.policy.superclass).to eq Tram::Policy
17
+ end
18
+
19
+ it "refers back to the model" do
20
+ expect(subject.model).to eq klass
21
+ end
22
+ end
23
+
24
+ describe ".option" do
25
+ it "is defined by Dry::Initializer DSL" do
26
+ expect(klass).to be_a Dry::Initializer
27
+ end
28
+
29
+ it "fails when method name is reserved for DSL" do
30
+ dsl_methods.each do |name|
31
+ expect { klass.option name }
32
+ .to raise_error Evil::Client::NameError
33
+ end
34
+ end
35
+
36
+ it "allows the option to be renamed" do
37
+ expect { klass.option :basic_auth, as: :something }.not_to raise_error
38
+ end
39
+ end
40
+
41
+ describe ".let" do
42
+ before do
43
+ klass.option :id
44
+ klass.let(:square_id) { id**2 }
45
+ end
46
+
47
+ subject { model.square_id }
48
+
49
+ it "adds the corresponding memoizer to the instance" do
50
+ expect(subject).to eq(42**2)
51
+ end
52
+
53
+ it "fails when method name is reserved for DSL" do
54
+ dsl_methods.each do |name|
55
+ expect { klass.let(name) { 0 } }
56
+ .to raise_error Evil::Client::NameError
57
+ end
58
+ end
59
+ end
60
+
61
+ describe ".validate" do
62
+ before do
63
+ klass.option :name
64
+ klass.validate { errors.add :name_present if name.to_s == "" }
65
+ end
66
+
67
+ let(:options) { { "name" => "" } }
68
+
69
+ it "adds validation for an instance" do
70
+ # see spec/fixtures/locale/en.yml
71
+ expect { model }
72
+ .to raise_error(Evil::Client::ValidationError, /The user has no name/)
73
+ end
74
+ end
75
+
76
+ describe ".new" do
77
+ subject { model }
78
+
79
+ context "with wrong options" do
80
+ before { klass.option :user, as: :customer }
81
+
82
+ it "raises Evil::Client::ValidationError" do
83
+ expect { subject }.to raise_error Evil::Client::ValidationError, /user/
84
+ end
85
+ end
86
+ end
87
+
88
+ describe ".extend" do
89
+ before do
90
+ class Test::Other < described_class
91
+ option :first_name, optional: true
92
+ option :last_name, optional: true
93
+
94
+ let(:name) { [first_name, last_name].compact.join(" ") }
95
+
96
+ validate { errors.add :empty_name if name == "" }
97
+ end
98
+
99
+ class Test::Model < described_class
100
+ extend Test::Other
101
+ option :email, optional: true
102
+ end
103
+ end
104
+
105
+ let(:options) do
106
+ { first_name: "Joe", last_name: "Doe", email: "joe@example.com" }
107
+ end
108
+
109
+ subject { model }
110
+
111
+ it "behaves like a model" do
112
+ expect(subject).to be_a klass
113
+ expect(subject.email).to eq "joe@example.com"
114
+ end
115
+
116
+ it "injects options from the other model" do
117
+ expect(subject.first_name).to eq "Joe"
118
+ expect(subject.last_name).to eq "Doe"
119
+ end
120
+
121
+ it "injects memoizers from the other model" do
122
+ expect(subject.name).to eq "Joe Doe"
123
+ end
124
+
125
+ context "with invalid options" do
126
+ let(:options) { { email: "joe@example.com" } }
127
+
128
+ it "injects validators from the other model" do
129
+ expect { subject }.to raise_error(StandardError, /name/)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -22,10 +22,10 @@ RSpec.describe Evil::Client::Policy do
22
22
  end
23
23
 
24
24
  it "keeps reference to the settings" do
25
- expect(subject.settings).to eq settings
25
+ expect(subject.model).to eq settings
26
26
  end
27
27
 
28
- it "takes the name from settings clsass" do
28
+ it "takes the name from settings class" do
29
29
  expect(subject.name).to eq "Foo.policy"
30
30
  end
31
31
  end
@@ -5,16 +5,18 @@ RSpec.describe Evil::Client::Resolver::Response, ".call" do
5
5
  let(:logger) { Logger.new log }
6
6
  let(:response) { [201, { "Content-Language" => "en" }, ["success"]] }
7
7
 
8
+ let(:root_response_handler) { proc { |*args| args } }
8
9
  let(:root_schema) do
9
10
  double :my_parent_schema,
10
- definitions: { responses: { 201 => proc { |*args| args } } },
11
+ definitions: { responses: { 201 => root_response_handler } },
11
12
  parent: nil
12
13
  end
13
14
 
15
+ let(:response_handler) { proc { |_, _, body| body.first } }
14
16
  let(:schema) do
15
17
  double :my_schema,
16
18
  definitions: {
17
- responses: { 201 => proc { |_, _, body| body.first } }
19
+ responses: { 201 => response_handler }
18
20
  },
19
21
  parent: root_schema
20
22
  end
@@ -53,6 +55,24 @@ RSpec.describe Evil::Client::Resolver::Response, ".call" do
53
55
  end
54
56
  end
55
57
 
58
+ context "when root definition reloaded but schema handler skips response" do
59
+ let(:response_handler) do
60
+ proc { |_, _, _| super! }
61
+ end
62
+
63
+ it "applies root schema to response" do
64
+ expect(subject).to eq response
65
+ end
66
+
67
+ context "when root definition skips response handling too" do
68
+ let(:root_response_handler) { proc { super! } }
69
+
70
+ it "raises Evil::Client::ResponseError" do
71
+ expect { subject }.to raise_error Evil::Client::ResponseError
72
+ end
73
+ end
74
+ end
75
+
56
76
  context "when no definitions was given for the status" do
57
77
  let(:response) { [202, { "Content-Language" => "en" }, ["success"]] }
58
78
 
@@ -26,11 +26,12 @@ RSpec.describe Evil::Client::Settings do
26
26
  subject { klass.policy }
27
27
 
28
28
  it "subclasses Evil::Client::Policy" do
29
- expect(subject.superclass).to eq Evil::Client::Policy
29
+ expect(subject.superclass).to eq described_class.policy
30
+ expect(described_class.policy.superclass).to eq Evil::Client::Policy
30
31
  end
31
32
 
32
33
  it "refers back to the settings" do
33
- expect(subject.settings).to eq klass
34
+ expect(subject.model).to eq klass
34
35
  end
35
36
  end
36
37
 
@@ -44,7 +45,7 @@ RSpec.describe Evil::Client::Settings do
44
45
  end
45
46
 
46
47
  it "refers back to the settings" do
47
- expect(subject.settings).to eq scope_klass
48
+ expect(subject.model).to eq scope_klass
48
49
  end
49
50
  end
50
51
  end
@@ -66,22 +67,9 @@ RSpec.describe Evil::Client::Settings do
66
67
  end
67
68
  end
68
69
 
69
- describe ".param" do
70
- before do
71
- klass.param :id, optional: true
72
- klass.param :email, optional: true
73
- end
74
-
75
- subject { settings.options }
76
-
77
- it "acts like .option" do
78
- expect(subject).to eq id: 42
79
- end
80
- end
81
-
82
70
  describe ".let" do
83
71
  before do
84
- klass.param :id
72
+ klass.option :id
85
73
  klass.let(:square_id) { id**2 }
86
74
  end
87
75
 
@@ -101,7 +89,7 @@ RSpec.describe Evil::Client::Settings do
101
89
 
102
90
  describe ".validate" do
103
91
  before do
104
- klass.param :name
92
+ klass.option :name
105
93
  klass.validate { errors.add :name_present if name.to_s == "" }
106
94
  end
107
95
 
@@ -207,7 +195,7 @@ RSpec.describe Evil::Client::Settings do
207
195
  end
208
196
 
209
197
  describe "#datetime" do
210
- let(:time) { DateTime.parse "2017-07-21 16:58:00 UTC" }
198
+ let(:time) { Time.parse "2017-07-21 16:58:00 UTC" }
211
199
  subject { settings.datetime value }
212
200
 
213
201
  context "with a parceable string" do
metadata CHANGED
@@ -1,157 +1,163 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evil-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kozin (nepalez)
8
8
  - Ravil Bairamgalin (brainopia)
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-09-02 00:00:00.000000000 Z
12
+ date: 2018-01-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
+ name: dry-initializer
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
18
  - - "~>"
18
19
  - !ruby/object:Gem::Version
19
- version: 2.0.0
20
- name: dry-initializer
21
- prerelease: false
20
+ version: '2.1'
22
21
  type: :runtime
22
+ prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 2.0.0
27
+ version: '2.1'
28
28
  - !ruby/object:Gem::Dependency
29
+ name: mime-types
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
32
  - - "~>"
32
33
  - !ruby/object:Gem::Version
33
- version: 0.2.1
34
- name: tram-policy
35
- prerelease: false
34
+ version: '3.1'
36
35
  type: :runtime
36
+ prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 0.2.1
41
+ version: '3.1'
42
42
  - !ruby/object:Gem::Dependency
43
+ name: rack
43
44
  requirement: !ruby/object:Gem::Requirement
44
45
  requirements:
45
46
  - - "~>"
46
47
  - !ruby/object:Gem::Version
47
- version: '3.1'
48
- name: mime-types
49
- prerelease: false
48
+ version: '2'
50
49
  type: :runtime
50
+ prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '3.1'
55
+ version: '2'
56
56
  - !ruby/object:Gem::Dependency
57
+ name: tram-policy
57
58
  requirement: !ruby/object:Gem::Requirement
58
59
  requirements:
59
60
  - - "~>"
60
61
  - !ruby/object:Gem::Version
61
- version: '2'
62
- name: rack
63
- prerelease: false
62
+ version: 0.2.2
63
+ - - "<="
64
+ - !ruby/object:Gem::Version
65
+ version: 0.2.3
64
66
  type: :runtime
67
+ prerelease: false
65
68
  version_requirements: !ruby/object:Gem::Requirement
66
69
  requirements:
67
70
  - - "~>"
68
71
  - !ruby/object:Gem::Version
69
- version: '2'
72
+ version: 0.2.2
73
+ - - "<="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.2.3
70
76
  - !ruby/object:Gem::Dependency
77
+ name: rake
71
78
  requirement: !ruby/object:Gem::Requirement
72
79
  requirements:
73
80
  - - ">="
74
81
  - !ruby/object:Gem::Version
75
82
  version: '10'
76
- name: rake
77
- prerelease: false
78
83
  type: :development
84
+ prerelease: false
79
85
  version_requirements: !ruby/object:Gem::Requirement
80
86
  requirements:
81
87
  - - ">="
82
88
  - !ruby/object:Gem::Version
83
89
  version: '10'
84
90
  - !ruby/object:Gem::Dependency
91
+ name: rspec
85
92
  requirement: !ruby/object:Gem::Requirement
86
93
  requirements:
87
94
  - - "~>"
88
95
  - !ruby/object:Gem::Version
89
96
  version: '3.0'
90
- name: rspec
91
- prerelease: false
92
97
  type: :development
98
+ prerelease: false
93
99
  version_requirements: !ruby/object:Gem::Requirement
94
100
  requirements:
95
101
  - - "~>"
96
102
  - !ruby/object:Gem::Version
97
103
  version: '3.0'
98
104
  - !ruby/object:Gem::Dependency
105
+ name: rspec-its
99
106
  requirement: !ruby/object:Gem::Requirement
100
107
  requirements:
101
108
  - - "~>"
102
109
  - !ruby/object:Gem::Version
103
110
  version: '1.2'
104
- name: rspec-its
105
- prerelease: false
106
111
  type: :development
112
+ prerelease: false
107
113
  version_requirements: !ruby/object:Gem::Requirement
108
114
  requirements:
109
115
  - - "~>"
110
116
  - !ruby/object:Gem::Version
111
117
  version: '1.2'
112
118
  - !ruby/object:Gem::Dependency
119
+ name: rubocop
113
120
  requirement: !ruby/object:Gem::Requirement
114
121
  requirements:
115
122
  - - "~>"
116
123
  - !ruby/object:Gem::Version
117
124
  version: '0.42'
118
- name: rubocop
119
- prerelease: false
120
125
  type: :development
126
+ prerelease: false
121
127
  version_requirements: !ruby/object:Gem::Requirement
122
128
  requirements:
123
129
  - - "~>"
124
130
  - !ruby/object:Gem::Version
125
131
  version: '0.42'
126
132
  - !ruby/object:Gem::Dependency
133
+ name: timecop
127
134
  requirement: !ruby/object:Gem::Requirement
128
135
  requirements:
129
136
  - - "~>"
130
137
  - !ruby/object:Gem::Version
131
138
  version: '0.9'
132
- name: timecop
133
- prerelease: false
134
139
  type: :development
140
+ prerelease: false
135
141
  version_requirements: !ruby/object:Gem::Requirement
136
142
  requirements:
137
143
  - - "~>"
138
144
  - !ruby/object:Gem::Version
139
145
  version: '0.9'
140
146
  - !ruby/object:Gem::Dependency
147
+ name: webmock
141
148
  requirement: !ruby/object:Gem::Requirement
142
149
  requirements:
143
150
  - - "~>"
144
151
  - !ruby/object:Gem::Version
145
152
  version: '2.1'
146
- name: webmock
147
- prerelease: false
148
153
  type: :development
154
+ prerelease: false
149
155
  version_requirements: !ruby/object:Gem::Requirement
150
156
  requirements:
151
157
  - - "~>"
152
158
  - !ruby/object:Gem::Version
153
159
  version: '2.1'
154
- description:
160
+ description:
155
161
  email:
156
162
  - andrew.kozin@gmail.com
157
163
  - nepalez@evilmartians.com
@@ -200,6 +206,7 @@ files:
200
206
  - lib/evil/client/container.rb
201
207
  - lib/evil/client/container/operation.rb
202
208
  - lib/evil/client/container/scope.rb
209
+ - lib/evil/client/dictionary.rb
203
210
  - lib/evil/client/exceptions/definition_error.rb
204
211
  - lib/evil/client/exceptions/name_error.rb
205
212
  - lib/evil/client/exceptions/response_error.rb
@@ -210,6 +217,7 @@ files:
210
217
  - lib/evil/client/formatter/multipart.rb
211
218
  - lib/evil/client/formatter/part.rb
212
219
  - lib/evil/client/formatter/text.rb
220
+ - lib/evil/client/model.rb
213
221
  - lib/evil/client/names.rb
214
222
  - lib/evil/client/options.rb
215
223
  - lib/evil/client/policy.rb
@@ -240,6 +248,7 @@ files:
240
248
  - spec/features/operation/request_spec.rb
241
249
  - spec/features/operation/response_spec.rb
242
250
  - spec/features/scope/options_spec.rb
251
+ - spec/fixtures/config.yml
243
252
  - spec/fixtures/locales/en.yml
244
253
  - spec/fixtures/test_client.rb
245
254
  - spec/spec_helper.rb
@@ -251,6 +260,7 @@ files:
251
260
  - spec/unit/container/operation_spec.rb
252
261
  - spec/unit/container/scope_spec.rb
253
262
  - spec/unit/container_spec.rb
263
+ - spec/unit/dictionary_spec.rb
254
264
  - spec/unit/exceptions/definition_error_spec.rb
255
265
  - spec/unit/exceptions/name_error_spec.rb
256
266
  - spec/unit/exceptions/response_error_spec.rb
@@ -261,6 +271,7 @@ files:
261
271
  - spec/unit/formatter/part_spec.rb
262
272
  - spec/unit/formatter/text_spec.rb
263
273
  - spec/unit/formatter_spec.rb
274
+ - spec/unit/model_spec.rb
264
275
  - spec/unit/options_spec.rb
265
276
  - spec/unit/policy_spec.rb
266
277
  - spec/unit/resolver/body_spec.rb
@@ -284,7 +295,7 @@ homepage: https://github.com/evilmartians/evil-client
284
295
  licenses:
285
296
  - MIT
286
297
  metadata: {}
287
- post_install_message:
298
+ post_install_message:
288
299
  rdoc_options: []
289
300
  require_paths:
290
301
  - lib
@@ -299,9 +310,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
299
310
  - !ruby/object:Gem::Version
300
311
  version: '0'
301
312
  requirements: []
302
- rubyforge_project:
303
- rubygems_version: 2.6.4
304
- signing_key:
313
+ rubyforge_project:
314
+ rubygems_version: 2.6.14
315
+ signing_key:
305
316
  specification_version: 4
306
317
  summary: Human-friendly DSL for building HTTP(s) clients in Ruby
307
318
  test_files:
@@ -311,6 +322,7 @@ test_files:
311
322
  - spec/features/operation/request_spec.rb
312
323
  - spec/features/operation/response_spec.rb
313
324
  - spec/features/scope/options_spec.rb
325
+ - spec/fixtures/config.yml
314
326
  - spec/fixtures/locales/en.yml
315
327
  - spec/fixtures/test_client.rb
316
328
  - spec/spec_helper.rb
@@ -322,6 +334,7 @@ test_files:
322
334
  - spec/unit/container/operation_spec.rb
323
335
  - spec/unit/container/scope_spec.rb
324
336
  - spec/unit/container_spec.rb
337
+ - spec/unit/dictionary_spec.rb
325
338
  - spec/unit/exceptions/definition_error_spec.rb
326
339
  - spec/unit/exceptions/name_error_spec.rb
327
340
  - spec/unit/exceptions/response_error_spec.rb
@@ -332,6 +345,7 @@ test_files:
332
345
  - spec/unit/formatter/part_spec.rb
333
346
  - spec/unit/formatter/text_spec.rb
334
347
  - spec/unit/formatter_spec.rb
348
+ - spec/unit/model_spec.rb
335
349
  - spec/unit/options_spec.rb
336
350
  - spec/unit/policy_spec.rb
337
351
  - spec/unit/resolver/body_spec.rb