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