mws-connect 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.
Files changed (57) hide show
  1. data/.gitignore +19 -0
  2. data/.sublime-project +19 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +111 -0
  6. data/Rakefile +1 -0
  7. data/lib/mws.rb +34 -0
  8. data/lib/mws/apis.rb +6 -0
  9. data/lib/mws/apis/feeds.rb +20 -0
  10. data/lib/mws/apis/feeds/api.rb +103 -0
  11. data/lib/mws/apis/feeds/distance.rb +23 -0
  12. data/lib/mws/apis/feeds/feed.rb +114 -0
  13. data/lib/mws/apis/feeds/image_listing.rb +44 -0
  14. data/lib/mws/apis/feeds/inventory.rb +77 -0
  15. data/lib/mws/apis/feeds/measurement.rb +32 -0
  16. data/lib/mws/apis/feeds/money.rb +31 -0
  17. data/lib/mws/apis/feeds/price_listing.rb +48 -0
  18. data/lib/mws/apis/feeds/product.rb +173 -0
  19. data/lib/mws/apis/feeds/sale_price.rb +31 -0
  20. data/lib/mws/apis/feeds/shipping.rb +160 -0
  21. data/lib/mws/apis/feeds/submission_info.rb +45 -0
  22. data/lib/mws/apis/feeds/submission_result.rb +87 -0
  23. data/lib/mws/apis/feeds/transaction.rb +37 -0
  24. data/lib/mws/apis/feeds/weight.rb +19 -0
  25. data/lib/mws/apis/orders.rb +23 -0
  26. data/lib/mws/connection.rb +84 -0
  27. data/lib/mws/enum.rb +81 -0
  28. data/lib/mws/errors.rb +32 -0
  29. data/lib/mws/query.rb +45 -0
  30. data/lib/mws/serializer.rb +81 -0
  31. data/lib/mws/signer.rb +20 -0
  32. data/lib/mws/utils.rb +50 -0
  33. data/mws.gemspec +25 -0
  34. data/scripts/catalog-workflow +136 -0
  35. data/spec/mws/apis/feeds/api_spec.rb +229 -0
  36. data/spec/mws/apis/feeds/distance_spec.rb +43 -0
  37. data/spec/mws/apis/feeds/feed_spec.rb +92 -0
  38. data/spec/mws/apis/feeds/image_listing_spec.rb +109 -0
  39. data/spec/mws/apis/feeds/inventory_spec.rb +135 -0
  40. data/spec/mws/apis/feeds/measurement_spec.rb +84 -0
  41. data/spec/mws/apis/feeds/money_spec.rb +43 -0
  42. data/spec/mws/apis/feeds/price_listing_spec.rb +90 -0
  43. data/spec/mws/apis/feeds/product_spec.rb +264 -0
  44. data/spec/mws/apis/feeds/shipping_spec.rb +78 -0
  45. data/spec/mws/apis/feeds/submission_info_spec.rb +111 -0
  46. data/spec/mws/apis/feeds/submission_result_spec.rb +157 -0
  47. data/spec/mws/apis/feeds/transaction_spec.rb +64 -0
  48. data/spec/mws/apis/feeds/weight_spec.rb +43 -0
  49. data/spec/mws/apis/orders_spec.rb +9 -0
  50. data/spec/mws/connection_spec.rb +331 -0
  51. data/spec/mws/enum_spec.rb +166 -0
  52. data/spec/mws/query_spec.rb +104 -0
  53. data/spec/mws/serializer_spec.rb +187 -0
  54. data/spec/mws/signer_spec.rb +67 -0
  55. data/spec/mws/utils_spec.rb +147 -0
  56. data/spec/spec_helper.rb +10 -0
  57. metadata +220 -0
@@ -0,0 +1,44 @@
1
+ require 'open-uri'
2
+
3
+ module Mws::Apis::Feeds
4
+
5
+ class ImageListing
6
+
7
+ Type = Mws::Enum.for(
8
+ main: 'Main',
9
+ alt1: 'PT1',
10
+ alt2: 'PT2',
11
+ alt3: 'PT3',
12
+ alt4: 'PT4',
13
+ alt5: 'PT5'
14
+ )
15
+
16
+ attr_reader :sku, :url
17
+
18
+ Mws::Enum.sym_reader self, :type
19
+
20
+ def initialize(sku, url, type=nil)
21
+ raise Mws::Errors::ValidationError, 'SKU is required.' if sku.nil? or sku.strip.empty?
22
+ @sku = sku
23
+ raise Mws::Errors::ValidationError, 'URL must be an unsecured http address.' unless url =~ URI::regexp('http')
24
+ @url = url
25
+ @type = Type.for(type) || Type.MAIN
26
+ end
27
+
28
+ def ==(other)
29
+ return true if equal? other
30
+ return false unless other.class == self.class
31
+ sku == other.sku and url == other.url and type == other.type
32
+ end
33
+
34
+ def to_xml(name='ProductImage', parent=nil)
35
+ Mws::Serializer.tree name, parent do | xml |
36
+ xml.SKU @sku
37
+ xml.ImageType @type.val
38
+ xml.ImageLocation @url
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,77 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Inventory
4
+
5
+ attr_reader :sku, :available, :quantity, :lookup, :fulfillment, :restock
6
+
7
+ def initialize(sku, options)
8
+ @sku = sku
9
+ @available = options[:available]
10
+ @quantity = options[:quantity]
11
+ @lookup = options[:lookup]
12
+ @fulfillment = Fulfillment.new(
13
+ options[:fulfillment_center],
14
+ options[:fulfillment_latency],
15
+ options[:fulfillment_type]
16
+ )
17
+ @restock = options[:restock]
18
+ validate
19
+ end
20
+
21
+ def to_xml(name='Inventory', parent=nil)
22
+ Mws::Serializer.tree name, parent do |xml|
23
+ xml.SKU @sku
24
+ xml.FulfillmentCenterID @fulfillment.center unless @fulfillment.center.nil?
25
+ xml.Available @available unless @available.nil?
26
+ xml.Quantity @quantity unless @quantity.nil?
27
+ xml.Lookup @lookup unless @lookup.nil?
28
+ xml.RestockDate @restock.iso8601 unless @restock.nil?
29
+ xml.FulfillmentLatency @fulfillment.latency unless @fulfillment.latency.nil?
30
+ xml.SwitchFulfillmentTo Fulfillment::Type.for(@fulfillment.type).val unless @fulfillment.type.nil?
31
+ end
32
+ end
33
+
34
+ class Fulfillment
35
+
36
+ Type = Mws::Enum.for afn: 'AFN', mfn: 'MFN'
37
+
38
+ attr_reader :center, :latency
39
+
40
+ Mws::Enum.sym_reader self, :type
41
+
42
+ def initialize(center, latency, type)
43
+ @center = center
44
+ @latency = latency
45
+ unless @latency.nil? or (@latency.to_i == @latency and @latency > 0)
46
+ raise Mws::Errors::ValidationError.new('Fulfillment latency must be a whole number greater than zero.')
47
+ end
48
+ @type = Type.for(type)
49
+ raise Mws::Errors::ValidationError.new("Fulfillment type must be either 'AFN' or 'MFN'.") if type and @type.nil?
50
+ end
51
+
52
+ end
53
+
54
+ private
55
+
56
+ def validate
57
+ raise Mws::Errors::ValidationError.new('SKU is required.') if @sku.nil? or @sku.to_s.strip.empty?
58
+ unless [ @available, @quantity, @lookup ].compact.size == 1
59
+ raise Mws::Errors::ValidationError.new("One and only one of 'available', 'quantity' or 'lookup' must be specified.")
60
+ end
61
+ unless @available.nil? or [ true, false ].include? @available
62
+ raise Mws::Errors::ValidationError.new('Available must be either true or false.')
63
+ end
64
+ unless @quantity.nil? or (@quantity.to_i == @quantity and @quantity >= 0)
65
+ raise Mws::Errors::ValidationError.new('Quantity must be a whole number greater than or equal to zero.')
66
+ end
67
+ unless @lookup.nil? or [ true, false ].include? @lookup
68
+ raise Mws::Errors::ValidationError.new('Lookup must be either true or false.')
69
+ end
70
+ unless @restock.nil? or (@restock.respond_to? :iso8601 and Time.now < @restock)
71
+ raise Mws::Errors::ValidationError.new('Restock date must be in the future.')
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,32 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Measurement
4
+
5
+ attr_reader :amount
6
+
7
+ Mws::Enum.sym_reader self, :unit
8
+
9
+ def initialize(amount, unit)
10
+ @amount = amount
11
+ @unit = self.class.const_get(:Unit).for(unit)
12
+ raise Mws::Errors::ValidationError, "Invalid unit of measure '#{unit}'" if @unit.nil?
13
+
14
+ end
15
+
16
+ def ==(other)
17
+ return true if equal? other
18
+ return false unless other.class == self.class
19
+ amount == other.amount and unit == other.unit
20
+ end
21
+
22
+ def to_xml(name=nil, parent=nil)
23
+ name ||= self.class.name.split('::').last
24
+ amount = @amount
25
+ amount = '%.2f' % amount if amount.to_s =~ /\d*\.\d\d+/
26
+ Mws::Serializer.leaf name, parent, amount, unitOfMeasure: @unit.val
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
@@ -0,0 +1,31 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Money < Measurement
4
+
5
+ Currency = Mws::Enum.for(
6
+ usd: 'USD',
7
+ gbp: 'GBP',
8
+ eur: 'EUR',
9
+ jpy: 'JPY',
10
+ cad: 'CAD',
11
+ default: 'DEFAULT'
12
+ )
13
+
14
+ Unit = Currency
15
+
16
+ def initialize(amount, currency=nil)
17
+ raise Mws::Errors::ValidationError, "Invalid currency '#{currency}'" if currency and Currency.for(currency).nil?
18
+ super amount, currency || :usd
19
+ end
20
+
21
+ def currency
22
+ unit
23
+ end
24
+
25
+ def to_xml(name='Price', parent=nil)
26
+ Mws::Serializer.leaf name, parent, '%.2f' % @amount, currency: @unit.val
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,48 @@
1
+ require 'nokogiri'
2
+
3
+ module Mws::Apis::Feeds
4
+
5
+ class PriceListing
6
+
7
+ attr_reader :sku, :currency, :base, :sale, :min
8
+
9
+ def initialize(sku, base, options={})
10
+ @sku = sku
11
+ @base = Money.new(base, options[:currency])
12
+ @currency = @base.currency
13
+ @min = Money.new(options[:min], @currency) if options.include? :min
14
+ on_sale(options[:sale][:amount], options[:sale][:from], options[:sale][:to]) if options.include? :sale
15
+ validate
16
+ end
17
+
18
+ def on_sale(amount, from, to)
19
+ @sale = SalePrice.new Money.new(amount, @currency), from, to
20
+ validate
21
+ self
22
+ end
23
+
24
+ def to_xml(name='Price', parent=nil)
25
+ Mws::Serializer.tree name, parent do |xml|
26
+ xml.SKU @sku
27
+ @base.to_xml 'StandardPrice', xml
28
+ @min.to_xml 'MAP', xml if @min
29
+ @sale.to_xml 'Sale', xml if @sale
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def validate
36
+ if @min
37
+ unless @min.amount < @base.amount
38
+ raise Mws::Errors::ValidationError, "'Base Price' must be greater than 'Minimum Advertised Price'."
39
+ end
40
+ if @sale and @sale.price.amount <= @min.amount
41
+ raise Mws::Errors::ValidationError, "'Sale Price' must be greater than 'Minimum Advertised Price'."
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,173 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Product
4
+
5
+ CategorySerializer = Mws::Serializer.new ce: 'CE', fba: 'FBA', eu_compliance: 'EUCompliance'
6
+
7
+ attr_reader :sku, :description
8
+
9
+ attr_accessor :upc, :tax_code, :msrp, :brand, :manufacturer, :name, :description, :bullet_points
10
+ attr_accessor :item_dimensions, :package_dimensions, :package_weight, :shipping_weight
11
+ attr_accessor :category, :details
12
+
13
+ def initialize(sku, &block)
14
+ @sku = sku
15
+ @bullet_points = []
16
+ ProductBuilder.new(self).instance_eval &block if block_given?
17
+ raise Mws::Errors::ValidationError, 'Product must have a category when details are specified.' if @details and @category.nil?
18
+ end
19
+
20
+ def to_xml(name='Product', parent=nil)
21
+ Mws::Serializer.tree name, parent do |xml|
22
+ xml.SKU @sku
23
+ xml.StandardProductID {
24
+ xml.Type 'UPC'
25
+ xml.Value @upc
26
+ } unless @upc.nil?
27
+ xml.ProductTaxCode @tax_code unless @upc.nil?
28
+ xml.DescriptionData {
29
+ xml.Title @name unless @name.nil?
30
+ xml.Brand @brand unless @brand.nil?
31
+ xml.Description @description unless @description.nil?
32
+ bullet_points.each do | bullet_point |
33
+ xml.BulletPoint bullet_point
34
+ end
35
+ @item_dimensions.to_xml('ItemDimensions', xml) unless @item_dimensions.nil?
36
+ @package_dimensions.to_xml('PackageDimensions', xml) unless @item_dimensions.nil?
37
+
38
+ @package_weight.to_xml('PackageWeight', xml) unless @package_weight.nil?
39
+ @shipping_weight.to_xml('ShippingWeight', xml) unless @shipping_weight.nil?
40
+
41
+ @msrp.to_xml 'MSRP', xml unless @msrp.nil?
42
+
43
+ xml.Manufacturer @manufacturer unless @manufacturer.nil?
44
+ }
45
+
46
+ unless @details.nil?
47
+ xml.ProductData {
48
+ CategorySerializer.xml_for @category, {product_type: @details}, xml
49
+ }
50
+ end
51
+ end
52
+ end
53
+
54
+ class DelegatingBuilder
55
+
56
+ def initialize(delegate)
57
+ @delegate = delegate
58
+ end
59
+
60
+ def method_missing(method, *args, &block)
61
+ @delegate.send("#{method}=", *args, &block) if @delegate.respond_to? "#{method}="
62
+ end
63
+ end
64
+
65
+ class ProductBuilder < DelegatingBuilder
66
+
67
+ def initialize(product)
68
+ super product
69
+ @product = product
70
+ end
71
+
72
+ def msrp(amount, currency)
73
+ @product.msrp = Money.new amount, currency
74
+ end
75
+
76
+ def item_dimensions(&block)
77
+ @product.item_dimensions = Dimensions.new
78
+ DimensionsBuilder.new(@product.item_dimensions).instance_eval &block if block_given?
79
+ end
80
+
81
+ def package_dimensions(&block)
82
+ @product.package_dimensions = Dimensions.new
83
+ DimensionsBuilder.new(@product.package_dimensions).instance_eval &block if block_given?
84
+ end
85
+
86
+ def package_weight(value, unit=nil)
87
+ @product.package_weight = Weight.new(value, unit)
88
+ end
89
+
90
+ def shipping_weight(value, unit=nil)
91
+ @product.shipping_weight = Weight.new(value, unit)
92
+ end
93
+
94
+ def bullet_point(bullet_point)
95
+ @product.bullet_points << bullet_point
96
+ end
97
+
98
+ def details(details=nil, &block)
99
+ @product.details = details || {}
100
+ DetailBuilder.new(@product.details).instance_eval &block if block_given?
101
+ end
102
+
103
+ end
104
+
105
+ class Dimensions
106
+
107
+ attr_accessor :length, :width, :height, :weight
108
+
109
+ def to_xml(name='Dimensions', parent=nil)
110
+ Mws::Serializer.tree name, parent do |xml|
111
+ @length.to_xml 'Length', xml unless @length.nil?
112
+ @width.to_xml 'Width', xml unless @width.nil?
113
+ @height.to_xml 'Height', xml unless @height.nil?
114
+ @weight.to_xml 'Weight', xml unless @weight.nil?
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ class DimensionsBuilder
121
+
122
+ def initialize(dimensions)
123
+ @dimensions = dimensions
124
+ end
125
+
126
+ def length(value, unit=nil)
127
+ @dimensions.length = Distance.new(value, unit)
128
+ end
129
+
130
+ def width(value, unit=nil)
131
+ @dimensions.width = Distance.new(value, unit)
132
+ end
133
+
134
+ def height(value, unit=nil)
135
+ @dimensions.height = Distance.new(value, unit)
136
+ end
137
+
138
+ def weight(value, unit=nil)
139
+ @dimensions.weight = Weight.new(value, unit)
140
+ end
141
+ end
142
+
143
+ class DetailBuilder
144
+
145
+ def initialize(details)
146
+ @details = details
147
+ end
148
+
149
+ def as_distance(amount, unit=nil)
150
+ Distance.new amount, unit
151
+ end
152
+
153
+ def as_weight(amount, unit=nil)
154
+ Weight.new amount, unit
155
+ end
156
+
157
+ def as_money(amount, currency=nil)
158
+ Money.new amount, currency
159
+ end
160
+
161
+ def method_missing(method, *args, &block)
162
+ if block_given?
163
+ @details[method] = {}
164
+ DetailBuilder.new(@details[method]).instance_eval(&block)
165
+ elsif args.length > 0
166
+ @details[method] = args[0]
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,31 @@
1
+ require 'nokogiri'
2
+
3
+ module Mws::Apis::Feeds
4
+
5
+ class SalePrice
6
+
7
+ attr_reader :price, :from, :to
8
+
9
+ def initialize(price, from, to)
10
+ @price = price
11
+ @from = from
12
+ @to = to
13
+ end
14
+
15
+ def ==(other)
16
+ return true if equal? other
17
+ return false unless other.class == self.class
18
+ @price == other.price and @from == other.from and @to == other.to
19
+ end
20
+
21
+ def to_xml(name='Sale', parent=nil)
22
+ Mws::Serializer.tree name, parent do |xml|
23
+ xml.StartDate @from.iso8601
24
+ xml.EndDate @to.iso8601
25
+ price.to_xml 'SalePrice', xml
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,160 @@
1
+ require 'nokogiri'
2
+
3
+ module Mws::Apis::Feeds
4
+
5
+ class Shipping
6
+
7
+ Region = Mws::Enum.for(
8
+ continental_us: 'Cont US',
9
+ us_protectorates: 'US Prot',
10
+ alaska_hawaii: 'Alaska Hawaii',
11
+ apo_fpo: 'APO/FPO',
12
+ canada: 'Canada',
13
+ europe: 'Europe',
14
+ asia: 'Asia',
15
+ other: 'Outside US, EU, CA, Asia'
16
+ )
17
+
18
+ Variant = Mws::Enum.for(
19
+ street: 'Street Addr',
20
+ po_box: 'PO Box'
21
+ )
22
+
23
+ Speed = Mws::Enum.for(
24
+ standard: 'Std',
25
+ expedited: 'Exp',
26
+ two_day: 'Second',
27
+ one_day: 'Next'
28
+ )
29
+
30
+ attr_reader :sku
31
+
32
+ def initialize(sku, &block)
33
+ raise Mws::Errors::ValidationError.new('SKU is required.') if sku.nil? or sku.to_s.strip.empty?
34
+ @sku = sku
35
+ @options = []
36
+ Builder.new(self).instance_eval &block if block_given?
37
+ end
38
+
39
+ def options
40
+ @options.dup
41
+ end
42
+
43
+ def <<(option)
44
+ @options << option
45
+ end
46
+
47
+ def to_xml(name='Override', parent=nil)
48
+ Mws::Serializer.tree name, parent do |xml|
49
+ xml.SKU @sku
50
+ @options.each { |option| option.to_xml 'ShippingOverride', xml }
51
+ end
52
+ end
53
+
54
+ class Option
55
+
56
+ Mws::Enum.sym_reader self, :region, :speed, :variant
57
+
58
+ def initialize(region, speed=Speed.STANDARD, variant=nil)
59
+ @region = Region.for(region)
60
+ @speed = Speed.for(speed)
61
+ @variant = nil
62
+ if supports_variant?
63
+ @variant = Variant.for(variant) || Variant.STREET
64
+ end
65
+ end
66
+
67
+ def supports_variant?
68
+ [ Region.CONTINENTAL_US, Region.US_PROTECTORATES, Region.ALASKA_HAWAII, Region.APO_FPO ].include? @region
69
+ end
70
+
71
+ def to_s
72
+ return @speed.val if [ Speed.TWO_DAY, Speed.ONE_DAY ].include? @speed
73
+ [ @speed, @region, @variant ].compact.map { |it| it.val }.join ' '
74
+ end
75
+
76
+ end
77
+
78
+ class Restriction
79
+
80
+ attr_reader :option, :restricted
81
+
82
+ def initialize(option, restricted=true)
83
+ @option = option
84
+ @restricted = restricted
85
+ end
86
+
87
+ def to_xml(name='ShippingOverride', parent=nil)
88
+ Mws::Serializer.tree name, parent do |xml|
89
+ xml.ShipOption @option
90
+ xml.IsShippingRestricted @restricted
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ class Override
97
+
98
+ Type = Mws::Enum.for(
99
+ adjust: 'Additive',
100
+ replace: 'Exclusive'
101
+ )
102
+
103
+ attr_reader :option, :amount
104
+
105
+ Mws::Enum.sym_reader self, :type
106
+
107
+ def initialize(option, type, amount)
108
+ @option = option
109
+ @type = Type.for(type)
110
+ @amount = amount
111
+ end
112
+
113
+ def to_xml(name='ShippingOverride', parent=nil)
114
+ Mws::Serializer.tree name, parent do |xml|
115
+ xml.ShipOption @option
116
+ xml.Type @type.val
117
+ @amount.to_xml 'ShipAmount', xml
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ class Builder
124
+
125
+ @target
126
+
127
+ def initialize(target)
128
+ @target = target
129
+ end
130
+
131
+ def restriction(restricted, region, speed, variant)
132
+ @target << Restriction.new(Option.new(region, speed, variant), restricted)
133
+ end
134
+
135
+ def restricted(region, speed, variant=nil)
136
+ restriction true, region, speed, variant
137
+ end
138
+
139
+ def unrestricted(region, speed, variant=nil)
140
+ restriction false, region, speed, variant
141
+ end
142
+
143
+ def override(type, amount, currency, region, speed, variant)
144
+ @target << Override.new(Option.new(region, speed, variant), type,
145
+ Money.new(amount, currency))
146
+ end
147
+
148
+ def adjust(amount, currency, region, speed, variant=nil)
149
+ override :adjust, amount, currency, region, speed, variant
150
+ end
151
+
152
+ def replace(amount, currency, region, speed, variant=nil)
153
+ override :replace, amount, currency, region, speed, variant
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+
160
+ end