├── .gitignore ├── .ruby-version ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── circle.yml ├── lib └── wamp │ ├── client.rb │ └── client │ ├── auth.rb │ ├── check.rb │ ├── connection.rb │ ├── event.rb │ ├── logging.rb │ ├── manager │ ├── base.rb │ ├── base_multiple.rb │ ├── establish.rb │ ├── registration.rb │ ├── require.rb │ └── subscription.rb │ ├── message.rb │ ├── request │ ├── base.rb │ ├── call.rb │ ├── publish.rb │ ├── register.rb │ ├── require.rb │ ├── subscribe.rb │ ├── unregister.rb │ └── unsubscribe.rb │ ├── response.rb │ ├── serializer.rb │ ├── session.rb │ ├── transport │ ├── base.rb │ ├── event_machine_base.rb │ ├── faye_web_socket.rb │ └── web_socket_event_machine.rb │ └── version.rb ├── scripts └── gen_message.rb ├── spec ├── spec_helper.rb ├── support │ ├── faye_web_socket_client_stub.rb │ ├── test_transport.rb │ └── web_socket_event_machine_client_stub.rb └── wamp │ └── client │ ├── auth_spec.rb │ ├── check_spec.rb │ ├── connection_spec.rb │ ├── message_spec.rb │ ├── session_spec.rb │ └── transport_spec.rb ├── tasks └── rspec.rake └── wamp_client.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | .idea 16 | *.tmp 17 | .DS_Store 18 | .idea/ 19 | *.gem 20 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in wamp_client.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Eric Chapman 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wamp::Client 2 | 3 | [![Gem Version](https://badge.fury.io/rb/wamp_client.svg)](https://badge.fury.io/rb/wamp_client) 4 | [![Circle CI](https://circleci.com/gh/ericchapman/ruby_wamp_client/tree/master.svg?&style=shield&circle-token=92813c17f9c9510c4c644e41683e7ba2572e0b2a)](https://circleci.com/gh/ericchapman/ruby_wamp_client/tree/master) 5 | [![Codecov](https://img.shields.io/codecov/c/github/ericchapman/ruby_wamp_client/master.svg)](https://codecov.io/github/ericchapman/ruby_wamp_client) 6 | 7 | Client for talking to a WAMP Router. This is defined [here](https://tools.ietf.org/html/draft-oberstet-hybi-tavendo-wamp-02) 8 | 9 | Note: [wamp_rails](https://github.com/ericchapman/ruby_wamp_rails) has been deprecated in favor of 10 | [wamp-worker](https://github.com/ericchapman/ruby_wamp_worker) which allows this library to be run 11 | in a RAILS worker. It will also allow integration with Sidekiq to support allowing handlers to 12 | execute in the background. 13 | 14 | ## Revision History 15 | 16 | - v0.2.2: 17 | - Fixed "time" issue with logging 18 | - v0.2.1: 19 | - Logging cleanup 20 | - v0.2.0: 21 | - Breaking changes to the API including 22 | - Moving CallResult, CallError, CallDefer, and ProgressiveCallDefer to common module called "Response" 23 | - Results to calls return a Hash object instead of a CallResult object 24 | - Catches "StandardError" instead of "Exception" 25 | - Session object has been split into *request* and *manager* objects 26 | - Callback subscriptions are now all "on(event)" 27 | - Lots of code cleanup and combining handling of methods 28 | - v0.1.4: 29 | - Wrapped defer logic inside of yield method for cleanliness 30 | - v0.1.3: 31 | - Improved logging 32 | - Minor code cleanup 33 | - Added including backtrace when unknown error occurs in "call" 34 | - v0.1.2: 35 | - Updated logging to use the standard Ruby logger 36 | - v0.1.1: 37 | - Added 'add_tick_loop' method to the transport to abstract the event machine 38 | - v0.1.0: 39 | - BREAKING CHANGE - Changed all of the namespaces to be "Wamp::Client" 40 | - v0.0.9: 41 | - Added support for transport override and 'faye-websocket' transport 42 | - Added "on(event)" callback (still support legacy methods) 43 | - Increased Test Coverage for 'Transport' and 'Connection' classes 44 | - v0.0.8: 45 | - Exposed 'yield' publicly to allow higher level libraries to not use the 'defer' 46 | - Removed library version dependency 47 | - v0.0.7: 48 | - Added 'session' to the 'details' in the callbacks and handlers 49 | - v0.0.6: 50 | - Added call cancelling 51 | - Added call timeout 52 | - v0.0.5: 53 | - Fixed issue where excluding the 'authmethods' and 'authid' was setting their values to none rather 54 | than excluding them. This was being rejected by some routers 55 | - v0.0.4: 56 | - Added the ability to turn on logging by adding 'verbose' to the options 57 | - v0.0.3: 58 | - Fixed issue 1: Empty args will omit kwargs on some message types 59 | - v0.0.2: 60 | - Added defer call result support 61 | - Added progressive callee support 62 | - v0.0.1: 63 | - Initial Release 64 | 65 | ## Installation 66 | 67 | Add this line to your application's Gemfile: 68 | 69 | ```ruby 70 | gem 'wamp_client' 71 | ``` 72 | 73 | And then execute: 74 | 75 | $ bundle 76 | 77 | Or install it yourself as: 78 | 79 | $ gem install wamp_client 80 | 81 | ## Usage 82 | 83 | ### Connection 84 | The connection object is used to instantiate and maintain a WAMP session as well as the underlying transport. A user 85 | creates a connection and then operates on the session once the session has been established. 86 | 87 | Note that once "connection.open" is called, the library will automatically attempt to reconnect if the connection 88 | closes for any reason. Calling "connection.close" will stop the reconnect logic as well as close the connection if it 89 | is open 90 | 91 | #### Creating a connection 92 | A connection can be created as follows 93 | 94 | ```ruby 95 | require 'wamp/client' 96 | 97 | connection = Wamp::Client::Connection.new(uri: 'ws://127.0.0.1:8080/ws', realm: 'realm1') 98 | 99 | connection.on(:join) do |session, details| 100 | puts "Session Open" 101 | 102 | # Register for something 103 | def add(args, kwargs, details) 104 | args[0] + args[1] 105 | end 106 | session.register('com.example.procedure', method(:add)) do |registration, error, details| 107 | 108 | # Call It 109 | session.call('com.example.procedure', [3,4]) do |result, error, details| 110 | if result 111 | puts result[:args][0] # => 7 112 | end 113 | end 114 | 115 | end 116 | 117 | 118 | end 119 | 120 | connection.open 121 | ``` 122 | 123 | #### Closing a connection 124 | A connection is closed by simply calling "close" 125 | 126 | ```ruby 127 | connection.close 128 | ``` 129 | 130 | Note that the connection will still call "on(:leave)" and "on(:disconnect)" as it closes the session and the transport 131 | 132 | #### Callbacks 133 | A connection has the following callbacks 134 | 135 | **on(:connect)** - Called when the transport is opened 136 | ```ruby 137 | connection.on(:connect) do 138 | 139 | end 140 | ``` 141 | 142 | **on(:join)** - Called when the session is established 143 | ```ruby 144 | connection.on(:join) do |session, details| 145 | 146 | end 147 | ``` 148 | 149 | **on(:leave)** - Called when the session is terminated 150 | ```ruby 151 | connection.on(:leave) do |reason, details| 152 | 153 | end 154 | ``` 155 | 156 | **on(:disconnect)** - Called when the connection is terminated 157 | ```ruby 158 | connection.on(:disconnect) do |reason| 159 | 160 | end 161 | ``` 162 | 163 | **on(:challenge)** - Called when an authentication challenge is created 164 | ```ruby 165 | connection.on(:challenge) do |authmethod, extra| 166 | 167 | end 168 | ``` 169 | 170 | #### Event Tick 171 | You can run a task on every event machine tick by using the transport class 172 | method 'add_tick_loop' 173 | 174 | ```ruby 175 | require 'wamp/client' 176 | 177 | connection = Wamp::Client::Connection.new(uri: 'ws://127.0.0.1:8080/ws', realm: 'realm1') 178 | 179 | connection.transport_class.add_tick_loop do 180 | # Do something periodic 181 | end 182 | 183 | ``` 184 | 185 | #### Overriding Transport 186 | By default, the library will use the "websocket-eventmachine-client" Gem as the websocket transport. 187 | However the library also supports overriding this. 188 | 189 | ##### GEM: faye-websocket 190 | To use this library, do the following 191 | 192 | Install the "faye-websocket" Gem: 193 | 194 | $ gem install faye-websocket 195 | 196 | Override the transport by doing the following: 197 | 198 | ```ruby 199 | require 'wamp/client' 200 | 201 | options = { 202 | uri: 'ws://127.0.0.1:8080/ws', 203 | realm: 'realm1', 204 | proxy: { # See faye-websocket documentation 205 | :origin => 'http://username:password@proxy.example.com', 206 | :headers => {'User-Agent' => 'ruby'} 207 | }, 208 | transport: Wamp::Client::Transport::FayeWebSocket 209 | } 210 | 211 | connection = Wamp::Client::Connection.new(options) 212 | 213 | # More code 214 | ``` 215 | 216 | Note that the "faye-wesbsocket" transport supports passing in a "proxy" as shown above. 217 | 218 | ##### Custom 219 | You can also create your own transports by wrapping them in a "Transport" object 220 | and including as shown above. For more details on this, see the files in 221 | "lib/wamp_client/transport" 222 | 223 | ### Authentication 224 | The library supports authentication. Here is how to perform the different methods 225 | 226 | #### WAMPCRA 227 | To perform WAMP CRA, do the following 228 | 229 | ```ruby 230 | require 'wamp/client' 231 | 232 | options = { 233 | uri: 'ws://127.0.0.1:8080/ws', 234 | realm: 'realm1', 235 | authid: 'joe', 236 | authmethods: ['wampcra'] 237 | } 238 | connection = Wamp::Client::Connection.new(options) 239 | 240 | connection.on(:challenge) do |authmethod, extra| 241 | puts 'Challenge' 242 | if authmethod == 'wampcra' 243 | Wamp::Client::Auth::Cra.sign('secret', extra[:challenge]) 244 | else 245 | raise RuntimeError, "Unsupported auth method #{authmethod}" 246 | end 247 | end 248 | 249 | connection.on(:join) do |session, details| 250 | puts "Session Open" 251 | end 252 | 253 | connection.open 254 | ``` 255 | 256 | ### Handlers and Callbacks 257 | This library makes extensive use of "blocks", "lambdas", "procs", and method pointers for any returned values because 258 | all communication is performed asynchronously. The library defines two types of methods 259 | 260 | - handlers - Can be called **AT ANY TIME**. These can be blocks, lambdas, procs, or method pointers 261 | - callbacks - Only invoked in response to specific call. These are only blocks 262 | 263 | Note that all callbacks can be set to nil, handlers however cannot since the user is explicitly setting them up. 264 | 265 | #### Handlers 266 | All handlers are called with the following parameters 267 | 268 | - args [Array] - Array of arguments 269 | - kwargs [Hash] - Hash of key/value arguments 270 | - details [Hash] - Hash containing some details about the call. Details include 271 | - session [Wamp::Client::Session] - The session 272 | - etc. 273 | 274 | Some examples of this are shown below 275 | 276 | **lambda** 277 | 278 | ```ruby 279 | handler = lambda do |args, kwargs, details| 280 | # TODO: Do Something!! 281 | end 282 | session.subscribe('com.example.topic', handler) 283 | ``` 284 | 285 | **method** 286 | 287 | ```ruby 288 | def handler(args, kwargs, details) 289 | # TODO: Do Something!! 290 | end 291 | session.subscribe('com.example.topic', method(:handler)) 292 | ``` 293 | 294 | #### Callbacks 295 | All callbacks are called with the following parameters 296 | 297 | - result [Object] - Some object with the result information (depends on the call) 298 | - error [Hash] - Hash containing "error", "args", and "kwargs" if an error occurred 299 | - details [Hash] - Hash containing some details about the call. Details include 300 | - type [String] - The type of message 301 | - session [Wamp::Client::Session] - The session 302 | - etc. 303 | 304 | An example of this is shown below 305 | 306 | ```ruby 307 | session.call('com.example.procedure') do |result, error, details| 308 | # TODO: Do something 309 | end 310 | ``` 311 | 312 | ### Topic Subscriptions and Publications 313 | 314 | #### Subscribe 315 | This method subscribes to a topic. The prototype for the method is 316 | 317 | ```ruby 318 | def subscribe(topic, handler, options={}, &callback) 319 | ``` 320 | 321 | where the parameters are defined as 322 | 323 | - topic [String] - The topic to subscribe to 324 | - handler [lambda] - The handler(args, kwargs, details) when an event is received 325 | - options [Hash] - The options for the subscription 326 | - callback [block] - The callback(subscription, error, details) called to signal if the subscription was a success or not 327 | 328 | To subscribe, do the following 329 | 330 | ```ruby 331 | handler = lambda do |args, kwargs, details| 332 | # TODO: Do something 333 | end 334 | 335 | session.subscribe('com.example.topic', handler) 336 | ``` 337 | 338 | If you would like confirmation of the success of the subscription, do the following 339 | 340 | ```ruby 341 | handler = lambda do |args, kwargs, details| 342 | # TODO: Do something 343 | end 344 | 345 | session.subscribe('com.example.topic', handler) do |subscription, error, details| 346 | # TODO: Do something 347 | end 348 | ``` 349 | 350 | Options are 351 | 352 | - match [String] - "exact", "prefix", or "wildcard" 353 | 354 | #### Unsubscribe 355 | This method unsubscribes from a topic. The prototype for the method is as follows 356 | 357 | ```ruby 358 | def unsubscribe(subscription, &callback) 359 | ``` 360 | 361 | where the parameters are defined as 362 | 363 | - subscription [Subscription] - The subscription object from when the subscription was created 364 | - callback [block] - The callback(subscription, error, details) called to signal if the unsubscription was a success or not 365 | 366 | To unsubscribe, do the following 367 | 368 | ```ruby 369 | handler = lambda do |args, kwargs, details| 370 | # TODO: Do something 371 | end 372 | 373 | session.subscribe('com.example.topic', handler) do |subscription, error, details| 374 | @subscription = subscription 375 | end 376 | 377 | # At some later time... 378 | 379 | session.unsubscribe(@subscription) 380 | 381 | # or ... 382 | 383 | @subscription.unsubscribe 384 | 385 | ``` 386 | 387 | #### Publish 388 | This method publishes an event to all of the subscribers. The prototype for the method is 389 | 390 | ```ruby 391 | def publish(topic, args=nil, kwargs=nil, options={}, &callback) 392 | ``` 393 | 394 | where the parameters are defined as 395 | 396 | - topic [String] - The topic to publish the event to 397 | - args [Array] - The arguments 398 | - kwargs [Hash] - The keyword arguments 399 | - options [Hash] - The options for the subscription 400 | - callback [block] - The callback(publish, error, details) is called to signal if the publish was a success or not 401 | 402 | To publish, do the following 403 | 404 | ```ruby 405 | session.publish('com.example.topic', [15], {param: value}) 406 | ``` 407 | 408 | If you would like confirmation, do the following 409 | 410 | ```ruby 411 | session.publish('com.example.topic', [15], {param: value}, {acknowledge: true}, callback) do |publish, error, details| 412 | # TODO: Do something 413 | end 414 | ``` 415 | 416 | Options are 417 | 418 | - acknowledge [Boolean] - set to "true" if you want the Broker to acknowledge if the Publish was successful or not 419 | - disclose_me [Boolean] - "true" if the publisher would like the subscribers to know his identity 420 | - exclude [Array[Integer]] - Array of session IDs to exclude 421 | - exclude_authid [Array[String]] - Array of auth IDs to exclude 422 | - exclude_authrole [Array[String]] - Array of auth roles to exclude 423 | - eligible [Array[Integer]] - Array of session IDs to include 424 | - eligible_authid [Array[String]] - Array of auth IDs to include 425 | - eligible_authrole [Array[String]] - Array of auth roles to include 426 | - exclude_me [Boolean] - set to "false" if you would like yourself to receive an event that you fired 427 | 428 | ### Procedure Registrations and Calls 429 | 430 | #### Register 431 | This method registers to a procedure. The prototype for the method is 432 | 433 | ```ruby 434 | def register(procedure, handler, options={}, &callback) 435 | ``` 436 | 437 | where the parameters are defined as 438 | 439 | - procedure [String] - The procedure to register for 440 | - handler [lambda] - The handler(args, kwargs, details) when a invocation is received 441 | - options [Hash] - The options for the registration 442 | - callback [block] - The callback(registration, error, details) called to signal if the registration was a success or not 443 | 444 | To register, do the following 445 | 446 | ```ruby 447 | handler = lambda do |args, kwargs, details| 448 | # TODO: Do something 449 | end 450 | 451 | session.register('com.example.procedure', handler) 452 | ``` 453 | 454 | If you would like confirmation of the success of the registration, do the following 455 | 456 | ```ruby 457 | handler = lambda do |args, kwargs, details| 458 | # TODO: Do something 459 | end 460 | 461 | session.register('com.example.procedure', handler, {}, callback) do |registration, error, details| 462 | # TODO: Do something 463 | end 464 | ``` 465 | 466 | Options are 467 | 468 | - match [String] - "exact", "prefix", or "wildcard" 469 | - invoke [String] - "single", "roundrobin", "random", "first", "last" 470 | 471 | #### Unregister 472 | This method unregisters from a procedure. The prototype for the method is as follows 473 | 474 | ```ruby 475 | def unregister(registration, &callback) 476 | ``` 477 | 478 | where the parameters are defined as 479 | 480 | - registration [Registration] - The registration object from when the registration was created 481 | - callback [lambda] - The callback(registration, error, details) called to signal if the unregistration was a success 482 | or not 483 | 484 | To unregister, do the following 485 | 486 | ```ruby 487 | handler = lambda do |args, kwargs, details| 488 | # TODO: Do something 489 | end 490 | 491 | session.register('com.example.procedure', handler, {}) do |registration, error, details| 492 | @registration = registration 493 | end 494 | 495 | # At some later time... 496 | 497 | session.unregister(@registration) 498 | 499 | # or ... 500 | 501 | @registration.unregister 502 | 503 | ``` 504 | 505 | #### Call 506 | This method calls a procedure. The prototype for the method is 507 | 508 | ```ruby 509 | def call(procedure, args=nil, kwargs=nil, options={}, &callback) 510 | ``` 511 | 512 | where the parameters are defined as 513 | 514 | - procedure [String] - The procedure to invoke 515 | - args [Array] - The arguments 516 | - kwargs [Hash] - The keyword arguments 517 | - options [Hash] - The options for the call 518 | - callback [block] - The callback(result, error, details) called to signal if the call was a success or not 519 | 520 | To call, do the following 521 | 522 | ```ruby 523 | session.call('com.example.procedure', [15], {param: value}, {}) do |result, error, details| 524 | # TODO: Do something 525 | args = result[:args] 526 | kwargs = result[:kwargs] 527 | end 528 | ``` 529 | 530 | Options are 531 | 532 | - receive_progress [Boolean] - "true" if you support results being able to be sent progressively 533 | - disclose_me [Boolean] - "true" if the caller would like the callee to know the identity 534 | - timeout [Integer] - specifies the number of milliseconds the caller should wait before cancelling the call 535 | 536 | #### Errors 537 | Errors can either be raised OR returned as shown below 538 | 539 | ```ruby 540 | handler = lambda do |args, kwargs, details| 541 | raise RuntimeError,'error' 542 | # OR 543 | raise Wamp::Client::Response::CallError.new('wamp.error', ['some error'], {details: true}) 544 | # OR 545 | Wamp::Client::Response::CallError.new('wamp.error', ['some error'], {details: true}) 546 | end 547 | session.register('com.example.procedure', handler) 548 | ``` 549 | 550 | All 3 of the above examples will return a WAMP Error 551 | 552 | #### Deferred Call 553 | A deferred call refers to a call where the response needs to be asynchronously fetched before it can be returned to the 554 | caller. This is shown below 555 | 556 | ```ruby 557 | def add(args, kwargs, details) 558 | defer = Wamp::Client::Response::CallDefer.new 559 | EM.add_timer(2) { # Something Async 560 | defer.succeed(args[0]+args[1]) 561 | } 562 | defer 563 | end 564 | session.register('com.example.procedure', method(:add)) 565 | ``` 566 | 567 | Errors are returned as follows 568 | 569 | ```ruby 570 | def add(args, kwargs, details) 571 | defer = Wamp::Client::Response::CallDefer.new 572 | EM.add_timer(2) { # Something Async 573 | defer.fail(Wamp::Client::Response::CallError.new('test.error')) 574 | } 575 | defer 576 | end 577 | session.register('com.example.procedure', method(:add)) 578 | ``` 579 | 580 | #### Progressive Calls 581 | Progressive calls are ones that return the result in pieces rather than all at once. They are invoked as follows 582 | 583 | **Caller** 584 | 585 | ```ruby 586 | results = [] 587 | session.call('com.example.procedure', [], {}, {receive_progress: true}) do |result, error, details| 588 | results = results + result[:args] 589 | unless details[:progress] 590 | puts results # => [1,2,3,4,5,6] 591 | end 592 | end 593 | ``` 594 | 595 | **Callee** 596 | 597 | ```ruby 598 | def add(args, kwargs, details) 599 | defer = Wamp::Client::Response::ProgressiveCallDefer.new 600 | EM.add_timer(2) { # Something Async 601 | defer.progress(Wamp::Client::Response::CallResult.new([1,2,3])) 602 | } 603 | EM.add_timer(4) { # Something Async 604 | defer.progress(Wamp::Client::Response::CallResult.new([4,5,6])) 605 | } 606 | EM.add_timer(6) { # Something Async 607 | defer.succeed(Wamp::Client::Response::CallResult.new) 608 | } 609 | defer 610 | end 611 | session.register('com.example.procedure', method(:add)) 612 | ``` 613 | 614 | #### Cancelled Call 615 | A cancelled call will tell a callee who implements a progressive call to cancel it 616 | 617 | **Caller** 618 | 619 | ```ruby 620 | call = session.call('com.example.procedure', [15], {param: value}, {}) do |result, error, details| 621 | # TODO: Do something 622 | args = result[:args] 623 | kwargs = result[:kwargs] 624 | end 625 | 626 | # At some later time... 627 | 628 | session.cancel(call, 'skip') # Options are 'skip', 'kill', or 'killnowait' 629 | 630 | # or ... 631 | 632 | call.cancel('skip') 633 | ``` 634 | 635 | **Callee** 636 | 637 | (There is probably a better way to do this. This is a bad example) 638 | 639 | ```ruby 640 | @interrupts = {} 641 | 642 | def interrupt_handler(request, mode) 643 | @interrups[request] = mode 644 | 645 | # To trigger a custom error, either return something or raise a "CallError" 646 | # else the library will raise a standard error for you 647 | end 648 | 649 | def add(args, kwargs, details) 650 | defer = Wamp::Client::Response::ProgressiveCallDefer.new 651 | EM.add_timer(2) { # Something Async 652 | if @interrupts[defer.request].nil? 653 | defer.progress(Wamp::Client::Response::CallResult.new([1,2,3])) 654 | end 655 | } 656 | EM.add_timer(4) { # Something Async 657 | if @interrupts[defer.request].nil? 658 | defer.progress(Wamp::Client::Response::CallResult.new([4,5,6])) 659 | end 660 | } 661 | EM.add_timer(6) { # Something Async 662 | if @interrupts[defer.request].nil? 663 | defer.succeed(Wamp::Client::Response::CallResult.new) 664 | end 665 | @interrupts.delete(request) 666 | } 667 | defer 668 | end 669 | 670 | session.register('com.example.procedure', method(:add), nil, method(:interrupt_handler)) 671 | ``` 672 | 673 | Notes: 674 | 675 | - Once the response is cancelled, subsequent succeed, progress, or errors are ignored 676 | and not sent to the caller 677 | - Cancels are only processed by calls that had defers. If the defer does not exist then 678 | the cancel is ignored 679 | 680 | ## Contributing 681 | 682 | 1. Fork it ( https://github.com/ericchapman/ruby_wamp_client ) 683 | 2. Create your feature branch (`git checkout -b my-new-feature`) 684 | 3. Commit your changes (`git commit -am 'Add some feature'`) 685 | 4. Push to the branch (`git push origin my-new-feature`) 686 | 5. Create a new Pull Request 687 | 688 | ### Testing 689 | 690 | The unit tests are run as follows 691 | 692 | $ bundle exec rake spec 693 | 694 | ### Scripts 695 | 696 | #### Message 697 | 698 | The *lib/wamp_client/message.rb* file and the *spec/message_spec.rb* file are autogenerated using the script 699 | *scripts/gen_message.rb*. This is done as follows 700 | 701 | $ cd scripts 702 | $ ./gen_message.rb 703 | $ mv message.rb.tmp ../lib/wamp/client/message.rb 704 | $ mv message_spec.rb.tmp ../spec/wamp/client/message_spec.rb 705 | 706 | As I was writing the code for the messages I caught myself cutting and pasting allot and decided these would be 707 | better suited to be autogenerated. 708 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | Dir.glob('tasks/**/*.rake').each(&method(:import)) -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | post: 3 | - bundle exec rake spec -------------------------------------------------------------------------------- /lib/wamp/client.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/version' 2 | require 'wamp/client/message' 3 | require 'wamp/client/serializer' 4 | require 'wamp/client/connection' 5 | require 'wamp/client/session' 6 | require 'wamp/client/auth' 7 | require 'wamp/client/response' 8 | require 'wamp/client/logging' 9 | 10 | module Wamp 11 | module Client 12 | 13 | # Returns the logger object 14 | # 15 | def self.logger 16 | Logging.logger 17 | end 18 | 19 | # Sets the log level 20 | # 21 | # @param log_level [Symbol] - the desired log level 22 | def self.log_level=(log_level) 23 | Logging.log_level = log_level 24 | end 25 | 26 | end 27 | end -------------------------------------------------------------------------------- /lib/wamp/client/auth.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'base64' 3 | 4 | module Wamp 5 | module Client 6 | module Auth 7 | module Cra 8 | 9 | # Generates the signature from the challenge 10 | # @param key [String] 11 | # @param challenge [String] 12 | def self.sign(key, challenge) 13 | hash = OpenSSL::HMAC.digest('sha256', key, challenge) 14 | Base64.encode64(hash).gsub(/\n/,'') 15 | end 16 | 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/wamp/client/check.rb: -------------------------------------------------------------------------------- 1 | module Wamp 2 | module Client 3 | module Check 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | end 8 | 9 | module ClassMethods 10 | 11 | def check_equal(name, expected, value) 12 | raise ArgumentError, "The '#{name}' argument must have the value '#{expected}'. Instead the value was '#{value}'" unless value == expected 13 | end 14 | 15 | def check_gte(name, expected, value) 16 | raise ArgumentError, "The '#{name}' argument must be greater than or equal to '#{expected}'. Instead the value was '#{value}'" unless value >= expected 17 | end 18 | 19 | def check_nil(name, param, nil_allowed) 20 | raise ArgumentError, "The '#{name}' argument cannot be nil" if param.nil? and not nil_allowed 21 | end 22 | 23 | def check_int(name, param, nil_allowed=false) 24 | check_nil(name, param, nil_allowed) 25 | raise ArgumentError, "The '#{name}' argument must be an integer" unless param.nil? or param.is_a? Integer 26 | end 27 | 28 | def check_string(name, param, nil_allowed=false) 29 | check_nil(name, param, nil_allowed) 30 | raise ArgumentError, "The '#{name}' argument must be a string" unless param.nil? or param.is_a? String 31 | end 32 | 33 | def check_bool(name, param, nil_allowed=false) 34 | check_nil(name, param, nil_allowed) 35 | raise ArgumentError, "The '#{name}' argument must be a boolean" unless param.nil? or !!param == param 36 | end 37 | 38 | def check_dict(name, param, nil_allowed=false) 39 | check_nil(name, param, nil_allowed) 40 | raise ArgumentError, "The '#{name}' argument must be a hash" unless param.nil? or param.is_a? Hash 41 | end 42 | 43 | def check_list(name, param, nil_allowed=false) 44 | check_nil(name, param, nil_allowed) 45 | raise ArgumentError, "The '#{name}' argument must be an array" unless param.nil? or param.is_a? Array 46 | end 47 | 48 | def check_uri(name, param, nil_allowed=false) 49 | check_string(name, param, nil_allowed) 50 | end 51 | 52 | def check_id(name, param, nil_allowed=false) 53 | check_int(name, param, nil_allowed) 54 | end 55 | end 56 | 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/wamp/client/connection.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/session' 2 | require 'wamp/client/event' 3 | require 'wamp/client/transport/web_socket_event_machine' 4 | require 'wamp/client/transport/faye_web_socket' 5 | 6 | module Wamp 7 | module Client 8 | class Connection 9 | include Event 10 | 11 | attr_accessor :options, :transport_class, :transport, :session 12 | 13 | create_event [:connect, :join, :challenge, :leave, :disconnect] 14 | 15 | # @param options [Hash] The different options to pass to the connection 16 | # @option options [String] :uri The uri of the WAMP router to connect to 17 | # @option options [String] :proxy The proxy to get to the router 18 | # @option options [String] :realm The realm to connect to 19 | # @option options [String,nil] :protocol The protocol (default if wamp.2.json) 20 | # @option options [String,nil] :authid The id to authenticate with 21 | # @option options [Array, nil] :authmethods The different auth methods that the client supports 22 | # @option options [Hash] :headers Custom headers to include during the connection 23 | # @option options [WampClient::Serializer::Base] :serializer The serializer to use (default is json) 24 | def initialize(options) 25 | self.transport_class = options.delete(:transport) || Wamp::Client::Transport::WebSocketEventMachine 26 | self.options = options || {} 27 | 28 | @reconnect = true 29 | @open = false 30 | 31 | logger.info("#{self.class.name} using version #{Wamp::Client::VERSION}") 32 | end 33 | 34 | # Opens the connection 35 | def open 36 | 37 | raise RuntimeError, 'connection is already open' if self.is_open? 38 | 39 | @reconnect = true 40 | @retry_timer = 1 41 | @retrying = false 42 | 43 | self.transport_class.start_event_machine do 44 | # Create the transport 45 | create_transport 46 | end 47 | 48 | end 49 | 50 | # Closes the connection 51 | def close 52 | 53 | raise RuntimeError, 'connection is already closed' unless self.is_open? 54 | 55 | # Leave the session 56 | @reconnect = false 57 | @retrying = false 58 | session.leave 59 | 60 | end 61 | 62 | # Returns true if the connection is open 63 | # 64 | def is_open? 65 | @open 66 | end 67 | 68 | private 69 | 70 | def create_session 71 | self.session = Wamp::Client::Session.new(self.transport, self.options) 72 | 73 | # Setup session callbacks 74 | self.session.on(:challenge) do |authmethod, extra| 75 | finish_retry 76 | trigger :challenge, authmethod, extra 77 | end 78 | 79 | self.session.on(:join) do |details| 80 | finish_retry 81 | trigger :join, self.session, details 82 | end 83 | 84 | self.session.on(:leave) do |reason, details| 85 | 86 | unless @retrying 87 | trigger :leave, reason, details 88 | end 89 | 90 | if @reconnect 91 | # Retry 92 | retry_connect unless @retrying 93 | else 94 | # Close the transport 95 | self.transport.disconnect 96 | end 97 | end 98 | 99 | self.session.join(self.options[:realm]) 100 | end 101 | 102 | def create_transport 103 | 104 | if self.transport 105 | self.transport.disconnect 106 | self.transport = nil 107 | end 108 | 109 | # Initialize the transport 110 | self.transport = self.transport_class.new(self.options) 111 | 112 | # Setup transport callbacks 113 | self.transport.on(:open) do 114 | 115 | logger.info("#{self.class.name} transport open") 116 | 117 | # Call the callback 118 | trigger :connect 119 | 120 | # Create the session 121 | create_session 122 | 123 | end 124 | 125 | self.transport.on(:close) do |reason| 126 | logger.info("#{self.class.name} transport closed: #{reason}") 127 | @open = false 128 | 129 | unless @retrying 130 | trigger :disconnect, reason 131 | end 132 | 133 | # Nil out the session since the transport closed underneath it 134 | self.session = nil 135 | 136 | if @reconnect 137 | # Retry 138 | retry_connect unless @retrying 139 | else 140 | # Stop the Event Machine 141 | self.transport_class.stop_event_machine 142 | end 143 | end 144 | 145 | self.transport.on(:error) do |message| 146 | logger.error("#{self.class.name} transport error: #{message}") 147 | end 148 | 149 | @open = true 150 | 151 | self.transport.connect 152 | 153 | end 154 | 155 | def finish_retry 156 | @retry_timer = 1 157 | @retrying = false 158 | end 159 | 160 | def retry_connect 161 | 162 | if self.session == nil or not self.session.is_open? 163 | @retry_timer = 2*@retry_timer unless @retry_timer == 32 164 | @retrying = true 165 | 166 | create_transport 167 | 168 | logger.info("#{self.class.name} reconnect in #{@retry_timer} seconds") 169 | self.transport_class.add_timer(@retry_timer*1000) do 170 | retry_connect if @retrying 171 | end 172 | end 173 | 174 | end 175 | 176 | # Returns the logger 177 | # 178 | def logger 179 | Wamp::Client.logger 180 | end 181 | 182 | end 183 | end 184 | end -------------------------------------------------------------------------------- /lib/wamp/client/event.rb: -------------------------------------------------------------------------------- 1 | module Wamp 2 | module Client 3 | 4 | # Module that adds event capabilities to the class. 5 | # 6 | # Usage: 7 | # 8 | # class MyClass 9 | # include Event 10 | # 11 | # create_event [:open, :close] 12 | # 13 | # def do_something 14 | # trigger :open, 4 15 | # end 16 | # 17 | # end 18 | # 19 | # object = MyClass.new 20 | # 21 | # object.on(:open) do |value| 22 | # puts value 23 | # end 24 | # 25 | # object.do_something 26 | # 27 | # Prints: 28 | # 29 | # 4 30 | # 31 | module Event 32 | 33 | def self.included(base) 34 | base.extend(ClassMethods) 35 | end 36 | 37 | module ClassMethods 38 | def create_event(events, attribute: nil, setter: nil, trigger: nil) 39 | attribute ||= :event 40 | setter ||= :on 41 | trigger ||= :trigger 42 | 43 | # Create the attributes 44 | callback_name = "#{attribute}_callbacks" 45 | event_list_name = "#{attribute}_list" 46 | 47 | # Creates the attribute to store the callbacks 48 | attr_accessor callback_name 49 | 50 | # Creates the attributes to store the allowed events 51 | define_method event_list_name do 52 | events 53 | end 54 | 55 | # Creates the setter. Default: "on" 56 | define_method setter do |event, &handler| 57 | unless self.send(event_list_name).include?(event) 58 | raise RuntimeError, "unknown #{setter}(event) '#{event}'" 59 | end 60 | 61 | callback = self.send(callback_name) || {} 62 | callback[event] = handler 63 | self.send("#{callback_name}=", callback) 64 | end 65 | 66 | # Create the trigger. Default: "trigger" 67 | define_method trigger do |event, *args| 68 | handler = (self.send(callback_name) || {})[event] 69 | if handler != nil 70 | handler.call(*args) 71 | end 72 | end 73 | end 74 | end 75 | 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /lib/wamp/client/logging.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'logger' 3 | 4 | module Wamp 5 | module Client 6 | module Logging 7 | 8 | LOG_LEVEL_LOOKUP = { 9 | error: Logger::ERROR, 10 | debug: Logger::DEBUG, 11 | info: Logger::INFO, 12 | warn: Logger::WARN, 13 | } 14 | 15 | class Pretty < Logger::Formatter 16 | def call(severity, time, program_name, message) 17 | "#{time.utc.iso8601(3)} #{::Process.pid} #{severity[0]}: #{message}\n" 18 | end 19 | end 20 | 21 | class WithoutTimestamp < Pretty 22 | def call(severity, time, program_name, message) 23 | "#{::Process.pid} #{severity[0]}: #{message}\n" 24 | end 25 | end 26 | 27 | # Returns the logger object 28 | # 29 | def self.logger 30 | unless defined?(@logger) 31 | $stdout.sync = true unless ENV['RAILS_ENV'] == "production" 32 | @logger = Logger.new $stdout 33 | @logger.level = Logger::INFO 34 | @logger.formatter = ENV['DYNO'] ? WithoutTimestamp.new : Pretty.new 35 | end 36 | @logger 37 | end 38 | 39 | # Sets the log level 40 | # 41 | # @param log_level [Symbol] - the desired log level 42 | def self.log_level=(log_level) 43 | self.logger.level = LOG_LEVEL_LOOKUP[log_level.to_sym] || :info 44 | end 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/wamp/client/manager/base.rb: -------------------------------------------------------------------------------- 1 | module Wamp 2 | module Client 3 | module Manager 4 | 5 | class Base 6 | attr_reader :session, :send_message_callback 7 | 8 | # Constructor 9 | # 10 | # @param session [Wamp::Client::Session] - The session 11 | # @param success [Block] - A block to run when the request was successful 12 | def initialize(session, send_message) 13 | @session = session 14 | @send_message_callback = send_message 15 | end 16 | 17 | private 18 | 19 | # Returns the logger 20 | # 21 | def logger 22 | Wamp::Client.logger 23 | end 24 | 25 | # Sends a message 26 | # 27 | def send_message(message) 28 | self.send_message_callback.call(message) if self.send_message_callback 29 | end 30 | 31 | # Triggers an event 32 | def trigger(event, *args) 33 | self.session.trigger event, *args 34 | end 35 | end 36 | end 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /lib/wamp/client/manager/base_multiple.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | module Wamp 4 | module Client 5 | module Manager 6 | 7 | class BaseMultiple < Base 8 | attr_reader :objects 9 | 10 | # Constructor 11 | # 12 | # @param session [Wamp::Client::Session] - The session 13 | # @param success [Block] - A block to run when the request was successful 14 | def initialize(session, send_message) 15 | super session, send_message 16 | @objects = {} 17 | end 18 | 19 | # Adds an object to the manager 20 | # 21 | # @param id [Int] - The ID of the object 22 | # @param object [Object] - The object to handle 23 | def add(id, object) 24 | self.objects[id] = object 25 | end 26 | 27 | # Removes an object 28 | # 29 | # @param id [Int] - The ID of the object 30 | def remove(id) 31 | self.objects.delete(id) 32 | end 33 | 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/wamp/client/manager/establish.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | module Wamp 4 | module Client 5 | module Manager 6 | 7 | class Establish < Base 8 | attr_accessor :goodbye_sent, :id, :realm 9 | 10 | WAMP_FEATURES = { 11 | caller: { 12 | features: { 13 | caller_identification: true, 14 | call_timeout: true, 15 | call_canceling: true, 16 | progressive_call_results: true 17 | } 18 | }, 19 | callee: { 20 | features: { 21 | caller_identification: true, 22 | ##call_trustlevels: true, 23 | pattern_based_registration: true, 24 | shared_registration: true, 25 | ##call_timeout: true, 26 | call_canceling: true, 27 | progressive_call_results: true, 28 | registration_revocation: true 29 | } 30 | }, 31 | publisher: { 32 | features: { 33 | publisher_identification: true, 34 | subscriber_blackwhite_listing: true, 35 | publisher_exclusion: true 36 | } 37 | }, 38 | subscriber: { 39 | features: { 40 | publisher_identification: true, 41 | ##publication_trustlevels: true, 42 | pattern_based_subscription: true, 43 | subscription_revocation: true 44 | ##event_history: true, 45 | } 46 | } 47 | } 48 | 49 | # Constructor 50 | # 51 | def initialize(session, send_message) 52 | super session, send_message 53 | 54 | self.id = nil 55 | self.realm = nil 56 | self.goodbye_sent = false 57 | end 58 | 59 | # Returns true if the session is open 60 | # 61 | def is_open? 62 | self.id != nil 63 | end 64 | 65 | # Will attempt to join a router 66 | # 67 | def join(realm) 68 | 69 | # Set the realm 70 | self.realm = realm 71 | 72 | # Create the details 73 | details = {} 74 | details[:roles] = WAMP_FEATURES 75 | details[:agent] = "Ruby-Wamp::Client-#{VERSION}" 76 | details[:authid] = self.session.options[:authid] if self.session.options[:authid] 77 | details[:authmethods] = self.session.options[:authmethods] if self.session.options[:authmethods] 78 | 79 | # Create the message 80 | hello = Message::Hello.new(realm, details) 81 | 82 | # Send it 83 | send_message(hello) 84 | end 85 | 86 | # Leave the session 87 | def leave(reason, message) 88 | 89 | # Create the details 90 | details = {} 91 | details[:message] = message 92 | 93 | # Create the goobdbye message 94 | goodbye = Message::Goodbye.new(details, reason) 95 | 96 | # Send it 97 | send_message(goodbye) 98 | 99 | # Send it 100 | self.goodbye_sent = true 101 | end 102 | 103 | # Handles the goodbye message 104 | # 105 | def goodbye(message) 106 | # If we didn't send the goodbye, respond 107 | unless self.goodbye_sent 108 | goodbye = Message::Goodbye.new({}, 'wamp.error.goodbye_and_out') 109 | send_message(goodbye) 110 | end 111 | 112 | # Close out session 113 | self.id = nil 114 | self.realm = nil 115 | self.goodbye_sent = false 116 | 117 | # Trigger leave event 118 | trigger :leave, message.reason, message.details 119 | end 120 | 121 | # Handles the welcome message 122 | # 123 | def welcome(message) 124 | # Get the session ID 125 | self.id = message.session 126 | 127 | # Log the message 128 | logger.info("#{self.session.class.name} joined session with realm '#{message.details[:realm]}'") 129 | 130 | # Trigger join event 131 | trigger :join, message.details 132 | end 133 | 134 | # Handles a challenge message 135 | # 136 | def challenge(message) 137 | # Log challenge received 138 | logger.debug("#{self.session.class.name} auth challenge '#{message.authmethod}', extra: #{message.extra}") 139 | 140 | # Call the callback if set 141 | signature, extra = trigger :challenge, message.authmethod, message.extra 142 | 143 | # Set with initial values 144 | signature ||= '' 145 | extra ||= {} 146 | 147 | # Create the message 148 | authenticate = Message::Authenticate.new(signature, extra) 149 | 150 | # Send it 151 | send_message(authenticate) 152 | end 153 | 154 | # Handles an abort message 155 | # 156 | def abort(message) 157 | # Log leaving the session 158 | logger.info("#{self.session.class.name} left session '#{message.reason}'") 159 | 160 | # Trigger the leave event 161 | trigger :leave, message.reason, message.details 162 | end 163 | 164 | end 165 | 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/wamp/client/manager/registration.rb: -------------------------------------------------------------------------------- 1 | require_relative "base_multiple" 2 | require 'wamp/client/response' 3 | 4 | module Wamp 5 | module Client 6 | module Manager 7 | 8 | class RegistrationObject 9 | attr_accessor :procedure, :handler, :i_handler, :options, :session, :id 10 | 11 | def initialize(procedure, handler, options, i_handler, session, id) 12 | self.procedure = procedure 13 | self.handler = handler 14 | self.options = options 15 | self.i_handler = i_handler 16 | self.session = session 17 | self.id = id 18 | end 19 | 20 | def unregister 21 | self.session.unregister(self) 22 | end 23 | 24 | end 25 | 26 | class Registration < BaseMultiple 27 | attr_reader :defers 28 | 29 | # Constructor 30 | # 31 | # @param session [Wamp::Client::Session] - The session 32 | # @param success [Block] - A block to run when the request was successful 33 | def initialize(session, send_message) 34 | super session, send_message 35 | @defers = {} 36 | end 37 | 38 | # Processes an incoming call 39 | # 40 | # @param message [Message::Event] - The incoming invoke message 41 | def invoke(message) 42 | 43 | # Get the arguments 44 | registration_id = message.registered_registration 45 | request_id = message.request 46 | args = message.call_arguments || [] 47 | kwargs = message.call_argumentskw || {} 48 | 49 | # If we have a registration, execute it 50 | registration = self.objects[registration_id] 51 | if registration 52 | 53 | # Create the details 54 | details = message.details || {} 55 | details[:request] = request_id 56 | details[:procedure] = registration.procedure 57 | details[:session] = self 58 | 59 | handler = registration.handler 60 | if handler 61 | # Use the invoke wrapper to process the result 62 | value = Response.invoke_handler do 63 | handler.call(args, kwargs, details) 64 | end 65 | 66 | # If a defer was returned, handle accordingly 67 | if value.is_a? Response::CallDefer 68 | value.request = request_id 69 | value.registration = registration_id 70 | 71 | # Store the defer 72 | self.defers[request_id] = value 73 | 74 | # On complete, send the result 75 | value.on :complete do |defer, result| 76 | result = Response::CallResult.ensure(result) 77 | self.yield(defer.request, result, {}, true) 78 | end 79 | 80 | # On error, send the error 81 | value.on :error do |defer, error| 82 | error = Response::CallError.ensure(error) 83 | self.yield(defer.request, error, {}, true) 84 | end 85 | 86 | # For progressive, return the progress 87 | if value.is_a? Response::ProgressiveCallDefer 88 | value.on :progress do |defer, result| 89 | result = Response::CallResult.ensure(result) 90 | self.yield(defer.request, result, { progress: true }, true) 91 | end 92 | end 93 | 94 | # Else it was a normal response 95 | else 96 | self.yield(request_id, value) 97 | end 98 | end 99 | end 100 | end 101 | 102 | # Processes a yield request 103 | # 104 | def yield(request_id, result, options={}, check_defer=false) 105 | # Prevent responses for defers that have already completed or had an error 106 | if check_defer and not self.defers[request_id] 107 | return 108 | end 109 | 110 | # Wrap the result accordingly 111 | result = Response::CallResult.ensure(result, allow_error: true) 112 | 113 | # Send either the error or the response 114 | if result.is_a?(Response::CallError) 115 | send_error(request_id, result) 116 | else 117 | yield_msg = Message::Yield.new(request_id, options, result.args, result.kwargs) 118 | send_message(yield_msg) 119 | end 120 | 121 | # Remove the defer if this was not a progress update 122 | if check_defer and not options[:progress] 123 | self.defers.delete(request_id) 124 | end 125 | 126 | end 127 | 128 | # Call Interrupt Handler 129 | # 130 | def interrupt(message) 131 | 132 | # Get parameters 133 | request_id = message.invocation_request 134 | mode = message.options[:mode] 135 | 136 | # Check if we have a pending request 137 | defer = self.defers[request_id] 138 | if defer 139 | registration = self.objects[defer.registration] 140 | if registration 141 | # If it exists, call the interrupt handler to inform it of the interrupt 142 | i_handler = registration.i_handler 143 | error = nil 144 | if i_handler 145 | error = Response.invoke_handler error: true do 146 | i_handler.call(request_id, mode) 147 | end 148 | 149 | # Add a default reason if none was supplied 150 | error.args << "interrupt" if error.args.count == 0 151 | end 152 | 153 | # Send the error back to the client 154 | send_error(request_id, error) 155 | end 156 | 157 | # Delete the defer 158 | self.defers.delete(request_id) 159 | end 160 | 161 | end 162 | 163 | private 164 | 165 | def send_error(request_id, error) 166 | # Make sure the response is an error 167 | error = Response::CallError.ensure(error) 168 | 169 | # Create error message 170 | error_msg = Message::Error.new( 171 | Message::Types::INVOCATION, 172 | request_id, {}, 173 | error.error, error.args, error.kwargs) 174 | 175 | # Send it 176 | send_message(error_msg) 177 | end 178 | 179 | end 180 | 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/wamp/client/manager/require.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/manager/subscription' 2 | require 'wamp/client/manager/registration' 3 | require 'wamp/client/manager/establish' 4 | -------------------------------------------------------------------------------- /lib/wamp/client/manager/subscription.rb: -------------------------------------------------------------------------------- 1 | require_relative "base_multiple" 2 | 3 | module Wamp 4 | module Client 5 | module Manager 6 | 7 | class SubscriptionObject 8 | attr_accessor :topic, :handler, :options, :session, :id 9 | 10 | def initialize(topic, handler, options, session, id) 11 | self.topic = topic 12 | self.handler = handler 13 | self.options = options 14 | self.session = session 15 | self.id = id 16 | end 17 | 18 | def unsubscribe 19 | self.session.unsubscribe(self) 20 | end 21 | 22 | end 23 | 24 | class Subscription < BaseMultiple 25 | 26 | # Processes and incoming event 27 | # 28 | # @param message [Message::Event] - The incoming event message 29 | def event(message) 30 | 31 | # Get the arguments 32 | subscription_id = message.subscribed_subscription 33 | args = message.publish_arguments || [] 34 | kwargs = message.publish_argumentskw || {} 35 | 36 | # If we have a subscription, execute it 37 | subscription = self.objects[subscription_id] 38 | if subscription 39 | 40 | # Create the detials 41 | details = message.details || {} 42 | details[:publication] = message.published_publication 43 | details[:topic] = subscription.topic 44 | details[:session] = self.session 45 | 46 | # Call the handler 47 | handler = subscription.handler 48 | handler.call(args, kwargs, details) if handler 49 | end 50 | end 51 | end 52 | 53 | end 54 | end 55 | end -------------------------------------------------------------------------------- /lib/wamp/client/message.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/check' 2 | 3 | # !!!!THIS FILE IS AUTOGENERATED. DO NOT HAND EDIT!!!! 4 | 5 | module Wamp 6 | module Client 7 | module Message 8 | 9 | module Types 10 | HELLO = 1 11 | WELCOME = 2 12 | ABORT = 3 13 | CHALLENGE = 4 14 | AUTHENTICATE = 5 15 | GOODBYE = 6 16 | ERROR = 8 17 | PUBLISH = 16 18 | PUBLISHED = 17 19 | SUBSCRIBE = 32 20 | SUBSCRIBED = 33 21 | UNSUBSCRIBE = 34 22 | UNSUBSCRIBED = 35 23 | EVENT = 36 24 | CALL = 48 25 | CANCEL = 49 26 | RESULT = 50 27 | REGISTER = 64 28 | REGISTERED = 65 29 | UNREGISTER = 66 30 | UNREGISTERED = 67 31 | INVOCATION = 68 32 | INTERRUPT = 69 33 | YIELD = 70 34 | end 35 | 36 | class Base 37 | include Wamp::Client::Check 38 | 39 | def payload 40 | [] 41 | end 42 | 43 | # @param params [Array] 44 | def self.parse(params) 45 | nil 46 | end 47 | end 48 | 49 | # Hello 50 | # Sent by a Client to initiate opening of a WAMP session to a Router attaching to a Realm. 51 | # Formats: 52 | # [HELLO, Realm|uri, Details|dict] 53 | class Hello < Base 54 | attr_accessor :realm, :details 55 | 56 | def initialize(realm, details) 57 | 58 | self.class.check_uri('realm', realm) 59 | self.class.check_dict('details', details) 60 | 61 | self.realm = realm 62 | self.details = details 63 | 64 | end 65 | 66 | def self.type 67 | Types::HELLO 68 | end 69 | 70 | def self.parse(params) 71 | 72 | self.check_gte('params list', 3, params.count) 73 | self.check_equal('message type', self.type, params[0]) 74 | 75 | params.shift 76 | self.new(*params) 77 | 78 | end 79 | 80 | def payload 81 | 82 | payload = [self.class.type] 83 | payload.push(self.realm) 84 | payload.push(self.details) 85 | 86 | payload 87 | end 88 | 89 | def to_s 90 | 'HELLO > ' + self.payload.to_s 91 | end 92 | 93 | end 94 | 95 | # Welcome 96 | # Sent by a Router to accept a Client. The WAMP session is now open. 97 | # Formats: 98 | # [WELCOME, Session|id, Details|dict] 99 | class Welcome < Base 100 | attr_accessor :session, :details 101 | 102 | def initialize(session, details) 103 | 104 | self.class.check_id('session', session) 105 | self.class.check_dict('details', details) 106 | 107 | self.session = session 108 | self.details = details 109 | 110 | end 111 | 112 | def self.type 113 | Types::WELCOME 114 | end 115 | 116 | def self.parse(params) 117 | 118 | self.check_gte('params list', 3, params.count) 119 | self.check_equal('message type', self.type, params[0]) 120 | 121 | params.shift 122 | self.new(*params) 123 | 124 | end 125 | 126 | def payload 127 | 128 | payload = [self.class.type] 129 | payload.push(self.session) 130 | payload.push(self.details) 131 | 132 | payload 133 | end 134 | 135 | def to_s 136 | 'WELCOME > ' + self.payload.to_s 137 | end 138 | 139 | end 140 | 141 | # Abort 142 | # Sent by a Peer*to abort the opening of a WAMP session. No response is expected. 143 | # Formats: 144 | # [ABORT, Details|dict, Reason|uri] 145 | class Abort < Base 146 | attr_accessor :details, :reason 147 | 148 | def initialize(details, reason) 149 | 150 | self.class.check_dict('details', details) 151 | self.class.check_uri('reason', reason) 152 | 153 | self.details = details 154 | self.reason = reason 155 | 156 | end 157 | 158 | def self.type 159 | Types::ABORT 160 | end 161 | 162 | def self.parse(params) 163 | 164 | self.check_gte('params list', 3, params.count) 165 | self.check_equal('message type', self.type, params[0]) 166 | 167 | params.shift 168 | self.new(*params) 169 | 170 | end 171 | 172 | def payload 173 | 174 | payload = [self.class.type] 175 | payload.push(self.details) 176 | payload.push(self.reason) 177 | 178 | payload 179 | end 180 | 181 | def to_s 182 | 'ABORT > ' + self.payload.to_s 183 | end 184 | 185 | end 186 | 187 | # Goodbye 188 | # Sent by a Peer to close a previously opened WAMP session. Must be echo'ed by the receiving Peer. 189 | # Formats: 190 | # [GOODBYE, Details|dict, Reason|uri] 191 | class Goodbye < Base 192 | attr_accessor :details, :reason 193 | 194 | def initialize(details, reason) 195 | 196 | self.class.check_dict('details', details) 197 | self.class.check_uri('reason', reason) 198 | 199 | self.details = details 200 | self.reason = reason 201 | 202 | end 203 | 204 | def self.type 205 | Types::GOODBYE 206 | end 207 | 208 | def self.parse(params) 209 | 210 | self.check_gte('params list', 3, params.count) 211 | self.check_equal('message type', self.type, params[0]) 212 | 213 | params.shift 214 | self.new(*params) 215 | 216 | end 217 | 218 | def payload 219 | 220 | payload = [self.class.type] 221 | payload.push(self.details) 222 | payload.push(self.reason) 223 | 224 | payload 225 | end 226 | 227 | def to_s 228 | 'GOODBYE > ' + self.payload.to_s 229 | end 230 | 231 | end 232 | 233 | # Error 234 | # Error reply sent by a Peer as an error response to different kinds of requests. 235 | # Formats: 236 | # [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri] 237 | # [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list] 238 | # [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list, ArgumentsKw|dict] 239 | class Error < Base 240 | attr_accessor :request_type, :request_request, :details, :error, :arguments, :argumentskw 241 | 242 | def initialize(request_type, request_request, details, error, arguments=nil, argumentskw=nil) 243 | 244 | self.class.check_int('request_type', request_type) 245 | self.class.check_id('request_request', request_request) 246 | self.class.check_dict('details', details) 247 | self.class.check_uri('error', error) 248 | self.class.check_list('arguments', arguments, true) 249 | self.class.check_dict('argumentskw', argumentskw, true) 250 | 251 | self.request_type = request_type 252 | self.request_request = request_request 253 | self.details = details 254 | self.error = error 255 | self.arguments = arguments 256 | self.argumentskw = argumentskw 257 | 258 | end 259 | 260 | def self.type 261 | Types::ERROR 262 | end 263 | 264 | def self.parse(params) 265 | 266 | self.check_gte('params list', 5, params.count) 267 | self.check_equal('message type', self.type, params[0]) 268 | 269 | params.shift 270 | self.new(*params) 271 | 272 | end 273 | 274 | def payload 275 | self.arguments ||= [] 276 | self.argumentskw ||= {} 277 | 278 | payload = [self.class.type] 279 | payload.push(self.request_type) 280 | payload.push(self.request_request) 281 | payload.push(self.details) 282 | payload.push(self.error) 283 | 284 | return payload if (self.arguments.empty? and self.argumentskw.empty?) 285 | payload.push(self.arguments) 286 | 287 | return payload if (self.argumentskw.empty?) 288 | payload.push(self.argumentskw) 289 | 290 | payload 291 | end 292 | 293 | def to_s 294 | 'ERROR > ' + self.payload.to_s 295 | end 296 | 297 | end 298 | 299 | # Publish 300 | # Sent by a Publisher to a Broker to publish an event. 301 | # Formats: 302 | # [PUBLISH, Request|id, Options|dict, Topic|uri] 303 | # [PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list] 304 | # [PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list, ArgumentsKw|dict] 305 | class Publish < Base 306 | attr_accessor :request, :options, :topic, :arguments, :argumentskw 307 | 308 | def initialize(request, options, topic, arguments=nil, argumentskw=nil) 309 | 310 | self.class.check_id('request', request) 311 | self.class.check_dict('options', options) 312 | self.class.check_uri('topic', topic) 313 | self.class.check_list('arguments', arguments, true) 314 | self.class.check_dict('argumentskw', argumentskw, true) 315 | 316 | self.request = request 317 | self.options = options 318 | self.topic = topic 319 | self.arguments = arguments 320 | self.argumentskw = argumentskw 321 | 322 | end 323 | 324 | def self.type 325 | Types::PUBLISH 326 | end 327 | 328 | def self.parse(params) 329 | 330 | self.check_gte('params list', 4, params.count) 331 | self.check_equal('message type', self.type, params[0]) 332 | 333 | params.shift 334 | self.new(*params) 335 | 336 | end 337 | 338 | def payload 339 | self.arguments ||= [] 340 | self.argumentskw ||= {} 341 | 342 | payload = [self.class.type] 343 | payload.push(self.request) 344 | payload.push(self.options) 345 | payload.push(self.topic) 346 | 347 | return payload if (self.arguments.empty? and self.argumentskw.empty?) 348 | payload.push(self.arguments) 349 | 350 | return payload if (self.argumentskw.empty?) 351 | payload.push(self.argumentskw) 352 | 353 | payload 354 | end 355 | 356 | def to_s 357 | 'PUBLISH > ' + self.payload.to_s 358 | end 359 | 360 | end 361 | 362 | # Published 363 | # Acknowledge sent by a Broker to a Publisher for acknowledged publications. 364 | # Formats: 365 | # [PUBLISHED, PUBLISH.Request|id, Publication|id] 366 | class Published < Base 367 | attr_accessor :publish_request, :publication 368 | 369 | def initialize(publish_request, publication) 370 | 371 | self.class.check_id('publish_request', publish_request) 372 | self.class.check_id('publication', publication) 373 | 374 | self.publish_request = publish_request 375 | self.publication = publication 376 | 377 | end 378 | 379 | def self.type 380 | Types::PUBLISHED 381 | end 382 | 383 | def self.parse(params) 384 | 385 | self.check_gte('params list', 3, params.count) 386 | self.check_equal('message type', self.type, params[0]) 387 | 388 | params.shift 389 | self.new(*params) 390 | 391 | end 392 | 393 | def payload 394 | 395 | payload = [self.class.type] 396 | payload.push(self.publish_request) 397 | payload.push(self.publication) 398 | 399 | payload 400 | end 401 | 402 | def to_s 403 | 'PUBLISHED > ' + self.payload.to_s 404 | end 405 | 406 | end 407 | 408 | # Subscribe 409 | # Subscribe request sent by a Subscriber to a Broker to subscribe to a topic. 410 | # Formats: 411 | # [SUBSCRIBE, Request|id, Options|dict, Topic|uri] 412 | class Subscribe < Base 413 | attr_accessor :request, :options, :topic 414 | 415 | def initialize(request, options, topic) 416 | 417 | self.class.check_id('request', request) 418 | self.class.check_dict('options', options) 419 | self.class.check_uri('topic', topic) 420 | 421 | self.request = request 422 | self.options = options 423 | self.topic = topic 424 | 425 | end 426 | 427 | def self.type 428 | Types::SUBSCRIBE 429 | end 430 | 431 | def self.parse(params) 432 | 433 | self.check_gte('params list', 4, params.count) 434 | self.check_equal('message type', self.type, params[0]) 435 | 436 | params.shift 437 | self.new(*params) 438 | 439 | end 440 | 441 | def payload 442 | 443 | payload = [self.class.type] 444 | payload.push(self.request) 445 | payload.push(self.options) 446 | payload.push(self.topic) 447 | 448 | payload 449 | end 450 | 451 | def to_s 452 | 'SUBSCRIBE > ' + self.payload.to_s 453 | end 454 | 455 | end 456 | 457 | # Subscribed 458 | # Acknowledge sent by a Broker to a Subscriber to acknowledge a subscription. 459 | # Formats: 460 | # [SUBSCRIBED, SUBSCRIBE.Request|id, Subscription|id] 461 | class Subscribed < Base 462 | attr_accessor :subscribe_request, :subscription 463 | 464 | def initialize(subscribe_request, subscription) 465 | 466 | self.class.check_id('subscribe_request', subscribe_request) 467 | self.class.check_id('subscription', subscription) 468 | 469 | self.subscribe_request = subscribe_request 470 | self.subscription = subscription 471 | 472 | end 473 | 474 | def self.type 475 | Types::SUBSCRIBED 476 | end 477 | 478 | def self.parse(params) 479 | 480 | self.check_gte('params list', 3, params.count) 481 | self.check_equal('message type', self.type, params[0]) 482 | 483 | params.shift 484 | self.new(*params) 485 | 486 | end 487 | 488 | def payload 489 | 490 | payload = [self.class.type] 491 | payload.push(self.subscribe_request) 492 | payload.push(self.subscription) 493 | 494 | payload 495 | end 496 | 497 | def to_s 498 | 'SUBSCRIBED > ' + self.payload.to_s 499 | end 500 | 501 | end 502 | 503 | # Unsubscribe 504 | # Unsubscribe request sent by a Subscriber to a Broker to unsubscribe a subscription. 505 | # Formats: 506 | # [UNSUBSCRIBE, Request|id, SUBSCRIBED.Subscription|id] 507 | class Unsubscribe < Base 508 | attr_accessor :request, :subscribed_subscription 509 | 510 | def initialize(request, subscribed_subscription) 511 | 512 | self.class.check_id('request', request) 513 | self.class.check_id('subscribed_subscription', subscribed_subscription) 514 | 515 | self.request = request 516 | self.subscribed_subscription = subscribed_subscription 517 | 518 | end 519 | 520 | def self.type 521 | Types::UNSUBSCRIBE 522 | end 523 | 524 | def self.parse(params) 525 | 526 | self.check_gte('params list', 3, params.count) 527 | self.check_equal('message type', self.type, params[0]) 528 | 529 | params.shift 530 | self.new(*params) 531 | 532 | end 533 | 534 | def payload 535 | 536 | payload = [self.class.type] 537 | payload.push(self.request) 538 | payload.push(self.subscribed_subscription) 539 | 540 | payload 541 | end 542 | 543 | def to_s 544 | 'UNSUBSCRIBE > ' + self.payload.to_s 545 | end 546 | 547 | end 548 | 549 | # Unsubscribed 550 | # Acknowledge sent by a Broker to a Subscriber to acknowledge unsubscription. 551 | # Formats: 552 | # [UNSUBSCRIBED, UNSUBSCRIBE.Request|id] 553 | class Unsubscribed < Base 554 | attr_accessor :unsubscribe_request 555 | 556 | def initialize(unsubscribe_request) 557 | 558 | self.class.check_id('unsubscribe_request', unsubscribe_request) 559 | 560 | self.unsubscribe_request = unsubscribe_request 561 | 562 | end 563 | 564 | def self.type 565 | Types::UNSUBSCRIBED 566 | end 567 | 568 | def self.parse(params) 569 | 570 | self.check_gte('params list', 2, params.count) 571 | self.check_equal('message type', self.type, params[0]) 572 | 573 | params.shift 574 | self.new(*params) 575 | 576 | end 577 | 578 | def payload 579 | 580 | payload = [self.class.type] 581 | payload.push(self.unsubscribe_request) 582 | 583 | payload 584 | end 585 | 586 | def to_s 587 | 'UNSUBSCRIBED > ' + self.payload.to_s 588 | end 589 | 590 | end 591 | 592 | # Event 593 | # Event dispatched by Broker to Subscribers for subscriptions the event was matching. 594 | # Formats: 595 | # [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict] 596 | # [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list] 597 | # [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentsKw|dict] 598 | class Event < Base 599 | attr_accessor :subscribed_subscription, :published_publication, :details, :publish_arguments, :publish_argumentskw 600 | 601 | def initialize(subscribed_subscription, published_publication, details, publish_arguments=nil, publish_argumentskw=nil) 602 | 603 | self.class.check_id('subscribed_subscription', subscribed_subscription) 604 | self.class.check_id('published_publication', published_publication) 605 | self.class.check_dict('details', details) 606 | self.class.check_list('publish_arguments', publish_arguments, true) 607 | self.class.check_dict('publish_argumentskw', publish_argumentskw, true) 608 | 609 | self.subscribed_subscription = subscribed_subscription 610 | self.published_publication = published_publication 611 | self.details = details 612 | self.publish_arguments = publish_arguments 613 | self.publish_argumentskw = publish_argumentskw 614 | 615 | end 616 | 617 | def self.type 618 | Types::EVENT 619 | end 620 | 621 | def self.parse(params) 622 | 623 | self.check_gte('params list', 4, params.count) 624 | self.check_equal('message type', self.type, params[0]) 625 | 626 | params.shift 627 | self.new(*params) 628 | 629 | end 630 | 631 | def payload 632 | self.publish_arguments ||= [] 633 | self.publish_argumentskw ||= {} 634 | 635 | payload = [self.class.type] 636 | payload.push(self.subscribed_subscription) 637 | payload.push(self.published_publication) 638 | payload.push(self.details) 639 | 640 | return payload if (self.publish_arguments.empty? and self.publish_argumentskw.empty?) 641 | payload.push(self.publish_arguments) 642 | 643 | return payload if (self.publish_argumentskw.empty?) 644 | payload.push(self.publish_argumentskw) 645 | 646 | payload 647 | end 648 | 649 | def to_s 650 | 'EVENT > ' + self.payload.to_s 651 | end 652 | 653 | end 654 | 655 | # Call 656 | # Call as originally issued by the _Caller_ to the _Dealer_. 657 | # Formats: 658 | # [CALL, Request|id, Options|dict, Procedure|uri] 659 | # [CALL, Request|id, Options|dict, Procedure|uri, Arguments|list] 660 | # [CALL, Request|id, Options|dict, Procedure|uri, Arguments|list, ArgumentsKw|dict] 661 | class Call < Base 662 | attr_accessor :request, :options, :procedure, :arguments, :argumentskw 663 | 664 | def initialize(request, options, procedure, arguments=nil, argumentskw=nil) 665 | 666 | self.class.check_id('request', request) 667 | self.class.check_dict('options', options) 668 | self.class.check_uri('procedure', procedure) 669 | self.class.check_list('arguments', arguments, true) 670 | self.class.check_dict('argumentskw', argumentskw, true) 671 | 672 | self.request = request 673 | self.options = options 674 | self.procedure = procedure 675 | self.arguments = arguments 676 | self.argumentskw = argumentskw 677 | 678 | end 679 | 680 | def self.type 681 | Types::CALL 682 | end 683 | 684 | def self.parse(params) 685 | 686 | self.check_gte('params list', 4, params.count) 687 | self.check_equal('message type', self.type, params[0]) 688 | 689 | params.shift 690 | self.new(*params) 691 | 692 | end 693 | 694 | def payload 695 | self.arguments ||= [] 696 | self.argumentskw ||= {} 697 | 698 | payload = [self.class.type] 699 | payload.push(self.request) 700 | payload.push(self.options) 701 | payload.push(self.procedure) 702 | 703 | return payload if (self.arguments.empty? and self.argumentskw.empty?) 704 | payload.push(self.arguments) 705 | 706 | return payload if (self.argumentskw.empty?) 707 | payload.push(self.argumentskw) 708 | 709 | payload 710 | end 711 | 712 | def to_s 713 | 'CALL > ' + self.payload.to_s 714 | end 715 | 716 | end 717 | 718 | # Result 719 | # Result of a call as returned by _Dealer_ to _Caller_. 720 | # Formats: 721 | # [RESULT, CALL.Request|id, Details|dict] 722 | # [RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list] 723 | # [RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list, YIELD.ArgumentsKw|dict] 724 | class Result < Base 725 | attr_accessor :call_request, :details, :yield_arguments, :yield_argumentskw 726 | 727 | def initialize(call_request, details, yield_arguments=nil, yield_argumentskw=nil) 728 | 729 | self.class.check_id('call_request', call_request) 730 | self.class.check_dict('details', details) 731 | self.class.check_list('yield_arguments', yield_arguments, true) 732 | self.class.check_dict('yield_argumentskw', yield_argumentskw, true) 733 | 734 | self.call_request = call_request 735 | self.details = details 736 | self.yield_arguments = yield_arguments 737 | self.yield_argumentskw = yield_argumentskw 738 | 739 | end 740 | 741 | def self.type 742 | Types::RESULT 743 | end 744 | 745 | def self.parse(params) 746 | 747 | self.check_gte('params list', 3, params.count) 748 | self.check_equal('message type', self.type, params[0]) 749 | 750 | params.shift 751 | self.new(*params) 752 | 753 | end 754 | 755 | def payload 756 | self.yield_arguments ||= [] 757 | self.yield_argumentskw ||= {} 758 | 759 | payload = [self.class.type] 760 | payload.push(self.call_request) 761 | payload.push(self.details) 762 | 763 | return payload if (self.yield_arguments.empty? and self.yield_argumentskw.empty?) 764 | payload.push(self.yield_arguments) 765 | 766 | return payload if (self.yield_argumentskw.empty?) 767 | payload.push(self.yield_argumentskw) 768 | 769 | payload 770 | end 771 | 772 | def to_s 773 | 'RESULT > ' + self.payload.to_s 774 | end 775 | 776 | end 777 | 778 | # Register 779 | # A _Callees_ request to register an endpoint at a _Dealer_. 780 | # Formats: 781 | # [REGISTER, Request|id, Options|dict, Procedure|uri] 782 | class Register < Base 783 | attr_accessor :request, :options, :procedure 784 | 785 | def initialize(request, options, procedure) 786 | 787 | self.class.check_id('request', request) 788 | self.class.check_dict('options', options) 789 | self.class.check_uri('procedure', procedure) 790 | 791 | self.request = request 792 | self.options = options 793 | self.procedure = procedure 794 | 795 | end 796 | 797 | def self.type 798 | Types::REGISTER 799 | end 800 | 801 | def self.parse(params) 802 | 803 | self.check_gte('params list', 4, params.count) 804 | self.check_equal('message type', self.type, params[0]) 805 | 806 | params.shift 807 | self.new(*params) 808 | 809 | end 810 | 811 | def payload 812 | 813 | payload = [self.class.type] 814 | payload.push(self.request) 815 | payload.push(self.options) 816 | payload.push(self.procedure) 817 | 818 | payload 819 | end 820 | 821 | def to_s 822 | 'REGISTER > ' + self.payload.to_s 823 | end 824 | 825 | end 826 | 827 | # Registered 828 | # Acknowledge sent by a _Dealer_ to a _Callee_ for successful registration. 829 | # Formats: 830 | # [REGISTERED, REGISTER.Request|id, Registration|id] 831 | class Registered < Base 832 | attr_accessor :register_request, :registration 833 | 834 | def initialize(register_request, registration) 835 | 836 | self.class.check_id('register_request', register_request) 837 | self.class.check_id('registration', registration) 838 | 839 | self.register_request = register_request 840 | self.registration = registration 841 | 842 | end 843 | 844 | def self.type 845 | Types::REGISTERED 846 | end 847 | 848 | def self.parse(params) 849 | 850 | self.check_gte('params list', 3, params.count) 851 | self.check_equal('message type', self.type, params[0]) 852 | 853 | params.shift 854 | self.new(*params) 855 | 856 | end 857 | 858 | def payload 859 | 860 | payload = [self.class.type] 861 | payload.push(self.register_request) 862 | payload.push(self.registration) 863 | 864 | payload 865 | end 866 | 867 | def to_s 868 | 'REGISTERED > ' + self.payload.to_s 869 | end 870 | 871 | end 872 | 873 | # Unregister 874 | # A _Callees_ request to unregister a previously established registration. 875 | # Formats: 876 | # [UNREGISTER, Request|id, REGISTERED.Registration|id] 877 | class Unregister < Base 878 | attr_accessor :request, :registered_registration 879 | 880 | def initialize(request, registered_registration) 881 | 882 | self.class.check_id('request', request) 883 | self.class.check_id('registered_registration', registered_registration) 884 | 885 | self.request = request 886 | self.registered_registration = registered_registration 887 | 888 | end 889 | 890 | def self.type 891 | Types::UNREGISTER 892 | end 893 | 894 | def self.parse(params) 895 | 896 | self.check_gte('params list', 3, params.count) 897 | self.check_equal('message type', self.type, params[0]) 898 | 899 | params.shift 900 | self.new(*params) 901 | 902 | end 903 | 904 | def payload 905 | 906 | payload = [self.class.type] 907 | payload.push(self.request) 908 | payload.push(self.registered_registration) 909 | 910 | payload 911 | end 912 | 913 | def to_s 914 | 'UNREGISTER > ' + self.payload.to_s 915 | end 916 | 917 | end 918 | 919 | # Unregistered 920 | # Acknowledge sent by a _Dealer_ to a _Callee_ for successful unregistration. 921 | # Formats: 922 | # [UNREGISTERED, UNREGISTER.Request|id] 923 | class Unregistered < Base 924 | attr_accessor :unregister_request 925 | 926 | def initialize(unregister_request) 927 | 928 | self.class.check_id('unregister_request', unregister_request) 929 | 930 | self.unregister_request = unregister_request 931 | 932 | end 933 | 934 | def self.type 935 | Types::UNREGISTERED 936 | end 937 | 938 | def self.parse(params) 939 | 940 | self.check_gte('params list', 2, params.count) 941 | self.check_equal('message type', self.type, params[0]) 942 | 943 | params.shift 944 | self.new(*params) 945 | 946 | end 947 | 948 | def payload 949 | 950 | payload = [self.class.type] 951 | payload.push(self.unregister_request) 952 | 953 | payload 954 | end 955 | 956 | def to_s 957 | 'UNREGISTERED > ' + self.payload.to_s 958 | end 959 | 960 | end 961 | 962 | # Invocation 963 | # Actual invocation of an endpoint sent by _Dealer_ to a _Callee_. 964 | # Formats: 965 | # [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict] 966 | # [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list] 967 | # [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list, CALL.ArgumentsKw|dict] 968 | class Invocation < Base 969 | attr_accessor :request, :registered_registration, :details, :call_arguments, :call_argumentskw 970 | 971 | def initialize(request, registered_registration, details, call_arguments=nil, call_argumentskw=nil) 972 | 973 | self.class.check_id('request', request) 974 | self.class.check_id('registered_registration', registered_registration) 975 | self.class.check_dict('details', details) 976 | self.class.check_list('call_arguments', call_arguments, true) 977 | self.class.check_dict('call_argumentskw', call_argumentskw, true) 978 | 979 | self.request = request 980 | self.registered_registration = registered_registration 981 | self.details = details 982 | self.call_arguments = call_arguments 983 | self.call_argumentskw = call_argumentskw 984 | 985 | end 986 | 987 | def self.type 988 | Types::INVOCATION 989 | end 990 | 991 | def self.parse(params) 992 | 993 | self.check_gte('params list', 4, params.count) 994 | self.check_equal('message type', self.type, params[0]) 995 | 996 | params.shift 997 | self.new(*params) 998 | 999 | end 1000 | 1001 | def payload 1002 | self.call_arguments ||= [] 1003 | self.call_argumentskw ||= {} 1004 | 1005 | payload = [self.class.type] 1006 | payload.push(self.request) 1007 | payload.push(self.registered_registration) 1008 | payload.push(self.details) 1009 | 1010 | return payload if (self.call_arguments.empty? and self.call_argumentskw.empty?) 1011 | payload.push(self.call_arguments) 1012 | 1013 | return payload if (self.call_argumentskw.empty?) 1014 | payload.push(self.call_argumentskw) 1015 | 1016 | payload 1017 | end 1018 | 1019 | def to_s 1020 | 'INVOCATION > ' + self.payload.to_s 1021 | end 1022 | 1023 | end 1024 | 1025 | # Yield 1026 | # Actual yield from an endpoint sent by a _Callee_ to _Dealer_. 1027 | # Formats: 1028 | # [YIELD, INVOCATION.Request|id, Options|dict] 1029 | # [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list] 1030 | # [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict] 1031 | class Yield < Base 1032 | attr_accessor :invocation_request, :options, :arguments, :argumentskw 1033 | 1034 | def initialize(invocation_request, options, arguments=nil, argumentskw=nil) 1035 | 1036 | self.class.check_id('invocation_request', invocation_request) 1037 | self.class.check_dict('options', options) 1038 | self.class.check_list('arguments', arguments, true) 1039 | self.class.check_dict('argumentskw', argumentskw, true) 1040 | 1041 | self.invocation_request = invocation_request 1042 | self.options = options 1043 | self.arguments = arguments 1044 | self.argumentskw = argumentskw 1045 | 1046 | end 1047 | 1048 | def self.type 1049 | Types::YIELD 1050 | end 1051 | 1052 | def self.parse(params) 1053 | 1054 | self.check_gte('params list', 3, params.count) 1055 | self.check_equal('message type', self.type, params[0]) 1056 | 1057 | params.shift 1058 | self.new(*params) 1059 | 1060 | end 1061 | 1062 | def payload 1063 | self.arguments ||= [] 1064 | self.argumentskw ||= {} 1065 | 1066 | payload = [self.class.type] 1067 | payload.push(self.invocation_request) 1068 | payload.push(self.options) 1069 | 1070 | return payload if (self.arguments.empty? and self.argumentskw.empty?) 1071 | payload.push(self.arguments) 1072 | 1073 | return payload if (self.argumentskw.empty?) 1074 | payload.push(self.argumentskw) 1075 | 1076 | payload 1077 | end 1078 | 1079 | def to_s 1080 | 'YIELD > ' + self.payload.to_s 1081 | end 1082 | 1083 | end 1084 | 1085 | # Challenge 1086 | # The "CHALLENGE" message is used with certain Authentication Methods. During authenticated session establishment, a *Router* sends a challenge message. 1087 | # Formats: 1088 | # [CHALLENGE, AuthMethod|string, Extra|dict] 1089 | class Challenge < Base 1090 | attr_accessor :authmethod, :extra 1091 | 1092 | def initialize(authmethod, extra) 1093 | 1094 | self.class.check_string('authmethod', authmethod) 1095 | self.class.check_dict('extra', extra) 1096 | 1097 | self.authmethod = authmethod 1098 | self.extra = extra 1099 | 1100 | end 1101 | 1102 | def self.type 1103 | Types::CHALLENGE 1104 | end 1105 | 1106 | def self.parse(params) 1107 | 1108 | self.check_gte('params list', 3, params.count) 1109 | self.check_equal('message type', self.type, params[0]) 1110 | 1111 | params.shift 1112 | self.new(*params) 1113 | 1114 | end 1115 | 1116 | def payload 1117 | 1118 | payload = [self.class.type] 1119 | payload.push(self.authmethod) 1120 | payload.push(self.extra) 1121 | 1122 | payload 1123 | end 1124 | 1125 | def to_s 1126 | 'CHALLENGE > ' + self.payload.to_s 1127 | end 1128 | 1129 | end 1130 | 1131 | # Authenticate 1132 | # The "AUTHENTICATE" message is used with certain Authentication Methods. A *Client* having received a challenge is expected to respond by sending a signature or token. 1133 | # Formats: 1134 | # [AUTHENTICATE, Signature|string, Extra|dict] 1135 | class Authenticate < Base 1136 | attr_accessor :signature, :extra 1137 | 1138 | def initialize(signature, extra) 1139 | 1140 | self.class.check_string('signature', signature) 1141 | self.class.check_dict('extra', extra) 1142 | 1143 | self.signature = signature 1144 | self.extra = extra 1145 | 1146 | end 1147 | 1148 | def self.type 1149 | Types::AUTHENTICATE 1150 | end 1151 | 1152 | def self.parse(params) 1153 | 1154 | self.check_gte('params list', 3, params.count) 1155 | self.check_equal('message type', self.type, params[0]) 1156 | 1157 | params.shift 1158 | self.new(*params) 1159 | 1160 | end 1161 | 1162 | def payload 1163 | 1164 | payload = [self.class.type] 1165 | payload.push(self.signature) 1166 | payload.push(self.extra) 1167 | 1168 | payload 1169 | end 1170 | 1171 | def to_s 1172 | 'AUTHENTICATE > ' + self.payload.to_s 1173 | end 1174 | 1175 | end 1176 | 1177 | # Cancel 1178 | # The "CANCEL" message is used with the Call Canceling advanced feature. A _Caller_ can cancel and issued call actively by sending a cancel message to the _Dealer_. 1179 | # Formats: 1180 | # [CANCEL, CALL.Request|id, Options|dict] 1181 | class Cancel < Base 1182 | attr_accessor :call_request, :options 1183 | 1184 | def initialize(call_request, options) 1185 | 1186 | self.class.check_id('call_request', call_request) 1187 | self.class.check_dict('options', options) 1188 | 1189 | self.call_request = call_request 1190 | self.options = options 1191 | 1192 | end 1193 | 1194 | def self.type 1195 | Types::CANCEL 1196 | end 1197 | 1198 | def self.parse(params) 1199 | 1200 | self.check_gte('params list', 3, params.count) 1201 | self.check_equal('message type', self.type, params[0]) 1202 | 1203 | params.shift 1204 | self.new(*params) 1205 | 1206 | end 1207 | 1208 | def payload 1209 | 1210 | payload = [self.class.type] 1211 | payload.push(self.call_request) 1212 | payload.push(self.options) 1213 | 1214 | payload 1215 | end 1216 | 1217 | def to_s 1218 | 'CANCEL > ' + self.payload.to_s 1219 | end 1220 | 1221 | end 1222 | 1223 | # Interrupt 1224 | # The "INTERRUPT" message is used with the Call Canceling advanced feature. Upon receiving a cancel for a pending call, a _Dealer_ will issue an interrupt to the _Callee_. 1225 | # Formats: 1226 | # [INTERRUPT, INVOCATION.Request|id, Options|dict] 1227 | class Interrupt < Base 1228 | attr_accessor :invocation_request, :options 1229 | 1230 | def initialize(invocation_request, options) 1231 | 1232 | self.class.check_id('invocation_request', invocation_request) 1233 | self.class.check_dict('options', options) 1234 | 1235 | self.invocation_request = invocation_request 1236 | self.options = options 1237 | 1238 | end 1239 | 1240 | def self.type 1241 | Types::INTERRUPT 1242 | end 1243 | 1244 | def self.parse(params) 1245 | 1246 | self.check_gte('params list', 3, params.count) 1247 | self.check_equal('message type', self.type, params[0]) 1248 | 1249 | params.shift 1250 | self.new(*params) 1251 | 1252 | end 1253 | 1254 | def payload 1255 | 1256 | payload = [self.class.type] 1257 | payload.push(self.invocation_request) 1258 | payload.push(self.options) 1259 | 1260 | payload 1261 | end 1262 | 1263 | def to_s 1264 | 'INTERRUPT > ' + self.payload.to_s 1265 | end 1266 | 1267 | end 1268 | 1269 | TYPE_LOOKUP = { 1270 | Types::HELLO => Hello, 1271 | Types::WELCOME => Welcome, 1272 | Types::ABORT => Abort, 1273 | Types::CHALLENGE => Challenge, 1274 | Types::AUTHENTICATE => Authenticate, 1275 | Types::GOODBYE => Goodbye, 1276 | Types::ERROR => Error, 1277 | Types::PUBLISH => Publish, 1278 | Types::PUBLISHED => Published, 1279 | Types::SUBSCRIBE => Subscribe, 1280 | Types::SUBSCRIBED => Subscribed, 1281 | Types::UNSUBSCRIBE => Unsubscribe, 1282 | Types::UNSUBSCRIBED => Unsubscribed, 1283 | Types::EVENT => Event, 1284 | Types::CALL => Call, 1285 | Types::CANCEL => Cancel, 1286 | Types::RESULT => Result, 1287 | Types::REGISTER => Register, 1288 | Types::REGISTERED => Registered, 1289 | Types::UNREGISTER => Unregister, 1290 | Types::UNREGISTERED => Unregistered, 1291 | Types::INVOCATION => Invocation, 1292 | Types::INTERRUPT => Interrupt, 1293 | Types::YIELD => Yield, 1294 | } 1295 | 1296 | # @param params [Array] 1297 | def self.parse(params) 1298 | klass = TYPE_LOOKUP[params[0]] 1299 | klass ? klass.parse(params.clone) : nil 1300 | end 1301 | 1302 | end 1303 | end 1304 | end 1305 | -------------------------------------------------------------------------------- /lib/wamp/client/request/base.rb: -------------------------------------------------------------------------------- 1 | module Wamp 2 | module Client 3 | module Request 4 | 5 | class Message::Error 6 | def request_id 7 | self.request_request 8 | end 9 | end 10 | 11 | # The request base class is used to abstract all of the requests that 12 | # will go to the broker/dealer. The model supports a request followed 13 | # by a response that is either a "success" or an error 14 | class Base 15 | attr_reader :requests, :session, :send_message_callback, :on_success 16 | 17 | # Constructor 18 | # 19 | # @param session [Wamp::Client::Session] - The session 20 | # @param send_message [lambda] - A lambda to send the message 21 | # @param success [Block] - A block to run when the request was successful 22 | def initialize(session, send_message, &on_success) 23 | @requests = {} 24 | @session = session 25 | @send_message_callback = send_message 26 | @on_success = on_success 27 | end 28 | 29 | # Generates a new ID for the request according to the specification 30 | # (Section 5.1.2) 31 | # 32 | # @param [Int] - A new ID 33 | def generate_id 34 | rand(0..9007199254740992) 35 | end 36 | 37 | # Makes the request to the broker/dealer 38 | # 39 | # @return [Int] - request_id 40 | def request(*args, &callback) 41 | 42 | # Generate an ID 43 | request_id = self.generate_id 44 | 45 | # Get the unique lookup/message for the request 46 | lookup, message = self.create_request(request_id, *args, &callback) 47 | 48 | # Store in the pending requests 49 | self.requests[request_id] = lookup if lookup 50 | 51 | # Send the message 52 | send_message(message) 53 | 54 | request_id 55 | end 56 | 57 | # Called when the response was a success 58 | # 59 | def success(message) 60 | # Get the request_id 61 | request_id = message.request_id 62 | 63 | # Get the lookup 64 | lookup = self.requests[request_id] 65 | 66 | # Parse the result 67 | callback, result, details, should_keep = self.process_success(message, lookup) 68 | 69 | if callback and details 70 | # Add items to details 71 | details[:session] = self.session 72 | 73 | # Call the callback 74 | callback.call(result, nil, details) if callback 75 | end 76 | 77 | # Delete if "should_keep" if false 78 | self.requests.delete(request_id) unless should_keep 79 | end 80 | 81 | def error(message) 82 | # Get the request_id 83 | request_id = message.request_id 84 | 85 | # Get the lookup 86 | lookup = self.requests.delete(request_id) 87 | 88 | # Parse the result 89 | callback, details = self.process_error(message, lookup) 90 | 91 | if callback and details 92 | # Add items to details 93 | details[:session] = self.session 94 | 95 | # Create the error 96 | error = Response::CallError.from_message(message) 97 | 98 | # Call the callback 99 | callback.call(nil, error.to_hash, details) if callback 100 | end 101 | end 102 | 103 | #region Override Methods 104 | def create_request(*args) 105 | end 106 | 107 | def process_success(message, lookup) 108 | end 109 | 110 | def process_error(message, lookup) 111 | end 112 | #endregion 113 | 114 | private 115 | 116 | # Sends a message 117 | # 118 | def send_message(message) 119 | self.send_message_callback.call(message) if self.send_message_callback 120 | end 121 | 122 | end 123 | end 124 | end 125 | end -------------------------------------------------------------------------------- /lib/wamp/client/request/call.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | 4 | module Wamp 5 | module Client 6 | module Request 7 | 8 | class Message::Result 9 | def request_id 10 | self.call_request 11 | end 12 | end 13 | 14 | class CallObject 15 | attr_accessor :session, :id 16 | 17 | def initialize(session, id) 18 | self.session = session 19 | self.id = id 20 | end 21 | 22 | def cancel(mode='skip') 23 | self.session.cancel(self, mode) 24 | end 25 | 26 | end 27 | 28 | class Call < Base 29 | 30 | # Method specific to this request that will cancel it 31 | # 32 | def cancel(request_id, mode='skip') 33 | 34 | # If the request is still in flight 35 | if self.requests[request_id] 36 | # Create the message 37 | message = Message::Cancel.new(request_id, { mode: mode }) 38 | 39 | # Send it 40 | send_message(message) 41 | end 42 | 43 | end 44 | 45 | def create_request(request_id, procedure, args=nil, kwargs=nil, options={}, &callback) 46 | 47 | # Create the lookup 48 | lookup = {p: procedure, a: args, k: kwargs, o: options, c: callback} 49 | 50 | # Create the message 51 | message = Message::Call.new(request_id, options, procedure, args, kwargs) 52 | 53 | # Return 54 | [lookup, message] 55 | end 56 | 57 | def process_success(message, lookup) 58 | if lookup 59 | # Get the params 60 | procedure = lookup[:p] 61 | options = lookup[:o] || {} 62 | callback = lookup[:c] 63 | 64 | # Create the details 65 | details = message.details || {} 66 | details[:procedure] = procedure unless details[:procedure] 67 | details[:type] = 'call' 68 | 69 | # Set the should keep flag if this is a progress message 70 | should_keep = details[:progress] 71 | 72 | # Only return the information if not progress or receive progress is true 73 | if not details[:progress] or (details[:progress] and options[:receive_progress]) 74 | 75 | # Create the response 76 | result = Response::CallResult.from_yield_message(message) 77 | 78 | # Return the values 79 | [callback, result.to_hash, details, should_keep] 80 | 81 | else 82 | [nil, nil, nil, should_keep] 83 | end 84 | else 85 | [nil, nil, nil] 86 | end 87 | end 88 | 89 | def process_error(message, lookup) 90 | if lookup 91 | # Get the params 92 | procedure = lookup[:p] 93 | callback = lookup[:c] 94 | 95 | # Create the details 96 | details = message.details || {} 97 | details[:procedure] = procedure unless details[:procedure] 98 | details[:type] = 'call' 99 | 100 | # Return the values 101 | [callback, details] 102 | else 103 | [nil, nil] 104 | end 105 | end 106 | 107 | end 108 | 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/wamp/client/request/publish.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | 4 | module Wamp 5 | module Client 6 | module Request 7 | 8 | class Message::Published 9 | def request_id 10 | self.publish_request 11 | end 12 | end 13 | 14 | class Publish < Base 15 | 16 | def create_request(request_id, topic, args=nil, kwargs=nil, options={}, &callback) 17 | 18 | # Create the lookup 19 | lookup = options[:acknowledge] ? {t: topic, a: args, k: kwargs, o: options, c: callback} : nil 20 | 21 | # Create the message 22 | message = Message::Publish.new(request_id, options, topic, args, kwargs) 23 | 24 | # Return 25 | [lookup, message] 26 | end 27 | 28 | def process_success(message, lookup) 29 | if lookup 30 | # Get the params 31 | topic = lookup[:t] 32 | args = lookup[:a] 33 | kwargs = lookup[:k] 34 | options = lookup[:o] 35 | callback = lookup[:c] 36 | 37 | # Create the details 38 | details = {} 39 | details[:topic] = topic 40 | details[:type] = 'publish' 41 | details[:publication] = message.publication 42 | 43 | # Return the values 44 | [callback, { args: args, kwargs: kwargs, options: options }, details] 45 | else 46 | [nil, nil, nil] 47 | end 48 | end 49 | 50 | def process_error(message, lookup) 51 | if lookup 52 | # Get the params 53 | topic = lookup[:t] 54 | callback = lookup[:c] 55 | 56 | # Create the details 57 | details = message.details || {} 58 | details[:topic] = topic unless details[:topic] 59 | details[:type] = 'publish' 60 | 61 | # Return the values 62 | [callback, details] 63 | else 64 | [nil, nil] 65 | end 66 | end 67 | 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/wamp/client/request/register.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | require "wamp/client/manager/registration" 4 | 5 | module Wamp 6 | module Client 7 | module Request 8 | 9 | class Message::Registered 10 | def request_id 11 | self.register_request 12 | end 13 | end 14 | 15 | class Register < Base 16 | 17 | def create_request(request_id, procedure, handler, options=nil, interrupt=nil, &callback) 18 | 19 | # Create the lookup 20 | lookup = {p: procedure, h: handler, i: interrupt, o: options, c: callback} 21 | 22 | # Create the message 23 | message = Message::Register.new(request_id, options, procedure) 24 | 25 | # Return 26 | [lookup, message] 27 | end 28 | 29 | def process_success(message, lookup) 30 | if lookup 31 | # Get the params 32 | procedure = lookup[:p] 33 | handler = lookup[:h] 34 | options = lookup[:o] 35 | interrupt = lookup[:i] 36 | callback = lookup[:c] 37 | 38 | # Create the subscription 39 | r_id = message.registration 40 | r = Manager::RegistrationObject.new(procedure, handler, options, interrupt, self.session, r_id) 41 | 42 | # Create the details 43 | details = {} 44 | details[:procedure] = procedure 45 | details[:type] = 'register' 46 | 47 | # Call the on_success method 48 | self.on_success.call(r_id, r) 49 | 50 | # Return the values 51 | [callback, r, details] 52 | else 53 | [nil, nil, nil] 54 | end 55 | end 56 | 57 | def process_error(message, lookup) 58 | if lookup 59 | # Get the params 60 | procedure = lookup[:p] 61 | callback = lookup[:c] 62 | 63 | # Create the details 64 | details = message.details || {} 65 | details[:procedure] = procedure unless details[:procedure] 66 | details[:type] = 'register' 67 | 68 | # Return the values 69 | [callback, details] 70 | else 71 | [nil, nil] 72 | end 73 | end 74 | 75 | end 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/wamp/client/request/require.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/request/subscribe' 2 | require 'wamp/client/request/unsubscribe' 3 | require 'wamp/client/request/register' 4 | require 'wamp/client/request/unregister' 5 | require 'wamp/client/request/publish' 6 | require 'wamp/client/request/call' 7 | -------------------------------------------------------------------------------- /lib/wamp/client/request/subscribe.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | require "wamp/client/manager/subscription" 4 | 5 | module Wamp 6 | module Client 7 | module Request 8 | 9 | class Message::Subscribed 10 | def request_id 11 | self.subscribe_request 12 | end 13 | end 14 | 15 | class Subscribe < Base 16 | 17 | def create_request(request_id, topic, handler, options={}, &callback) 18 | 19 | # Create the lookup 20 | lookup = {t: topic, h: handler, o: options, c: callback} 21 | 22 | # Create the message 23 | message = Message::Subscribe.new(request_id, options, topic) 24 | 25 | # Return 26 | [lookup, message] 27 | end 28 | 29 | def process_success(message, lookup) 30 | if lookup 31 | # Get the params 32 | topic = lookup[:t] 33 | handler = lookup[:h] 34 | options = lookup[:o] 35 | callback = lookup[:c] 36 | 37 | # Create the subscription 38 | s_id = message.subscription 39 | s = Manager::SubscriptionObject.new(topic, handler, options, self.session, s_id) 40 | 41 | # Create the details 42 | details = {} 43 | details[:topic] = topic unless details[:topic] 44 | details[:type] = 'subscribe' 45 | 46 | # Call the on_success method 47 | self.on_success.call(s_id, s) 48 | 49 | # Return the values 50 | [callback, s, details] 51 | else 52 | [nil, nil, nil] 53 | end 54 | end 55 | 56 | def process_error(message, lookup) 57 | if lookup 58 | # Get the params 59 | topic = lookup[:t] 60 | callback = lookup[:c] 61 | 62 | # Create the details 63 | details = message.details || {} 64 | details[:topic] = topic unless details[:topic] 65 | details[:type] = 'subscribe' 66 | 67 | # Return the values 68 | [callback, details] 69 | else 70 | [nil, nil] 71 | end 72 | end 73 | 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/wamp/client/request/unregister.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | 4 | module Wamp 5 | module Client 6 | module Request 7 | 8 | class Message::Unregistered 9 | def request_id 10 | self.unregister_request 11 | end 12 | end 13 | 14 | class Unregister < Base 15 | 16 | def create_request(request_id, registration, &callback) 17 | 18 | # Create the lookup 19 | lookup = { r: registration, c: callback } 20 | 21 | # Create the message 22 | message = Message::Unregister.new(request_id, registration.id) 23 | 24 | # Return 25 | [lookup, message] 26 | end 27 | 28 | def process_success(message, lookup) 29 | if lookup 30 | # Get the params 31 | registration = lookup[:r] 32 | callback = lookup[:c] 33 | 34 | # Create the details 35 | details = {} 36 | details[:procedure] = registration.procedure 37 | details[:type] = 'unregister' 38 | 39 | # Call the on_success method 40 | self.on_success.call(registration.id) 41 | 42 | # Return the values 43 | [callback, registration, details] 44 | else 45 | [nil, nil, nil] 46 | end 47 | end 48 | 49 | def process_error(message, lookup) 50 | if lookup 51 | # Get the params 52 | registration = lookup[:r] 53 | callback = lookup[:c] 54 | 55 | # Create the details 56 | details = message.details || {} 57 | details[:procedure] = registration.procedure unless details[:procedure] 58 | details[:type] = 'unregister' 59 | 60 | # Return the values 61 | [callback, details] 62 | else 63 | [nil, nil] 64 | end 65 | end 66 | 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/wamp/client/request/unsubscribe.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require "wamp/client/message" 3 | 4 | module Wamp 5 | module Client 6 | module Request 7 | 8 | class Message::Unsubscribed 9 | def request_id 10 | self.unsubscribe_request 11 | end 12 | end 13 | 14 | class Unsubscribe < Base 15 | 16 | def create_request(request_id, subscription, &callback) 17 | 18 | # Create the lookup 19 | lookup = { s: subscription, c: callback } 20 | 21 | # Create the message 22 | message = Message::Unsubscribe.new(request_id, subscription.id) 23 | 24 | # Return 25 | [lookup, message] 26 | end 27 | 28 | def process_success(message, lookup) 29 | if lookup 30 | # Get the params 31 | subscription = lookup[:s] 32 | callback = lookup[:c] 33 | 34 | # Create the details 35 | details = {} 36 | details[:topic] = subscription.topic 37 | details[:type] = 'unsubscribe' 38 | 39 | # Call the on_success method 40 | self.on_success.call(subscription.id) 41 | 42 | # Return the values 43 | [callback, subscription, details] 44 | else 45 | [nil, nil, nil] 46 | end 47 | end 48 | 49 | def process_error(message, lookup) 50 | if lookup 51 | # Get the params 52 | subscription = lookup[:s] 53 | callback = lookup[:c] 54 | 55 | # Create the details 56 | details = message.details || {} 57 | details[:topic] = subscription.topic unless details[:topic] 58 | details[:type] = 'unsubscribe' 59 | 60 | # Return the values 61 | [callback, details] 62 | else 63 | [nil, nil] 64 | end 65 | 66 | end 67 | 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/wamp/client/response.rb: -------------------------------------------------------------------------------- 1 | require "wamp/client/event" 2 | 3 | module Wamp 4 | module Client 5 | module Response 6 | DEFAULT_ERROR = "wamp.error.runtime" 7 | 8 | # This method wraps the handling of the result from a procedure. 9 | # or interrupt. It is intended to standardize the processing 10 | # 11 | # @param [Bool] - "true" is we want an error out of this 12 | # @return [CallResult, CallError, CallDefer] - A response object 13 | def self.invoke_handler(error: false, &callback) 14 | logger = Wamp::Client.logger 15 | 16 | # Invoke the request 17 | begin 18 | result = callback.call 19 | rescue CallError => e 20 | result = e 21 | rescue StandardError => e 22 | logger.error("Wamp::Client::Response - #{e.message}") 23 | e.backtrace.each { |line| logger.error(" #{line}") } 24 | result = CallError.new(DEFAULT_ERROR, [e.message], { backtrace: e.backtrace }) 25 | end 26 | 27 | # Ensure an expected class is returned 28 | if error 29 | CallError.ensure(result) 30 | else 31 | CallResult.ensure(result, allow_error: true, allow_defer: true) 32 | end 33 | end 34 | 35 | # This method will instantiate either a CallResult or CallError based 36 | # on the payload 37 | # 38 | # @param hash [Hash] - The hash 39 | # @return [CallResult, CallError] - The result 40 | def self.from_hash(hash) 41 | if hash[:error] != nil 42 | CallError.from_hash(hash) 43 | else 44 | CallResult.from_hash(hash) 45 | end 46 | end 47 | 48 | class CallResult 49 | attr_reader :args, :kwargs 50 | 51 | def initialize(args=nil, kwargs=nil) 52 | @args = args || [] 53 | @kwargs = kwargs || {} 54 | end 55 | 56 | def self.from_hash(hash) 57 | self.new(hash[:args], hash[:kwargs]) 58 | end 59 | 60 | def to_hash 61 | { args: self.args, kwargs: self.kwargs } 62 | end 63 | 64 | def self.from_yield_message(msg) 65 | self.new(msg.yield_arguments, msg.yield_argumentskw) 66 | end 67 | 68 | def self.ensure(result, allow_error: false, allow_defer: false) 69 | unless result.is_a?(self) or 70 | (allow_error and result.is_a?(CallError)) or 71 | (allow_defer and result.is_a?(CallDefer)) 72 | result = result != nil ? self.new([result]) : self.new 73 | end 74 | 75 | result 76 | end 77 | end 78 | 79 | class CallError < StandardError 80 | attr_reader :error, :args, :kwargs 81 | 82 | def initialize(error, args=nil, kwargs=nil) 83 | @error = error 84 | @args = args || [] 85 | @kwargs = kwargs || {} 86 | end 87 | 88 | def self.from_hash(hash) 89 | self.new(hash[:error], hash[:args], hash[:kwargs]) 90 | end 91 | 92 | def to_hash 93 | { error: self.error, args: self.args, kwargs: self.kwargs } 94 | end 95 | 96 | def self.from_message(msg) 97 | self.new(msg.error, msg.arguments, msg.argumentskw) 98 | end 99 | 100 | def self.ensure(result) 101 | unless result.is_a?(self) 102 | args = result != nil ? [result] : nil 103 | result = self.new(DEFAULT_ERROR, args) 104 | end 105 | 106 | result 107 | end 108 | end 109 | 110 | class CallDefer 111 | include Event 112 | attr_accessor :request, :registration 113 | 114 | create_event [:complete, :error, :progress] 115 | 116 | def succeed(result) 117 | trigger :complete, self, result 118 | end 119 | 120 | def fail(error) 121 | trigger :error, self, error 122 | end 123 | 124 | end 125 | 126 | class ProgressiveCallDefer < CallDefer 127 | 128 | def progress(result) 129 | trigger :progress, self, result 130 | end 131 | 132 | end 133 | 134 | end 135 | end 136 | end -------------------------------------------------------------------------------- /lib/wamp/client/serializer.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Wamp 4 | module Client 5 | module Serializer 6 | class Base 7 | 8 | attr_accessor :type 9 | 10 | # Serializes the object 11 | # @param object - The object to serialize 12 | def serialize(object) 13 | end 14 | 15 | # Deserializes the object 16 | # @param string [String] - The string to deserialize 17 | # @return The deserialized object 18 | def deserialize(string) 19 | end 20 | 21 | end 22 | 23 | class JSONSerializer < Base 24 | 25 | def initialize 26 | self.type = 'json' 27 | end 28 | 29 | def serialize(object) 30 | JSON.generate object 31 | end 32 | 33 | def deserialize(string) 34 | JSON.parse(string, {:symbolize_names => true}) 35 | end 36 | 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/wamp/client/session.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/transport/base' 2 | require 'wamp/client/message' 3 | require 'wamp/client/check' 4 | require 'wamp/client/version' 5 | require 'wamp/client/event' 6 | require 'wamp/client/request/require' 7 | require 'wamp/client/manager/require' 8 | 9 | module Wamp 10 | module Client 11 | 12 | CLOSED_SESSION_METHOD_LOOKUP = { 13 | Message::Types::WELCOME => -> s, m { s.establish.welcome(m) }, 14 | Message::Types::CHALLENGE => -> s, m { s.establish.challenge(m) }, 15 | Message::Types::ABORT => -> s, m { s.establish.abort(m) }, 16 | } 17 | 18 | OPEN_SESSION_METHOD_LOOKUP = { 19 | # Establish Response 20 | Message::Types::GOODBYE => -> s, m { s.establish.goodbye(m) }, 21 | 22 | # Error Responses 23 | Message::Types::SUBSCRIBE => -> s, m { s.request[:subscribe].error(m) }, 24 | Message::Types::UNSUBSCRIBE => -> s, m { s.request[:unsubscribe].error(m) }, 25 | Message::Types::PUBLISH => -> s, m { s.request[:publish].error(m) }, 26 | Message::Types::REGISTER => -> s, m { s.request[:register].error(m) }, 27 | Message::Types::UNREGISTER => -> s, m { s.request[:unregister].error(m) }, 28 | Message::Types::CALL => -> s, m { s.request[:call].error(m) }, 29 | 30 | # Result Responses 31 | Message::Types::SUBSCRIBED => -> s, m { s.request[:subscribe].success(m) }, 32 | Message::Types::UNSUBSCRIBED => -> s, m { s.request[:unsubscribe].success(m) }, 33 | Message::Types::PUBLISHED => -> s, m { s.request[:publish].success(m) }, 34 | Message::Types::EVENT => -> s, m { s.subscription.event(m) }, 35 | Message::Types::REGISTERED => -> s, m { s.request[:register].success(m) }, 36 | Message::Types::UNREGISTERED => -> s, m { s.request[:unregister].success(m) }, 37 | Message::Types::INVOCATION => -> s, m { s.registration.invoke(m) }, 38 | Message::Types::INTERRUPT => -> s, m { s.registration.interrupt(m) }, 39 | Message::Types::RESULT => -> s, m { s.request[:call].success(m) }, 40 | } 41 | 42 | class Session 43 | include Check 44 | include Event 45 | 46 | attr_accessor :transport, :options, :request, :callback, 47 | :subscription, :registration, :establish 48 | 49 | create_event [:join, :challenge, :leave] 50 | 51 | # Constructor 52 | # @param transport [Transport::Base] The transport that the session will use 53 | # @param options [Hash] Hash containing different session options 54 | # @option options [String] :authid The authentication ID 55 | # @option options [Array] :authmethods Different auth methods that this client supports 56 | def initialize(transport, options={}) 57 | 58 | # Parameters 59 | self.options = options || {} 60 | 61 | # Log the event 62 | logger.info("#{self.class.name} created with options") 63 | logger.info(" uri: #{options[:uri]}") 64 | logger.info(" realm: #{options[:realm]}") 65 | 66 | # Create the send message lambda for the request objects 67 | send_message_lambda = -> m { send_message(m) } 68 | 69 | # Outstanding Requests 70 | self.request = { 71 | publish: Request::Publish.new(self, send_message_lambda), 72 | subscribe: Request::Subscribe.new(self, send_message_lambda) { |s_id, s| self.subscription.add(s_id, s) }, 73 | unsubscribe: Request::Unsubscribe.new(self, send_message_lambda) { |s_id| self.subscription.remove(s_id) }, 74 | call: Request::Call.new(self, send_message_lambda), 75 | register: Request::Register.new(self, send_message_lambda) { |r_id, r| self.registration.add(r_id, r) }, 76 | unregister: Request::Unregister.new(self, send_message_lambda) { |r_id| self.registration.remove(r_id) }, 77 | } 78 | 79 | # Init Subs and Regs in place 80 | self.subscription = Manager::Subscription.new(self, send_message_lambda) 81 | self.registration = Manager::Registration.new(self, send_message_lambda) 82 | self.establish = Manager::Establish.new(self, send_message_lambda) 83 | 84 | # Setup session callbacks 85 | self.callback = {} 86 | 87 | # Setup Transport 88 | self.transport = transport 89 | self.transport.on :message do |msg| 90 | receive_message(msg) 91 | end 92 | 93 | end 94 | 95 | # Returns 'true' if the session is open 96 | # 97 | def is_open? 98 | self.establish.is_open? 99 | end 100 | 101 | # Returns the ID of the session 102 | # 103 | def id 104 | self.establish.id 105 | end 106 | 107 | # Returns the realm of the session 108 | # 109 | def realm 110 | self.establish.realm 111 | end 112 | 113 | # Joins the WAMP Router 114 | # 115 | # @param realm [String] The name of the realm 116 | def join(realm) 117 | check_closed 118 | 119 | # Check params 120 | self.class.check_uri('realm', realm) 121 | 122 | # Attempt to join 123 | self.establish.join(realm) 124 | end 125 | 126 | # Leaves the WAMP Router 127 | # 128 | # @param reason [String] URI signalling the reason for leaving 129 | def leave(reason='wamp.close.normal', message='user initiated') 130 | check_open 131 | 132 | # Check params 133 | self.class.check_uri('reason', reason, true) 134 | self.class.check_string('message', message, true) 135 | 136 | # Leave the session 137 | self.establish.leave(reason, message) 138 | end 139 | 140 | # Subscribes to a topic 141 | # 142 | # @param topic [String] The topic to subscribe to 143 | # @param handler [lambda] The handler(args, kwargs, details) when an event is received 144 | # @param options [Hash] The options for the subscription 145 | # @param callback [block] The callback(subscription, error) called to signal if the subscription was a success or not 146 | def subscribe(topic, handler, options={}, &callback) 147 | check_open 148 | 149 | # Check params 150 | self.class.check_uri('topic', topic) 151 | self.class.check_dict('options', options) 152 | self.class.check_nil('handler', handler, false) 153 | 154 | # Make the request 155 | make_request(:subscribe, :request, topic, handler, options, &callback) 156 | end 157 | 158 | # Unsubscribes from a subscription 159 | # 160 | # @param subscription [Subscription] The subscription object from when the subscription was created 161 | # @param callback [block] The callback(subscription, error, details) called to signal if the subscription was a success or not 162 | def unsubscribe(subscription, &callback) 163 | check_open 164 | 165 | # Check params 166 | self.class.check_nil('subscription', subscription, false) 167 | 168 | # Make the request 169 | make_request(:unsubscribe, :request, subscription, &callback) 170 | end 171 | 172 | # Publishes and event to a topic 173 | # 174 | # @param topic [String] The topic to publish the event to 175 | # @param args [Array] The arguments 176 | # @param kwargs [Hash] The keyword arguments 177 | # @param options [Hash] The options for the publish 178 | # @param callback [block] The callback(publish, error, details) called to signal if the publish was a success or not 179 | def publish(topic, args=nil, kwargs=nil, options={}, &callback) 180 | check_open 181 | 182 | # Check params 183 | self.class.check_uri('topic', topic) 184 | self.class.check_dict('options', options) 185 | self.class.check_list('args', args, true) 186 | self.class.check_dict('kwargs', kwargs, true) 187 | 188 | # Make the request 189 | make_request(:publish, :request, topic, args, kwargs, options, &callback) 190 | end 191 | 192 | # Register to a procedure 193 | # 194 | # @param procedure [String] The procedure to register for 195 | # @param handler [lambda] The handler(args, kwargs, details) when an invocation is received 196 | # @param options [Hash, nil] The options for the registration 197 | # @param interrupt [lambda] The handler(request, mode) when an interrupt is received 198 | # @param callback [block] The callback(registration, error, details) called to signal if the registration was a success or not 199 | def register(procedure, handler, options=nil, interrupt=nil, &callback) 200 | check_open 201 | 202 | options ||= {} 203 | 204 | # Check params 205 | self.class.check_uri('procedure', procedure) 206 | self.class.check_nil('handler', handler, false) 207 | 208 | # Make the request 209 | make_request(:register, :request, procedure, handler, options, interrupt, &callback) 210 | end 211 | 212 | # Sends a result for the invocation 213 | # 214 | # @param request [Integer] - The id of the request 215 | # @param result [CallError, CallResult, anything] - If it is a CallError, the error will be returned 216 | # @param options [Hash] - The options to be sent with the yield 217 | def yield(request, result, options={}, check_defer=false) 218 | check_open 219 | 220 | # Call the registration yield method 221 | self.registration.yield(request, result, options, check_defer) 222 | end 223 | 224 | # Unregisters from a procedure 225 | # 226 | # @param registration [Registration] The registration object from when the registration was created 227 | # @param callback [block] The callback(registration, error, details) called to signal if the unregistration was a success or not 228 | def unregister(registration, &callback) 229 | check_open 230 | 231 | # Check params 232 | self.class.check_nil('registration', registration, false) 233 | 234 | # Make the request 235 | make_request(:unregister, :request, registration, &callback) 236 | end 237 | 238 | # Publishes and event to a topic 239 | # 240 | # @param procedure [String] The procedure to invoke 241 | # @param args [Array] The arguments 242 | # @param kwargs [Hash] The keyword arguments 243 | # @param options [Hash] The options for the call 244 | # @param callback [block] The callback(result, error, details) called to signal if the call was a success or not 245 | # @return [Call] An object representing the call 246 | def call(procedure, args=nil, kwargs=nil, options={}, &callback) 247 | check_open 248 | 249 | # Check params 250 | self.class.check_uri('procedure', procedure) 251 | self.class.check_dict('options', options) 252 | self.class.check_list('args', args, true) 253 | self.class.check_dict('kwargs', kwargs, true) 254 | 255 | # Make the request 256 | request_id = make_request(:call, :request, procedure, args, kwargs, options, &callback) 257 | 258 | # Create the call object 259 | call = Request::CallObject.new(self, request_id) 260 | 261 | # Timeout Logic 262 | if options[:timeout] and options[:timeout] > 0 263 | # Once the timer expires, if the call hasn't completed, cancel it 264 | self.transport.add_timer(options[:timeout]) do 265 | call.cancel 266 | end 267 | end 268 | 269 | call 270 | end 271 | 272 | # Cancels a call 273 | # 274 | # @param call [Call] - The call object 275 | # @param mode [String] - The mode of the skip. Options are 'skip', 'kill', 'killnowait' 276 | def cancel(call, mode='skip') 277 | check_open 278 | 279 | # Check params 280 | self.class.check_nil('call', call, false) 281 | 282 | # Cancel the request 283 | make_request(:call, :cancel, call.id, mode) 284 | end 285 | 286 | private 287 | 288 | def check_closed 289 | if is_open? 290 | raise RuntimeError, "session must be closed to call this method" 291 | end 292 | end 293 | 294 | def check_open 295 | unless is_open? 296 | raise RuntimeError, "session must be open to call this method" 297 | end 298 | end 299 | 300 | def make_request(name, method, *args, &callback) 301 | self.request[name].send(method, *args, &callback) 302 | end 303 | 304 | def logger 305 | Wamp::Client.logger 306 | end 307 | 308 | def send_message(msg) 309 | 310 | # Log the message 311 | logger.debug("#{self.class.name} TX: #{msg.to_s}") 312 | 313 | # Send it to the transport 314 | self.transport.send_message(msg.payload) 315 | end 316 | 317 | def receive_message(msg) 318 | 319 | # Print the raw message 320 | logger.debug("#{self.class.name} RX(raw): #{msg.to_s}") 321 | 322 | # Parse the WAMP message 323 | message = Message.parse(msg) 324 | 325 | # Print the parsed WAMP message 326 | logger.debug("#{self.class.name} RX: #{message.to_s}") 327 | 328 | # Get the lookup based on the state of the session 329 | lookup = self.is_open? ? OPEN_SESSION_METHOD_LOOKUP : CLOSED_SESSION_METHOD_LOOKUP 330 | 331 | # Get the type of message 332 | type = message.is_a?(Message::Error) ? message.request_type : message.class.type 333 | 334 | # Get the handler 335 | handler = lookup[type] 336 | 337 | # Execute the handler 338 | if handler != nil 339 | # Catch any standard exception and log it 340 | begin 341 | handler.call(self, message) 342 | rescue StandardError => e 343 | logger.error("#{self.class.name} - #{e.message}") 344 | e.backtrace.each { |line| logger.error(" #{line}") } 345 | end 346 | else 347 | logger.error("#{self.class.name} unknown message type '#{type}'") 348 | end 349 | end 350 | 351 | end 352 | end 353 | end -------------------------------------------------------------------------------- /lib/wamp/client/transport/base.rb: -------------------------------------------------------------------------------- 1 | require 'wamp/client/serializer' 2 | require 'wamp/client/event' 3 | 4 | module Wamp 5 | module Client 6 | module Transport 7 | class Base 8 | include Event 9 | 10 | attr_accessor :uri, :proxy, :headers, :protocol, :serializer, :connected 11 | 12 | create_event [:open, :close, :message, :error] 13 | 14 | # Constructor for the transport 15 | # @param options [Hash] The connection options. the different options are as follows 16 | # @option options [String] :uri The url to connect to 17 | # @option options [String] :proxy The proxy to use 18 | # @option options [String] :protocol The protocol 19 | # @option options [Hash] :headers Custom headers to include during the connection 20 | # @option options [WampClient::Serializer::Base] :serializer The serializer to use 21 | def initialize(options) 22 | 23 | # Initialize the parameters 24 | self.connected = false 25 | self.uri = options[:uri] 26 | self.proxy = options[:proxy] 27 | self.headers = options[:headers] || {} 28 | self.protocol = options[:protocol] || 'wamp.2.json' 29 | self.serializer = options[:serializer] || Wamp::Client::Serializer::JSONSerializer.new 30 | 31 | # Add the wamp.2.json protocol header 32 | self.headers['Sec-WebSocket-Protocol'] = self.protocol 33 | end 34 | 35 | # Connects to the WAMP Server using the transport 36 | def connect 37 | # Implement in subclass 38 | end 39 | 40 | # Disconnects from the WAMP Server 41 | def disconnect 42 | # Implement in subclass 43 | end 44 | 45 | # Returns true if the transport it connected 46 | def connected? 47 | self.connected 48 | end 49 | 50 | # Sends a Message 51 | # @param [Array] msg - The message payload to send 52 | def send_message(msg) 53 | # Implement in subclass 54 | end 55 | 56 | # Process the callback when the timer expires 57 | # @param [Integer] milliseconds - The number 58 | # @param [block] callback - The callback that is fired when the timer expires 59 | def self.add_timer(milliseconds, &callback) 60 | # Implement in subclass 61 | end 62 | def add_timer(milliseconds, &callback) 63 | self.class.add_timer(milliseconds, &callback) 64 | end 65 | 66 | # Method to start the event machine for the socket 67 | def self.start_event_machine(&block) 68 | # Implement in subclass 69 | end 70 | 71 | # Method to stop the vent machine 72 | def self.stop_event_machine 73 | # Implement in subclass 74 | end 75 | 76 | # Method to add a tick loop to the event machine 77 | def self.add_tick_loop(&block) 78 | # Implement in subclass 79 | end 80 | end 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /lib/wamp/client/transport/event_machine_base.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require_relative 'base' 3 | 4 | module Wamp 5 | module Client 6 | module Transport 7 | class EventMachineBase < Base 8 | 9 | def self.start_event_machine(&block) 10 | EM.run do 11 | block.call 12 | end 13 | end 14 | 15 | def self.stop_event_machine 16 | EM.stop 17 | end 18 | 19 | def self.add_timer(milliseconds, &callback) 20 | delay = (milliseconds.to_f/1000.0).ceil 21 | EM.add_timer(delay) { 22 | callback.call 23 | } 24 | end 25 | 26 | def self.add_tick_loop(&block) 27 | EM.tick_loop(&block) 28 | end 29 | 30 | end 31 | end 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/wamp/client/transport/faye_web_socket.rb: -------------------------------------------------------------------------------- 1 | require_relative 'event_machine_base' 2 | 3 | # This implementation uses the 'faye-websocket' Gem. 4 | module Wamp 5 | module Client 6 | module Transport 7 | class FayeWebSocket < EventMachineBase 8 | attr_accessor :socket 9 | 10 | def initialize(options) 11 | super(options) 12 | self.socket = nil 13 | 14 | # Only make them include the gem if they are going to use it 15 | require 'faye/websocket' 16 | end 17 | 18 | def connect 19 | options = { :headers => self.headers } 20 | options[:proxy] = self.proxy if self.proxy != nil 21 | self.socket = Faye::WebSocket::Client.new(self.uri, [self.protocol], options) 22 | 23 | self.socket.on(:open) do |event| 24 | self.connected = true 25 | trigger :open 26 | end 27 | 28 | self.socket.on(:message) do |event| 29 | trigger :message, self.serializer.deserialize(event.data) 30 | end 31 | 32 | self.socket.on(:close) do |event| 33 | self.connected = false 34 | trigger :close, event.reason 35 | end 36 | 37 | self.socket.on(:error) do |event| 38 | trigger :error, event.message 39 | end 40 | end 41 | 42 | def disconnect 43 | self.socket.close 44 | self.connected = false 45 | end 46 | 47 | def send_message(msg) 48 | if self.connected 49 | self.socket.send(self.serializer.serialize(msg)) 50 | else 51 | raise RuntimeError, "Socket must be open to call 'send_message'" 52 | end 53 | end 54 | 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/wamp/client/transport/web_socket_event_machine.rb: -------------------------------------------------------------------------------- 1 | require_relative 'event_machine_base' 2 | 3 | # This implementation uses the 'websocket-eventmachine-client' Gem. 4 | # This is the default if no transport is included 5 | module Wamp 6 | module Client 7 | module Transport 8 | class WebSocketEventMachine < EventMachineBase 9 | attr_accessor :socket 10 | 11 | def initialize(options) 12 | super(options) 13 | self.socket = nil 14 | 15 | # Only make them include the gem if they are going to use it 16 | require 'websocket-eventmachine-client' 17 | 18 | # Raise an exception if proxy was included (not supported) 19 | if self.proxy != nil 20 | raise RuntimeError, "The WebSocketEventMachine transport does not support 'proxy'. Try using 'faye-websocket' transport instead" 21 | end 22 | end 23 | 24 | def connect 25 | self.socket = WebSocket::EventMachine::Client.connect( 26 | :uri => self.uri, 27 | :headers => self.headers 28 | ) 29 | 30 | self.socket.onopen do 31 | self.connected = true 32 | trigger :open 33 | end 34 | 35 | self.socket.onmessage do |msg, type| 36 | trigger :message, self.serializer.deserialize(msg) 37 | end 38 | 39 | self.socket.onclose do |code, reason| 40 | self.connected = false 41 | trigger :close, reason 42 | end 43 | end 44 | 45 | def disconnect 46 | self.connected = !self.socket.close # close returns 'true' if the connection was closed immediately 47 | end 48 | 49 | def send_message(msg) 50 | if self.connected 51 | self.socket.send(self.serializer.serialize(msg), {type: 'text'}) 52 | else 53 | raise RuntimeError, "Socket must be open to call 'send_message'" 54 | end 55 | end 56 | 57 | end 58 | end 59 | end 60 | end 61 | 62 | -------------------------------------------------------------------------------- /lib/wamp/client/version.rb: -------------------------------------------------------------------------------- 1 | module Wamp 2 | module Client 3 | VERSION = '0.2.2' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /scripts/gen_message.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | def value_from_type(type) 4 | 5 | value = nil 6 | 7 | if type == 'int' or type == 'id' 8 | value = '123' 9 | elsif type == 'uri' or type == 'string' 10 | value = "'string'" 11 | elsif type == 'list' 12 | value = "['test']" 13 | elsif type == 'dict' 14 | value = '{ test: 1 }' 15 | end 16 | 17 | value 18 | 19 | end 20 | 21 | def empty_value_from_type(type) 22 | 23 | value = nil 24 | 25 | if type == 'int' or type == 'id' 26 | value = '0' 27 | elsif type == 'uri' or type == 'string' 28 | value = "''" 29 | elsif type == 'list' 30 | value = "[]" 31 | elsif type == 'dict' 32 | value = '{}' 33 | end 34 | 35 | value 36 | 37 | end 38 | 39 | message_type_lookup = { 40 | 'HELLO' => 1, 41 | 'WELCOME' => 2, 42 | 'ABORT' => 3, 43 | 'CHALLENGE' => 4, 44 | 'AUTHENTICATE' => 5, 45 | 'GOODBYE' => 6, 46 | 'ERROR' => 8, 47 | 'PUBLISH' => 16, 48 | 'PUBLISHED' => 17, 49 | 'SUBSCRIBE' => 32, 50 | 'SUBSCRIBED' => 33, 51 | 'UNSUBSCRIBE' => 34, 52 | 'UNSUBSCRIBED' => 35, 53 | 'EVENT' => 36, 54 | 'CALL' => 48, 55 | 'CANCEL' => 49, 56 | 'RESULT' => 50, 57 | 'REGISTER' => 64, 58 | 'REGISTERED' => 65, 59 | 'UNREGISTER' => 66, 60 | 'UNREGISTERED' => 67, 61 | 'INVOCATION' => 68, 62 | 'INTERRUPT' => 69, 63 | 'YIELD' => 70 64 | } 65 | 66 | message_type_define = '' 67 | message_lookup_define = '' 68 | message_type_lookup.each do |name, value| 69 | 70 | # Generate the defines 71 | message_type_define += " #{name} = #{value}\n" 72 | 73 | # Generate the lookup 74 | message_lookup_define += " Types::#{name} => #{name.downcase.capitalize},\n" 75 | end 76 | 77 | source_file_header = "require 'wamp/client/check' 78 | 79 | # !!!!THIS FILE IS AUTOGENERATED. DO NOT HAND EDIT!!!! 80 | 81 | module Wamp 82 | module Client 83 | module Message 84 | 85 | module Types 86 | #{message_type_define} end 87 | 88 | class Base 89 | include Wamp::Client::Check 90 | 91 | def payload 92 | [] 93 | end 94 | 95 | # @param params [Array] 96 | def self.parse(params) 97 | nil 98 | end 99 | end 100 | " 101 | 102 | source_file_footer = " 103 | TYPE_LOOKUP = { 104 | #{message_lookup_define} } 105 | 106 | # @param params [Array] 107 | def self.parse(params) 108 | klass = TYPE_LOOKUP[params[0]] 109 | klass ? klass.parse(params.clone) : nil 110 | end 111 | 112 | end 113 | end 114 | end 115 | " 116 | 117 | test_file_header = "require 'spec_helper' 118 | 119 | # !!!!THIS FILE IS AUTOGENERATED. DO NOT HAND EDIT!!!! 120 | 121 | describe Wamp::Client::Message do 122 | " 123 | 124 | test_file_footer = " 125 | end 126 | " 127 | 128 | source_file = source_file_header 129 | test_file = test_file_header 130 | 131 | ############################################### 132 | # Iterate through message types 133 | ############################################### 134 | 135 | messages = [ 136 | { 137 | name: 'hello', 138 | description: 'Sent by a Client to initiate opening of a WAMP session to a Router attaching to a Realm.', 139 | formats: [ 140 | '[HELLO, Realm|uri, Details|dict]' 141 | ] 142 | }, 143 | { 144 | name: 'welcome', 145 | description: 'Sent by a Router to accept a Client. The WAMP session is now open.', 146 | formats: [ 147 | '[WELCOME, Session|id, Details|dict]' 148 | ] 149 | }, 150 | { 151 | name: 'abort', 152 | description: 'Sent by a Peer*to abort the opening of a WAMP session. No response is expected.', 153 | formats: [ 154 | '[ABORT, Details|dict, Reason|uri]' 155 | ] 156 | }, 157 | { 158 | name: 'goodbye', 159 | description: "Sent by a Peer to close a previously opened WAMP session. Must be echo'ed by the receiving Peer.", 160 | formats: [ 161 | '[GOODBYE, Details|dict, Reason|uri]' 162 | ] 163 | }, 164 | { 165 | name: 'error', 166 | description: 'Error reply sent by a Peer as an error response to different kinds of requests.', 167 | formats: [ 168 | '[ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri]', 169 | '[ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list]', 170 | '[ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list, ArgumentsKw|dict]' 171 | ] 172 | }, 173 | { 174 | name: 'publish', 175 | description: 'Sent by a Publisher to a Broker to publish an event.', 176 | formats: [ 177 | '[PUBLISH, Request|id, Options|dict, Topic|uri]', 178 | '[PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list]', 179 | '[PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list, ArgumentsKw|dict]' 180 | ] 181 | }, 182 | { 183 | name: 'published', 184 | description: 'Acknowledge sent by a Broker to a Publisher for acknowledged publications.', 185 | formats: [ 186 | '[PUBLISHED, PUBLISH.Request|id, Publication|id]' 187 | ] 188 | }, 189 | { 190 | name: 'subscribe', 191 | description: 'Subscribe request sent by a Subscriber to a Broker to subscribe to a topic.', 192 | formats: [ 193 | '[SUBSCRIBE, Request|id, Options|dict, Topic|uri]' 194 | ] 195 | }, 196 | { 197 | name: 'subscribed', 198 | description: 'Acknowledge sent by a Broker to a Subscriber to acknowledge a subscription.', 199 | formats: [ 200 | '[SUBSCRIBED, SUBSCRIBE.Request|id, Subscription|id]' 201 | ] 202 | }, 203 | { 204 | name: 'unsubscribe', 205 | description: 'Unsubscribe request sent by a Subscriber to a Broker to unsubscribe a subscription.', 206 | formats: [ 207 | '[UNSUBSCRIBE, Request|id, SUBSCRIBED.Subscription|id]' 208 | ] 209 | }, 210 | { 211 | name: 'unsubscribed', 212 | description: 'Acknowledge sent by a Broker to a Subscriber to acknowledge unsubscription.', 213 | formats: [ 214 | '[UNSUBSCRIBED, UNSUBSCRIBE.Request|id]' 215 | ] 216 | }, 217 | { 218 | name: 'event', 219 | description: 'Event dispatched by Broker to Subscribers for subscriptions the event was matching.', 220 | formats: [ 221 | '[EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict]', 222 | '[EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list]', 223 | '[EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentsKw|dict]' 224 | ] 225 | }, 226 | { 227 | name: 'call', 228 | description: 'Call as originally issued by the _Caller_ to the _Dealer_.', 229 | formats: [ 230 | '[CALL, Request|id, Options|dict, Procedure|uri]', 231 | '[CALL, Request|id, Options|dict, Procedure|uri, Arguments|list]', 232 | '[CALL, Request|id, Options|dict, Procedure|uri, Arguments|list, ArgumentsKw|dict]' 233 | ] 234 | }, 235 | { 236 | name: 'result', 237 | description: 'Result of a call as returned by _Dealer_ to _Caller_.', 238 | formats: [ 239 | '[RESULT, CALL.Request|id, Details|dict]', 240 | '[RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list]', 241 | '[RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list, YIELD.ArgumentsKw|dict]' 242 | ] 243 | }, 244 | { 245 | name: 'register', 246 | description: 'A _Callees_ request to register an endpoint at a _Dealer_.', 247 | formats: [ 248 | '[REGISTER, Request|id, Options|dict, Procedure|uri]' 249 | ] 250 | }, 251 | { 252 | name: 'registered', 253 | description: 'Acknowledge sent by a _Dealer_ to a _Callee_ for successful registration.', 254 | formats: [ 255 | '[REGISTERED, REGISTER.Request|id, Registration|id]' 256 | ] 257 | }, 258 | { 259 | name: 'unregister', 260 | description: 'A _Callees_ request to unregister a previously established registration.', 261 | formats: [ 262 | '[UNREGISTER, Request|id, REGISTERED.Registration|id]' 263 | ] 264 | }, 265 | { 266 | name: 'unregistered', 267 | description: 'Acknowledge sent by a _Dealer_ to a _Callee_ for successful unregistration.', 268 | formats: [ 269 | '[UNREGISTERED, UNREGISTER.Request|id]' 270 | ] 271 | }, 272 | { 273 | name: 'invocation', 274 | description: 'Actual invocation of an endpoint sent by _Dealer_ to a _Callee_.', 275 | formats: [ 276 | '[INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict]', 277 | '[INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list]', 278 | '[INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list, CALL.ArgumentsKw|dict]' 279 | ] 280 | }, 281 | { 282 | name: 'yield', 283 | description: 'Actual yield from an endpoint sent by a _Callee_ to _Dealer_.', 284 | formats: [ 285 | '[YIELD, INVOCATION.Request|id, Options|dict]', 286 | '[YIELD, INVOCATION.Request|id, Options|dict, Arguments|list]', 287 | '[YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict]' 288 | ] 289 | }, 290 | { 291 | name: 'challenge', 292 | description: 'The "CHALLENGE" message is used with certain Authentication Methods. During authenticated session establishment, a *Router* sends a challenge message.', 293 | formats: [ 294 | '[CHALLENGE, AuthMethod|string, Extra|dict]' 295 | ] 296 | }, 297 | { 298 | name: 'authenticate', 299 | description: 'The "AUTHENTICATE" message is used with certain Authentication Methods. A *Client* having received a challenge is expected to respond by sending a signature or token.', 300 | formats: [ 301 | '[AUTHENTICATE, Signature|string, Extra|dict]' 302 | ] 303 | }, 304 | { 305 | name: 'cancel', 306 | description: 'The "CANCEL" message is used with the Call Canceling advanced feature. A _Caller_ can cancel and issued call actively by sending a cancel message to the _Dealer_.', 307 | formats: [ 308 | '[CANCEL, CALL.Request|id, Options|dict]' 309 | ] 310 | }, 311 | { 312 | name: 'interrupt', 313 | description: 'The "INTERRUPT" message is used with the Call Canceling advanced feature. Upon receiving a cancel for a pending call, a _Dealer_ will issue an interrupt to the _Callee_.', 314 | formats: [ 315 | '[INTERRUPT, INVOCATION.Request|id, Options|dict]' 316 | ] 317 | } 318 | ] 319 | 320 | messages.each do |message| 321 | 322 | ############################################### 323 | # Generate Lookups 324 | ############################################### 325 | count = 0 326 | params_lookup = {} 327 | params = [] 328 | required_count = 0 329 | param_formats = '' 330 | message[:formats].each do |format| 331 | param_formats += ' # ' + format + "\n" 332 | 333 | # Generate the params 334 | temp_format = format.delete(' ') 335 | temp_format = temp_format.delete('[') 336 | temp_format = temp_format.delete(']') 337 | temp_format = temp_format.gsub('.', '_') 338 | format_params = temp_format.split(',') 339 | format_params.shift 340 | 341 | format_params.each do |format_param| 342 | parsed_param = format_param.split('|') 343 | param_name = parsed_param[0].downcase 344 | param_type = parsed_param[1] 345 | 346 | if params_lookup[param_name].nil? 347 | params.push( 348 | { 349 | name: param_name, 350 | type: param_type, 351 | required: count == 0 352 | }) 353 | params_lookup[param_name] = true 354 | end 355 | end 356 | 357 | if count == 0 358 | required_count = params.count 359 | end 360 | 361 | count += 1 362 | end 363 | 364 | ############################################### 365 | # Source File 366 | ############################################### 367 | source_file += "\n" 368 | source_file += ' # ' + message[:name].capitalize + "\n" 369 | source_file += ' # ' + message[:description] + "\n" 370 | source_file += " # Formats:\n" 371 | source_file += param_formats 372 | source_file += ' class ' + message[:name].capitalize + " < Base\n" 373 | 374 | # Generate the local variables 375 | source_file += ' attr_accessor' 376 | count = 0 377 | params.each do |param| 378 | source_file += ',' unless count == 0 379 | source_file += " :#{param[:name]}" 380 | count += 1 381 | end 382 | source_file += "\n" 383 | 384 | # Generate the constructor 385 | source_file += "\n def initialize(" 386 | count = 0 387 | checks = '' 388 | setters = '' 389 | params.each do |param| 390 | setters += " self.#{param[:name]} = #{param[:name]}\n" 391 | 392 | source_file += ', ' if count > 0 393 | if param[:required] 394 | source_file += "#{param[:name]}" 395 | checks += " self.class.check_#{param[:type]}('#{param[:name]}', #{param[:name]})\n" 396 | else 397 | source_file += "#{param[:name]}=nil" 398 | checks += " self.class.check_#{param[:type]}('#{param[:name]}', #{param[:name]}, true)\n" 399 | end 400 | 401 | count += 1 402 | end 403 | source_file += ")\n\n" 404 | source_file += checks + "\n" 405 | source_file += setters + "\n" 406 | source_file += " end\n" 407 | 408 | # Generate the 'type' method 409 | source_file += "\n def self.type\n Types::#{message[:name].upcase}\n end\n" 410 | 411 | # Generate the parser 412 | source_file += "\n def self.parse(params)\n" 413 | source_file += "\n self.check_gte('params list', #{required_count+1}, params.count)\n" 414 | source_file += " self.check_equal('message type', self.type, params[0])\n" 415 | source_file += "\n params.shift\n self.new(*params)\n" 416 | source_file += "\n end\n" 417 | 418 | # Generate the payload 419 | source_file += "\n def payload\n" 420 | 421 | optional_params = [] 422 | params.each do |param| 423 | unless param[:required] 424 | optional_params.push(param) 425 | source_file += " self.#{param[:name]} ||= #{empty_value_from_type(param[:type])}\n" 426 | end 427 | end 428 | source_file += "\n" 429 | 430 | source_file += " payload = [self.class.type]\n" 431 | optional_count = 0 432 | params.each do |param| 433 | if param[:required] 434 | source_file += " payload.push(self.#{param[:name]})\n" 435 | else 436 | optional_count += 1 437 | source_file += "\n return payload if (self.#{param[:name]}.empty?" 438 | 439 | # Insert remaining parameters 440 | for i in optional_count..(optional_params.size-1) do 441 | source_file += " and self.#{optional_params[i][:name]}.empty?" 442 | end 443 | 444 | source_file += ")\n" 445 | source_file += " payload.push(self.#{param[:name]})\n" 446 | end 447 | end 448 | source_file += "\n payload\n" 449 | source_file += " end\n" 450 | 451 | # Generate the string 452 | source_file += "\n def to_s\n" 453 | source_file += " '#{message[:name].upcase} > ' + self.payload.to_s\n" 454 | source_file += " end\n" 455 | 456 | 457 | source_file += "\n end\n" 458 | 459 | ############################################### 460 | # Test File 461 | ############################################### 462 | 463 | value_array = [] 464 | params.each do |param| 465 | if param[:required] 466 | value_array.push(value_from_type(param[:type])) 467 | end 468 | end 469 | 470 | class_name = "Wamp::Client::Message::#{message[:name].capitalize}" 471 | 472 | test_file += "\n describe #{class_name} do\n" 473 | 474 | # Generate Constructor Test 475 | test_file += "\n it 'creates the message object' do\n" 476 | test_file += " params = [#{value_array.join(',')}]\n" 477 | test_file += " object = #{class_name}.new(*params)\n\n" 478 | params.each do |param| 479 | if param[:required] 480 | test_file += " expect(object.#{param[:name]}).to eq(#{value_from_type(param[:type])})\n" 481 | end 482 | end 483 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 484 | test_file += " end\n" 485 | 486 | # Generate Parser Test 487 | test_file += "\n it 'parses the message and creates an object' do\n" 488 | test_file += " params = [#{message_type_lookup[message[:name].upcase]},#{value_array.join(',')}]\n" 489 | test_file += " object = #{class_name}.parse(params)\n\n" 490 | params.each do |param| 491 | if param[:required] 492 | test_file += " expect(object.#{param[:name]}).to eq(#{value_from_type(param[:type])})\n" 493 | end 494 | end 495 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 496 | test_file += " end\n" 497 | 498 | # Generate Global Parser Test 499 | test_file += "\n it 'globally parses the message and creates an object' do\n" 500 | test_file += " params = [#{message_type_lookup[message[:name].upcase]},#{value_array.join(',')}]\n" 501 | test_file += " object = Wamp::Client::Message.parse(params)\n\n" 502 | params.each do |param| 503 | if param[:required] 504 | test_file += " expect(object.#{param[:name]}).to eq(#{value_from_type(param[:type])})\n" 505 | end 506 | end 507 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 508 | test_file += " end\n" 509 | 510 | # Generate Payload Test 511 | test_file += "\n it 'generates the payload' do\n" 512 | test_file += " params = [#{value_array.join(',')}]\n" 513 | test_file += " object = #{class_name}.new(*params)\n" 514 | test_file += " payload = object.payload\n\n" 515 | count = 0 516 | test_file += " expect(payload.count).to eq(#{value_array.count+1})\n" 517 | test_file += " expect(payload[0]).to eq(#{message_type_lookup[message[:name].upcase]})\n" 518 | value_array.each do |value| 519 | test_file += " expect(payload[#{count+1}]).to eq(#{value})\n" 520 | count += 1 521 | end 522 | test_file += " end\n" 523 | 524 | number_of_optional_params = 0 525 | 526 | # Generate non-required parameter tests 527 | params.each do |param| 528 | unless param[:required] 529 | number_of_optional_params += 1 530 | 531 | temp_value_array = Array.new(value_array) 532 | temp_value_array.push(value_from_type(param[:type])) 533 | 534 | test_file += "\n describe 'checks optional parameter #{param[:name]}' do\n" 535 | 536 | # Generate Constructor Test 537 | test_file += "\n it 'creates the message object' do\n" 538 | test_file += " params = [#{temp_value_array.join(',')}]\n" 539 | test_file += " object = #{class_name}.new(*params)\n\n" 540 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 541 | test_file += " end\n" 542 | 543 | # Generate Parser Test 544 | test_file += "\n it 'parses the message and creates an object' do\n" 545 | test_file += " params = [#{message_type_lookup[message[:name].upcase]},#{temp_value_array.join(',')}]\n" 546 | test_file += " object = #{class_name}.parse(params)\n\n" 547 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 548 | test_file += " end\n" 549 | 550 | # Generate Payload Test 551 | test_file += "\n it 'generates the payload' do\n" 552 | test_file += " params = [#{temp_value_array.join(',')}]\n" 553 | test_file += " object = #{class_name}.new(*params)\n" 554 | test_file += " payload = object.payload\n\n" 555 | count = 0 556 | test_file += " expect(payload.count).to eq(#{temp_value_array.count+1})\n" 557 | test_file += " expect(payload[0]).to eq(#{message_type_lookup[message[:name].upcase]})\n" 558 | temp_value_array.each do |value| 559 | test_file += " expect(payload[#{count+1}]).to eq(#{value})\n" 560 | count += 1 561 | end 562 | test_file += " end\n" 563 | 564 | test_file += "\n end\n" 565 | 566 | value_array.push(empty_value_from_type(param[:type])) 567 | end 568 | end 569 | 570 | ## Test the final one and make sure they omit 571 | if number_of_optional_params > 0 572 | 573 | # Generate check params 574 | check_params = [] 575 | for i in 0..(value_array.size-number_of_optional_params-1) do 576 | check_params.push(value_array[i]) 577 | end 578 | 579 | test_file += "\n describe 'checks optional parameters' do\n" 580 | 581 | # Generate Constructor Test 582 | test_file += "\n it 'creates the message object' do\n" 583 | test_file += " params = [#{value_array.join(',')}]\n" 584 | test_file += " object = #{class_name}.new(*params)\n\n" 585 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 586 | test_file += " end\n" 587 | 588 | # Generate Parser Test 589 | test_file += "\n it 'parses the message and creates an object' do\n" 590 | test_file += " params = [#{message_type_lookup[message[:name].upcase]},#{value_array.join(',')}]\n" 591 | test_file += " object = #{class_name}.parse(params)\n\n" 592 | test_file += " expect(object.is_a?(#{class_name})).to eq(true)\n" 593 | test_file += " end\n" 594 | 595 | # Generate Payload Test 596 | test_file += "\n it 'generates the payload' do\n" 597 | test_file += " params = [#{value_array.join(',')}]\n" 598 | test_file += " object = #{class_name}.new(*params)\n" 599 | test_file += " payload = object.payload\n\n" 600 | count = 0 601 | test_file += " expect(payload.count).to eq(#{check_params.count+1})\n" 602 | test_file += " expect(payload[0]).to eq(#{message_type_lookup[message[:name].upcase]})\n" 603 | check_params.each do |value| 604 | test_file += " expect(payload[#{count+1}]).to eq(#{value})\n" 605 | count += 1 606 | end 607 | test_file += " end\n" 608 | 609 | test_file += "\n end\n" 610 | 611 | end 612 | 613 | test_file += "\n end\n" 614 | 615 | end 616 | 617 | source_file += source_file_footer 618 | test_file += test_file_footer 619 | 620 | File.open('message.rb.tmp', 'w') { |file| file.write(source_file) } 621 | File.open('message_spec.rb.tmp', 'w') { |file| file.write(test_file) } 622 | 623 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'wamp/client' 5 | 6 | if ENV['CODECOV_TOKEN'] 7 | require 'codecov' 8 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 9 | end 10 | 11 | Dir[File.expand_path('spec/support/**/*.rb')].each { |f| require f } 12 | 13 | require "rspec/em" 14 | 15 | Wamp::Client.log_level = :error 16 | 17 | -------------------------------------------------------------------------------- /spec/support/faye_web_socket_client_stub.rb: -------------------------------------------------------------------------------- 1 | class FayeWebSocketClientStub 2 | class Event 3 | attr_accessor :data, :reason 4 | end 5 | 6 | attr_accessor :last_message 7 | 8 | def initialize 9 | EM.add_timer(1) { 10 | @on_open.call(Event.new) if @on_open != nil 11 | } 12 | end 13 | 14 | @on_open 15 | @on_message 16 | @on_close 17 | def on(event, &block) 18 | if event == :open 19 | @on_open = block 20 | elsif event == :close 21 | @on_close = block 22 | elsif event == :message 23 | @on_message = block 24 | end 25 | end 26 | 27 | def close 28 | event = Event.new 29 | event.reason = 'closed' 30 | @on_close.call(event) if @on_close != nil 31 | end 32 | 33 | def send(message) 34 | self.last_message = message 35 | end 36 | 37 | def receive(message) 38 | event = Event.new 39 | event.data = message 40 | @on_message.call(event) if @on_message != nil 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/test_transport.rb: -------------------------------------------------------------------------------- 1 | class TestTransport < Wamp::Client::Transport::EventMachineBase 2 | @@event_machine_on = false 3 | attr_accessor :messages 4 | 5 | def initialize(options) 6 | super(options) 7 | @connected = true 8 | self.messages = [] 9 | end 10 | 11 | def connect 12 | self.add_timer(1000) do 13 | trigger :open 14 | end 15 | end 16 | 17 | def disconnect 18 | @connected = false 19 | trigger :close 20 | end 21 | 22 | def self.start_event_machine(&block) 23 | @@event_machine_on = true 24 | block.call 25 | end 26 | 27 | def self.stop_event_machine 28 | @@event_machine_on = false 29 | end 30 | 31 | def self.event_machine_on? 32 | @@event_machine_on 33 | end 34 | 35 | def send_message(msg) 36 | self.messages.push(msg) 37 | end 38 | 39 | def receive_message(msg) 40 | 41 | # Emulate serialization/deserialization 42 | serialize = self.serializer.serialize(msg) 43 | deserialize = self.serializer.deserialize(serialize) 44 | 45 | # Call the received message 46 | trigger :message, deserialize 47 | end 48 | 49 | end 50 | 51 | -------------------------------------------------------------------------------- /spec/support/web_socket_event_machine_client_stub.rb: -------------------------------------------------------------------------------- 1 | class WebSocketEventMachineClientStub 2 | attr_accessor :last_message 3 | 4 | def initialize 5 | EM.add_timer(1) { 6 | @onopen.call if @onopen != nil 7 | } 8 | end 9 | 10 | @onopen 11 | def onopen(&onopen) 12 | @onopen = onopen 13 | end 14 | 15 | @onmessage 16 | def onmessage(&onmessage) 17 | @onmessage = onmessage 18 | end 19 | 20 | @onclose 21 | def onclose(&onclose) 22 | @onclose = onclose 23 | end 24 | 25 | def close 26 | @onclose.call if @onclose != nil 27 | true 28 | end 29 | 30 | def send(message, type) 31 | self.last_message = message 32 | end 33 | 34 | def receive(message) 35 | @onmessage.call(message, {type:'text'}) if @onmessage != nil 36 | end 37 | 38 | end 39 | 40 | -------------------------------------------------------------------------------- /spec/wamp/client/auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Wamp::Client::Auth do 4 | 5 | describe 'cra' do 6 | 7 | it 'generates the signature' do 8 | 9 | challenge = "{ \"nonce\": \"LHRTC9zeOIrt_9U3\", \"authprovider\": \"userdb\", \"authid\": \"peter\", \"timestamp\": \"2014-06-22T16:36:25.448Z\", \"authrole\": \"user\", \"authmethod\": \"wampcra\", \"session\": 3251278072152162}" 10 | secret = 'secret' 11 | signature = Wamp::Client::Auth::Cra.sign(secret, challenge) 12 | expect(signature).to eq('Pji30JC9tb/T9tbEwxw5i0RyRa5UVBxuoIVTgT7hnkE=') 13 | 14 | end 15 | 16 | end 17 | 18 | end -------------------------------------------------------------------------------- /spec/wamp/client/check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Wamp::Client::Check do 4 | 5 | class DummyClass 6 | include Wamp::Client::Check 7 | end 8 | 9 | describe '#check_equal' do 10 | 11 | it 'raises exception when value does not equal expected' do 12 | expect { DummyClass.check_equal('test', false, true) }.to raise_exception("The 'test' argument must have the value 'false'. Instead the value was 'true'") 13 | end 14 | 15 | it 'does not raise exception when value equals expected' do 16 | expect { DummyClass.check_equal('test', true, true) }.not_to raise_exception 17 | end 18 | 19 | end 20 | 21 | describe '#check_gte' do 22 | 23 | it 'raises exception when value is less than expected' do 24 | expect { DummyClass.check_gte('test', 3, 2) }.to raise_exception("The 'test' argument must be greater than or equal to '3'. Instead the value was '2'") 25 | end 26 | 27 | it 'does not raise exception when value equals expected' do 28 | expect { DummyClass.check_gte('test', 3, 3) }.not_to raise_exception 29 | end 30 | 31 | it 'does not raise exception when value is greater than expected' do 32 | expect { DummyClass.check_gte('test', 3, 4) }.not_to raise_exception 33 | end 34 | 35 | end 36 | 37 | describe '#check_nil' do 38 | 39 | it 'raises exception when nil and nil is not allowed' do 40 | expect { DummyClass.check_nil('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 41 | end 42 | 43 | it 'does not raise exception when nil and nil is allowed' do 44 | expect { DummyClass.check_nil('test', nil, true) }.not_to raise_exception 45 | end 46 | 47 | it 'does not raise exception when not nil and nil is not allowed' do 48 | expect { DummyClass.check_nil('test', 'value', false) }.not_to raise_exception 49 | end 50 | 51 | it 'does not raise exception when not nil and nil is allowed' do 52 | expect { DummyClass.check_nil('test', 'value', true) }.not_to raise_exception 53 | end 54 | 55 | end 56 | 57 | describe '#check_integer' do 58 | 59 | it 'raises exception when nil and nil is not allowed' do 60 | expect { DummyClass.check_int('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 61 | end 62 | 63 | it 'does not raise exception when nil and nil is allowed' do 64 | expect { DummyClass.check_int('test', nil, true) }.not_to raise_exception 65 | end 66 | 67 | it 'raises exception when not an integer' do 68 | expect { DummyClass.check_int('test', '1') }.to raise_exception("The 'test' argument must be an integer") 69 | end 70 | 71 | it 'does not raise exception when it is an integer' do 72 | expect { DummyClass.check_int('test', 1) }.not_to raise_exception 73 | end 74 | 75 | end 76 | 77 | describe '#check_string' do 78 | 79 | it 'raises exception when nil and nil is not allowed' do 80 | expect { DummyClass.check_string('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 81 | end 82 | 83 | it 'does not raise exception when nil and nil is allowed' do 84 | expect { DummyClass.check_string('test', nil, true) }.not_to raise_exception 85 | end 86 | 87 | it 'raises exception when not an integer' do 88 | expect { DummyClass.check_string('test', 1) }.to raise_exception("The 'test' argument must be a string") 89 | end 90 | 91 | it 'does not raise exception when it is an integer' do 92 | expect { DummyClass.check_string('test', '1') }.not_to raise_exception 93 | end 94 | 95 | end 96 | 97 | describe '#check_bool' do 98 | 99 | it 'raises exception when nil and nil is not allowed' do 100 | expect { DummyClass.check_bool('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 101 | end 102 | 103 | it 'does not raise exception when nil and nil is allowed' do 104 | expect { DummyClass.check_bool('test', nil, true) }.not_to raise_exception 105 | end 106 | 107 | it 'raises exception when not a boolean' do 108 | expect { DummyClass.check_bool('test', 1) }.to raise_exception("The 'test' argument must be a boolean") 109 | end 110 | 111 | it 'does not raise exception when it is a boolean' do 112 | expect { DummyClass.check_bool('test', true) }.not_to raise_exception 113 | end 114 | 115 | end 116 | 117 | describe '#check_dict' do 118 | 119 | it 'raises exception when nil and nil is not allowed' do 120 | expect { DummyClass.check_dict('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 121 | end 122 | 123 | it 'does not raise exception when nil and nil is allowed' do 124 | expect { DummyClass.check_dict('test', nil, true) }.not_to raise_exception 125 | end 126 | 127 | it 'raises exception when not a hash' do 128 | expect { DummyClass.check_dict('test', 1) }.to raise_exception("The 'test' argument must be a hash") 129 | end 130 | 131 | it 'does not raise exception when it is a hash' do 132 | expect { DummyClass.check_dict('test', {}) }.not_to raise_exception 133 | end 134 | 135 | end 136 | 137 | describe '#check_list' do 138 | 139 | it 'raises exception when nil and nil is not allowed' do 140 | expect { DummyClass.check_list('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 141 | end 142 | 143 | it 'does not raise exception when nil and nil is allowed' do 144 | expect { DummyClass.check_list('test', nil, true) }.not_to raise_exception 145 | end 146 | 147 | it 'raises exception when not a hash' do 148 | expect { DummyClass.check_list('test', 1) }.to raise_exception("The 'test' argument must be an array") 149 | end 150 | 151 | it 'does not raise exception when it is a hash' do 152 | expect { DummyClass.check_list('test', []) }.not_to raise_exception 153 | end 154 | 155 | end 156 | 157 | describe '#check_id' do 158 | 159 | it 'raises exception when nil and nil is not allowed' do 160 | expect { DummyClass.check_id('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 161 | end 162 | 163 | it 'does not raise exception when nil and nil is allowed' do 164 | expect { DummyClass.check_id('test', nil, true) }.not_to raise_exception 165 | end 166 | 167 | it 'raises exception when not an integer' do 168 | expect { DummyClass.check_id('test', '1') }.to raise_exception("The 'test' argument must be an integer") 169 | end 170 | 171 | it 'does not raise exception when it is an integer' do 172 | expect { DummyClass.check_id('test', 1) }.not_to raise_exception 173 | end 174 | 175 | end 176 | 177 | describe '#check_uri' do 178 | 179 | it 'raises exception when nil and nil is not allowed' do 180 | expect { DummyClass.check_uri('test', nil, false) }.to raise_exception("The 'test' argument cannot be nil") 181 | end 182 | 183 | it 'does not raise exception when nil and nil is allowed' do 184 | expect { DummyClass.check_uri('test', nil, true) }.not_to raise_exception 185 | end 186 | 187 | it 'raises exception when not an integer' do 188 | expect { DummyClass.check_uri('test', 1) }.to raise_exception("The 'test' argument must be a string") 189 | end 190 | 191 | it 'does not raise exception when it is an integer' do 192 | expect { DummyClass.check_uri('test', '1') }.not_to raise_exception 193 | end 194 | 195 | end 196 | 197 | end -------------------------------------------------------------------------------- /spec/wamp/client/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Wamp::Client::Connection do 4 | include RSpec::EM::FakeClock 5 | 6 | before { clock.stub } 7 | after { clock.reset } 8 | 9 | before(:each) { TestTransport.stop_event_machine } 10 | 11 | let(:options) { 12 | { 13 | uri: 'wss://example.com', 14 | realm: 'realm1', 15 | transport: TestTransport, 16 | } 17 | } 18 | let(:connection) { described_class.new(options) } 19 | let(:transport) { connection.transport } 20 | let(:session) { connection.session } 21 | 22 | def open_connection 23 | connection.open 24 | clock.tick(1) 25 | end 26 | 27 | def open_session_from_server 28 | # Send welcome form server 29 | welcome = Wamp::Client::Message::Welcome.new(1234, {}) 30 | transport.receive_message(welcome.payload) 31 | end 32 | 33 | def close_session_from_server 34 | # Send goodbye from server 35 | goodbye = Wamp::Client::Message::Goodbye.new({}, 'felt.like.it') 36 | transport.receive_message(goodbye.payload) 37 | end 38 | 39 | def check_em_on 40 | expect(TestTransport.event_machine_on?).to eq(true) 41 | end 42 | 43 | def check_em_off 44 | expect(TestTransport.event_machine_on?).to eq(false) 45 | end 46 | 47 | describe 'transport' do 48 | it 'selects the default transport' do 49 | connection = described_class.new({}) 50 | expect(connection.transport_class).to be(Wamp::Client::Transport::WebSocketEventMachine) 51 | end 52 | 53 | it 'overrides the default transport' do 54 | connection = described_class.new({ transport: Wamp::Client::Transport::FayeWebSocket }) 55 | expect(connection.transport_class).to be(Wamp::Client::Transport::FayeWebSocket) 56 | end 57 | end 58 | 59 | it 'opens the transport/session and sends the hello message' do 60 | called = false 61 | connection.on(:connect) do 62 | called = true 63 | end 64 | 65 | open_connection 66 | 67 | expect(transport).not_to be_nil 68 | expect(session).not_to be_nil 69 | expect(transport.messages.count).to eq(1) 70 | expect(transport.messages[0][0]).to eq(Wamp::Client::Message::Types::HELLO) 71 | 72 | expect(called).to eq(true) 73 | end 74 | 75 | it 'opens the transport/session and joins' do 76 | called = false 77 | connection.on(:join) do 78 | called = true 79 | end 80 | 81 | open_connection 82 | open_session_from_server 83 | 84 | check_em_on 85 | expect(called).to eq(true) 86 | end 87 | 88 | it 'closes the connection' do 89 | left = false 90 | connection.on(:leave) do 91 | left = true 92 | end 93 | disconnected = false 94 | connection.on(:disconnect) do 95 | disconnected = true 96 | end 97 | 98 | open_connection 99 | open_session_from_server 100 | 101 | connection.close 102 | 103 | # Nothing happens until the server responds 104 | check_em_on 105 | expect(left).to eq(false) 106 | expect(disconnected).to eq(false) 107 | 108 | close_session_from_server 109 | 110 | check_em_off 111 | expect(left).to eq(true) 112 | expect(disconnected).to eq(true) 113 | end 114 | 115 | it 'retries if the session is closed from the server' do 116 | left = false 117 | connection.on(:leave) do 118 | left = true 119 | end 120 | 121 | joined = false 122 | connection.on(:join) do 123 | joined = true 124 | end 125 | 126 | open_connection 127 | open_session_from_server 128 | 129 | check_em_on 130 | expect(joined).to eq(true) 131 | joined = false 132 | expect(left).to eq(false) 133 | 134 | close_session_from_server 135 | 136 | check_em_on 137 | expect(joined).to eq(false) 138 | expect(left).to eq(true) 139 | left = false 140 | 141 | clock.tick(5) 142 | open_session_from_server 143 | 144 | check_em_on 145 | 146 | expect(joined).to eq(true) 147 | joined = false 148 | expect(left).to eq(false) 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/wamp/client/transport_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require "rspec/em" 3 | require 'websocket-eventmachine-client' 4 | require 'faye/websocket' 5 | 6 | describe Wamp::Client::Transport do 7 | describe Wamp::Client::Transport::EventMachineBase do 8 | it '#start/stop' do 9 | value = 0 10 | described_class.start_event_machine do 11 | EM.tick_loop do 12 | value += 1 13 | described_class.stop_event_machine if value == 10 14 | end 15 | end 16 | 17 | expect(value).to eq(10) 18 | end 19 | 20 | it '#add_tick_loop' do 21 | value = 0 22 | described_class.start_event_machine do 23 | described_class.add_tick_loop do 24 | value += 1 25 | described_class.stop_event_machine if value == 10 26 | end 27 | end 28 | 29 | expect(value).to eq(10) 30 | end 31 | 32 | context '#add_timer' do 33 | include RSpec::EM::FakeClock 34 | 35 | before { clock.stub } 36 | after { clock.reset } 37 | 38 | it 'adds a timer with the class method' do 39 | value = 0 40 | described_class.add_timer(3000) do 41 | value = 1 42 | end 43 | 44 | expect(value).to eq(0) 45 | clock.tick(1) 46 | expect(value).to eq(0) 47 | clock.tick(1) 48 | expect(value).to eq(0) 49 | clock.tick(1) 50 | expect(value).to eq(1) 51 | end 52 | 53 | it 'adds a timer with the instance method' do 54 | transport = described_class.new({}) 55 | 56 | value = 0 57 | transport.add_timer(2000) do 58 | value = 1 59 | end 60 | 61 | expect(value).to eq(0) 62 | clock.tick(1) 63 | expect(value).to eq(0) 64 | clock.tick(1) 65 | expect(value).to eq(1) 66 | end 67 | end 68 | end 69 | 70 | context 'transports' do 71 | let(:test_uri) { 'wss://router.examples.com' } 72 | let(:test_proxy) { 'http://proxy.examples.com' } 73 | let(:options) { 74 | { 75 | uri: test_uri, 76 | headers: {} 77 | } 78 | } 79 | 80 | describe Wamp::Client::Transport::WebSocketEventMachine do 81 | include RSpec::EM::FakeClock 82 | before { clock.stub } 83 | after { clock.reset } 84 | 85 | let(:transport) { described_class.new(options) } 86 | let(:socket) { transport.socket } 87 | before(:each) { 88 | allow(WebSocket::EventMachine::Client).to receive(:connect) { |options| 89 | WebSocketEventMachineClientStub.new 90 | } 91 | 92 | transport.connect 93 | clock.tick(1) # Simulate connecting 94 | } 95 | 96 | it 'initializes' do 97 | expect(transport.uri).to eq(test_uri) 98 | end 99 | 100 | it 'connects to the router' do 101 | expect(transport.connected?).to eq(true) 102 | end 103 | 104 | it 'disconnects from the router' do 105 | value = false 106 | transport.on(:close) do 107 | value = true 108 | end 109 | 110 | transport.disconnect 111 | 112 | expect(transport.connected?).to eq(false) 113 | expect(value).to eq(true) 114 | end 115 | 116 | it 'sends a message' do 117 | transport.send_message({test: 'value'}) 118 | expect(socket.last_message).to eq('{"test":"value"}') 119 | end 120 | 121 | it 'receives a message' do 122 | message = nil 123 | transport.on(:message) do |msg, type| 124 | message = msg 125 | end 126 | 127 | socket.receive('{"test":"value"}') 128 | expect(message).to eq({test: 'value'}) 129 | end 130 | 131 | it 'raises exception if sending message when closed' do 132 | transport.disconnect 133 | expect { 134 | transport.send_message({test: 'value'}) 135 | }.to raise_error(RuntimeError) 136 | end 137 | 138 | it 'raises exception if proxy is included' do 139 | expect { 140 | options[:proxy] = { origin: 'something', headers: 'something' } 141 | described_class.new(options) 142 | }.to raise_error(RuntimeError) 143 | end 144 | end 145 | 146 | describe Wamp::Client::Transport::FayeWebSocket do 147 | include RSpec::EM::FakeClock 148 | before { clock.stub } 149 | after { clock.reset } 150 | 151 | let(:transport) { described_class.new(options) } 152 | let(:socket) { transport.socket } 153 | before(:each) { 154 | allow(Faye::WebSocket::Client).to receive(:new) { |uri, protocols, options| 155 | FayeWebSocketClientStub.new 156 | } 157 | 158 | transport.connect 159 | clock.tick(1) # Simulate connecting 160 | } 161 | 162 | it 'initializes' do 163 | expect(transport.uri).to eq(test_uri) 164 | end 165 | 166 | it 'connects to the router' do 167 | expect(transport.connected?).to eq(true) 168 | end 169 | 170 | it 'disconnects from the router' do 171 | value = false 172 | transport.on(:close) do 173 | value = true 174 | end 175 | 176 | transport.disconnect 177 | 178 | expect(transport.connected?).to eq(false) 179 | expect(value).to eq(true) 180 | end 181 | 182 | it 'sends a message' do 183 | transport.send_message({test: 'value'}) 184 | expect(socket.last_message).to eq('{"test":"value"}') 185 | end 186 | 187 | it 'receives a message' do 188 | message = nil 189 | transport.on(:message) do |msg, type| 190 | message = msg 191 | end 192 | 193 | socket.receive('{"test":"value"}') 194 | expect(message).to eq({test: 'value'}) 195 | end 196 | 197 | it 'raises exception if sending message when closed' do 198 | transport.disconnect 199 | expect { 200 | transport.send_message({test: 'value'}) 201 | }.to raise_error(RuntimeError) 202 | end 203 | 204 | it 'does not raise exception if proxy is included' do 205 | expect { 206 | options[:proxy] = { origin: 'something', headers: 'something' } 207 | described_class.new(options) 208 | }.not_to raise_error 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) -------------------------------------------------------------------------------- /wamp_client.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'wamp/client/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'wamp_client' 8 | spec.version = Wamp::Client::VERSION 9 | spec.authors = ['Eric Chapman'] 10 | spec.email = ['eric.chappy@gmail.com'] 11 | spec.summary = %q{Web Application Messaging Protocol Client} 12 | spec.description = %q{An implementation of The Web Application Messaging Protocol (WAMP)} 13 | spec.homepage = 'https://github.com/ericchapman/ruby_wamp_client' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.7' 22 | spec.add_development_dependency 'rake', '~> 10.0' 23 | spec.add_development_dependency 'rspec', '~> 3.5' 24 | spec.add_development_dependency 'rspec-eventmachine', '~> 0.2' 25 | spec.add_development_dependency 'simplecov', '~> 0.12' 26 | spec.add_development_dependency 'codecov', '~> 0.1.9' 27 | spec.add_development_dependency 'faye-websocket', '~> 0.10.4' 28 | 29 | spec.required_ruby_version = '>= 2.0' 30 | 31 | spec.add_dependency 'websocket-eventmachine-client', '~> 1.1' 32 | spec.add_dependency 'json', '~> 2.0' 33 | end 34 | --------------------------------------------------------------------------------