├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── CHANGES.md ├── Gemfile ├── LICENSE ├── NOTE.md ├── README.md ├── Rakefile ├── TODO.md ├── example ├── simple.rb └── use-cases.rb ├── lib ├── rest-core.rb └── rest-core │ ├── client │ └── universal.rb │ ├── client_oauth1.rb │ ├── event.rb │ ├── middleware │ ├── auth_basic.rb │ ├── bypass.rb │ ├── cache.rb │ ├── clash_response.rb │ ├── common_logger.rb │ ├── default_headers.rb │ ├── default_payload.rb │ ├── default_query.rb │ ├── default_site.rb │ ├── defaults.rb │ ├── error_detector.rb │ ├── error_detector_http.rb │ ├── error_handler.rb │ ├── follow_redirect.rb │ ├── json_request.rb │ ├── json_response.rb │ ├── oauth1_header.rb │ ├── oauth2_header.rb │ ├── oauth2_query.rb │ ├── query_response.rb │ ├── retry.rb │ ├── smash_response.rb │ └── timeout.rb │ ├── test.rb │ ├── util │ ├── clash.rb │ ├── config.rb │ ├── dalli_extension.rb │ ├── hmac.rb │ ├── json.rb │ ├── parse_link.rb │ ├── parse_query.rb │ └── smash.rb │ └── version.rb ├── rest-core.gemspec └── test ├── config └── rest-core.yaml ├── test_auth_basic.rb ├── test_cache.rb ├── test_clash.rb ├── test_clash_response.rb ├── test_client_oauth1.rb ├── test_config.rb ├── test_dalli_extension.rb ├── test_default_headers.rb ├── test_default_payload.rb ├── test_default_query.rb ├── test_default_site.rb ├── test_error_detector.rb ├── test_error_detector_http.rb ├── test_error_handler.rb ├── test_follow_redirect.rb ├── test_json_request.rb ├── test_json_response.rb ├── test_oauth1_header.rb ├── test_oauth2_header.rb ├── test_parse_link.rb ├── test_query_response.rb ├── test_retry.rb ├── test_smash.rb ├── test_smash_response.rb ├── test_timeout.rb └── test_universal.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | /coverage/ 3 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | stages: 3 | - test 4 | 5 | .test: 6 | stage: test 7 | image: ruby:${RUBY_VERSION}-bullseye 8 | variables: 9 | GIT_DEPTH: "1" 10 | GIT_SUBMODULE_STRATEGY: recursive 11 | # httpclient does not work with frozen string literal 12 | # RUBYOPT: --enable-frozen-string-literal 13 | before_script: 14 | - bundle install --retry=3 15 | - unset CI # Coverage doesn't work well with frozen literal 16 | script: 17 | - ruby -vr bundler/setup -S rake test 18 | 19 | .json: 20 | variables: 21 | json_lib: json 22 | 23 | .yajl: 24 | variables: 25 | json_lib: yajl 26 | 27 | ruby:3.0 json: 28 | extends: 29 | - .test 30 | - .json 31 | variables: 32 | RUBY_VERSION: '3.0' 33 | 34 | ruby:3.0 yajl: 35 | extends: 36 | - .test 37 | - .yajl 38 | variables: 39 | RUBY_VERSION: '3.0' 40 | 41 | ruby:3.1 json: 42 | extends: 43 | - .test 44 | - .json 45 | variables: 46 | RUBY_VERSION: '3.1' 47 | 48 | ruby:3.1 yajl: 49 | extends: 50 | - .test 51 | - .yajl 52 | variables: 53 | RUBY_VERSION: '3.1' 54 | 55 | ruby:3.2 json: 56 | extends: 57 | - .test 58 | - .json 59 | variables: 60 | RUBY_VERSION: '3.2' 61 | 62 | ruby:3.2 yajl: 63 | extends: 64 | - .test 65 | - .yajl 66 | variables: 67 | RUBY_VERSION: '3.2' 68 | 69 | jruby:latest: 70 | extends: 71 | - .test 72 | image: jruby:latest 73 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "task"] 2 | path = task 3 | url = https://github.com/godfat/gemgem.git 4 | [submodule "rest-builder"] 5 | path = rest-builder 6 | url = https://github.com/godfat/rest-builder.git 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGES 2 | 3 | ## rest-core 4.0.1 -- 2017-05-15 4 | 5 | * [RC::Cache] Check against `Numeric` instead of `Fixnum` for Ruby 2.4 6 | compatibility. 7 | * [RC::JsonResponse] Fix encoding issue when trying to remove BOM. 8 | See https://github.com/godfat/rest-core/issues/24 9 | and https://github.com/godfat/rest-core/commit/4123ca485ecc3b9d31749423f7039bfa1652a3a3 10 | Thanks AshwiniDoddamaniFluke 11 | 12 | ## rest-core 4.0.0 -- 2016-02-04 13 | 14 | Now the core functionality was extracted to a new gem, rest-builder. 15 | rest-core from now on would just bundle middleware and some utilities. 16 | Things like `RC::Builder` should still work for the sake of compatibility, 17 | but it's actually `RestBuilder::Builder` underneath. Note that the core 18 | concurrency facility was extracted to another new gem, promise_pool. 19 | Since some parts were also rewritten, things might change somehow. 20 | At least no public APIs were changed. It's completely compatible. 21 | 22 | ### Incompatible changes 23 | 24 | * `RC::Simple` was removed. There's no point for that. 25 | * `RC.id` was removed, in favour of `:itself.to_proc`. 26 | * Include `RC::Middleware` would no longer include `RC` as well. 27 | 28 | ### Enhancements 29 | 30 | * We no longer `autoload` a lot of stuffs, rather, we just load it. 31 | * Previously, only when you try to _peek_ the future, the callback would be 32 | called. From now on, whenever the request is done, it would call the 33 | callback regardless. However, if there's an exception, it won't be raised. 34 | It would only raise whenever you _peek_ it. An obvious difference for this 35 | is the `RC::CommonLogger`. Previously since callback would only be called 36 | when you _peek_ it, the time would be the difference from request done to 37 | you _peeked_ it. From now on, it would just be how much time the request 38 | has been taken, regardless when you _peek_ it. 39 | 40 | ## rest-core 3.6.0 -- 2016-01-27 41 | 42 | ### Incompatible changes 43 | 44 | * Client.defer would now raise an error if the block would raise an error. 45 | 46 | ### Enhancements 47 | 48 | * EventSource would now try to close the socket (actually, pipe from 49 | httpclient) if there's no data coming in in 35 seconds, 50 | (RC::EventSource::READ_WAIT) therefore we could reconnect in this case. 51 | This is mostly for rest-firebase, reference: 52 | 53 | We would surely need a way to configure this timeout rather than 54 | hard coding it for 35 seconds as different services could use different 55 | timeout. Thanks @volksport for investigating this. 56 | 57 | ## rest-core 3.5.92 -- 2015-12-28 58 | 59 | ### Enhancements 60 | 61 | * Added `RestCore::DalliExtension` for making `Dalli::Client` walk and quad 62 | like `Hash` so that you could pass it as a cache client to 63 | `RestCore::Cache`. 64 | 65 | ## rest-core 3.5.91 -- 2015-12-11 66 | 67 | ### Bugs fixed 68 | 69 | * Instead of forcing to load `http/cookie_jar/hash_store.rb`, which is only 70 | available when _http-cookie_ is available, we just initialize httpclient 71 | and throw it away. Hopefully this would be more compatible between versions. 72 | 73 | ## rest-core 3.5.9 -- 2015-12-11 74 | 75 | ### Bugs fixed 76 | 77 | * Fixed a potential deadlock or using a partially loaded stuff for 78 | _httpclient_. We fixed this by requiring eagerly instead of loading 79 | it lazily. The offender was: _http-cookie_: `http/cookie_jar/hash_store.rb`. 80 | _httpclient_ could try to load this eagerly or just don't load it since 81 | we're not using `cookie_manager` anyway. The errors we've seen: 82 | * `NoMethodError: undefined method `implementation' for HTTP::CookieJar::AbstractStore:Class` 83 | * `ArgumentError: cookie store unavailable: :hash` 84 | * deadlock (because it's requiring it in a thread) 85 | 86 | ## rest-core 3.5.8 -- 2015-12-07 87 | 88 | ### Enhancements 89 | 90 | * Added `Client.defer` for doing arbitrary asynchronous tasks. 91 | 92 | ## rest-core 3.5.7 -- 2015-09-10 93 | 94 | ### Incompatible changes 95 | 96 | * JSON_REQUEST_METHOD was removed. 97 | 98 | ### Bugs fixed 99 | 100 | * GET/DELETE/HEAD/OPTIONS would no longer try to attach any payload. 101 | 102 | ### Enhancements 103 | 104 | * Introduced Middleware.has_payload? which would detect if the request 105 | should attach a payload or not. 106 | 107 | ## rest-core 3.5.6 -- 2015-07-23 108 | 109 | ### Bugs fixed 110 | 111 | * Removed changes shouldn't be made into JsonResponse. My bad. 112 | I should `git stash` before making releases. 113 | 114 | ## rest-core 3.5.5 -- 2015-07-22 115 | 116 | ### Bugs fixed 117 | 118 | * Fixed a possible data race for thread pool when enqueuing very quickly. 119 | 120 | ## rest-core 3.5.4 -- 2015-01-17 121 | 122 | ### Bugs fixed 123 | 124 | * Fixed a regression where callback is not called for `RC::Cache` when cache 125 | is available. 126 | 127 | ## rest-core 3.5.3 -- 2015-01-11 128 | 129 | ### Bugs fixed 130 | 131 | * Fixed a regression where timeout is not properly handled for thread pool. 132 | 133 | ## rest-core 3.5.2 -- 2015-01-09 134 | 135 | ### Bugs fixed 136 | 137 | * Now callbacks would respect `RC::RESPONSE_KEY`. 138 | * Clear `Thread.current[:backtrace]` after done in thread pool to reduce 139 | memory footprint. 140 | * Fixed backtrace for exception raised in callbacks. 141 | * Fixed some potential corner cases where errors are not properly handled 142 | when timeout happened. 143 | 144 | ## rest-core 3.5.1 -- 2014-12-27 145 | 146 | * Ruby 2.2 compatibility. 147 | 148 | ## rest-core 3.5.0 -- 2014-12-09 149 | 150 | ### Incompatible changes 151 | 152 | * `RC::Builder` would now only deep copy arrays and hashes. 153 | * `RC::ErrorHandler`'s only responsibility is now creating the exception. 154 | Raising the exceptions or passing it to the callback is now handled by 155 | `RC::Client` instead. (Thanks Andrew Clunis, #6) 156 | * Since exceptions are raised by `RC::Client` now, `RC::Timeout` would not 157 | raise any exception, but just hand over to `RC::Client`. 158 | * Support for Ruby version < 1.9.2 is dropped. 159 | 160 | ### Bugs fixed 161 | 162 | * Reverted #10 because it caused the other encoding issue. (#12) 163 | * `RC::Client#wait` and `RC::Client.wait` would now properly wait for 164 | `RC::FollowRedirect` 165 | * `RC::Event::CacheHit` is properly logged again. 166 | 167 | ### Enhancements 168 | 169 | * Introduced `RC::Client#error_callback` which would get called for each 170 | exceptions raised. This is useful for monitoring and logging errors. 171 | 172 | * Introduced `RC::Retry` which could retry the request upon certain errors. 173 | Specify `max_retries` for maximum times for retrying, and `retry_exceptions` 174 | for which exceptions should be trying. 175 | Default is `[IOError, SystemCallError]` 176 | 177 | * Eliminated a few warnings when `-w` is used. 178 | 179 | ## rest-core 3.4.1 -- 2014-11-29 180 | 181 | ### Bugs fixed 182 | 183 | * Handle errors for `RC::EventSource` more conservatively to avoid any 184 | potential deadlock. 185 | 186 | * It would not deadlock even if logging failed. 187 | 188 | ## rest-core 3.4.0 -- 2014-11-26 189 | 190 | ### Incompatible changes 191 | 192 | * Removed rest-client support. 193 | * Removed net-http-persistent support. 194 | * Removed patch for old multi-json. 195 | 196 | ### Bugs fixed 197 | 198 | * `RC::JsonRequest` can now POST, PUT, or PATCH with a single `false`. 199 | 200 | * Previously, we're not handling timeout correctly. We left all that to 201 | httpclient, and which would raise `HTTPClient::ConnectTimeoutError` and 202 | `HTTPClient::ReceiveTimeoutError`. The problem is that connecting and 203 | receiving are counted separately. That means, if we have 30 seconds timeout, 204 | we might be ending up with 58 seconds requesting time, for 29 seconds 205 | connecting time and 29 seconds receiving time. Now it should probably 206 | interrupt the request in 30 seconds by handling this by ourselves with a 207 | timer thread in the background. The timer thread would be shut down 208 | automatically if there's no jobs left, and it would recreate the thread 209 | when there's a job comes in, keeping working until there's no jobs left. 210 | 211 | ### Enhancements 212 | 213 | * Introduced `RC::Timer.interval` which is the interval to check if there's 214 | a request timed out. The default interval is 1 second, which means it would 215 | check if there's a request timed out for every 1 second. This also means 216 | timeout with less than 1 second won't be accurate at all. You could 217 | decrease the value if you need timeout less than 1 second, or increase it 218 | if your timeout is far from 30 second by calling `RC::Timer.interval = 5`. 219 | 220 | ## rest-core 3.3.3 -- 2014-11-07 221 | 222 | ### Bugs fixed 223 | 224 | * `RC::EventSource` would now properly reconnect for SystemCallError such as 225 | `Errno::ECONNRESET`. 226 | 227 | * It would now always emit a warning whenever there's an exception raised 228 | asynchronously. 229 | 230 | * All exceptions raised from a thread or thread pool would now have a 231 | proper backtrace. This was fixed by introducing `RC::Promise.backtrace` 232 | 233 | ### Enhancements 234 | 235 | * Introduced `RC::Promise.backtrace`. Using this in a callback could give you 236 | proper backtrace, comparing to `caller` would only give you the backtrace 237 | for current thread. 238 | 239 | * Introduced `RC::Promise.set_backtrace`. Using this we could set exceptions 240 | with proper backtrace. 241 | 242 | ## rest-core 3.3.2 -- 2014-10-11 243 | 244 | * Just use `File.join` for `RC::DefaultSite` as `File::SEPARATOR` is 245 | universally `/` and it would not try to raise exceptions for improperly 246 | encoded URI. #11 Man Vuong (@kidlab) 247 | 248 | ## rest-core 3.3.1 -- 2014-10-08 249 | 250 | * `RC::Oauth1Header` would now properly encode queries in oauth_callback. 251 | rest-more#6 khoa nguyen (@khoan) 252 | 253 | * Made all literal regular expression UTF-8 encoded, fixing encoding issue 254 | on JRuby. #10 Joe Chen (@joe1chen) 255 | 256 | * Now `RC::DefaultSite` would use `URI.join` to prepend the default site, 257 | therefore eliminating duplicated / or missing /. #11 Man Vuong (@kidlab) 258 | 259 | * Fixed deprecation warnings from `URI.escape`. Lin Jen-Shin (@godfat) 260 | 261 | * Now we properly wait for the callback to be called to consider the request 262 | is done. Lin Jen-Shin (@godfat) 263 | 264 | ## rest-core 3.3.0 -- 2014-08-25 265 | 266 | ### Incompatible changes 267 | 268 | * Removed `RC::Wrapper`. Apparently it's introducing more troubles than the 269 | benefit than it brings. Currently, only `RC::Cache` is really using it, 270 | and now the old functionality is merged back to `RC::Builder`. 271 | 272 | * Therefore `RC::Cache` is no longer accepting a block. 273 | 274 | * `RC::Universal` is then updated accordingly to respect the new `RC::Cache`. 275 | 276 | ### Enhancements 277 | 278 | * Now `RC::DefaultQuery`, `RC::DefaultPayload`, and `RC::DefaultHeaders` 279 | work the same way. Previously they merge hashes slightly differently. 280 | 281 | * Introduced `RC::Middleware#member=` in addition to `RC::Middleware#member`. 282 | 283 | * RC::JsonResponse would now strip the problematic UTF-8 BOM before parsing. 284 | This was introduced because Stackoverflow would return it. We also 285 | try to not raise any encoding issues here. 286 | 287 | ## rest-core 3.2.0 -- 2014-06-27 288 | 289 | ### Enhancements 290 | 291 | * Introduced `RC::JsonResponse::ParseError` which would be a subclass of 292 | `RC::Json::ParseError`, and contain the original text before parsing. 293 | This should be great for debugging. For example, some servers might 294 | return HTML instead JSON, and we would like to read the HTML to learn 295 | more why they are doing this. 296 | 297 | * Introduced `RC::ParseLink` utility for parsing web links described in 298 | [RFC5988](http://tools.ietf.org/html/rfc5988) 299 | 300 | * Introduced `RC::Clash` which is a hash wrapper protecting you from getting 301 | nils from hashes. This is useful whenever we want to access a value deeply 302 | inside a hash. For example: `json['a']['b']['c']['d']` would never fail 303 | due to nils. Note that `RC::Clash` is recursive. 304 | 305 | * Introduced `RC::Smash` which is a hash wrapper protecting you from getting 306 | nils from hashes. This is useful whenever we want to access a value deeply 307 | inside a hash. Instead of using multiple layers of subscript operators, 308 | we try to use a "path" to specify which value we want. For example: 309 | `json['a', 'b', 'c', 'd']` is the same as `json['a']['b']['c']['d']` but 310 | with protection from nils in the middle. Note that `RC:Smash` is *not* 311 | recursive. 312 | 313 | * Introduced `RC::ClashResponse` which would wrap the response inside 314 | `RC::Clash`. This is useful along with `RC::JsonResponse`. 315 | 316 | * Introduced `RC::SmashResponse` which would wrap the response inside 317 | `RC::Smash`. This is useful along with `RC::JsonResponse`. 318 | 319 | * Introduced `RC::Client.shutdown` which is essentially the same as 320 | `RC::Client.thread_pool.shutdown` and `RC::Client.wait`. 321 | 322 | * `RC::ClashResponse` and `RC::SmashResponse` is added into `RC::Universal` 323 | with `{:clash_response => false, :smash_response => false}` by default. 324 | 325 | * Introduced `RC::Promise#future_response` to allow you customize the 326 | behaviour of promises more easily. 327 | 328 | * Introduced `RC::Promise.claim` to allow you pre-fill a promise. 329 | 330 | * Introduced `RC::Promise#then` to allow you append a callback whenever 331 | the response is ready. The type should be: `Env -> Response` 332 | 333 | * Now `RC::Promise#inspect` would show REQUEST_URI instead of REQUEST_PATH, 334 | which should be easier to debug. 335 | 336 | * Introduced `RC::ThreadPool#size` which is a short hand for 337 | `RC::ThreadPool#workers.size`. 338 | 339 | ### Bugs fixed 340 | 341 | * Inheritance with `RC::Client` now works properly. 342 | * Now `RC::Cache` properly return cached headers. 343 | * Now `RC::Cache` would work more like `RC::Engine`, wrapping responses 344 | inside futures. 345 | 346 | ## rest-core 3.1.1 -- 2014-05-13 347 | 348 | ### Enhancements 349 | 350 | * Introduced `RC::Client.wait` along with `RC::Client#wait`. It would collect 351 | all the promises from all instances of the client, so we could wait on all 352 | promises we're waiting. This would make writing graceful shutdown much 353 | easier. For example, we could have: `at_exit{ RC::Universal.wait }` to 354 | wait on all requests from the universal client before exiting the process. 355 | 356 | ## rest-core 3.1.0 -- 2014-05-09 357 | 358 | ### Incompatible changes 359 | 360 | * Now that the second argument `key` in `RC::Client#request` is replaced by 361 | `RC::RESPONSE_KEY` passed in env. This would make it easier to use and 362 | more consistent internally. 363 | * Now RC::EventSource#onmessage would receive the event in the first argument, 364 | and the data in the second argument instead of a hash in the first argument. 365 | 366 | ### Enhancements 367 | 368 | * Added RC::REQUEST_URI in the env so that we could access the requesting 369 | URI easily. 370 | * Added middleware RC::QueryResponse which could parse query in response. 371 | * Added RC::Client.event_source_class which we could easily swap the class 372 | used for event_source. Used in Firebase client to parse data in JSON. 373 | * Now methods in RC::EventSource would return itself so that we could chain 374 | callbacks. 375 | * Added RC::EventSource#onreconnect callback to handle if we're going to 376 | reconnect automatically or not. 377 | * RC::Config was extracted from rest-more, which could help us manage config 378 | files. 379 | * RC::Json using JSON would now parse in quirks_mode, so that it could parse 380 | not only JSON but also a single value. 381 | 382 | ### Bugs Fixes 383 | 384 | * We should never cache hijacked requests. 385 | * Now we preserve payload and properly ignore query for RC::FollowRedirect. 386 | 387 | ## rest-core 3.0.0 -- 2014-05-04 388 | 389 | Highlights: 390 | 391 | * Hijack for streaming responses 392 | * EventSource for SSE (server-sent events) 393 | * Thread pool 394 | * Keep-alive connections from httpclient 395 | 396 | ### Incompatible changes 397 | 398 | * Since eventmachine is buggy, and fibers without eventmachine doesn't make 399 | too much sense, we have removed the support for eventmachine and fibers. 400 | 401 | * We also changed the default HTTP client from rest-client to httpclient. 402 | If you still want to use rest-client, switch it like this: 403 | 404 | RC::Builder.default_engine = RC::RestClient 405 | 406 | Be warned, we might remove rest-client support in the future. 407 | 408 | * `RC::Client#options` would now return the headers instead of response body. 409 | 410 | * Removed support for Ruby 1.8.7 without openssl installed. 411 | 412 | * `RC::Future` is renamed to `RC::Promise`, and `RC::Future::Proxy` is 413 | renamed to `RC::Promise::Future`. 414 | 415 | ### Enhancements 416 | 417 | * HIJACK support, which is similar to Rack's HIJACK feature. If you're 418 | passing `{RC::HIJACK => true}` whenever making a request, rest-core would 419 | rather set the `RC::RESPONSE_BODY` as an empty string, and set 420 | `RC::RESPONSE_SOCKET` as a socket for the response. This is used for 421 | `RC::EventSource`, and you could also use this for streaming the response. 422 | Note that this only works for default engine, httpclient. 423 | 424 | * Introduce `RC::EventSource`. You could obtain the object via 425 | `RC::Client#event_source`, and then setup `onopen`, `onmessage`, and 426 | `onerror` respectively, and then call `RC::EventSource#start` to begin 427 | making the request, and receive the SSE (sever-sent events) from the server. 428 | This is used in `RC::Firebase` from rest-more. 429 | 430 | * Now we have thread pool support. We could set the pool size with: 431 | `RC::YourClient.pool_size = 10` and thread idle time with: 432 | `RC::YourClient.pool_idle_time = 60`. By default, `pool_size` is 0 433 | which means we don't use a thread pool. Setting it to a negative number 434 | would mean do not spawn any threads, just make a blocking request. 435 | `pool_idle_time` is default to 60, meaning an idle thread would be shut 436 | down after 60 seconds without being used. 437 | 438 | * Since we're now using httpclient by default, we should also take the 439 | advantage of using keep-alive connections for the same host. 440 | 441 | * Now `RC::Middleware#fail` and `RC::Middleware#log` could accept `nil` as 442 | an input, which would then do nothing. This could much simplify the code 443 | building middleware. 444 | 445 | * Now we're using timers gem which should be less buggy from previous timeout. 446 | 447 | ## rest-core 2.1.2 -- 2013-05-31 448 | 449 | ### Incompatible changes 450 | 451 | * Remove support for Ruby < 1.9.2 452 | 453 | ### Bugs fixes 454 | 455 | * [`Client`] Fixed a bug where if we're using duplicated attributes. 456 | 457 | ## rest-core 2.1.1 -- 2013-05-21 458 | 459 | ### Bugs fixes 460 | 461 | * Fixed em-http-request support. 462 | 463 | ### Enhancements 464 | 465 | * [`Payload`] Now it is a class rather than a module. 466 | * [`Paylaod`] Introduced `Payload.generate_with_headers`. 467 | * [`Paylaod`] Give a `nil` if payload passing to `Payload.generate` should 468 | not have any payload at all. 469 | 470 | ## rest-core 2.1.0 -- 2013-05-08 471 | 472 | ### Incompatible changes 473 | 474 | * We no longer support Rails-like POST payload, like translating 475 | `{:foo => [1, 2]}` to `'foo[]=1&foo[]=2'`. It would now be translated to 476 | `'foo=1&foo=2'`. If you like `'foo[]'` as the key, simply pass it as 477 | `{'foo[]' => [1, 2]}`. 478 | 479 | * This also applies to nested hashes like `{:foo => {:bar => 1}`. If you 480 | want that behaviour, just pass `{'foo[bar]' => 1}` which would then be 481 | translated to `'foo[bar]=1'`. 482 | 483 | ### Bugs fixes 484 | 485 | * [`Payload`] Now we could correctly support payload with "foo=1&foo=2". 486 | * [`Client`] Fix inspect spacing. 487 | 488 | ### Enhancements 489 | 490 | * [`Payload`] With this class introduced, replacing rest-client's own 491 | payload implementation, we could pass StringIO or other sockets as the 492 | payload body. This would also fix the issue that using the same key for 493 | different values as allowed in the spec. 494 | * [`EmHttpRequest`] Send payload as a file directly if it's a file. Buffer 495 | the payload into a tempfile if it's from a socket or a large StringIO. 496 | This should greatly reduce the memory usage as we don't build large 497 | Ruby strings in the memory. Streaming is not yet supported though. 498 | * [`Client`] Make inspect shorter. 499 | * [`Client`] Introduce Client#default_env 500 | * [`Middleware`] Introduce Middleware.percent_encode. 501 | * [`Middleware`] Introduce Middleware.contain_binary?. 502 | 503 | ## rest-core 2.0.4 -- 2013-04-30 504 | 505 | * [`EmHttpRequest`] Use `EM.schedule` to fix thread-safety issue. 506 | 507 | ## rest-core 2.0.3 -- 2013-04-01 508 | 509 | * Use `URI.escape(string, UNRESERVED)` for URI escaping instead of 510 | `CGI.escape` 511 | 512 | * [`Defaults`] Use `respond_to_missing?` instead of `respond_to?` 513 | 514 | ## rest-core 2.0.2 -- 2013-02-07 515 | 516 | ### Bugs fixes 517 | 518 | * [`Cache`] Fix cache with multiline response body. This might invalidate 519 | your existing cache. 520 | 521 | ## rest-core 2.0.1 -- 2013-01-08 522 | 523 | ### Bugs fixes 524 | 525 | * Don't walk into parent's constants in `RC.eagerload`. 526 | * Also rescue `NameError` in `RC.eagerload`. 527 | 528 | ### Enhancements 529 | 530 | * Remove unnecessary `future.wrap` in `EmHttpRequest`. 531 | * Introduce Future#callback_in_async. 532 | * We would never double resume the fiber, so no need to rescue `FiberError`. 533 | 534 | ## rest-core 2.0.0 -- 2012-10-31 535 | 536 | This is a major release which introduces some incompatible changes. 537 | This is intended to cleanup some internal implementation and introduce 538 | a new mechanism to handle multiple requests concurrently, avoiding needless 539 | block. 540 | 541 | Before we go into detail, here's who can upgrade without changing anything, 542 | and who should make a few adjustments in their code: 543 | 544 | * If you're only using rest-more, e.g. `RC::Facebook` or `RC::Twitter`, etc., 545 | you don't have to change anything. This won't affect rest-more users. 546 | (except that JsonDecode is renamed to JsonResponse, and json_decode is 547 | renamed to json_response.) 548 | 549 | * If you're only using rest-core's built in middlewares to build your own 550 | clients, you don't have to change anything as well. All the hard works are 551 | done in rest-core. (except that ErrorHandler works a bit differently now. 552 | We'll talk about detail later.) 553 | 554 | * If you're building your own middlewares, then you are the ones who need to 555 | make changes. `RC::ASYNC` is changed to a flag to mean whether the callback 556 | should be called directly, or only after resuming from the future (fiber 557 | or thread). And now you have always to get the response from `yield`, that 558 | is, you're forced to pass a callback to `call`. 559 | 560 | This might be a bit user unfriendly at first glimpse, but it would much 561 | simplify the internal structure of rest-core, because in the middlewares, 562 | you don't have to worry if the user would pass a callback or not, branching 563 | everywhere to make it work both synchronously and asynchronously. 564 | 565 | Also, the old fiber based asynchronous HTTP client is removed, in favor 566 | of the new _future_ based approach. The new one is more like a superset 567 | of the old one, which have anything the old one can provide. Yet internally 568 | it works a lot differently. They are both synchronous to the outsides, 569 | but while the old one is also synchronous inside, the new one is 570 | asynchronous inside, just like the purely asynchronous HTTP client. 571 | 572 | That is, internally, it's always asynchronously, and fiber/async didn't 573 | make much difference here now. This is also the reason why I removed 574 | the old fiber one. This would make the middleware implementation much 575 | easier, considering much fewer possible cases. 576 | 577 | If you don't really understand what above does mean, then just remember, 578 | now we ask all middlewares work asynchronously. You have always to work 579 | with callbacks which passed along in `app.call(env){ |response| }` 580 | That's it. 581 | 582 | So what's the most important improvement? From now on, we have only two 583 | modes. One is callback mode, in which case `env[ASYNC]` would be set, and 584 | the callback would be called. No exception would be raised in this case. 585 | If there's an exception, then it would be passed to the block instead. 586 | 587 | The other mode, which is synchronous, is achieved by the futures. We have 588 | two different kinds of futures for now, one is thread based, and the other 589 | is fiber based. For RestClient, thread based future would be used. For 590 | EventMachine, depending on the context, if the future is created on the 591 | main thread, then it would assume it's also wrapped inside a fiber. Since, 592 | we can never block the event loop! If you're not calling it in a thread, 593 | you must call it in a fiber. But if you're calling it in a thread, then 594 | the thread based future would be picked. This is because otherwise it won't 595 | work well exchanging information around threads and fibers. 596 | 597 | In short, rest-core would run concurrently in all contexts, archived by 598 | either threads or fibers depending on the context, and it would pick the 599 | right strategy for you. 600 | 601 | You can see [use-cases.rb][] for all possible use cases. 602 | 603 | It's a bit outdated, but you can also checkout my blog post. 604 | [rest-core 2.0 roadmap, thunk based response][post] 605 | (p.s. now thunk is renamed to future) 606 | 607 | [use-cases.rb]: https://github.com/godfat/rest-core/blob/master/example/use-cases.rb 608 | [post]: http://blogger.godfat.org/2012/06/rest-core-20-roadmap-thunk-based.html 609 | 610 | ### Incompatible changes 611 | 612 | * [JsonDecode] is renamed to JsonResponse, and json_decode is also renamed 613 | to json_response. 614 | * [Json] Now you can use `Json.decode` and `Json.encode` to parse and 615 | generate JSONs, instead of `JsonDecode.json_decode`. 616 | * [Cache] Support for "cache.post" is removed. 617 | * [Cache] The cache key is changed accordingly to support cache for headers 618 | and HTTP status. If you don't have persistent cache, this doesn't matter. 619 | 620 | * [EmHttpRequestFiber] is removed in favor of `EmHttpRequest` 621 | * cool.io support is removed. 622 | * You must provide a block to `app.call(env){ ... }`. 623 | * Rename Wrapper#default_app to Wrapper#default_engine 624 | 625 | ### Enhancements 626 | 627 | * The default engine is changed from `RestClient` to `Auto`, which would 628 | be using `EmHttpRequest` under the context of a event loop, while 629 | use `RestClient` in other context as before. 630 | 631 | * `RestCore.eagerload` is introduced to load all constants eagerly. You can 632 | use this before loading the application to avoid thread-safety issue in 633 | autoload. For the lazies. 634 | 635 | * [JsonResponse] This is originally JsonDecode, and now we prefer multi_json 636 | first, yajl-ruby second, lastly json. 637 | * [JsonResponse] give JsonResponse a default header Accept: application/json, 638 | thanks @ayamomiji 639 | * [JsonRequest] This middleware would encode your payload into a JSON. 640 | * [CommonLogger] Now we log the request method as well. 641 | * [DefaultPayload] Accept arbitrary payload. 642 | * [DefaultQuery] Now before merging queries, converting every single key into 643 | a string. This allows you to use :symbols for default query. 644 | 645 | * [ErrorHandler] So now ErrorHandler is working differently. It would first 646 | try to see if `env[FAIL]` has any exception in it. If there is, then raise 647 | it. Otherwise it would call error_handler and expect it to generate an 648 | error object. If the error object is an exception, then raise it. If it's 649 | not, then it would merge it into `env[FAIL]`. On the other hand, in the 650 | case of using callbacks instead of futures, it would pass the exception 651 | as the `env[RESPONSE_BODY]` instead. The reason is that you can't raise 652 | an exception asynchronously and handle it safely. 653 | 654 | * [Cache] Now response headers and HTTP status are also cached. 655 | * [Cache] Not only GET requests are cached, HEAD and OPTIONS are cached too. 656 | * [Cache] The cache key is also respecting the request headers too. Suppose 657 | you're making a request with different Accept header. 658 | 659 | * [Client] Add Client#wait which would block until all requests for this 660 | particular client are done. 661 | 662 | ### Bugs fixes 663 | 664 | * [Middleware] Sort the query before generating the request URI, making 665 | sure the order is always the same. 666 | * [Middleware] The middleware could have no members at all. 667 | * [ParseQuery] The fallback function for the absence of Rack is fixed. 668 | * [Auto] Only use EmHttpRequest if em-http-request is loaded, 669 | thanks @ayamomiji 670 | 671 | ## rest-core 1.0.3 -- 2012-08-15 672 | 673 | ### Enhancements 674 | 675 | * [Client] `client.head` now returns the headers instead of response body. 676 | It doesn't make sense to return the response body, because there's no 677 | such things in a HEAD request. 678 | 679 | ### Bugs fixes 680 | 681 | * [Cache] The cache object you passed in would only need to respond to 682 | `[]` and `[]=`. If the cache object accepts an `:expires_in` option, 683 | then it must also respond to `store`, too. 684 | 685 | * [Oauth1Header] Fixed a long standing bug that tilde (~) shouldn't be 686 | escaped. Many thanks to @brucehsu for discovering this! 687 | 688 | ## rest-core 1.0.2 -- 2012-06-05 689 | 690 | ### Enhancements 691 | 692 | * Some internal refactoring. 693 | 694 | ### Bugs fixes 695 | 696 | * Properly handle asynchronous timers for eventmachine and cool.io. 697 | 698 | ## rest-core 1.0.1 -- 2012-05-14 699 | 700 | * [`Auto`] Check for eventmachine first instead of cool.io 701 | * [`EmHttpRequestFiber`] Also pass callback for errback 702 | * [`DefaultQuery`] Make default query to {} instead of nil 703 | 704 | ## rest-core 1.0.0 -- 2012-03-17 705 | 706 | This is a very significant release. The most important change is now we 707 | support asynchronous requests, by either passing a callback block or using 708 | fibers in Ruby 1.9 to make the whole program still look synchronous. 709 | 710 | Please read [README.md](https://github.com/godfat/rest-core/blob/master/README.md) 711 | or [example](https://github.com/godfat/rest-core/tree/master/example) 712 | for more detail. 713 | 714 | * [`Client`] Client#inspect is fixed for clients which do not have any 715 | attributes. 716 | 717 | * [`Client`] HEAD, OPTIONS, and PATCH requests are added. For example: 718 | 719 | ``` ruby 720 | client = Client.new 721 | client.head('path') 722 | client.options('path') 723 | client.patch('path') 724 | ``` 725 | 726 | * [`Client`] Now if you passed a block to either `get` or `post` or other 727 | requests, the response would be returned to the block instead the caller. 728 | In this case, the return value of `get` or `post` would be the client 729 | itself. For example: 730 | 731 | ``` ruby 732 | client = Client.new 733 | client.get('path'){ |response| puts response.insepct }. 734 | get('math'){ |response| puts response.insepct } 735 | ``` 736 | 737 | * [`RestClient`] Now all the response headers names are converted to upper 738 | cases and underscores (_). Also, if a header has only presented once, it 739 | would not be wrapped inside an array. This is more consistent with 740 | em-http-request, cool.io-http, and http_parser.rb 741 | 742 | * [`RestClient`] From now on, the default HTTP client, i.e. `RestClient` won't 743 | follow any redirect. To follow redirect, please use `FollowRedirect` 744 | middleware. Two reasons. One is that the underlying HTTP client should 745 | be minimal. Another one is that a FollowRedirect middleware could be 746 | used for all HTTP clients. This would make it more consistent across 747 | all HTTP clients. 748 | 749 | * [`RestClient`] Added a patch to avoid `"123".to_i` returning `200`, 750 | please see: 751 | I would remove this once after this patch is merged. 752 | 753 | * [`RestClient`] Added a patch to properly returning response whenever 754 | a redirect is happened. Please see: 755 | 756 | I would remove this once after this patch is merged. 757 | 758 | * [`FollowRedirect`] This middleware would follow the redirect. Pass 759 | :max_redirects for the maximum redirect times. For example: 760 | 761 | ``` ruby 762 | Client = RestCore::Builder.client do 763 | use FollowRedirect, 2 # default :max_redirects 764 | end 765 | client = Client.new 766 | client.get('path', {}, :max_redirects => 5) 767 | ``` 768 | 769 | * [`Middleware`] Added `Middleware#run` which can return the underlying HTTP 770 | client, if you need to know the underlying HTTP client can support 771 | asynchronous requests or not. 772 | 773 | * [`Cache`] Now it's asynchrony-aware. 774 | * [`CommonLogger`] Now it's asynchrony-aware. 775 | * [`ErrorDetector`] Now it's asynchrony-aware. 776 | * [`ErrorHandler`] Now it's asynchrony-aware. 777 | * [`JsonDecode`] Now it's asynchrony-aware. 778 | * [`Timeout`] Now it's asynchrony-aware. 779 | 780 | * [`Universal`] `FollowRedirect` middleware is added. 781 | * [`Universal`] `Defaults` middleware is removed. 782 | 783 | * Added `RestCore::ASYNC` which should be the callback function which is 784 | called whenever the response is available. It's similar to Rack's 785 | async.callback. 786 | 787 | * Added `RestCore::TIMER` which is only used in Timeout middleware. We need 788 | this to disable timer whenever the response is back. 789 | 790 | * [`EmHttpRequestAsync`] This HTTP client accepts a block to make asynchronous 791 | HTTP requests via em-http-request gem. 792 | 793 | * [`EmHttpRequestFiber`] This HTTP client would make asynchronous HTTP 794 | requests with em-http-request but also wrapped inside a fiber, so that it 795 | looks synchronous to the program who calls it. 796 | 797 | * [`EmHttpRequest`] This HTTP client would would use `EmHttpRequestAsync` if 798 | a block (`RestCore::ASYNC`) is passed, otherwise use `EmHttpRequestFiber`. 799 | 800 | * [`CoolioAsync`] This HTTP client is basically the same as 801 | `EmHttpRequestAsync`, but using cool.io-http instead of em-http-request. 802 | 803 | * [`CoolioFiber`] This HTTP client is basically the same as 804 | `EmHttpRequestFiber`, but using cool.io-http instead of em-http-request. 805 | 806 | * [`Coolio`] This HTTP client is basically the same as `EmHttpRequest`, 807 | but using cool.io-http instead of em-http-request. 808 | 809 | * [`Auto`] This HTTP client would auto-select a suitable client. Under 810 | eventmachine, it would use `EmHttpRequest`. Under cool.io, it would use 811 | `Coolio`. Otherwise, it would use `RestClient`. 812 | 813 | ## rest-core 0.8.2 -- 2012-02-18 814 | 815 | ### Enhancements 816 | 817 | * [`DefaultPayload`] This middleware allows you to have default payload for 818 | POST and PUT requests. 819 | 820 | * [`Client`] Now `lighten` would give all Unserializable to nil instead of 821 | false 822 | 823 | ### Bugs fixes 824 | 825 | * [`ErrorDetector`] Now it would do nothing instead of crashing if there's no 826 | any error_detector. 827 | 828 | ## rest-core 0.8.1 -- 2012-02-09 829 | 830 | ### Enhancements 831 | 832 | * [`Wrapper`] Introducing `Wrapper.default_app` (also `Builder.default_app`) 833 | which you can change the default app from `RestClient` to other HTTP 834 | clients. 835 | 836 | ### Bugs fixes 837 | 838 | * [`OAuth1Header`] Correctly handle the signature when it comes to multipart 839 | requests. 840 | 841 | * [`ErrorDetectorHttp`] Fixed argument error upon calling `lighten` for 842 | clients using this middleware. (e.g. rest-more's Twitter client) 843 | 844 | ## rest-core 0.8.0 -- 2011-11-29 845 | 846 | Changes are mostly related to OAuth. 847 | 848 | ### Incompatible changes 849 | 850 | * [`OAuth1Header`] `callback` is renamed to `oauth_callback` 851 | * [`OAuth1Header`] `verifier` is renamed to `oauth_verifier` 852 | 853 | * [`Oauth2Header`] The first argument is changed from `access_token` to 854 | `access_token_type`. Previously, the access_token_type is "OAuth" which 855 | is used in Mixi. But mostly, we might want to use "Bearer" (according to 856 | [OAuth 2.0 spec][]) Argument for the access_token is changed to the second 857 | argument. 858 | 859 | * [`Defaults`] Now we're no longer call `call` for any default values. 860 | That is, if you're using this: `use s::Defaults, :data => lambda{{}}` 861 | that would break. Previously, this middleware would call `call` on the 862 | lambda so that `data` is default to a newly created hash. Now, it would 863 | merely be default to the lambda. To make it work as before, please define 864 | `def default_data; {}; end` in the client directly. Please see 865 | `OAuth1Client` as an example. 866 | 867 | [OAuth 2.0 spec]: http://tools.ietf.org/html/draft-ietf-oauth-v2-22 868 | 869 | ### Enhancements 870 | 871 | * [`AuthBasic`] Added a new middleware which could do 872 | [basic authentication][]. 873 | 874 | * [`OAuth1Header`] Introduced `data` which is a hash and is used to store 875 | tokens and other information sent from authorization servers. 876 | 877 | * [`ClientOauth1`] Now `authorize_url!` accepts opts which you can pass 878 | `authorize_url!(:oauth_callback => 'http://localhost/callback')`. 879 | 880 | * [`ClientOauth1`] Introduced `authorize_url` which would not try to ask 881 | for a request token, instead, it would use the current token as the 882 | request token. If you don't understand what does this mean, then keep 883 | using `authorize_url!`, which would call this underneath. 884 | 885 | * [`ClientOauth1`] Introduced `authorized?` 886 | * [`ClientOauth1`] Now it would set `data['authorized'] = 'true'` when 887 | `authorize!` is called, and it is also used to check if we're authorized 888 | or not in `authorized?` 889 | 890 | * [`ClientOauth1`] Introduced `data_json` and `data_json=` which allow you to 891 | serialize and deserialize `data` with JSON along with a `sig` to check 892 | if it hasn't been changed. You can put this into browser cookie. Because 893 | of the `sig`, you would know if the user changed something in data without 894 | using `consumer_secret` to generate a correct sig corresponded to the data. 895 | 896 | * [`ClientOauth1`] Introduced `oauth_token`, `oauth_token=`, 897 | `oauth_token_secret`, `oauth_token_secret=`, `oauth_callback`, 898 | and `oauth_callback=` which take the advantage of `data`. 899 | 900 | * [`ClientOauth1`] Introduced `default_data` which is a hash. 901 | 902 | [basic authentication]: http://en.wikipedia.org/wiki/Basic_access_authentication 903 | 904 | ## rest-core 0.7.2 -- 2011-11-04 905 | 906 | * Moved rib-rest-core to [rest-more][] 907 | * Moved `RestCore::Config` to [rest-more][] 908 | * Renamed `RestCore::Vendor` to `RestCore::ParseQuery` 909 | 910 | ## rest-core 0.7.0 -- 2011-10-08 911 | 912 | ### IMPORTANT CHANGE! 913 | 914 | From now on, prebuilt clients such as `RC::Facebook`, `RC::Twitter` and 915 | others are moved to [rest-more][]. Since bundler didn't like cyclic 916 | dependency, so rest-core is not depending on rest-more. Please install 917 | _rest-more_ if you want to use them. 918 | 919 | [rest-more]: https://github.com/godfat/rest-more 920 | 921 | ## rest-core 0.4.0 -- 2011-09-26 922 | 923 | ### Incompatible changes 924 | 925 | * [dry] Now `RestCore::Ask` is renamed to `RestCore::Dry` for better 926 | understanding. Thanks miaout17 927 | 928 | * [client] Now `request` method takes an env and an app to make requests, 929 | instead of a weird requests array. 930 | 931 | * [client] Now if you really want to disable something, for example, 932 | disabling cache when the default cache is `Rails.cache`, you'll need to 933 | pass `false` instead of `nil`. This is because `nil` stands for using 934 | defaults in rest-core. 935 | 936 | * [client] Defaults priorities are changed to: 937 | per-request > instance variable > class defaults > middleware defaults 938 | See *test_client.rb* for more detailed definition. If you don't understand 939 | this, don't worry, since then this won't affect you. 940 | 941 | ### Compatible changes 942 | 943 | * [client] Introduced a new method `request_full` which is exactly the same 944 | as `request` but also returns various information from the app, including 945 | `RESPONSE_STATUS` and `RESPONSE_HEADERS` 946 | 947 | * [client] Removed various unused, untested, undocumented legacy from 948 | rest-graph. 949 | 950 | * [error] Introduced `RestCore::Error` which is the base class for all 951 | exceptions raised by rest-core 952 | 953 | * [builder] Now `RestCore::Builder.default_app` is the default app which 954 | would be used for building clients without setting an app. By default, 955 | it's `RestClient`, but you can change it if you like. 956 | 957 | * [builder] It no longer builds a @wrapped app. If you don't understand this, 958 | then this does nothing for you. It's an internal change. (or bug fix) 959 | 960 | * [wrapper] Now `RestCore::Wrapper.default_app` is the default app which 961 | would be used for wrapping middlewares without setting an app. By default, 962 | it's `Dry`, but you can change it if you like. 963 | 964 | * [wrapped] Fixed a bug that force middlewares to implement `members` method, 965 | which should be optional. Thanks miaout17 966 | 967 | * [facebook][rails_util] Now default cache is `Rails.cache` instead of nil 968 | * [simple] Added a Simple client, which only wraps RestClient 969 | * [univeral] Added an Universal client, which could be used for anything 970 | * [flurry] Added a Flurry client, along with its `Flurry::RailsUtil` 971 | * [mixi] Added a Mixi client 972 | 973 | * [bypass] Added a Bypass middleware which does nothing but passing env 974 | * [oauth2_header] OAuth2Header is a middleware which would pass access_token 975 | in header instead of in query string. 976 | * [common_logger] nil object would no longer be logged 977 | * [json_decode] Do nothing if we are being asked for env (dry mode) 978 | * [cache] Now default `:expires_in` is 600 down from 3600 979 | * [middleware] Now not only query values would be escaped, but also keys. 980 | 981 | * [rib-rest-core] Introduced an interactive shell. You'll need [rib][] to 982 | run this: `rib rest-core`. It is using an universal client to access 983 | arbitrary websites. 984 | 985 | [rib]: https://github.com/godfat/rib 986 | 987 | ## rest-core 0.3.0 -- 2011-09-03 988 | 989 | * [facebook] RestGraph is Facebook now. 990 | * [facebook] Facebook::RailsUtil is imported from [rest-graph][] 991 | * [facebook] Use json_decode instead of auto_decode as rest-graph 992 | * [facebook] No longer calls URI.encode on Facebook broken URL 993 | * [twitter] Fixed opts in Twitter#tweet 994 | * [twitter] Introduced Twitter::Error instead of RuntimeError! 995 | * [twitter] By default log nothing 996 | * [rest-core] We no longer explicitly depends on Rack 997 | * [rest-core] Added a shorthand RC to access RestCore 998 | * [rest-core] Eliminated a lot of warnings 999 | * [cache] All clients no longer have default hash cache 1000 | * [oauth2_query] Now we always use the term "access_token" 1001 | * [config] Now Config#load and Config#load_for_rails take namespace 1002 | e.g. rest-core.yaml: 1003 | 1004 | development: 1005 | facebook: 1006 | app_id: 123 1007 | twitter: 1008 | consumer_key: abc 1009 | 1010 | [rest-graph]: https://github.com/godfat/rest-graph 1011 | 1012 | ## rest-core 0.2.3 -- 2011-08-27 1013 | 1014 | * Adding rack as a runtime dependency for now. 1015 | To reduce frustration for new comer... 1016 | 1017 | ## rest-core 0.2.2 -- 2011-08-26 1018 | 1019 | * Adding rest-client as a runtime dependency for now. 1020 | In the future, it should be taken out because of multiple 1021 | selectable HTTP client backend (rest-core app). 1022 | 1023 | ## rest-core 0.2.1 -- 2011-08-25 1024 | 1025 | * [twitter] Fixed default site 1026 | * [twitter] Now Twitter#tweet accepts a 2nd argument to upload an image 1027 | * [oauth1_header] Fixed a bug for multipart posting. Since Rails' uploaded 1028 | file is not an IO object, so we can't only test against 1029 | IO object, but also read method. 1030 | 1031 | ## rest-core 0.2.0 -- 2011-08-24 1032 | 1033 | * First serious release! 1034 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org/' 3 | 4 | gemspec 5 | 6 | gem 'rest-builder', :path => 'rest-builder' 7 | gem 'promise_pool', :path => 'rest-builder/promise_pool' 8 | 9 | gem 'rake' 10 | gem 'pork' 11 | gem 'muack' 12 | gem 'webmock' 13 | 14 | gem 'json' 15 | gem 'json_pure' 16 | gem 'multi_json' 17 | 18 | gem 'rack' 19 | 20 | gem 'simplecov', :require => false if ENV['COV'] 21 | gem 'coveralls', :require => false if ENV['CI'] 22 | 23 | platforms :ruby do 24 | gem 'yajl-ruby' 25 | end 26 | 27 | platforms :jruby do 28 | gem 'jruby-openssl' 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTE.md: -------------------------------------------------------------------------------- 1 | 2 | ## NOTE 3 | 4 | In an era of web service and mashup, we saw a blooming of REST API. One might 5 | wonder, how do we easily and elegantly use those API? We might first try to 6 | find dedicated clients for each web service. It might work pretty well for 7 | the web services using dedicated clients which are designed for, but lately 8 | found that those dedicated clients might not work well together, because 9 | they might have different dependencies with the same purpose, and they might 10 | conflict with each other, or they might be suffering from dependencies hell 11 | or code bloat. 12 | 13 | This might not be a serious issue because that we might only use one or two 14 | web services. But we are all growing, so do our applications. At some point, 15 | the complexity of our applications might grow into something that are very 16 | hard to control. Then we might want to separate accessing each web service 17 | with each different process, say, different dedicated workers. So that we 18 | won't be suffering from the issues described above. 19 | 20 | Yes this would work, definitely. But this might require more efforts than 21 | it should be. If the dedicated clients can work together seamlessly, then 22 | why not? On the other hand, what if there's no dedicated client at the 23 | moment for the web service we want to access? 24 | 25 | Thanks that now we are all favoring REST over SOAP, building a dedicated 26 | client might not be that hard. So why not just build the dedicated clients 27 | ourselves? Yet there's still another issue. We're not only embracing REST, 28 | but also JSON. We would want some kind of JSON support for our hand crafted 29 | clients, but we don't want to copy codes from client A to client B. That's 30 | not so called DRY. We want reusable components, composing them together, 31 | adding some specific features for some particular web service, and then we 32 | get the dedicated clients, not only a generic one which might work for any 33 | web service, but dedicated clients make us feel smooth to use for the 34 | particular web service. 35 | 36 | [rest-core][] is invented for that, inspired by [Rack][] and [Faraday][]. One 37 | can simply use pre-built dedicated clients provided by rest-core, assuming 38 | this would be the most cases. Or if someone is not satisfied with the 39 | pre-built ones, one can use pre-built "middlewares" and "apps" provided by 40 | rest-core, to compose and build the dedicated clients (s)he prefers. Or, even 41 | go further that create custom "middlewares", which should be fairly easy, 42 | and use that along with pre-built ones to compose a very customized client. 43 | 44 | We present you rest-core. 45 | 46 | [rest-core]: https://github.com/godfat/rest-core 47 | [Rack]: https://github.com/rack/rack 48 | [Faraday]: https://github.com/technoweenie/faraday 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest-core [![Pipeline status](https://gitlab.com/godfat/rest-core/badges/master/pipeline.svg)](https://gitlab.com/godfat/rest-core/-/pipelines) 2 | 3 | by Lin Jen-Shin ([godfat](https://godfat.org)) 4 | 5 | Lin Jen-Shin had given a talk about rest-core on 6 | [RubyConf Taiwan 2011][talk]. The slide is in English, but the 7 | talk is in Mandarin. There's another talk about [The Promise of rest-core][] 8 | 9 | [talk]: https://rubyconf.tw/2011/#6 10 | [The Promise of rest-core]: https://godfat.org/slide/2015-01-13-rest-core-promise/ 11 | 12 | ## LINKS: 13 | 14 | * [github](https://github.com/godfat/rest-core) 15 | * [rubygems](https://rubygems.org/gems/rest-core) 16 | * [rdoc](https://rubydoc.info/github/godfat/rest-core) 17 | * [issues](https://github.com/godfat/rest-core/issues) (feel free to ask for support) 18 | 19 | ## DESCRIPTION: 20 | 21 | Various [rest-builder](https://github.com/godfat/rest-builder) middleware 22 | for building REST clients. 23 | 24 | Checkout [rest-more](https://github.com/godfat/rest-more) for pre-built 25 | clients. 26 | 27 | ## FEATURES: 28 | 29 | * Modular interface for REST clients similar to WSGI/Rack for servers 30 | via [rest-builder][]. 31 | * Concurrent requests with synchronous or asynchronous interfaces with 32 | threads via [promise_pool][]. 33 | 34 | ## WHY? 35 | 36 | Build your own API clients for less dependencies, less codes, 37 | less memory, less conflicts, and run faster. 38 | 39 | ## REQUIREMENTS: 40 | 41 | ### Mandatory: 42 | 43 | * Tested with MRI (official CRuby) and JRuby. 44 | * gem [rest-builder][] 45 | * gem [promise_pool][] 46 | * gem [timers][] 47 | * gem [httpclient][] 48 | * gem [mime-types][] 49 | 50 | [rest-builder]: https://github.com/godfat/rest-builder 51 | [promise_pool]: https://github.com/godfat/promise_pool 52 | [timers]: https://github.com/celluloid/timers 53 | [httpclient]: https://github.com/nahi/httpclient 54 | [mime-types]: https://github.com/halostatue/mime-types 55 | 56 | ### Optional: 57 | 58 | * gem json or yajl-ruby, or multi_json (if `JsonResponse` or 59 | `JsonRequest` middleware is used) 60 | 61 | ## INSTALLATION: 62 | 63 | ``` shell 64 | gem install rest-core 65 | ``` 66 | 67 | Or if you want development version, put this in Gemfile: 68 | 69 | ``` ruby 70 | gem 'rest-core', :git => 'git://github.com/godfat/rest-core.git', 71 | :submodules => true 72 | ``` 73 | 74 | If you just want to use Facebook or Twitter clients, please take a look at 75 | [rest-more][] which has a lot of clients built with rest-core. 76 | 77 | ## Build Your Own Clients: 78 | 79 | You can use `RestCore::Builder` to build your own dedicated clients. 80 | Note that `RC` is an alias of `RestCore` 81 | 82 | ``` ruby 83 | require 'rest-core' 84 | YourClient = RC::Builder.client do 85 | use RC::DefaultSite , 'https://api.github.com/users/' 86 | use RC::JsonResponse, true 87 | use RC::CommonLogger, method(:puts) 88 | use RC::Cache , nil, 3600 # :expires_in if cache store supports 89 | end 90 | ``` 91 | 92 | ### Basic Usage: 93 | 94 | And use it with per-instance basis (clients could have different 95 | configuration, e.g. different cache time or timeout time): 96 | 97 | ``` ruby 98 | client = YourClient.new(:cache => {}) 99 | client.get('godfat') # cache miss 100 | client.get('godfat') # cache hit 101 | 102 | client.site = 'https://github.com/api/v2/json/user/show/' 103 | client.get('godfat') # cache miss 104 | client.get('godfat') # cache hit 105 | ``` 106 | 107 | ### Concurrent Requests with Futures: 108 | 109 | You can also make concurrent requests easily: 110 | (see "Advanced Concurrent HTTP Requests -- Embrace the Future" for detail) 111 | 112 | ``` ruby 113 | a = [client.get('godfat'), client.get('cardinalblue')] 114 | puts "It's not blocking... but doing concurrent requests underneath" 115 | p a.map{ |r| r['name'] } # here we want the values, so it blocks here 116 | puts "DONE" 117 | ``` 118 | 119 | ### Exception Handling for Futures: 120 | 121 | Note that since the API call would only block whenever you're looking at 122 | the response, it won't raise any exception at the time the API was called. 123 | So if you want to block and handle the exception at the time API was called, 124 | you would do something like this: 125 | 126 | ``` ruby 127 | begin 128 | response = client.get('bad-user').tap{} # .tap{} is the point 129 | do_the_work(response) 130 | rescue => e 131 | puts "Got an exception: #{e}" 132 | end 133 | ``` 134 | 135 | The trick here is forcing the future immediately give you the exact response, 136 | so that rest-core could see the response and raise the exception. You can 137 | call whatever methods on the future to force this behaviour, but since `tap{}` 138 | is a method from `Kernel` (which is included in `Object`), it's always 139 | available and would return the original value, so it is the easiest method 140 | to be remembered and used. 141 | 142 | If you know the response must be a string, then you can also use `to_s`. 143 | Like this: 144 | 145 | ``` ruby 146 | begin 147 | response = client.get('bad-user').to_s 148 | do_the_work(response) 149 | rescue => e 150 | puts "Got an exception: #{e}" 151 | end 152 | ``` 153 | 154 | Or you can also do this: 155 | 156 | ``` ruby 157 | begin 158 | response = client.get('bad-user') 159 | response.class # simply force it to load 160 | do_the_work(response) 161 | rescue => e 162 | puts "Got an exception: #{e}" 163 | end 164 | ``` 165 | 166 | The point is simply making a method call to force it load, whatever method 167 | should work. 168 | 169 | ### Concurrent Requests with Callbacks: 170 | 171 | On the other hand, callback mode also available: 172 | 173 | ``` ruby 174 | client.get('godfat'){ |v| p v } 175 | puts "It's not blocking... but doing concurrent requests underneath" 176 | client.wait # we block here to wait for the request done 177 | puts "DONE" 178 | ``` 179 | 180 | ### Exception Handling for Callbacks: 181 | 182 | What about exception handling in callback mode? You know that we cannot 183 | raise any exception in the case of using a callback. So rest-core would 184 | pass the exception object into your callback. You can handle the exception 185 | like this: 186 | 187 | ``` ruby 188 | client.get('bad-user') do |response| 189 | if response.kind_of?(Exception) 190 | puts "Got an exception: #{response}" 191 | else 192 | do_the_work(response) 193 | end 194 | end 195 | puts "It's not blocking... but doing concurrent requests underneath" 196 | client.wait # we block here to wait for the request done 197 | puts "DONE" 198 | ``` 199 | 200 | ### Thread Pool / Connection Pool 201 | 202 | Underneath, rest-core would spawn a thread for each request, freeing you 203 | from blocking. However, occasionally we would not want this behaviour, 204 | giving that we might have limited resource and cannot maximize performance. 205 | 206 | For example, maybe we could not afford so many threads running concurrently, 207 | or the target server cannot accept so many concurrent connections. In those 208 | cases, we would want to have limited concurrent threads or connections. 209 | 210 | ``` ruby 211 | YourClient.pool_size = 10 212 | YourClient.pool_idle_time = 60 213 | ``` 214 | 215 | This could set the thread pool size to 10, having a maximum of 10 threads 216 | running together, growing from requests. Each threads idled more than 60 217 | seconds would be shut down automatically. 218 | 219 | Note that `pool_size` should at least be larger than 4, or it might be 220 | very likely to have _deadlock_ if you're using nested callbacks and having 221 | a large number of concurrent calls. 222 | 223 | Also, setting `pool_size` to `-1` would mean we want to make blocking 224 | requests, without spawning any threads. This might be useful for debugging. 225 | 226 | ### Gracefully shutdown 227 | 228 | To shutdown gracefully, consider shutdown the thread pool (if we're using it), 229 | and wait for all requests for a given client. For example, suppose we're using 230 | `RC::Universal`, we'll do this when we're shutting down: 231 | 232 | ``` ruby 233 | RC::Universal.shutdown 234 | ``` 235 | 236 | We could put them in `at_exit` callback like this: 237 | 238 | ``` ruby 239 | at_exit do 240 | RC::Universal.shutdown 241 | end 242 | ``` 243 | 244 | If you're using unicorn, you probably want to put that in the config. 245 | 246 | ### Random Asynchronous Tasks 247 | 248 | Occasionally we might want to do some asynchronous tasks which could take 249 | the advantage of the concurrency facilities inside rest-core, for example, 250 | using `wait` and `shutdown`. You could do this with `defer` for a particular 251 | client. Still take `RC::Universal` as an example: 252 | 253 | ``` ruby 254 | RC::Universal.defer do 255 | sleep(1) 256 | puts "Slow task done" 257 | end 258 | 259 | RC::Universal.wait 260 | ``` 261 | 262 | ### Persistent connections (keep-alive connections) 263 | 264 | Since we're using [httpclient][] by default now, we would reuse connections, 265 | making it much faster for hitting the same host repeatedly. 266 | 267 | ### Streaming Requests 268 | 269 | Suppose we want to POST a file, instead of trying to read all the contents 270 | in memory and send them, we could stream it from the file system directly. 271 | 272 | ``` ruby 273 | client.post('path', File.open('README.md')) 274 | ``` 275 | 276 | Basically, payloads could be any IO object. Check out 277 | [RC::Payload](lib/rest-core/util/payload.rb) for more information. 278 | 279 | ### Streaming Responses 280 | 281 | This one is much harder then streaming requests, since all built-in 282 | middleware actually assume the responses should be blocking and buffered. 283 | Say, some JSON parser could not really parse from streams. 284 | 285 | We solve this issue similarly to the way Rack solves it. That is, we hijack 286 | the socket. This would be how we're doing: 287 | 288 | ``` ruby 289 | sock = client.get('path', {}, RC::HIJACK => true) 290 | p sock.read(10) 291 | p sock.read(10) 292 | p sock.read(10) 293 | ``` 294 | 295 | Of course, if we don't want to block in order to get the socket, we could 296 | always use the callback form: 297 | 298 | ``` ruby 299 | client.get('path', {}, RC::HIJACK => true) do |sock| 300 | p sock.read(10) 301 | p sock.read(10) 302 | p sock.read(10) 303 | end 304 | ``` 305 | 306 | Note that since the socket would be put inside `RC::RESPONSE_SOCKET` 307 | instead of `RC::RESPONSE_BODY`, not all middleware would handle the socket. 308 | In the case of hijacking, `RC::RESPONSE_BODY` would always be mapped to an 309 | empty string, as it does not make sense to store the response in this case. 310 | 311 | ### SSE (Server-Sent Events) 312 | 313 | Not only JavaScript could receive server-sent events, any languages could. 314 | Doing so would establish a keep-alive connection to the server, and receive 315 | data periodically. We'll take Firebase as an example: 316 | 317 | If you are using Firebase, please consider [rest-firebase][] instead. 318 | 319 | [rest-firebase]: https://github.com/CodementorIO/rest-firebase 320 | 321 | ``` ruby 322 | require 'rest-core' 323 | 324 | # Streaming over 'users/tom.json' 325 | cl = RC::Universal.new(:site => 'https://SampleChat.firebaseIO-demo.com/') 326 | es = cl.event_source('users/tom.json', {}, # this is query, none here 327 | :headers => {'Accept' => 'text/event-stream'}) 328 | 329 | @reconnect = true 330 | 331 | es.onopen { |sock| p sock } # Called when connected 332 | es.onmessage{ |event, data, sock| p event, data } # Called for each message 333 | es.onerror { |error, sock| p error } # Called whenever there's an error 334 | # Extra: If we return true in onreconnect callback, it would automatically 335 | # reconnect the node for us if disconnected. 336 | es.onreconnect{ |error, sock| p error; @reconnect } 337 | 338 | # Start making the request 339 | es.start 340 | 341 | # Try to close the connection and see it reconnects automatically 342 | es.close 343 | 344 | # Update users/tom.json 345 | p cl.put('users/tom.json', RC::Json.encode(:some => 'data')) 346 | p cl.post('users/tom.json', RC::Json.encode(:some => 'other')) 347 | p cl.get('users/tom.json') 348 | p cl.delete('users/tom.json') 349 | 350 | # Need to tell onreconnect stops reconnecting, or even if we close 351 | # the connection manually, it would still try to reconnect again. 352 | @reconnect = false 353 | 354 | # Close the connection to gracefully shut it down. 355 | es.close 356 | ``` 357 | 358 | Those callbacks would be called in a separate background thread, 359 | so we don't have to worry about blocking it. If we want to wait for 360 | the connection to be closed, we could call `wait`: 361 | 362 | ``` ruby 363 | es.wait # This would block until the connection is closed 364 | ``` 365 | 366 | ### More Control with `request_full`: 367 | 368 | You can also use `request_full` to retrieve everything including response 369 | status, response headers, and also other rest-core options. But since using 370 | this interface is like using Rack directly, you have to build the env 371 | manually. To help you build the env manually, everything has a default, 372 | including the path. 373 | 374 | ``` ruby 375 | client.request_full({})[RC::RESPONSE_BODY] # {"message"=>"Not Found"} 376 | # This would print something like this: 377 | # RestCore: spent 1.135713 Requested GET https://api.github.com/users/ 378 | 379 | client.request_full(RC::REQUEST_PATH => 'godfat')[RC::RESPONSE_STATUS] 380 | client.request_full(RC::REQUEST_PATH => 'godfat')[RC::RESPONSE_HEADERS] 381 | # Headers are normalized with all upper cases and 382 | # dashes are replaced by underscores. 383 | 384 | # To make POST (or any other request methods) request: 385 | client.request_full(RC::REQUEST_PATH => 'godfat', 386 | RC::REQUEST_METHOD => :post)[RC::RESPONSE_STATUS] # 404 387 | ``` 388 | 389 | ### Examples: 390 | 391 | Runnable example is at: [example/simple.rb][]. Please see [rest-more][] 392 | for more complex examples to build clients, and [slides][] from 393 | [rubyconf.tw/2011][talk] for concepts. 394 | 395 | [example/simple.rb]: example/simple.rb 396 | [slides]: https://www.godfat.org/slide/2011-08-27-rest-core.html 397 | 398 | ## Playing Around: 399 | 400 | You can also play around with `RC::Universal` client, which has installed 401 | _all_ reasonable middleware built-in rest-core. So the above example could 402 | also be achieved by: 403 | 404 | ``` ruby 405 | require 'rest-core' 406 | client = RC::Universal.new(:site => 'https://api.github.com/users/', 407 | :json_response => true, 408 | :log_method => method(:puts)) 409 | client.get('godfat') 410 | ``` 411 | 412 | `RC::Universal` is defined as: 413 | 414 | ``` ruby 415 | module RestCore 416 | Universal = Builder.client do 417 | use Timeout , 0 418 | 419 | use DefaultSite , nil 420 | use DefaultHeaders, {} 421 | use DefaultQuery , {} 422 | use DefaultPayload, {} 423 | use JsonRequest , false 424 | use AuthBasic , nil, nil 425 | use CommonLogger , method(:puts) 426 | use ErrorHandler , nil 427 | use ErrorDetectorHttp 428 | 429 | use SmashResponse , false 430 | use ClashResponse , false 431 | use JsonResponse , false 432 | use QueryResponse , false 433 | 434 | use Cache , {}, 600 # default :expires_in 600 but the default 435 | # cache {} didn't support it 436 | 437 | use FollowRedirect, 10 438 | end 439 | end 440 | ``` 441 | 442 | If you have both [rib][] and [rest-more][] installed, you can also play 443 | around with an interactive shell, like this: 444 | 445 | ``` shell 446 | rib rest-core 447 | ``` 448 | 449 | And you will be entering a rib shell, which `self` is an instance of 450 | `RC::Universal` you can play: 451 | 452 | rest-core>> get 'https://api.github.com/users/godfat' 453 | 454 | will print out the response from Github. You can also do this to make 455 | calling Github easier: 456 | 457 | rest-core>> self.site = 'https://api.github.com/users/' 458 | rest-core>> self.json_response = true 459 | 460 | Then it would do exactly like the original example: 461 | 462 | rest-core>> get 'godfat' # you get a nice parsed hash 463 | 464 | This is mostly for fun and experimenting, so it's only included in 465 | [rest-more][] and [rib][]. Please make sure you have both of them 466 | installed before trying this. 467 | 468 | [rib]: https://github.com/godfat/rib 469 | 470 | ## List of built-in Middleware: 471 | 472 | ### [RC::AuthBasic][] 473 | 474 | ### [RC::Bypass][] 475 | 476 | ### [RC::Cache][] 477 | 478 | use RC::Cache, cache, expires_in 479 | 480 | where `cache` is the cache store which the cache data would be storing to. 481 | `expires_in` would be passed to 482 | `cache.store(key, value :expires_in => expires_in)` if `store` method is 483 | available and its arity should be at least 3. The interface to the cache 484 | could be referenced from [moneta][], namely: 485 | 486 | * (required) `[](key)` 487 | * (required) `[]=(key, value)` 488 | * (optional, required if :expires_in is needed) `store(key, value, options)` 489 | 490 | Note that `{:expires_in => seconds}` would be passed as the options in 491 | `store(key, value, options)`, and a plain old Ruby hash `{}` satisfies 492 | the mandatory requirements: `[](key)` and `[]=(key, value)`, but not the 493 | last one for `:expires_in` because the `store` method for Hash did not take 494 | the third argument. That means we could use `{}` as the cache but it would 495 | simply ignore `:expires_in`. 496 | 497 | ### [RC::CommonLogger][] 498 | 499 | ### [RC::DefaultHeaders][] 500 | 501 | ### [RC::DefaultPayload][] 502 | 503 | ### [RC::DefaultQuery][] 504 | 505 | ### [RC::DefaultSite][] 506 | 507 | ### [RC::Defaults][] 508 | 509 | ### [RC::ErrorDetector][] 510 | 511 | ### [RC::ErrorDetectorHttp][] 512 | 513 | ### [RC::ErrorHandler][] 514 | 515 | ### [RC::FollowRedirect][] 516 | 517 | ### [RC::JsonRequest][] 518 | 519 | ### [RC::JsonResponse][] 520 | 521 | ### [RC::Oauth1Header][] 522 | 523 | ### [RC::Oauth2Header][] 524 | 525 | ### [RC::Oauth2Query][] 526 | 527 | ### [RC::Timeout][] 528 | 529 | [RC::AuthBasic]: lib/rest-core/middleware/auth_basic.rb 530 | [RC::Bypass]: lib/rest-core/middleware/bypass.rb 531 | [RC::Cache]: lib/rest-core/middleware/cache.rb 532 | [RC::ClashResponse]: lib/rest-core/middleware/clash_response.rb 533 | [RC::CommonLogger]: lib/rest-core/middleware/common_logger.rb 534 | [RC::DefaultHeaders]: lib/rest-core/middleware/default_headers.rb 535 | [RC::DefaultPayload]: lib/rest-core/middleware/default_payload.rb 536 | [RC::DefaultQuery]: lib/rest-core/middleware/default_query.rb 537 | [RC::DefaultSite]: lib/rest-core/middleware/default_site.rb 538 | [RC::Defaults]: lib/rest-core/middleware/defaults.rb 539 | [RC::ErrorDetector]: lib/rest-core/middleware/error_detector.rb 540 | [RC::ErrorDetectorHttp]: lib/rest-core/middleware/error_detector_http.rb 541 | [RC::ErrorHandler]: lib/rest-core/middleware/error_handler.rb 542 | [RC::FollowRedirect]: lib/rest-core/middleware/follow_redirect.rb 543 | [RC::JsonRequest]: lib/rest-core/middleware/json_request.rb 544 | [RC::JsonResponse]: lib/rest-core/middleware/json_response.rb 545 | [RC::Oauth1Header]: lib/rest-core/middleware/oauth1_header.rb 546 | [RC::Oauth2Header]: lib/rest-core/middleware/oauth2_header.rb 547 | [RC::Oauth2Query]: lib/rest-core/middleware/oauth2_query.rb 548 | [RC::SmashResponse]: lib/rest-core/middleware/smash_response.rb 549 | [RC::Retry]: lib/rest-core/middleware/retry.rb 550 | [RC::Timeout]: lib/rest-core/middleware/timeout.rb 551 | [moneta]: https://github.com/minad/moneta#expiration 552 | 553 | ## Advanced Concurrent HTTP Requests -- Embrace the Future 554 | 555 | ### The Interface 556 | 557 | There are a number of different ways to make concurrent requests in 558 | rest-core. They could be roughly categorized to two different forms. 559 | One is using the well known callbacks, while the other one is using 560 | through a technique called [future][]. Basically, it means it would 561 | return you a promise, which would eventually become the real value 562 | (response here) you were asking for whenever you really want it. 563 | Otherwise, the program keeps running until the value is evaluated, 564 | and blocks there if the computation (response) hasn't been done yet. 565 | If the computation is already done, then it would simply return you 566 | the result. 567 | 568 | Here's a very simple example for using futures: 569 | 570 | ``` ruby 571 | require 'rest-core' 572 | YourClient = RC::Builder.client do 573 | use RC::DefaultSite , 'https://api.github.com/users/' 574 | use RC::JsonResponse, true 575 | use RC::CommonLogger, method(:puts) 576 | end 577 | 578 | client = YourClient.new 579 | puts "httpclient with threads doing concurrent requests" 580 | a = [client.get('godfat'), client.get('cardinalblue')] 581 | puts "It's not blocking... but doing concurrent requests underneath" 582 | p a.map{ |r| r['name'] } # here we want the values, so it blocks here 583 | puts "DONE" 584 | ``` 585 | 586 | And here's a corresponded version for using callbacks: 587 | 588 | ``` ruby 589 | require 'rest-core' 590 | YourClient = RC::Builder.client do 591 | use RC::DefaultSite , 'https://api.github.com/users/' 592 | use RC::JsonResponse, true 593 | use RC::CommonLogger, method(:puts) 594 | end 595 | 596 | client = YourClient.new 597 | puts "httpclient with threads doing concurrent requests" 598 | client.get('godfat'){ |v| 599 | p v['name'] 600 | }. 601 | get('cardinalblue'){ |v| 602 | p v['name'] 603 | } 604 | puts "It's not blocking... but doing concurrent requests underneath" 605 | client.wait # until all requests are done 606 | puts "DONE" 607 | ``` 608 | 609 | You can pick whatever works for you. 610 | 611 | [future]: https://en.wikipedia.org/wiki/Futures_and_promises 612 | 613 | A full runnable example is at: [example/simple.rb][]. If you want to know 614 | all the possible use cases, you can also see: [example/use-cases.rb][]. It's 615 | also served as a test for each possible combinations, so it's quite complex 616 | and complete. 617 | 618 | [example/use-cases.rb]: example/use-cases.rb 619 | 620 | ## Configure the underlying HTTP engine 621 | 622 | Occasionally we might want to configure the underlying HTTP engine, which 623 | for now is [httpclient][]. For example, we might not want to decompress 624 | gzip automatically, (rest-core configures httpclient to request and 625 | decompress gzip automatically). or we might want to skip verifying SSL 626 | in some situation. (e.g. making requests against a self-signed testing server) 627 | 628 | In such cases, we could use `config_engine` option to configure the underlying 629 | engine. This could be set with request based, client instance based, or 630 | client class based. Please refer to: 631 | [How We Pick the Default Value](#how-we-pick-the-default-value), 632 | except that there's no middleware for `config_engine`. 633 | 634 | Here are some examples: 635 | 636 | ``` ruby 637 | require 'rest-core' 638 | YourClient = RC::Builder.client 639 | 640 | # class based: 641 | def YourClient.default_config_engine 642 | lambda do |engine| 643 | # disable auto-gzip: 644 | engine.transparent_gzip_decompression = false 645 | 646 | # disable verifying SSL 647 | engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE 648 | end 649 | end 650 | 651 | # instance based: 652 | client = YourClient.new(:config_engine => lambda do |engine| 653 | # disable auto-gzip: 654 | engine.transparent_gzip_decompression = false 655 | 656 | # disable verifying SSL 657 | engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE 658 | end) 659 | 660 | # request based: 661 | client.get('http://example.com/', {}, :config_engine => lambda do |engine| 662 | # disable auto-gzip: 663 | engine.transparent_gzip_decompression = false 664 | 665 | # disable verifying SSL 666 | engine.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE 667 | end) 668 | ``` 669 | 670 | As we stated in 671 | [How We Pick the Default Value](#how-we-pick-the-default-value), 672 | the priority here is: 673 | 674 | 0. request based 675 | 0. instance based 676 | 0. class based 677 | 678 | ## rest-core users: 679 | 680 | * [rest-firebase][] 681 | * [rest-more][] 682 | * [rest-more-yahoo_buy](https://github.com/GoodLife/rest-more-yahoo_buy) 683 | * [s2sync](https://github.com/brucehsu/s2sync) 684 | * [s2sync_web](https://github.com/brucehsu/s2sync_web) 685 | * [topcoder](https://github.com/miaout17/topcoder) 686 | 687 | ## Powered sites: 688 | 689 | * [GW2 Account Viewer](https://gw2.godfat.org) 690 | 691 | ## CHANGES: 692 | 693 | * [CHANGES](CHANGES.md) 694 | 695 | ## CONTRIBUTORS: 696 | 697 | * Andrew Liu (@eggegg) 698 | * andy (@coopsite) 699 | * Barnabas Debreczeni (@keo) 700 | * Bruce Chu (@bruchu) 701 | * Ethan Czahor (@ethanz5) 702 | * Florent Vaucelle (@florent) 703 | * Jaime Cham (@jcham) 704 | * Joe Chen (@joe1chen) 705 | * John Fan (@johnfan) 706 | * khoa nguyen (@khoan) 707 | * Lin Jen-Shin (@godfat) 708 | * lulalala (@lulalala) 709 | * Man Vuong (@kidlab) 710 | * Mariusz Pruszynski (@snicky) 711 | * Mr. Big Cat (@miaout17) 712 | * Nicolas Fouché (@nfo) 713 | * Robert Balousek (@volksport) 714 | * Szu-Kai Hsu (@brucehsu) 715 | 716 | ## LICENSE: 717 | 718 | Apache License 2.0 (Apache-2.0) 719 | 720 | Copyright (c) 2011-2023, Lin Jen-Shin (godfat) 721 | 722 | Licensed under the Apache License, Version 2.0 (the "License"); 723 | you may not use this file except in compliance with the License. 724 | You may obtain a copy of the License at 725 | 726 | 727 | 728 | Unless required by applicable law or agreed to in writing, software 729 | distributed under the License is distributed on an "AS IS" BASIS, 730 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 731 | See the License for the specific language governing permissions and 732 | limitations under the License. 733 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | begin 3 | require "#{__dir__}/task/gemgem" 4 | rescue LoadError 5 | sh 'git submodule update --init --recursive' 6 | exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV 7 | end 8 | 9 | Gemgem.init(__dir__, :submodules => 10 | %w[rest-builder 11 | rest-builder/promise_pool]) do |s| 12 | require 'rest-core/version' 13 | s.name = 'rest-core' 14 | s.version = RestCore::VERSION 15 | 16 | %w[rest-builder].each(&s.method(:add_runtime_dependency)) 17 | end 18 | 19 | task :test do 20 | require ENV['json_lib'] if ENV['json_lib'] 21 | end 22 | 23 | desc 'Run console' 24 | task 'console' do 25 | ARGV.shift 26 | load `which rib`.chomp 27 | end 28 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # NEAR FUTURE 4 | 5 | * revisit error_callback (use middleware) 6 | * ActiveRecord::ConnectionAdapters::ConnectionManagement 7 | 8 | # FAR FUTURE 9 | 10 | * middleware composer 11 | * headers and payload logs for CommonLogger 12 | * fix DRY by defining `prepare :: env -> env` 13 | * FAIL and LOG need to be reimplemented as well. 14 | -------------------------------------------------------------------------------- /example/simple.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core' 3 | 4 | YourClient = RC::Builder.client do 5 | use RC::DefaultSite , 'https://api.github.com/users/' 6 | use RC::JsonResponse, true 7 | use RC::CommonLogger, method(:puts) 8 | use RC::Timeout , 10 9 | use RC::Cache , nil, 3600 10 | end 11 | 12 | YourClient.pool_size = 5 13 | 14 | client = YourClient.new(:cache => {}) 15 | p client.get('godfat') # cache miss 16 | puts 17 | p client.get('godfat') # cache hit 18 | 19 | client.cache = false 20 | 21 | puts "concurrent requests" 22 | a = [client.get('godfat'), client.get('cardinalblue')] 23 | puts "It's not blocking... but doing concurrent requests underneath" 24 | p a.map{ |r| r['name'] } # here we want the values, so it blocks here 25 | puts "DONE" 26 | 27 | puts "callback" 28 | client.get('godfat'){ |v| p v } 29 | puts "It's not blocking... but doing concurrent requests underneath" 30 | client.wait # we block here to wait for the request done 31 | puts "DONE" 32 | -------------------------------------------------------------------------------- /example/use-cases.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core' 3 | RC.eagerload 4 | 5 | RC::Universal.pool_size = 0 # default to no thread pool 6 | 7 | RC::Universal.module_eval do 8 | def default_query 9 | {:access_token => ENV['FACEBOOK_ACCESS_TOKEN']} 10 | end 11 | 12 | def default_timeout 13 | 10 14 | end 15 | end 16 | 17 | def def_use_case name, &block 18 | singleton_class.send(:define_method, "#{name}_") do 19 | begin 20 | yield 21 | rescue => e 22 | q "Encountering: #{e}" 23 | end 24 | end 25 | singleton_class.send(:define_method, name) do 26 | @count ||= 0 27 | printf "Use case #%02d: %s\n", @count+=1, name 28 | puts '-' * 70 29 | start = Time.now 30 | send("#{name}_") 31 | puts "Spent #{Time.now - start} seconds for this use case." 32 | puts 33 | end 34 | end 35 | 36 | @mutex = Mutex.new 37 | 38 | def q str 39 | @mutex.synchronize{ puts "\e[33m=> #{str.inspect}\e[0m" } 40 | end 41 | 42 | # ---------------------------------------------------------------------- 43 | 44 | def_use_case 'pure_ruby_single_request' do 45 | q RC::Universal.new(:json_response => true). 46 | get('https://graph.facebook.com/4')['name'] 47 | end 48 | 49 | def_use_case 'pure_ruby_concurrent_requests' do 50 | client = RC::Universal.new(:json_response => true, 51 | :site => 'https://graph.facebook.com/') 52 | q [client.get('4'), client.get('5')].map{ |u| u['name'] } 53 | end 54 | 55 | def_use_case 'pure_ruby_cache_requests' do 56 | client = RC::Universal.new(:json_response => true, :cache => {}) 57 | 3.times{ q client.get('https://graph.facebook.com/4')['name'] } 58 | end 59 | 60 | def_use_case 'pure_ruby_callback_requests' do 61 | RC::Universal.new(:json_response => true , 62 | :site => 'https://graph.facebook.com/' , 63 | :log_method => lambda{|str| 64 | @mutex.synchronize{puts(str)}}). 65 | get('4'){ |res| 66 | if res.kind_of?(Exception) 67 | q "Encountering: #{res}" 68 | next 69 | end 70 | q res['name'] 71 | }. 72 | get('5'){ |res| 73 | if res.kind_of?(Exception) 74 | q "Encountering: #{res}" 75 | next 76 | end 77 | q res['name'] 78 | }.wait 79 | end 80 | 81 | def_use_case 'pure_ruby_nested_concurrent_requests' do 82 | c = RC::Universal.new(:json_response => true , 83 | :site => 'https://graph.facebook.com/' , 84 | :log_method => lambda{ |str| 85 | @mutex.synchronize{puts(str)}}) 86 | 87 | %w[4 5].each{ |user| 88 | c.get(user, :fields => 'cover'){ |data| 89 | if data.kind_of?(Exception) 90 | q "Encountering: #{data}" 91 | next 92 | end 93 | 94 | cover = data['cover'] 95 | comments = c.get("#{cover['id']}/comments") 96 | likes = c.get("#{cover['id']}/likes") 97 | most_liked_comment = comments['data'].max_by{|d|d['like_count']} 98 | 99 | q "Author of most liked comment from #{user}'s cover photo:" 100 | q most_liked_comment['from']['name'] 101 | 102 | y = !!likes['data'].find{|d|d['id'] == most_liked_comment['from']['id']} 103 | q "Did the user also like the cover?: #{y}" 104 | } 105 | } 106 | 107 | c.wait 108 | end 109 | 110 | # ---------------------------------------------------------------------- 111 | 112 | def_use_case 'pure_ruby' do 113 | pure_ruby_single_request 114 | pure_ruby_concurrent_requests 115 | pure_ruby_cache_requests 116 | pure_ruby_callback_requests 117 | pure_ruby_nested_concurrent_requests 118 | end 119 | 120 | # ---------------------------------------------------------------------- 121 | 122 | pure_ruby 123 | -------------------------------------------------------------------------------- /lib/rest-core.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-builder' 3 | 4 | module RestCore 5 | # for backward compatibility 6 | RestBuilder.constants.each do |const| 7 | const_set(const, RestBuilder.const_get(const)) 8 | end 9 | 10 | # oauth1 utilities 11 | autoload :ClientOauth1 , 'rest-core/client_oauth1' 12 | 13 | # misc utilities 14 | autoload :Hmac , 'rest-core/util/hmac' 15 | autoload :Json , 'rest-core/util/json' 16 | autoload :ParseLink , 'rest-core/util/parse_link' 17 | autoload :ParseQuery , 'rest-core/util/parse_query' 18 | autoload :Config , 'rest-core/util/config' 19 | autoload :Clash , 'rest-core/util/clash' 20 | autoload :Smash , 'rest-core/util/smash' 21 | autoload :DalliExtension , 'rest-core/util/dalli_extension' 22 | 23 | # middlewares 24 | autoload :AuthBasic , 'rest-core/middleware/auth_basic' 25 | autoload :Bypass , 'rest-core/middleware/bypass' 26 | autoload :Cache , 'rest-core/middleware/cache' 27 | autoload :ClashResponse , 'rest-core/middleware/clash_response' 28 | autoload :SmashResponse , 'rest-core/middleware/smash_response' 29 | autoload :CommonLogger , 'rest-core/middleware/common_logger' 30 | autoload :DefaultHeaders , 'rest-core/middleware/default_headers' 31 | autoload :DefaultQuery , 'rest-core/middleware/default_query' 32 | autoload :DefaultPayload , 'rest-core/middleware/default_payload' 33 | autoload :DefaultSite , 'rest-core/middleware/default_site' 34 | autoload :Defaults , 'rest-core/middleware/defaults' 35 | autoload :ErrorDetector , 'rest-core/middleware/error_detector' 36 | autoload :ErrorDetectorHttp, 'rest-core/middleware/error_detector_http' 37 | autoload :ErrorHandler , 'rest-core/middleware/error_handler' 38 | autoload :FollowRedirect , 'rest-core/middleware/follow_redirect' 39 | autoload :JsonRequest , 'rest-core/middleware/json_request' 40 | autoload :JsonResponse , 'rest-core/middleware/json_response' 41 | autoload :QueryResponse , 'rest-core/middleware/query_response' 42 | autoload :Oauth1Header , 'rest-core/middleware/oauth1_header' 43 | autoload :Oauth2Header , 'rest-core/middleware/oauth2_header' 44 | autoload :Oauth2Query , 'rest-core/middleware/oauth2_query' 45 | autoload :Retry , 'rest-core/middleware/retry' 46 | autoload :Timeout , 'rest-core/middleware/timeout' 47 | 48 | # clients 49 | autoload :Universal , 'rest-core/client/universal' 50 | 51 | # You might want to call this before launching your application in a 52 | # threaded environment to avoid thread-safety issue in autoload. 53 | def self.eagerload const=self, loaded={} 54 | return if loaded[const.name] 55 | loaded[const.name] = true 56 | const.constants(false).each{ |n| 57 | begin 58 | c = const.const_get(n) 59 | rescue LoadError, NameError => e 60 | warn "RestCore: WARN: #{e} for #{const}\n" \ 61 | " from #{e.backtrace.grep(/top.+required/).first}" 62 | end 63 | eagerload(c, loaded) if c.respond_to?(:constants) && !loaded[n] 64 | } 65 | end 66 | end 67 | 68 | RC = RestCore unless Object.const_defined?(:RC) 69 | -------------------------------------------------------------------------------- /lib/rest-core/client/universal.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core' 3 | 4 | module RestCore 5 | Universal = RestBuilder::Builder.client do 6 | use DefaultSite , nil 7 | use DefaultHeaders, {} 8 | use DefaultQuery , {} 9 | use DefaultPayload, {} 10 | use JsonRequest , false 11 | use AuthBasic , nil, nil 12 | use Retry , 0, Retry::DefaultRetryExceptions 13 | use Timeout , 0 14 | use ErrorHandler , nil 15 | use ErrorDetectorHttp 16 | 17 | use SmashResponse , false 18 | use ClashResponse , false 19 | use JsonResponse , false 20 | use QueryResponse , false 21 | use FollowRedirect, 10 22 | use CommonLogger , method(:puts) 23 | use Cache , {}, 600 # default :expires_in 600 but the default 24 | # cache {} didn't support it 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/rest-core/client_oauth1.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'digest/md5' 3 | 4 | require 'rest-core/util/parse_query' 5 | require 'rest-core/util/json' 6 | 7 | module RestCore 8 | module ClientOauth1 9 | def authorize_url! opts={} 10 | self.data = ParseQuery.parse_query( 11 | post(request_token_path, {}, {}, 12 | {:json_response => false}.merge(opts))) 13 | 14 | authorize_url 15 | end 16 | 17 | def authorize_url 18 | url(authorize_path, :oauth_token => oauth_token) 19 | end 20 | 21 | def authorize! opts={} 22 | self.data = ParseQuery.parse_query( 23 | post(access_token_path, {}, {}, 24 | {:json_response => false}.merge(opts))) 25 | 26 | data['authorized'] = 'true' 27 | data 28 | end 29 | 30 | def authorized? 31 | !!(oauth_token && oauth_token_secret && data['authorized']) 32 | end 33 | 34 | def data_json 35 | Json.encode(data.merge('sig' => calculate_sig)) 36 | end 37 | 38 | def data_json= json 39 | self.data = check_sig_and_return_data(Json.decode(json)) 40 | rescue Json.const_get(:ParseError) 41 | self.data = nil 42 | end 43 | 44 | def oauth_token 45 | data['oauth_token'] if data.kind_of?(Hash) 46 | end 47 | def oauth_token= token 48 | data['oauth_token'] = token if data.kind_of?(Hash) 49 | end 50 | def oauth_token_secret 51 | data['oauth_token_secret'] if data.kind_of?(Hash) 52 | end 53 | def oauth_token_secret= secret 54 | data['oauth_token_secret'] = secret if data.kind_of?(Hash) 55 | end 56 | def oauth_callback 57 | data['oauth_callback'] if data.kind_of?(Hash) 58 | end 59 | def oauth_callback= uri 60 | data['oauth_callback'] = uri if data.kind_of?(Hash) 61 | end 62 | 63 | private 64 | def default_data 65 | {} 66 | end 67 | 68 | def check_sig_and_return_data hash 69 | hash if consumer_secret && hash.kind_of?(Hash) && 70 | calculate_sig(hash) == hash['sig'] 71 | end 72 | 73 | def calculate_sig hash=data 74 | base = hash.reject{ |(k, _)| k == 'sig' }.sort.map{ |(k, v)| 75 | "#{Middleware.escape(k.to_s)}=#{Middleware.escape(v.to_s)}" 76 | }.join('&') 77 | Digest::MD5.hexdigest("#{Middleware.escape(consumer_secret)}&#{base}") 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/rest-core/event.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class Event < Struct.new(:duration, :message) 4 | def name; self.class.name[/(?<=::)\w+$/]; end 5 | def to_s 6 | if duration 7 | "spent #{duration} #{name} #{message}" 8 | else 9 | "#{name} #{message}" 10 | end 11 | end 12 | end 13 | 14 | Event::MultiDone = Class.new(Event) 15 | Event::Requested = Class.new(Event) 16 | Event::CacheHit = Class.new(Event) 17 | Event::CacheCleared = Class.new(Event) 18 | Event::Failed = Class.new(Event) 19 | Event::WithHeader = Class.new(Event) 20 | Event::Retrying = Class.new(Event) 21 | end 22 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/auth_basic.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class AuthBasic 4 | def self.members; [:username, :password]; end 5 | include Middleware 6 | 7 | def call env, &k 8 | if username(env) 9 | if password(env) 10 | app.call(env.merge(REQUEST_HEADERS => 11 | auth_basic_header(env).merge(env[REQUEST_HEADERS])), &k) 12 | else 13 | app.call(log(env, "AuthBasic: username provided: #{username(env)},"\ 14 | " but password is missing."), &k) 15 | end 16 | elsif password(env) 17 | app.call(log(env, "AuthBasic: password provided: #{password(env)}," \ 18 | " but username is missing."), &k) 19 | else 20 | app.call(env, &k) 21 | end 22 | end 23 | 24 | def auth_basic_header env 25 | {'Authorization' => 26 | "Basic #{["#{username(env)}:#{password(env)}"].pack('m0')}"} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/bypass.rb: -------------------------------------------------------------------------------- 1 | 2 | # the simplest middleware 3 | module RestCore 4 | class Bypass 5 | def initialize app 6 | @app = app 7 | end 8 | 9 | def call env, &k 10 | @app.call(env, &k) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/cache.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'digest/md5' 3 | require 'rest-core/event' 4 | 5 | module RestCore 6 | class Cache 7 | def self.members; [:cache, :expires_in]; end 8 | include Middleware 9 | 10 | def initialize app, cache, expires_in, &block 11 | super(&block) 12 | @app, @cache, @expires_in = app, cache, expires_in 13 | end 14 | 15 | def call env, &k 16 | e = if env['cache.update'] 17 | cache_clear(env) 18 | else 19 | env 20 | end 21 | 22 | cache_get(e){ |cached| 23 | e[TIMER].cancel if e[TIMER] 24 | k.call(cached) 25 | } || app_call(e, &k) 26 | end 27 | 28 | def app_call env 29 | app.call(env) do |res| 30 | yield(if (res[FAIL] || []).empty? 31 | cache_for(res) 32 | else 33 | res 34 | end) 35 | end 36 | end 37 | 38 | def cache_key env 39 | "rest-core:cache:#{Digest::MD5.hexdigest(env['cache.key'] || 40 | cache_key_raw(env))}" 41 | end 42 | 43 | def cache_get env, &k 44 | return unless cache(env) 45 | return unless cache_for?(env) 46 | 47 | uri = request_uri(env) 48 | start_time = Time.now 49 | return unless data = cache(env)[cache_key(env)] 50 | res = log(env.merge(REQUEST_URI => uri), 51 | Event::CacheHit.new(Time.now - start_time, uri)) 52 | data_extract(data, res, k) 53 | end 54 | 55 | private 56 | def cache_key_raw env 57 | "#{env[REQUEST_METHOD]}:#{request_uri(env)}:#{header_cache_key(env)}" 58 | end 59 | 60 | def cache_clear env 61 | return env unless cache(env) 62 | return env unless cache_for?(env) 63 | 64 | cache_store(env, :[]=, nil) 65 | end 66 | 67 | def cache_for res 68 | return res unless cache(res) 69 | return res unless cache_for?(res) && cacheable_response?(res) 70 | 71 | if expires_in(res).kind_of?(Numeric) && 72 | cache(res).respond_to?(:store) && 73 | cache(res).method(:store).arity == -3 74 | 75 | cache_store(res, :store, data_construct(res), 76 | :expires_in => expires_in(res)) 77 | else 78 | cache_store(res, :[]= , data_construct(res)) 79 | end 80 | end 81 | 82 | def cache_store res, msg, value, *args, **kargs 83 | start_time = Time.now 84 | cache(res).send(msg, cache_key(res), value, *args, **kargs) 85 | 86 | if value 87 | res 88 | else 89 | log(res, 90 | Event::CacheCleared.new(Time.now - start_time, request_uri(res))) 91 | end 92 | end 93 | 94 | def data_construct res 95 | "#{ res[RESPONSE_STATUS]}\n" \ 96 | "#{(res[RESPONSE_HEADERS]||{}).map{|k,v|"#{k}: #{v}\n"}.join}\n\n" \ 97 | "#{ res[RESPONSE_BODY]}" 98 | end 99 | 100 | def data_extract data, res, k 101 | _, status, headers, body = 102 | data.match(/\A(\d*)\n((?:[^\n]+\n)*)\n\n(.*)\Z/m).to_a 103 | 104 | result = res.merge(RESPONSE_STATUS => status.to_i, 105 | RESPONSE_HEADERS => 106 | Hash[(headers||'').scan(/([^:]+): ([^\n]+)\n/)], 107 | RESPONSE_BODY => body) 108 | 109 | result.merge(Promise.claim(result, &k).future_response) 110 | end 111 | 112 | def cache_for? env 113 | [:get, :head, :otpions].include?(env[REQUEST_METHOD]) && 114 | !env[DRY] && !env[HIJACK] 115 | end 116 | 117 | def cacheable_response? res 118 | # https://datatracker.ietf.org/doc/html/rfc7231#section-6.1 119 | [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501]. 120 | include?(res[RESPONSE_STATUS]) 121 | end 122 | 123 | def header_cache_key env 124 | env[REQUEST_HEADERS].sort.map{|(k,v)|"#{k}=#{v}"}.join('&') 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/clash_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/util/clash' 3 | 4 | module RestCore 5 | class ClashResponse 6 | def self.members; [:clash_response]; end 7 | include Middleware 8 | 9 | def call env, &k 10 | return app.call(env, &k) if env[DRY] 11 | return app.call(env, &k) unless clash_response(env) 12 | 13 | app.call(env){ |res| 14 | if res[RESPONSE_BODY].kind_of?(Hash) 15 | yield(res.merge(RESPONSE_BODY => Clash.new(res[RESPONSE_BODY]))) 16 | else 17 | yield(res) 18 | end 19 | } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/common_logger.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/event' 3 | 4 | module RestCore 5 | class CommonLogger 6 | def self.members; [:log_method]; end 7 | include Middleware 8 | 9 | def call env 10 | start_time = Time.now 11 | flushed = flush(env) 12 | app.call(flushed){ |response| 13 | yield(process(response, start_time)) 14 | } 15 | rescue 16 | process(flushed, start_time) 17 | raise 18 | end 19 | 20 | def process response, start_time 21 | flush(log(response, log_request(start_time, response))) 22 | end 23 | 24 | def flush env 25 | return env if !log_method(env) || env[DRY] 26 | (env[LOG] || []).compact. 27 | each{ |obj| log_method(env).call("RestCore: #{obj}") } 28 | env.merge(LOG => []) 29 | end 30 | 31 | def log_request start_time, response 32 | Event::Requested.new(Time.now - start_time, 33 | "#{response[REQUEST_METHOD].to_s.upcase} #{request_uri(response)}") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/default_headers.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class DefaultHeaders 4 | def self.members; [:headers]; end 5 | include Middleware 6 | def call env, &k 7 | h = merge_hash(@headers, headers(env), env[REQUEST_HEADERS]) 8 | app.call(env.merge(REQUEST_HEADERS => h), &k) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/default_payload.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class DefaultPayload 4 | def self.members; [:payload]; end 5 | include Middleware 6 | def call env, &k 7 | p = merge_hash(@payload, payload(env), env[REQUEST_PAYLOAD]) 8 | app.call(env.merge(REQUEST_PAYLOAD => p), &k) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/default_query.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class DefaultQuery 4 | def self.members; [:query]; end 5 | include Middleware 6 | def call env, &k 7 | q = merge_hash(@query, query(env), env[REQUEST_QUERY]) 8 | app.call(env.merge(REQUEST_QUERY => q), &k) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/default_site.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class DefaultSite 4 | def self.members; [:site]; end 5 | include Middleware 6 | 7 | def call env, &k 8 | path = if env[REQUEST_PATH].to_s.include?('://') 9 | env[REQUEST_PATH] 10 | else 11 | File.join(site(env).to_s, env[REQUEST_PATH].to_s) 12 | end 13 | 14 | app.call(env.merge(REQUEST_PATH => path), &k) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/defaults.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class Defaults 4 | def self.members; [:defaults]; end 5 | include Middleware 6 | 7 | # the use of singleton_class is making serialization hard! 8 | # def initialize app, defaults 9 | # super 10 | # singleton_class.module_eval do 11 | # defaults.each{ |(key, value)| 12 | # define_method(key) do |env| 13 | # if value.respond_to?(:call) 14 | # value.call 15 | # else 16 | # value 17 | # end 18 | # end 19 | # } 20 | # end 21 | # end 22 | 23 | def method_missing msg, *args, &block 24 | env = args.first 25 | if env.kind_of?(Hash) && (d = defaults(env)) && d.key?(msg) 26 | defaults(env)[msg] 27 | else 28 | super 29 | end 30 | end 31 | 32 | def respond_to_missing? msg, include_private=false 33 | # since psych would call respond_to? before setting up 34 | # instance variables when restoring ruby objects, we might 35 | # be accessing undefined ivars in that case even all ivars are 36 | # defined in initialize. we can't avoid this because we can't 37 | # use singleton_class (otherwise we can't serialize this) 38 | return super unless instance_variable_defined?(:@defaults) 39 | if (d = defaults({})) && d.key?(msg) 40 | true 41 | else 42 | super 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/error_detector.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class ErrorDetector 4 | def self.members; [:error_detector]; end 5 | include Middleware 6 | 7 | def call env 8 | app.call(env){ |response| 9 | detector = error_detector(env) 10 | yield(fail(response, detector && detector.call(response))) 11 | } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/error_detector_http.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/middleware/error_detector' 3 | 4 | module RestCore 5 | class ErrorDetectorHttp < RestCore::ErrorDetector 6 | def initialize app, detector=nil 7 | super(app, detector || 8 | lambda{ |env| (env[RESPONSE_STATUS] || 200) / 100 >= 4 }) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/error_handler.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class ErrorHandler 4 | def self.members; [:error_handler]; end 5 | include Middleware 6 | 7 | def call env 8 | app.call(env){ |res| 9 | h = error_handler(res) 10 | f = res[FAIL] || [] 11 | yield(if f.empty? || f.find{ |ff| ff.kind_of?(Exception) } || !h 12 | res 13 | else 14 | fail(res, h.call(res)) 15 | end)} 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/follow_redirect.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class FollowRedirect 4 | def self.members; [:max_redirects]; end 5 | include Middleware 6 | 7 | def call env, &k 8 | if env[DRY] 9 | app.call(env, &k) 10 | else 11 | app.call(env){ |res| process(res, k) } 12 | end 13 | end 14 | 15 | def process res, k 16 | return k.call(res) if max_redirects(res) <= 0 17 | status = res[RESPONSE_STATUS] 18 | return k.call(res) if ![301,302,303,307].include?(status) 19 | return k.call(res) if [301,302 ,307].include?(status) && 20 | ![:get, :head ].include?(res[REQUEST_METHOD]) 21 | 22 | location = [res[RESPONSE_HEADERS]['LOCATION']].flatten.first 23 | meth = if status == 303 24 | :get 25 | else 26 | res[REQUEST_METHOD] 27 | end 28 | 29 | give_promise(call(res.merge( 30 | REQUEST_METHOD => meth , 31 | REQUEST_PATH => location, 32 | REQUEST_QUERY => {} , 33 | 'max_redirects' => max_redirects(res) - 1), &k)) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/json_request.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/util/json' 3 | 4 | module RestCore 5 | class JsonRequest 6 | def self.members; [:json_request]; end 7 | include Middleware 8 | 9 | JSON_REQUEST_HEADER = {'Content-Type' => 'application/json'}.freeze 10 | 11 | def call env, &k 12 | return app.call(env, &k) unless json_request(env) 13 | return app.call(env, &k) unless has_payload?(env) 14 | 15 | headers = env[REQUEST_HEADERS] || {} 16 | app.call(env.merge( 17 | REQUEST_HEADERS => JSON_REQUEST_HEADER.merge(headers), 18 | REQUEST_PAYLOAD => Json.encode(env[REQUEST_PAYLOAD])), &k) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/json_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/util/json' 3 | 4 | module RestCore 5 | class JsonResponse 6 | def self.members; [:json_response]; end 7 | include Middleware 8 | 9 | class ParseError < Json.const_get(:ParseError) 10 | attr_reader :cause, :body 11 | def initialize cause, body 12 | msg = cause.message.force_encoding('utf-8') 13 | super("#{msg}\nOriginal text: #{body}") 14 | @cause, @body = cause, body 15 | end 16 | end 17 | 18 | JSON_RESPONSE_HEADER = {'Accept' => 'application/json'}.freeze 19 | 20 | def call env, &k 21 | return app.call(env, &k) if env[DRY] 22 | return app.call(env, &k) unless json_response(env) 23 | 24 | app.call(env.merge(REQUEST_HEADERS => 25 | JSON_RESPONSE_HEADER.merge(env[REQUEST_HEADERS]||{}))){ |response| 26 | yield(process(response)) 27 | } 28 | end 29 | 30 | def process response 31 | body = response[RESPONSE_BODY] 32 | json = if body.kind_of?(String) 33 | Json.normalize(body) 34 | else 35 | # Yajl supports streaming, so let's pass it directly to make 36 | # it possible to do streaming here. Although indeed we should 37 | # use RESPONSE_SOCKET in this case, but doing that could 38 | # introduce some incompatibility which I don't want to take 39 | # care of for now. 40 | body 41 | end 42 | 43 | response.merge(RESPONSE_BODY => json && Json.decode(json)) 44 | rescue Json.const_get(:ParseError) => error 45 | fail(response, ParseError.new(error, body)) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/oauth1_header.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'openssl' 3 | require 'rest-core/event' 4 | require 'rest-core/util/hmac' 5 | 6 | module RestCore 7 | # http://tools.ietf.org/html/rfc5849 8 | class Oauth1Header 9 | def self.members 10 | [:request_token_path, :access_token_path, :authorize_path, 11 | :consumer_key, :consumer_secret, 12 | :oauth_callback, :oauth_verifier, 13 | :oauth_token, :oauth_token_secret, :data] 14 | end 15 | include Middleware 16 | 17 | def call env, &k 18 | start_time = Time.now 19 | headers = {'Authorization' => oauth_header(env)}. 20 | merge(env[REQUEST_HEADERS]) 21 | 22 | event = Event::WithHeader.new(Time.now - start_time, 23 | "Authorization: #{headers['Authorization']}") 24 | 25 | app.call(log(env.merge(REQUEST_HEADERS => headers), event), &k) 26 | end 27 | 28 | def oauth_header env 29 | oauth = attach_signature(env, 30 | 'oauth_consumer_key' => consumer_key(env), 31 | 'oauth_signature_method' => 'HMAC-SHA1', 32 | 'oauth_timestamp' => Time.now.to_i.to_s, 33 | 'oauth_nonce' => nonce, 34 | 'oauth_version' => '1.0', 35 | 'oauth_callback' => oauth_callback(env), 36 | 'oauth_verifier' => oauth_verifier(env), 37 | 'oauth_token' => oauth_token(env)). 38 | map{ |(k, v)| "#{k}=\"#{escape(v)}\"" }.join(', ') 39 | 40 | "OAuth #{oauth}" 41 | end 42 | 43 | def attach_signature env, oauth_params 44 | params = reject_blank(oauth_params) 45 | params.merge('oauth_signature' => signature(env, params)) 46 | end 47 | 48 | def signature env, params 49 | [Hmac.sha1("#{consumer_secret(env)}&#{oauth_token_secret(env)}", 50 | base_string(env, params))].pack('m0') 51 | end 52 | 53 | def base_string env, oauth_params 54 | method = env[REQUEST_METHOD].to_s.upcase 55 | base_uri = env[REQUEST_PATH] 56 | payload = payload_params(env) 57 | query = reject_blank(env[REQUEST_QUERY]) 58 | params = reject_blank(oauth_params.merge(query.merge(payload))). 59 | to_a.sort.map{ |(k, v)| 60 | "#{escape(k.to_s)}=#{escape(v.to_s)}"}.join('&') 61 | 62 | "#{method}&#{escape(base_uri)}&#{escape(params)}" 63 | end 64 | 65 | def nonce 66 | [OpenSSL::Random.random_bytes(32)].pack('m0').tr("+/=", '') 67 | end 68 | 69 | # according to OAuth 1.0a spec, only: 70 | # Content-Type: application/x-www-form-urlencoded 71 | # should take payload as a part of the base_string 72 | def payload_params env 73 | # if we already specified Content-Type and which is not 74 | # application/x-www-form-urlencoded, then we should not 75 | # take payload as a part of the base_string 76 | if env[REQUEST_HEADERS].kind_of?(Hash) && 77 | env[REQUEST_HEADERS]['Content-Type'] && 78 | env[REQUEST_HEADERS]['Content-Type'] != 79 | 'application/x-www-form-urlencoded' 80 | {} 81 | 82 | # if it contains any binary data, 83 | # then it shouldn't be application/x-www-form-urlencoded either 84 | # the Content-Type header would be handled in our HTTP client 85 | elsif contain_binary?(env[REQUEST_PAYLOAD]) 86 | {} 87 | 88 | # so the Content-Type header must be application/x-www-form-urlencoded 89 | else 90 | reject_blank(env[REQUEST_PAYLOAD]) 91 | end 92 | end 93 | 94 | def reject_blank params 95 | params.reject{ |k, v| v.nil? || v == false || 96 | (v.respond_to?(:strip) && 97 | v.respond_to?(:empty) && 98 | v.strip.empty? == true) } 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/oauth2_header.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/event' 3 | 4 | module RestCore 5 | # http://tools.ietf.org/html/rfc6749 6 | class Oauth2Header 7 | def self.members; [:access_token_type, :access_token]; end 8 | include Middleware 9 | 10 | def call env, &k 11 | start_time = Time.now 12 | headers = build_headers(env) 13 | auth = headers['Authorization'] 14 | event = Event::WithHeader.new(Time.now - start_time, 15 | "Authorization: #{auth}") if auth 16 | 17 | app.call(log(env.merge(REQUEST_HEADERS => headers), event), &k) 18 | end 19 | 20 | def build_headers env 21 | auth = case token = access_token(env) 22 | when String 23 | token 24 | when Hash 25 | token.map{ |(k, v)| "#{k}=\"#{v}\"" }.join(', ') 26 | end 27 | 28 | if auth 29 | {'Authorization' => "#{access_token_type(env)} #{auth}"} 30 | else 31 | {} 32 | end.merge(env[REQUEST_HEADERS]) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/oauth2_query.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | # http://tools.ietf.org/html/rfc6749 4 | class Oauth2Query 5 | def self.members; [:access_token]; end 6 | include Middleware 7 | 8 | def call env, &k 9 | local = if access_token(env) 10 | env.merge(REQUEST_QUERY => 11 | {'access_token' => access_token(env)}. 12 | merge(env[REQUEST_QUERY])) 13 | else 14 | env 15 | end 16 | 17 | app.call(local, &k) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/query_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/util/parse_query' 3 | 4 | module RestCore 5 | class QueryResponse 6 | def self.members; [:query_response]; end 7 | include Middleware 8 | 9 | QUERY_RESPONSE_HEADER = 10 | {'Accept' => 'application/x-www-form-urlencoded'}.freeze 11 | 12 | def call env, &k 13 | return app.call(env, &k) if env[DRY] 14 | return app.call(env, &k) unless query_response(env) 15 | 16 | headers = QUERY_RESPONSE_HEADER.merge(env[REQUEST_HEADERS]||{}) 17 | app.call(env.merge(REQUEST_HEADERS => headers)) do |response| 18 | body = ParseQuery.parse_query(response[RESPONSE_BODY]) 19 | yield(response.merge(RESPONSE_BODY => body)) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/retry.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/event' 3 | 4 | module RestCore 5 | class Retry 6 | def self.members; [:max_retries, :retry_exceptions]; end 7 | include Middleware 8 | 9 | DefaultRetryExceptions = [IOError, SystemCallError] 10 | 11 | def call env, &k 12 | if env[DRY] 13 | app.call(env, &k) 14 | else 15 | app.call(env){ |res| process(res, k) } 16 | end 17 | end 18 | 19 | def process res, k 20 | times = max_retries(res) 21 | return k.call(res) if times <= 0 22 | errors = retry_exceptions(res) || DefaultRetryExceptions 23 | 24 | if idx = res[FAIL].index{ |f| errors.find{ |e| f.kind_of?(e) } } 25 | err = res[FAIL].delete_at(idx) 26 | error_callback(res, err) 27 | env = res.merge('max_retries' => times - 1) 28 | event = Event::Retrying.new(nil, "(#{times}) for: #{err.inspect}") 29 | give_promise(call(log(env, event), &k)) 30 | else 31 | k.call(res) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/smash_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/util/smash' 3 | 4 | module RestCore 5 | class SmashResponse 6 | def self.members; [:smash_response]; end 7 | include Middleware 8 | 9 | def call env, &k 10 | return app.call(env, &k) if env[DRY] 11 | return app.call(env, &k) unless smash_response(env) 12 | 13 | app.call(env){ |res| 14 | if res[RESPONSE_BODY].kind_of?(Hash) 15 | yield(res.merge(RESPONSE_BODY => Smash.new(res[RESPONSE_BODY]))) 16 | else 17 | yield(res) 18 | end 19 | } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rest-core/middleware/timeout.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'timeout' 3 | 4 | module RestCore 5 | class Timeout 6 | def self.members; [:timeout]; end 7 | include Middleware 8 | 9 | def call env, &k 10 | return app.call(env, &k) if env[DRY] || timeout(env) == 0 11 | process(env, &k) 12 | end 13 | 14 | def process env, &k 15 | timer = PromisePool::Timer.new(timeout(env), timeout_error) 16 | app.call(env.merge(TIMER => timer), &k) 17 | rescue Exception 18 | timer.cancel 19 | raise 20 | end 21 | 22 | def timeout_error 23 | ::Timeout::Error.new('execution expired') 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/rest-core/test.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core' 3 | 4 | require 'pork/auto' 5 | require 'muack' 6 | require 'webmock' 7 | 8 | WebMock.enable! 9 | WebMock.disable_net_connect!(:allow_localhost => true) 10 | Pork::Suite.include(Muack::API, WebMock::API) 11 | -------------------------------------------------------------------------------- /lib/rest-core/util/clash.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | class Clash 4 | Empty = Hash.new(&(l = lambda{|_,_|Hash.new(&l).freeze})).freeze 5 | 6 | attr_accessor :data 7 | def initialize data 8 | self.data = data 9 | end 10 | 11 | def [] k 12 | if data.key?(k) 13 | convert(data[k]) 14 | else 15 | Empty 16 | end 17 | end 18 | 19 | def == rhs 20 | if rhs.kind_of?(Clash) 21 | data == rhs.data 22 | else 23 | data == rhs 24 | end 25 | end 26 | 27 | private 28 | def convert value 29 | case value 30 | when Hash 31 | Clash.new(value) 32 | when Array 33 | value.map{ |ele| convert(ele) } 34 | else 35 | value 36 | end 37 | end 38 | 39 | def respond_to_missing? msg, include_private=false 40 | data.respond_to?(msg, include_private) 41 | end 42 | 43 | def method_missing msg, *args, &block 44 | if data.respond_to?(msg) 45 | data.public_send(msg, *args, &block) 46 | else 47 | super 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rest-core/util/config.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'erb' 3 | require 'yaml' 4 | 5 | module RestCore 6 | module Config 7 | extend self 8 | DefaultModuleName = 'DefaultAttributes' 9 | 10 | def load klass, path, env, namespace=nil 11 | config = YAML.load(ERB.new(File.read(path)).result(binding)) 12 | defaults = config[env] 13 | return false unless defaults 14 | return false unless defaults[namespace] if namespace 15 | data = if namespace 16 | defaults[namespace] 17 | else 18 | defaults 19 | end 20 | raise ArgumentError.new("#{data} is not a hash") unless 21 | data.kind_of?(Hash) 22 | 23 | default_attributes_module(klass).module_eval( 24 | data.inject(["extend self\n"]){ |r, (k, v)| 25 | # quote strings, leave others free (e.g. false, numbers, etc) 26 | r << <<-RUBY 27 | def default_#{k} 28 | #{v.inspect} 29 | end 30 | RUBY 31 | }.join, __FILE__, __LINE__) 32 | end 33 | 34 | def default_attributes_module klass 35 | mod = if klass.const_defined?(DefaultModuleName) 36 | klass.const_get(DefaultModuleName) 37 | else 38 | klass.send(:const_set, DefaultModuleName, Module.new) 39 | end 40 | 41 | singleton_class = if klass.respond_to?(:singleton_class) 42 | klass.singleton_class 43 | else 44 | class << klass; self; end 45 | end 46 | 47 | klass.send(:extend, mod) unless singleton_class < mod 48 | mod 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rest-core/util/dalli_extension.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | module DalliExtension 4 | def [] *args 5 | get(*args) 6 | end 7 | 8 | def []= *args 9 | set(*args) 10 | end 11 | 12 | def store key, value, expires_in: nil 13 | set(key, value, expires_in) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rest-core/util/hmac.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'openssl' 3 | 4 | module RestCore 5 | module Hmac 6 | module_function 7 | def sha256 key, data 8 | OpenSSL::HMAC.digest('sha256', key, data) 9 | end 10 | 11 | def sha1 key, data 12 | OpenSSL::HMAC.digest('sha1', key, data) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rest-core/util/json.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | module Json 4 | module MultiJson 5 | def self.extended mod 6 | mod.const_set(:ParseError, ::MultiJson::DecodeError) 7 | end 8 | def encode hash 9 | ::MultiJson.dump(hash) 10 | end 11 | def decode json 12 | ::MultiJson.load(json) 13 | end 14 | end 15 | 16 | module YajlRuby 17 | def self.extended mod 18 | mod.const_set(:ParseError, Yajl::ParseError) 19 | end 20 | def encode hash 21 | Yajl::Encoder.encode(hash) 22 | end 23 | def decode json 24 | Yajl::Parser.parse(json) 25 | end 26 | end 27 | 28 | module Json 29 | def self.extended mod 30 | mod.const_set(:ParseError, JSON::ParserError) 31 | end 32 | def encode hash 33 | JSON.dump(hash) 34 | end 35 | def decode json 36 | JSON.parse(json, :quirks_mode => true) 37 | end 38 | end 39 | 40 | def self.select_json! mod, picked=false 41 | if Object.const_defined?(:MultiJson) 42 | mod.send(:extend, MultiJson) 43 | elsif Object.const_defined?(:Yajl) 44 | mod.send(:extend, YajlRuby) 45 | elsif Object.const_defined?(:JSON) 46 | mod.send(:extend, Json) 47 | elsif picked 48 | raise LoadError.new( 49 | 'No JSON library found. Tried: multi_json, yajl-ruby, json.') 50 | else 51 | # pick a json gem if available 52 | %w[multi_json yajl json].each{ |json| 53 | begin 54 | require json 55 | break 56 | rescue LoadError 57 | end 58 | } 59 | select_json!(mod, true) 60 | end 61 | end 62 | 63 | select_json!(self) 64 | 65 | def self.normalize json 66 | empty_to_null(strip_bom(json)) 67 | end 68 | 69 | def self.strip_bom json 70 | case json.encoding.name 71 | when 'UTF-8' 72 | # StackExchange returns the problematic BOM! in UTF-8, so we 73 | # need to strip it or it would break JSON parsers (i.e. 74 | # yajl-ruby and json) 75 | json.sub(/\A\xEF\xBB\xBF/u, '') 76 | when 'ASCII-8BIT' 77 | # In case if Content-Type doesn't have a charset for UTF-8, 78 | # httpclient would set the response to ASCII-8BIT in this 79 | # case. 80 | json.sub(/\A\xEF\xBB\xBF/n, '') 81 | else 82 | json 83 | end 84 | end 85 | 86 | def self.empty_to_null json 87 | if json.empty? 88 | 'null' 89 | else 90 | json 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/rest-core/util/parse_link.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | module ParseLink 4 | module_function 5 | # http://tools.ietf.org/html/rfc5988 6 | parname = '"?([^"]+)"?' 7 | LINKPARAM = /#{parname}=#{parname}/ 8 | def parse_link link 9 | link.split(',').inject({}) do |r, value| 10 | uri, *pairs = value.split(';') 11 | params = Hash[pairs.map{ |p| p.strip.match(LINKPARAM)[1..2] }] 12 | r[params['rel']] = params.merge('uri' => uri[/<([^>]+)>/, 1]) 13 | r 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rest-core/util/parse_query.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | module ParseQuery 4 | module_function 5 | begin 6 | require 'rack/utils' 7 | def parse_query(qs, d = nil) 8 | Rack::Utils.parse_query(qs, d) 9 | end 10 | rescue LoadError 11 | require 'uri' 12 | # Stolen from Rack 13 | def parse_query(qs, d = nil) 14 | params = {} 15 | 16 | (qs || '').split(d ? /[#{d}] */n : /[&;] */n).each do |p| 17 | k, v = p.split('=', 2).map { |x| URI.decode_www_form_component(x) } 18 | if cur = params[k] 19 | if cur.class == Array 20 | params[k] << v 21 | else 22 | params[k] = [cur, v] 23 | end 24 | else 25 | params[k] = v 26 | end 27 | end 28 | 29 | return params 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rest-core/util/smash.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore; end 3 | class RestCore::Smash 4 | attr_accessor :data 5 | def initialize data 6 | self.data = data 7 | end 8 | 9 | def [] *keys 10 | keys.inject(data) do |r, k| 11 | if r.respond_to?(:key) && r.key?(k) 12 | r[k] 13 | elsif r.respond_to?(:[]) 14 | r[k] 15 | else 16 | return nil # stop here 17 | end 18 | end 19 | end 20 | 21 | def == rhs 22 | if rhs.kind_of?(RestCore::Smash) 23 | data == rhs.data 24 | else 25 | data == rhs 26 | end 27 | end 28 | 29 | private 30 | def respond_to_missing? msg, include_private=false 31 | data.respond_to?(msg, include_private) 32 | end 33 | 34 | def method_missing msg, *args, &block 35 | if data.respond_to?(msg) 36 | data.public_send(msg, *args, &block) 37 | else 38 | super 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/rest-core/version.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestCore 3 | VERSION = '4.0.1' 4 | end 5 | -------------------------------------------------------------------------------- /rest-core.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: rest-core 4.0.1 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rest-core".freeze 6 | s.version = "4.0.1" 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 9 | s.require_paths = ["lib".freeze] 10 | s.authors = ["Lin Jen-Shin (godfat)".freeze] 11 | s.date = "2023-01-01" 12 | s.description = "Various [rest-builder](https://github.com/godfat/rest-builder) middleware\nfor building REST clients.\n\nCheckout [rest-more](https://github.com/godfat/rest-more) for pre-built\nclients.".freeze 13 | s.email = ["godfat (XD) godfat.org".freeze] 14 | s.files = [ 15 | ".gitignore".freeze, 16 | ".gitlab-ci.yml".freeze, 17 | ".gitmodules".freeze, 18 | "CHANGES.md".freeze, 19 | "Gemfile".freeze, 20 | "LICENSE".freeze, 21 | "NOTE.md".freeze, 22 | "README.md".freeze, 23 | "Rakefile".freeze, 24 | "TODO.md".freeze, 25 | "example/simple.rb".freeze, 26 | "example/use-cases.rb".freeze, 27 | "lib/rest-core.rb".freeze, 28 | "lib/rest-core/client/universal.rb".freeze, 29 | "lib/rest-core/client_oauth1.rb".freeze, 30 | "lib/rest-core/event.rb".freeze, 31 | "lib/rest-core/middleware/auth_basic.rb".freeze, 32 | "lib/rest-core/middleware/bypass.rb".freeze, 33 | "lib/rest-core/middleware/cache.rb".freeze, 34 | "lib/rest-core/middleware/clash_response.rb".freeze, 35 | "lib/rest-core/middleware/common_logger.rb".freeze, 36 | "lib/rest-core/middleware/default_headers.rb".freeze, 37 | "lib/rest-core/middleware/default_payload.rb".freeze, 38 | "lib/rest-core/middleware/default_query.rb".freeze, 39 | "lib/rest-core/middleware/default_site.rb".freeze, 40 | "lib/rest-core/middleware/defaults.rb".freeze, 41 | "lib/rest-core/middleware/error_detector.rb".freeze, 42 | "lib/rest-core/middleware/error_detector_http.rb".freeze, 43 | "lib/rest-core/middleware/error_handler.rb".freeze, 44 | "lib/rest-core/middleware/follow_redirect.rb".freeze, 45 | "lib/rest-core/middleware/json_request.rb".freeze, 46 | "lib/rest-core/middleware/json_response.rb".freeze, 47 | "lib/rest-core/middleware/oauth1_header.rb".freeze, 48 | "lib/rest-core/middleware/oauth2_header.rb".freeze, 49 | "lib/rest-core/middleware/oauth2_query.rb".freeze, 50 | "lib/rest-core/middleware/query_response.rb".freeze, 51 | "lib/rest-core/middleware/retry.rb".freeze, 52 | "lib/rest-core/middleware/smash_response.rb".freeze, 53 | "lib/rest-core/middleware/timeout.rb".freeze, 54 | "lib/rest-core/test.rb".freeze, 55 | "lib/rest-core/util/clash.rb".freeze, 56 | "lib/rest-core/util/config.rb".freeze, 57 | "lib/rest-core/util/dalli_extension.rb".freeze, 58 | "lib/rest-core/util/hmac.rb".freeze, 59 | "lib/rest-core/util/json.rb".freeze, 60 | "lib/rest-core/util/parse_link.rb".freeze, 61 | "lib/rest-core/util/parse_query.rb".freeze, 62 | "lib/rest-core/util/smash.rb".freeze, 63 | "lib/rest-core/version.rb".freeze, 64 | "rest-core.gemspec".freeze, 65 | "task/README.md".freeze, 66 | "task/gemgem.rb".freeze, 67 | "test/config/rest-core.yaml".freeze, 68 | "test/test_auth_basic.rb".freeze, 69 | "test/test_cache.rb".freeze, 70 | "test/test_clash.rb".freeze, 71 | "test/test_clash_response.rb".freeze, 72 | "test/test_client_oauth1.rb".freeze, 73 | "test/test_config.rb".freeze, 74 | "test/test_dalli_extension.rb".freeze, 75 | "test/test_default_headers.rb".freeze, 76 | "test/test_default_payload.rb".freeze, 77 | "test/test_default_query.rb".freeze, 78 | "test/test_default_site.rb".freeze, 79 | "test/test_error_detector.rb".freeze, 80 | "test/test_error_detector_http.rb".freeze, 81 | "test/test_error_handler.rb".freeze, 82 | "test/test_follow_redirect.rb".freeze, 83 | "test/test_json_request.rb".freeze, 84 | "test/test_json_response.rb".freeze, 85 | "test/test_oauth1_header.rb".freeze, 86 | "test/test_oauth2_header.rb".freeze, 87 | "test/test_parse_link.rb".freeze, 88 | "test/test_query_response.rb".freeze, 89 | "test/test_retry.rb".freeze, 90 | "test/test_smash.rb".freeze, 91 | "test/test_smash_response.rb".freeze, 92 | "test/test_timeout.rb".freeze, 93 | "test/test_universal.rb".freeze] 94 | s.homepage = "https://github.com/godfat/rest-core".freeze 95 | s.licenses = ["Apache-2.0".freeze] 96 | s.rubygems_version = "3.4.1".freeze 97 | s.summary = "Various [rest-builder](https://github.com/godfat/rest-builder) middleware".freeze 98 | s.test_files = [ 99 | "test/test_auth_basic.rb".freeze, 100 | "test/test_cache.rb".freeze, 101 | "test/test_clash.rb".freeze, 102 | "test/test_clash_response.rb".freeze, 103 | "test/test_client_oauth1.rb".freeze, 104 | "test/test_config.rb".freeze, 105 | "test/test_dalli_extension.rb".freeze, 106 | "test/test_default_headers.rb".freeze, 107 | "test/test_default_payload.rb".freeze, 108 | "test/test_default_query.rb".freeze, 109 | "test/test_default_site.rb".freeze, 110 | "test/test_error_detector.rb".freeze, 111 | "test/test_error_detector_http.rb".freeze, 112 | "test/test_error_handler.rb".freeze, 113 | "test/test_follow_redirect.rb".freeze, 114 | "test/test_json_request.rb".freeze, 115 | "test/test_json_response.rb".freeze, 116 | "test/test_oauth1_header.rb".freeze, 117 | "test/test_oauth2_header.rb".freeze, 118 | "test/test_parse_link.rb".freeze, 119 | "test/test_query_response.rb".freeze, 120 | "test/test_retry.rb".freeze, 121 | "test/test_smash.rb".freeze, 122 | "test/test_smash_response.rb".freeze, 123 | "test/test_timeout.rb".freeze, 124 | "test/test_universal.rb".freeze] 125 | 126 | s.specification_version = 4 127 | 128 | s.add_runtime_dependency(%q.freeze, [">= 0"]) 129 | end 130 | -------------------------------------------------------------------------------- /test/config/rest-core.yaml: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | facebook: 4 | app_id: 41829 5 | secret: <%= 'r41829'.reverse %> 6 | json_response: false 7 | lang: zh-tw 8 | auto_authorize_scope: 'publish_stream' 9 | -------------------------------------------------------------------------------- /test/test_auth_basic.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::AuthBasic do 5 | before do 6 | @auth = RC::AuthBasic.new(RC::Identity.new, nil, nil) 7 | end 8 | 9 | env = {RC::REQUEST_HEADERS => {}} 10 | 11 | would 'do nothing' do 12 | @auth.call({}){ |res| res.should.eq({}) } 13 | end 14 | 15 | would 'send Authorization header' do 16 | @auth.instance_eval{@username = 'Aladdin'} 17 | @auth.instance_eval{@password = 'open sesame'} 18 | 19 | @auth.call(env){ |res| 20 | res.should.eq(RC::REQUEST_HEADERS => 21 | {'Authorization' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}) 22 | } 23 | 24 | acc = {'Accept' => 'text/plain'} 25 | env = {RC::REQUEST_HEADERS => acc} 26 | 27 | @auth.call(env){ |res| 28 | res.should.eq({RC::REQUEST_HEADERS => 29 | {'Authorization' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}.merge(acc)}) 30 | } 31 | end 32 | 33 | would 'leave a log if username are not both provided' do 34 | @auth.instance_eval{@username = 'Aladdin'} 35 | @auth.call(env){ |res| res[RC::LOG].size.should.eq 1 } 36 | end 37 | 38 | would 'leave a log if password are not both provided' do 39 | @auth.instance_eval{@password = 'open sesame'} 40 | @auth.call(env){ |res| res[RC::LOG].size.should.eq 1 } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_cache.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Cache do 5 | after do 6 | WebMock.reset! 7 | Muack.verify 8 | end 9 | 10 | def simple_client 11 | RC::Builder.client{ use RC::Cache, {}, nil }.new 12 | end 13 | 14 | def json_client 15 | RC::Builder.client do 16 | use RC::JsonResponse, true 17 | use RC::Cache, {}, 3600 18 | end.new 19 | end 20 | 21 | would 'basic 0' do 22 | c = RC::Builder.client do 23 | use RC::Cache, {}, 3600 24 | run Class.new{ 25 | attr_accessor :tick 26 | def initialize 27 | self.tick = 0 28 | end 29 | def call env 30 | self.tick +=1 31 | yield(env.merge(RC::RESPONSE_BODY => 'response', 32 | RC::RESPONSE_HEADERS => {'A' => 'B'}, 33 | RC::RESPONSE_STATUS => 200)) 34 | end 35 | } 36 | end.new 37 | c.get('/') 38 | key = Digest::MD5.hexdigest('get:/:') 39 | c.cache.should.eq("rest-core:cache:#{key}" => "200\nA: B\n\n\nresponse") 40 | c.app.app.tick.should.eq 1 41 | c.get('/') 42 | c.app.app.tick.should.eq 1 43 | c.cache.clear 44 | c.get('/') 45 | c.app.app.tick.should.eq 2 46 | c.head('/').should.eq('A' => 'B') 47 | c.get('/').should.eq 'response' 48 | c.request(RC::REQUEST_PATH => '/', 49 | RC::RESPONSE_KEY => RC::RESPONSE_STATUS).should.eq 200 50 | end 51 | 52 | would 'basic 1' do 53 | path = 'http://a' 54 | stub_request(:get , path).to_return(:body => 'OK') 55 | stub_request(:post, path).to_return(:body => 'OK') 56 | c = RC::Builder.client do 57 | use RC::Cache, nil, nil 58 | end 59 | 60 | c.new . get(path).should.eq('OK') 61 | c.new(:cache => (h={})).post(path).should.eq('OK') 62 | h.should.eq({}) 63 | c.new(:cache => (h={})). get(path).should.eq('OK') 64 | h.size.should.eq 1 65 | c.new(:cache => (h={})). get(path, {}, :cache => false).should.eq('OK') 66 | h.should.eq({}) 67 | c.new . get(path, {}, 'cache.update' => true). 68 | should.eq('OK') 69 | end 70 | 71 | would 'still call callback for cached response' do 72 | client = RC::Builder.client{ use RC::Cache, {}, nil; run RC::Dry }.new 73 | client.get('', {}, RC::RESPONSE_BODY => 'nnf') do |a| 74 | client.get('') do |res| 75 | res.should.eq 'nnf' 76 | end 77 | end.wait 78 | end 79 | 80 | would 'not raise error if headers is nil' do 81 | path = 'http://a' 82 | stub_request(:get , path).to_return(:body => 'OK', :headers => nil) 83 | c = simple_client 84 | c.get(path).should.eq 'OK' 85 | c.get(path).should.eq 'OK' 86 | end 87 | 88 | would 'head then get' do 89 | c = simple_client 90 | path = 'http://example.com' 91 | stub_request(:head, path).to_return(:headers => {'A' => 'B'}) 92 | c.head(path).should.eq('A' => 'B') 93 | stub_request(:get , path).to_return(:body => 'body') 94 | c.get(path).should.eq('body') 95 | end 96 | 97 | would 'only [] and []= should be implemented' do 98 | cache = Class.new do 99 | def initialize ; @h = {} ; end 100 | def [] key ; @h[key] ; end 101 | def []= key, value; @h[key] = value.sub('4', '5'); end 102 | end.new 103 | c = RC::Builder.client do 104 | use RC::Cache, cache, 0 105 | run Class.new{ 106 | def call env 107 | yield(env.merge(RC::RESPONSE_BODY => env[RC::REQUEST_PATH], 108 | RC::RESPONSE_STATUS => 200)) 109 | end 110 | } 111 | end.new 112 | c.get('4') 113 | c.get('4').should.eq '5' 114 | end 115 | 116 | would 'cache the original response' do 117 | c = json_client 118 | stub_request(:get, 'http://me').to_return(:body => body = '{"a":"b"}') 119 | c.get('http://me').should.eq 'a' => 'b' 120 | c.cache.values.first.should.eq "200\n\n\n#{body}" 121 | end 122 | 123 | [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501].each do |status| 124 | would "only cache for HTTP status #{status}" do 125 | c = simple_client 126 | stub_request(:get, 'http://me'). 127 | to_return(:body => body = 'ok', :status => status) 128 | c.get('http://me').should.eq 'ok' 129 | c.cache.values.first.should.eq "#{status}\n\n\n#{body}" 130 | end 131 | end 132 | 133 | [201, 202, 134 | 302, 303, 304, 135 | 400, 401, 403, 406, 409, 136 | 500, 503, 504].each do |status| 137 | would "not cache for HTTP status #{status}" do 138 | c = simple_client 139 | stub_request(:get, 'http://me'). 140 | to_return(:body => body = 'ok', :status => status) 141 | c.get('http://me').should.eq 'ok' 142 | c.cache.values.first.should.nil? 143 | end 144 | end 145 | 146 | would 'cache multiple headers' do 147 | c = simple_client 148 | stub_request(:get, 'http://me').to_return(:headers => 149 | {'Apple' => 'Orange', 'Orange' => 'Apple'}) 150 | expected = {'APPLE' => 'Orange', 'ORANGE' => 'Apple'} 151 | args = ['http://me', {}, {RC::RESPONSE_KEY => RC::RESPONSE_HEADERS}] 152 | 2.times{ c.get(*args).should.eq expected } 153 | end 154 | 155 | would 'preserve promise and REQUEST_URI' do 156 | c = simple_client 157 | uri = 'http://me?a=b' 158 | stub_request(:get, uri) 159 | args = ['http://me', {:a => 'b'}, {RC::RESPONSE_KEY => RC::PROMISE}] 160 | 2.times{ c.get(*args).yield[RC::REQUEST_URI].should.eq uri } 161 | end 162 | 163 | # TODO: Pork::Error: Missing assertions 164 | # https://travis-ci.org/godfat/rest-core/jobs/105298775 165 | # https://travis-ci.org/godfat/rest-core/jobs/306729633 166 | would 'preserve promise and preserve wrapped call' do 167 | c = json_client 168 | stub_request(:get, 'http://me').to_return(:body => '{"a":"b"}') 169 | args = ['http://me', {}, {RC::RESPONSE_KEY => RC::PROMISE}] 170 | 2.times do 171 | c.get(*args).then{ |r| r[RC::RESPONSE_BODY].should.eq 'a' => 'b' }.yield 172 | end 173 | end 174 | 175 | would 'multiline response' do 176 | c = simple_client 177 | stub_request(:get, 'http://html').to_return(:body => body = "a\n\nb") 178 | c.get('http://html').should.eq body 179 | c.cache.values.first.should.eq "200\n\n\n#{body}" 180 | c.get('http://html').should.eq body 181 | end 182 | 183 | would "follow redirect with cache.update correctly" do 184 | c = RC::Builder.client do 185 | use RC::FollowRedirect, 10 186 | use RC::Cache, {}, nil 187 | end.new 188 | x, y, z = 'http://X', 'http://Y', 'http://Z' 189 | stub_request(:get, x).to_return(:headers => {'Location' => y}, 190 | :status => 301) 191 | stub_request(:get, y).to_return(:headers => {'Location' => z}, 192 | :status => 302) 193 | stub_request(:get, z).to_return(:body => 'OK') 194 | c.get(x, {}, 'cache.update' => true).should.eq 'OK' 195 | end 196 | 197 | would 'not cache post/put/delete' do 198 | [:put, :post, :delete].each{ |meth| 199 | url, body = "https://cache", 'ok' 200 | stub_request(meth, url).to_return(:body => body).times(3) 201 | 202 | cache = {} 203 | c = RC::Builder.client{use RC::Cache, cache, nil}.new 204 | 3.times{ 205 | if meth == :delete 206 | c.send(meth, url).should.eq(body) 207 | else 208 | c.send(meth, url, 'payload').should.eq(body) 209 | end 210 | } 211 | cache.should.eq({}) 212 | } 213 | end 214 | 215 | would 'not cache dry run' do 216 | c = simple_client 217 | c.url('test') 218 | c.cache.should.eq({}) 219 | end 220 | 221 | would 'not cache hijacking' do 222 | stub_request(:get, 'http://a').to_return(:body => 'ok') 223 | c = simple_client 224 | 2.times do 225 | c.get('http://a', {}, RC::HIJACK => true, 226 | RC::RESPONSE_KEY => RC::RESPONSE_SOCKET). 227 | read.should.eq 'ok' 228 | end 229 | c.cache.should.eq({}) 230 | end 231 | 232 | would 'update cache if there is cache option set to false' do 233 | url, body = "https://cache", 'ok' 234 | stub_request(:get, url).to_return(:body => body) 235 | c = simple_client 236 | 237 | c.get(url) .should.eq body 238 | stub_request(:get, url).to_return(:body => body.reverse).times(2) 239 | c.get(url) .should.eq body 240 | c.get(url, {}, 'cache.update' => true).should.eq body.reverse 241 | c.get(url) .should.eq body.reverse 242 | c.cache = nil 243 | c.get(url, {}, 'cache.update' => true).should.eq body.reverse 244 | end 245 | 246 | describe 'expires_in' do 247 | before do 248 | @url, @body = "https://cache", 'ok' 249 | stub_request(:get, @url).to_return(:body => @body) 250 | @cache = {} 251 | mock(@cache).method(:store){ mock.arity{ -3 }.object } 252 | mock(@cache).store(is_a(String), is_a(String), :expires_in => 3){} 253 | @cache 254 | end 255 | 256 | would 'respect in options' do 257 | c = RC::Builder.client{use RC::Cache, nil, nil}.new 258 | c.get(@url, {}, :cache => @cache, :expires_in => 3).should.eq @body 259 | end 260 | 261 | would 'respect in middleware' do 262 | c = RC::Builder.client{use RC::Cache, nil, 3}.new(:cache => @cache) 263 | c.get(@url).should.eq @body 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /test/test_clash.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Clash do 5 | would 'never give nil for non-existing values' do 6 | h = {0 => 1, 2 => {3 => 4, 5 => [6, {7 => 8}]}, 9 => false, 10 => nil} 7 | c = RC::Clash.new(h) 8 | c[0] .should.eq(1) 9 | c[1] .should.eq({}) 10 | c[1][2] .should.eq({}) 11 | c[1][2][3] .should.eq({}) 12 | c[2] .should.eq(3 => 4, 5 => [6, {7 => 8}]) 13 | c[2][3] .should.eq(4) 14 | c[2][4] .should.eq({}) 15 | c[2][4][5] .should.eq({}) 16 | c[2][5] .should.eq([6, {7 => 8}]) 17 | c[2][5][1] .should.eq(7 => 8) 18 | c[2][5][1][7] .should.eq(8) 19 | c[2][5][1][8] .should.eq({}) 20 | c[2][5][1][8][9].should.eq({}) 21 | c[9] .should.eq(false) 22 | c[10] .should.eq(nil) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_clash_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ClashResponse do 5 | describe 'app' do 6 | app = RC::ClashResponse.new(RC::Identity.new, true) 7 | 8 | would 'do nothing' do 9 | env = {RC::RESPONSE_BODY => []} 10 | app.call(env) do |res| 11 | res.should.eq(env) 12 | res[RC::RESPONSE_BODY].should.kind_of?(Array) 13 | end 14 | end 15 | 16 | would 'clash' do 17 | app.call(RC::RESPONSE_BODY => {}) do |res| 18 | body = res[RC::RESPONSE_BODY] 19 | body.should.kind_of?(RC::Clash) 20 | body.should.empty? 21 | body[0].should.eq({}) 22 | body[0][0].should.eq({}) 23 | end 24 | end 25 | 26 | describe 'client' do 27 | body = {0 => {1 => 2}} 28 | client = RC::Builder.client do 29 | use RC::ClashResponse, true 30 | run Class.new{ 31 | define_method(:call) do |env, &block| 32 | block.call(env.merge(RC::RESPONSE_BODY => body)) 33 | end 34 | } 35 | end 36 | 37 | would 'do nothing' do 38 | b = client.new(:clash_response => false).get(''){ |res| 39 | res.should.eq(body) 40 | res.should.kind_of?(Hash) 41 | }.get('') 42 | b.should.eq(body) 43 | b.should.kind_of?(Hash) 44 | end 45 | 46 | would 'clash' do 47 | b = client.new.get(''){ |res| 48 | res.should.eq(body) 49 | res.should.kind_of?(RC::Clash) 50 | }.get('') 51 | b.should.eq(body) 52 | b.should.kind_of?(RC::Clash) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/test_client_oauth1.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ClientOauth1 do 5 | after do 6 | WebMock.reset! 7 | Muack.verify 8 | end 9 | 10 | client = RC::Builder.client do 11 | use RC::Oauth1Header 12 | end 13 | 14 | client.send(:include, RC::ClientOauth1) 15 | 16 | would 'restore data with correct sig' do 17 | data = {'a' => 'b', 'c' => 'd'} 18 | sig = Digest::MD5.hexdigest('e&a=b&c=d') 19 | data_sig = data.merge('sig' => sig) 20 | data_json = RC::Json.encode(data_sig) 21 | c = client.new(:data => data, :consumer_secret => 'e') 22 | 23 | c.send(:calculate_sig).should.eq sig 24 | c.data_json.should.eq data_json 25 | 26 | c.data_json = data_json 27 | c.data.should.eq data_sig 28 | 29 | c.data_json = RC::Json.encode(data_sig.merge('sig' => 'wrong')) 30 | c.data.should.eq({}) 31 | 32 | c.data_json = data_json 33 | c.data.should.eq data_sig 34 | 35 | c.data_json = 'bad json' 36 | c.data.should.eq({}) 37 | end 38 | 39 | would 'have correct default data' do 40 | c = client.new 41 | c.data.should.eq({}) 42 | c.data = nil 43 | c.data['a'] = 'b' 44 | c.data['a'].should.eq 'b' 45 | end 46 | 47 | would 'authorize' do 48 | stub_request(:post, 'http://localhost'). 49 | to_return(:body => 'oauth_token=abc') 50 | 51 | stub_request(:post, 'http://nocalhost'). 52 | to_return(:body => 'user_id=123&haha=point') 53 | 54 | c = client.new(:request_token_path => 'http://localhost', 55 | :authorize_path => 'http://mocalhost', 56 | :access_token_path => 'http://nocalhost') 57 | 58 | c.authorize_url!.should.eq 'http://mocalhost?oauth_token=abc' 59 | c.authorize!.should.eq('user_id' => '123', 'haha' => 'point', 60 | 'authorized' => 'true') 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/test_config.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Config do 5 | before do 6 | @klass = RC::Builder.client 7 | end 8 | 9 | after do 10 | Muack.verify 11 | end 12 | 13 | def check 14 | @klass.default_app_id .should.eq 41829 15 | @klass.default_secret .should.eq 'r41829'.reverse 16 | @klass.default_json_response.should.eq false 17 | @klass.default_lang .should.eq 'zh-tw' 18 | end 19 | 20 | would 'honor config' do 21 | RC::Config.load( 22 | @klass, 23 | "#{File.dirname(__FILE__)}/config/rest-core.yaml", 24 | 'test', 25 | 'facebook') 26 | check 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_dalli_extension.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::DalliExtension do 5 | after do 6 | Muack.verify 7 | end 8 | 9 | def engine env 10 | yield(env.merge(RC::RESPONSE_STATUS => 200, 11 | RC::RESPONSE_HEADERS => {'A' => 'B'}, 12 | RC::RESPONSE_BODY => 'ok')) 13 | end 14 | 15 | def call dalli, opts={}, &block 16 | dalli.extend(RC::DalliExtension) 17 | RC::Cache.new(method(:engine), dalli, 10). 18 | call({RC::REQUEST_METHOD => :get, 19 | RC::REQUEST_HEADERS => {}}.merge(opts), 20 | &lambda{|_|_})[RC::RESPONSE_BODY] 21 | end 22 | 23 | would 'set and get' do 24 | dalli = Object.new 25 | mock(dalli).set(is_a(String), nil) do |key, value| 26 | mock(dalli).get(key){ "200\nA: B\n\n\nok" } 27 | end 28 | 29 | expect(call(dalli, 'cache.update' => true)).eq 'ok' 30 | end 31 | 32 | would 'store' do 33 | dalli = Object.new 34 | mock(dalli).get(is_a(String)) do |key| 35 | mock(dalli).set(key, "200\nA: B\n\n\nok", 10){} 36 | nil 37 | end 38 | 39 | expect(call(dalli)).eq 'ok' 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_default_headers.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::DefaultHeaders do 5 | app = RC::DefaultHeaders.new(RC::Dry.new, 'a' => 'b') 6 | 7 | would 'also merge the very default headers' do 8 | app.call('headers' => {'b' => 'c'}, 9 | RC::REQUEST_HEADERS => {'c' => 'd'}) do |r| 10 | r[RC::REQUEST_HEADERS].should.eq 'a' => 'b', 'b' => 'c', 'c' => 'd' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_default_payload.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::DefaultPayload do 5 | app = RC::DefaultPayload.new(RC::Identity.new, {}) 6 | env = {RC::REQUEST_PAYLOAD => {}} 7 | 8 | before do 9 | app.instance_eval{@payload = {}} 10 | end 11 | 12 | would 'do nothing' do 13 | app.call(env){ |r| r[RC::REQUEST_PAYLOAD].should.eq({}) } 14 | end 15 | 16 | would 'merge payload' do 17 | app.instance_eval{@payload = {'pay' => 'load'}} 18 | 19 | app.call(env){ |r| r.should.eq({RC::REQUEST_PAYLOAD => 20 | {'pay' => 'load'}}) } 21 | 22 | format = {'format' => 'json'} 23 | e = {RC::REQUEST_PAYLOAD => format} 24 | 25 | app.call(e){ |r| r.should.eq({RC::REQUEST_PAYLOAD => 26 | {'pay' => 'load'}.merge(format)})} 27 | end 28 | 29 | would 'also merge the very default payload' do 30 | a = RC::DefaultPayload.new(RC::Identity.new, 'a' => 'b') 31 | a.call('payload' => {'b' => 'c'}, 32 | RC::REQUEST_PAYLOAD => {'c' => 'd'}) do |r| 33 | r[RC::REQUEST_PAYLOAD].should.eq 'a' => 'b', 'b' => 'c', 'c' => 'd' 34 | end 35 | end 36 | 37 | would 'accept non-hash payload' do 38 | u = RC::Universal.new(:log_method => false) 39 | e = {RC::REQUEST_PAYLOAD => 'payload'} 40 | u.request_full( e, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload') 41 | 42 | u.payload = 'default' 43 | u.request_full( e, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload') 44 | u.request_full({}, u.dry)[RC::REQUEST_PAYLOAD].should.eq('default') 45 | 46 | u = RC::Builder.client{use RC::DefaultPayload, 'maylord'}.new 47 | u.request_full({}, u.dry)[RC::REQUEST_PAYLOAD].should.eq('maylord') 48 | u.request_full( e, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload') 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_default_query.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::DefaultQuery do 5 | before do 6 | @app = RC::DefaultQuery.new(RC::Identity.new, {}) 7 | end 8 | 9 | env = {RC::REQUEST_QUERY => {}} 10 | 11 | describe 'when given query' do 12 | would 'do nothing' do 13 | @app.call(env){ |r| r[RC::REQUEST_QUERY].should.eq({}) } 14 | end 15 | 16 | would 'merge query' do 17 | @app.instance_eval{@query = {'q' => 'uery'}} 18 | 19 | @app.call(env){ |r| r.should.eq({RC::REQUEST_QUERY => {'q' => 'uery'}}) } 20 | 21 | format = {'format' => 'json'} 22 | e = {RC::REQUEST_QUERY => format} 23 | 24 | @app.call(e){ |r| 25 | r.should.eq({RC::REQUEST_QUERY => {'q' => 'uery'}.merge(format)}) } 26 | end 27 | 28 | would 'string_keys in query' do 29 | e = {'query' => {:symbol => 'value'}} 30 | @app.call(env.merge(e)){ |r| 31 | r.should.eq({RC::REQUEST_QUERY => {'symbol' => 'value'}}.merge(e)) 32 | } 33 | end 34 | 35 | would 'also merge the very default query' do 36 | @app.query = {'a' => 'b'} 37 | @app.call('query' => {'b' => 'c'}, 38 | RC::REQUEST_QUERY => {'c' => 'd'}) do |r| 39 | r[RC::REQUEST_QUERY].should.eq 'a' => 'b', 'b' => 'c', 'c' => 'd' 40 | end 41 | end 42 | end 43 | 44 | describe 'when not given query' do 45 | would 'merge query with {}' do 46 | @app.call(env){ |r| r.should.eq(RC::REQUEST_QUERY => {}) } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/test_default_site.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::DefaultSite do 5 | app = RC::DefaultSite.new(RC::Dry.new, 'http://example.com/') 6 | 7 | would 'leave site along if it already has a protocol' do 8 | app.call(RC::REQUEST_PATH => 'http://nnf.tw') do |res| 9 | res[RC::REQUEST_PATH].should.eq 'http://nnf.tw' 10 | end 11 | end 12 | 13 | would 'prepend the site if there is no protocol' do 14 | app.call(RC::REQUEST_PATH => 'nnf.tw') do |res| 15 | res[RC::REQUEST_PATH].should.eq 'http://example.com/nnf.tw' 16 | end 17 | end 18 | 19 | would 'not prepend duplicated /' do 20 | app.call(RC::REQUEST_PATH => '/nnf.tw') do |res| 21 | res[RC::REQUEST_PATH].should.eq 'http://example.com/nnf.tw' 22 | end 23 | end 24 | 25 | would 'concatenate site and path regardlessly' do 26 | app.call(RC::REQUEST_PATH => 'nnf.tw', 'site' => 'example.com') do |res| 27 | res[RC::REQUEST_PATH].should.eq 'example.com/nnf.tw' 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_error_detector.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ErrorDetector do 5 | would 'lighten' do 6 | client = RC::Builder.client do 7 | use RC::ErrorDetector 8 | run RC::Dry 9 | end.new.lighten 10 | 11 | client.attributes.should.key?(:error_detector) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_error_detector_http.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ErrorDetectorHttp do 5 | would 'lighten' do 6 | client = RC::Builder.client do 7 | use RC::ErrorDetectorHttp 8 | run RC::Dry 9 | end.new.lighten 10 | 11 | client.attributes.should.key?(:error_detector) 12 | client.error_detector.should.kind_of?(Proc) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/test_error_handler.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ErrorHandler do 5 | client = RC::Builder.client do 6 | use RC::ErrorHandler 7 | run RC::Dry 8 | end 9 | 10 | exp = Class.new(Exception) 11 | 12 | describe 'there is an exception' do 13 | would 'raise an error with future' do 14 | lambda{ 15 | client.new.get('/', {}, RC::FAIL => [exp.new('fail')]) 16 | }.should.raise(exp) 17 | end 18 | 19 | would 'give an error with callback' do 20 | client.new.get('/', {}, RC::FAIL => [exp.new('fail')]){ |res| 21 | res.should.kind_of?(exp) 22 | }.wait 23 | end 24 | end 25 | 26 | describe 'error_handler gives an exception' do 27 | would 'raise an error with future' do 28 | lambda{ 29 | client.new(:error_handler => lambda{ |res| exp.new }). 30 | get('/', {}, RC::FAIL => [true]) 31 | }.should.raise(exp) 32 | end 33 | 34 | would 'give an error with callback' do 35 | client.new(:error_handler => lambda{ |res| exp.new }). 36 | get('/', {}, RC::FAIL => [true]){ |res| res.should.kind_of?(exp) }. 37 | wait 38 | end 39 | end 40 | 41 | would 'no exception but errors' do 42 | client.new(:error_handler => lambda{ |res| 1 }). 43 | request(RC::FAIL => [0], RC::RESPONSE_KEY => RC::FAIL).should.eq [0, 1] 44 | end 45 | 46 | would 'set full backtrace' do 47 | url = 'http://example.com/' 48 | c = RC::Builder.client do 49 | use RC::ErrorHandler, lambda{ |env| 50 | RuntimeError.new(env[RC::RESPONSE_BODY]) } 51 | use RC::ErrorDetectorHttp 52 | end.new 53 | stub_request(:get, url).to_return(:status => 404, :body => 'nnf') 54 | c.get(url) do |error| 55 | error.message.should.eq 'nnf' 56 | error.backtrace.grep(/^#{__FILE__}/).should.not.empty? 57 | end 58 | c.wait 59 | end 60 | 61 | would 'not call error_handler if there is already an exception' do 62 | called = false 63 | RC::Builder.client do 64 | use RC::ErrorHandler, lambda{ |_| called = true } 65 | end.new.get('http://localhost:1') do |error| 66 | error.should.kind_of?(SystemCallError) 67 | end.wait 68 | called.should.eq false 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/test_follow_redirect.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::FollowRedirect do 5 | dry = Class.new do 6 | attr_accessor :status 7 | def call env 8 | yield(env.merge(RC::RESPONSE_STATUS => status, 9 | RC::RESPONSE_HEADERS => {'LOCATION' => 'location'})) 10 | env 11 | end 12 | end.new 13 | app = RC::FollowRedirect.new(dry, 1) 14 | 15 | after do 16 | Muack.verify 17 | end 18 | 19 | [301, 302, 303, 307].each do |status| 20 | would "not follow redirect if reached max_redirects: #{status}" do 21 | dry.status = status 22 | app.call(RC::REQUEST_METHOD => :get, 'max_redirects' => 0) do |res| 23 | res[RC::RESPONSE_HEADERS]['LOCATION'].should.eq 'location' 24 | end 25 | end 26 | 27 | would "follow once: #{status}" do 28 | dry.status = status 29 | app.call(RC::REQUEST_METHOD => :get) do |res| 30 | res[RC::RESPONSE_HEADERS]['LOCATION'].should.eq 'location' 31 | end 32 | end 33 | 34 | would "not carry query string: #{status}" do 35 | dry.status = status 36 | app.call(RC::REQUEST_METHOD => :get, 37 | RC::REQUEST_QUERY => {:a => 'a'}) do |res| 38 | res[RC::REQUEST_PATH] .should.eq 'location' 39 | res[RC::REQUEST_QUERY].should.empty? 40 | end 41 | end 42 | 43 | would "carry payload for #{status}" do 44 | dry.status = status 45 | app.call(RC::REQUEST_METHOD => :put, 46 | RC::REQUEST_PAYLOAD => {:a => 'a'}) do |res| 47 | res[RC::REQUEST_PAYLOAD].should.eq(:a => 'a') 48 | end 49 | end if status != 303 50 | end 51 | 52 | [200, 201, 404, 500].each do |status| 53 | would "not follow redirect if it's not a redirect status: #{status}" do 54 | dry.status = status 55 | app.call(RC::REQUEST_METHOD => :get, 'max_redirects' => 9) do |res| 56 | res[RC::RESPONSE_HEADERS]['LOCATION'].should.eq 'location' 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_json_request.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::JsonRequest do 5 | app = RC::JsonRequest.new(RC::Identity.new, true) 6 | env = {RC::REQUEST_HEADERS => {}, RC::REQUEST_METHOD => :post} 7 | request_params = { 8 | 'key' => 'value', 9 | 'array' => [1, 2, 3], 10 | 'nested' => {'k' => 'v', 'a' => [4, 5, 6]} 11 | } 12 | 13 | would 'encode payload as json' do 14 | e = env.merge(RC::REQUEST_METHOD => :post, 15 | RC::REQUEST_PAYLOAD => request_params) 16 | 17 | app.call(e){ |res| 18 | res.should.eq( 19 | RC::REQUEST_METHOD => :post, 20 | RC::REQUEST_HEADERS => {'Content-Type' => 'application/json'}, 21 | RC::REQUEST_PAYLOAD => RC::Json.encode(request_params))} 22 | end 23 | 24 | [[nil, 'null'], 25 | [false, 'false'], 26 | [true, 'true'], 27 | [{}, '{}'], 28 | [RC::Payload::Unspecified.new, {}]].each do |(value, exp)| 29 | would "encode #{value} as #{exp.inspect}" do 30 | [:post, :put, :patch, :delete].each do |meth| 31 | e = env.merge(RC::REQUEST_METHOD => meth, 32 | RC::REQUEST_PAYLOAD => value) 33 | app.call(e){ |res| res[RC::REQUEST_PAYLOAD].should.eq(exp) } 34 | end 35 | end 36 | end 37 | 38 | would 'do nothing if payload is not specified' do 39 | [:get, :head, :options, :delete, :post].each do |meth| 40 | e = env.merge(RC::REQUEST_PAYLOAD => RC::Payload::Unspecified.new, 41 | RC::REQUEST_METHOD => meth) 42 | app.call(e){ |res| res.should.eq e } 43 | end 44 | end 45 | 46 | would 'do nothing if json_request is false' do 47 | a = RC::JsonRequest.new(RC::Identity.new, false) 48 | a.call(env){ |res| res.should.eq env } 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_json_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::JsonResponse do 5 | describe 'app' do 6 | app = RC::JsonResponse.new(RC::Identity.new, true) 7 | bad = 'bad json' 8 | 9 | would 'do nothing' do 10 | expected = {RC::RESPONSE_BODY => nil, 11 | RC::REQUEST_HEADERS => {'Accept' => 'application/json'}} 12 | app.call(RC::RESPONSE_BODY => '') do |response| 13 | response.should.eq(expected) 14 | end 15 | end 16 | 17 | would 'decode' do 18 | expected = {RC::RESPONSE_BODY => {}, 19 | RC::REQUEST_HEADERS => {'Accept' => 'application/json'}} 20 | app.call(RC::RESPONSE_BODY => '{}') do |response| 21 | response.should.eq(expected) 22 | end 23 | end 24 | 25 | would 'not decode but just return nil if response body is nil' do 26 | expected = {RC::RESPONSE_BODY => nil, 27 | RC::REQUEST_HEADERS => {'Accept' => 'application/json'}} 28 | app.call({}) do |response| 29 | response.should.eq(expected) 30 | end 31 | end 32 | 33 | would 'give proper parse error' do 34 | app.call(RC::RESPONSE_BODY => bad) do |response| 35 | err = response[RC::FAIL].first 36 | err.should.kind_of?(RC::Json.const_get(:ParseError)) 37 | err.should.kind_of?(RC::JsonResponse::ParseError) 38 | end 39 | end 40 | 41 | would 'give me original text' do 42 | app.call(RC::RESPONSE_BODY => bad) do |response| 43 | err = response[RC::FAIL].first 44 | err.message .should.include?(bad) 45 | err.body .should.eq(bad) 46 | err.cause.class.should.eq(RC::Json.const_get(:ParseError)) 47 | end 48 | end 49 | 50 | would 'remove UTF-8 BOM' do 51 | body = %Q{\xEF\xBB\xBF"UTF-8"} 52 | 53 | app.call(RC::RESPONSE_BODY => body) do |response| 54 | expect(response[RC::RESPONSE_BODY]).eq 'UTF-8' 55 | end 56 | end 57 | 58 | would 'remove UTF-8 BOM for ASCII-8BIT' do 59 | body = %Q{\xEF\xBB\xBF"UTF-8"}.force_encoding('ASCII-8BIT') 60 | 61 | app.call(RC::RESPONSE_BODY => body) do |response| 62 | expect(response[RC::RESPONSE_BODY]).eq 'UTF-8' 63 | end 64 | end 65 | end 66 | 67 | describe 'client' do 68 | client = RC::Builder.client do 69 | use RC::JsonResponse, true 70 | run Class.new{ 71 | def call env 72 | yield(env.merge(RC::RESPONSE_BODY => '{}')) 73 | end 74 | } 75 | end 76 | 77 | would 'do nothing' do 78 | expected = '{}' 79 | client.new(:json_response => false).get(''){ |response| 80 | response.should.eq(expected) 81 | }.get('').should.eq(expected) 82 | end 83 | 84 | would 'decode' do 85 | expected = {} 86 | client.new.get(''){ |response| 87 | response.should.eq(expected) 88 | }.get('').should.eq(expected) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/test_oauth1_header.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Oauth1Header do 5 | env = {RC::REQUEST_METHOD => :post, 6 | RC::REQUEST_PATH => 7 | 'https://api.twitter.com/oauth/request_token', 8 | RC::REQUEST_QUERY => {}, 9 | RC::REQUEST_PAYLOAD => {}} 10 | 11 | callback = 12 | 'http://localhost:3005/the_dance/process_callback?service_provider_id=11' 13 | 14 | oauth_params = 15 | {'oauth_callback' => callback , 16 | 'oauth_consumer_key' => 'GDdmIQH6jhtmLUypg82g' , 17 | 'oauth_nonce' => 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk', 18 | 'oauth_timestamp' => '1272323042' , 19 | 'oauth_version' => '1.0' , 20 | 'oauth_signature_method' => 'HMAC-SHA1'} 21 | 22 | auth = RC::Oauth1Header.new(RC::Dry.new, 23 | nil, nil, nil, 24 | 'GDdmIQH6jhtmLUypg82g', 25 | 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98') 26 | 27 | sig = '8wUi7m5HFQy76nowoCThusfgB+Q=' 28 | 29 | after do 30 | Muack.verify 31 | end 32 | 33 | would 'have correct signature' do 34 | auth.signature(env, oauth_params).should.eq(sig) 35 | end 36 | 37 | would 'escape keys and values' do 38 | mock(Time).now{ 39 | mock.to_i{ 40 | mock.to_s{ 41 | oauth_params['oauth_timestamp'] }.object }.object } 42 | mock(auth).nonce{ oauth_params['oauth_nonce'] } 43 | 44 | oauth = auth.oauth_header(env.merge(oauth_params)) 45 | oauth.should. 46 | include?("oauth_callback=\"#{RC::Middleware.escape(callback)}\"") 47 | oauth.should. 48 | include?("oauth_signature=\"#{RC::Middleware.escape(sig)}\"") 49 | end 50 | 51 | describe 'base_string' do 52 | base_string = 53 | 'POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&' \ 54 | 'oauth_callback%3Dhttp%253A%252F%252Flocalhost%253A3005%252F' \ 55 | 'the_dance%252Fprocess_callback%253Fservice_provider_id%253D' \ 56 | '11%26oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonc' \ 57 | 'e%3DQP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk%26oauth_sig' \ 58 | 'nature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272323042%26' \ 59 | 'oauth_version%3D1.0' 60 | 61 | check = lambda do |e, b| 62 | auth.base_string(e, oauth_params).should.eq b 63 | end 64 | 65 | would 'have correct base_string' do 66 | check[env, base_string] 67 | end 68 | 69 | would 'not use payload in multipart request for base_string' do 70 | File.open(__FILE__) do |f| 71 | check[env.merge(RC::REQUEST_PAYLOAD => {'file' => f}), base_string] 72 | end 73 | end 74 | 75 | would 'not use payload if it contains binary' do 76 | File.open(__FILE__) do |f| 77 | check[env.merge(RC::REQUEST_PAYLOAD => f), base_string] 78 | end 79 | end 80 | 81 | would 'not use payload if it contains [binary]' do 82 | File.open(__FILE__) do |f| 83 | check[env.merge(RC::REQUEST_PAYLOAD => [f]), base_string] 84 | end 85 | end 86 | 87 | would 'not use payload if Content-Type is not x-www-form-urlencoded' do 88 | check[ 89 | env.merge(RC::REQUEST_PAYLOAD => {'pay' => 'load'}, 90 | RC::REQUEST_HEADERS => {'Content-Type' => 'text/plain'}), 91 | base_string] 92 | end 93 | 94 | would 'use payload if Content-Type is x-www-form-urlencoded' do 95 | check[env.merge( 96 | RC::REQUEST_PAYLOAD => {'pay' => 'load'}, 97 | RC::REQUEST_HEADERS => 98 | {'Content-Type' => 'application/x-www-form-urlencoded'}), 99 | base_string + '%26pay%3Dload'] 100 | end 101 | 102 | would 'use payload if there is no binary data' do 103 | check[env.merge(RC::REQUEST_PAYLOAD => {'pay' => 'load'}), 104 | base_string + '%26pay%3Dload'] 105 | end 106 | 107 | would 'not escape ~' do 108 | check[env.merge(RC::REQUEST_PAYLOAD => {'tilde' => '~'}), 109 | base_string + '%26tilde%3D~'] 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/test_oauth2_header.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Oauth2Header do 5 | env = {RC::REQUEST_HEADERS => {}} 6 | auth = RC::Oauth2Header.new(RC::Identity.new) 7 | 8 | would 'do nothing if no access token' do 9 | auth.call(env){ |res| res.should.eq(env) } 10 | end 11 | 12 | would 'Bearer token' do 13 | auth.call(env.merge('access_token_type' => 'Bearer', 14 | 'access_token' => 'token')){ |res| 15 | res[RC::REQUEST_HEADERS].should.eq 'Authorization' => 'Bearer token' 16 | } 17 | end 18 | 19 | would 'MAC token' do # http://tools.ietf.org/html/rfc6749#section-7.1 20 | auth.call(env.merge('access_token_type' => 'MAC', 21 | 'access_token' => 22 | {'id' => 'h480djs93hd8', 23 | 'mac' => 'kDZvddkndxv='})){ |res| 24 | res[RC::REQUEST_HEADERS].should.eq \ 25 | 'Authorization' => 'MAC id="h480djs93hd8", mac="kDZvddkndxv="' 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_parse_link.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::ParseLink do 5 | describe 'http://tools.ietf.org/html/rfc5988' do 6 | would '5.5 a' do 7 | link = '; rel="previous"; title="previous chapter"' 8 | RC::ParseLink.parse_link(link).should.eq( 9 | 'previous' => {'uri' => 'http://example.com/TheBook/chapter2', 10 | 'rel' => 'previous', 11 | 'title' => 'previous chapter'}) 12 | end 13 | 14 | would '5.5 b' do 15 | link = '; rel="http://example.net/foo"' 16 | RC::ParseLink.parse_link(link).should.eq( 17 | 'http://example.net/foo' => {'uri' => '/', 18 | 'rel' => 'http://example.net/foo'}) 19 | end 20 | 21 | would '5.5 c (we did not implement * and unescape for now)' do 22 | link = <<-LINK 23 | ; rel="previous"; title*=UTF-8'de'letztes%20Kapitel, ; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel 24 | LINK 25 | RC::ParseLink.parse_link(link).should.eq( 26 | 'previous' => {'uri' => '/TheBook/chapter2', 27 | 'rel' => 'previous', 28 | 'title*' => "UTF-8'de'letztes%20Kapitel"}, 29 | 'next' => {'uri' => '/TheBook/chapter4', 30 | 'rel' => 'next', 31 | 'title*' => "UTF-8'de'n%c3%a4chstes%20Kapitel"}) 32 | end 33 | 34 | would '5.5 d' do 35 | link = '; rel="start http://example.net/relation/other"' 36 | 37 | RC::ParseLink.parse_link(link).should.eq( 38 | 'start http://example.net/relation/other' => 39 | {'uri' => 'http://example.org/', 40 | 'rel' => 'start http://example.net/relation/other'}) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/test_query_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::QueryResponse do 5 | describe 'app' do 6 | app = RC::QueryResponse.new(RC::Identity.new, true) 7 | expected = {RC::RESPONSE_BODY => {}, 8 | RC::REQUEST_HEADERS => 9 | {'Accept' => 'application/x-www-form-urlencoded'}} 10 | 11 | would 'give {} for nil' do 12 | app.call({}){ |response| response.should.eq(expected) } 13 | end 14 | 15 | would 'give {} for ""' do 16 | app.call(RC::RESPONSE_BODY => ''){ |r| r.should.eq(expected) } 17 | end 18 | 19 | would 'give {"a" => "b"} for "a=b"' do 20 | e = expected.merge(RC::RESPONSE_BODY => {'a' => 'b'}) 21 | app.call(RC::RESPONSE_BODY => 'a=b'){ |r| r.should.eq(e) } 22 | end 23 | end 24 | 25 | describe 'client' do 26 | client = RC::Builder.client do 27 | use RC::QueryResponse, true 28 | run Class.new{ 29 | def call env 30 | yield(env.merge(RC::RESPONSE_BODY => 'a=b&c=d')) 31 | end 32 | } 33 | end 34 | 35 | would 'do nothing' do 36 | expected = 'a=b&c=d' 37 | client.new(:query_response => false).get(''){ |response| 38 | response.should.eq(expected) 39 | }.get('').should.eq(expected) 40 | end 41 | 42 | would 'parse' do 43 | expected = {'a' => 'b', 'c' => 'd'} 44 | client.new.get(''){ |response| 45 | response.should.eq(expected) 46 | }.get('').should.eq(expected) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/test_retry.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Retry do 5 | before do 6 | @called = called = [] 7 | @errors = errors = [] 8 | engine = Class.new do 9 | define_method :call do |env, &block| 10 | called << true 11 | env[RC::FAIL].should.eq [true] 12 | block.call(env.merge(RC::FAIL => [true, errors.shift])) 13 | {} 14 | end 15 | end.new 16 | @app = RC::Retry.new(engine, 5) 17 | end 18 | 19 | after do 20 | @errors.size.should.eq 0 21 | end 22 | 23 | def call env={} 24 | @app.call({RC::FAIL => [true]}.merge(env)){} 25 | end 26 | 27 | def max_retries 28 | @app.max_retries({}) 29 | end 30 | 31 | would 'retry max_retries times' do 32 | @errors.replace([IOError.new] * max_retries) 33 | call 34 | @called.size.should.eq max_retries + 1 35 | end 36 | 37 | would 'retry several times' do 38 | @errors.replace([IOError.new] * 2) 39 | call 40 | @called.size.should.eq 3 41 | end 42 | 43 | would 'not retry RuntimeError by default' do 44 | @errors.replace([RuntimeError.new]) 45 | call 46 | @called.size.should.eq 1 47 | end 48 | 49 | would 'retry RuntimeError when setup' do 50 | @errors.replace([RuntimeError.new] * max_retries) 51 | @app.retry_exceptions = [RuntimeError] 52 | call 53 | @called.size.should.eq max_retries + 1 54 | end 55 | 56 | would 'call error_callback upon retrying' do 57 | @errors.replace([IOError.new] * 2) 58 | errors = [] 59 | call(RC::CLIENT => stub.error_callback{errors.method(:<<)}.object) 60 | @called.size.should.eq 3 61 | errors.size.should.eq 2 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/test_smash.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Smash do 5 | would 'deep access' do 6 | h = {0 => 1, 2 => {3 => 4, 5 => [6, {7 => 8}]}, 9 => false, 10 => nil} 7 | c = RC::Smash.new(h) 8 | c[0] .should.eq(1) 9 | c[1] .should.eq(nil) 10 | c[1, 2] .should.eq(nil) 11 | c[1, 2, 3] .should.eq(nil) 12 | c[2] .should.eq(3 => 4, 5 => [6, {7 => 8}]) 13 | c[2, 3] .should.eq(4) 14 | c[2, 4] .should.eq(nil) 15 | c[2, 4, 5] .should.eq(nil) 16 | c[2, 5] .should.eq([6, {7 => 8}]) 17 | c[2, 5, 1] .should.eq(7 => 8) 18 | c[2, 5, 1, 7] .should.eq(8) 19 | c[2, 5, 1, 8] .should.eq(nil) 20 | c[2, 5, 1, 8, 9].should.eq(nil) 21 | c[9] .should.eq(false) 22 | c[10] .should.eq(nil) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_smash_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::SmashResponse do 5 | describe 'app' do 6 | app = RC::SmashResponse.new(RC::Identity.new, true) 7 | 8 | would 'do nothing' do 9 | env = {RC::RESPONSE_BODY => []} 10 | app.call(env) do |res| 11 | res.should.eq(env) 12 | res[RC::RESPONSE_BODY].should.kind_of?(Array) 13 | end 14 | end 15 | 16 | would 'smash' do 17 | app.call(RC::RESPONSE_BODY => {}) do |res| 18 | body = res[RC::RESPONSE_BODY] 19 | body.should.kind_of?(RC::Smash) 20 | body.should.empty? 21 | body[0].should.eq(nil) 22 | body[0, 0].should.eq(nil) 23 | end 24 | end 25 | 26 | describe 'client' do 27 | body = {0 => {1 => 2}} 28 | client = RC::Builder.client do 29 | use RC::SmashResponse, true 30 | run Class.new{ 31 | define_method(:call) do |env, &block| 32 | block.call(env.merge(RC::RESPONSE_BODY => body)) 33 | end 34 | } 35 | end 36 | 37 | would 'do nothing' do 38 | b = client.new(:smash_response => false).get(''){ |res| 39 | res.should.eq(body) 40 | res.should.kind_of?(Hash) 41 | }.get('') 42 | b.should.eq(body) 43 | b.should.kind_of?(Hash) 44 | end 45 | 46 | would 'clash' do 47 | b = client.new.get(''){ |res| 48 | res.should.eq(body) 49 | res.should.kind_of?(RC::Smash) 50 | }.get('') 51 | b.should.eq(body) 52 | b.should.kind_of?(RC::Smash) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/test_timeout.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Timeout do 5 | app = RC::Timeout.new(RC::Identity.new, 0) 6 | 7 | after do 8 | WebMock.reset! 9 | Muack.verify 10 | end 11 | 12 | would 'bypass timeout if timeout is 0' do 13 | mock(app).process.times(0) 14 | app.call({}){ |e| e.should.eq({}) } 15 | end 16 | 17 | would 'run the process to setup timeout' do 18 | env = {'timeout' => 2} 19 | mock(app).process(env) 20 | app.call(env){|e| e[RC::TIMER].should.kind_of?(PromisePool::Timer)} 21 | end 22 | 23 | would "not raise timeout error if there's already an error" do 24 | env = {'timeout' => 0.01} 25 | mock(app.app).call(having(env)){ raise "error" } 26 | lambda{ app.call(env){} }.should .raise(RuntimeError) 27 | lambda{ sleep 0.01 }.should.not.raise(Timeout::Error) 28 | end 29 | 30 | def fake_timer 31 | Object.new.instance_eval do 32 | @block = nil 33 | def on_timeout; @block = true; Thread.new{yield}; end 34 | def error ; RuntimeError.new('boom') ; end 35 | def cancel ; ; end 36 | def timer ; @block; end 37 | self 38 | end 39 | end 40 | 41 | def sleeping_app 42 | RC::Builder.client do 43 | run Class.new(RC::Engine){ 44 | def request _ 45 | sleep 46 | end 47 | } 48 | end 49 | end 50 | 51 | would 'cancel the task if timing out for thread pool' do 52 | timer = fake_timer 53 | a = sleeping_app 54 | a.pool_size = 1 55 | a.new.request(RC::TIMER => timer, RC::ASYNC => true). 56 | message.should.eq 'boom' 57 | timer.timer.should.not.nil? 58 | end 59 | 60 | would 'still timeout if the task never processed for thread pool' do 61 | a = sleeping_app 62 | a.pool_size = 1 63 | a.new.request(RC::TIMER => fake_timer, RC::ASYNC => true) do |e| 64 | e.message.should.eq 'boom' 65 | a.new.request(RC::TIMER => fake_timer, RC::ASYNC => true).tap{} 66 | end 67 | a.wait 68 | end 69 | 70 | # TODO: ConcurrencyError: interrupted waiting for mutex 71 | # https://travis-ci.org/godfat/rest-core/jobs/105298777 72 | would 'interrupt the task if timing out' do 73 | rd, wr = IO.pipe 74 | timer = Object.new.instance_eval do 75 | @block = nil 76 | define_singleton_method :on_timeout do |&block| 77 | @block = block 78 | Thread.new do 79 | rd.gets 80 | block.call 81 | @block = nil 82 | end 83 | end 84 | def error ; RuntimeError.new('boom'); end 85 | def cancel ; ; end 86 | def timer ; @block ; end 87 | self 88 | end 89 | a = RC::Builder.client do 90 | run Class.new(RC::Engine){ 91 | def request env 92 | env['pipe'].puts 93 | sleep 94 | end 95 | } 96 | end 97 | (-1..1).each do |size| 98 | a.pool_size = size 99 | a.new.request(RC::TIMER => timer, RC::ASYNC => true, 'pipe' => wr). 100 | message.should.eq 'boom' 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/test_universal.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core/test' 3 | 4 | describe RC::Universal do 5 | url = 'http://localhost:1' 6 | 7 | after do 8 | WebMock.reset! 9 | end 10 | 11 | would 'only send payload when there is something' do 12 | m = [:get, :head, :options, :put, :post, :patch, :delete] 13 | c = RC::Universal.new(:log_method => false, :payload => '$payload') 14 | m.each do |method| 15 | stub_request(method, url) 16 | c.send(method, url).tap{} 17 | assert_requested(method, url, :body => '$payload') 18 | end 19 | 20 | WebMock.reset! 21 | 22 | c = RC::Universal.new(:log_method => false) 23 | m.each do |method| 24 | stub_request(method, url).with(:body => nil) 25 | c.send(method, url).tap{} 26 | assert_requested(method, url, :body => nil) 27 | end 28 | ok 29 | end 30 | 31 | would 'send Authorization header' do 32 | u = RC::Universal.new(:log_method => false) 33 | u.username = 'Aladdin' 34 | u.password = 'open sesame' 35 | 36 | u.request_full({}, u.dry)[RC::REQUEST_HEADERS].should.eq( 37 | {'Authorization' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}) 38 | 39 | acc = {'Accept' => 'text/plain'} 40 | env = {RC::REQUEST_HEADERS => acc} 41 | 42 | u.request_full(env, u.dry)[RC::REQUEST_HEADERS].should.eq( 43 | {'Authorization' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}.merge(acc)) 44 | end 45 | 46 | would 'clash' do 47 | stub_request(:get, url).to_return(:body => '{"a":{"b":"c"}}') 48 | res = RC::Universal.new(:json_response => true, 49 | :clash_response => true, 50 | :log_method => false).get(url) 51 | res['a']['d'].should.eq({}) 52 | end 53 | 54 | would 'follow redirect regardless response body' do 55 | called = [] 56 | stub_request(:get, url).to_return(:body => 'bad json!', 57 | :status => 302, :headers => {'Location' => "#{url}/a"}) 58 | stub_request(:get, "#{url}/a").to_return do 59 | Thread.pass 60 | {:body => '{"good":"json!"}'} 61 | end 62 | RC::Universal.new(:json_response => true, :log_method => false). 63 | get(url, &called.method(:<<)).wait 64 | called.should.eq([{'good' => 'json!'}]) 65 | end 66 | 67 | would 'retry and call error_callback' do 68 | errors = [] 69 | called = [] 70 | RC::Universal.new(:error_callback => errors.method(:<<), 71 | :max_retries => 1, :log_method => false). 72 | get(url, &called.method(:<<)).wait 73 | 74 | expect(errors.size).eq 2 75 | errors.all?{ |err| expect(is_a(SystemCallError)).match(err) } 76 | expect(called.size).eq 1 77 | called.all?{ |err| expect(is_a(SystemCallError)).match(err) } 78 | end 79 | 80 | would 'not deadlock with ErrorHandler' do 81 | c = RC::Universal.new(:log_method => false). 82 | event_source('http://localhost:1') 83 | c.onerror{ |e| e.should.kind_of?(SystemCallError) } 84 | c.start.wait 85 | end 86 | end 87 | --------------------------------------------------------------------------------