restify 3.0.0.rc2 → 3.0.0.rc3
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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +0 -1
- data/lib/restify/adapter/ethon/easy.rb +147 -0
- data/lib/restify/adapter/ethon/event_loop.rb +255 -0
- data/lib/restify/adapter/ethon/options.rb +81 -0
- data/lib/restify/adapter/ethon/pool.rb +43 -0
- data/lib/restify/adapter/ethon.rb +56 -307
- data/lib/restify/adapter/telemetry.rb +1 -1
- data/lib/restify/adapter/typhoeus.rb +5 -1
- data/lib/restify/context.rb +11 -1
- data/lib/restify/processors/base/parsing.rb +30 -25
- data/lib/restify/promise.rb +181 -47
- data/lib/restify/relation.rb +19 -5
- data/lib/restify/timeout.rb +19 -36
- data/lib/restify/version.rb +1 -1
- data/lib/restify/webmock.rb +1 -1
- data/lib/restify.rb +0 -1
- data/spec/restify/adapter/ethon/easy_spec.rb +111 -0
- data/spec/restify/adapter/ethon/event_loop_spec.rb +96 -0
- data/spec/restify/adapter/ethon/options_spec.rb +104 -0
- data/spec/restify/adapter/ethon/pool_spec.rb +81 -0
- data/spec/restify/adapter/ethon_spec.rb +111 -67
- data/spec/restify/adapter/loop_lock_spec.rb +6 -6
- data/spec/restify/context_spec.rb +14 -0
- data/spec/restify/features/opentelemetry_spec.rb +2 -0
- data/spec/restify/processors/json_spec.rb +67 -0
- data/spec/restify/promise_spec.rb +218 -21
- data/spec/restify/relation_spec.rb +23 -0
- data/spec/restify/timeout_spec.rb +38 -39
- metadata +9 -15
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require 'ethon'
|
|
4
4
|
require 'nio'
|
|
5
5
|
|
|
6
|
+
require 'restify/adapter/ethon/options'
|
|
7
|
+
require 'restify/adapter/ethon/easy'
|
|
8
|
+
require 'restify/adapter/ethon/event_loop'
|
|
9
|
+
require 'restify/adapter/ethon/pool'
|
|
10
|
+
|
|
6
11
|
module Restify
|
|
7
12
|
module Adapter
|
|
8
13
|
#
|
|
@@ -27,6 +32,16 @@ module Restify
|
|
|
27
32
|
class Ethon < Base
|
|
28
33
|
include Logging
|
|
29
34
|
|
|
35
|
+
# Restify follows relations from URLs in server responses, and
|
|
36
|
+
# libcurl supports far more than HTTP, e.g. `file://` or `scp://`.
|
|
37
|
+
# Therefore only HTTP-like protocols must ever be used, both for
|
|
38
|
+
# the request itself and when following redirects.
|
|
39
|
+
#
|
|
40
|
+
# This is intentionally not configurable.
|
|
41
|
+
PROTOCOLS = %i[http https].freeze
|
|
42
|
+
|
|
43
|
+
# Set headers bypassing the wait for 100-continue responses many
|
|
44
|
+
# servers do not send correctly.
|
|
30
45
|
DEFAULT_HEADERS = {
|
|
31
46
|
'Expect' => '',
|
|
32
47
|
'Transfer-Encoding' => '',
|
|
@@ -39,38 +54,31 @@ module Restify
|
|
|
39
54
|
tcp_keepintvl: 5,
|
|
40
55
|
}.freeze
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
# This is intentionally not configurable.
|
|
48
|
-
PROTOCOLS = %i[http https].freeze
|
|
57
|
+
ENFORCED_OPTIONS = {
|
|
58
|
+
# Do not use signal handlers in libcurl, as it would interfere
|
|
59
|
+
# with the threads.
|
|
60
|
+
nosignal: true,
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
inout: :rw,
|
|
62
|
+
# Do not allow using and redirecting to unexpected (e.g. local)
|
|
63
|
+
# protocols.
|
|
64
|
+
protocols: PROTOCOLS,
|
|
65
|
+
redir_protocols: PROTOCOLS,
|
|
55
66
|
}.freeze
|
|
56
67
|
|
|
68
|
+
# Maximum number of idle easy handles kept for reuse.
|
|
69
|
+
POOL_SIZE = 64
|
|
70
|
+
|
|
57
71
|
def initialize(options: {}, **multi)
|
|
58
|
-
@options =
|
|
72
|
+
@options = Options.new({
|
|
73
|
+
**DEFAULT_OPTIONS,
|
|
74
|
+
**options,
|
|
75
|
+
**ENFORCED_OPTIONS,
|
|
76
|
+
})
|
|
59
77
|
@multi_options = multi
|
|
60
78
|
|
|
61
|
-
# libcurl only stores the function pointers, therefore the procs
|
|
62
|
-
# must be referenced here too, or they would be garbage
|
|
63
|
-
# collected.
|
|
64
|
-
@socketfunction = method(:on_socket).to_proc
|
|
65
|
-
@timerfunction = method(:on_timer).to_proc
|
|
66
|
-
|
|
67
79
|
@mutex = Mutex.new
|
|
68
80
|
@thread = nil
|
|
69
81
|
|
|
70
|
-
# Wake up the background thread from `select` when a waiting
|
|
71
|
-
# thread wants to run the loop.
|
|
72
|
-
@loop = LoopLock.new { @selector.wakeup }
|
|
73
|
-
|
|
74
82
|
setup
|
|
75
83
|
|
|
76
84
|
super()
|
|
@@ -87,22 +95,16 @@ module Restify
|
|
|
87
95
|
url: request.uri,
|
|
88
96
|
timeout: request.timeout
|
|
89
97
|
|
|
90
|
-
@queue << easy
|
|
91
|
-
|
|
92
98
|
# Ensure the event loop is running and let it pick up the new
|
|
93
99
|
# request.
|
|
94
100
|
thread
|
|
95
|
-
|
|
101
|
+
|
|
102
|
+
@events.enqueue(easy)
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
# Run the event loop in the calling thread until the promise is
|
|
99
|
-
# complete or the timeout expires, see `Promise#wait
|
|
100
|
-
#
|
|
101
|
-
# Several threads can wait at the same time, but only one runs the
|
|
102
|
-
# loop. The others sleep until either their promise completes or
|
|
103
|
-
# the loop is released. The background thread keeps processing
|
|
104
|
-
# requests nobody waits on, but releases the loop as soon as a
|
|
105
|
-
# thread starts waiting.
|
|
106
|
+
# complete or the timeout expires, see `Promise#wait` and
|
|
107
|
+
# `EventLoop#drive`.
|
|
106
108
|
#
|
|
107
109
|
# This only processes transfers, i.e. fulfills or rejects the
|
|
108
110
|
# adapter's promises. Callbacks chained with `Promise#then` still
|
|
@@ -111,22 +113,10 @@ module Restify
|
|
|
111
113
|
# Returns false when the loop cannot be run in the calling thread,
|
|
112
114
|
# i.e. when waiting from within the loop itself.
|
|
113
115
|
#
|
|
114
|
-
def drive(promise, timeout)
|
|
116
|
+
def drive(promise, timeout)
|
|
115
117
|
check_fork!
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
while @loop.acquire(timeout) { promise.complete? }
|
|
120
|
-
begin
|
|
121
|
-
step(timeout.remaining) until promise.complete? || !timeout.remaining.positive?
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
error(e)
|
|
124
|
-
ensure
|
|
125
|
-
@loop.release
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
true
|
|
119
|
+
@events.drive(promise, timeout)
|
|
130
120
|
end
|
|
131
121
|
|
|
132
122
|
private
|
|
@@ -136,15 +126,9 @@ module Restify
|
|
|
136
126
|
end
|
|
137
127
|
|
|
138
128
|
def setup
|
|
139
|
-
@pid
|
|
140
|
-
@
|
|
141
|
-
@
|
|
142
|
-
@timer = nil
|
|
143
|
-
@queue = Queue.new
|
|
144
|
-
|
|
145
|
-
@multi = ::Ethon::Multi.new(execution_mode: :socket_action, **@multi_options)
|
|
146
|
-
@multi.socketfunction = @socketfunction
|
|
147
|
-
@multi.timerfunction = @timerfunction
|
|
129
|
+
@pid = Process.pid
|
|
130
|
+
@pool = Pool.new(size: POOL_SIZE) { Easy.new }
|
|
131
|
+
@events = EventLoop.new(**@multi_options)
|
|
148
132
|
end
|
|
149
133
|
|
|
150
134
|
def check_fork!
|
|
@@ -160,91 +144,43 @@ module Restify
|
|
|
160
144
|
# Therefore, abandon the state without releasing it, and set up
|
|
161
145
|
# everything again.
|
|
162
146
|
def forked!
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@multi.handle.autorelease = false
|
|
167
|
-
abandoned.each {|easy| easy.handle.autorelease = false }
|
|
147
|
+
@events.forked!
|
|
148
|
+
@pool.forked!
|
|
168
149
|
|
|
169
150
|
@thread = nil
|
|
170
|
-
@loop.reset!
|
|
171
151
|
setup
|
|
172
|
-
|
|
173
|
-
abandoned.each do |easy|
|
|
174
|
-
easy._restify_writer.reject(
|
|
175
|
-
Restify::NetworkError.new(easy._restify_request, 'Request started before fork'),
|
|
176
|
-
)
|
|
177
|
-
end
|
|
178
152
|
end
|
|
179
153
|
|
|
180
154
|
def convert(request, writer)
|
|
181
|
-
|
|
155
|
+
@pool.checkout.tap do |easy|
|
|
182
156
|
easy._otel_span = OpenTelemetry::Trace.current_span
|
|
183
157
|
easy._restify_request = request
|
|
184
158
|
easy._restify_writer = writer
|
|
185
159
|
|
|
186
|
-
easy.
|
|
187
|
-
request.uri.to_s,
|
|
188
|
-
request.method,
|
|
189
|
-
request_options(request),
|
|
190
|
-
)
|
|
160
|
+
easy.prepare(request, @options)
|
|
191
161
|
|
|
192
162
|
easy.on_complete do |completed|
|
|
193
163
|
complete(completed, request, writer)
|
|
194
164
|
|
|
165
|
+
@pool.release(completed)
|
|
166
|
+
|
|
195
167
|
# Wake up threads waiting on the loop to check if their
|
|
196
168
|
# result is available now.
|
|
197
|
-
@
|
|
169
|
+
@events.notify
|
|
198
170
|
end
|
|
199
171
|
end
|
|
200
172
|
end
|
|
201
173
|
|
|
202
|
-
def request_options(request)
|
|
203
|
-
# libcurl requires millisecond-based timeouts so fractional
|
|
204
|
-
# seconds must be converted and rounded.
|
|
205
|
-
timeout = request.timeout && (request.timeout * 1000).ceil
|
|
206
|
-
|
|
207
|
-
@options.merge(
|
|
208
|
-
headers: DEFAULT_HEADERS.merge(request.headers),
|
|
209
|
-
body: request.body,
|
|
210
|
-
timeout_ms: timeout,
|
|
211
|
-
connecttimeout_ms: timeout,
|
|
212
|
-
|
|
213
|
-
# Merged last, so that neither can be overridden via `options`.
|
|
214
|
-
protocols: PROTOCOLS,
|
|
215
|
-
redir_protocols: PROTOCOLS,
|
|
216
|
-
|
|
217
|
-
# Do not use signal handlers in libcurl, as it would interfere
|
|
218
|
-
# with the threads.
|
|
219
|
-
nosignal: true,
|
|
220
|
-
)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
174
|
def complete(easy, request, writer)
|
|
224
175
|
writer.set do
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
status: status,
|
|
231
|
-
message: code
|
|
232
|
-
|
|
233
|
-
if code != :ok
|
|
234
|
-
raise Restify::NetworkError.new(
|
|
235
|
-
request,
|
|
236
|
-
::Ethon::Curl.easy_strerror(code),
|
|
237
|
-
)
|
|
176
|
+
if logger&.debug?
|
|
177
|
+
debug 'request:complete',
|
|
178
|
+
tag: request.object_id,
|
|
179
|
+
status: easy.response_code,
|
|
180
|
+
message: easy.return_code
|
|
238
181
|
end
|
|
239
182
|
|
|
240
|
-
|
|
241
|
-
raise Restify::NetworkError.new(
|
|
242
|
-
request,
|
|
243
|
-
'Response without HTTP status',
|
|
244
|
-
)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
convert_back(easy, request)
|
|
183
|
+
easy.response(request)
|
|
248
184
|
end
|
|
249
185
|
rescue StandardError => e
|
|
250
186
|
# This runs inside a libcurl callback, therefore no exception
|
|
@@ -253,210 +189,23 @@ module Restify
|
|
|
253
189
|
error(e)
|
|
254
190
|
end
|
|
255
191
|
|
|
256
|
-
def convert_back(easy, request)
|
|
257
|
-
::Restify::Response.new(
|
|
258
|
-
request,
|
|
259
|
-
effective_uri(easy, request),
|
|
260
|
-
easy.response_code,
|
|
261
|
-
convert_headers(easy.response_headers),
|
|
262
|
-
easy.response_body,
|
|
263
|
-
)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def effective_uri(easy, request)
|
|
267
|
-
url = easy.effective_url
|
|
268
|
-
url ? Addressable::URI.parse(url) : request.uri
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def convert_headers(raw)
|
|
272
|
-
headers = {}
|
|
273
|
-
return headers if raw.nil?
|
|
274
|
-
|
|
275
|
-
# The raw headers can contain multiple blocks, e.g. from
|
|
276
|
-
# informational responses or when following redirects; only use
|
|
277
|
-
# the latest ones:
|
|
278
|
-
block = raw.split(/\r?\n\r?\n/).reject {|b| b.strip.empty? }.last
|
|
279
|
-
|
|
280
|
-
# Split on newlines that are not followed by whitespace to keep
|
|
281
|
-
# folded header values together.
|
|
282
|
-
block.to_s.split(/\r?\n(?!\s)/).each do |line|
|
|
283
|
-
line = line.strip
|
|
284
|
-
next if line.empty? || line.start_with?('HTTP/')
|
|
285
|
-
|
|
286
|
-
key, value = line.split(':', 2)
|
|
287
|
-
next if value.nil?
|
|
288
|
-
|
|
289
|
-
key = key.strip.upcase.tr('-', '_')
|
|
290
|
-
value = value.strip.gsub(/\r?\n\s*/, ' ')
|
|
291
|
-
|
|
292
|
-
case (current = headers[key])
|
|
293
|
-
when nil then headers[key] = value
|
|
294
|
-
when Array then current << value
|
|
295
|
-
else headers[key] = [current, value]
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
headers
|
|
300
|
-
end
|
|
301
|
-
|
|
302
192
|
def thread
|
|
303
193
|
@mutex.synchronize do
|
|
304
194
|
# Spawn thread if not yet started, or recreate it if it died.
|
|
305
195
|
if @thread.nil? || !@thread.status
|
|
306
196
|
debug 'loop:spawn'
|
|
307
|
-
@thread = Thread.new { run }
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
@thread
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def run
|
|
315
|
-
loop do
|
|
316
|
-
@loop.acquire_background
|
|
317
197
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
ensure
|
|
321
|
-
@loop.release
|
|
198
|
+
events = @events
|
|
199
|
+
@thread = Thread.new { events.run }
|
|
322
200
|
end
|
|
323
|
-
rescue StandardError => e
|
|
324
|
-
error(e)
|
|
325
|
-
end
|
|
326
|
-
ensure
|
|
327
|
-
debug 'loop:exit'
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# Run one iteration of the event loop. Must only be called by the
|
|
331
|
-
# thread owning the loop.
|
|
332
|
-
def step(limit = nil)
|
|
333
|
-
dequeue_all
|
|
334
|
-
|
|
335
|
-
# libcurl needs to be notified about its own timeouts, e.g. to
|
|
336
|
-
# start newly added transfers or to time out stalled ones.
|
|
337
|
-
timeout!
|
|
338
|
-
|
|
339
|
-
timeout = select_timeout
|
|
340
|
-
timeout = limit if limit && (timeout.nil? || timeout > limit)
|
|
341
|
-
debug 'loop:select', timeout: timeout
|
|
342
|
-
|
|
343
|
-
# nil on timeout; empty array when woken up
|
|
344
|
-
@selector.select(timeout)&.each do |monitor|
|
|
345
|
-
# libcurl can remove sockets while earlier events of the same
|
|
346
|
-
# batch are processed, e.g. when a completed transfer tears
|
|
347
|
-
# down other connections. Skip monitors that are gone by now.
|
|
348
|
-
next unless @monitors[monitor.value].equal?(monitor)
|
|
349
|
-
|
|
350
|
-
socket_action(monitor.value, readiness(monitor))
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def dequeue_all
|
|
355
|
-
loop do
|
|
356
|
-
easy = begin
|
|
357
|
-
@queue.pop(true)
|
|
358
|
-
rescue ThreadError
|
|
359
|
-
break
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
add(easy)
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def add(easy)
|
|
367
|
-
# Restore the OpenTelemetry span the request originated from, so
|
|
368
|
-
# that the Ethon instrumentation picks up the correct parent
|
|
369
|
-
# when the easy handle is added to libcurl.
|
|
370
|
-
#
|
|
371
|
-
# Handle exceptions from Ethon or WebMock too, and reject the
|
|
372
|
-
# promise, so that the errors can be handled in user code.
|
|
373
|
-
OpenTelemetry::Trace.with_span(easy._otel_span) do
|
|
374
|
-
@multi.add(easy)
|
|
375
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
376
|
-
easy._restify_writer.reject(e)
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
# Seconds until libcurl wants to be called again, or nil to block
|
|
381
|
-
# until a socket becomes ready or the loop is woken up.
|
|
382
|
-
def select_timeout
|
|
383
|
-
return nil unless @timer
|
|
384
|
-
|
|
385
|
-
[@timer - now, 0].max
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
def timeout!
|
|
389
|
-
return unless @timer
|
|
390
|
-
return if now < @timer
|
|
391
201
|
|
|
392
|
-
|
|
393
|
-
# from within the socket action.
|
|
394
|
-
@timer = nil
|
|
395
|
-
|
|
396
|
-
socket_action
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def readiness(monitor)
|
|
400
|
-
readiness = []
|
|
401
|
-
readiness << :in if monitor.readable?
|
|
402
|
-
readiness << :out if monitor.writable?
|
|
403
|
-
readiness
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def socket_action(socket = nil, readiness = 0)
|
|
407
|
-
@multi.socket_action(socket, readiness)
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# libcurl: called when asking for readiness monitoring
|
|
411
|
-
def on_socket(_easy, socket, what, _userp, _socketp)
|
|
412
|
-
debug 'socket:action', tag: socket, what: what
|
|
413
|
-
|
|
414
|
-
if what == :remove
|
|
415
|
-
@monitors.delete(socket)&.close
|
|
416
|
-
else
|
|
417
|
-
monitor = (@monitors[socket] ||= register(socket))
|
|
418
|
-
monitor.interests = INTERESTS.fetch(what)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
:ok
|
|
422
|
-
rescue StandardError => e
|
|
423
|
-
error(e)
|
|
424
|
-
:ok
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# libcurl: called on timeout changes
|
|
428
|
-
def on_timer(_handle, timeout_ms, _userp)
|
|
429
|
-
debug 'timer:set', timeout: timeout_ms
|
|
430
|
-
@timer = timeout_ms.negative? ? nil : now + (timeout_ms / 1000.0)
|
|
431
|
-
|
|
432
|
-
:ok
|
|
433
|
-
rescue StandardError => e
|
|
434
|
-
error(e)
|
|
435
|
-
:ok
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def register(socket)
|
|
439
|
-
# FD owned by libcurl: IO/ruby must never close it
|
|
440
|
-
io = IO.for_fd(socket, autoclose: false)
|
|
441
|
-
|
|
442
|
-
@selector.register(io, :r).tap do |monitor|
|
|
443
|
-
monitor.value = socket
|
|
202
|
+
@thread
|
|
444
203
|
end
|
|
445
204
|
end
|
|
446
205
|
|
|
447
|
-
def now
|
|
448
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
449
|
-
end
|
|
450
|
-
|
|
451
206
|
def _log_prefix
|
|
452
207
|
"[#{object_id}/#{Thread.current.object_id}]"
|
|
453
208
|
end
|
|
454
|
-
|
|
455
|
-
# Keep track of the OTEL span, the request and the promise writer to
|
|
456
|
-
# reject on errors in the background thread or after fork
|
|
457
|
-
class Easy < ::Ethon::Easy
|
|
458
|
-
attr_accessor :_otel_span, :_restify_request, :_restify_writer
|
|
459
|
-
end
|
|
460
209
|
end
|
|
461
210
|
end
|
|
462
211
|
end
|
|
@@ -26,7 +26,7 @@ module Restify
|
|
|
26
26
|
OpenTelemetry.propagation.inject(request.headers)
|
|
27
27
|
|
|
28
28
|
super.tap do |x|
|
|
29
|
-
x.add_observer do |
|
|
29
|
+
x.add_observer do |response, err|
|
|
30
30
|
if response
|
|
31
31
|
span.set_attribute('http.response.status_code', response&.code)
|
|
32
32
|
span.status = OpenTelemetry::Trace::Status.error unless (100..399).cover?(response&.code)
|
|
@@ -144,7 +144,11 @@ module Restify
|
|
|
144
144
|
|
|
145
145
|
def effective_uri(response, request)
|
|
146
146
|
url = response.effective_url
|
|
147
|
-
|
|
147
|
+
|
|
148
|
+
# Only parse the URL after redirects, as parsing is expensive.
|
|
149
|
+
return request.uri if url.nil? || url == request.uri.to_s
|
|
150
|
+
|
|
151
|
+
Addressable::URI.parse(url)
|
|
148
152
|
end
|
|
149
153
|
|
|
150
154
|
def convert_headers(headers)
|
data/lib/restify/context.rb
CHANGED
|
@@ -30,8 +30,18 @@ module Restify
|
|
|
30
30
|
@options = kwargs
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Resolve a URI against the context URI. Absolute URIs are returned
|
|
34
|
+
# as they are, e.g. when already joined by a relation.
|
|
35
|
+
#
|
|
36
|
+
# @param uri [Addressable::URI, String] URI to resolve.
|
|
37
|
+
# @return [Addressable::URI] Absolute URI.
|
|
38
|
+
#
|
|
33
39
|
def join(uri)
|
|
34
|
-
|
|
40
|
+
if uri.is_a?(Addressable::URI) && uri.absolute?
|
|
41
|
+
uri
|
|
42
|
+
else
|
|
43
|
+
self.uri.join(uri)
|
|
44
|
+
end
|
|
35
45
|
end
|
|
36
46
|
|
|
37
47
|
def inherit(uri, **)
|
|
@@ -9,6 +9,8 @@ module Restify
|
|
|
9
9
|
# Parses generic data structures into resources
|
|
10
10
|
#
|
|
11
11
|
module Parsing
|
|
12
|
+
RELATION_NAME = /\A\w+_url\z/i
|
|
13
|
+
|
|
12
14
|
def load
|
|
13
15
|
# No data, e.g. when a server sets a content type but sends an
|
|
14
16
|
# empty body.
|
|
@@ -27,41 +29,44 @@ module Restify
|
|
|
27
29
|
|
|
28
30
|
def parse(object, root: false)
|
|
29
31
|
case object
|
|
30
|
-
when Hash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Resource.new context,
|
|
35
|
-
data:,
|
|
36
|
-
response: root ? response : nil,
|
|
37
|
-
relations:
|
|
38
|
-
|
|
39
|
-
when Array
|
|
40
|
-
object.map {|each| parse(each) }
|
|
41
|
-
else
|
|
42
|
-
object
|
|
32
|
+
when Hash then build_resource(object, root)
|
|
33
|
+
when Array then object.map {|each| parse(each) }
|
|
34
|
+
else object
|
|
43
35
|
end
|
|
44
36
|
end
|
|
45
37
|
|
|
46
38
|
private
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
# Build data and relations of a resource in one pass.
|
|
41
|
+
def build_resource(object, root)
|
|
42
|
+
data = {}
|
|
43
|
+
relations = {}
|
|
44
|
+
|
|
45
|
+
object.each_pair do |key, value|
|
|
46
|
+
key = key.to_s
|
|
47
|
+
data[key] = parse(value)
|
|
48
|
+
parse_relation(relations, key, value) if value.is_a?(String)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Resource.new(
|
|
52
|
+
context,
|
|
53
|
+
data:,
|
|
54
|
+
response: root ? response : nil,
|
|
55
|
+
relations:,
|
|
56
|
+
)
|
|
50
57
|
end
|
|
51
58
|
|
|
52
|
-
def
|
|
53
|
-
name =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'self'
|
|
58
|
-
else
|
|
59
|
-
return
|
|
59
|
+
def parse_relation(relations, key, value)
|
|
60
|
+
name = if key.match?(RELATION_NAME)
|
|
61
|
+
key[0, key.length - 4].downcase
|
|
62
|
+
elsif key.casecmp?('url')
|
|
63
|
+
'self'
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
return if
|
|
66
|
+
return if name.nil? || relations.key?(name)
|
|
67
|
+
return if value.empty?
|
|
63
68
|
|
|
64
|
-
relations[name] =
|
|
69
|
+
relations[name] = value
|
|
65
70
|
end
|
|
66
71
|
end
|
|
67
72
|
end
|