vending-machine 0.1.0

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 (43) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +19 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +155 -0
  6. data/Rakefile +14 -0
  7. data/lib/vend.rb +46 -0
  8. data/lib/vend/errors.rb +12 -0
  9. data/lib/vend/has_resources.rb +34 -0
  10. data/lib/vend/modules/declarative_setters.rb +74 -0
  11. data/lib/vend/modules/finders.rb +5 -0
  12. data/lib/vend/modules/paths.rb +37 -0
  13. data/lib/vend/remote_resource.rb +46 -0
  14. data/lib/vend/resource.rb +100 -0
  15. data/lib/vend/resource_collection.rb +72 -0
  16. data/lib/vend/resources/brand.rb +4 -0
  17. data/lib/vend/resources/contact.rb +4 -0
  18. data/lib/vend/resources/customer.rb +8 -0
  19. data/lib/vend/resources/inventory.rb +14 -0
  20. data/lib/vend/resources/outlet.rb +4 -0
  21. data/lib/vend/resources/payment_type.rb +4 -0
  22. data/lib/vend/resources/price_book_entry.rb +10 -0
  23. data/lib/vend/resources/product.rb +30 -0
  24. data/lib/vend/resources/product_type.rb +7 -0
  25. data/lib/vend/resources/register.rb +9 -0
  26. data/lib/vend/resources/register_sale_payment.rb +6 -0
  27. data/lib/vend/resources/register_sale_product.rb +12 -0
  28. data/lib/vend/resources/sale.rb +17 -0
  29. data/lib/vend/resources/supplier.rb +6 -0
  30. data/lib/vend/resources/tax.rb +13 -0
  31. data/lib/vend/resources/user.rb +4 -0
  32. data/lib/vend/response.rb +9 -0
  33. data/lib/vend/store.rb +103 -0
  34. data/lib/vend/version.rb +3 -0
  35. data/lib/vending-machine.rb +1 -0
  36. data/spec/resource_spec.rb +22 -0
  37. data/spec/resource_urls_spec.rb +59 -0
  38. data/spec/spec_helper.rb +8 -0
  39. data/spec/store_spec.rb +21 -0
  40. data/spec/support/url_assertions.rb +23 -0
  41. data/spec/support/webmock.rb +53 -0
  42. data/vending-machine.gemspec +46 -0
  43. metadata +238 -0
@@ -0,0 +1,46 @@
1
+ module Vend
2
+ class RemoteResource < Resource
3
+ include Vend::DeclarativeSetters
4
+ include Vend::Paths
5
+
6
+ def save
7
+ id ? save_existing : save_new
8
+ end
9
+
10
+ def load
11
+ self.attributes = store.get(
12
+ path: resource_path,
13
+ status: 200
14
+ ).data
15
+ self
16
+ end
17
+
18
+ private
19
+
20
+ def sendable_attributes
21
+ blacklist = %w{
22
+ created_at
23
+ updated_at
24
+ }
25
+ attributes.delete_if{|name,value| blacklist.include? name }.dup
26
+ end
27
+
28
+ def save_new
29
+ self.attributes = store.post(
30
+ path: create_path,
31
+ parameters: sendable_attributes,
32
+ status: 200
33
+ ).data
34
+ self
35
+ end
36
+
37
+ def save_existing
38
+ self.attributes = store.put(
39
+ path: update_path,
40
+ parameters: sendable_attributes,
41
+ status: 200
42
+ ).data
43
+ self
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,100 @@
1
+ require 'virtus'
2
+
3
+ module Vend
4
+ class Resource
5
+ include Virtus
6
+
7
+ attr_accessor :store
8
+
9
+ attribute :id, String
10
+
11
+ alias_method :initialize_virtus, :initialize
12
+ def initialize attributes={}
13
+ if attributes[:store_object]
14
+ @store = store_object
15
+ end
16
+ initialize_virtus attributes
17
+ end
18
+
19
+ alias_method :set_attributes_virtus, :set_attributes
20
+ def set_attributes(attributes)
21
+ public_method_names = public_methods.map(&:to_s)
22
+
23
+ unknown_attributes = attributes.
24
+ dup.
25
+ delete_if{|k, v| public_method_names.include?("#{k.to_s}=") }
26
+
27
+ if unknown_attributes.size > 0
28
+ dates = %w{ created_at deleted_at updated_at }
29
+ self.extend Virtus unless self.respond_to?(:attribute)
30
+ unknown_attributes.
31
+ each do |k,v|
32
+ case
33
+ when dates.include?(k)
34
+ klass = DateTime
35
+ else
36
+ klass = String
37
+ end
38
+ self.attribute k, klass
39
+ end
40
+ end
41
+
42
+ set_attributes_virtus attributes
43
+ end
44
+
45
+
46
+ private
47
+
48
+ def has_many_reader(name, class_proc)
49
+ variable_name = "@_has_many_reader_#{name}"
50
+ instance_variable_get(variable_name) ||
51
+ instance_variable_set(variable_name, (
52
+ klass = class_proc.call
53
+ ResourceCollection.new(
54
+ store,
55
+ class_proc.call,
56
+ (@_has_many || {})[name] || nil
57
+ )
58
+ )
59
+ )
60
+ rescue KeyError
61
+ raise Error, "No reference to children :#{name}"
62
+ end
63
+
64
+ def has_one_reader(name, class_proc)
65
+ klass = class_proc.call
66
+ record = (@_has_one || {}).fetch(name)
67
+ if record.is_a?(Resource)
68
+ record
69
+ else
70
+ @_has_one[name] = klass.new(record)
71
+ end
72
+ rescue KeyError
73
+ raise Error, "No reference to child :#{name}"
74
+ end
75
+
76
+ end
77
+ module Attributes
78
+ class CSV < Virtus::Attribute::Object
79
+ primitive Array
80
+
81
+ def coerce(value)
82
+ require 'csv'
83
+ ::CSV.parse(value).first
84
+ end
85
+ end
86
+ class Boolean < Virtus::Attribute::Object
87
+ primitive Boolean
88
+
89
+ def coerce(value)
90
+ if [1, '1'].include? value
91
+ true
92
+ elsif [0, '0'].include? value
93
+ false
94
+ else
95
+ value == true
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,72 @@
1
+ module Vend
2
+ class ResourceCollection
3
+ attr_accessor :parameters, :store
4
+
5
+ include Vend::Finders
6
+ include Enumerable
7
+
8
+ def initialize(store, klass, records = nil)
9
+ @store = store
10
+ @klass = klass
11
+ @records = records
12
+ end
13
+
14
+ ##
15
+ # Check for a collection method
16
+ # defined on the resource
17
+ def method_missing symbol, *args
18
+ if @klass.collection_methods[symbol]
19
+ instance_exec *args, &@klass.collection_methods[symbol]
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def create(attributes = {})
26
+ build(attributes).tap(&:save)
27
+ end
28
+
29
+ def build(attributes)
30
+ @klass.new(attributes.merge store: @store)
31
+ end
32
+
33
+ def << record
34
+ (@records || []) << record
35
+ end
36
+
37
+ def to_a
38
+ records.to_a
39
+ end
40
+
41
+ def each
42
+ records.each do |attributes|
43
+ yield build(attributes)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def http_response
50
+ @_http_response ||= @store.get(
51
+ path: @klass.collection_path,
52
+ parameters: @parameters,
53
+ status: 200
54
+ )
55
+ end
56
+
57
+ def remote_records
58
+ http_response.data.first.last
59
+ end
60
+
61
+ def records
62
+ # Data comes back with a root element, so get the first response's value
63
+ # { products: [ {}, {}, ..] }.first.last == [ {}, {}, ... ]
64
+ @records ||= remote_records
65
+ end
66
+
67
+ def clear
68
+ @records = nil
69
+ @_http_response = nil
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,4 @@
1
+ module Vend
2
+ class Brand < Resource
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Vend
2
+ class Contact < Resource
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ module Vend
2
+ class Customer < RemoteResource
3
+ attribute :balance, Float
4
+ attribute :contact, Vend::Contact
5
+ attribute :points, Integer
6
+ attribute :year_to_date, Float
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ # TODO: Implement the attribute declaration as a Proc to avoid this
2
+ module Vend
3
+ class Product < RemoteResource
4
+ end
5
+ end
6
+ module Vend
7
+ class Inventory < RemoteResource
8
+ attribute :attributed_cost, Float
9
+ attribute :count, Integer
10
+ attribute :product, Vend::Product
11
+ attribute :outlet, Vend::Outlet
12
+ end
13
+ end
14
+
@@ -0,0 +1,4 @@
1
+ module Vend
2
+ class Outlet < Resource
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Vend
2
+ class PaymentType < RemoteResource
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Vend
2
+ class PriceBookEntry < Resource
3
+ attribute :max_units, Integer
4
+ attribute :min_units, Integer
5
+ attribute :price, Float
6
+ attribute :tax, Float
7
+ attribute :tax_rate, Float
8
+ end
9
+ end
10
+
@@ -0,0 +1,30 @@
1
+ module Vend
2
+ class Product < RemoteResource
3
+ ##
4
+ # Attribute Coercion
5
+ attribute :active, Boolean
6
+ attribute :brand, Vend::Brand
7
+ attribute :display_retail_price_tax_inclusive, Attributes::Boolean
8
+ attribute :inventory, Array[Vend::Inventory]
9
+ attribute :price, Float
10
+ attribute :price_book_entries, Array[Vend::PriceBookEntry]
11
+ attribute :product_type, Vend::ProductType
12
+ attribute :supplier, Vend::Supplier
13
+ attribute :supply_price, Float
14
+ attribute :tags, Attributes::CSV
15
+ attribute :tax, Float
16
+ attribute :tax_rate, Float
17
+
18
+ ##
19
+ # Scopes
20
+ collection_scope :active, default: true
21
+ collection_scope :order_by
22
+ collection_scope :order_direction
23
+
24
+ collection_method :order do |by, direction='ASC'|
25
+ order_by by
26
+ order_direction direction
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module Vend
2
+ class ProductType < RemoteResource
3
+ path "product_types"
4
+
5
+ resource_api_path "1.0/product_type"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Vend
2
+ class Register < RemoteResource
3
+ attribute :invoice_sequence, Integer
4
+ attribute :print_receipt, Attributes::Boolean
5
+ attribute :register_close_time, DateTime
6
+ attribute :register_open_time, DateTime
7
+ attribute :register_open_count_sequence, Integer
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Vend
2
+ class RegisterSalePayment < Resource
3
+ attribute :amount, Float
4
+ attribute :payment_type_id, Integer
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module Vend
2
+ class RegisterSaleProduct < Resource
3
+ attribute :display_retail_price_tax_inclusive, Attributes::Boolean
4
+ attribute :price, Float
5
+ attribute :price_set, Boolean
6
+ attribute :price_total, Float
7
+ attribute :quantity, Integer
8
+ attribute :tax, Float
9
+ attribute :tax_rate, Float
10
+ attribute :tax_total, Float
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module Vend
2
+ class Sale < RemoteResource
3
+ attribute :customer, Vend::Customer
4
+ attribute :invoice_number, Integer
5
+ attribute :register_sale_products, Array[RegisterSaleProduct]
6
+ attribute :register_sale_payments, Array[RegisterSalePayment]
7
+ attribute :total_cost, Float
8
+ attribute :total_price, Float
9
+ attribute :total_tax, Float
10
+ attribute :totals, Hash[Symbol => Float] # Requires updated virtus gem
11
+ #attribute :totals, Array[Float]
12
+ attribute :user, Vend::User
13
+
14
+ path "register_sale"
15
+ end
16
+ end
17
+
@@ -0,0 +1,6 @@
1
+ module Vend
2
+ class Supplier < RemoteResource
3
+ attribute :contact, Vend::Contact
4
+ collection_api_path "supplier"
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module Vend
2
+ class Tax < RemoteResource
3
+ attribute :rate, Float
4
+ attribute :default, Attributes::Boolean
5
+ collection_api_path "taxes"
6
+
7
+ ##
8
+ # Scopes
9
+ collection_method :default do
10
+ select(&:default).first
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ module Vend
2
+ class User < RemoteResource
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module Vend
2
+ class Response
3
+ attr_reader :status, :headers, :data
4
+
5
+ def initialize status, headers, data
6
+ @status, @headers, @data = status, headers, data
7
+ end
8
+ end
9
+ end
data/lib/vend/store.rb ADDED
@@ -0,0 +1,103 @@
1
+ require "addressable/uri"
2
+ require "faraday"
3
+ require "faraday_middleware"
4
+
5
+ module Vend
6
+ class Store
7
+ include HasResources
8
+
9
+ def initialize name, username, password
10
+ @name, @credentials = name, [ username, password ]
11
+ end
12
+
13
+ ##
14
+ # Dispatch a request using HTTP GET
15
+ def get options
16
+ dispatch :get, options
17
+ end
18
+
19
+ ##
20
+ # Dispatch a request using HTTP POST
21
+ def post options
22
+ dispatch :post, options
23
+ end
24
+
25
+ private
26
+
27
+ ##
28
+ # Dispatches an HTTP request
29
+ def dispatch method, options
30
+ path = options[:path]
31
+ parameters = options[:parameters]
32
+ data = options[:data]
33
+
34
+ if parameters
35
+ path = Addressable::URI.parse(path).tap do |uri|
36
+ uri.query_values = (uri.query_values || {}).merge(parameters)
37
+ end.to_s
38
+ end
39
+
40
+ headers = faraday.headers.merge(options[:headers] || {})
41
+ response = faraday.run_request(method, path, data, headers)
42
+
43
+ assert_http_status(response, options[:status])
44
+
45
+ ::Vend::Response.new(
46
+ response.status,
47
+ response.headers,
48
+ response.body
49
+ )
50
+ end
51
+
52
+ ##
53
+ # Raises an error unless the response status matches the required status
54
+ def assert_http_status(response, status)
55
+ case response.status
56
+ when nil, status then return
57
+ when 401
58
+ raise Unauthorized
59
+ when 422
60
+ raise ValidationFailed, "Validation failed: #{response.body.to_s}"
61
+ when 500
62
+ raise ServerError, internal_server_error_message(status, response)
63
+ else
64
+ raise Error, "Expected HTTP #{status}, got HTTP #{response.status}"
65
+ end
66
+ end
67
+
68
+ def internal_server_error_message(expected_status, response)
69
+ body = response.body
70
+ if body["type"] && body["message"] && body["backtrace"]
71
+ "Expected HTTP %d, got HTTP %d with error:\n%s\n%s\n\n%s" % [
72
+ expected_status,
73
+ response.status,
74
+ response.body["type"],
75
+ response.body["message"],
76
+ response.body["backtrace"].join("\n"),
77
+ ]
78
+ else
79
+ "Expected HTTP %d, got %d" % [
80
+ expected_status,
81
+ response.status,
82
+ ]
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Base URL for the Vend AP
88
+ def url
89
+ "%s://%s.%s/api" % [ Vend.scheme, @name, Vend.domain ]
90
+ end
91
+
92
+ ##
93
+ # Memoized Faraday client with auth credentials
94
+ def faraday
95
+ @_faraday ||= Faraday.new(url) do |conn|
96
+ conn.request :json
97
+ conn.response :json
98
+ conn.basic_auth *@credentials
99
+ conn.adapter Faraday.default_adapter
100
+ end
101
+ end
102
+ end
103
+ end