orator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +19 -0
- data/README.md +85 -0
- data/Rakefile +0 -0
- data/bin/orator +10 -0
- data/lib/orator/cli.rb +163 -0
- data/lib/orator/client.rb +44 -0
- data/lib/orator/engine.rb +5 -0
- data/lib/orator/event_handler.rb +97 -0
- data/lib/orator/handlers/base.rb +158 -0
- data/lib/orator/handlers.rb +9 -0
- data/lib/orator/middle_ground.rb +38 -0
- data/lib/orator/server.rb +70 -0
- data/lib/orator/version.rb +5 -0
- data/lib/orator.rb +21 -0
- data/spec/base_handler_spec.rb +45 -0
- data/spec/client_spec.rb +14 -0
- data/spec/event_handler_spec.rb +62 -0
- data/spec/middle_ground_spec.rb +19 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MDJhNWFlYjk5NjdjZTI1Nzk5MGEzMzVhMjc5N2NhNjRkY2JkMGVlZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
Zjk5YzBmNjNiMzFhZTNkMGNlYTZiYmMxN2ExMWRkNTAwMDk4OWU1MQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZWE2N2Y4ZTU3ZWM4ZDRiNmZhMWQyNGRlY2RkNzQ5ZWVhMTRhZmUyODc1ZGMy
|
10
|
+
ZjYwMWY4YWU3Y2FjN2QxOGQ2YzBiMzQ4MzQ1YmVhOWZhMjk0NDNkYzNhYzgx
|
11
|
+
MTk2ZWQxMjdmMGViZmVlODg3ZTU3MmIwOTFmMThmOGQ4NDE1OGY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NzJhODQzZWNhZDhkZDU3NGViN2ZiYmYzYTY4NDU3NTVkZWQ0ZGE3YmJjYzc3
|
14
|
+
ZTkxYTJlOGExZjI5MmQ0NzFhNDI0MGE2YTBhZGNmOTRhOWE0Mjg0YzA0MzU2
|
15
|
+
YTFjYTM0ZmE1ODcwYTFiZmM1MWI4OTQyNjgxMjUyMGZiNDQ3ODU=
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013 Jeremy Rodi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Orator [![Code Climate](https://codeclimate.com/github/redjazz96/orator.png)](https://codeclimate.com/github/redjazz96/orator) [![Build Status](https://travis-ci.org/redjazz96/orator.png?branch=master)](https://travis-ci.org/redjazz96/orator)
|
2
|
+
|
3
|
+
Orator is a websocket client/server combination that uses events in order to
|
4
|
+
talk to each other. It is still early in its development.
|
5
|
+
|
6
|
+
This project uses [web-socket-js](https://github.com/gimite/web-socket-js/) as
|
7
|
+
well as [em-websocket](https://github.com/igrigorik/em-websocket).
|
8
|
+
|
9
|
+
## Ruby on Rails Integration
|
10
|
+
Orator integrates easily with Ruby on Rails. Just add
|
11
|
+
|
12
|
+
gem 'orator', :github => "redjazz96/orator"
|
13
|
+
|
14
|
+
to your Gemfile and `bundle install` away. In order to use it, you'll need to
|
15
|
+
add the following line to your `application.js` file:
|
16
|
+
|
17
|
+
//= require orator
|
18
|
+
|
19
|
+
Now you can use the orator client in your JavaScript! If the browser doesn't
|
20
|
+
support JavaScript, it'll fall back to a flash object.
|
21
|
+
|
22
|
+
## JavaScript Client
|
23
|
+
Let's go through an example together.
|
24
|
+
|
25
|
+
$(document).ready(function() {
|
26
|
+
Orator.setup("ws://host:port/path?query", function(events){
|
27
|
+
|
28
|
+
`Orator.setup` handles setting up our socket for us. It also sets up the
|
29
|
+
events and routing, as well. There are some locally defined events that are
|
30
|
+
triggered even if the server hadn't sent them, such as `socket.open` and
|
31
|
+
`socket.close`.
|
32
|
+
|
33
|
+
events.on('some.event', function() {})
|
34
|
+
|
35
|
+
Here we're binding some event named `some.event` to an empty function. Boring.
|
36
|
+
Note that the name of the event doesn't matter here.
|
37
|
+
|
38
|
+
events.on('test.ping', function() {
|
39
|
+
this.server.send('test.pong', { message: 'pong' });
|
40
|
+
});
|
41
|
+
|
42
|
+
Here we're responding to a ping that a server might send to this client. We
|
43
|
+
send back a `test.pong` event, with the message 'pong'. The contents of the
|
44
|
+
event can be anything.
|
45
|
+
|
46
|
+
events.on('test.pong', function(data) {
|
47
|
+
console.log(data.message) # => "pong"
|
48
|
+
});
|
49
|
+
|
50
|
+
Here we received a pong back from the server - this was triggered by the server
|
51
|
+
in response to our ping, which we could have sent at any time.
|
52
|
+
|
53
|
+
events.on('socket.open', function() {
|
54
|
+
this.user = {}
|
55
|
+
|
56
|
+
this.server.send('user.alias', { name: some_name })
|
57
|
+
});
|
58
|
+
|
59
|
+
Here we defined an event that will set up a an empty object on the property
|
60
|
+
user of `this`. This will become important. We then send the event
|
61
|
+
`user.alias` to the server (which probably changes the name of the user) with
|
62
|
+
the new name (assuming `some_name` has a string value).
|
63
|
+
|
64
|
+
events.on('user.alias', function(data) {
|
65
|
+
this.user.name = data.name;
|
66
|
+
});
|
67
|
+
|
68
|
+
It is common place for the server's response events to be named the same as the
|
69
|
+
request events (although the response events may be sent at any time, with no
|
70
|
+
context). Here we see setting the name of the user object to the name the
|
71
|
+
server sent back. This is the same user object that we defined in the event
|
72
|
+
above.
|
73
|
+
|
74
|
+
});
|
75
|
+
|
76
|
+
});
|
77
|
+
|
78
|
+
And we close our braces.
|
79
|
+
|
80
|
+
## Server Stuff
|
81
|
+
I'm not entirely sure how to set this part up... any ideas?
|
82
|
+
|
83
|
+
I'm trying to provide support for all types of rails applications, including
|
84
|
+
those on passenger. I'll probably require a settings file in
|
85
|
+
`Rails.root/config` for orator...
|
data/Rakefile
ADDED
File without changes
|
data/bin/orator
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
library_path = File.expand_path("../../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift library_path unless $LOAD_PATH.include? library_path
|
5
|
+
require "orator"
|
6
|
+
require "orator/cli"
|
7
|
+
|
8
|
+
cli = Orator::CLI.new
|
9
|
+
cli.parse_arguments ARGV
|
10
|
+
cli.handle_command
|
data/lib/orator/cli.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Orator
|
5
|
+
|
6
|
+
# Handles the command line interface for Orator.
|
7
|
+
class CLI
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
:command => :help,
|
11
|
+
:file => "./orator_config.yml",
|
12
|
+
:daemonize => false
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@options = {}.merge DEFAULT_OPTIONS
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_arguments(args)
|
20
|
+
OptionParser.new do |opts|
|
21
|
+
|
22
|
+
opts.on('--config FILE', "Loads the configuration settings from FILE.") do |file|
|
23
|
+
@options[:file] = file
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('-cCOMMAND', '--command COMMAND', "The command to run.") do |command|
|
27
|
+
@options[:command] = command
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('-D', '--[no-]daemonize', "Whether or not to daemonize the process.") do |d|
|
31
|
+
@options[:daemonize] = d
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('-h', '--help', "Shows this message.") { puts opts; exit }
|
35
|
+
opts.on('-v', '--version', "Shows the version of orator.") do
|
36
|
+
puts Orator::VERSION
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.separator ""
|
41
|
+
opts.separator "Valid Commands:"
|
42
|
+
opts.separator "\tstart: start the orator server."
|
43
|
+
opts.separator "\tstop: stop the orator server."
|
44
|
+
end.parse!(args)
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_command
|
48
|
+
Orator.debug = yaml_options[:debug]
|
49
|
+
send(@options[:command])
|
50
|
+
end
|
51
|
+
|
52
|
+
def start
|
53
|
+
daemonize? do
|
54
|
+
load_handlers
|
55
|
+
server = Orator::Server.new(yaml_options[:server_options])
|
56
|
+
handlers = self.class.handlers
|
57
|
+
|
58
|
+
puts "Starting server..." if Orator.debug
|
59
|
+
server.run do
|
60
|
+
handlers.each do |handler|
|
61
|
+
if handler.is_a? Proc
|
62
|
+
handler.call(self)
|
63
|
+
else
|
64
|
+
handler.register_with(self)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def stop
|
72
|
+
if File.exists? yaml_options[:pid_file]
|
73
|
+
Process.kill 1, File.open(yaml_options[:pid_file], 'r').read.to_i
|
74
|
+
puts "Stopped Orator."
|
75
|
+
File.unlink yaml_options[:pid_file]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add a handler to be used by the server.
|
80
|
+
#
|
81
|
+
# @param klass [Class] the class to use as a handler.
|
82
|
+
# @return [Array<Class>] a list of handlers that will be used for the
|
83
|
+
# server.
|
84
|
+
def self.add_handler(klass = nil, &block)
|
85
|
+
handlers << (klass || block)
|
86
|
+
end
|
87
|
+
|
88
|
+
# A list of handlers.
|
89
|
+
#
|
90
|
+
# @return [Array<Class>] the handlers.
|
91
|
+
def self.handlers
|
92
|
+
@handlers ||= []
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Grab the options from the options file.
|
98
|
+
#
|
99
|
+
# @returns [Hash]
|
100
|
+
def yaml_options
|
101
|
+
@yaml_options ||= YAML::load_file @options[:file]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Checks the PID file (assuming it exists) to see if the process is still
|
105
|
+
# running.
|
106
|
+
#
|
107
|
+
# @raises [ProcessExistsError] if the process is still running.
|
108
|
+
def check_pid_file
|
109
|
+
pid = File.open(yaml_options[:pid_file], 'r').read.to_i
|
110
|
+
|
111
|
+
begin
|
112
|
+
Process.kill 0, pid
|
113
|
+
raise ProcessExistsError
|
114
|
+
rescue Errno::ESRCH
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Loads the handlers from the YAML file by requiring each file.
|
119
|
+
#
|
120
|
+
# @return [void]
|
121
|
+
def load_handlers
|
122
|
+
handlers = []
|
123
|
+
if yaml_options[:handler]
|
124
|
+
handlers << yaml_options[:handler]
|
125
|
+
end
|
126
|
+
|
127
|
+
if yaml_options[:handlers]
|
128
|
+
handlers.concat(yaml_options[:handlers])
|
129
|
+
end
|
130
|
+
|
131
|
+
handlers.each do |handler|
|
132
|
+
begin
|
133
|
+
require_relative handler
|
134
|
+
rescue LoadError
|
135
|
+
require handler
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Daemonizes the block, if it's applicable.
|
141
|
+
#
|
142
|
+
# @yields [] in the new process, if daemonized; otherwise, a normal yield.
|
143
|
+
def daemonize?
|
144
|
+
if @options[:daemonize]
|
145
|
+
check_pid_file if File.exists? yaml_options[:pid_file]
|
146
|
+
child_process = fork do
|
147
|
+
$stdout = $stderr = File.open(yaml_options[:log_file], 'a')
|
148
|
+
$stdout.sync = true
|
149
|
+
|
150
|
+
yield
|
151
|
+
end
|
152
|
+
|
153
|
+
File.open(yaml_options[:pid_file], 'w') { |f| f.write child_process }
|
154
|
+
Process.detach child_process
|
155
|
+
else
|
156
|
+
yield
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class ProcessExistsError < StandardError; end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Orator
|
2
|
+
|
3
|
+
# A representation of a client.
|
4
|
+
class Client
|
5
|
+
|
6
|
+
# The event handler for this client.
|
7
|
+
#
|
8
|
+
# @return [EventHandler]
|
9
|
+
attr_accessor :event_handler
|
10
|
+
|
11
|
+
# The context to be used by the client.
|
12
|
+
#
|
13
|
+
# @return [Object]
|
14
|
+
attr_accessor :context
|
15
|
+
|
16
|
+
# The socket to be used to talk to the client.
|
17
|
+
#
|
18
|
+
# @return [#send]
|
19
|
+
attr_accessor :socket
|
20
|
+
|
21
|
+
# Initialize.
|
22
|
+
#
|
23
|
+
# @param data [Hash] the keys are used to set data on the client to the
|
24
|
+
# values of the hash. If the attribute doesn't exist on the client,
|
25
|
+
# it is ignored.
|
26
|
+
# @example
|
27
|
+
# client = Client.new :something => "foo"
|
28
|
+
# client.something # => "foo"
|
29
|
+
def initialize(data)
|
30
|
+
data.each do |k, v|
|
31
|
+
send "#{k}=", v if respond_to? "#{k}="
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# This forwards the method call to [EventHandler#trigger], but removes an
|
36
|
+
# argument: the context.
|
37
|
+
#
|
38
|
+
# @see [EventHandler#trigger]
|
39
|
+
def trigger(event, *args)
|
40
|
+
event_handler.trigger(event, context, *args)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Orator
|
2
|
+
|
3
|
+
# This handles events and calls the callbacks for them.
|
4
|
+
class EventHandler
|
5
|
+
|
6
|
+
# Initialize the event handler.
|
7
|
+
#
|
8
|
+
# @yields [] in the current instance. Mainly for setting up the events.
|
9
|
+
# @return [void]
|
10
|
+
def initialize(&block)
|
11
|
+
@events = []
|
12
|
+
@_number = 0
|
13
|
+
instance_exec &block if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
# This adds an event to the set. It can accept a block or a class. If
|
17
|
+
# it's a class, the class should inherit from [Handlers::Base]. If both
|
18
|
+
# a class and a block is given, the block is preferred.
|
19
|
+
#
|
20
|
+
# @param event [String, Symbol] the event to bind it to.
|
21
|
+
# @param klass [Handlers::Base, nil] the class that contains the method to
|
22
|
+
# use for the event.
|
23
|
+
# @param count [Numeric, nil] the number of times to run this event. No
|
24
|
+
# value is an infinite number of times.
|
25
|
+
# @raise [ArgumentError] if klass isn't a subclass of Handlers::Base.
|
26
|
+
# @return [void]
|
27
|
+
def on(event, klass = nil, count = nil, &block)
|
28
|
+
puts "Binding #{event} to #{klass || block}..." if Orator.debug
|
29
|
+
data = { :event => event.to_s, :count => count }
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
data[:block] = block
|
33
|
+
elsif klass and klass < Handlers::Base
|
34
|
+
data[:class] = klass
|
35
|
+
else
|
36
|
+
raise ArgumentError, "No class or block was given."
|
37
|
+
end
|
38
|
+
|
39
|
+
@events << data unless @events.include?(data)
|
40
|
+
end
|
41
|
+
|
42
|
+
# This triggers the events. The events aren't triggered in any particular
|
43
|
+
# order, since they are stored in a [Set]. Extra arguments are passed to
|
44
|
+
# the block/class.
|
45
|
+
#
|
46
|
+
# @param event [String, Symbol] the event to trigger.
|
47
|
+
# @param context [Object] the context to run in. If the event is mapped to
|
48
|
+
# a block, the block is executed in the context. In all cases, the
|
49
|
+
# context is appended to the end of the arguments.
|
50
|
+
# @return [Object, nil]
|
51
|
+
def trigger(event, context, *args)
|
52
|
+
puts "Running event #{event}..." if Orator.debug
|
53
|
+
events(event).map do |event|
|
54
|
+
puts "Found responder #{event[:block] || event[:class]}" if Orator.debug
|
55
|
+
if event[:block]
|
56
|
+
context.instance_exec *args, context, &event[:block]
|
57
|
+
elsif event[:class]
|
58
|
+
event[:class].new(context).__trigger(event[:event], *args)
|
59
|
+
end
|
60
|
+
|
61
|
+
if event[:count] then event[:count] -= 1 end
|
62
|
+
end
|
63
|
+
|
64
|
+
clear_events
|
65
|
+
end
|
66
|
+
|
67
|
+
# Accessor for events. If the first parameter is passed, it checks for
|
68
|
+
# events that match that event.
|
69
|
+
#
|
70
|
+
# @param matching [String, Symbol] the event to find.
|
71
|
+
# @return [Set<Hash>]
|
72
|
+
def events(matching = nil)
|
73
|
+
if matching
|
74
|
+
@events.select { |e| e[:event] == matching.to_s }
|
75
|
+
else
|
76
|
+
@events.dup
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# This removes events where their [:count] is less than or equal to zero.
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
def clear_events
|
86
|
+
@events.delete_if do |event|
|
87
|
+
if event[:count]
|
88
|
+
event[:count] <= 0
|
89
|
+
else
|
90
|
+
false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Orator
|
2
|
+
module Handlers
|
3
|
+
|
4
|
+
# A base for the handlers to split off of. It handles contexts and running
|
5
|
+
# methods. Most of the methods defined in this class are prefixed with
|
6
|
+
# two dashes to prevent polluting the namespace.
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
class Base
|
10
|
+
|
11
|
+
# Initialize the handler with the context.
|
12
|
+
#
|
13
|
+
# @param context [Object]
|
14
|
+
def initialize(context)
|
15
|
+
@__context = context
|
16
|
+
end
|
17
|
+
|
18
|
+
# This triggers an event if it's defined on this handler. If it is not
|
19
|
+
# defined, it raises an error.
|
20
|
+
#
|
21
|
+
# @param event [Symbol, String] the event to trigger.
|
22
|
+
# @raise [NoMethodError] if the event isn't defined on the handler.
|
23
|
+
# @return [Array<Object>] the values of the trigger.
|
24
|
+
def __trigger(event, *args)
|
25
|
+
handler, method_name = event.to_s.split('.')
|
26
|
+
|
27
|
+
if handler != self.class.name
|
28
|
+
raise NoMethodError,
|
29
|
+
"Event #{handler} does not match #{self.class.name}"
|
30
|
+
return
|
31
|
+
elsif !__method_exists?(method_name)
|
32
|
+
raise NoMethodError,
|
33
|
+
"Method #{method_name} does not exist on #{self.class.name}"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
self.class.before_list.map { |b| instance_exec &b }
|
38
|
+
|
39
|
+
__run_event(method_name, args) unless @__prevent_event
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks the event list to see if this class responds to it.
|
43
|
+
#
|
44
|
+
# @return [Set<Symbol, Proc>, nil]
|
45
|
+
def __method_exists?(method)
|
46
|
+
__event_list[method]
|
47
|
+
end
|
48
|
+
|
49
|
+
# A map of the events that determine how this class will respond.
|
50
|
+
#
|
51
|
+
# @return [Hash]
|
52
|
+
# @see [ClassMethods::event_list]
|
53
|
+
def __event_list
|
54
|
+
self.class.event_list
|
55
|
+
end
|
56
|
+
|
57
|
+
# This tells the handler to not execute the events in the class. This
|
58
|
+
# should only be called in a `before` block.
|
59
|
+
def prevent_event
|
60
|
+
@__prevent_event = true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handles missing methods by delegating them to the context. Should
|
64
|
+
# never be called directly.
|
65
|
+
#
|
66
|
+
# @return [Object]
|
67
|
+
def method_missing(method, *args, &block)
|
68
|
+
super unless respond_to_missing?(method)
|
69
|
+
|
70
|
+
@__context.public_send(method, *args, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Lets Ruby know that there are some undefined methods on this class.
|
74
|
+
#
|
75
|
+
# @return [Boolean]
|
76
|
+
def respond_to_missing?(method, include_private = false)
|
77
|
+
@__context.respond_to?(method, include_private)
|
78
|
+
end
|
79
|
+
|
80
|
+
# This forwards {#send} on to the context.
|
81
|
+
#
|
82
|
+
# @return [Object]
|
83
|
+
def send(*args, &block)
|
84
|
+
@__context.send(*args, &block)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def __run_event(method_name, args)
|
90
|
+
result = __event_list[method_name].map do |responder|
|
91
|
+
responder = method(responder) if responder.is_a? Symbol
|
92
|
+
instance_exec *args, &responder
|
93
|
+
end
|
94
|
+
|
95
|
+
self.class.after_list.map { |a| instance_exec &a }
|
96
|
+
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
module ClassMethods
|
101
|
+
# The name of the handler. This is used to determine whether or not
|
102
|
+
# the handler can run it.
|
103
|
+
#
|
104
|
+
# @return [String]
|
105
|
+
attr_accessor :name
|
106
|
+
|
107
|
+
# This registers a method for this Handler.
|
108
|
+
#
|
109
|
+
# @param event [Symbol] the event to register it to.
|
110
|
+
# @return [void]
|
111
|
+
def on(event, method = nil, &block)
|
112
|
+
event_list[event.to_s] ||= []
|
113
|
+
|
114
|
+
if block_given?
|
115
|
+
event_list[event.to_s] << block
|
116
|
+
else
|
117
|
+
event_list[event.to_s] << (method || event).to_sym
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# The event matchings. The keys are the event name, the values can be
|
122
|
+
# an array of mappings or a the mapping itself.
|
123
|
+
#
|
124
|
+
# @return [Hash] the event list.
|
125
|
+
def event_list
|
126
|
+
@event_list ||= {}
|
127
|
+
end
|
128
|
+
|
129
|
+
# This registers this handler with the event handler by telling it what
|
130
|
+
# methods it responds to.
|
131
|
+
#
|
132
|
+
# @param event_handler [EventHandler]
|
133
|
+
def register_with(event_handler)
|
134
|
+
event_list.keys.each do |event|
|
135
|
+
event_handler.on("#{self.name}.#{event}", self)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
[:before, :after].each do |type|
|
140
|
+
module_eval <<-METHOD, __FILE__, __LINE__
|
141
|
+
def #{type}(method = nil, &block)
|
142
|
+
#{type}_list << (method || block)
|
143
|
+
end
|
144
|
+
|
145
|
+
def #{type}_list
|
146
|
+
@#{type}_list ||= Set.new
|
147
|
+
end
|
148
|
+
METHOD
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
extend ClassMethods
|
153
|
+
self.name = ''
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Orator
|
2
|
+
|
3
|
+
# This is used as a context for the client execution.
|
4
|
+
class MiddleGround
|
5
|
+
|
6
|
+
# Provides access to the client.
|
7
|
+
#
|
8
|
+
# @return [Client]
|
9
|
+
attr_reader :client
|
10
|
+
|
11
|
+
# Initialize the class.
|
12
|
+
def initialize(client)
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
# This sends a message to the client.
|
17
|
+
#
|
18
|
+
# @param data [Hash] the data to be sent to the client.
|
19
|
+
# @return [void]
|
20
|
+
def send(data)
|
21
|
+
@client.socket.send Oj.dump(data, :mode => :null)
|
22
|
+
end
|
23
|
+
|
24
|
+
# This builds a message from a hash and an event.
|
25
|
+
#
|
26
|
+
# @param event [String, Symbol] the event for the message.
|
27
|
+
# @param data [Hash] the data for the message.
|
28
|
+
def message(event, data)
|
29
|
+
new_data = { "event" => event.to_s }
|
30
|
+
|
31
|
+
data.each do |k, v|
|
32
|
+
new_data[k.to_s] = v
|
33
|
+
end
|
34
|
+
|
35
|
+
new_data
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Orator
|
2
|
+
|
3
|
+
# Handles the server connections and passing stuff to the event handler.
|
4
|
+
class Server
|
5
|
+
|
6
|
+
# The options defined for the server.
|
7
|
+
#
|
8
|
+
# @return [Hash]
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
# The list of clients that are connected.
|
12
|
+
#
|
13
|
+
# @return [Set<Client>]
|
14
|
+
attr_reader :clients
|
15
|
+
|
16
|
+
# The event handler that is duplicated for every client. This is to reduce
|
17
|
+
# the overhead of reinitializing the event handler for every client.
|
18
|
+
#
|
19
|
+
# @return [EventHandler]
|
20
|
+
attr_reader :event_handler
|
21
|
+
|
22
|
+
# Initialize the server.
|
23
|
+
#
|
24
|
+
# @return [void]
|
25
|
+
def initialize(options)
|
26
|
+
@options = options
|
27
|
+
@clients = Set.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Runs the server with the given options.
|
31
|
+
def run(&block)
|
32
|
+
@event_handler = EventHandler.new(&block)
|
33
|
+
EM::WebSocket.start(@options, &method(:handle_socket))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Handles setting up the socket. This binds the [#onopen], [#onclose],
|
39
|
+
# [#onerror], and [#onmessage] events.
|
40
|
+
#
|
41
|
+
# @param [Object]
|
42
|
+
def handle_socket(socket)
|
43
|
+
client = Client.new :event_handler => @event_handler.dup,
|
44
|
+
:socket => socket, :context => nil
|
45
|
+
client.context = MiddleGround.new(client)
|
46
|
+
clients << client
|
47
|
+
|
48
|
+
socket.onopen do |handshake|
|
49
|
+
client.trigger 'socket.open', handshake
|
50
|
+
end
|
51
|
+
|
52
|
+
socket.onerror do |error|
|
53
|
+
client.trigger 'socket.error', error
|
54
|
+
end
|
55
|
+
|
56
|
+
socket.onclose do |message|
|
57
|
+
client.trigger 'socket.close', message
|
58
|
+
clients.delete client
|
59
|
+
end
|
60
|
+
|
61
|
+
socket.onmessage do |data|
|
62
|
+
json_data = Oj.load data, :mode => :null
|
63
|
+
client.trigger json_data["event"], json_data
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/lib/orator.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# A websocket server.
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
require 'set'
|
5
|
+
require 'em-websocket'
|
6
|
+
|
7
|
+
require 'orator/server'
|
8
|
+
require 'orator/client'
|
9
|
+
require 'orator/version'
|
10
|
+
require 'orator/handlers'
|
11
|
+
require 'orator/event_handler'
|
12
|
+
require 'orator/middle_ground'
|
13
|
+
|
14
|
+
require 'orator/engine' if defined? Rails
|
15
|
+
|
16
|
+
module Orator
|
17
|
+
|
18
|
+
class << self; attr_accessor :debug; end
|
19
|
+
Orator.debug = false
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
describe Orator::Handlers::Base do
|
2
|
+
it "should call before blocks" do
|
3
|
+
something = double('something')
|
4
|
+
something.should_receive(:to_call)
|
5
|
+
klass = Class.new(described_class) do
|
6
|
+
self.name = 'test'
|
7
|
+
before { something.to_call }
|
8
|
+
on 'some_event'
|
9
|
+
def some_event; end
|
10
|
+
end
|
11
|
+
|
12
|
+
klass.new(nil).__trigger('test.some_event')
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should raise error when it doesn't match the event" do
|
16
|
+
expect { described_class.new(nil).__trigger('other.thing') }.to raise_error(NoMethodError)
|
17
|
+
expect { described_class.new(nil).__trigger('.thing') }.to raise_error(NoMethodError)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not run the event if the before callback prevents it" do
|
21
|
+
something = double('something')
|
22
|
+
something.should_not_receive(:to_call)
|
23
|
+
klass = Class.new(described_class) do
|
24
|
+
self.name = 'test'
|
25
|
+
before { prevent_event }
|
26
|
+
on('some_event') { something.to_call }
|
27
|
+
end
|
28
|
+
|
29
|
+
klass.new(nil).__trigger('test.some_event')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should register itself with an event handler" do
|
33
|
+
event_handler = double('event_handler')
|
34
|
+
|
35
|
+
klass = Class.new(described_class) do
|
36
|
+
self.name = 'test'
|
37
|
+
|
38
|
+
on('some_event') { }
|
39
|
+
end
|
40
|
+
|
41
|
+
event_handler.should_receive(:on).with("test.some_event", klass)
|
42
|
+
|
43
|
+
klass.register_with(event_handler)
|
44
|
+
end
|
45
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
describe Orator::Client do
|
2
|
+
it "should accept a hash" do
|
3
|
+
client = described_class.new :context => 42, :other_thing => 124
|
4
|
+
|
5
|
+
client.context.should be 42
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should trigger event with context" do
|
9
|
+
client = described_class.new :context => 42,
|
10
|
+
:event_handler => double('event_handler')
|
11
|
+
client.event_handler.should_receive(:trigger).with('some.event', 42, 54)
|
12
|
+
client.trigger('some.event', 54)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
describe Orator::EventHandler do
|
2
|
+
it "should initalize with block" do
|
3
|
+
expect {
|
4
|
+
EventHandler.new { raise StandardError }
|
5
|
+
}.to raise_error(StandardError)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should add an event with a block" do
|
9
|
+
subject.events.length.should be 0
|
10
|
+
subject.on(:some_event) { something }
|
11
|
+
subject.events.length.should be 1
|
12
|
+
subject.events.first[:event].should eq "some_event"
|
13
|
+
subject.events.first[:block].should be_kind_of Proc
|
14
|
+
end
|
15
|
+
|
16
|
+
class SomeClass < Orator::Handlers::Base; end
|
17
|
+
|
18
|
+
it "should accept a class for an event" do
|
19
|
+
subject.on(:some_event, SomeClass)
|
20
|
+
subject.events.first[:class].should be SomeClass
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should accept a count" do
|
24
|
+
subject.on(:some_event, SomeClass, 4)
|
25
|
+
subject.events.first[:count].should be 4
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should decrease the count by one" do
|
29
|
+
something = double("something")
|
30
|
+
something.should_receive(:to_call)
|
31
|
+
subject.on(:some_event, nil, 4) { something.to_call }
|
32
|
+
subject.events.first[:count].should be 4
|
33
|
+
subject.trigger(:some_event, self)
|
34
|
+
subject.events.first[:count].should be 3
|
35
|
+
end
|
36
|
+
|
37
|
+
it "shouldn't add the same event twice" do
|
38
|
+
subject.on(:some_event, SomeClass)
|
39
|
+
subject.events.length.should be 1
|
40
|
+
subject.on(:some_event, SomeClass)
|
41
|
+
subject.events.length.should be 1
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should remove events with a count of 0" do
|
45
|
+
something = double("something")
|
46
|
+
something.should_receive(:to_call)
|
47
|
+
subject.on(:some_event, nil, 1) { something.to_call }
|
48
|
+
subject.on(:another_event) { }
|
49
|
+
subject.events.length.should be 2
|
50
|
+
subject.trigger(:some_event, self)
|
51
|
+
subject.events.length.should be 1
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should select events with the same event" do
|
55
|
+
subject.on(:some_event) { }
|
56
|
+
subject.on(:another_event) { }
|
57
|
+
subject.on(:some_event) { do_something }
|
58
|
+
|
59
|
+
subject.events.length.should be 3
|
60
|
+
subject.events(:some_event).length.should be 2
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
describe Orator::MiddleGround do
|
2
|
+
#it "should build a message" do
|
3
|
+
# subje
|
4
|
+
#end
|
5
|
+
|
6
|
+
subject { described_class.new(double('client')) }
|
7
|
+
|
8
|
+
it "should build a message" do
|
9
|
+
subject.message('some.thing', :data => 'value').should eq(
|
10
|
+
"event" => "some.thing", "data" => "value")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should send a message to the client" do
|
14
|
+
socket = double('socket')
|
15
|
+
socket.should_receive(:send).with('{"event":"some.thing","data":"value"}')
|
16
|
+
subject.client.should_receive(:socket).and_return(socket)
|
17
|
+
subject.send("event" => "some.thing", "data" => "value")
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: orator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Rodi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: em-websocket
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: oj
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.0.10
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.0.10
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: swf_fu
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.4
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.4
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.13.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.13.0
|
69
|
+
description: ! ' An implementation of an evented websocket system for rails.
|
70
|
+
|
71
|
+
'
|
72
|
+
email: redjazz96@gmail.com
|
73
|
+
executables:
|
74
|
+
- orator
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- lib/orator/middle_ground.rb
|
79
|
+
- lib/orator/version.rb
|
80
|
+
- lib/orator/handlers.rb
|
81
|
+
- lib/orator/server.rb
|
82
|
+
- lib/orator/cli.rb
|
83
|
+
- lib/orator/client.rb
|
84
|
+
- lib/orator/handlers/base.rb
|
85
|
+
- lib/orator/engine.rb
|
86
|
+
- lib/orator/event_handler.rb
|
87
|
+
- lib/orator.rb
|
88
|
+
- bin/orator
|
89
|
+
- spec/client_spec.rb
|
90
|
+
- spec/middle_ground_spec.rb
|
91
|
+
- spec/event_handler_spec.rb
|
92
|
+
- spec/base_handler_spec.rb
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- LICENSE
|
96
|
+
homepage: http://github.com/redjazz96/orator
|
97
|
+
licenses: []
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.0.2
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Magic for your browser.
|
119
|
+
test_files: []
|
120
|
+
has_rdoc: false
|