mqtt_pipe 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 93caadb951ff60325a712d56794e4431adbcf621
4
+ data.tar.gz: b9a8f6b3b8a31d2925601444bad95bb054e00c5e
5
+ SHA512:
6
+ metadata.gz: bf62b6c097b52d0770b64b5bf63dc299a399120a525eb4682b03a92f74d74ce3734fe6221c6008c9620774d4c3c28e009b4196d1a0492f822886fbe83b53e645
7
+ data.tar.gz: 9e8cf7c06b4e8a625bff2944337bbf90bb8b180f16e898d1127417a47fc34641f030baec5fb0f0e860023eed030f268bfde664a318ed4de36695843efc30e292
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mqtt_pipe.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Sebastian Lindberg
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.
@@ -0,0 +1,43 @@
1
+ # MQTTPipe
2
+
3
+ This gem wraps the [MQTT gem](https://github.com/njh/ruby-mqtt) and adds a serializer for simple data structures.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mqtt_pipe'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install mqtt_pipe
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ pipe = MQTTPipe.create do
25
+ on 'hello/world/#' do |message, id|
26
+ p message, id
27
+ end
28
+ end
29
+
30
+ pipe.open 'test.mosquitto.org', port: 1883 do
31
+ 100.times do |i|
32
+ send "hello/world/#{i}", Time.now
33
+ end
34
+ end
35
+ ```
36
+
37
+ ## Contributing
38
+
39
+ 1. Fork it ( https://github.com/[my-github-username]/mqtt_pipe/fork )
40
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
41
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
42
+ 4. Push to the branch (`git push origin my-new-feature`)
43
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,9 @@
1
+ require 'mqtt_pipe'
2
+
3
+ pipe = MQTTPipe.create do
4
+ on 'hello/world/#' do |message, id|
5
+ p message, id
6
+ end
7
+ end
8
+
9
+ pipe.open 'test.mosquitto.org', port: 1883
@@ -0,0 +1,13 @@
1
+ require 'mqtt_pipe'
2
+
3
+ pipe = MQTTPipe.create
4
+
5
+ pipe.open 'test.mosquitto.org', port: 1883 do
6
+ counter = 0
7
+ loop do
8
+ send 'hello/world/' + counter.to_s, Time.now
9
+ puts 'sending...'
10
+ counter += 1
11
+ sleep 1
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'mqtt'
2
+
3
+ require 'mqtt_pipe/version'
4
+ require 'mqtt_pipe/types'
5
+ require 'mqtt_pipe/packer'
6
+ require 'mqtt_pipe/config'
7
+ require 'mqtt_pipe/listener'
8
+ require 'mqtt_pipe/pipe'
9
+
10
+ module MQTTPipe
11
+ extend self
12
+
13
+ def create &block
14
+ Pipe.new &block
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module MQTTPipe
2
+
3
+ ##
4
+ # An instance of Config is used as the context in which
5
+ # the pipe is configured.
6
+
7
+ class Config
8
+ attr_reader :listeners
9
+
10
+ def initialize
11
+ @listeners = []
12
+ end
13
+
14
+ ##
15
+ # Subscribe to a topic and attatch an action that will
16
+ # be called once a message with a matching topic is
17
+ # received.
18
+
19
+ def on topic, &action
20
+ raise ArgumentError, 'No block given' if action.nil?
21
+ @listeners << Listener.new(topic, &action)
22
+ end
23
+
24
+ ##
25
+ # Subscribe to all topics
26
+
27
+ def on_anything &action
28
+ on '#', &action
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ module MQTTPipe
2
+
3
+ ##
4
+ # Used to store topics along with their actions.
5
+ # Contains conveniens methods for matching the topic to a
6
+ # given string as well as calling the action.
7
+
8
+ class Listener
9
+ attr_reader :topic, :pattern
10
+
11
+ ##
12
+ # The listener requires a topic string and a callable
13
+ # action to initialize.
14
+ #
15
+ # An ArgumentError is raised if no action is given
16
+
17
+ def initialize topic, &action
18
+ raise ArgumentError, 'No block given' if action.nil?
19
+
20
+ @topic = topic
21
+ @action = action
22
+
23
+ pattern = topic.gsub('*', '([^/]+)').gsub('/#', '/?(.*)')
24
+ @pattern = %r{^#{pattern}$}
25
+ end
26
+
27
+ ##
28
+ # Check if a given topic string matches the listener
29
+ # topic.
30
+ #
31
+ # Returns an array containing any matched sections of
32
+ # topic, if there was a match. False otherwise.
33
+
34
+ def match topic
35
+ m = @pattern.match topic
36
+ m.nil? ? false : m.captures
37
+ end
38
+
39
+ ##
40
+ # Returns true if the topic matches listener topic
41
+ # Otherwise false.
42
+
43
+ def === topic
44
+ @pattern === topic
45
+ end
46
+
47
+ ##
48
+ # Call the listener action
49
+
50
+ def call *args
51
+ #raise ArgumentError, 'No value provided' if args.empty?
52
+ @action.call *args
53
+ end
54
+
55
+ alias_method :run, :call
56
+ end
57
+ end
@@ -0,0 +1,83 @@
1
+ module MQTTPipe
2
+
3
+ ##
4
+ # The packer module is used to pack/unpack classes that
5
+ # supports it.
6
+
7
+ module Packer
8
+ extend self
9
+
10
+ ##
11
+ # Raised when the packet being unpacked is badly
12
+ # formatted.
13
+
14
+ class FormatError < StandardError; end
15
+
16
+ ##
17
+ # Used to signal the end of a packet as it is being
18
+ # unpacked.
19
+
20
+ class EndOfPacket < StandardError; end
21
+
22
+ # Use the refinements made to the supported classes
23
+
24
+ using Types
25
+
26
+ ##
27
+ # Packs the arguments acording to their type.
28
+ #
29
+ # An ArgumentError is raised if any given class does
30
+ # not support packing.
31
+
32
+ def pack *values
33
+ values.map{|value| value.to_packed }.join
34
+ rescue NoMethodError
35
+ raise ArgumentError, 'Unknown input format'
36
+ end
37
+
38
+ alias_method :[], :pack
39
+
40
+
41
+ ##
42
+ # Unpacks a serialized object and returns an array of
43
+ # the original values.
44
+
45
+ def unpack raw, limit: nil
46
+ raw = StringIO.new raw unless raw.respond_to? :read
47
+ result = []
48
+
49
+ # Either loop infinately or the number of times
50
+ # specified by limit
51
+
52
+ (limit.nil? ? loop : limit.times).each do
53
+ result << unpack_single(raw)
54
+ end
55
+
56
+ return result
57
+ rescue EndOfPacket
58
+ return result
59
+ end
60
+
61
+ ##
62
+ # A simple helper method to read a given number of bytes
63
+ # +from+ IO object and format them +as+ anything
64
+ # supported by Array#unpack.
65
+
66
+ def read_packed_bytes n = 1, from:, as: 'C'
67
+ raw = from.read(n)
68
+ raise FormatError if raw.nil? or raw.length != n
69
+
70
+ raw.unpack(as).first
71
+ end
72
+
73
+ private
74
+
75
+ def unpack_single raw
76
+ code = raw.read 1
77
+ raise EndOfPacket if code.nil?
78
+
79
+ type = code.unpack(?C).first
80
+ Types::Type.lookup(type).from_packed type, raw
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,88 @@
1
+ module MQTTPipe
2
+
3
+ ##
4
+ # The actual wrapper class for MQTT
5
+
6
+ class Pipe
7
+
8
+ ##
9
+ # Raised when the connection unexpectedly lost.
10
+
11
+ class ConnectionError < StandardError; end
12
+
13
+
14
+ def initialize &block
15
+ @config = Config.new
16
+ @config.instance_eval &block unless block.nil?
17
+ end
18
+
19
+ ##
20
+ # Open the pipe
21
+
22
+ def open host, port: 1883, &block
23
+ MQTT::Client.connect host: host, port: port do |client|
24
+
25
+ # Subscribe
26
+ topics = @config.listeners.map{|listener| listener.topic }
27
+ listener_thread = nil
28
+
29
+ unless topics.empty?
30
+ listener_thread = Thread.new do
31
+ client.get do |topic, data|
32
+ begin
33
+ unpacked_data = Packer.unpack data
34
+
35
+ @config.listeners.each do |listener|
36
+ if m = listener.match(topic)
37
+ listener.call unpacked_data, *m
38
+ end
39
+ end
40
+
41
+ rescue Packer::FormatError
42
+ # TODO: Handle more gracefully
43
+ puts 'Could not parse data!'
44
+ next
45
+ end
46
+ end
47
+ end
48
+
49
+ client.subscribe *topics
50
+ end
51
+
52
+ unless block.nil?
53
+ context = Context.new client
54
+
55
+ begin
56
+ context.instance_eval &block
57
+ rescue ConnectionError
58
+
59
+ puts 'Need to reconnect'
60
+ rescue Interrupt
61
+ ensure
62
+ listener_thread.exit unless topics.empty?
63
+ end
64
+
65
+ else
66
+ begin
67
+ listener_thread.join unless topics.empty?
68
+ rescue Interrupt
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ class Context
77
+ def initialize client
78
+ @client = client
79
+ end
80
+
81
+ def send topic, *data
82
+ raise ConnectionError unless @client.connected?
83
+ @client.publish topic, Packer.pack(*data)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
@@ -0,0 +1,11 @@
1
+ require_relative 'types/string'
2
+ require_relative 'types/nil'
3
+ require_relative 'types/false'
4
+ require_relative 'types/true'
5
+ require_relative 'types/integer'
6
+ require_relative 'types/float'
7
+ require_relative 'types/time'
8
+ require_relative 'types/color'
9
+ require_relative 'types/array'
10
+ require_relative 'types/class'
11
+ require_relative 'types/type'
@@ -0,0 +1,36 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine Array.singleton_class do
4
+ def packer_code; 0x80; end
5
+
6
+ def from_packed type, raw
7
+ length = if type == packer_code
8
+ Packer.read_packed_bytes(1, from: raw) + 31
9
+ else
10
+ type - packer_code
11
+ end
12
+
13
+ array = Packer.unpack raw, limit: length
14
+ raise Packer::FormatError, 'Badly formatted array' unless array.length == length
15
+
16
+ return array
17
+ end
18
+ end
19
+
20
+ refine Array do
21
+ def to_packed
22
+ header = case length
23
+ when 0 then return nil.to_packed
24
+ when 1..31
25
+ [self.class.packer_code + length].pack(?C)
26
+ when 32..288
27
+ [self.class.packer_code, length - 31].pack('C2')
28
+ else
29
+ raise ArgumentError, 'Array is too long'
30
+ end
31
+
32
+ header + map{|v| v.to_packed }.join
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine Class do
4
+ def to_packed
5
+ [Type.packer_code, self.packer_code].pack 'C2'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ class Color
4
+ PACKER_CODE = 0xC9
5
+
6
+ attr_reader :r, :g, :b
7
+
8
+ def initialize r, g, b
9
+ @r, @g, @b = r, g, b
10
+ end
11
+
12
+ def to_packed
13
+ [PACKER_CODE, r, g, b].pack 'C4'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine FalseClass.singleton_class do
4
+ def packer_code; 0xC2; end
5
+
6
+ def from_packed type, _
7
+ type == packer_code ? false : true
8
+ end
9
+ end
10
+
11
+ refine FalseClass do
12
+ def to_packed
13
+ [self.class.packer_code].pack ?C
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine Float.singleton_class do
4
+ def packer_code; 0xC7; end
5
+
6
+ def from_packed _, raw
7
+ Packer.read_packed_bytes 4, from: raw, as: 'e'
8
+ end
9
+ end
10
+
11
+ refine Float do
12
+ def to_packed
13
+ [self.class.packer_code, self].pack 'Ce'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine Integer.singleton_class do
4
+ def packer_code; 0xC4; end
5
+
6
+ def from_packed type, raw
7
+ case type
8
+ when 0xC4
9
+ Packer.read_packed_bytes 1, from: raw
10
+ when 0xC5
11
+ Packer.read_packed_bytes 2, from: raw, as: 's<'
12
+ when 0xC6
13
+ Packer.read_packed_bytes 4, from: raw, as: 'l<'
14
+ when 0..0x7F, 0xD0..0xFF
15
+ [type].pack('C').unpack('c').first
16
+ end
17
+ end
18
+ end
19
+
20
+ refine Integer do
21
+ def to_packed
22
+ case self
23
+ when -48..127
24
+ [self].pack ?C
25
+ when 0..255
26
+ [self.class.packer_code, self].pack 'C2'
27
+ when -32_768..32_767
28
+ [self.class.packer_code + 1, self].pack 'Cs<'
29
+ when -2_147_483_648..2_147_483_647
30
+ [self.class.packer_code + 2, self].pack 'Cl<'
31
+ else
32
+ raise ArgumentError, 'Integer is larger than 32 bit signed'
33
+ end
34
+ end
35
+
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine NilClass.singleton_class do
4
+ def packer_code; 0xC1; end
5
+
6
+ def from_packed _, _
7
+ return nil
8
+ end
9
+ end
10
+
11
+ refine NilClass do
12
+ def to_packed
13
+ [self.class.packer_code].pack ?C
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine String.singleton_class do
4
+ def packer_code; 0xA0; end
5
+
6
+ def from_packed type, raw
7
+ length = if type == packer_code
8
+ Packer.read_packed_bytes(1, from: raw) + 31
9
+ else
10
+ type - packer_code
11
+ end
12
+ Packer.read_packed_bytes length, from: raw, as: 'A*'
13
+ end
14
+ end
15
+
16
+ refine String do
17
+ def to_packed
18
+ case length
19
+ when 0 then return nil.to_packed
20
+ when 1..31
21
+ [self.class.packer_code + length, self].pack('CA*')
22
+ when 32..288
23
+ [self.class.packer_code, length - 31, self].pack('C2A*')
24
+ else
25
+ raise ArgumentError, 'String is too long'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine Time.singleton_class do
4
+ def packer_code; 0xC8; end
5
+
6
+ def from_packed _, raw
7
+ at(Packer.read_packed_bytes 4, from: raw, as: 'L<')
8
+ end
9
+ end
10
+
11
+ refine Time do
12
+ def to_packed
13
+ [self.class.packer_code, to_i].pack 'CL<'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module MQTTPipe
2
+ module Types
3
+ refine TrueClass.singleton_class do
4
+ def packer_code; 0xC2; end
5
+
6
+ def from_packed type, _
7
+ type == packer_code ? false : true
8
+ end
9
+ end
10
+
11
+ refine TrueClass do
12
+ def to_packed
13
+ [self.class.packer_code + 1].pack ?C
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module MQTTPipe
2
+ module Types
3
+ module Type
4
+ extend self
5
+
6
+ def packer_code
7
+ 0xC0
8
+ end
9
+
10
+ def to_packed
11
+ [packer_code, packer_code].pack 'C2'
12
+ end
13
+
14
+ def lookup type
15
+ case type
16
+ when 0x80..0x9F then Array
17
+ when 0xA0..0xBF then String
18
+ when 0xC0 then Type
19
+ when 0xC1 then NilClass
20
+ when 0xC2 then FalseClass
21
+ when 0xC3 then TrueClass
22
+ when 0xC7 then Float
23
+ when 0xC8 then Time
24
+ when 0xC9 then Color
25
+ when 0x00..0x7F,
26
+ 0xD0..0xFF,
27
+ 0xC4..0xC6 then Integer
28
+ end
29
+ end
30
+
31
+ def from_packed _, raw
32
+ lookup(Packer.read_packed_bytes 1, from: raw)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module MQTTPipe
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mqtt_pipe/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mqtt_pipe"
8
+ spec.version = MQTTPipe::VERSION
9
+ spec.authors = ["Sebastian Lindberg"]
10
+ spec.email = ["seb.lindberg@gmail.com"]
11
+ spec.summary = %q{A gem for sending a small set of objects via MQTT.}
12
+ spec.description = %q{This gem wraps the MQTT gem by njh (on Github) and adds a serializer for simple data structures.}
13
+ spec.homepage = "https://github.com/seblindberg/ruby-mqtt-pipe"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "mqtt", "~> 0.3"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.3"
26
+ end
@@ -0,0 +1,27 @@
1
+ require 'mqtt_pipe'
2
+
3
+ describe MQTTPipe::Config do
4
+ let(:klass) { MQTTPipe::Config }
5
+
6
+ before :each do
7
+ @config = klass.new
8
+ end
9
+
10
+ describe '#on' do
11
+ it 'requires one argument and a block' do
12
+ expect{@config.on}.to raise_error(ArgumentError)
13
+ expect{@config.on 'test'}.to raise_error(ArgumentError)
14
+ expect{@config.on('test') {}}.not_to raise_error
15
+ end
16
+
17
+ it 'stores the listener' do
18
+ expect(@config.listeners.length).to eq(0)
19
+
20
+ @config.on('test') {}
21
+
22
+ expect(@config.listeners.length).to eq(1)
23
+ end
24
+ end
25
+
26
+
27
+ end
@@ -0,0 +1,87 @@
1
+ require 'mqtt_pipe'
2
+
3
+ describe MQTTPipe::Listener do
4
+ let(:klass) { MQTTPipe::Listener }
5
+ let(:topic) { 'test/*/topic/#' }
6
+ let(:matching_topic_1) { 'test/some/topic' }
7
+ let(:matching_topic_2) { 'test/some/topic/with/more/5' }
8
+ let(:non_matching_topic) { 'test/topic/with/8' }
9
+
10
+ describe '#new' do
11
+ it 'expects a topic and an action block' do
12
+ expect{klass.new}.to raise_error(ArgumentError)
13
+ expect{klass.new topic}.to raise_error(ArgumentError)
14
+ expect{klass.new(topic) {}}.not_to raise_error
15
+ end
16
+ end
17
+
18
+ context 'Using the listener' do
19
+ before :each do
20
+ @listener = klass.new(topic) {|value, captures = 2| value * captures.to_i }
21
+ end
22
+
23
+ describe '#topic' do
24
+ it 'returns the topic' do
25
+ expect(@listener.topic).to eq topic
26
+ end
27
+ end
28
+
29
+ describe '#pattern' do
30
+ it 'returns a regular expression mathing the pattern' do
31
+ pattern = @listener.pattern
32
+
33
+ expect(pattern).to be_a(Regexp)
34
+ expect(pattern === topic).to be true
35
+ end
36
+
37
+ it 'leaves lopics without wildcards as is' do
38
+ listener = klass.new('test/of') {}
39
+ expect(listener.pattern === 'test/of').to be_truthy
40
+ expect(listener.pattern).to eq %r{^test/of$}
41
+ end
42
+ end
43
+
44
+ describe '#match' do
45
+ it 'requires one argument' do
46
+ expect{@listener.match}.to raise_error ArgumentError
47
+ end
48
+
49
+ it 'returns true for a matching topic' do
50
+ expect(@listener.match matching_topic_1).to be_truthy
51
+ expect(@listener.match matching_topic_2).to be_truthy
52
+ end
53
+
54
+ it 'returns nil for a non matching topic' do
55
+ expect(@listener.match non_matching_topic).to be false
56
+ end
57
+
58
+ it 'also responds to #===' do
59
+ expect(@listener === matching_topic_1).to be true
60
+ expect(@listener === non_matching_topic).to be false
61
+ end
62
+
63
+ it 'captures wildcard match groups' do
64
+ m = @listener.match matching_topic_2
65
+
66
+ expect(m).not_to be false
67
+ expect(m[0]).to eq 'some'
68
+ expect(m[1]).to eq 'with/more/5'
69
+ end
70
+ end
71
+
72
+ describe '#call' do
73
+ it 'runs the given callback' do
74
+ expect(@listener.call 42).to eq 84
75
+ end
76
+
77
+ it 'accepts an optional second argument' do
78
+ expect(@listener.call 12, 3).to eq 12*3
79
+ end
80
+
81
+ it 'also responds to #run' do
82
+ expect(@listener.run 42).to eq 84
83
+ end
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,264 @@
1
+ require 'mqtt_pipe'
2
+
3
+ describe MQTTPipe::Packer do
4
+ let(:klass) { MQTTPipe::Packer }
5
+
6
+ describe '#pack/#[]' do
7
+ describe 'Array' do
8
+ it 'serializes empty arrays to nil' do
9
+ expect(klass[[]]).to eq [0xC1].pack('C')
10
+ end
11
+
12
+ it 'serializes short arrays' do
13
+ arr = (0..4).to_a
14
+ expect(klass[arr]).to eq [0x85, 0, 1, 2, 3, 4].pack('C*')
15
+ end
16
+
17
+ it 'serializes long arrays' do
18
+ arr = (1..100).to_a
19
+ expect(klass[arr].length).to be 102
20
+ end
21
+ end
22
+
23
+ describe 'String' do
24
+ it 'serializes empty strings to nil' do
25
+ expect(klass['']).to eq [0xC1].pack('C')
26
+ end
27
+
28
+ it 'serializes short strings' do
29
+ expect{klass['string']}.not_to raise_error
30
+ expect(klass['string']).to eq [0xA6, 'string'].pack('CA*')
31
+ end
32
+
33
+ it 'serializes long strings' do
34
+ long_string = '*' * 33
35
+ expect(klass[long_string]).to eq [0xA0, long_string.length - 31, long_string].pack('C2A*')
36
+ end
37
+
38
+ it 'does not serialize string longer than 288 chars' do
39
+ too_long_string = '*' * 289
40
+ expect{klass[too_long_string]}.to raise_error ArgumentError
41
+ end
42
+ end
43
+
44
+ describe 'Type' do
45
+ it 'serializes classes' do
46
+ expect(klass[MQTTPipe::Types::Type]).to eq [0xC0, 0xC0].pack('C2')
47
+
48
+ expect(klass[NilClass]).to eq [0xC0, 0xC1].pack('C2')
49
+
50
+ expect(klass[FalseClass]).to eq [0xC0, 0xC2].pack('C2')
51
+ expect(klass[TrueClass]).to eq [0xC0, 0xC2].pack('C2')
52
+
53
+ expect(klass[Array]).to eq [0xC0, 0x80].pack('C2')
54
+ expect(klass[String]).to eq [0xC0, 0xA0].pack('C2')
55
+
56
+ expect(klass[Fixnum]).to eq [0xC0, 0xC4].pack('C2')
57
+ expect(klass[Integer]).to eq [0xC0, 0xC4].pack('C2')
58
+
59
+ expect(klass[Float]).to eq [0xC0, 0xC7].pack('C2')
60
+ expect(klass[Time]).to eq [0xC0, 0xC8].pack('C2')
61
+ end
62
+ end
63
+
64
+ describe 'Nil' do
65
+ it 'serializes nil values' do
66
+ expect(klass[nil]).to eq [0xC1].pack(?C)
67
+ end
68
+ end
69
+
70
+ describe 'Boolean' do
71
+ it 'serializes false values' do
72
+ expect(klass[false]).to eq [0xC2].pack(?C)
73
+ end
74
+
75
+ it 'serializes true values' do
76
+ expect(klass[true]).to eq [0xC3].pack(?C)
77
+ end
78
+ end
79
+
80
+ describe 'Integer' do
81
+ it 'serializes small integers' do
82
+ expect(klass[4]).to eq [4].pack(?c)
83
+ expect(klass[127]).to eq [127].pack(?c)
84
+ expect(klass[-48]).to eq [-48].pack(?c)
85
+ end
86
+
87
+ it 'serializes bytes' do
88
+ expect(klass[128]).to eq [0xC4, 128].pack('C2')
89
+ expect(klass[255]).to eq [0xC4, 255].pack('C2')
90
+ end
91
+
92
+ it 'serializes short integers' do
93
+ expect(klass[32_767]).to eq [0xC5, 32_767].pack('Cs<')
94
+ expect(klass[-32_768]).to eq [0xC5, -32_768].pack('Cs<')
95
+ end
96
+
97
+ it 'serializes integers' do
98
+ expect(klass[2_147_483_647]).to eq [0xC6, 2_147_483_647].pack('Cl<')
99
+ expect(klass[-2_147_483_648]).to eq [0xC6, -2_147_483_648].pack('Cl<')
100
+ end
101
+
102
+ it 'does not serialize integers larger than 32 bit' do
103
+ expect{klass[2_147_483_648]}.to raise_error ArgumentError
104
+ expect{klass[-2_147_483_649]}.to raise_error ArgumentError
105
+ end
106
+ end
107
+
108
+ describe 'Float' do
109
+ it 'serializes 32 bit floats' do
110
+ expect(klass[0.2]).to eq [0xC7, 0.2].pack('Ce')
111
+ end
112
+ end
113
+
114
+ describe 'Time' do
115
+ it 'serializes time' do
116
+ timestamp = Time.now
117
+ expect(klass[timestamp]).to eq [0xC8, timestamp.to_i].pack('CL<')
118
+ end
119
+ end
120
+ end
121
+
122
+
123
+ describe '#unpack' do
124
+ it 'raises an error on malformated packets' do
125
+ # Make the list one item longer than what is written
126
+ raw = [0x87, *(1..6).to_a].pack 'CC*'
127
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
128
+
129
+ # Strings
130
+ raw = [0xA7, 'string'].pack 'CA*'
131
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
132
+
133
+ # Integers
134
+ raw = [0xC4].pack 'C'
135
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
136
+
137
+ raw = [0xC5, 1].pack 'C*'
138
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
139
+
140
+ raw = [0xC6, 1,2,3].pack 'C*'
141
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
142
+
143
+ # Float
144
+ raw = [0xC7].pack 'C*'
145
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
146
+
147
+ # Time
148
+ raw = [0xC8, 1].pack 'C*'
149
+ expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
150
+ end
151
+
152
+ describe 'Array' do
153
+ it 'deserializes short arrays' do
154
+ raw = [0x86, *(1..6).to_a].pack 'CC6'
155
+ expect(klass.unpack(raw).first).to eq (1..6).to_a
156
+ end
157
+ end
158
+
159
+ describe 'String' do
160
+ it 'deserializes short strings' do
161
+ raw = [0xA6, 'string'].pack 'CA*'
162
+
163
+ expect(klass.unpack(raw).first).to eq 'string'
164
+ end
165
+
166
+ it 'deserializes long strings' do
167
+ long_string = '*' * 33
168
+ raw = [0xA0, long_string.length - 31, long_string].pack 'C2A*'
169
+
170
+ expect(klass.unpack(raw).first).to eq long_string
171
+ end
172
+ end
173
+
174
+ describe 'Type' do
175
+ it 'deserializes types' do
176
+ raw = [0xC0, 0xC0].pack 'C2'
177
+ expect(klass.unpack(raw).first).to be MQTTPipe::Types::Type
178
+ end
179
+ end
180
+
181
+ describe 'Nil' do
182
+ it 'deserializes nil' do
183
+ raw = [0xC1].pack 'C'
184
+ expect(klass.unpack(raw).first).to be nil
185
+ end
186
+ end
187
+
188
+ describe 'Boolean' do
189
+ it 'deserializes to false' do
190
+ raw = [0xC2].pack 'C'
191
+ expect(klass.unpack(raw).first).to be false
192
+ end
193
+
194
+ it 'deserializes to true' do
195
+ raw = [0xC3].pack 'C'
196
+ expect(klass.unpack(raw).first).to be true
197
+ end
198
+ end
199
+
200
+ describe 'Integer' do
201
+ it 'deserializes tiny integers' do
202
+ raw = [127].pack ?C
203
+ expect(klass.unpack(raw).first).to eq 127
204
+
205
+ raw = [-48].pack ?C
206
+ expect(klass.unpack(raw).first).to eq -48
207
+ end
208
+
209
+ it 'deserializes bytes' do
210
+ raw = [0xC4, 128].pack 'C2'
211
+ expect(klass.unpack(raw).first).to eq 128
212
+ end
213
+
214
+ it 'deserializes short integers' do
215
+ raw = [0xC5, 32_767].pack 'Cs<'
216
+ expect(klass.unpack(raw).first).to eq 32_767
217
+
218
+ raw = [0xC5, -32_768].pack 'Cs<'
219
+ expect(klass.unpack(raw).first).to eq -32_768
220
+ end
221
+
222
+ it 'deserializes long integers' do
223
+ raw = [0xC6, 2_147_483_647].pack 'Cl<'
224
+ expect(klass.unpack(raw).first).to eq 2_147_483_647
225
+
226
+ raw = [0xC6, -2_147_483_648].pack 'Cl<'
227
+ expect(klass.unpack(raw).first).to eq -2_147_483_648
228
+ end
229
+ end
230
+
231
+ describe 'Float' do
232
+ it 'deserializes float' do
233
+ raw = [0xC7, 0.4].pack 'Ce'
234
+ expect(klass.unpack(raw).first).to be_within(0.0001).of(0.4)
235
+ end
236
+ end
237
+
238
+ describe 'Time' do
239
+ it 'deserializes time' do
240
+ t = Time.now
241
+ raw = [0xC8, t.to_i].pack 'CL<'
242
+ expect(klass.unpack(raw).first).to be_a Time
243
+ expect(klass.unpack(raw).first - t).to be_within(1).of(0)
244
+ end
245
+ end
246
+ end
247
+
248
+ describe 'End to end' do
249
+ it 'can serialize and deserialize an object' do
250
+ packet = ['Hello there!', 42, 42_531, ['hi', Time], true, []]
251
+
252
+ req = klass.pack *packet
253
+ res = klass.unpack req
254
+
255
+ expect(res[0]).to eq 'Hello there!'
256
+ expect(res[1]).to eq 42
257
+ expect(res[2]).to eq 42_531
258
+ expect(res[3][0]).to eq 'hi'
259
+ expect(res[3][1]).to eq Time
260
+ expect(res[4]).to eq true
261
+ expect(res[5]).to eq nil
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,9 @@
1
+ require 'mqtt_pipe'
2
+
3
+ describe MQTTPipe do
4
+ describe '#create' do
5
+ it 'returns a pipe' do
6
+ expect(MQTTPipe.create).to be_a MQTTPipe::Pipe
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mqtt_pipe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sebastian Lindberg
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mqtt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.3'
69
+ description: This gem wraps the MQTT gem by njh (on Github) and adds a serializer
70
+ for simple data structures.
71
+ email:
72
+ - seb.lindberg@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - examples/receiver.rb
83
+ - examples/sender.rb
84
+ - lib/mqtt_pipe.rb
85
+ - lib/mqtt_pipe/config.rb
86
+ - lib/mqtt_pipe/listener.rb
87
+ - lib/mqtt_pipe/packer.rb
88
+ - lib/mqtt_pipe/pipe.rb
89
+ - lib/mqtt_pipe/types.rb
90
+ - lib/mqtt_pipe/types/array.rb
91
+ - lib/mqtt_pipe/types/class.rb
92
+ - lib/mqtt_pipe/types/color.rb
93
+ - lib/mqtt_pipe/types/false.rb
94
+ - lib/mqtt_pipe/types/float.rb
95
+ - lib/mqtt_pipe/types/integer.rb
96
+ - lib/mqtt_pipe/types/nil.rb
97
+ - lib/mqtt_pipe/types/string.rb
98
+ - lib/mqtt_pipe/types/time.rb
99
+ - lib/mqtt_pipe/types/true.rb
100
+ - lib/mqtt_pipe/types/type.rb
101
+ - lib/mqtt_pipe/version.rb
102
+ - mqtt_pipe.gemspec
103
+ - spec/config_spec.rb
104
+ - spec/listener_spec.rb
105
+ - spec/packer_spec.rb
106
+ - spec/pipe_spec.rb
107
+ homepage: https://github.com/seblindberg/ruby-mqtt-pipe
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.4.5
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: A gem for sending a small set of objects via MQTT.
131
+ test_files:
132
+ - spec/config_spec.rb
133
+ - spec/listener_spec.rb
134
+ - spec/packer_spec.rb
135
+ - spec/pipe_spec.rb