noam_lemma 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 87f2a65c040cab0caea74d12a7e8d58d4ed36dd1
4
+ data.tar.gz: 0d9d0a71273809b6f28436a944fd69b2b9696c7e
5
+ SHA512:
6
+ metadata.gz: 1883fc59b41232681372d16811a62d4d73d135aca9fba538f74f924b693d7d3b06a3df0b8452a0db50b35a5a56b1f85ebbabdfbde15aafc75e577c677d7636d1
7
+ data.tar.gz: 4933af13dc4c016f0f2038cff34c1bff9d9d6af4b83e027c6238cd52b3b71aaae389dddc58fee98f8154234e780e2285529c2a5e252d840b8d79e515a829580c
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -r spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in noam_lemma.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 John Van Enk
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ noam\_lemma
2
+ ===============
3
+
4
+ A Noam Lemma implementation for Ruby.
5
+
6
+ This library exposes the fundamental concepts in a Lemma to Ruby developers. It
7
+ handles registration, subscription, and message processing. All one needs to do
8
+ to create a new Lemma in a network is interact with the Noam::Lemma class. See
9
+ the `example/` directory in the project for further details on usage.
10
+
11
+ Build the ruby gem from noam lemma directory
12
+ `gem build noam_lemma.gemspec`
13
+
14
+ Install the gem
15
+ `gem install noam_lemma-version.gem`
16
+
17
+ Known Issues
18
+ ------------
19
+
20
+ * Listening fails un-gracefully when the server goes down.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ require "./examples/lemma_verification"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ desc "perform verification tests"
9
+ task :verify do
10
+ Noam::LemmaVerification.run
11
+ end
@@ -0,0 +1,34 @@
1
+ require 'noam_lemma'
2
+
3
+ # This is an example of a Ruby Lemma that publishes message and *also* uses the
4
+ # "Guest" model of connection. This Lemma will advertise that it's available on
5
+ # the local network and only begin speaking messages once a server requests a
6
+ # connection from the Lemma.
7
+
8
+ publisher = Noam::Lemma.new('example-guest-publisher', [], ["e3"])
9
+
10
+ # Using the `advertise` method asks the Lemma to announce it's presence and
11
+ # wait for a message from a server that may want to connect to it.
12
+ #
13
+ # The "local-test" parameter is the room name. Servers with a room name that's
14
+ # the same as the Lemma's advertised room name will connect automatically.
15
+ publisher.advertise("")
16
+
17
+ seq = 0
18
+ e = "e3"
19
+ loop do
20
+ # Construct a value to send with the event.
21
+ v = {"seq" => seq, "time" => Time.now.to_s}
22
+
23
+ # If `speak` returns false, we're unable to speak the message likely because
24
+ # the socket has closed. The connection would have to be restarted.
25
+ unless publisher.speak(e, v)
26
+ puts "Done"
27
+ break
28
+ end
29
+ puts "Wrote: #{e} -> #{v.inspect}"
30
+
31
+ seq += 1
32
+ # Sleep for a while so that we don't bog down the network.
33
+ sleep(1)
34
+ end
@@ -0,0 +1,31 @@
1
+ require 'noam_lemma'
2
+
3
+ # This is an example of a Ruby Lemma that publishes message and *also* uses the
4
+ # "Guest" model of connection. This Lemma will advertise that it's available on
5
+ # the local network and only begin subscribing to messages once a server
6
+ # requests a connection from the Lemma.
7
+
8
+ subscriber = Noam::Lemma.new('example-guest-subscriber', ["e3"], [])
9
+
10
+ # Using the `advertise` method asks the Lemma to announce it's presence and
11
+ # wait for a message from a server that may want to connect to it.
12
+ #
13
+ # The "local-test" parameter is the room name. Servers with a room name that's
14
+ # the same as the Lemma's advertised room name will connect automatically.
15
+ subscriber.advertise("")
16
+
17
+ loop do
18
+ # The `listen` method will return an Event object once one is received by the
19
+ # Lemma. Until an event is heard, the `listen` method blocks.
20
+ m = subscriber.listen
21
+
22
+ # There's one special value that's returned from `listen`: the `:cancelled`
23
+ # symbol. If this shows up, it means some one else has called the `stop`
24
+ # method on the Lemma.
25
+ if :cancelled == m
26
+ puts "Done"
27
+ break
28
+ else
29
+ puts "Read: #{m.event} -> #{m.value.inspect}"
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ require "noam_lemma"
2
+
3
+ class Noam::LemmaVerification
4
+ def self.run
5
+ echo
6
+ plus_one
7
+ sum
8
+ name
9
+ end
10
+
11
+ def self.echo
12
+ lemma = Noam::Lemma.new("verification", ["Echo"], ["EchoVerify"])
13
+ lemma.advertise("lemma_verification")
14
+ event = lemma.listen
15
+ lemma.speak("EchoVerify", event.value)
16
+ lemma.stop
17
+ end
18
+
19
+ def self.plus_one
20
+ lemma = Noam::Lemma.new("verification", ["PlusOne"], ["PlusOneVerify"])
21
+ lemma.advertise("lemma_verification")
22
+ event = lemma.listen
23
+ lemma.speak("PlusOneVerify", event.value + 1)
24
+ lemma.stop
25
+ end
26
+
27
+ def self.sum
28
+ lemma = Noam::Lemma.new("verification", ["Sum"], ["SumVerify"])
29
+ lemma.advertise("lemma_verification")
30
+ event = lemma.listen
31
+ lemma.speak("SumVerify", event.value.inject {|sum, v| sum + v})
32
+ lemma.stop
33
+ end
34
+
35
+ def self.name
36
+ lemma = Noam::Lemma.new("verification", ["Name"], ["NameVerify"])
37
+ lemma.advertise("lemma_verification")
38
+ event = lemma.listen
39
+ fullname = "#{event.value["firstName"]} #{event.value["lastName"]}"
40
+ lemma.speak("NameVerify", {fullName: fullname})
41
+ lemma.stop
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ require 'noam_lemma'
2
+
3
+ # This is an example of a Ruby Lemma that publishes messages. It expects a Noam
4
+ # server to be running. It also expects that the noam-lemma.rb file is in the
5
+ # search path. If you run this example from the project root, the following
6
+ # command should work:
7
+ #
8
+ # ruby -Ilib example/publisher.rb
9
+ #
10
+ # This example _will not_ work on the same machine running the Noam server as
11
+ # both programs need to bind to UDP port 1030.
12
+
13
+ publisher = Noam::Lemma.new('example-publisher', [], ["e1", "e2"])
14
+
15
+ # Using the `discover` method asks the Lemma to proactively try and discover a
16
+ # server to connect to on the local network. Once the server is discovered, it
17
+ # will connect and send a Noam 'register' message. When `discover` returns, the
18
+ # Lemma is ready to send events.
19
+ publisher.discover
20
+
21
+ seq = 0
22
+ loop do
23
+ # This method block picks an event to send. It's randomized a bit so that we
24
+ # can see things change over time.
25
+ e = if 0.5 < rand()
26
+ "e1"
27
+ else
28
+ "e2"
29
+ end
30
+
31
+ # Next, package the event sequence and the current time into a value.
32
+ v = {"seq" => seq, "time" => Time.now.to_s}
33
+
34
+ # Attempt to speak the chosen event with the value. Note, the event is either
35
+ # "e1" or "e2" based on how rand() returned.
36
+ unless publisher.speak(e, v)
37
+ # If `speak` returns false, we're unable to speak the message likely because
38
+ # the socket has closed. The connection would have to be restarted.
39
+ puts "Done"
40
+ break
41
+ end
42
+ puts "Wrote: #{e} -> #{v.inspect}"
43
+
44
+ seq += 1
45
+ # Sleep for a while so that we don't bog down the network.
46
+ sleep(1)
47
+ end
@@ -0,0 +1,35 @@
1
+ require 'noam_lemma'
2
+
3
+ # This is an example of a Ruby Lemma that subscribes to messages. It expects a
4
+ # Noam server to be running. It also expects that the noam-lemma.rb file is in
5
+ # the search path. If you run this example from the project root, the following
6
+ # command should work:
7
+ #
8
+ # ruby -Ilib example/subscriber.rb
9
+ #
10
+ # This example _will not_ work on the same machine running the Noam server as
11
+ # both programs need to bind to UDP port 1030.
12
+
13
+ subscriber = Noam::Lemma.new('example-subscriber', ["e1", "e2"], [])
14
+
15
+ # Using the `discover` method asks the Lemma to proactively try and discover a
16
+ # server to connect to on the local network. Once the server is discovered, it
17
+ # will connect and send a Noam 'register' message. When `discover` returns, the
18
+ # Lemma is ready to receive events.
19
+ subscriber.discover
20
+
21
+ loop do
22
+ # The `listen` method will return an Event object once one is received by the
23
+ # Lemma. Until an event is heard, the `listen` method blocks.
24
+ m = subscriber.listen
25
+
26
+ # There's one special value that's returned from `listen`: the `:cancelled`
27
+ # symbol. If this shows up, it means some one else has called the `stop`
28
+ # method on the Lemma.
29
+ if :cancelled == m
30
+ puts "Done"
31
+ break
32
+ else
33
+ puts "Read: #{m.event} -> #{m.value.inspect}"
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ module Noam
2
+ class Beacon
3
+ attr_reader :name, :host, :port
4
+
5
+ def initialize(name, host, port)
6
+ @name = name
7
+ @host = host
8
+ @port = port
9
+ end
10
+
11
+ def self.discover(net = "0.0.0.0")
12
+ socket = UDPSocket.new
13
+ begin
14
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
15
+ socket.bind(net, Noam::BEACON_PORT)
16
+
17
+ raise "Didn't see beacon after #{WAIT_TIME} seconds." unless message_received?(socket)
18
+
19
+ data, addr = socket.recvfrom(MAX_RESPONSE_LENGTH)
20
+ parsed_data = JSON.parse(data)
21
+ Beacon.new(parsed_data[1], addr[2], parsed_data[2])
22
+ ensure
23
+ socket.close
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ MAX_RESPONSE_LENGTH = 1600
30
+ WAIT_TIME = 10.0
31
+
32
+ def self.message_received?(socket)
33
+ IO.select([socket], [], [], WAIT_TIME) != nil
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ require "noam_lemma/message_filter"
2
+
3
+ module Noam
4
+ class Lemma
5
+ attr_reader :name, :listener, :player, :speaks
6
+
7
+ def initialize(name, hears = [], speaks = [])
8
+ @name = name
9
+ @speaks = speaks
10
+ @player = nil
11
+ @listener = nil
12
+
13
+ initialize_message_filter(hears)
14
+ end
15
+
16
+ def discover(beacon = nil)
17
+ beacon ||= Beacon.discover
18
+ start(beacon.host, beacon.port)
19
+ end
20
+
21
+ def advertise(room_name)
22
+ marco = Noam::Message::Marco.new(room_name, @name)
23
+ polo = marco.start
24
+ start(polo.host, polo.port)
25
+ end
26
+
27
+ def speak(event, value)
28
+ if @player
29
+ @player.put(Noam::Message::Playable.new(@name, event, value))
30
+ true
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ def listen
37
+ @listener.take
38
+ end
39
+
40
+ def stop
41
+ @player.stop if @player
42
+ @listener.stop if @listener
43
+ @player = nil
44
+ @listener = nil
45
+ end
46
+
47
+ def hears
48
+ @message_filter.hears
49
+ end
50
+
51
+ def set_message_filter(message_filter)
52
+ @message_filter = message_filter
53
+ end
54
+
55
+ private
56
+
57
+ def start(host, port)
58
+ @listener = Listener.new
59
+ @player = Player.new(host, port)
60
+ @player.put(Message::Register.new(@name, @listener.port, @message_filter.hears, @speaks))
61
+ end
62
+
63
+ def initialize_message_filter(hears)
64
+ @message_filter = MessageFilter.new
65
+ hears.each do |event_name|
66
+ @message_filter.hear(event_name) {}
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,73 @@
1
+ require 'thread'
2
+
3
+ module Noam
4
+ class Listener
5
+ attr_reader :port
6
+
7
+ def initialize
8
+ @queue = Queue.new
9
+ @server = TCPServer.new(0)
10
+ @port = @server.addr[1]
11
+
12
+ manage_queue_on_thread
13
+ end
14
+
15
+ def take
16
+ @queue.pop
17
+ end
18
+
19
+ def stop
20
+ @exit_requested = true
21
+ @thread.join
22
+ end
23
+
24
+ private
25
+
26
+ def manage_queue_on_thread
27
+ @thread = Thread.new do |t|
28
+ begin
29
+ loop_listen
30
+ ensure
31
+ @server.close
32
+ end
33
+ end
34
+ end
35
+
36
+ def loop_listen
37
+ loop do
38
+ if client = listen_for_connection
39
+ read_from_client(client)
40
+ client.close
41
+ end
42
+
43
+ if exiting?
44
+ @queue.push(:cancelled)
45
+ break
46
+ end
47
+ end
48
+ end
49
+
50
+ def listen_for_connection
51
+ timeout_sec = 0.1
52
+ available_ios = select([@server], nil, nil, timeout_sec)
53
+ @server.accept if available_ios
54
+ end
55
+
56
+ def read_from_client(client)
57
+ begin
58
+ loop do
59
+ message_length = client.read_nonblock(Message::MESSAGE_LENGTH_STRING_SIZE).to_i
60
+ message_content = client.read_nonblock(message_length)
61
+ @queue.push(Message::Heard.from_noam(message_content))
62
+ break if exiting?
63
+ end
64
+ rescue IO::WaitReadable
65
+ retry unless exiting?
66
+ end
67
+ end
68
+
69
+ def exiting?
70
+ return @exit_requested
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ module Noam
2
+ module Message
3
+ class Heard
4
+ attr_reader :source, :event, :value
5
+
6
+ def initialize(source, event, value)
7
+ @source = source
8
+ @event = event
9
+ @value = value
10
+ end
11
+
12
+ def self.from_noam(noam)
13
+ _, source, event, value = JSON.parse(noam)
14
+ Heard.new(source, event, value)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ module Noam
5
+ module Message
6
+ class Marco
7
+ attr_reader :port
8
+
9
+ def initialize(room_name, lemma_name)
10
+ @room_name = room_name
11
+ @lemma_name = lemma_name
12
+ end
13
+
14
+ def start
15
+ bcast_socket = UDPSocket.new
16
+ reply_socket = UDPSocket.new
17
+ reply_socket.bind("0.0.0.0", 0)
18
+
19
+ begin
20
+ bcast_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
21
+
22
+ loop do
23
+ bcast_socket.send(noam_encode, 0, "255.255.255.255", Noam::BEACON_PORT)
24
+ if message_received?(bcast_socket)
25
+ break
26
+ end
27
+ end
28
+
29
+ get_polo_response(bcast_socket)
30
+ ensure
31
+ reply_socket.close
32
+ end
33
+ end
34
+
35
+ def noam_encode
36
+ ["marco", @lemma_name, @room_name, Noam::DEVICE_TYPE, Noam::VERSION].to_json
37
+ end
38
+
39
+ private
40
+
41
+ MAX_RESPONSE_LENGTH = 1600
42
+ WAIT_TIME = 5.0
43
+
44
+ def message_received?(socket)
45
+ IO.select([socket], [], [], WAIT_TIME)
46
+ end
47
+
48
+ def get_polo_response(socket)
49
+ message, sockaddr = socket.recvfrom(MAX_RESPONSE_LENGTH)
50
+ _, _, noam_port = JSON.parse(message)
51
+ _, _, addr, _ = sockaddr
52
+
53
+ Polo.new(addr, noam_port)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ module Noam
2
+ module Message
3
+ class Playable
4
+ def initialize(host_id, event, value)
5
+ @host_id = host_id
6
+ @event = event
7
+ @value = value
8
+ end
9
+
10
+ def noam_encode
11
+ j = ['event', @host_id, @event, @value].to_json
12
+ Noam::Message.encode_length(j.length) + j
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module Noam
2
+ module Message
3
+ class Polo
4
+ attr_reader :host, :port
5
+
6
+ class InvalidHost < Exception; end
7
+ class InvalidPort < Exception; end
8
+
9
+ def initialize(host, port)
10
+ raise InvalidHost.new if (@host = host).nil?
11
+ raise InvalidPort.new if (@port = port).nil?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Noam
2
+ module Message
3
+ class Register
4
+ def initialize(device_id, port, hears, speaks)
5
+ @device_id = device_id
6
+ @port = port
7
+ @hears = hears
8
+ @speaks = speaks
9
+ end
10
+
11
+ def noam_encode
12
+ j = ["register", @device_id, @port.to_i, @hears, @speaks, Noam::DEVICE_TYPE, Noam::VERSION].to_json
13
+ Noam::Message.encode_length(j.length) + j
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Noam
2
+ module Message
3
+ MESSAGE_LENGTH_STRING_SIZE = 6
4
+
5
+ def self.encode_length(l)
6
+ ("%06u" % l)
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'noam_lemma/message/heard'
12
+ require 'noam_lemma/message/marco'
13
+ require 'noam_lemma/message/playable'
14
+ require 'noam_lemma/message/polo'
15
+ require 'noam_lemma/message/register'
@@ -0,0 +1,24 @@
1
+ module Noam
2
+ class MessageFilter
3
+ def initialize
4
+ @hears = {}
5
+ end
6
+
7
+ def hear(event_name, &block)
8
+ @hears[event_name] ||= []
9
+ @hears[event_name] << block
10
+ end
11
+
12
+ def receive(message)
13
+ blocks = @hears[message.event] || []
14
+ blocks.each do |block|
15
+ block.call(message)
16
+ end
17
+ message
18
+ end
19
+
20
+ def hears
21
+ @hears.keys.uniq
22
+ end
23
+ end
24
+ end