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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +155 -0
- data/Rakefile +14 -0
- data/lib/vend.rb +46 -0
- data/lib/vend/errors.rb +12 -0
- data/lib/vend/has_resources.rb +34 -0
- data/lib/vend/modules/declarative_setters.rb +74 -0
- data/lib/vend/modules/finders.rb +5 -0
- data/lib/vend/modules/paths.rb +37 -0
- data/lib/vend/remote_resource.rb +46 -0
- data/lib/vend/resource.rb +100 -0
- data/lib/vend/resource_collection.rb +72 -0
- data/lib/vend/resources/brand.rb +4 -0
- data/lib/vend/resources/contact.rb +4 -0
- data/lib/vend/resources/customer.rb +8 -0
- data/lib/vend/resources/inventory.rb +14 -0
- data/lib/vend/resources/outlet.rb +4 -0
- data/lib/vend/resources/payment_type.rb +4 -0
- data/lib/vend/resources/price_book_entry.rb +10 -0
- data/lib/vend/resources/product.rb +30 -0
- data/lib/vend/resources/product_type.rb +7 -0
- data/lib/vend/resources/register.rb +9 -0
- data/lib/vend/resources/register_sale_payment.rb +6 -0
- data/lib/vend/resources/register_sale_product.rb +12 -0
- data/lib/vend/resources/sale.rb +17 -0
- data/lib/vend/resources/supplier.rb +6 -0
- data/lib/vend/resources/tax.rb +13 -0
- data/lib/vend/resources/user.rb +4 -0
- data/lib/vend/response.rb +9 -0
- data/lib/vend/store.rb +103 -0
- data/lib/vend/version.rb +3 -0
- data/lib/vending-machine.rb +1 -0
- data/spec/resource_spec.rb +22 -0
- data/spec/resource_urls_spec.rb +59 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/store_spec.rb +21 -0
- data/spec/support/url_assertions.rb +23 -0
- data/spec/support/webmock.rb +53 -0
- data/vending-machine.gemspec +46 -0
- 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,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,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,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,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
|
+
|
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
|