hushed 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/Rakefile +9 -0
- data/hushed.gemspec +29 -0
- data/lib/hushed.rb +6 -0
- data/lib/hushed/blackboard.rb +44 -0
- data/lib/hushed/client.rb +92 -0
- data/lib/hushed/documents/document.rb +15 -0
- data/lib/hushed/documents/request/shipment_order.rb +89 -0
- data/lib/hushed/documents/response/shipment_order_result.rb +56 -0
- data/lib/hushed/message.rb +43 -0
- data/lib/hushed/queue.rb +26 -0
- data/lib/hushed/request.rb +11 -0
- data/lib/hushed/response.rb +11 -0
- data/lib/hushed/version.rb +3 -0
- data/spec/fixtures/credentials.yml +12 -0
- data/spec/fixtures/documents/responses/shipment_order_result.xml +23 -0
- data/spec/fixtures/messages/purchase_order_message.xml +11 -0
- data/spec/remote/blackboard_spec.rb +43 -0
- data/spec/remote/queue_spec.rb +63 -0
- data/spec/spec_helper.rb +169 -0
- data/spec/unit/blackboard_spec.rb +102 -0
- data/spec/unit/client_spec.rb +97 -0
- data/spec/unit/documents/document_spec.rb +44 -0
- data/spec/unit/documents/request/shipment_order_spec.rb +90 -0
- data/spec/unit/documents/response/shipment_order_result_spec.rb +26 -0
- data/spec/unit/message_spec.rb +71 -0
- data/spec/unit/queue_spec.rb +48 -0
- metadata +207 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Hushed
|
2
|
+
class Message
|
3
|
+
|
4
|
+
NAMESPACE = "http://schemas.quietlogistics.com/V2/EventMessage.xsd"
|
5
|
+
|
6
|
+
class MissingDocumentError < StandardError; end
|
7
|
+
class MissingClientError < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :client, :document, :xml
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@xml = Nokogiri::XML::Document.parse(options[:xml]) if options[:xml]
|
13
|
+
@client = options[:client]
|
14
|
+
@document = options[:document]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_xml
|
18
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
19
|
+
xml.EventMessage(attributes)
|
20
|
+
end
|
21
|
+
builder.to_xml
|
22
|
+
end
|
23
|
+
|
24
|
+
def document_type
|
25
|
+
@document ? @document.type : @xml.css('EventMessage').first['DocumentType']
|
26
|
+
end
|
27
|
+
|
28
|
+
def document_name
|
29
|
+
@document ? @document.filename : @xml.css('EventMessage').first['DocumentName']
|
30
|
+
end
|
31
|
+
|
32
|
+
def attributes
|
33
|
+
raise(MissingClientError.new("client cannot be missing")) unless @client
|
34
|
+
raise(MissingDocumentError.new("document cannot be missing")) unless @document
|
35
|
+
{
|
36
|
+
ClientId: @client.client_id, BusinessUnit: @client.business_unit,
|
37
|
+
DocumentName: @document.filename, DocumentType: @document.type,
|
38
|
+
Warehouse: @document.warehouse, MessageDate: @document.date.utc,
|
39
|
+
MessageId: @document.message_id, xmlns: NAMESPACE
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/hushed/queue.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hushed
|
2
|
+
class Queue
|
3
|
+
attr_reader :client
|
4
|
+
def initialize(client)
|
5
|
+
@client = client
|
6
|
+
end
|
7
|
+
|
8
|
+
def send(message)
|
9
|
+
queue = client.to_quiet_queue
|
10
|
+
queue.send_message(message.to_xml)
|
11
|
+
end
|
12
|
+
|
13
|
+
def receive
|
14
|
+
queue = client.from_quiet_queue
|
15
|
+
message = nil
|
16
|
+
queue.receive_message do |msg|
|
17
|
+
message = Message.new(xml: msg.body)
|
18
|
+
end
|
19
|
+
message || Message.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def approximate_pending_messages
|
23
|
+
client.from_quiet_queue.approximate_number_of_messages
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
access_key_id: abracadabra
|
2
|
+
secret_access_key: alakazam
|
3
|
+
client_id: HUSHED
|
4
|
+
business_unit: HUSHED
|
5
|
+
warehouse: SPACE
|
6
|
+
buckets:
|
7
|
+
to: hushed-to-quiet
|
8
|
+
from: hushed-from-quiet
|
9
|
+
queues:
|
10
|
+
to: http://queue.amazonaws/1234567890/hushed_to_quiet
|
11
|
+
from: http://queue.amazonaws/1234567890/hushed_from_quiet
|
12
|
+
inventory: http://queue.amazonaws/1234567890/hushed_inventory
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<SOResult
|
3
|
+
xmlns="http://schemas.quiettechnology.com/V2/SOResultDocument.xsd"
|
4
|
+
ClientID="HUSHED"
|
5
|
+
BusinessUnit="HUSHED"
|
6
|
+
CartonCount="1"
|
7
|
+
DateShipped="2009-09-01T00:00:00Z"
|
8
|
+
FreightCost="10.00"
|
9
|
+
OrderNumber="1234567890">
|
10
|
+
<Line Line="1" Quantity="1" />
|
11
|
+
<Line Line="2" Quantity="1" />
|
12
|
+
<Carton
|
13
|
+
Carrier="USPS"
|
14
|
+
CartonId="S12345678901"
|
15
|
+
CartonNumber="1"
|
16
|
+
FreightCost="10.00"
|
17
|
+
ServiceLevel="FIRST"
|
18
|
+
TrackingId="40000000000"
|
19
|
+
Weight="0.66">
|
20
|
+
<Content Line="1"Quantity="1" />
|
21
|
+
<Content Line="2"Quantity="1" />
|
22
|
+
</Carton>
|
23
|
+
</SOResult>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<EventMessage
|
3
|
+
xmlns="http://schemas.quietlogistics.com/V2/EventMessage.xsd"
|
4
|
+
ClientId="HUSHED"
|
5
|
+
BusinessUnit="HUSHED"
|
6
|
+
DocumentName="HUSHED_PurchaseOrder_1234_20100927_132505124.xml"
|
7
|
+
DocumentType="PurchaseOrder"
|
8
|
+
MessageId="EF1CE966-38A2-428b-BA67-EFF23AF22F57"
|
9
|
+
Warehouse="CORP1"
|
10
|
+
MessageDate="2009-09-01T12:00:00Z">
|
11
|
+
</EventMessage>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'hushed'
|
3
|
+
require 'hushed/blackboard'
|
4
|
+
|
5
|
+
module Hushed
|
6
|
+
describe "BlackboardRemote" do
|
7
|
+
include Configuration
|
8
|
+
include Fixtures
|
9
|
+
|
10
|
+
before do
|
11
|
+
AWS.config(stub_requests: false)
|
12
|
+
@client = Client.new(load_configuration)
|
13
|
+
@blackboard = Blackboard.new(@client)
|
14
|
+
@document = DocumentDouble.new(
|
15
|
+
message_id: '1234567',
|
16
|
+
date: Time.new(2013, 04, 05, 12, 30, 15).utc,
|
17
|
+
filename: 'neat_beans.xml',
|
18
|
+
client: @client,
|
19
|
+
type: 'ShipmentOrderResult'
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
after do
|
24
|
+
buckets = [@client.to_quiet_bucket, @client.from_quiet_bucket]
|
25
|
+
buckets.each { |bucket| bucket.objects.delete_all }
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be able to write a document to an S3 bucket" do
|
29
|
+
message = @blackboard.post(@document)
|
30
|
+
file = message.document_name
|
31
|
+
bucket = @client.to_quiet_bucket
|
32
|
+
assert bucket.objects[file].exists?, "It appears that #{file} was not written to S3"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be able to fetch a document from an S3 bucket when given a message" do
|
36
|
+
expected_contents = load_response('shipment_order_result').read()
|
37
|
+
@client.from_quiet_bucket.objects[@document.filename].write(expected_contents)
|
38
|
+
message = MessageDouble.new(document_name: @document.filename, document_type: @document.type)
|
39
|
+
document = @blackboard.fetch(message)
|
40
|
+
assert_equal expected_contents, document.io
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'openssl'
|
3
|
+
require 'hushed'
|
4
|
+
require 'hushed/message'
|
5
|
+
|
6
|
+
module Hushed
|
7
|
+
describe "QueueRemote" do
|
8
|
+
include Configuration
|
9
|
+
|
10
|
+
before do
|
11
|
+
AWS.config(:stub_requests => false)
|
12
|
+
@client = Client.new(load_configuration)
|
13
|
+
@sqs_queues = [@client.to_quiet_queue, @client.from_quiet_queue]
|
14
|
+
@default_wait_time = @sqs_queues.map(&:wait_time_seconds).max
|
15
|
+
@sqs_queues.each { |queue| queue.wait_time_seconds = 1 }
|
16
|
+
|
17
|
+
@document = DocumentDouble.new(
|
18
|
+
:message_id => '1234567',
|
19
|
+
:date => Time.new(2013, 04, 05, 12, 30, 15).utc,
|
20
|
+
:filename => 'neat_beans.xml',
|
21
|
+
:client => @client,
|
22
|
+
:type => 'Thinger'
|
23
|
+
)
|
24
|
+
|
25
|
+
@message = Message.new(:client => @client, :document => @document)
|
26
|
+
@queue = Queue.new(@client)
|
27
|
+
end
|
28
|
+
|
29
|
+
after do
|
30
|
+
@sqs_queues.each do |queue|
|
31
|
+
flush(queue)
|
32
|
+
end
|
33
|
+
@sqs_queues.each { |queue| queue.wait_time_seconds = @default_wait_time }
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should be able to push a message onto the queue" do
|
37
|
+
expected_md5 = OpenSSL::Digest::MD5.new.hexdigest(@message.to_xml)
|
38
|
+
sent_message = @queue.send(@message)
|
39
|
+
assert_equal 1, @client.to_quiet_queue.approximate_number_of_messages
|
40
|
+
assert_equal expected_md5, sent_message.md5
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should be able to fetch a message from the queue" do
|
44
|
+
@client.from_quiet_queue.send_message(@message.to_xml)
|
45
|
+
message = @queue.receive
|
46
|
+
assert_equal @message.to_xml, message.xml.to_xml
|
47
|
+
assert_equal @message.document_type, message.document_type
|
48
|
+
assert_equal @message.document_name, message.document_name
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def flush(queue)
|
53
|
+
pending_messages = queue.approximate_number_of_messages
|
54
|
+
while pending_messages > 0
|
55
|
+
queue.receive_message do |message|
|
56
|
+
message.delete
|
57
|
+
pending_messages -= 1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'yaml'
|
4
|
+
require 'mocha/setup'
|
5
|
+
require 'hushed'
|
6
|
+
|
7
|
+
module Fixtures
|
8
|
+
def load_fixture(path)
|
9
|
+
File.open(path, 'rb')
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_response(response_name)
|
13
|
+
load_fixture("spec/fixtures/documents/responses/#{response_name}.xml")
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_message(message_name)
|
17
|
+
load_fixture("spec/fixtures/messages/#{message_name}.xml")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Configuration
|
22
|
+
def load_configuration
|
23
|
+
test_credentials_file = ENV['HOME'] + '/.hushed/credentials.yml'
|
24
|
+
test_credentials_file = "spec/fixtures/credentials.yml" unless File.exists?(test_credentials_file)
|
25
|
+
YAML.load(File.open(test_credentials_file, 'rb'))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class LineItemDouble
|
30
|
+
DEFAULT_OPTIONS = {
|
31
|
+
:id => 123456,
|
32
|
+
:quantity => 1,
|
33
|
+
:unit_of_measure => 'EA',
|
34
|
+
:price => '12.95'
|
35
|
+
}
|
36
|
+
|
37
|
+
attr_reader :id, :quantity, :unit_of_measure, :price
|
38
|
+
def initialize(options = {})
|
39
|
+
@id = options[:id]
|
40
|
+
@quantity = options[:quantity]
|
41
|
+
@unit_of_measure = options[:unit_of_measure]
|
42
|
+
@price = options[:price]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.example(options = {})
|
46
|
+
self.new(DEFAULT_OPTIONS.merge(options))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AddressDouble
|
51
|
+
DEFAULT_OPTIONS = {
|
52
|
+
:company => 'Shopify',
|
53
|
+
:name => 'John Smith',
|
54
|
+
:address1 => '123 Fake St',
|
55
|
+
:address2 => 'Unit 128',
|
56
|
+
:city => 'Ottawa',
|
57
|
+
:province_code => 'ON',
|
58
|
+
:country_code => 'CA',
|
59
|
+
:zip => 'K1N 5T5'
|
60
|
+
}
|
61
|
+
|
62
|
+
attr_reader :company, :name, :address1, :address2, :city, :province_code
|
63
|
+
attr_reader :country_code, :zip
|
64
|
+
def initialize(options = {})
|
65
|
+
@company = options[:company]
|
66
|
+
@name = options[:name]
|
67
|
+
@address1 = options[:address1]
|
68
|
+
@address2 = options[:address2]
|
69
|
+
@city = options[:city]
|
70
|
+
@province_code = options[:province_code]
|
71
|
+
@country_code = options[:country_code]
|
72
|
+
@zip = options[:zip]
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.example(options = {})
|
76
|
+
self.new(DEFAULT_OPTIONS.merge(options))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class ShippingLineDouble
|
81
|
+
def initialize(options = {})
|
82
|
+
@options = options
|
83
|
+
end
|
84
|
+
|
85
|
+
def carrier
|
86
|
+
@options[:code].split('_').first
|
87
|
+
end
|
88
|
+
|
89
|
+
def service_level
|
90
|
+
@options[:code].split('_').last
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class OrderDouble
|
95
|
+
DEFAULT_OPTIONS = {
|
96
|
+
:line_items => [LineItemDouble.example],
|
97
|
+
:shipping_address => AddressDouble.example,
|
98
|
+
:billing_address => AddressDouble.example,
|
99
|
+
:note => 'Happy Birthday',
|
100
|
+
:created_at => Time.new(2013, 04, 05, 12, 30, 00),
|
101
|
+
:id => 123456,
|
102
|
+
:shipping_lines => [ShippingLineDouble.new(code: "FEDEX_GROUND", price: "34.40", source: "fedex", title: "FedEx Ground")],
|
103
|
+
:email => 'john@smith.com',
|
104
|
+
:total_price => '123.45'
|
105
|
+
}
|
106
|
+
|
107
|
+
attr_reader :line_items, :shipping_address, :billing_address, :note, :email
|
108
|
+
attr_reader :total_price, :email, :id, :type, :created_at, :shipping_lines
|
109
|
+
def initialize(options = {})
|
110
|
+
@line_items = options[:line_items]
|
111
|
+
@shipping_address = options[:shipping_address]
|
112
|
+
@billing_address = options[:billing_address]
|
113
|
+
@note = options[:note]
|
114
|
+
@created_at = options[:created_at]
|
115
|
+
@id = options[:id]
|
116
|
+
@shipping_lines = options[:shipping_lines]
|
117
|
+
@email = options[:email]
|
118
|
+
@total_price = options[:total_price]
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.example(options = {})
|
122
|
+
self.new(DEFAULT_OPTIONS.merge(options))
|
123
|
+
end
|
124
|
+
|
125
|
+
def type
|
126
|
+
@type || "SO"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class DocumentDouble
|
131
|
+
include Hushed::Documents::Document
|
132
|
+
attr_accessor :type, :message_id, :warehouse, :date, :client
|
133
|
+
attr_accessor :business_unit, :document_number, :io
|
134
|
+
|
135
|
+
def initialize(options = {})
|
136
|
+
options.each do |key, value|
|
137
|
+
self.public_send("#{key}=".to_sym, value)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_xml
|
142
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
143
|
+
xml.DocumentDouble 'Hello World'
|
144
|
+
end
|
145
|
+
builder.to_xml
|
146
|
+
end
|
147
|
+
|
148
|
+
def filename=(filename)
|
149
|
+
@filename = filename
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class MessageDouble
|
154
|
+
attr_accessor :document_name, :document_type
|
155
|
+
|
156
|
+
def initialize(options = {})
|
157
|
+
@document_name = options[:document_name]
|
158
|
+
@document_type = options[:document_type]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class ClientDouble
|
163
|
+
attr_accessor :client_id, :business_unit
|
164
|
+
|
165
|
+
def initialize(options = {})
|
166
|
+
@client_id = options[:client_id]
|
167
|
+
@business_unit = options[:business_unit]
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'hushed/blackboard'
|
3
|
+
|
4
|
+
module Hushed
|
5
|
+
module Documents
|
6
|
+
module Response
|
7
|
+
class ThingerResponse
|
8
|
+
attr_reader :contents
|
9
|
+
def initialize(contents)
|
10
|
+
@contents = contents
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Request
|
16
|
+
class ThingerRequest
|
17
|
+
attr_reader :contents
|
18
|
+
def initialize(contents)
|
19
|
+
@contents = contents
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Hushed
|
27
|
+
describe "Blackboard" do
|
28
|
+
before do
|
29
|
+
@bucket = mock()
|
30
|
+
|
31
|
+
@client = mock()
|
32
|
+
@client.stubs(:to_quiet_bucket).returns(@bucket)
|
33
|
+
@client.stubs(:from_quiet_bucket).returns(@bucket)
|
34
|
+
|
35
|
+
@io = StringIO.new
|
36
|
+
|
37
|
+
@document = mock()
|
38
|
+
@document.stubs(:to_xml).returns("actually doesn't matter")
|
39
|
+
@document.stubs(:filename).returns("abracadabra_1234_xyz.xml")
|
40
|
+
|
41
|
+
@message = mock()
|
42
|
+
@message.stubs(:document_type).returns('ShipmentOrderResult')
|
43
|
+
@message.stubs(:document_name).returns('abracadabra_4321_zyx.xml')
|
44
|
+
|
45
|
+
@blackboard = Blackboard.new(@client)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be possible to post a document to the blackboard" do
|
49
|
+
@bucket.expects(:objects).returns({@document.filename => @io})
|
50
|
+
@blackboard.post(@document)
|
51
|
+
assert_io(@document.to_xml, @io)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should be possible to build a document for a response type" do
|
55
|
+
Response.expects(:valid_type?).returns(true)
|
56
|
+
response = @blackboard.build_document('ThingerResponse', 'thinger')
|
57
|
+
assert_equal 'thinger', response.contents[:io]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should be possible to build a document for a request type" do
|
61
|
+
Request.expects(:valid_type?).returns(true)
|
62
|
+
response = @blackboard.build_document('ThingerRequest', 'thinger')
|
63
|
+
assert_equal 'thinger', response.contents[:io]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return nil if the type was not valid" do
|
67
|
+
assert_equal nil, @blackboard.build_document('ThingerRequest', 'thinger')
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be possible to fetch a document from the blackboard" do
|
71
|
+
@io.write('fancy noodles')
|
72
|
+
@io.rewind
|
73
|
+
@bucket.expects(:objects).returns({@message.document_name => @io})
|
74
|
+
Response.expects(:valid_type?).returns(true)
|
75
|
+
@message.stubs(:document_type).returns('ThingerResponse')
|
76
|
+
assert_equal 'fancy noodles', @blackboard.fetch(@message).contents[:io]
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should be possible to remove a document from the blackboard" do
|
80
|
+
s3object = mock()
|
81
|
+
s3object.expects(:delete)
|
82
|
+
s3object.expects(:exists?).returns(true)
|
83
|
+
|
84
|
+
@bucket.expects(:objects).returns({@message.document_name => s3object})
|
85
|
+
assert_equal true, @blackboard.remove(@message)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should not raise an exception if the document for removal couldn't be found" do
|
89
|
+
s3object = mock()
|
90
|
+
s3object.expects(:delete).never
|
91
|
+
s3object.expects(:exists?).returns(false)
|
92
|
+
|
93
|
+
@bucket.expects(:objects).returns({@message.document_name => s3object})
|
94
|
+
assert_equal false, @blackboard.remove(@message)
|
95
|
+
end
|
96
|
+
|
97
|
+
def assert_io(expectation, io)
|
98
|
+
io.rewind
|
99
|
+
assert_equal expectation, io.read
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|