vending-machine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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