hushed 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hushed.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Chris Saunders
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,64 @@
1
+ # Hushed
2
+
3
+ Client library for integrating with the Quiet Logistics API
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'hushed'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install hushed
18
+
19
+ ## Usage
20
+
21
+ Basic usage for Hushed goes as follows:
22
+
23
+
24
+ ```ruby
25
+ require 'hushed'
26
+ require 'hushed/documents/request/shipment_order'
27
+
28
+ credentials = {
29
+ access_key_id: 'AWS_ACCESS_KEY', secret_access_key: 'SECRET_ACCESS_KEY',
30
+ client_id: 'QUIET CLIENT ID', business_unit: 'QUIET BUSINESS UNIT',
31
+ warehouse: 'QUIET WAREHOUSE',
32
+ buckets: {
33
+ to: 'hushed-to-quiet',
34
+ from: 'hushed-from-quiet'
35
+ },
36
+ queues: {
37
+ to: http://queue.amazonaws/1234567890/hushed_to_quiet
38
+ from: http://queue.amazonaws/1234567890/hushed_from_quiet
39
+ inventory: http://queue.amazonaws/1234567890/hushed_inventory
40
+ }
41
+ }
42
+ client = Hushed::Client.new(credentials)
43
+ order = Order.new # Orders are expected to have similar attributes as ShopifyAPI::Order
44
+ document = Hushed::Documents::Request::ShipmentOrder.new(client: client, order: order)
45
+
46
+ blackboard = Hushed::Blackboard.new(client)
47
+ queue = Hushed::Queue.new(client)
48
+
49
+ message = blackboard.post(document)
50
+ queue.send(message)
51
+
52
+ response_message = queue.receive
53
+ response_document = blackboard.fetch(message)
54
+ process_document(response_document)
55
+ blackboard.remove(message)
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create new Pull Request
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs = ['lib', 'spec']
6
+ t.ruby_opts << '-rubygems'
7
+ t.verbose = true
8
+ t.test_files = FileList['spec/**/*_spec.rb']
9
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hushed/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hushed"
8
+ spec.version = Hushed::VERSION
9
+ spec.authors = ["Chris Saunders"]
10
+ spec.email = ["chris.saunders@shopify.com"]
11
+ spec.description = "API Client for Quiet Logistics Services"
12
+ spec.summary = "Integrates with QL Blackboard and work Queue"
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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('aws-sdk', '~> 1.10.0')
22
+ spec.add_dependency('nokogiri')
23
+ spec.add_dependency('activesupport')
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "minitest", ">= 5.0.0"
28
+ spec.add_development_dependency "mocha"
29
+ end
@@ -0,0 +1,6 @@
1
+ require "nokogiri"
2
+ require "hushed/documents/document"
3
+ require "hushed/version"
4
+ require "hushed/client"
5
+ require "hushed/blackboard"
6
+ require "hushed/queue"
@@ -0,0 +1,44 @@
1
+ require 'hushed/response'
2
+ require 'hushed/request'
3
+ module Hushed
4
+ class Blackboard
5
+ attr_reader :client
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ def post(document)
12
+ bucket = client.to_quiet_bucket
13
+ if bucket.objects[document.filename].write(document.to_xml)
14
+ Message.new(:client => @client, :document => document)
15
+ end
16
+ end
17
+
18
+ def fetch(message)
19
+ bucket = client.from_quiet_bucket
20
+ contents = bucket.objects[message.document_name].read
21
+ build_document(message.document_type, contents)
22
+ end
23
+
24
+ def remove(message)
25
+ bucket = client.from_quiet_bucket
26
+ object = bucket.objects[message.document_name]
27
+ if object.exists?
28
+ object.delete
29
+ true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def build_document(type, contents)
36
+ namespace = if Response.valid_type?(type)
37
+ Documents::Response
38
+ elsif Request.valid_type?(type)
39
+ Documents::Request
40
+ end
41
+ namespace.const_get(type).new(:io => contents) if namespace
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,92 @@
1
+ require 'aws/s3'
2
+ require 'aws/sqs'
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
6
+ module Hushed
7
+ class Client
8
+
9
+ class InitializationError < StandardError; end
10
+ class InvalidBucketError < StandardError; end
11
+ class InvalidQueueError < StandardError; end
12
+
13
+ attr_reader :client_id, :business_unit, :warehouse
14
+
15
+ def initialize(options = {})
16
+ options = options.with_indifferent_access
17
+ @buckets = options[:buckets]
18
+ @queues = options[:queues]
19
+ @client_id = options[:client_id]
20
+ @warehouse = options[:warehouse]
21
+ @business_unit = options[:business_unit]
22
+ @credentials = options.slice(:access_key_id, :secret_access_key)
23
+ verify!
24
+ end
25
+
26
+ def to_quiet_bucket
27
+ @to_quiet_bucket ||= fetch_bucket(@buckets[:to])
28
+ end
29
+
30
+ def from_quiet_bucket
31
+ @from_quiet_bucket ||= fetch_bucket(@buckets[:from])
32
+ end
33
+
34
+ def to_quiet_queue
35
+ @to_quiet_queue ||= fetch_queue(@queues[:to])
36
+ end
37
+
38
+ def from_quiet_queue
39
+ @from_quiet_queue ||= fetch_queue(@queues[:from])
40
+ end
41
+
42
+ def quiet_inventory_queue
43
+ @quiet_inventory_queue ||= fetch_queue(@queues[:inventory])
44
+ end
45
+
46
+ private
47
+ def sqs_client
48
+ @sqs_client ||= AWS::SQS.new(@credentials)
49
+ end
50
+
51
+ def s3_client
52
+ @sqs_client ||= AWS::S3.new(@credentials)
53
+ end
54
+
55
+ def fetch_bucket(name)
56
+ bucket = s3_client.buckets[name]
57
+ raise(InvalidBucketError.new("#{name} is not a valid bucket")) unless bucket.exists?
58
+ bucket
59
+ end
60
+
61
+ def fetch_queue(url)
62
+ queue = sqs_client.queues[url]
63
+ raise(InvalidQueueError.new("#{url} is not a valid queue")) unless queue.exists?
64
+ queue
65
+ end
66
+
67
+ def verify!
68
+ raise(InitializationError.new("Credentials cannot be missing")) unless all_credentials?
69
+ raise(InitializationError.new("Both to and from buckets need to be set")) unless all_buckets?
70
+ raise(InitializationError.new("To, From and Inventory queues need to be set")) unless all_queues?
71
+ raise(InitializationError.new("client_id needs to be set")) unless @client_id
72
+ raise(InitializationError.new("business_unit needs to be set")) unless @business_unit
73
+ raise(InitializationError.new("warehouse needs to be set")) unless @warehouse
74
+ end
75
+
76
+ def all_credentials?
77
+ has_keys?(@credentials, [:access_key_id, :secret_access_key])
78
+ end
79
+
80
+ def all_buckets?
81
+ has_keys?(@buckets, [:from, :to])
82
+ end
83
+
84
+ def all_queues?
85
+ has_keys?(@queues, [:from, :to, :inventory])
86
+ end
87
+
88
+ def has_keys?(hash, keys)
89
+ keys.reduce(hash) {|result, key| result && hash[key]}
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,15 @@
1
+ module Hushed
2
+ module Documents
3
+ module Document
4
+ DATEFORMAT = "%Y%m%d_%H%M%S"
5
+
6
+ def to_xml
7
+ raise NotImplementedError("To be implemented by subclasses")
8
+ end
9
+
10
+ def filename
11
+ @filename ||= "#{business_unit}_#{type}_#{document_number}_#{date.strftime(DATEFORMAT)}.xml"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,89 @@
1
+ module Hushed
2
+ module Documents
3
+ module Request
4
+ class ShipmentOrder
5
+ include Hushed::Documents::Document
6
+ extend Forwardable
7
+
8
+ NAMESPACE = "http://schemas.quietlogistics.com/V2/ShipmentOrder.xsd"
9
+
10
+ DATEFORMAT = "%Y%m%d_%H%M%S"
11
+
12
+ class MissingOrderError < StandardError; end
13
+ class MissingClientError < StandardError; end
14
+
15
+ attr_reader :order, :client
16
+
17
+ def_delegators :@client, :warehouse, :business_unit, :client_id
18
+
19
+ def initialize(options = {})
20
+ @order = options[:order] || raise(MissingOrderError.new("order cannot be missing"))
21
+ @client = options[:client] || raise(MissingClientError.new("client cannot be missing"))
22
+ end
23
+
24
+ def to_xml
25
+ builder = Nokogiri::XML::Builder.new do |xml|
26
+ xml.ShipOrderDocument(xmlns: NAMESPACE) do
27
+ xml.ClientID client_id
28
+ xml.BusinessUnit business_unit
29
+ xml.OrderHeader(order_header_attributes) do
30
+ xml.Comments @order.note
31
+ xml.ShipMode(shipping_attributes(@order.shipping_lines.first))
32
+ xml.ShipTo(address_attributes(@order.shipping_address))
33
+ xml.BillTo(address_attributes(@order.billing_address))
34
+ xml.DeclaredValue @order.total_price
35
+ @order.line_items.each.with_index(1) do |line_item, index|
36
+ xml.OrderDetails(line_item_attributes(line_item, index))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ builder.to_xml
42
+ end
43
+
44
+ def type
45
+ 'ShipmentOrder'
46
+ end
47
+
48
+ def document_number
49
+ @order.id
50
+ end
51
+
52
+ def order_type
53
+ 'SO'
54
+ end
55
+
56
+ def date
57
+ @order.created_at.utc
58
+ end
59
+
60
+ def filename
61
+ "#{business_unit}_#{type}_#{document_number}_#{date.strftime(DATEFORMAT)}.xml"
62
+ end
63
+
64
+ def order_header_attributes
65
+ {OrderNumber: document_number, OrderType: order_type, OrderDate: date, ShipDate: date}
66
+ end
67
+
68
+ def shipping_attributes(shipping_line)
69
+ {Carrier: shipping_line.carrier, ServiceLevel: shipping_line.service_level}
70
+ end
71
+
72
+ def address_attributes(address)
73
+ {
74
+ Company: address.company, Contact: address.name, Address1: address.address1, Address2: address.address2,
75
+ City: address.city, State: address.province_code, PostalCode: address.zip, Country: address.country_code,
76
+ Email: @order.email
77
+ }
78
+ end
79
+
80
+ def line_item_attributes(line_item, line_number)
81
+ {
82
+ ItemNumber: line_item.id, Line: line_number, QuantityOrdered: line_item.quantity,
83
+ QuantityToShip: line_item.quantity, UOM: line_item.unit_of_measure, Price: line_item.price
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,56 @@
1
+ require 'time'
2
+ module Hushed
3
+ module Documents
4
+ module Response
5
+ class ShipmentOrderResult
6
+ attr_reader :io
7
+
8
+ def initialize(options = {})
9
+ @io = options[:io]
10
+ @document = Nokogiri::XML::Document.parse(@io)
11
+ end
12
+
13
+ def client_id
14
+ @client_id ||= so_result['ClientID']
15
+ end
16
+
17
+ def business_unit
18
+ @business_unit ||= so_result['BusinessUnit']
19
+ end
20
+
21
+ def date_shipped
22
+ @date_shipped ||= Time.parse(so_result['DateShipped']).utc
23
+ end
24
+
25
+ def order_number
26
+ @order_number ||= so_result['OrderNumber']
27
+ end
28
+
29
+ def carton_count
30
+ @carton_count ||= so_result['CartonCount'].to_i
31
+ end
32
+
33
+ def carrier
34
+ @carrier ||= carton['Carrier']
35
+ end
36
+
37
+ def service_level
38
+ @service_level ||= carton['ServiceLevel']
39
+ end
40
+
41
+ def tracking_number
42
+ @tracking_number ||= carton['TrackingId']
43
+ end
44
+
45
+ def so_result
46
+ @so_result ||= @document.css('SOResult').first
47
+ end
48
+
49
+ def carton
50
+ @carton ||= @document.css('Carton').first
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end