hushed 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.
- 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
|