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 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.listen do |serv, msg, sender|
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.listen {|serv, msg| puts "rec'd #{msg}"}
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 thanks for your message 127.0.0.1, I received: hey, joe
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. A block is required, that will get
41
- # three arguments: the datagrammer object itself, the packet data (as an array), and the
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
- block.call(self, Packet.decode(data), info.last)
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
@@ -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
- { 's' => lambda { scanner.scan_string },
16
- 'i' => lambda { scanner.scan_integer },
17
- 'f' => lambda { scanner.scan_float }
18
- }[type].call()
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
@@ -16,21 +16,72 @@ describe Datagrammer do
16
16
  s
17
17
  end
18
18
 
19
- it "should send an encoded packet out to its speaking port" do
20
- Thread.start { sleep 0.1; @serv.speak("hi") }
21
- sock = setup_socket(10001)
22
- IO.select [sock]
23
- data, info = sock.recvfrom(1024)
24
- sock.close
25
- data.should == "hi\000\000,\000\000\000"
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
- it "should receive encoded packets while listening and feed it to the callback" do
29
- @serv.listen {|dg, msg| @foo = msg }
30
- s = UDPSocket.new
31
- s.send("hi\000\000,\000\000\000", 0, '0.0.0.0', 10000)
32
- sleep 0.1
33
- @foo.should == ['hi']
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.2"
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-01 00:00:00 -07:00
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