rest-builder 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2ebc038a71c8fa9399ddd41fde21d014c2a47e3a
4
+ data.tar.gz: 49ee5262a808a711f8bbcff6ec7f5a011c7832ed
5
+ SHA512:
6
+ metadata.gz: 14a85d6c66f9e61df4fd7d68c8de783641994ef893bc124f384ee1e0696a756d7e2146e2144ddecc0b9de5cc9be36b57609435cdca4020436bc43a32c57b7243
7
+ data.tar.gz: 984ad801875e5d21f1dba3d13e91875451987f22b00d9be1e598e030019a4492aaf3c61de541260bf2f16b46be69bc92dca379a534b9df0b4b4062039a22c086
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /pkg/
2
+ /coverage/
data/.gitmodules ADDED
@@ -0,0 +1,6 @@
1
+ [submodule "task"]
2
+ path = task
3
+ url = git://github.com/godfat/gemgem
4
+ [submodule "promise_pool"]
5
+ path = promise_pool
6
+ url = git://github.com/godfat/promise_pool.git
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2
5
+ - 2.3.0
6
+ - rbx
7
+ - jruby-9
8
+
9
+ before_install:
10
+ - rvm get head
11
+ - rvm reload
12
+ - rvm use --install $TRAVIS_RUBY_VERSION --binary --latest
13
+ install: 'bundle install --retry=3'
14
+ script: 'ruby -vr bundler/setup -S rake test'
data/CHANGES.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGES
2
+
3
+ ## rest-builder 0.9.0 -- 2016-01-31
4
+
5
+ * Birthday!
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+
2
+ source 'https://rubygems.org/'
3
+
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'pork'
8
+ gem 'muack'
9
+ gem 'webmock'
10
+
11
+ gem 'simplecov', :require => false if ENV['COV']
12
+ gem 'coveralls', :require => false if ENV['CI']
13
+
14
+ platforms :rbx do
15
+ gem 'rubysl-weakref' # used in rest-core
16
+ gem 'rubysl-socket' # used in test
17
+ gem 'rubysl-singleton' # used in rake
18
+ gem 'rubysl-rexml' # used in crack used in webmock
19
+ gem 'rubysl-bigdecimal' # used in crack used in webmock
20
+ end
21
+
22
+ platforms :jruby do
23
+ gem 'jruby-openssl'
24
+ end
data/README.md ADDED
@@ -0,0 +1,577 @@
1
+ # rest-builder [![Build Status](https://secure.travis-ci.org/godfat/rest-builder.png?branch=master)](http://travis-ci.org/godfat/rest-builder) [![Coverage Status](https://coveralls.io/repos/godfat/rest-builder/badge.png)](https://coveralls.io/r/godfat/rest-builder) [![Join the chat at https://gitter.im/godfat/rest-builder](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/godfat/rest-builder)
2
+
3
+ by Lin Jen-Shin ([godfat](http://godfat.org))
4
+
5
+ ## LINKS:
6
+
7
+ * [github](https://github.com/godfat/rest-builder)
8
+ * [rubygems](https://rubygems.org/gems/rest-builder)
9
+ * [rdoc](http://rdoc.info/projects/godfat/rest-builder)
10
+
11
+ ## DESCRIPTION:
12
+
13
+ Modular Ruby clients interface for REST APIs.
14
+
15
+ Build your own API clients for less dependencies, less codes, less memory,
16
+ less conflicts, and run faster. Checkout [rest-core][] for pre-built
17
+ middleware and [rest-more][] for pre-built clients.
18
+
19
+ [rest-core]: https://github.com/godfat/rest-core
20
+ [rest-more]: https://github.com/godfat/rest-more
21
+
22
+ ## FEATURES:
23
+
24
+ * Modular interface for REST clients similar to WSGI/Rack for servers.
25
+ * Concurrent requests with synchronous or asynchronous interfaces with
26
+ threads.
27
+
28
+ ## WHY?
29
+
30
+ This was extracted from [rest-core][] because rest-core itself is getting
31
+ too complex, and this would be the new core of rest-core. Naming it
32
+ rest-core-core is a bit silly, and due to compatibility reason,
33
+ rest-core should work as is.
34
+
35
+ ## REQUIREMENTS:
36
+
37
+ ### Mandatory:
38
+
39
+ * Tested with MRI (official CRuby), Rubinius and JRuby.
40
+ * gem [promise_pool][]
41
+ * gem [timers][]
42
+ * gem [httpclient][]
43
+ * gem [mime-types][]
44
+
45
+ [promise_pool]: https://github.com/godfat/promise_pool
46
+ [timers]: https://github.com/celluloid/timers
47
+ [httpclient]: https://github.com/nahi/httpclient
48
+ [mime-types]: https://github.com/halostatue/mime-types
49
+
50
+ ## INSTALLATION:
51
+
52
+ ``` shell
53
+ gem install rest-builder
54
+ ```
55
+
56
+ Or if you want development version, put this in Gemfile:
57
+
58
+ ``` ruby
59
+ gem 'rest-builder', :git => 'git://github.com/godfat/rest-builder.git',
60
+ :submodules => true
61
+ ```
62
+
63
+ If you want to use pre-built middleware instead of rolling your own,
64
+ please checkout [rest-core][].
65
+
66
+ If you just want to use Facebook or Twitter clients, please take a look at
67
+ [rest-more][] which has a lot of clients built with rest-core.
68
+
69
+ ## Basic Usage:
70
+
71
+ Use `RestBuilder::Builder` to build your own clients like `Rack::Builder` to
72
+ build your application. The client you built this way would be a class which
73
+ you could then make client instances from. This way, each instance could
74
+ carry different configuration, e.g. different cache time or timeout time.
75
+
76
+ ``` ruby
77
+ require 'rest-builder'
78
+
79
+ YourClient = RestBuilder::Builder.client do
80
+ # use ...
81
+ # use ...
82
+ # run ...
83
+ end
84
+ client = YourClient.new
85
+ client.get('http://example.com/') # make a request to http://example.com/
86
+ ```
87
+
88
+ ## Build Your Own Middleware:
89
+
90
+ ### How We Pick the Default Value:
91
+
92
+ There are a number of ways to specify a default value, each with different
93
+ priorities. Suppose we have a middleware which remembers an integer:
94
+
95
+ ``` ruby
96
+ class HP
97
+ def self.members; [:hp]; end
98
+ include RestBuilder::Middleware
99
+ def call env, &k
100
+ puts "HP: #{hp(env)}"
101
+ app.call(env, &k)
102
+ end
103
+ end
104
+ Mage = RestBuilder::Builder.client do
105
+ use HP, 5 # the very last default
106
+ end
107
+ mage = Mage.new
108
+ ```
109
+
110
+ 1. The one passed to the request directly gets the first priority, e.g.
111
+
112
+ ``` ruby
113
+ mage.get('http://example.com/', {}, :hp => 1) # prints HP: 1
114
+ ```
115
+
116
+ 2. The one saved as an instance variable in the client gets the 2nd place.
117
+
118
+ ``` ruby
119
+ mage.hp = 2
120
+ mage.get('http://example.com/') # prints HP: 2
121
+ mage.get('http://example.com/', {}, :hp => 1) # prints HP: 1
122
+ mage.hp # still 2
123
+ mage.hp = false # disable hp
124
+ mage.hp = nil # reset to default
125
+ ```
126
+
127
+ 3. The method defined in the client instance named `default_hp` gets the 3rd.
128
+
129
+ ``` ruby
130
+ class Mage
131
+ def default_hp
132
+ 3
133
+ end
134
+ end
135
+ mage.get('http://example.com/') # prints HP: 3
136
+ mage.hp # 3
137
+ mage.hp = nil # reset default
138
+ Mage.send(:remove_method, :default_hp)
139
+ ```
140
+
141
+ 4. The method defined in the client class named `default_hp` gets the 4rd.
142
+ P.S. In [rest-more][], with `RestCore::Config` it would generate a
143
+ `DefaultAttributes` module which defines this kind of default method and
144
+ then is extended into the client class. You could still define this method
145
+ to override the default though.
146
+
147
+ ``` ruby
148
+ class Mage
149
+ def self.default_hp
150
+ 4
151
+ end
152
+ end
153
+ mage.get('http://example.com/') # prints HP: 4
154
+ mage.hp # 4
155
+ mage.hp = nil # reset to default
156
+ Mage.singleton_class.send(:remove_method, :default_hp)
157
+ ```
158
+
159
+ 5. The one defined in the middleware gets the last place.
160
+
161
+ ``` ruby
162
+ mage.get('http://example.com/') # prints HP: 5
163
+ mage.hp # 5
164
+ mage.hp = nil # reset to default
165
+ ```
166
+
167
+ You can find all the details in client.rb and middleware.rb. See the
168
+ included method hooks.
169
+
170
+ ## Concurrent Requests with Futures:
171
+
172
+ You can also make concurrent requests easily:
173
+ (see "Advanced Concurrent HTTP Requests -- Embrace the Future" for detail)
174
+
175
+ ``` ruby
176
+ a = [client.get('http://example.com/a'), client.get('http://example.com/b')]
177
+ puts "It's not blocking... but doing concurrent requests underneath"
178
+ p a # Here we want the values, so it blocks here
179
+ puts "DONE"
180
+ ```
181
+
182
+ ## Exception Handling for Futures:
183
+
184
+ Note that since the API call would only block whenever you're looking at
185
+ the response, it won't raise any exception at the time the API was called.
186
+ So if you want to block and handle the exception at the time API was called,
187
+ you would do something like this:
188
+
189
+ ``` ruby
190
+ begin
191
+ response = client.get('http://nonexist/').itself # itself is the point
192
+ do_the_work(response)
193
+ rescue => e
194
+ puts "Got an exception: #{e}"
195
+ end
196
+ ```
197
+
198
+ The trick here is forcing the future immediately give you the exact response,
199
+ so that rest-builder could see the response and raise the exception. You can
200
+ call whatever methods on the future to force this behaviour, but since
201
+ `itself` is a method from `Kernel` (which is included in `Object`), it's
202
+ always available and would return the original value, so it is the easiest
203
+ method to be remembered and used.
204
+
205
+ If you know the response must be a string, then you can also use `to_s`.
206
+ Like this:
207
+
208
+ ``` ruby
209
+ begin
210
+ response = client.get('http://nonexist/').to_s
211
+ do_the_work(response)
212
+ rescue => e
213
+ puts "Got an exception: #{e}"
214
+ end
215
+ ```
216
+
217
+ Or you can also do this:
218
+
219
+ ``` ruby
220
+ begin
221
+ response = client.get('http://nonexist/')
222
+ response.class # simply force it to load
223
+ do_the_work(response)
224
+ rescue => e
225
+ puts "Got an exception: #{e}"
226
+ end
227
+ ```
228
+
229
+ The point is simply making a method call to force it load, whatever method
230
+ should work.
231
+
232
+ ## Concurrent Requests with Callbacks:
233
+
234
+ On the other hand, callback mode also available:
235
+
236
+ ``` ruby
237
+ client.get('http://example.com/'){ |v| p v }
238
+ puts "It's not blocking... but doing concurrent requests underneath"
239
+ client.wait # we block here to wait for the request done
240
+ puts "DONE"
241
+ ```
242
+
243
+ ## Exception Handling for Callbacks:
244
+
245
+ What about exception handling in callback mode? You know that we cannot
246
+ raise any exception in the case of using a callback. So rest-builder would
247
+ pass the exception object into your callback. You can handle the exception
248
+ like this:
249
+
250
+ ``` ruby
251
+ client.get('http://nonexist/') do |response|
252
+ if response.kind_of?(Exception)
253
+ puts "Got an exception: #{response}"
254
+ else
255
+ do_the_work(response)
256
+ end
257
+ end
258
+ puts "It's not blocking... but doing concurrent requests underneath"
259
+ client.wait # we block here to wait for the request done
260
+ puts "DONE"
261
+ ```
262
+
263
+ ## Thread Pool / Connection Pool
264
+
265
+ Underneath, rest-builder would spawn a thread for each request, freeing you
266
+ from blocking. However, occasionally we would not want this behaviour,
267
+ giving that we might have limited resource and cannot maximize performance.
268
+
269
+ For example, maybe we could not afford so many threads running concurrently,
270
+ or the target server cannot accept so many concurrent connections. In those
271
+ cases, we would want to have limited concurrent threads or connections.
272
+
273
+ ``` ruby
274
+ YourClient.pool_size = 10
275
+ YourClient.pool_idle_time = 60
276
+ ```
277
+
278
+ This could set the thread pool size to 10, having a maximum of 10 threads
279
+ running together, growing from requests. Each threads idled more than 60
280
+ seconds would be shut down automatically.
281
+
282
+ Note that `pool_size` should at least be larger than 4, or it might be
283
+ very likely to have _deadlock_ if you're using nested callbacks and having
284
+ a large number of concurrent calls.
285
+
286
+ Also, setting `pool_size` to `-1` would mean we want to make blocking
287
+ requests, without spawning any threads. This might be useful for debugging.
288
+
289
+ ## Gracefully shutdown
290
+
291
+ To shutdown gracefully, consider shutdown the thread pool (if we're using it),
292
+ and wait for all requests for a given client. For example, we'll do this when
293
+ we're shutting down:
294
+
295
+ ``` ruby
296
+ YourClient.shutdown
297
+ ```
298
+
299
+ We could put them in `at_exit` callback like this:
300
+
301
+ ``` ruby
302
+ at_exit do
303
+ YourClient.shutdown
304
+ end
305
+ ```
306
+
307
+ If you're using unicorn, you probably want to put that in the config.
308
+
309
+ ## Random Asynchronous Tasks
310
+
311
+ Occasionally we might want to do some asynchronous tasks which could take
312
+ the advantage of the concurrency facilities inside rest-builder, for example,
313
+ using `wait` and `shutdown`. You could do this with `defer` for a particular
314
+ client. For example:
315
+
316
+ ``` ruby
317
+ YourClient.defer do
318
+ sleep(1)
319
+ puts "Slow task done"
320
+ end
321
+
322
+ YourClient.wait
323
+ ```
324
+
325
+ ## Persistent connections (keep-alive connections)
326
+
327
+ Since we're using [httpclient][] by default now, we would reuse connections,
328
+ making it much faster for hitting the same host repeatedly.
329
+
330
+ ## Streaming Requests
331
+
332
+ Suppose we want to POST a file, instead of trying to read all the contents
333
+ in memory and send them, we could stream it from the file system directly.
334
+
335
+ ``` ruby
336
+ client.post('path', File.open('README.md'))
337
+ ```
338
+
339
+ Basically, payloads could be any IO object. Check out
340
+ [RestBuilder::Payload](lib/rest-builder/payload.rb) for more information.
341
+
342
+ ## Streaming Responses
343
+
344
+ This one is much harder then streaming requests, since all built-in
345
+ middleware actually assume the responses should be blocking and buffered.
346
+ Say, some JSON parser could not really parse from streams.
347
+
348
+ We solve this issue similarly to the way Rack solves it. That is, we hijack
349
+ the socket. This would be how we're doing:
350
+
351
+ ``` ruby
352
+ sock = client.get('path', {}, RestBuilder::HIJACK => true)
353
+ p sock.read(10)
354
+ p sock.read(10)
355
+ p sock.read(10)
356
+ ```
357
+
358
+ Of course, if we don't want to block in order to get the socket, we could
359
+ always use the callback form:
360
+
361
+ ``` ruby
362
+ client.get('path', {}, RestBuilder::HIJACK => true) do |sock|
363
+ p sock.read(10)
364
+ p sock.read(10)
365
+ p sock.read(10)
366
+ end
367
+ ```
368
+
369
+ Note that since the socket would be put inside `RestBuilder::RESPONSE_SOCKET`
370
+ instead of `RestBuilder::RESPONSE_BODY`, not all middleware would handle the
371
+ socket. In the case of hijacking, `RestBuilder::RESPONSE_BODY` would always
372
+ be mapped to an empty string, as it does not make sense to store the response
373
+ in this case.
374
+
375
+ ## SSE (Server-Sent Events)
376
+
377
+ Not only JavaScript could receive server-sent events, any languages could.
378
+ Doing so would establish a keep-alive connection to the server, and receive
379
+ data periodically. We'll take Firebase as an example:
380
+
381
+ If you are using Firebase, please consider [rest-firebase][] instead.
382
+
383
+ [rest-firebase]: https://github.com/CodementorIO/rest-firebase
384
+
385
+ ``` ruby
386
+ require 'rest-builder'
387
+
388
+ # Streaming over 'users/tom.json'
389
+ cl = RestBuilder::Builder.client.new
390
+ ph = 'https://SampleChat.firebaseIO-demo.com/users/tom.json'
391
+ es = cl.event_source(ph, {}, # this is query, none here
392
+ RestBuilder::REQUEST_HEADERS =>
393
+ {'Accept' => 'text/event-stream'})
394
+
395
+ @reconnect = true
396
+
397
+ es.onopen { |sock| p sock } # Called when connected
398
+ es.onmessage{ |event, data, sock| p event, data } # Called for each message
399
+ es.onerror { |error, sock| p error } # Called whenever there's an error
400
+ # Extra: If we return true in onreconnect callback, it would automatically
401
+ # reconnect the node for us if disconnected.
402
+ es.onreconnect{ |error, sock| p error; @reconnect }
403
+
404
+ # Start making the request
405
+ es.start
406
+
407
+ # Try to close the connection and see it reconnects automatically
408
+ es.close
409
+
410
+ # Update users/tom.json
411
+ p cl.put(ph, '{"some":"data"}')
412
+ p cl.post(ph, '{"some":"other"}')
413
+ p cl.get(ph)
414
+ p cl.delete(ph)
415
+
416
+ # Need to tell onreconnect stops reconnecting, or even if we close
417
+ # the connection manually, it would still try to reconnect again.
418
+ @reconnect = false
419
+
420
+ # Close the connection to gracefully shut it down.
421
+ es.close
422
+ ```
423
+
424
+ Those callbacks would be called in a separate background thread,
425
+ so we don't have to worry about blocking it. If we want to wait for
426
+ the connection to be closed, we could call `wait`:
427
+
428
+ ``` ruby
429
+ es.wait # This would block until the connection is closed
430
+ ```
431
+
432
+ ## More Control with `request_full`:
433
+
434
+ You can also use `request_full` to retrieve everything including response
435
+ status, response headers, and also other rest-builder options. But since
436
+ using this interface is like using Rack directly, you have to build the env
437
+ manually. To help you build the env manually, everything has a default,
438
+ including the path.
439
+
440
+ ``` ruby
441
+ client.request_full(RestBuilder::REQUEST_PATH =>
442
+ 'http://example.com/')[RestBuilder::RESPONSE_BODY]
443
+ client.request_full(RestBuilder::REQUEST_PATH =>
444
+ 'http://example.com/')[RestBuilder::RESPONSE_STATUS]
445
+ client.request_full(RestBuilder::REQUEST_PATH =>
446
+ 'http://example.com/')[RestBuilder::RESPONSE_HEADERS]
447
+ # Headers are normalized with all upper cases and
448
+ # dashes are replaced by underscores.
449
+
450
+ # To make POST (or any other request methods) request:
451
+ client.request_full(RestBuilder::REQUEST_PATH => 'http://example.com/',
452
+ RestBuilder::REQUEST_METHOD =>
453
+ :post)[RestBuilder::RESPONSE_STATUS] # 404
454
+ ```
455
+
456
+ ## Advanced Concurrent HTTP Requests -- Embrace the Future
457
+
458
+ ### The Interface
459
+
460
+ There are a number of different ways to make concurrent requests in
461
+ rest-builder. They could be roughly categorized to two different forms.
462
+ One is using the well known callbacks, while the other one is using
463
+ through a technique called [future][]. Basically, it means it would
464
+ return you a promise, which would eventually become the real value
465
+ (response here) you were asking for whenever you really want it.
466
+ Otherwise, the program keeps running until the value is evaluated,
467
+ and blocks there if the computation (response) hasn't been done yet.
468
+ If the computation is already done, then it would simply return you
469
+ the result.
470
+
471
+ Here's a very simple example for using futures:
472
+
473
+ ``` ruby
474
+ client = YourClient.new
475
+ puts "httpclient with threads doing concurrent requests"
476
+ a = [client.get('http://example.com/a'), client.get('http://example.com/b')]
477
+ puts "It's not blocking... but doing concurrent requests underneath"
478
+ p a # Here we want the values, so it blocks here
479
+ puts "DONE"
480
+ ```
481
+
482
+ And here's a corresponded version for using callbacks:
483
+
484
+ ``` ruby
485
+ client = YourClient.new
486
+ puts "httpclient with threads doing concurrent requests"
487
+ client.get('http://example.com/a'){ |v|
488
+ p v
489
+ }.
490
+ get('http://example.com/b'){ |v|
491
+ p v
492
+ }
493
+ puts "It's not blocking... but doing concurrent requests underneath"
494
+ client.wait # until all requests are done
495
+ puts "DONE"
496
+ ```
497
+
498
+ You can pick whatever works for you.
499
+
500
+ [future]: http://en.wikipedia.org/wiki/Futures_and_promises
501
+
502
+ ## Configure the underlying HTTP engine
503
+
504
+ Occasionally we might want to configure the underlying HTTP engine, which
505
+ for now is [httpclient][]. For example, we might not want to decompress
506
+ gzip automatically, (rest-core configures httpclient to request and
507
+ decompress gzip automatically). or we might want to skip verifying SSL
508
+ in some situation. (e.g. making requests against a self-signed testing server)
509
+
510
+ In such cases, we could use `config_engine` option to configure the underlying
511
+ engine. This could be set with request based, client instance based, or
512
+ client class based. Please refer to:
513
+ [How We Pick the Default Value](#how-we-pick-the-default-value),
514
+ except that there's no middleware for `config_engine`.
515
+
516
+ Here are some examples:
517
+
518
+ ``` ruby
519
+ # class based:
520
+ def YourClient.default_config_engine
521
+ lambda do |engine|
522
+ # disable auto-gzip:
523
+ engine.transparent_gzip_decompression = false
524
+
525
+ # disable verifying SSL
526
+ engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
527
+ end
528
+ end
529
+
530
+ # instance based:
531
+ client = YourClient.new(:config_engine => lambda do |engine|
532
+ # disable auto-gzip:
533
+ engine.transparent_gzip_decompression = false
534
+
535
+ # disable verifying SSL
536
+ engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
537
+ end)
538
+
539
+ # request based:
540
+ client.get('http://example.com/', {}, :config_engine => lambda do |engine|
541
+ # disable auto-gzip:
542
+ engine.transparent_gzip_decompression = false
543
+
544
+ # disable verifying SSL
545
+ engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
546
+ end)
547
+ ```
548
+
549
+ As we stated in
550
+ [How We Pick the Default Value](#how-we-pick-the-default-value),
551
+ the priority here is:
552
+
553
+ 0. request based
554
+ 0. instance based
555
+ 0. class based
556
+
557
+ ## CONTRIBUTORS:
558
+
559
+ * Lin Jen-Shin (@godfat)
560
+
561
+ ## LICENSE:
562
+
563
+ Apache License 2.0
564
+
565
+ Copyright (c) 2016, Lin Jen-Shin (godfat)
566
+
567
+ Licensed under the Apache License, Version 2.0 (the "License");
568
+ you may not use this file except in compliance with the License.
569
+ You may obtain a copy of the License at
570
+
571
+ <http://www.apache.org/licenses/LICENSE-2.0>
572
+
573
+ Unless required by applicable law or agreed to in writing, software
574
+ distributed under the License is distributed on an "AS IS" BASIS,
575
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
576
+ See the License for the specific language governing permissions and
577
+ limitations under the License.