sanford-protocol 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sanford-protocol.gemspec
4
+ gemspec
5
+
6
+ gem 'bson_ext', '~>1.7'
7
+ gem 'rake', '~>0.9.2'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 jcredding
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,145 @@
1
+ # Sanford Protocol
2
+
3
+ Ruby implementation of the Sanford communication protocol.
4
+
5
+ ## The Protocol
6
+
7
+ **Version**: `1`
8
+
9
+ Sanford communicates using binary encoded messages. Sanford messages are two headers and a body:
10
+
11
+ ```
12
+ |------ 1B -------|------ 4B -------|---- (Body Size)B ----|
13
+ | (packed header) | (packed header) | (BSON binary string) |
14
+ | Version | Body Size | Body |
15
+ |-----------------|-----------------|----------------------|
16
+ ```
17
+
18
+ ### Version
19
+
20
+ The first header represents the protocol version in use. It is a 1 byte unsigned integer and exists to ensure both the client and the server are talking the same protocol.
21
+
22
+ ### Body Size
23
+
24
+ The second header represents the size of the message's body. It is a 4 byte unsigned integer and tells the receiver how many bytes to read to receive the body.
25
+
26
+ ### Body
27
+
28
+ The Body is the content of the message. It is a [BSON](http://bsonspec.org/) encoded binary string that decodes to a ruby hash. Since the size of the body is encoded as a 4 byte (32 bit) unsigned integer, there is a size limit for body data (`(2 ** 32) - 1` or `4,294,967,295` or `~4GB`).
29
+
30
+ ## Request
31
+
32
+ A request is made up of 3 required parts: the version, the name, and the params.
33
+
34
+ * **version** - (string) version of the requested API.
35
+ * **name** - (string) name of the requested API service.
36
+ * **params** - (object) data for the service call - can be any BSON serializable object.
37
+
38
+ Requests are encoded as BSON hashes when transmitted in messages.
39
+
40
+ ```ruby
41
+ { 'version': 'v1',
42
+ 'name': 'some_service',
43
+ 'params': 'something'
44
+ }
45
+ ```
46
+
47
+ ## Response
48
+
49
+ A response is made up of 2 parts: the status and the result.
50
+
51
+ * **status** - (tuple, required) A code and message describing the result of the service call.
52
+ * **result** - (object, optional) Result of calling the service. This can be any BSON serializable object. Typically won't be set if the request is not successful.
53
+
54
+ Responses are encoded as BSON hashes when transmitted in messages.
55
+
56
+ ```ruby
57
+ { 'status': [ 200, 'The request was successful.' ]
58
+ 'result': true
59
+ }
60
+ ```
61
+
62
+ ### Status Codes
63
+
64
+ This is the list of defined status codes.
65
+
66
+ * `200` - `ok` - The request was successful.
67
+ * `400` - `bad_request` - The request couldn't be read. This is usually because it was not formed correctly.
68
+ * `404` - `not_found` - The server couldn't find something requested.
69
+ * `500` - `error` - The server errored responding to the request.
70
+
71
+ In addition to these, a service can return custom status codes, but they should use a number greater than or equal to `600` to avoid collisions with Sanford's defined status codes.
72
+
73
+ ## Usage
74
+
75
+ The `Sanford::Protocol` module defines helper methods for encoding and decoding messages.
76
+
77
+ ```ruby
78
+ # encode a message
79
+ data = { 'something' => true }
80
+ msg_body = Sanford::Protocol.msg_body.encode(data)
81
+ msg_size = Sanford::Protocol.msg_size.encode(msg_body.bytesize)
82
+ msg = [Sanford::Protocol.msg_version, msg_size, msg_body].join
83
+ ```
84
+
85
+ ### Connection
86
+
87
+ If you are sending and receiving messages using a tcp socket, use `Sanford::Protocol::Connection`.
88
+
89
+ ```ruby
90
+ connection = Sanford::Protocol::Connection.new(tcp_socket)
91
+ incoming_data = connection.read
92
+ connection.write(outgoing_data)
93
+ ```
94
+
95
+ For incoming messages, it reads them off the socket, validate them, and return the decoded body data. For outgoing messages, it encodes the message body from given data, adds the appropiate message headers, and writes the message to the socket.
96
+
97
+ ### Requests And Responses
98
+
99
+ Request and response objects have helpers for sending and receiving data using a connection.
100
+
101
+ ```ruby
102
+ # For a server...
103
+ # use Request#parse to build an incoming request
104
+ data_hash = server_connection.read
105
+ incoming_request = Sanford::Protocol::Request.parse(data_hash)
106
+ # use Response#to_hash to send a response
107
+ outgoing_response = Sanford::Protocol::Response.new(status, result)
108
+ server_connection.write(outgoing_response.to_hash)
109
+
110
+ # For a client...
111
+ # use Request#to_hash to send a request
112
+ outgoing_request = Sanford::Protocol::Request.new(name, version, params)
113
+ client_connection.write(outgoing_request.to_hash)
114
+ # use Response#parse to build an incoming response
115
+ data_hash = client_connection.read
116
+ incoming_response = Sanford::Protocol::Response.parse(data_hash)
117
+ ```
118
+
119
+ ## Test Helpers
120
+
121
+ A `FakeSocket` helper class and an associated `Test::Helpers` module are provided to help test receiving and sending Sanford::Protocol messages without using real sockets.
122
+
123
+ ```ruby
124
+ # fake a socket with some incoming binary
125
+ socket = FakeSocket.new(msg_binary_string)
126
+ connection = Sanford::Protocol::Connection.new(socket)
127
+ msg_data = connection.read
128
+
129
+ # write some binary to that fake socket and verify it
130
+ connection.write(msg_data)
131
+ puts socket.out # => msg binary string
132
+
133
+ # create a socket with an incoming request and verify the request
134
+ socket = FakeSocket.with_request(*request_params)
135
+ msg_data = Sanford::Protocol::Connection.new(socket).read
136
+ request = Sanford::Protocol::Request.parse(msg_data)
137
+ ```
138
+
139
+ ## Contributing
140
+
141
+ 1. Fork it
142
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
143
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
144
+ 4. Push to the branch (`git push origin my-new-feature`)
145
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "assert/rake_tasks"
4
+ Assert::RakeTasks.install
@@ -0,0 +1,54 @@
1
+ require 'sanford-protocol/msg_data'
2
+
3
+ module Sanford::Protocol
4
+
5
+ # Sanford Protocol's connection class wraps a socket and provides a `read` and
6
+ # `write` method for using Sanford's message protocol. Mixes in the protocol
7
+ # to get the `msg_version`, `msg_size`, and `msg_body` handler methods.
8
+
9
+ class Connection
10
+ include Sanford::Protocol
11
+
12
+ def initialize(tcp_socket)
13
+ @socket = Socket.new(tcp_socket)
14
+ end
15
+
16
+ # Message format (see sanford-protocal.rb):
17
+ # |------ 1B -------|------ 4B -------|-- (msg body size)B --|
18
+ # | (packed header) | (packed header) | (BSON binary string) |
19
+ # | msg version | msg body size | msg body |
20
+ # |-----------------|-----------------|----------------------|
21
+
22
+ def read
23
+ MsgVersion.new{ @socket.read msg_version.bytesize }.validate!
24
+ size = MsgSize.new{ @socket.decode msg_size, msg_size.bytes }.validate!.value
25
+ return MsgBody.new{ @socket.decode msg_body, size }.validate!.value
26
+ end
27
+
28
+ def write(data)
29
+ body = @socket.encode msg_body, data
30
+ size = @socket.encode msg_size, body.bytesize
31
+
32
+ @socket.write(msg_version, size, body)
33
+ end
34
+
35
+ class Socket < Struct.new(:tcp_socket)
36
+ def decode(handler, num_bytes)
37
+ handler.decode(read(num_bytes))
38
+ end
39
+
40
+ def encode(handler, data)
41
+ handler.encode data
42
+ end
43
+
44
+ def read(number_of_bytes)
45
+ tcp_socket.recvfrom(number_of_bytes).first
46
+ end
47
+
48
+ def write(*binary_strings)
49
+ tcp_socket.send(binary_strings.join, 0)
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ module Sanford::Protocol
2
+
3
+ class BadMessageError < RuntimeError
4
+ def initialize(message, bt=nil)
5
+ super(message)
6
+ set_backtrace(bt || caller)
7
+ end
8
+ end
9
+
10
+ class MsgData
11
+ attr_reader :value
12
+
13
+ def initialize(called_from=nil, &get_value)
14
+ @called_from = called_from || caller
15
+
16
+ # By default, any exceptions from getting the value are "hidden" behind a
17
+ # more general `BadMessageError`. In non-debug scenarios this is ideal and
18
+ # allows you to rescue from a common exception and respond in a standard way.
19
+ # In debug scenarios, however, go ahead and raise the real exception.
20
+
21
+ begin
22
+ @value = get_value.call
23
+ rescue Exception => err
24
+ ENV['SANFORD_PROTOCOL_DEBUG'] ? raise(err) : self.error!(self.get_value_error)
25
+ end
26
+ end
27
+
28
+ def get_value_error; "Error reading the message"; end
29
+ def validate!; self; end
30
+
31
+ def error!(message)
32
+ raise BadMessageError.new(message, @called_from)
33
+ end
34
+
35
+ end
36
+
37
+ class MsgSize < MsgData
38
+ def get_value_error; "Error reading message body size."; end
39
+ def validate!
40
+ error!("Empty message size") if self.value.nil?
41
+ super
42
+ end
43
+ end
44
+
45
+ class MsgVersion < MsgData
46
+ def get_value_error; "Error reading message protocol version"; end
47
+ def validate!
48
+ error!("Protocol version mismatch") if version_mismatch?
49
+ super
50
+ end
51
+
52
+ def version_mismatch?
53
+ self.value != Sanford::Protocol.msg_version
54
+ end
55
+ end
56
+
57
+ class MsgBody < MsgData
58
+ def get_value_error; "Error reading message body."; end
59
+ end
60
+
61
+ end
@@ -0,0 +1,48 @@
1
+ # The Request class models a specific type of Sanford message body and provides
2
+ # a defined structure for it. A request requires a message body to contain a
3
+ # version, name and params. It provides methods for working with message bodies
4
+ # (hashes) with `parse` and `to_hash`. In addition to this, a request has a
5
+ # `valid?` method, that returns whether it is valid and if it isn't why.
6
+
7
+ module Sanford::Protocol
8
+
9
+ class Request
10
+
11
+ def self.parse(body)
12
+ self.new(body['version'], body['name'], body['params'])
13
+ end
14
+
15
+ attr_reader :version, :name, :params
16
+
17
+ def initialize(version, name, params)
18
+ @version, @name, @params = version, name, params
19
+ end
20
+
21
+ def to_hash
22
+ { 'version' => @version,
23
+ 'name' => @name,
24
+ 'params' => @params
25
+ }
26
+ end
27
+
28
+ def valid?
29
+ if !@version
30
+ [ false, "The request doesn't contain a version." ]
31
+ elsif !@name
32
+ [ false, "The request doesn't contain a name." ]
33
+ else
34
+ [ true ]
35
+ end
36
+ end
37
+
38
+ def inspect
39
+ reference = '0x0%x' % (self.object_id << 1)
40
+ "#<#{self.class}:#{reference}"\
41
+ " @version=#{@version.inspect}"\
42
+ " @name=#{@name.inspect}"\
43
+ " @params=#{@params.inspect}>"
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,45 @@
1
+ require 'sanford-protocol/response_status'
2
+
3
+ # The Response class models a specific type of Sanford message body and provides
4
+ # a defined structure for it. A response requires a message body to contain a
5
+ # status (which is a code and optional message) and a result. It provides
6
+ # methods for working with message bodies (hashes) with `parse` and `to_hash`.
7
+
8
+ module Sanford::Protocol
9
+
10
+ class Response
11
+
12
+ def self.parse(hash)
13
+ self.new(hash['status'], hash['result'])
14
+ end
15
+
16
+ attr_reader :status, :result
17
+
18
+ def initialize(status, result = nil)
19
+ @status, @result = build_status(status), result
20
+ end
21
+
22
+ def to_hash
23
+ { 'status' => [ @status.code, @status.message ],
24
+ 'result' => @result
25
+ }
26
+ end
27
+
28
+ def inspect
29
+ reference = '0x0%x' % (self.object_id << 1)
30
+ "#<#{self.class}:#{reference}"\
31
+ " @status=#{@status.inspect}"\
32
+ " @result=#{@result.inspect}>"
33
+ end
34
+
35
+ private
36
+
37
+ def build_status(status)
38
+ return status if status.kind_of?(ResponseStatus)
39
+
40
+ ResponseStatus.new(*status)
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,44 @@
1
+ # The Response Status class models a code and optional message. This makes up
2
+ # part of a response and provides methods for building and displaying statuses.
3
+
4
+ module Sanford::Protocol
5
+ class ResponseStatus < Struct.new(:code_obj, :message)
6
+
7
+ def initialize(code, message = nil)
8
+ super(Code.new(code), message)
9
+ end
10
+
11
+ def code; code_obj.number; end
12
+ def name; code_obj.name; end
13
+ def to_s; code_obj.to_s; end
14
+
15
+ def inspect
16
+ reference = '0x0%x' % (self.object_id << 1)
17
+ "#<#{self.class}:#{reference}"\
18
+ " @code=#{code_obj}"\
19
+ " @message=#{message.inspect}>"
20
+ end
21
+
22
+ class Code < Struct.new(:number, :name)
23
+ NUMBERS = {
24
+ 'ok' => 200,
25
+ 'bad_request' => 400,
26
+ 'not_found' => 404,
27
+ 'error' => 500
28
+ }.freeze
29
+
30
+ def initialize(key)
31
+ num = NUMBERS[key.to_s] || key.to_i
32
+ name = NUMBERS.index(num) || NoName
33
+ super(num, name.upcase)
34
+ end
35
+
36
+ def to_s; "[#{[number, name].compact.join(', ')}]"; end
37
+
38
+ class NoName
39
+ def self.upcase; nil; end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ # The FakeSocket class can be used to work with Sanford Protocol in a test
2
+ # environment. Instead of passing a real socket, pass an instance of this class.
3
+ # It mimics the socket API that sanford is concerned with.
4
+
5
+ require 'sanford-protocol'
6
+ require 'sanford-protocol/request'
7
+
8
+ module Sanford::Protocol::Test
9
+ class FakeSocket
10
+
11
+ def self.with_request(*request_params)
12
+ request = Sanford::Protocol::Request.new(*request_params)
13
+ self.with_msg_body(request.to_hash)
14
+ end
15
+
16
+ def self.with_msg_body(body, size=nil, encoded_version=nil)
17
+ encoded_body = Sanford::Protocol.msg_body.encode(body)
18
+ self.with_encoded_msg_body(encoded_body, size, encoded_version)
19
+ end
20
+
21
+ def self.with_encoded_msg_body(encoded_body, size=nil, encoded_version=nil)
22
+ encoded_size = Sanford::Protocol.msg_size.encode(size || encoded_body.bytesize)
23
+ encoded_version ||= Sanford::Protocol.msg_version
24
+ self.new(encoded_version, encoded_size, encoded_body)
25
+ end
26
+
27
+ def initialize(*bytes)
28
+ @out = StringIO.new
29
+ @in = StringIO.new
30
+ reset(*bytes)
31
+ end
32
+
33
+ def reset(*new_bytes)
34
+ @in << new_bytes.join; @in.rewind;
35
+ end
36
+
37
+ def in; @in.string; end
38
+ def out; @out.string; end
39
+
40
+ # Socket methods -- requied by Sanford::Protocol
41
+
42
+ def recvfrom(number_of_bytes)
43
+ [ @in.read(number_of_bytes.to_i) ]
44
+ end
45
+
46
+ def send(bytes, flag)
47
+ @out << bytes
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ require 'sanford-protocol/test/fake_socket'
2
+ require 'sanford-protocol/response'
3
+
4
+ module Sanford::Protocol::Test
5
+
6
+ module Helpers
7
+ extend self
8
+
9
+ def fake_socket_with_request(*args)
10
+ FakeSocket.with_request(*args)
11
+ end
12
+
13
+ def fake_socket_with_msg_body(*args)
14
+ FakeSocket.with_msg_body(*args)
15
+ end
16
+
17
+ def fake_socket_with_encoded_msg_body(*args)
18
+ FakeSocket.with_encoded_msg_body(*args)
19
+ end
20
+
21
+ def fake_socket_with(*args)
22
+ FakeSocket.new(*args)
23
+ end
24
+
25
+ def read_response_from_fake_socket(from_fake_socket)
26
+ data = Sanford::Protocol::Connection.new(from_fake_socket).read
27
+ Sanford::Protocol::Response.parse(data)
28
+ end
29
+
30
+ def read_written_response_from_fake_socket(from_fake_socket)
31
+ read_response_from_fake_socket(FakeSocket.new(from_fake_socket.out))
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,5 @@
1
+ module Sanford
2
+ module Protocol
3
+ GEM_VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,52 @@
1
+ require 'sanford-protocol/connection'
2
+ require 'sanford-protocol/request'
3
+ require 'sanford-protocol/response'
4
+
5
+ module Sanford
6
+ module Protocol
7
+
8
+ extend self
9
+
10
+ # If anything changes in this file, the VERSION number should be
11
+ # incremented. This is used by clients and servers to ensure they are
12
+ # working under the same assumptions. In addition to incrementing this, the
13
+ # README needs to be updated to display the current version and needs to
14
+ # describe everything in this file.
15
+
16
+ VERSION = 1
17
+
18
+ # The message version is the 1B encoding of the `VERSION` above. It is
19
+ # encoded using Array#pack 'C' (8-bit unsigned integer). The max value it
20
+ # can encode is 255 (`(2 ** 8) - 1`).
21
+
22
+ def msg_version; @msg_version ||= PackedHeader.new(1, 'C').encode(VERSION); end
23
+
24
+ # The message size is encoded using Array#pack 'N'. This encoding represents
25
+ # a 32-bit (4 byte) unsigned integer. The max value that can be encoded in
26
+ # 4 bytes is 4,294,967,295 (`(2 ** 32) - 1`) or a size of ~4GB.
27
+
28
+ def msg_size; @msg_size ||= PackedHeader.new(4, 'N'); end
29
+
30
+ # THe message body is encoded using BSON.
31
+
32
+ def msg_body; @msg_body ||= BsonBody.new; end
33
+
34
+ class PackedHeader < Struct.new(:bytes, :directive)
35
+ def encode(data); [*data].pack(directive); end
36
+ def decode(binary); binary.to_s.unpack(directive).first; end
37
+ end
38
+
39
+ class BsonBody
40
+ require 'bson'
41
+
42
+ # BSON returns a byte buffer when serializing. This doesn't always behave
43
+ # like a string, so convert it to one.
44
+ def encode(data); ::BSON.serialize(data).to_s; end
45
+
46
+ # BSON returns an ordered hash when deserializing. This should be
47
+ # functionally equivalent to a regular hash.
48
+ def decode(binary); ::BSON.deserialize(binary); end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sanford-protocol/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "sanford-protocol"
8
+ gem.version = Sanford::Protocol::GEM_VERSION
9
+ gem.authors = ["Collin Redding", "Kelly Redding"]
10
+ gem.email = ["collin.redding@me.com", "kelly@kellyredding.com"]
11
+ gem.description = "Ruby implementation of Sanford's communication protocol."
12
+ gem.summary = "Ruby implementation of Sanford's communication protocol."
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("bson", ["~>1.7"])
21
+
22
+ gem.add_development_dependency("assert", ["~> 0.8"])
23
+ gem.add_development_dependency("assert-mocha", ["~> 0.1"])
24
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,32 @@
1
+ ROOT = File.expand_path('../..', __FILE__)
2
+
3
+ ENV['SANFORD_PROTOCOL_DEBUG'] = 'yes'
4
+
5
+ require 'sanford-protocol/test/fake_socket'
6
+ FakeSocket = Sanford::Protocol::Test::FakeSocket
7
+
8
+ require 'assert-mocha' if defined?(Assert)
9
+
10
+ class Assert::Context
11
+
12
+ def setup_some_msg_data(data=nil)
13
+ @data = data || { 'something' => true }
14
+ @encoded_body = Sanford::Protocol.msg_body.encode(@data)
15
+ @encoded_size = Sanford::Protocol.msg_size.encode(@encoded_body.bytesize)
16
+ @encoded_version = Sanford::Protocol.msg_version
17
+ @msg = [@encoded_version, @encoded_size, @encoded_body].join
18
+ end
19
+
20
+ def setup_some_request_data
21
+ @request_params = ['1', 'a_service', {:some => 'data'}]
22
+ @request = Sanford::Protocol::Request.new(*@request_params)
23
+ setup_some_msg_data(@request.to_hash)
24
+ end
25
+
26
+ def setup_some_response_data
27
+ @response_params = [200, 'in testing all is well']
28
+ @response = Sanford::Protocol::Response.new(*@response_params)
29
+ setup_some_msg_data(@response.to_hash)
30
+ end
31
+
32
+ end
@@ -0,0 +1,27 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/connection'
3
+
4
+ class Sanford::Protocol::Connection
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "Sanford::Protocol::Connection"
8
+ setup do
9
+ setup_some_msg_data
10
+ @socket = FakeSocket.new(@msg)
11
+ @connection = Sanford::Protocol::Connection.new(@socket)
12
+ end
13
+ subject{ @connection }
14
+
15
+ should have_instance_methods :read, :write
16
+
17
+ should "read messages off the socket with #read" do
18
+ assert_equal @data, subject.read
19
+ end
20
+
21
+ should "write messages to the socket with #write" do
22
+ subject.write(@data)
23
+ assert_equal @msg, @socket.out
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,85 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/test/fake_socket'
3
+ require 'sanford-protocol/request'
4
+
5
+ class Sanford::Protocol::Test::FakeSocket
6
+
7
+ class BaseTests < Assert::Context
8
+ desc "a FakeSocket"
9
+ setup do
10
+ @fs = FakeSocket.new
11
+ end
12
+ subject { @fs }
13
+
14
+ should have_cmeths :with_request, :with_msg_body, :with_encoded_msg_body
15
+ should have_imeths :in, :out, :reset
16
+ should have_imeths :recvfrom, :send
17
+
18
+ should "have no `in` or `out` data by default" do
19
+ assert_empty subject.in
20
+ assert_empty subject.out
21
+ end
22
+
23
+ should "push `out` data using #send" do
24
+ subject.send('some out data', 0)
25
+ assert_equal 'some out data', subject.out
26
+ end
27
+
28
+ end
29
+
30
+ class WithInDataTests < BaseTests
31
+ desc "created given some data"
32
+ setup do
33
+ @in_data = 'some in data'
34
+ @fs = FakeSocket.new(@in_data)
35
+ end
36
+
37
+ should "add the data as `in` data" do
38
+ assert_equal @in_data, subject.in
39
+ end
40
+
41
+ should "pull `in` data using #recvfrom" do
42
+ recvfrom_data = subject.recvfrom(@in_data.bytesize)
43
+
44
+ assert_kind_of ::Array, recvfrom_data
45
+ assert_equal @in_data, recvfrom_data.first
46
+ end
47
+
48
+ should "reset its `in` data using #reset" do
49
+ subject.reset('some different in data')
50
+ assert_equal 'some different in data', subject.in
51
+ end
52
+
53
+ end
54
+
55
+ class EncodedMessageTests < BaseTests
56
+ desc "with encoded msg data"
57
+ setup do
58
+ setup_some_msg_data
59
+ end
60
+
61
+ should "build with the msg as `in` data given the encoded msg body" do
62
+ s = FakeSocket.with_encoded_msg_body(@encoded_body)
63
+ assert_equal @msg, s.in
64
+ end
65
+
66
+ should "build with the msg as `in` data given the unencoded msg body" do
67
+ s = FakeSocket.with_msg_body(@data)
68
+ assert_equal @msg, s.in
69
+ end
70
+
71
+ end
72
+
73
+ class RequestTests < BaseTests
74
+ desc "that is a request"
75
+ setup do
76
+ setup_some_request_data
77
+ end
78
+
79
+ should "build with the request msg as `in` data given the request" do
80
+ s = FakeSocket.with_request(*@request_params)
81
+ assert_equal @msg, s.in
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,121 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/msg_data'
3
+ require 'sanford-protocol'
4
+
5
+ class Sanford::Protocol::MsgData
6
+
7
+ class BadMessageTests < Assert::Context
8
+ desc "reading data on a bad message"
9
+ setup do
10
+ @debug = ENV['SANFORD_PROTOCOL_DEBUG']
11
+ ENV.delete('SANFORD_PROTOCOL_DEBUG')
12
+
13
+ @connection = Sanford::Protocol::Connection.new(FakeSocket.new)
14
+ @socket = @connection.instance_variable_get "@socket"
15
+ end
16
+ teardown do
17
+ ENV['SANFORD_PROTOCOL_DEBUG'] = @debug
18
+ end
19
+ subject{ @connection }
20
+
21
+ def assert_bad_message(expected_msg)
22
+ exception = begin; subject.read; rescue Exception => err; err; end
23
+ assert_instance_of Sanford::Protocol::BadMessageError, exception
24
+ assert_equal expected_msg, exception.message
25
+ end
26
+
27
+ end
28
+
29
+ class BadSizeTests < BadMessageTests
30
+ desc "that errors when reading the version part"
31
+ setup do
32
+ @socket.stubs(:read). # when reading the version, fail
33
+ with(Sanford::Protocol.msg_version.bytesize).
34
+ raises("simulated socket read error!")
35
+ end
36
+ teardown do
37
+ @socket.unstub(:read)
38
+ end
39
+
40
+ should "raise a BadMessageError with a relevant message" do
41
+ assert_bad_message "Error reading message protocol version"
42
+ end
43
+ end
44
+
45
+ class BadVersionTests < BadMessageTests
46
+ desc "that errors when reading the size part"
47
+ setup do
48
+ @socket.stubs(:read). # when reading the version, succeed
49
+ with(Sanford::Protocol.msg_version.bytesize).
50
+ returns(Sanford::Protocol.msg_version)
51
+ @socket.stubs(:read). # when reading the size, fail
52
+ with(Sanford::Protocol.msg_size.bytes).
53
+ raises("simulated socket read error!")
54
+ end
55
+ teardown do
56
+ @socket.unstub(:read)
57
+ end
58
+
59
+ should "raise a BadMessageError with a relevant message" do
60
+ assert_bad_message "Error reading message body size."
61
+ end
62
+ end
63
+
64
+ class BadBodyTests < BadMessageTests
65
+ desc "that errors when reading the body part"
66
+ setup do
67
+ @socket.stubs(:read). # when reading the version, succeed
68
+ with(Sanford::Protocol.msg_version.bytesize).
69
+ returns(Sanford::Protocol.msg_version)
70
+ @socket.stubs(:read). # when reading the size, succeed
71
+ with(Sanford::Protocol.msg_size.bytes).
72
+ returns(Sanford::Protocol.msg_size.encode(50))
73
+ @socket.stubs(:read). # when reading the body, fail
74
+ with(50).
75
+ raises("simulated socket read error!")
76
+ end
77
+ teardown do
78
+ Sanford::Protocol.unstub(:read)
79
+ end
80
+
81
+ should "raise a BadMessageError with a relevant message" do
82
+ assert_bad_message "Error reading message body."
83
+ end
84
+ end
85
+
86
+ class NilSizeTests < BadMessageTests
87
+ desc 'that reads a nil size'
88
+ setup do
89
+ @socket.stubs(:read). # when reading the version, succeed
90
+ with(Sanford::Protocol.msg_version.bytesize).
91
+ returns(Sanford::Protocol.msg_version)
92
+ @socket.stubs(:read). # when reading the size, return nil
93
+ with(Sanford::Protocol.msg_size.bytes).
94
+ returns(nil)
95
+ end
96
+ teardown do
97
+ @socket.unstub(:read)
98
+ end
99
+
100
+ should "raise a BadMessageError with a relevant message" do
101
+ assert_bad_message "Empty message size"
102
+ end
103
+ end
104
+
105
+ class VersionMismatchTests < BadMessageTests
106
+ desc "with a mismatched protocol version"
107
+ setup do
108
+ @socket.stubs(:read). # when reading the version, encode wrong number
109
+ with(Sanford::Protocol.msg_version.bytesize).
110
+ returns(Sanford::Protocol::PackedHeader.new(1, 'C').encode(0))
111
+ end
112
+ teardown do
113
+ @socket.unstub(:read)
114
+ end
115
+
116
+ should "raise a BadMessageError with a relevant message" do
117
+ assert_bad_message "Protocol version mismatch"
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,51 @@
1
+ # These tests are intended to be brittle and make sure the protocol conforms to
2
+ # an expected spec. If any of these tests fail, you probably need to modify the
3
+ # protocol's version constant.
4
+
5
+ require 'assert'
6
+ require 'sanford-protocol'
7
+
8
+ module Sanford::Protocol
9
+ class BaseTests < Assert::Context
10
+ desc "Sanford::Protocol"
11
+ subject{ Sanford::Protocol }
12
+
13
+ should have_instance_methods :msg_version, :msg_size, :msg_body
14
+
15
+ should "define the protocol version" do
16
+ assert_equal 1, subject::VERSION
17
+ end
18
+
19
+ should "encode the protocol version to a 1B binary string" do
20
+ assert_equal 1, subject.msg_version.bytesize
21
+
22
+ expected = [ subject::VERSION ].pack('C')
23
+ assert_equal expected, subject.msg_version
24
+ end
25
+
26
+ should "encode/decode the size to/from a 4B binary string" do
27
+ assert_equal 4, subject.msg_size.bytes
28
+
29
+ size = (2 ** 32) - 1 # the max number it supports
30
+ binary = [ size ].pack('N')
31
+ assert_equal binary, subject.msg_size.encode(size)
32
+ assert_equal size, subject.msg_size.decode(binary)
33
+ end
34
+
35
+ should "encode the body to BSON" do
36
+ data = {
37
+ 'string' => 'test',
38
+ 'int' => 1,
39
+ 'float' => 2.1,
40
+ 'boolean' => true,
41
+ 'array' => [ 1, 2, 3 ],
42
+ 'hash' => { 'something' => 'else' }
43
+ }
44
+ binary = ::BSON.serialize(data).to_s
45
+
46
+ assert_equal binary, subject.msg_body.encode(data)
47
+ assert_equal data, subject.msg_body.decode(binary)
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/request'
3
+
4
+ class Sanford::Protocol::Request
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "Sanford::Protocol::Request"
8
+ setup do
9
+ @request = Sanford::Protocol::Request.new('v1', 'some_service', [ true ])
10
+ end
11
+ subject{ @request }
12
+
13
+ should have_instance_methods :name, :version, :params, :to_hash, :valid?
14
+ should have_class_methods :parse
15
+
16
+ should "return an instance of a Sanford::Protocol::Request given a hash using #parse" do
17
+ # using BSON messages are hashes
18
+ hash = {
19
+ 'name' => 'service_name',
20
+ 'version' => 'service_version',
21
+ 'params' => 'service_params'
22
+ }
23
+ request = Sanford::Protocol::Request.parse(hash)
24
+
25
+ assert_instance_of Sanford::Protocol::Request, request
26
+ assert_equal hash['name'], request.name
27
+ assert_equal hash['version'], request.version
28
+ assert_equal hash['params'], request.params
29
+ end
30
+
31
+ should "return the request as a hash with #to_hash" do
32
+ # using BSON messages are hashes
33
+ expected = {
34
+ 'version' => 'v1',
35
+ 'name' => 'some_service',
36
+ 'params' => [ true ]
37
+ }
38
+
39
+ assert_equal expected, subject.to_hash
40
+ end
41
+ end
42
+
43
+ class ValidTests < BaseTests
44
+ desc "valid?"
45
+
46
+ should "return true and no message with a valid request" do
47
+ request = Sanford::Protocol::Request.new('name', 'v1', {})
48
+ is_valid, message = request.valid?
49
+
50
+ assert_equal true, is_valid
51
+ assert_equal nil, message
52
+ end
53
+
54
+ should "return false and a message when there isn't a name" do
55
+ request = Sanford::Protocol::Request.new('v1', nil, {})
56
+ is_valid, message = request.valid?
57
+
58
+ assert_equal false, is_valid
59
+ assert_equal "The request doesn't contain a name.", message
60
+ end
61
+
62
+ should "return false and a message when there isn't a version" do
63
+ request = Sanford::Protocol::Request.new(nil, 'name', {})
64
+ is_valid, message = request.valid?
65
+
66
+ assert_equal false, is_valid
67
+ assert_equal "The request doesn't contain a version.", message
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,44 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/response_status'
3
+
4
+ class Sanford::Protocol::ResponseStatus
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "Sanford::Protocol::ResponseStatus"
8
+ setup do
9
+ @status = Sanford::Protocol::ResponseStatus.new(200, "OK")
10
+ end
11
+ subject{ @status }
12
+
13
+ should have_readers :code_obj, :message
14
+ should have_instance_methods :code, :name
15
+
16
+ should "know it's code name" do
17
+ named = Sanford::Protocol::ResponseStatus.new(200)
18
+ unamed = Sanford::Protocol::ResponseStatus.new(999)
19
+
20
+ assert_equal "OK", named.name
21
+ assert_equal nil, unamed.name
22
+ end
23
+
24
+ should "know it's code numbers" do
25
+ Code::NUMBERS.each do |name, value|
26
+ status = Sanford::Protocol::ResponseStatus.new(name)
27
+ assert_equal value, status.code
28
+ end
29
+
30
+ unamed = Sanford::Protocol::ResponseStatus.new('unamed')
31
+ assert_equal 0, unamed.code
32
+ end
33
+
34
+ should "return it's code number and code name with #to_s" do
35
+ named = Sanford::Protocol::ResponseStatus.new(200)
36
+ unamed = Sanford::Protocol::ResponseStatus.new(999)
37
+
38
+ assert_equal "[#{named.code}, #{named.name}]", named.to_s
39
+ assert_equal "[#{unamed.code}]", unamed.to_s
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,74 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/response'
3
+
4
+ class Sanford::Protocol::Response
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "Sanford::Protocol::Response"
8
+ setup do
9
+ @response = Sanford::Protocol::Response.new([ 672, 'YAR!' ], { 'something' => true })
10
+ end
11
+ subject{ @response }
12
+
13
+ should have_instance_methods :status, :result, :to_hash
14
+ should have_class_methods :parse
15
+
16
+ should "return an instance of a Sanford::Protocol::Response given a hash using #parse" do
17
+ # using BSON messages are hashes
18
+ hash = {
19
+ 'status' => [ 200, 'OK' ],
20
+ 'result' => 'yes'
21
+ }
22
+ request = Sanford::Protocol::Response.parse(hash)
23
+
24
+ assert_instance_of Sanford::Protocol::Response, request
25
+ assert_equal hash['status'].first, request.status.code
26
+ assert_equal hash['status'].last, request.status.message
27
+ assert_equal hash['result'], request.result
28
+ end
29
+
30
+ should "return the request as a hash with #to_hash" do
31
+ # using BSON messages are hashes
32
+ expected = {
33
+ 'status' => [ 672, 'YAR!' ],
34
+ 'result' => { 'something' => true }
35
+ }
36
+
37
+ assert_equal expected, subject.to_hash
38
+ end
39
+ end
40
+
41
+ # Somewhat of a system test, want to make sure if Response is passed some
42
+ # "fuzzy" args that it will build it's status object as expected
43
+ class StatusBuildingTests < BaseTests
44
+
45
+ should "build a status with it's code set, given an integer" do
46
+ response = Sanford::Protocol::Response.new(574)
47
+
48
+ assert_equal 574, response.status.code
49
+ assert_equal nil, response.status.message
50
+ end
51
+
52
+ should "build a status with it's code set, given a name" do
53
+ response = Sanford::Protocol::Response.new('ok')
54
+
55
+ assert_equal 200, response.status.code
56
+ assert_equal nil, response.status.message
57
+ end
58
+
59
+ should "use a status object, if given one" do
60
+ status = Sanford::Protocol::ResponseStatus.new(200, "OK")
61
+ response = Sanford::Protocol::Response.new(status)
62
+
63
+ assert_same status, response.status
64
+ end
65
+
66
+ should "build a status with a code and message set, when given both" do
67
+ response = Sanford::Protocol::Response.new([ 348, "my message" ])
68
+
69
+ assert_equal 348, response.status.code
70
+ assert_equal "my message", response.status.message
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,35 @@
1
+ require 'assert'
2
+ require 'sanford-protocol/test/helpers'
3
+
4
+ module Sanford::Protocol::Test::Helpers
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "the test helpers"
8
+ subject { Sanford::Protocol::Test::Helpers }
9
+
10
+ should have_imeths :fake_socket_with_request, :fake_socket_with_msg_body
11
+ should have_imeths :fake_socket_with_encoded_msg_body, :fake_socket_with
12
+ should have_imeths :read_response_from_fake_socket
13
+ should have_imeths :read_written_response_from_fake_socket
14
+
15
+ should "be able to read responses given a fake socket" do
16
+ setup_some_response_data
17
+ fs = FakeSocket.new(@msg)
18
+ response = subject.read_response_from_fake_socket(fs)
19
+
20
+ assert_kind_of Sanford::Protocol::Response, response
21
+ assert_equal @data, response.to_hash
22
+ end
23
+
24
+ should "be able to read responses written to a fake socket" do
25
+ setup_some_response_data
26
+ fs = FakeSocket.new; fs.send(@msg, 0)
27
+ response = subject.read_written_response_from_fake_socket(fs)
28
+
29
+ assert_kind_of Sanford::Protocol::Response, response
30
+ assert_equal @data, response.to_hash
31
+ end
32
+
33
+ end
34
+
35
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sanford-protocol
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Collin Redding
14
+ - Kelly Redding
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-11-14 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: bson
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 1
29
+ segments:
30
+ - 1
31
+ - 7
32
+ version: "1.7"
33
+ type: :runtime
34
+ requirement: *id001
35
+ prerelease: false
36
+ - !ruby/object:Gem::Dependency
37
+ name: assert
38
+ version_requirements: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 27
44
+ segments:
45
+ - 0
46
+ - 8
47
+ version: "0.8"
48
+ type: :development
49
+ requirement: *id002
50
+ prerelease: false
51
+ - !ruby/object:Gem::Dependency
52
+ name: assert-mocha
53
+ version_requirements: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ hash: 9
59
+ segments:
60
+ - 0
61
+ - 1
62
+ version: "0.1"
63
+ type: :development
64
+ requirement: *id003
65
+ prerelease: false
66
+ description: Ruby implementation of Sanford's communication protocol.
67
+ email:
68
+ - collin.redding@me.com
69
+ - kelly@kellyredding.com
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files: []
75
+
76
+ files:
77
+ - .gitignore
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/sanford-protocol.rb
83
+ - lib/sanford-protocol/connection.rb
84
+ - lib/sanford-protocol/msg_data.rb
85
+ - lib/sanford-protocol/request.rb
86
+ - lib/sanford-protocol/response.rb
87
+ - lib/sanford-protocol/response_status.rb
88
+ - lib/sanford-protocol/test/fake_socket.rb
89
+ - lib/sanford-protocol/test/helpers.rb
90
+ - lib/sanford-protocol/version.rb
91
+ - sanford-protocol.gemspec
92
+ - test/helper.rb
93
+ - test/unit/connection_tests.rb
94
+ - test/unit/fake_socket_tests.rb
95
+ - test/unit/msg_data_tests.rb
96
+ - test/unit/protocol_tests.rb
97
+ - test/unit/request_tests.rb
98
+ - test/unit/response_status_tests.rb
99
+ - test/unit/response_tests.rb
100
+ - test/unit/test_helpers_tests.rb
101
+ homepage: ""
102
+ licenses: []
103
+
104
+ post_install_message:
105
+ rdoc_options: []
106
+
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 3
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 3
124
+ segments:
125
+ - 0
126
+ version: "0"
127
+ requirements: []
128
+
129
+ rubyforge_project:
130
+ rubygems_version: 1.8.24
131
+ signing_key:
132
+ specification_version: 3
133
+ summary: Ruby implementation of Sanford's communication protocol.
134
+ test_files:
135
+ - test/helper.rb
136
+ - test/unit/connection_tests.rb
137
+ - test/unit/fake_socket_tests.rb
138
+ - test/unit/msg_data_tests.rb
139
+ - test/unit/protocol_tests.rb
140
+ - test/unit/request_tests.rb
141
+ - test/unit/response_status_tests.rb
142
+ - test/unit/response_tests.rb
143
+ - test/unit/test_helpers_tests.rb