vend 0.0.4
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 +6 -0
- data/.travis.yml +4 -0
- data/Gemfile +6 -0
- data/README.rdoc +8 -0
- data/Rakefile +18 -0
- data/example.rb +51 -0
- data/lib/vend.rb +24 -0
- data/lib/vend/base.rb +200 -0
- data/lib/vend/base_factory.rb +34 -0
- data/lib/vend/client.rb +82 -0
- data/lib/vend/exception.rb +35 -0
- data/lib/vend/http_client.rb +119 -0
- data/lib/vend/logable.rb +9 -0
- data/lib/vend/null_logger.rb +9 -0
- data/lib/vend/pagination_info.rb +30 -0
- data/lib/vend/resource/customer.rb +11 -0
- data/lib/vend/resource/outlet.rb +7 -0
- data/lib/vend/resource/payment_type.rb +7 -0
- data/lib/vend/resource/product.rb +12 -0
- data/lib/vend/resource/register.rb +7 -0
- data/lib/vend/resource/register_sale.rb +19 -0
- data/lib/vend/resource/register_sale_product.rb +21 -0
- data/lib/vend/resource/tax.rb +7 -0
- data/lib/vend/resource/user.rb +7 -0
- data/lib/vend/resource_collection.rb +132 -0
- data/lib/vend/scope.rb +27 -0
- data/lib/vend/version.rb +3 -0
- data/spec/integration/customer_spec.rb +33 -0
- data/spec/integration/outlet_spec.rb +15 -0
- data/spec/integration/payment_types_spec.rb +15 -0
- data/spec/integration/product_spec.rb +76 -0
- data/spec/integration/register_sale_spec.rb +35 -0
- data/spec/integration/register_spec.rb +16 -0
- data/spec/integration/tax_spec.rb +16 -0
- data/spec/integration/user_spec.rb +16 -0
- data/spec/mock_responses/customers.find_by_email.json +44 -0
- data/spec/mock_responses/customers.json +20 -0
- data/spec/mock_responses/outlets.json +14 -0
- data/spec/mock_responses/payment_types.json +7 -0
- data/spec/mock_responses/products.active.since.json +184 -0
- data/spec/mock_responses/products.json +115 -0
- data/spec/mock_responses/products/page/1.json +130 -0
- data/spec/mock_responses/products/page/2.json +130 -0
- data/spec/mock_responses/register_sales.find_by_state.json +324 -0
- data/spec/mock_responses/register_sales.json +158 -0
- data/spec/mock_responses/register_sales/2e658bce-9627-bc27-d77d-6c9ba2e8216e.json +158 -0
- data/spec/mock_responses/registers.json +32 -0
- data/spec/mock_responses/taxes.json +7 -0
- data/spec/mock_responses/users.json +17 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/matchers/have_attributes.rb +11 -0
- data/spec/support/shared_examples/integration.rb +79 -0
- data/spec/support/shared_examples/logger.rb +25 -0
- data/spec/vend/base_factory_spec.rb +48 -0
- data/spec/vend/base_spec.rb +348 -0
- data/spec/vend/client_spec.rb +93 -0
- data/spec/vend/http_client_spec.rb +129 -0
- data/spec/vend/null_logger_spec.rb +5 -0
- data/spec/vend/pagination_info_spec.rb +48 -0
- data/spec/vend/resource/register_sale_product_spec.rb +27 -0
- data/spec/vend/resource/register_sale_spec.rb +24 -0
- data/spec/vend/resource_collection_spec.rb +312 -0
- data/spec/vend/scope_spec.rb +41 -0
- data/vend.gemspec +26 -0
- metadata +179 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'cgi'
|
4
|
+
module Vend
|
5
|
+
class HttpClient
|
6
|
+
|
7
|
+
UNAUTHORIZED_MESSAGE = "Client not authorized. Check your store URL and credentials are correct and try again."
|
8
|
+
|
9
|
+
include Logable
|
10
|
+
|
11
|
+
attr_accessor :base_url, :verify_ssl, :username, :password
|
12
|
+
alias :verify_ssl? :verify_ssl
|
13
|
+
|
14
|
+
def initialize(options = {})
|
15
|
+
@base_url = options[:base_url]
|
16
|
+
@username = options[:username]
|
17
|
+
@password = options[:password]
|
18
|
+
@verify_ssl = if options.has_key?(:verify_ssl)
|
19
|
+
options[:verify_ssl]
|
20
|
+
else
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# sets up a http connection
|
26
|
+
def get_http_connection(host, port)
|
27
|
+
http = Net::HTTP.new(host, port)
|
28
|
+
http.use_ssl = true
|
29
|
+
http.verify_mode = verify_mode
|
30
|
+
http
|
31
|
+
end
|
32
|
+
|
33
|
+
# Makes a request to the specified path within the Vend API
|
34
|
+
# E.g. request('foo') will make a GET request to
|
35
|
+
# http://storeurl.vendhq.com/api/foo
|
36
|
+
#
|
37
|
+
# The HTTP method may be specified, by default it is GET.
|
38
|
+
#
|
39
|
+
# An optional hash of arguments may be specified. Possible options include:
|
40
|
+
# :method - The HTTP method
|
41
|
+
# E.g. request('foo', :method => :post) will perform a POST request for
|
42
|
+
# http://storeurl.vendhq.com/api/foo
|
43
|
+
#
|
44
|
+
# :url_params - The URL parameters for GET requests.
|
45
|
+
# E.g. request('foo', :url_params => {:bar => "baz"}) will request
|
46
|
+
# http://storeurl.vendhq.com/api/foo?bar=baz
|
47
|
+
#
|
48
|
+
# :id - The ID required for performing actions on specific resources
|
49
|
+
# (e.g. delete).
|
50
|
+
# E.g. request('foos', :method => :delete, :id => 1) will request
|
51
|
+
# DELETE http://storeurl.vendhq.com/api/foos/1
|
52
|
+
#
|
53
|
+
# :body - The request body
|
54
|
+
# E.g. For submitting a POST to http://storeurl.vendhq.com/api/foo
|
55
|
+
# with the JSON data {"baz":"baloo"} we would call
|
56
|
+
# request('foo', :method => :post, :body => '{\"baz\":\"baloo\"}'
|
57
|
+
#
|
58
|
+
def request(path, options = {})
|
59
|
+
options = {:method => :get}.merge options
|
60
|
+
url = URI.parse(base_url + path)
|
61
|
+
http = get_http_connection(url.host, url.port)
|
62
|
+
|
63
|
+
# FIXME extract method
|
64
|
+
method = ("Net::HTTP::" + options[:method].to_s.classify).constantize
|
65
|
+
request = method.new(url.path + url_params_for(options[:url_params]))
|
66
|
+
request.basic_auth username, password
|
67
|
+
|
68
|
+
request.body = options[:body] if options[:body]
|
69
|
+
logger.debug url
|
70
|
+
response = http.request(request)
|
71
|
+
raise Unauthorized.new(UNAUTHORIZED_MESSAGE) if response.kind_of?(Net::HTTPUnauthorized)
|
72
|
+
raise HTTPError.new(response) unless response.kind_of?(Net::HTTPSuccess)
|
73
|
+
logger.debug response
|
74
|
+
JSON.parse response.body unless response.body.nil? or response.body.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the SSL verification mode, based upon the value of verify_ssl?
|
78
|
+
def verify_mode
|
79
|
+
if verify_ssl?
|
80
|
+
OpenSSL::SSL::VERIFY_PEER
|
81
|
+
else
|
82
|
+
OpenSSL::SSL::VERIFY_NONE
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Internal method to parse URL parameters.
|
87
|
+
# Returns an empty string from a nil argument
|
88
|
+
#
|
89
|
+
# E.g. url_params_for({:field => "value"}) will return ?field=value
|
90
|
+
# url_params_for({:field => ["value1","value2"]}) will return ?field[]=value1&field[]=value2
|
91
|
+
protected
|
92
|
+
def url_params_for(options)
|
93
|
+
ary = Array.new
|
94
|
+
if !options.nil?
|
95
|
+
options.each do |option,value|
|
96
|
+
if value.class == Array
|
97
|
+
ary << value.collect { |key| "#{option}%5B%5D=#{CGI::escape(key.to_s)}" }.join('&')
|
98
|
+
else
|
99
|
+
ary << "#{option}=#{CGI::escape(value.to_s)}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
'?'.concat(ary.join('&'))
|
103
|
+
else
|
104
|
+
''
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
# Modifies path with the provided options
|
110
|
+
def expand_path_with_options(path, options)
|
111
|
+
# FIXME - Remove from here
|
112
|
+
if options[:id]
|
113
|
+
path += "/#{options[:id]}"
|
114
|
+
end
|
115
|
+
return path
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
data/lib/vend/logable.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Vend
|
2
|
+
class PaginationInfo
|
3
|
+
attr_reader :response
|
4
|
+
|
5
|
+
def initialize(response)
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def pages
|
10
|
+
pagination['pages']
|
11
|
+
end
|
12
|
+
|
13
|
+
def page
|
14
|
+
pagination['page']
|
15
|
+
end
|
16
|
+
|
17
|
+
def paged?
|
18
|
+
!response['pagination'].nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def last_page?
|
22
|
+
pages == page
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
def pagination
|
27
|
+
@pagination ||= (response['pagination'] || {"pages" => 1, "page" => 1})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module Vend
|
3
|
+
module Resource
|
4
|
+
|
5
|
+
class RegisterSale < Vend::Base
|
6
|
+
url_scope :since
|
7
|
+
url_scope :outlet_id
|
8
|
+
url_scope :tag
|
9
|
+
findable_by :state, :as => :status
|
10
|
+
|
11
|
+
def register_sale_products
|
12
|
+
attrs["register_sale_products"].collect do |sale_product_attrs|
|
13
|
+
RegisterSaleProduct.new(sale_product_attrs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Vend
|
2
|
+
module Resource
|
3
|
+
# Note this class does not have a corresponding endpoint in the vend api
|
4
|
+
# It is used to provide a consistent interface to clients using this gem
|
5
|
+
class RegisterSaleProduct
|
6
|
+
attr_reader :attrs
|
7
|
+
|
8
|
+
def initialize(attrs)
|
9
|
+
@attrs = attrs
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method_name, *args, &block)
|
13
|
+
if attrs.keys.include? method_name.to_s
|
14
|
+
attrs[method_name.to_s]
|
15
|
+
else
|
16
|
+
super(method_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module Vend
|
3
|
+
# This is an enumerable class which allows iteration over a collection of
|
4
|
+
# resources. This class will automatically fetch paginated results if the
|
5
|
+
# target_class supports it.
|
6
|
+
class ResourceCollection
|
7
|
+
|
8
|
+
class PageOutOfBoundsError < StandardError ; end
|
9
|
+
class AlreadyScopedError < StandardError ; end
|
10
|
+
class ScopeNotFoundError < StandardError ; end
|
11
|
+
|
12
|
+
include Enumerable
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
attr_reader :client, :target_class, :endpoint, :request_args
|
16
|
+
|
17
|
+
def_delegators :pagination, :pages, :page
|
18
|
+
def_delegators :target_class, :accepts_scope?
|
19
|
+
|
20
|
+
def initialize(client, target_class, endpoint, request_args = {})
|
21
|
+
@client = client
|
22
|
+
@target_class = target_class
|
23
|
+
@endpoint = endpoint
|
24
|
+
@request_args = request_args
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
# If each has previously been invoked on this collection, the response
|
29
|
+
# member will already be set, causing last_page? to immeadiatly return
|
30
|
+
# true. So reset it here.
|
31
|
+
self.response = nil
|
32
|
+
|
33
|
+
until last_page?
|
34
|
+
target_class.build_from_json(client, get_next_page).map do |resource|
|
35
|
+
yield resource
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def pagination
|
42
|
+
if response.instance_of? Hash
|
43
|
+
PaginationInfo.new(response)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def last_page?
|
48
|
+
pagination && pagination.last_page?
|
49
|
+
end
|
50
|
+
|
51
|
+
def paged?
|
52
|
+
pagination && pagination.paged?
|
53
|
+
end
|
54
|
+
|
55
|
+
def scopes
|
56
|
+
@scopes ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Adds a new URL scope parameter to this ResourceCollection. Calling
|
60
|
+
# scope(:foo, 'bar') will effectively append '/foo/bar' to the resource
|
61
|
+
# URL.
|
62
|
+
def scope(name, value)
|
63
|
+
raise AlreadyScopedError if has_scope?(name)
|
64
|
+
scopes << Scope.new(name, value)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def has_scope?(name)
|
69
|
+
scopes.any? {|s| s.name == name }
|
70
|
+
end
|
71
|
+
|
72
|
+
def method_missing(method_name, *args, &block)
|
73
|
+
if accepts_scope?(method_name)
|
74
|
+
scope(method_name, *args)
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def respond_to?(method_name)
|
81
|
+
return true if accepts_scope?(method_name)
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
85
|
+
def increment_page
|
86
|
+
if paged?
|
87
|
+
page_scope = get_or_create_page_scope
|
88
|
+
page_scope.value = page_scope.value + 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_scope(name)
|
93
|
+
|
94
|
+
result = scopes.find { |scope| scope.name == name }
|
95
|
+
if result.nil?
|
96
|
+
raise ScopeNotFoundError.new(
|
97
|
+
"Scope: #{name} was not found in #{scopes}."
|
98
|
+
)
|
99
|
+
end
|
100
|
+
return result
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_or_create_page_scope
|
104
|
+
unless has_scope? :page
|
105
|
+
scope(:page, page)
|
106
|
+
end
|
107
|
+
get_scope :page
|
108
|
+
end
|
109
|
+
|
110
|
+
def url
|
111
|
+
increment_page
|
112
|
+
endpoint_with_scopes
|
113
|
+
end
|
114
|
+
|
115
|
+
def endpoint_with_scopes
|
116
|
+
endpoint + scopes.join
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
attr_accessor :response
|
121
|
+
|
122
|
+
protected
|
123
|
+
def get_next_page
|
124
|
+
if last_page?
|
125
|
+
raise PageOutOfBoundsError.new(
|
126
|
+
"get_next_page called when already on last page"
|
127
|
+
)
|
128
|
+
end
|
129
|
+
self.response = client.request(url, request_args)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/vend/scope.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Vend
|
2
|
+
class Scope
|
3
|
+
|
4
|
+
attr_reader :name
|
5
|
+
attr_accessor :value
|
6
|
+
|
7
|
+
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
8
|
+
|
9
|
+
def initialize(name, value)
|
10
|
+
@name = name
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def escaped_value
|
15
|
+
if value.instance_of? Time
|
16
|
+
result = value.strftime(DATETIME_FORMAT)
|
17
|
+
else
|
18
|
+
result = value.to_s
|
19
|
+
end
|
20
|
+
CGI::escape(result)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"/%s/%s" % [name, escaped_value]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|