mattly-datagrammer 0.2 → 0.4
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.
- data/README.mkdn +5 -7
- data/lib/datagrammer.rb +48 -7
- data/lib/datagrammer/packet.rb +23 -4
- data/spec/datagrammer_spec.rb +64 -13
- data/spec/packet_spec.rb +11 -0
- metadata +2 -4
- data/lib/datagrammer/generic_handler.rb +0 -54
- data/spec/generic_handler_spec.rb +0 -60
data/README.mkdn
CHANGED
@@ -21,17 +21,15 @@ encoding/decoding stuff, as well as providing a non-blocking listening thread.
|
|
21
21
|
require 'datagrammer'
|
22
22
|
|
23
23
|
server = Datagrammer.new(5000)
|
24
|
-
server.
|
25
|
-
serv.speak("thanks for your message #{sender}, I received: #{msg.join(', ')}")
|
26
|
-
end
|
24
|
+
server.register_rule /.*/, lambda {|msg| server.speak("received: #{msg.join(',')}") }
|
27
25
|
|
28
26
|
# set to speak at server's default speak destination
|
29
|
-
client = Datagrammer.new(5001, :speak_port => 5000)
|
30
|
-
client.
|
31
|
-
|
27
|
+
client = Datagrammer.new(5001, :speak_port => 5000)
|
28
|
+
client.register_rule 'received:', lambda {|msg| puts "rec'd #{msg}.join(' ')" }
|
29
|
+
|
32
30
|
client.speak(%w(hey joe))
|
33
31
|
sleep 0.1
|
34
|
-
# rec'd
|
32
|
+
# rec'd received: hey, joe
|
35
33
|
|
36
34
|
## REQUIREMENTS
|
37
35
|
|
data/lib/datagrammer.rb
CHANGED
@@ -3,7 +3,6 @@ require 'socket'
|
|
3
3
|
$:.unshift File.dirname(__FILE__)
|
4
4
|
require 'datagrammer/packet'
|
5
5
|
require 'datagrammer/packet_scanner'
|
6
|
-
require 'datagrammer/generic_handler'
|
7
6
|
|
8
7
|
# A Datagrammer object will listen on a given address and port for messages and
|
9
8
|
# will decode the data (it's assumed to be in a OSC-style format) and perform
|
@@ -16,36 +15,76 @@ require 'datagrammer/generic_handler'
|
|
16
15
|
# # OSC-style packet (f.e. from Max/MSP's udpsender) will be echoed back on port
|
17
16
|
# # 5001 in the string format given in the block.
|
18
17
|
class Datagrammer
|
19
|
-
attr_accessor :thread, :socket, :speak_address, :speak_port
|
18
|
+
attr_accessor :thread, :socket, :speak_address, :speak_port, :string_rules, :regex_rules
|
20
19
|
|
21
20
|
# Creates a new Datagrammer object bound to the specified port. The following
|
22
21
|
# options are available:
|
23
22
|
# * (({:address})): IP to listen on. defaults to "0.0.0.0"
|
24
23
|
# * (({:speak_address})): default IP to send to. defaults to "0.0.0.0"
|
25
24
|
# * (({:speak_port})): default port to speak on, defaults to port + 1
|
25
|
+
# * (({:rules})): a hash of key, proc pairs that will become the initial ruleset for figuring out what to do with
|
26
|
+
# received data. See '#register_rule' for more information
|
27
|
+
# * (({:listen})): boolean, default true, to start listening on instantiation
|
28
|
+
#
|
29
|
+
# Your datagrammer instance will start listening automatically unless otherwise directed by :listen
|
26
30
|
def initialize(port, opts={})
|
27
31
|
@port = port
|
28
32
|
@address = opts[:address] || "0.0.0.0"
|
29
33
|
@speak_address = opts[:speak_address] || @address
|
30
34
|
@speak_port = opts[:speak_port] || port + 1
|
35
|
+
@string_rules, @regex_rules = {}, {}
|
36
|
+
@default_rule = nil
|
37
|
+
(opts[:rules]||{}).each_pair {|key, value| register_rule(key, value) }
|
31
38
|
@socket = UDPSocket.new
|
32
39
|
@socket.bind(@address, @port)
|
40
|
+
listen unless opts.has_key?(:listen) && ! opts[:listen]
|
33
41
|
end
|
34
42
|
|
43
|
+
# Defines a rule to match and a process to run if matched. Rule keys are matched
|
44
|
+
# against the first item in the "data" value passed in from Datagrammer, or the
|
45
|
+
# so-called "path" of the message. Data like (({['/foo/bar', 'baz', 'bee']})) would
|
46
|
+
# use "/foo/bar" as the path to match against.
|
47
|
+
#
|
48
|
+
# Rules are evaluated as follows:
|
49
|
+
# * If a rule key is a string and matches the path exactly, the rule will be used.
|
50
|
+
# * If a rule key is a regex and there are matches against the path, the rule will be used.
|
51
|
+
# Matches from the regex are prepended to the argument list passed to the process.
|
52
|
+
# * If no string or regex rules match and there is a rule called :default, that rule will be used
|
53
|
+
# * If there is no :default rule and no matches are made, nothing will happen
|
54
|
+
def register_rule(rule, block)
|
55
|
+
@string_rules[rule] = block if rule.kind_of?(String)
|
56
|
+
@regex_rules[rule] = block if rule.kind_of?(Regexp)
|
57
|
+
@default_rule = block if rule == :default
|
58
|
+
end
|
59
|
+
|
60
|
+
# runs the first part of the incoming message against the registered rules.
|
61
|
+
def handle(address, arguments=[])
|
62
|
+
list = []
|
63
|
+
list << [@string_rules[address], arguments]
|
64
|
+
list += @regex_rules.select {|key, value| address =~ key }.map do |regex, action|
|
65
|
+
msg = [address.scan(regex), arguments].flatten
|
66
|
+
msg.delete('')
|
67
|
+
[action, msg]
|
68
|
+
end
|
69
|
+
list.delete_if {|callback, args| callback.nil? }
|
70
|
+
list << [@default_rule, arguments] if list.empty? && @default_rule
|
71
|
+
list.each {|callback, args| callback.call(args) }
|
72
|
+
end
|
73
|
+
|
74
|
+
|
35
75
|
# Sets the default speak destination
|
36
76
|
def speak_destination=(addr, port)
|
37
77
|
@speak_address, @speak_port = addr, port
|
38
78
|
end
|
39
79
|
|
40
|
-
# Starts the thread to listen on the selected port.
|
41
|
-
|
42
|
-
# address of the sender.
|
43
|
-
def listen(&block)
|
80
|
+
# Starts the thread to listen on the selected port.
|
81
|
+
def listen
|
44
82
|
@thread = Thread.start do
|
45
83
|
loop do
|
46
84
|
IO.select([@socket])
|
47
85
|
data, info = @socket.recvfrom(65535)
|
48
|
-
|
86
|
+
data = Packet.decode(data)
|
87
|
+
handle(data.shift, data)
|
49
88
|
end
|
50
89
|
end
|
51
90
|
end
|
@@ -55,4 +94,6 @@ class Datagrammer
|
|
55
94
|
@socket.send(Packet.encode([message]), 0, addr, port)
|
56
95
|
end
|
57
96
|
|
97
|
+
private
|
98
|
+
|
58
99
|
end
|
data/lib/datagrammer/packet.rb
CHANGED
@@ -12,10 +12,25 @@ class Datagrammer
|
|
12
12
|
message = scanner.scan_string
|
13
13
|
argument_types = scanner.scan_string.sub(/^,/,'').split('')
|
14
14
|
arguments = argument_types.collect do |type|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
case type
|
16
|
+
when 's'; scanner.scan_string
|
17
|
+
when 'i'; scanner.scan_integer
|
18
|
+
when 'f'; scanner.scan_float
|
19
|
+
when 'T'; true
|
20
|
+
when 'F'; false
|
21
|
+
when 'N'; nil
|
22
|
+
# not supported yet:
|
23
|
+
# when 'b'; # blob
|
24
|
+
# when 'S'; # symbol
|
25
|
+
# when 't'; # OSC timetag
|
26
|
+
# when 'h'; # 64-bit integer
|
27
|
+
# when 'd'; # 64-bit "double" float
|
28
|
+
# when 'r'; # 32-bit RGBA color
|
29
|
+
# when 'm'; # four-byte midi message
|
30
|
+
# when 'I'; # infinity
|
31
|
+
# when '['; # beginning of array
|
32
|
+
# when ']'; # end of array
|
33
|
+
end
|
19
34
|
end
|
20
35
|
arguments.unshift(message)
|
21
36
|
end
|
@@ -45,6 +60,9 @@ class Datagrammer
|
|
45
60
|
when String; 's'
|
46
61
|
when Integer; 'i'
|
47
62
|
when Float; 'f'
|
63
|
+
when TrueClass; 'T'
|
64
|
+
when FalseClass; 'F'
|
65
|
+
when NilClass; 'N'
|
48
66
|
end
|
49
67
|
end.join
|
50
68
|
pad(str)
|
@@ -56,6 +74,7 @@ class Datagrammer
|
|
56
74
|
when String; pad(argument)
|
57
75
|
when Integer; [argument].pack('N')
|
58
76
|
when Float; [argument].pack('g')
|
77
|
+
when TrueClass, FalseClass, NilClass; # no arguments
|
59
78
|
end
|
60
79
|
end.join
|
61
80
|
end
|
data/spec/datagrammer_spec.rb
CHANGED
@@ -16,21 +16,72 @@ describe Datagrammer do
|
|
16
16
|
s
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
describe "sending messages" do
|
20
|
+
it "should send an encoded packet out to its speaking port" do
|
21
|
+
Thread.start { sleep 0.1; @serv.speak("hi") }
|
22
|
+
sock = setup_socket(10001)
|
23
|
+
IO.select [sock]
|
24
|
+
data, info = sock.recvfrom(1024)
|
25
|
+
sock.close
|
26
|
+
data.should == "hi\000\000,\000\000\000"
|
27
|
+
end
|
26
28
|
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
describe "handling received messages" do
|
31
|
+
|
32
|
+
before do
|
33
|
+
@bar = 'baz'
|
34
|
+
@foo = lambda {|args| @bar = "foo: #{args.join(', ')}" }
|
35
|
+
@serv.register_rule('foo', @foo)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should receive encoded packets while listening and feed it to the handler" do
|
39
|
+
@serv.should_receive(:handle).with('foo', ['hi'])
|
40
|
+
s = UDPSocket.new
|
41
|
+
s.send("foo\000,s\000\000hi\000\000", 0, '0.0.0.0', 10000)
|
42
|
+
sleep 0.1
|
43
|
+
end
|
44
|
+
|
45
|
+
it "calls the handler for a given address" do
|
46
|
+
@serv.handle('foo', %w(bar baz))
|
47
|
+
@bar.should == "foo: bar, baz"
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "with regex handlers" do
|
51
|
+
before do
|
52
|
+
@i = Hash.new {|hash, key| hash[key] = [] }
|
53
|
+
@serv.register_rule(/^\/reg\/(.*)$/, lambda {|a| @i[a.shift] += a})
|
54
|
+
end
|
55
|
+
|
56
|
+
it "accepts regexes as hanlder keys and calls their values when matched" do
|
57
|
+
@serv.handle('/reg/foo', %w(bar baz))
|
58
|
+
@i.should == {'foo' => %w(bar baz)}
|
59
|
+
end
|
60
|
+
|
61
|
+
it "handles values for ALL regexes that match given string" do
|
62
|
+
@serv.register_rule(/.*/, lambda {|a| @i['default'] += a })
|
63
|
+
@serv.handle('/reg/foo', %w(bar baz))
|
64
|
+
@i.should == {'foo' => %w(bar baz), 'default' => %w(/reg/foo bar baz)}
|
65
|
+
end
|
66
|
+
|
67
|
+
it "uses regexes if exact match from string" do
|
68
|
+
@serv.register_rule('/reg/foo', lambda {|a| @i['exact'] += a })
|
69
|
+
@serv.handle('/reg/foo', %w(bar baz))
|
70
|
+
@i.should == {'exact' => %w(bar baz), 'foo' => %w(bar baz)}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "with no handler found" do
|
75
|
+
it "uses 'default' if exists" do
|
76
|
+
@serv.register_rule(:default, lambda {@i = 'default'})
|
77
|
+
@serv.handle('/non-existant')
|
78
|
+
@i.should == 'default'
|
79
|
+
end
|
80
|
+
|
81
|
+
it "does nothing if no default handler" do
|
82
|
+
@serv.handle('/non-existant').should == []
|
83
|
+
end
|
84
|
+
end
|
34
85
|
end
|
35
86
|
|
36
87
|
end
|
data/spec/packet_spec.rb
CHANGED
@@ -26,11 +26,22 @@ describe Datagrammer::Packet do
|
|
26
26
|
encoded = "tick\n\000\000\000,ii\000\000\000\000\n\000\000\334\n"
|
27
27
|
Datagrammer::Packet.decode(encoded).should == ["tick\n", 10, 56330]
|
28
28
|
end
|
29
|
+
|
30
|
+
it "handles encoded booleans correctly" do
|
31
|
+
encoded = "boolean\000,TF\000"
|
32
|
+
Datagrammer::Packet.decode(encoded).should == ["boolean", true, false]
|
33
|
+
end
|
34
|
+
|
35
|
+
it "handles encoded nils correctly" do
|
36
|
+
encoded = "nil\000,N\000\000"
|
37
|
+
Datagrammer::Packet.decode(encoded).should == ["nil", nil]
|
38
|
+
end
|
29
39
|
end
|
30
40
|
|
31
41
|
describe "encode" do
|
32
42
|
[ [['hello'], "hello\000\000\000,\000\000\000"],
|
33
43
|
[['hello','world'], "hello\000\000\000,s\000\000world\000\000\000"],
|
44
|
+
[['hello', true, false, nil], "hello\000\000\000,TFN\000\000\000\000"],
|
34
45
|
[['hello', 'world', 1, 2.0], "hello\000\000\000,sif\000\000\000\000world\000\000\000\000\000\000\001@\000\000\000"]
|
35
46
|
].each do |message, expected|
|
36
47
|
describe "message: #{message.join(', ')}" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mattly-datagrammer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0.
|
4
|
+
version: "0.4"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Lyon
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-11-
|
12
|
+
date: 2008-11-07 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,12 +25,10 @@ files:
|
|
25
25
|
- README.mkdn
|
26
26
|
- Rakefile
|
27
27
|
- spec/datagrammer_spec.rb
|
28
|
-
- spec/generic_handler_spec.rb
|
29
28
|
- spec/packet_scanner_spec.rb
|
30
29
|
- spec/packet_spec.rb
|
31
30
|
- spec/spec_helper.rb
|
32
31
|
- lib/datagrammer
|
33
|
-
- lib/datagrammer/generic_handler.rb
|
34
32
|
- lib/datagrammer/packet.rb
|
35
33
|
- lib/datagrammer/packet_scanner.rb
|
36
34
|
- lib/datagrammer.rb
|
@@ -1,54 +0,0 @@
|
|
1
|
-
class Datagrammer
|
2
|
-
# A simple class for registering multiple "handlers" for messages that come from a Datagrammer object.
|
3
|
-
class GenericHandler
|
4
|
-
attr_accessor :rules
|
5
|
-
|
6
|
-
# Takes a hash of key, proc values for an initial set of rules. Rule keys are matched
|
7
|
-
# against the first item in the "data" value passed in from Datagrammer, or the
|
8
|
-
# so-called "path" of the message. Data like (({['/foo/bar', 'baz', 'bee']})) would
|
9
|
-
# use "/foo/bar" as the path to match against.
|
10
|
-
#
|
11
|
-
# Rules are evaluated in this order:
|
12
|
-
# * If a rule key is a string and it matches the path exactly, that rule will be used.
|
13
|
-
# * If no string rule keys match, any rule keys that are regexes will be run against the
|
14
|
-
# path and ALL regexes that match will be called. The match(es) from the regex will be
|
15
|
-
# prepended to the argument list passed to the proc.
|
16
|
-
# * If there is no match yet and a rule key called '\default' exists, that rule will be used.
|
17
|
-
# * If no match has been made at this point, nothing will happen, the data will be ignored.
|
18
|
-
def initialize(rules={})
|
19
|
-
@rules = rules
|
20
|
-
end
|
21
|
-
|
22
|
-
# sets a proc to be called when a rule is matched. See #new for more information.
|
23
|
-
def register_rule(address, block)
|
24
|
-
@rules[address] = block
|
25
|
-
end
|
26
|
-
|
27
|
-
# What Datagrammer calls. Runs the path against registered rules as described in #new
|
28
|
-
def handle(address, arguments=[])
|
29
|
-
list = if @rules.has_key?(address)
|
30
|
-
[[@rules[address], arguments]]
|
31
|
-
else
|
32
|
-
matches = @rules.select {|k,v| k.kind_of?(Regexp) && address =~ k }
|
33
|
-
if ! matches.empty?
|
34
|
-
matches.collect do |regex, p|
|
35
|
-
msg = [address.scan(regex), arguments].flatten
|
36
|
-
msg.delete('')
|
37
|
-
[p, msg]
|
38
|
-
end
|
39
|
-
elsif @rules.has_key?('\default')
|
40
|
-
[[@rules['\default'], arguments]]
|
41
|
-
else
|
42
|
-
[]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
list.map {|callback, args| callback.call(args) }
|
46
|
-
end
|
47
|
-
|
48
|
-
# returns a lambda for a Datagrammer object's listen method
|
49
|
-
def handler
|
50
|
-
lambda {|dg, args, sender| self.handle(args.shift, args) }
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
54
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
-
|
3
|
-
describe Datagrammer::GenericHandler do
|
4
|
-
|
5
|
-
before do
|
6
|
-
@handler = Datagrammer::GenericHandler.new
|
7
|
-
@foo = lambda {|args| "foo: #{args.join(', ')}" }
|
8
|
-
@handler.register_rule('/foo', @foo)
|
9
|
-
end
|
10
|
-
|
11
|
-
it "registers handlers" do
|
12
|
-
@handler.rules['/foo'].should == @foo
|
13
|
-
end
|
14
|
-
|
15
|
-
it "calls the handler for a given address" do
|
16
|
-
@handler.handle('/foo', %w(bar baz)).should == ["foo: bar, baz"]
|
17
|
-
end
|
18
|
-
|
19
|
-
it "spits out a lambda for calling the handler from DG" do
|
20
|
-
@handler.should_receive(:handle).with('/foo',%w(bar baz))
|
21
|
-
@handler.handler.call(nil, %w(/foo bar baz), nil)
|
22
|
-
end
|
23
|
-
|
24
|
-
describe "regex handlers" do
|
25
|
-
before do
|
26
|
-
@i = Hash.new {|hash, key| hash[key] = [] }
|
27
|
-
@handler.register_rule(/^\/reg\/(.*)$/, lambda {|a| @i[a.shift] += a})
|
28
|
-
end
|
29
|
-
|
30
|
-
it "accepts regexes as hanlder keys and calls their values when matched" do
|
31
|
-
@handler.handle('/reg/foo', %w(bar baz))
|
32
|
-
@i.should == {'foo' => %w(bar baz)}
|
33
|
-
end
|
34
|
-
|
35
|
-
it "handles values for ALL regexes that match given string" do
|
36
|
-
@handler.register_rule(/.*/, lambda {|a| @i['default'] += a })
|
37
|
-
@handler.handle('/reg/foo', %w(bar baz))
|
38
|
-
@i.should == {'foo' => %w(bar baz), 'default' => %w(/reg/foo bar baz)}
|
39
|
-
end
|
40
|
-
|
41
|
-
it "does not use regexes if exact match from string" do
|
42
|
-
@handler.register_rule('/reg/foo', lambda {|a| @i['exact'] += a })
|
43
|
-
@handler.handle('/reg/foo', %w(bar baz))
|
44
|
-
@i.should == {'exact' => %w(bar baz)}
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
describe "no handler found" do
|
49
|
-
it "uses 'default' if exists" do
|
50
|
-
default = lambda { 'default' }
|
51
|
-
@handler.register_rule('\default', default)
|
52
|
-
@handler.handle('/non-existant').should == ['default']
|
53
|
-
end
|
54
|
-
|
55
|
-
it "does nothing if no default handler" do
|
56
|
-
@handler.handle('/non-existant').should == []
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|