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
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
|