evil-client 2.0.0 → 2.1.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.
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