mattly-datagrammer 0.1.1 → 0.2

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/lib/datagrammer.rb CHANGED
@@ -3,6 +3,7 @@ require 'socket'
3
3
  $:.unshift File.dirname(__FILE__)
4
4
  require 'datagrammer/packet'
5
5
  require 'datagrammer/packet_scanner'
6
+ require 'datagrammer/generic_handler'
6
7
 
7
8
  # A Datagrammer object will listen on a given address and port for messages and
8
9
  # will decode the data (it's assumed to be in a OSC-style format) and perform
@@ -15,8 +16,9 @@ require 'datagrammer/packet_scanner'
15
16
  # # OSC-style packet (f.e. from Max/MSP's udpsender) will be echoed back on port
16
17
  # # 5001 in the string format given in the block.
17
18
  class Datagrammer
19
+ attr_accessor :thread, :socket, :speak_address, :speak_port
18
20
 
19
- # creates a new Datagrammer object bound to the specified port. The following
21
+ # Creates a new Datagrammer object bound to the specified port. The following
20
22
  # options are available:
21
23
  # * (({:address})): IP to listen on. defaults to "0.0.0.0"
22
24
  # * (({:speak_address})): default IP to send to. defaults to "0.0.0.0"
@@ -24,18 +26,20 @@ class Datagrammer
24
26
  def initialize(port, opts={})
25
27
  @port = port
26
28
  @address = opts[:address] || "0.0.0.0"
27
- @speak_address = opts[:speak_address] || "0.0.0.0"
29
+ @speak_address = opts[:speak_address] || @address
28
30
  @speak_port = opts[:speak_port] || port + 1
29
31
  @socket = UDPSocket.new
30
32
  @socket.bind(@address, @port)
31
33
  end
32
34
 
35
+ # Sets the default speak destination
33
36
  def speak_destination=(addr, port)
34
37
  @speak_address, @speak_port = addr, port
35
38
  end
36
39
 
37
- attr_accessor :thread, :socket, :speak_address, :speak_port
38
-
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.
39
43
  def listen(&block)
40
44
  @thread = Thread.start do
41
45
  loop do
@@ -46,6 +50,7 @@ class Datagrammer
46
50
  end
47
51
  end
48
52
 
53
+ # Encodes and sends a packet to the specified address and port
49
54
  def speak(message, addr=@speak_address, port=@speak_port)
50
55
  @socket.send(Packet.encode([message]), 0, addr, port)
51
56
  end
@@ -0,0 +1,54 @@
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,36 +1,39 @@
1
1
  require 'strscan'
2
2
 
3
3
  class Datagrammer
4
+
5
+ # Decodes and Encodes packets into a basic OSC (Open Sound Control)-like format.
4
6
  module Packet
5
7
 
8
+ # decodes packet data from f.e. (({"hello\000\000\000,s\000\000world\000\000\000"})) to
9
+ # (({%w(hello world)}))
6
10
  def self.decode(packet_string='')
7
- scanner = StringScanner.new(packet_string)
11
+ scanner = PacketScanner.new(packet_string)
8
12
  message = scanner.scan_string
9
13
  argument_types = scanner.scan_string.sub(/^,/,'').split('')
10
- arguments = argument_types.inject([]) do |memo, type|
11
- case type
12
- when 's'; memo << scanner.scan_string
13
- when 'i'; memo << scanner.scan_integer
14
- when 'f'; memo << scanner.scan_float
15
- end
16
- memo
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()
17
19
  end
18
20
  arguments.unshift(message)
19
21
  end
20
22
 
21
- def self.encode(message=[])
23
+ # Turns a list or array into an encoded string
24
+ def self.encode(*message)
22
25
  message = [message].flatten
23
26
  string = pad(message.shift)
24
27
  string += encode_arguments(message)
25
28
  end
26
29
 
30
+ protected
31
+
27
32
  def self.pad(string='')
28
33
  string += "\000"
29
34
  string + "\000" * ((4-string.size) % 4)
30
35
  end
31
36
 
32
- protected
33
-
34
37
  def self.encode_arguments(arguments)
35
38
  encode_argument_types(arguments) + encode_argument_data(arguments)
36
39
  end
@@ -1,27 +1,28 @@
1
- class Datagrammer
2
- module PacketScanner
3
-
4
- def skip_buffer
5
- self.pos += 4 - (pos % 4)
6
- end
7
-
8
- def scan_string
9
- string = scan(/[^\000]+/)
10
- skip_buffer
11
- string
12
- end
13
-
14
- def scan_integer
15
- integer = scan(/.{4}/m).unpack('N').first
16
- integer = -1 * (2**32 - integer) if integer > (2**31 - 1)
17
- integer
18
- end
19
-
20
- def scan_float
21
- scan(/.{4}/).unpack('g').first
22
- end
23
-
1
+ # Subclass of StringScanner to take the boring out of certain aspects of packet decoding.
2
+ class PacketScanner < StringScanner
3
+
4
+ # Moves the scan head to the next multiple of 4
5
+ def skip_buffer
6
+ self.pos += 4 - (pos % 4)
24
7
  end
8
+
9
+ # Grabs all non-null characters and moves the scan head past the null buffer
10
+ def scan_string
11
+ string = scan(/[^\000]+/)
12
+ skip_buffer
13
+ string
14
+ end
15
+
16
+ # Decodes an integer, adjusting for polarity
17
+ def scan_integer
18
+ integer = scan(/.{4}/m).unpack('N').first
19
+ integer = -1 * (2**32 - integer) if integer > (2**31 - 1)
20
+ integer
21
+ end
22
+
23
+ # Decodes a 32-bit float
24
+ def scan_float
25
+ scan(/.{4}/).unpack('g').first
26
+ end
27
+
25
28
  end
26
-
27
- StringScanner.send(:include, Datagrammer::PacketScanner)
@@ -0,0 +1,60 @@
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
@@ -1,10 +1,10 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
- describe StringScanner do
3
+ describe PacketScanner do
4
4
 
5
5
  describe "skip_buffer" do
6
6
  it "sets the position to the next multiple of four" do
7
- s = StringScanner.new('more than four bytes')
7
+ s = PacketScanner.new('more than four bytes')
8
8
  [[4,8], [6,8], [1,4]].each do |pos, expected|
9
9
  s.pos = pos
10
10
  s.skip_buffer
@@ -16,7 +16,7 @@ describe StringScanner do
16
16
  describe "scan_string" do
17
17
 
18
18
  before do
19
- @s = StringScanner.new("this is a string!\000\000\000end.")
19
+ @s = PacketScanner.new("this is a string!\000\000\000end.")
20
20
  @string = @s.scan_string
21
21
  end
22
22
 
@@ -33,7 +33,7 @@ describe StringScanner do
33
33
  [[1,"\000\000\000\001"], [-10, "\377\377\377\366"], [2147483647, "\177\377\377\377"]].each do |integer, encoded|
34
34
  describe "decdoing #{integer}" do
35
35
  before do
36
- @s = StringScanner.new(encoded)
36
+ @s = PacketScanner.new(encoded)
37
37
  @int = @s.scan_integer
38
38
  end
39
39
 
@@ -52,7 +52,7 @@ describe StringScanner do
52
52
  [[1.0, "?\200\000\000"], [3.141593, "@I\017\333"], [-1.618034, "\277\317\e\275"]].each do |float, encoded|
53
53
  describe "decoding #{float}" do
54
54
  before do
55
- @s = StringScanner.new(encoded)
55
+ @s = PacketScanner.new(encoded)
56
56
  @float = @s.scan_float
57
57
  end
58
58
 
data/spec/packet_spec.rb CHANGED
@@ -41,13 +41,4 @@ describe Datagrammer::Packet do
41
41
  end
42
42
  end
43
43
 
44
- describe "pad" do
45
- it "fills out to the nearest word length" do
46
- Datagrammer::Packet.pad("h").should == "h\000\000\000"
47
- end
48
-
49
- it "appends nulls if string is already at wordlength" do
50
- Datagrammer::Packet.pad("word").should == "word\000\000\000\000"
51
- end
52
- end
53
44
  end
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.1.1
4
+ version: "0.2"
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-10-19 00:00:00 -07:00
12
+ date: 2008-11-01 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -19,20 +19,22 @@ executables: []
19
19
 
20
20
  extensions: []
21
21
 
22
- extra_rdoc_files: []
23
-
22
+ extra_rdoc_files:
23
+ - README.mkdn
24
24
  files:
25
25
  - README.mkdn
26
26
  - Rakefile
27
27
  - spec/datagrammer_spec.rb
28
+ - spec/generic_handler_spec.rb
28
29
  - spec/packet_scanner_spec.rb
29
30
  - spec/packet_spec.rb
30
31
  - spec/spec_helper.rb
31
32
  - lib/datagrammer
33
+ - lib/datagrammer/generic_handler.rb
32
34
  - lib/datagrammer/packet.rb
33
35
  - lib/datagrammer/packet_scanner.rb
34
36
  - lib/datagrammer.rb
35
- has_rdoc: false
37
+ has_rdoc: true
36
38
  homepage: http://github.com/mattly/datagrammer
37
39
  post_install_message:
38
40
  rdoc_options: []