orator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 [](https://codeclimate.com/github/redjazz96/orator) [](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
|