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
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
|
7
|
+
Dir.glob('lib/tasks/*.rake').each { |r| import r }
|
8
|
+
|
9
|
+
task :default => [:spec]
|
10
|
+
|
11
|
+
desc "Run RSpec tests"
|
12
|
+
RSpec::Core::RakeTask.new(:spec)
|
13
|
+
|
14
|
+
Rake::RDocTask.new(:doc) do |rd|
|
15
|
+
rd.main = 'README.rdoc'
|
16
|
+
rd.rdoc_dir = 'doc'
|
17
|
+
rd.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
|
18
|
+
end
|
data/example.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require './lib/vend'
|
2
|
+
require 'log4r'
|
3
|
+
|
4
|
+
STORE = ARGV[0]
|
5
|
+
USERNAME = ARGV[1]
|
6
|
+
PASSWORD = ARGV[2]
|
7
|
+
|
8
|
+
unless STORE and USERNAME and PASSWORD
|
9
|
+
$stderr.puts "Usage: example.rb store username password"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
client = Vend::Client.new(STORE, USERNAME, PASSWORD)
|
14
|
+
|
15
|
+
logger = Log4r::Logger.new 'vend'
|
16
|
+
logger.outputters = Log4r::Outputter.stdout
|
17
|
+
client.http_client.logger = client.logger = logger
|
18
|
+
|
19
|
+
# puts client.request('products', :method => :put, :body => '{"foo":"bar"}')
|
20
|
+
|
21
|
+
# puts "###### Products ######"
|
22
|
+
# client.Product.all.each do |product|
|
23
|
+
# puts product.name
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# puts "###### Customers ######"
|
27
|
+
# client.Customer.all.each do |customer|
|
28
|
+
# puts "#{customer.name} (#{customer.customer_code})"
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# puts "###### Creating a Customer ######"
|
32
|
+
# response = client.request('customers', :method => :post, :body => '{"customer_code":"foo"}')
|
33
|
+
# puts response
|
34
|
+
#
|
35
|
+
# puts "###### Finding a Customer by name ######"
|
36
|
+
# response = client.Customer.find_by_name('Foo')
|
37
|
+
# puts response.inspect
|
38
|
+
#
|
39
|
+
# puts "###### Finding a Customer by email ######"
|
40
|
+
# response = client.Customer.find_by_email('foo@example.com')
|
41
|
+
# puts response.inspect
|
42
|
+
#
|
43
|
+
# puts "###### Outlets ######"
|
44
|
+
# client.Outlet.all.each do |outlet|
|
45
|
+
# puts outlet.name
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# puts "###### Payment Types ######"
|
49
|
+
# client.PaymentType.all.each do |payment_type|
|
50
|
+
# puts payment_type.name
|
51
|
+
# end
|
data/lib/vend.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
require 'vend/exception'
|
5
|
+
require 'vend/null_logger'
|
6
|
+
require 'vend/logable'
|
7
|
+
require 'vend/pagination_info'
|
8
|
+
require 'vend/scope'
|
9
|
+
require 'vend/resource_collection'
|
10
|
+
require 'vend/base_factory'
|
11
|
+
require 'vend/base'
|
12
|
+
|
13
|
+
require 'vend/resource/outlet'
|
14
|
+
require 'vend/resource/product'
|
15
|
+
require 'vend/resource/customer'
|
16
|
+
require 'vend/resource/payment_type'
|
17
|
+
require 'vend/resource/register'
|
18
|
+
require 'vend/resource/register_sale'
|
19
|
+
require 'vend/resource/register_sale_product'
|
20
|
+
require 'vend/resource/tax'
|
21
|
+
require 'vend/resource/user'
|
22
|
+
|
23
|
+
require 'vend/http_client'
|
24
|
+
require 'vend/client'
|
data/lib/vend/base.rb
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
module Vend
|
2
|
+
|
3
|
+
# This Base class provides the basic mapping between Vend::Resource subclasses
|
4
|
+
# and the HTTP endpoints in the Vend API.
|
5
|
+
#
|
6
|
+
# Not all CRUD actions are available for every resource, and at current there
|
7
|
+
# is no PUT endpoint and hence no update action
|
8
|
+
class Base
|
9
|
+
|
10
|
+
# Reference to the Vend::Client client object providing the HTTP interface to the
|
11
|
+
# API
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
# Hash of attributes for this instance. Represents the data returned from
|
15
|
+
# the Vend API
|
16
|
+
attr_accessor :attrs
|
17
|
+
|
18
|
+
def initialize(client, options = {}) #:nodoc:
|
19
|
+
@client = client
|
20
|
+
@attrs = options[:attrs] || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.cast_attribute(attribute_name, type)
|
24
|
+
attribute_casts[attribute_name] = type
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.attribute_casts
|
28
|
+
@attribute_casts ||= {}
|
29
|
+
end
|
30
|
+
# Returns the endpoint name for the resource, used in API urls when making
|
31
|
+
# requests.
|
32
|
+
def self.endpoint_name
|
33
|
+
self.name.split('::').last.underscore
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the endpoint name for a collection of this resource
|
37
|
+
def self.collection_name
|
38
|
+
endpoint_name.pluralize
|
39
|
+
end
|
40
|
+
|
41
|
+
def singular_name
|
42
|
+
self.class.singular_name(id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.singular_name(id)
|
46
|
+
"#{collection_name}/#{id}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a collection containing all of the specified resource objects.
|
50
|
+
# Will paginate.
|
51
|
+
def self.all(client)
|
52
|
+
initialize_collection(client, collection_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the list of URL parameter scopes that are available for this
|
56
|
+
# resource. For example, if the resource accepted URLs like:
|
57
|
+
#
|
58
|
+
# /api/resources/since/YYYY-MM-DD HH:MM:SS/outlet_id/abc-1234-def
|
59
|
+
#
|
60
|
+
# this method would return [:since, :outlet_id]
|
61
|
+
def self.available_scopes
|
62
|
+
@available_scopes ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.accepts_scope?(scope_name)
|
66
|
+
available_scopes.include?(scope_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Creates a class method that allows access to a filtered collection of
|
70
|
+
# resources on the API. For example:
|
71
|
+
#
|
72
|
+
# class MyResource << Vend::Base
|
73
|
+
# url_scope :since
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Will create a class method:
|
77
|
+
#
|
78
|
+
# client.MyResource.since(argument)
|
79
|
+
#
|
80
|
+
# That will call the following URL on the Vend API:
|
81
|
+
#
|
82
|
+
# /api/my_resources/since/:argument
|
83
|
+
#
|
84
|
+
# And return the corresponding collection of resources
|
85
|
+
def self.url_scope(method_name)
|
86
|
+
(class << self ; self ; end).instance_eval do
|
87
|
+
define_method(method_name) do |client, arg|
|
88
|
+
initialize_collection(client, collection_name).scope(method_name, arg)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
available_scopes << method_name
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.findable_by(field, options = {})
|
95
|
+
|
96
|
+
(class << self ; self ; end).instance_eval do
|
97
|
+
define_method("find_by_#{field}") do |client, *args|
|
98
|
+
search(client, options[:as] || field, *args)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# Sends a search request to the API and initializes a collection of Resources
|
103
|
+
# from the response.
|
104
|
+
# This method is only used internally by find_by_field methods.
|
105
|
+
def self.search(client, field, query)
|
106
|
+
initialize_collection(
|
107
|
+
client, collection_name, :url_params => { field.to_sym => query }
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Builds a new instance of the described resource using the specified
|
112
|
+
# attributes.
|
113
|
+
def self.build(client, attrs)
|
114
|
+
self.new(client, :attrs => attrs)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Builds a collection of instances from a JSON response
|
118
|
+
def self.build_from_json(client, json)
|
119
|
+
json[collection_name].map do |attrs|
|
120
|
+
self.build(client, attrs)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Initializes a single object from a JSON response.
|
125
|
+
# Assumes the response is a JSON array with a single item.
|
126
|
+
def self.initialize_singular(client, json)
|
127
|
+
self.build(client, json[collection_name].first)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Will initialize a collection of Resources from the APIs JSON Response.
|
131
|
+
def self.initialize_collection(client, endpoint, args = {})
|
132
|
+
ResourceCollection.new(client, self, endpoint, args)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Attempts to pull a singular object from Vend through the singular GET
|
137
|
+
# endpoint.
|
138
|
+
def self.find(client, id)
|
139
|
+
response = client.request(singular_name(id))
|
140
|
+
initialize_singular(client, response)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Whether or not this resource can be paginated, false by default.
|
144
|
+
# Override this method in specific classes to enable pagination for that
|
145
|
+
# resource.
|
146
|
+
def self.paginates?
|
147
|
+
false
|
148
|
+
end
|
149
|
+
|
150
|
+
# Overrides respond_to? to query the attrs hash for the key before
|
151
|
+
# proxying it to the object
|
152
|
+
def respond_to?(method_name)
|
153
|
+
if attrs.keys.include? method_name.to_s
|
154
|
+
true
|
155
|
+
else
|
156
|
+
super(method_name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Overrides method_missing to query the attrs hash for the value stored
|
161
|
+
# at the specified key before proxying it to the object
|
162
|
+
def method_missing(method_name, *args, &block)
|
163
|
+
if attrs.keys.include? method_name.to_s
|
164
|
+
attribute_value_for(method_name)
|
165
|
+
else
|
166
|
+
super(method_name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Attempts to delete the resource. If an exception is thrown by delete!,
|
171
|
+
# it is caught and false is returned (typically when the resource is not
|
172
|
+
# a type which can be deleted).
|
173
|
+
def delete
|
174
|
+
delete!
|
175
|
+
rescue Vend::Resource::IllegalAction
|
176
|
+
false
|
177
|
+
end
|
178
|
+
|
179
|
+
# Attempts to delete there resource. Will throw an exception when the attempt
|
180
|
+
# fails, otherwise will return true.
|
181
|
+
def delete!
|
182
|
+
raise(Vend::Resource::IllegalAction,
|
183
|
+
"#{self.class.name} has no unique ID") unless attrs['id']
|
184
|
+
client.request(singular_name, :method => :delete)
|
185
|
+
true
|
186
|
+
end
|
187
|
+
|
188
|
+
protected
|
189
|
+
def attribute_value_for(attribute_name)
|
190
|
+
if self.class.attribute_casts.has_key? attribute_name
|
191
|
+
case self.class.attribute_casts[attribute_name].to_s
|
192
|
+
when "Float"
|
193
|
+
return attrs[attribute_name.to_s].to_f
|
194
|
+
end
|
195
|
+
else
|
196
|
+
attrs[attribute_name.to_s]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Vend
|
2
|
+
|
3
|
+
class BaseFactory
|
4
|
+
|
5
|
+
attr_reader :client, :target_class
|
6
|
+
|
7
|
+
def initialize(client, target_class)
|
8
|
+
@target_class = target_class
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
# The main point of this factory class is to proxy methods to the target
|
13
|
+
# class and prepend client to the argument list.
|
14
|
+
def method_missing(method_name, *args, &block)
|
15
|
+
args.unshift(client)
|
16
|
+
target_class.send(method_name, *args, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to?(method_name)
|
20
|
+
return true if target_class.respond_to?(method_name)
|
21
|
+
super(method_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
## Generates find_by_field methods which call a search on the target class
|
25
|
+
#def self.findable_by(field, options = {})
|
26
|
+
# url_param = options[:as] || field
|
27
|
+
# define_method "find_by_#{field.to_s}" do |*args|
|
28
|
+
# target_class.send(:search, @client, url_param, *args)
|
29
|
+
# end
|
30
|
+
#end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/vend/client.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Vend #:nodoc:
|
4
|
+
|
5
|
+
# Main access point which allows resources within the Vend gem to
|
6
|
+
# make HTTP requests to the Vend API.
|
7
|
+
#
|
8
|
+
# Client must be initialized with:
|
9
|
+
# * a store url (e.g. the storeurl portion of http://storeurl.vendhq.com/),
|
10
|
+
# * a valid username and password
|
11
|
+
#
|
12
|
+
class Client
|
13
|
+
|
14
|
+
DEFAULT_OPTIONS = {}
|
15
|
+
|
16
|
+
extend Forwardable
|
17
|
+
def_delegator :http_client, :request
|
18
|
+
|
19
|
+
include Logable
|
20
|
+
|
21
|
+
# The store url for this client
|
22
|
+
attr_accessor :store, :username, :password
|
23
|
+
attr_reader :options
|
24
|
+
|
25
|
+
def initialize(store, username, password, options = {}) #:nodoc:
|
26
|
+
@store = store
|
27
|
+
@username = username
|
28
|
+
@password = password
|
29
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def Product #:nodoc:
|
33
|
+
Vend::BaseFactory.new(self, Resource::Product)
|
34
|
+
end
|
35
|
+
|
36
|
+
def Outlet #:nodoc:
|
37
|
+
Vend::BaseFactory.new(self, Resource::Outlet)
|
38
|
+
end
|
39
|
+
|
40
|
+
def Customer #:nodoc:
|
41
|
+
Vend::BaseFactory.new(self, Resource::Customer)
|
42
|
+
end
|
43
|
+
|
44
|
+
def PaymentType #:nodoc:
|
45
|
+
Vend::BaseFactory.new(self, Resource::PaymentType)
|
46
|
+
end
|
47
|
+
|
48
|
+
def Register #:nodoc:
|
49
|
+
Vend::BaseFactory.new(self, Resource::Register)
|
50
|
+
end
|
51
|
+
|
52
|
+
def RegisterSale #:nodoc:
|
53
|
+
Vend::BaseFactory.new(self, Resource::RegisterSale)
|
54
|
+
end
|
55
|
+
|
56
|
+
def Tax #:nodoc:
|
57
|
+
Vend::BaseFactory.new(self, Resource::Tax)
|
58
|
+
end
|
59
|
+
|
60
|
+
def User #:nodoc:
|
61
|
+
Vend::BaseFactory.new(self, Resource::User)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the base API url for the client.
|
65
|
+
# E.g. for the store 'foo', it returns https://foo.vendhq.com/api/
|
66
|
+
def base_url
|
67
|
+
"https://#{@store}.vendhq.com/api/"
|
68
|
+
end
|
69
|
+
|
70
|
+
def http_client
|
71
|
+
@http_client ||= HttpClient.new(http_client_options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def http_client_options
|
75
|
+
options.merge(
|
76
|
+
:base_url => base_url, :username => username, :password => password
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Vend
|
4
|
+
module Resource
|
5
|
+
# Raised when the action being performed is not possible or applicable to
|
6
|
+
# the resource under the current conditions
|
7
|
+
class IllegalAction < StandardError; end
|
8
|
+
|
9
|
+
# Raised when the response from the API is not able to be interpreted
|
10
|
+
# in a useful way by the Gem (E.g. Parsing errors for non-JSON responses)
|
11
|
+
class InvalidResponse < StandardError; end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when the specified endpoint does not exist in the API. (Indicated
|
15
|
+
# by the response being a redirect to the signin page)
|
16
|
+
class InvalidRequest < StandardError; end
|
17
|
+
|
18
|
+
# This exception is thrown when a 401 Unauthorized response is received
|
19
|
+
# from the Vend API
|
20
|
+
class Unauthorized < StandardError; end
|
21
|
+
|
22
|
+
# Nonspecific HTTP error which is usually thrown when a non 2xx response
|
23
|
+
# is received.
|
24
|
+
class HTTPError < StandardError
|
25
|
+
extend Forwardable
|
26
|
+
|
27
|
+
delegate [:message, :code] => :response
|
28
|
+
|
29
|
+
attr_reader :response
|
30
|
+
|
31
|
+
def initialize(response)
|
32
|
+
@response = response
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|