restify 2.0.2 → 3.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/README.md +31 -5
  4. data/lib/restify/adapter/base.rb +11 -2
  5. data/lib/restify/adapter/ethon.rb +462 -0
  6. data/lib/restify/adapter/loop_lock.rb +124 -0
  7. data/lib/restify/adapter/telemetry.rb +6 -4
  8. data/lib/restify/adapter/typhoeus.rb +48 -23
  9. data/lib/restify/cache.rb +1 -1
  10. data/lib/restify/context.rb +5 -5
  11. data/lib/restify/error.rb +13 -0
  12. data/lib/restify/global.rb +9 -30
  13. data/lib/restify/logging.rb +10 -4
  14. data/lib/restify/processors/base/parsing.rb +13 -1
  15. data/lib/restify/promise.rb +20 -2
  16. data/lib/restify/registry.rb +2 -2
  17. data/lib/restify/relation.rb +9 -17
  18. data/lib/restify/request.rb +2 -2
  19. data/lib/restify/resource.rb +20 -2
  20. data/lib/restify/response.rb +41 -5
  21. data/lib/restify/timeout.rb +11 -5
  22. data/lib/restify/version.rb +3 -3
  23. data/lib/restify/webmock.rb +126 -0
  24. data/lib/restify.rb +5 -1
  25. data/spec/restify/adapter/base_spec.rb +33 -0
  26. data/spec/restify/adapter/ethon_spec.rb +263 -0
  27. data/spec/restify/adapter/loop_lock_spec.rb +144 -0
  28. data/spec/restify/adapter/telemetry_spec.rb +61 -0
  29. data/spec/restify/adapter/typhoeus_spec.rb +26 -0
  30. data/spec/restify/cache_spec.rb +1 -3
  31. data/spec/restify/context_spec.rb +57 -2
  32. data/spec/restify/features/opentelemetry_spec.rb +1 -1
  33. data/spec/restify/features/protocols_spec.rb +28 -0
  34. data/spec/restify/features/redirects_spec.rb +45 -0
  35. data/spec/restify/features/request_errors_spec.rb +1 -1
  36. data/spec/restify/features/response_bodies_spec.rb +45 -0
  37. data/spec/restify/features/response_encoding_spec.rb +82 -0
  38. data/spec/restify/features/response_errors_spec.rb +22 -1
  39. data/spec/restify/features/webmock_spec.rb +92 -1
  40. data/spec/restify/global_spec.rb +118 -1
  41. data/spec/restify/link_spec.rb +13 -3
  42. data/spec/restify/processors/json_spec.rb +36 -0
  43. data/spec/restify/processors/msgpack_spec.rb +36 -0
  44. data/spec/restify/promise_spec.rb +82 -0
  45. data/spec/restify/relation_spec.rb +22 -7
  46. data/spec/restify/request_spec.rb +26 -0
  47. data/spec/restify/resource_spec.rb +6 -0
  48. data/spec/restify/timeout_spec.rb +11 -0
  49. data/spec/restify_spec.rb +1 -1
  50. data/spec/spec_helper.rb +10 -7
  51. data/spec/support/stub_server.rb +4 -4
  52. metadata +27 -105
  53. data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/LICENSE +0 -21
  54. data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/README.md +0 -23
  55. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/CHANGELOG.md +0 -255
  56. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/MIT-LICENSE +0 -20
  57. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/README.rdoc +0 -40
  58. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/CHANGELOG.md +0 -301
  59. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/LICENSE.txt +0 -202
  60. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/README.md +0 -121
  61. data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/LICENSE.txt +0 -22
  62. data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/README.md +0 -48
  63. data/vendor/bundle/ruby/3.4.0/gems/benchmark-0.4.0/README.md +0 -138
  64. data/vendor/bundle/ruby/3.4.0/gems/bigdecimal-3.1.9/LICENSE +0 -56
  65. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/CHANGELOG.md +0 -603
  66. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/LICENSE.txt +0 -21
  67. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/README.md +0 -407
  68. data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/LICENSE +0 -20
  69. data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/README.md +0 -167
  70. data/vendor/bundle/ruby/3.4.0/gems/drb-2.2.1/LICENSE.txt +0 -22
  71. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/CHANGELOG.md +0 -375
  72. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/LICENSE +0 -20
  73. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/README.md +0 -118
  74. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/CHANGELOG.md +0 -473
  75. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE +0 -24
  76. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE.SPECS +0 -22
  77. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/README.md +0 -137
  78. data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/LICENSE.txt +0 -16
  79. data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/README.md +0 -187
  80. data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/MIT-LICENSE +0 -20
  81. data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/README.md +0 -127
  82. data/vendor/bundle/ruby/3.4.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
  83. data/vendor/bundle/ruby/3.4.0/gems/logger-1.7.0/README.md +0 -104
  84. data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/LICENSE +0 -22
  85. data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/README.md +0 -140
  86. data/vendor/bundle/ruby/3.4.0/gems/minitest-5.25.5/README.rdoc +0 -842
  87. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/ChangeLog +0 -368
  88. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/LICENSE +0 -177
  89. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/README.md +0 -302
  90. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/CHANGELOG.md +0 -275
  91. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/LICENSE.md +0 -20
  92. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/README.md +0 -121
  93. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/CHANGELOG.md +0 -193
  94. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/LICENSE +0 -201
  95. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/README.md +0 -68
  96. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/CHANGELOG.md +0 -98
  97. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/LICENSE +0 -201
  98. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/README.md +0 -62
  99. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/CHANGELOG.md +0 -498
  100. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/LICENSE.txt +0 -22
  101. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/README.md +0 -222
  102. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/CHANGELOG.md +0 -1146
  103. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/MIT-LICENSE +0 -20
  104. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/README.md +0 -355
  105. data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/MIT-LICENSE +0 -21
  106. data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/README.rdoc +0 -155
  107. data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/LICENSE +0 -21
  108. data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/README.md +0 -107
  109. data/vendor/bundle/ruby/3.4.0/gems/securerandom-0.4.1/README.md +0 -72
  110. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/CHANGELOG.md +0 -410
  111. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/LICENSE +0 -22
  112. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/README.md +0 -588
  113. data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/LICENSE +0 -19
  114. data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/README.md +0 -406
  115. data/vendor/bundle/ruby/3.4.0/gems/uri-1.0.3/README.md +0 -55
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restify
4
+ module Adapter
5
+ #
6
+ # Manages coordination for exclusive ownership of an event loop,
7
+ # shared between a background thread and threads waiting on results.
8
+ # This way, the event loop can run on waiting threads directly,
9
+ # avoiding costly context switches to the background thread.
10
+ #
11
+ # Waiting threads have priority: the background thread only acquires
12
+ # the loop while no thread is waiting for it, and is interrupted to
13
+ # release it as soon as one is.
14
+ #
15
+ # Waiting threads can give up waiting once their result is
16
+ # available, e.g. because another thread running the loop produced
17
+ # it. Whoever runs the loop must call `#notify` after producing a
18
+ # result.
19
+ #
20
+ class LoopLock
21
+ # @yield Called when a waiting thread needs the background thread to
22
+ # release the loop, e.g. to wake it up from `select`.
23
+ #
24
+ def initialize(&interrupt)
25
+ @interrupt = interrupt
26
+
27
+ reset!
28
+ end
29
+
30
+ # Acquire the loop for a thread waiting on a result.
31
+ #
32
+ # @param timeout [Restify::Timeout, Numeric] Maximum time to wait,
33
+ # derived from the promise timeout of the waiting thread.
34
+ #
35
+ # @yieldreturn [Boolean] Whether the result is available, and
36
+ # the loop is not needed anymore.
37
+ #
38
+ # @return [Boolean] true when the calling thread holds the loop,
39
+ # false when the result is available or the timeout expired
40
+ # first.
41
+ #
42
+ def acquire(timeout, &done)
43
+ timeout = Timeout.new(timeout)
44
+
45
+ background = @mutex.synchronize do
46
+ @waiters += 1
47
+ @background
48
+ end
49
+
50
+ @interrupt&.call if background
51
+
52
+ @mutex.synchronize do
53
+ loop do
54
+ return false if done&.call
55
+
56
+ remaining = timeout.remaining
57
+ return false unless remaining.positive?
58
+
59
+ if free?
60
+ @owner = Thread.current
61
+ return true
62
+ end
63
+
64
+ @changed.wait(@mutex, remaining)
65
+ end
66
+ ensure
67
+ @waiters -= 1
68
+
69
+ # Let the background thread continue.
70
+ @changed.broadcast if @waiters.zero?
71
+ end
72
+ end
73
+
74
+ # Acquire the loop for the background thread. Blocks while any
75
+ # other thread holds the loop or waits for it.
76
+ def acquire_background
77
+ @mutex.synchronize do
78
+ @changed.wait(@mutex) until free? && @waiters.zero?
79
+
80
+ @owner = Thread.current
81
+ @background = true
82
+ end
83
+ end
84
+
85
+ def release
86
+ @mutex.synchronize do
87
+ @owner = nil
88
+ @background = false
89
+ @changed.broadcast
90
+ end
91
+ end
92
+
93
+ def owned?
94
+ @owner.equal?(Thread.current)
95
+ end
96
+
97
+ def notify
98
+ return unless @waiters.positive?
99
+
100
+ @mutex.synchronize { @changed.broadcast }
101
+ end
102
+
103
+ # Forget all threads holding or waiting for the loop, e.g. in a
104
+ # forked child process, where these threads do not exist.
105
+ #
106
+ # Must not be called while other threads use the lock.
107
+ def reset!
108
+ @mutex = Mutex.new
109
+ @changed = ConditionVariable.new
110
+ @owner = nil
111
+ @background = false
112
+ @waiters = 0
113
+ end
114
+
115
+ private
116
+
117
+ # A thread holding the loop can be gone without releasing it, e.g.
118
+ # when killed.
119
+ def free?
120
+ @owner.nil? || !@owner.alive?
121
+ end
122
+ end
123
+ end
124
+ end
@@ -8,16 +8,18 @@ module Restify
8
8
  module Telemetry
9
9
  def call(request)
10
10
  method = request.method.to_s.upcase
11
- uri = URI.parse(request.uri)
12
- name = "#{method} #{uri.scheme}://#{uri.host}:#{uri.port}"
11
+ uri = request.uri
12
+ port = uri.inferred_port
13
+ name = "#{method} #{uri.scheme}://#{uri.host}:#{port}"
13
14
 
15
+ # Remove `nil` as OTEL spans must not contain any nil attributes
14
16
  attributes = {
15
17
  'http.request.method' => method,
16
18
  'server.address' => uri.host,
17
- 'server.port' => uri.port,
19
+ 'server.port' => port,
18
20
  'url.full' => uri.to_s,
19
21
  'url.scheme' => uri.scheme,
20
- }
22
+ }.compact
21
23
 
22
24
  span = tracer.start_span(name, attributes:, kind: :client)
23
25
  OpenTelemetry::Trace.with_span(span) do
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'typhoeus'
4
-
5
- Ethon.logger = Logging.logger[Ethon]
3
+ begin
4
+ require 'typhoeus'
5
+ rescue LoadError => e
6
+ raise LoadError.new(
7
+ "Restify::Adapter::Typhoeus requires the `typhoeus` gem, add it to your Gemfile (#{e.message})",
8
+ )
9
+ end
6
10
 
7
11
  module Restify
8
12
  module Adapter
@@ -23,6 +27,14 @@ module Restify
23
27
  tcp_keepintvl: 5,
24
28
  }.freeze
25
29
 
30
+ # Restify follows relations from URLs in server responses, and
31
+ # libcurl supports far more than HTTP, e.g. `file://` or `scp://`.
32
+ # Therefore only HTTP-like protocols must ever be used, both for
33
+ # the request itself and when following redirects.
34
+ #
35
+ # This is intentionally not configurable.
36
+ PROTOCOLS = %i[http https].freeze
37
+
26
38
  # Patch Hydra to restore the correct OpenTelemetry span when
27
39
  # adding the request, so that the Ethon instrumentation can
28
40
  # properly pick up the context where the Restify request
@@ -41,8 +53,8 @@ module Restify
41
53
  end
42
54
  end
43
55
 
44
- def initialize(sync: false, options: {}, **kwargs)
45
- @hydra = ::Typhoeus::Hydra.new(**kwargs)
56
+ def initialize(sync: false, options: {}, **)
57
+ @hydra = ::Typhoeus::Hydra.new(**)
46
58
  @hydra.extend(EasyOverride)
47
59
 
48
60
  @mutex = Mutex.new
@@ -62,8 +74,11 @@ module Restify
62
74
  req = convert(request, writer)
63
75
 
64
76
  if sync?
65
- @hydra.queue(req)
66
- @hydra.run
77
+ begin
78
+ req.run
79
+ rescue Exception => e # rubocop:disable Lint/RescueException
80
+ writer.reject(e)
81
+ end
67
82
  else
68
83
  debug 'request:add',
69
84
  tag: request.object_id,
@@ -71,7 +86,7 @@ module Restify
71
86
  url: request.uri,
72
87
  timeout: request.timeout
73
88
 
74
- @queue << convert(request, writer)
89
+ @queue << req
75
90
 
76
91
  thread.run unless thread.status
77
92
  end
@@ -88,33 +103,38 @@ module Restify
88
103
  body: request.body,
89
104
  timeout: request.timeout,
90
105
  connecttimeout: request.timeout,
106
+
107
+ # Block accessing and redirecting to non-HTTP protocols:
108
+ protocols: PROTOCOLS,
109
+ redir_protocols: PROTOCOLS,
91
110
  ).tap do |req|
92
111
  req._otel_span = OpenTelemetry::Trace.current_span
93
112
  req._restify_writer = writer
94
113
 
95
114
  req.on_complete do |response|
96
- debug 'request:complete',
97
- tag: request.object_id,
98
- status: response.code,
99
- message: response.return_message,
100
- timeout: response.timed_out?
101
-
102
- if response.timed_out? || response.code.zero?
103
- writer.reject \
104
- Restify::NetworkError.new(request, response.return_message)
105
- else
106
- writer.fulfill convert_back(response, request)
115
+ writer.set do
116
+ debug 'request:complete',
117
+ tag: request.object_id,
118
+ status: response.code,
119
+ message: response.return_message,
120
+ timeout: response.timed_out?
121
+
122
+ if response.timed_out? || response.code.zero?
123
+ raise Restify::NetworkError.new(request, response.return_message)
124
+ end
125
+
126
+ convert_back(response, request)
107
127
  end
108
128
 
109
- # Add all newly queued requests to active hydra, e.g. requests
110
- # queued in a completion callback.
129
+ # Add all newly queued requests to active hydra, e.g.
130
+ # requests queued in a completion callback.
111
131
  dequeue_all
112
132
  end
113
133
  end
114
134
  end
115
135
 
116
136
  def convert_back(response, request)
117
- uri = request.uri
137
+ uri = effective_uri(response, request)
118
138
  status = response.code
119
139
  body = response.body
120
140
  headers = convert_headers(response.headers)
@@ -122,6 +142,11 @@ module Restify
122
142
  ::Restify::Response.new(request, uri, status, headers, body)
123
143
  end
124
144
 
145
+ def effective_uri(response, request)
146
+ url = response.effective_url
147
+ url ? Addressable::URI.parse(url) : request.uri
148
+ end
149
+
125
150
  def convert_headers(headers)
126
151
  return {} unless headers.respond_to?(:each_pair)
127
152
 
@@ -165,7 +190,7 @@ module Restify
165
190
  runs += 1
166
191
  debug 'hydra:completed'
167
192
  rescue StandardError => e
168
- logger.error(e)
193
+ error(e)
169
194
  end
170
195
  ensure
171
196
  debug 'hydra:exit'
data/lib/restify/cache.rb CHANGED
@@ -19,7 +19,7 @@ module Restify
19
19
 
20
20
  private
21
21
 
22
- def match(_request)
22
+ def match(_request) # rubocop:disable Naming/PredicateMethod
23
23
  false
24
24
  end
25
25
 
@@ -34,9 +34,9 @@ module Restify
34
34
  self.uri.join uri
35
35
  end
36
36
 
37
- def inherit(uri, **kwargs)
37
+ def inherit(uri, **)
38
38
  uri ||= self.uri
39
- Context.new(uri, **kwargs, **options)
39
+ Context.new(uri, **, **options)
40
40
  end
41
41
 
42
42
  def process(response)
@@ -47,16 +47,16 @@ module Restify
47
47
  processor.new(context, response).resource
48
48
  end
49
49
 
50
- def request(method, uri, data: nil, headers: {}, **kwargs)
50
+ def request(method, uri, data: nil, headers: {}, **)
51
51
  request = Request.new(
52
52
  headers: default_headers.merge(headers),
53
- **kwargs,
53
+ **,
54
54
  method:,
55
55
  uri: join(uri),
56
56
  data:,
57
57
  )
58
58
 
59
- ret = cache.call(request) {|req| adapter.call(req) }
59
+ ret = cache ? cache.call(request) {|req| adapter.call(req) } : adapter.call(request)
60
60
  ret.then do |response|
61
61
  raise ResponseError.from_code(response) if response.errored?
62
62
 
data/lib/restify/error.rb CHANGED
@@ -13,6 +13,19 @@ module Restify
13
13
  end
14
14
  end
15
15
 
16
+ # A {ParseError} is raised when accessing the data of a resource,
17
+ # whose response body could not be parsed, e.g. invalid JSON. The
18
+ # original error is available as {#cause}.
19
+ #
20
+ class ParseError < StandardError
21
+ attr_reader :response
22
+
23
+ def initialize(response, message)
24
+ @response = response
25
+ super("Could not parse response body for `#{response&.uri}': #{message}")
26
+ end
27
+ end
28
+
16
29
  # A {ResponseError} is returned on a non-successful
17
30
  # response from server. Usually it will either be a
18
31
  # {ClientError} or a {ServerError}.
@@ -2,47 +2,26 @@
2
2
 
3
3
  module Restify
4
4
  module Global
5
- def new(uri, **opts)
6
- context = resolve_context uri, **opts
5
+ def new(uri, **)
6
+ context = resolve_context(uri, **)
7
7
 
8
8
  Relation.new context, context.uri
9
9
  end
10
10
 
11
- def adapter
12
- @adapter ||= begin
13
- Restify::Adapter::Typhoeus.new
14
- end
15
- end
16
-
17
- def adapter=(adapter)
18
- @adapter = adapter
19
- end
20
-
21
- def cache
22
- @cache ||= begin
23
- require 'active_support'
24
- require 'active_support/cache'
25
- Restify::Cache.new store: ActiveSupport::Cache::MemoryStore.new
26
- end
27
- end
28
-
29
- def cache=(cache)
30
- @cache = cache
31
- end
11
+ attr_writer :adapter
12
+ attr_accessor :cache, :logger
32
13
 
33
- def logger
34
- ::Logging.logger[Restify]
14
+ def adapter
15
+ @adapter ||= Restify::Adapter::Ethon.new
35
16
  end
36
17
 
37
- def logger=(logger); end
38
-
39
18
  private
40
19
 
41
- def resolve_context(uri, **opts)
20
+ def resolve_context(uri, **)
42
21
  if uri.is_a? Symbol
43
- Restify::Registry.fetch(uri).inherit(nil, **opts)
22
+ Restify::Registry.fetch(uri).inherit(nil, **)
44
23
  else
45
- Context.new(uri, **opts)
24
+ Context.new(uri, **)
46
25
  end
47
26
  end
48
27
  end
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logging'
4
-
5
3
  module Restify
6
4
  module Logging
7
5
  def logger
8
- @logger ||= ::Logging.logger[self]
6
+ Restify.logger
9
7
  end
10
8
 
11
9
  def debug(message = nil, tag: nil, **kwargs)
12
- logger.debug do
10
+ logger&.debug(_log_name) do
13
11
  [
14
12
  _log_prefix,
15
13
  *Array(tag),
@@ -19,6 +17,14 @@ module Restify
19
17
  end
20
18
  end
21
19
 
20
+ def error(exception)
21
+ logger&.error(_log_name) { exception }
22
+ end
23
+
24
+ def _log_name
25
+ is_a?(Module) ? name : self.class.name
26
+ end
27
+
22
28
  def _log_prefix
23
29
  nil
24
30
  end
@@ -10,7 +10,19 @@ module Restify
10
10
  #
11
11
  module Parsing
12
12
  def load
13
- parse deserialized_body, root: true
13
+ # No data, e.g. when a server sets a content type but sends an
14
+ # empty body.
15
+ return if body.nil? || body.empty?
16
+
17
+ begin
18
+ data = deserialized_body
19
+ rescue StandardError => e
20
+ # Keep the resource, e.g. with relations from headers, but
21
+ # raise on accessing its data.
22
+ return Resource.new(context, response:, error: e)
23
+ end
24
+
25
+ parse(data, root: true)
14
26
  end
15
27
 
16
28
  def parse(object, root: false)
@@ -2,9 +2,16 @@
2
2
 
3
3
  module Restify
4
4
  class Promise < Concurrent::IVar
5
+ # A driver that can complete this promise in the waiting thread,
6
+ # e.g. an adapter running its event loop. See `#wait`.
7
+ #
8
+ # @api private
9
+ attr_writer :driver
10
+
5
11
  def initialize(*dependencies, &task)
6
12
  @task = task
7
13
  @dependencies = dependencies.flatten
14
+ @driver = nil
8
15
 
9
16
  super(&nil)
10
17
 
@@ -19,7 +26,11 @@ module Restify
19
26
 
20
27
  execute(t) if pending?
21
28
 
22
- super
29
+ # Let the driver run on the current thread instead of sleeping and
30
+ # switching to another thread if possible. If unsupported, the
31
+ # driver returns false.
32
+ super unless incomplete? && @driver&.drive(self, t)
33
+
23
34
  raise t if incomplete?
24
35
 
25
36
  self
@@ -62,8 +73,9 @@ module Restify
62
73
  end
63
74
 
64
75
  class << self
65
- def create
76
+ def create(driver: nil)
66
77
  promise = Promise.new
78
+ promise.driver = driver
67
79
  yield Writer.new(promise)
68
80
  promise
69
81
  end
@@ -93,6 +105,12 @@ module Restify
93
105
  def reject(reason)
94
106
  @promise.send :complete, false, nil, reason
95
107
  end
108
+
109
+ def set
110
+ fulfill(yield)
111
+ rescue Exception => e # rubocop:disable Lint/RescueException
112
+ reject(e)
113
+ end
96
114
  end
97
115
  end
98
116
  end
@@ -6,8 +6,8 @@ module Restify
6
6
  @registry = {}
7
7
  end
8
8
 
9
- def store(name, uri, **opts)
10
- @registry[name] = Context.new(uri, **opts)
9
+ def store(name, uri, **)
10
+ @registry[name] = Context.new(uri, **)
11
11
  end
12
12
 
13
13
  def fetch(name)
@@ -16,23 +16,23 @@ module Restify
16
16
 
17
17
  def initialize(context, template)
18
18
  @context = context
19
- @template = Addressable::Template.new template
19
+ @template = Addressable::Template.new(template)
20
20
  end
21
21
 
22
- def request(method:, params: {}, **opts)
23
- context.request(method, expand(params), **opts)
22
+ def request(method:, params: {}, **)
23
+ context.request(method, expand(params), **)
24
24
  end
25
25
 
26
- def get(data = {}, params: {}, **opts)
27
- request(**opts, method: :get, params: data.merge(params))
26
+ def get(data = {}, params: {}, **)
27
+ request(**, method: :get, params: data.merge(params))
28
28
  end
29
29
 
30
- def head(data = {}, params: {}, **opts)
31
- request(**opts, method: :head, params: data.merge(params))
30
+ def head(data = {}, params: {}, **)
31
+ request(**, method: :head, params: data.merge(params))
32
32
  end
33
33
 
34
- def delete(data = {}, params: {}, **opts)
35
- request(**opts, method: :delete, params: data.merge(params))
34
+ def delete(data = {}, params: {}, **)
35
+ request(**, method: :delete, params: data.merge(params))
36
36
  end
37
37
 
38
38
  def post(data = nil, **opts)
@@ -122,13 +122,5 @@ module Restify
122
122
  end
123
123
  end
124
124
  end
125
-
126
- def to_template(pattern)
127
- if pattern.is_a?(Addressable::Template)
128
- pattern
129
- else
130
- Addressable::Template.new pattern
131
- end
132
- end
133
125
  end
134
126
  end
@@ -11,7 +11,7 @@ module Restify
11
11
 
12
12
  # Request URI.
13
13
  #
14
- # @return [String] Request URI.
14
+ # @return [Addressable::URI] Request URI.
15
15
  #
16
16
  attr_reader :uri
17
17
 
@@ -30,7 +30,7 @@ module Restify
30
30
  attr_reader :timeout
31
31
 
32
32
  def initialize(uri:, method: :get, data: nil, timeout: 300, headers: {})
33
- @uri = uri
33
+ @uri = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
34
34
  @method = method.to_s.downcase
35
35
  @data = data
36
36
  @timeout = timeout
@@ -6,12 +6,13 @@ module Restify
6
6
  class Resource < SimpleDelegator
7
7
  # @api private
8
8
  #
9
- def initialize(context, response: nil, data: nil, relations: {})
9
+ def initialize(context, response: nil, data: nil, relations: {}, error: nil)
10
10
  super(data)
11
11
 
12
12
  @context = context
13
13
  @response = response
14
14
  @relations = relations
15
+ @error = error
15
16
  end
16
17
 
17
18
  # Check if resource has a relation with given name.
@@ -42,14 +43,31 @@ module Restify
42
43
 
43
44
  alias rel relation
44
45
 
46
+ # @api private
47
+ def __getobj__
48
+ # `Delegator` removes `Kernel#raise`, and the cause can only be
49
+ # set when raising.
50
+ ::Kernel.raise ParseError.new(@response, @error.message), cause: @error if @error
51
+
52
+ super
53
+ end
54
+
45
55
  # @!method data
46
56
  #
47
57
  # Return response data. Usually a hash or array.
48
58
  #
49
59
  # @return [Object] Response data.
60
+ # @raise [ParseError] If the response body could not be parsed.
50
61
  #
51
62
  alias data __getobj__
52
63
 
64
+ # @api private
65
+ def respond_to_missing?(name, include_private = false)
66
+ return false if @error
67
+
68
+ super
69
+ end
70
+
53
71
  # @!method response
54
72
  #
55
73
  # Return response if available.
@@ -95,7 +113,7 @@ module Restify
95
113
  # @api private
96
114
  def inspect
97
115
  text = {
98
- '@data' => data,
116
+ (@error ? '@error' : '@data') => @error || data,
99
117
  '@relations' => @relations,
100
118
  }.map {|k, v| "#{k}=#{v.inspect}" }.join(' ')
101
119