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.
Files changed (65) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +4 -0
  3. data/Gemfile +6 -0
  4. data/README.rdoc +8 -0
  5. data/Rakefile +18 -0
  6. data/example.rb +51 -0
  7. data/lib/vend.rb +24 -0
  8. data/lib/vend/base.rb +200 -0
  9. data/lib/vend/base_factory.rb +34 -0
  10. data/lib/vend/client.rb +82 -0
  11. data/lib/vend/exception.rb +35 -0
  12. data/lib/vend/http_client.rb +119 -0
  13. data/lib/vend/logable.rb +9 -0
  14. data/lib/vend/null_logger.rb +9 -0
  15. data/lib/vend/pagination_info.rb +30 -0
  16. data/lib/vend/resource/customer.rb +11 -0
  17. data/lib/vend/resource/outlet.rb +7 -0
  18. data/lib/vend/resource/payment_type.rb +7 -0
  19. data/lib/vend/resource/product.rb +12 -0
  20. data/lib/vend/resource/register.rb +7 -0
  21. data/lib/vend/resource/register_sale.rb +19 -0
  22. data/lib/vend/resource/register_sale_product.rb +21 -0
  23. data/lib/vend/resource/tax.rb +7 -0
  24. data/lib/vend/resource/user.rb +7 -0
  25. data/lib/vend/resource_collection.rb +132 -0
  26. data/lib/vend/scope.rb +27 -0
  27. data/lib/vend/version.rb +3 -0
  28. data/spec/integration/customer_spec.rb +33 -0
  29. data/spec/integration/outlet_spec.rb +15 -0
  30. data/spec/integration/payment_types_spec.rb +15 -0
  31. data/spec/integration/product_spec.rb +76 -0
  32. data/spec/integration/register_sale_spec.rb +35 -0
  33. data/spec/integration/register_spec.rb +16 -0
  34. data/spec/integration/tax_spec.rb +16 -0
  35. data/spec/integration/user_spec.rb +16 -0
  36. data/spec/mock_responses/customers.find_by_email.json +44 -0
  37. data/spec/mock_responses/customers.json +20 -0
  38. data/spec/mock_responses/outlets.json +14 -0
  39. data/spec/mock_responses/payment_types.json +7 -0
  40. data/spec/mock_responses/products.active.since.json +184 -0
  41. data/spec/mock_responses/products.json +115 -0
  42. data/spec/mock_responses/products/page/1.json +130 -0
  43. data/spec/mock_responses/products/page/2.json +130 -0
  44. data/spec/mock_responses/register_sales.find_by_state.json +324 -0
  45. data/spec/mock_responses/register_sales.json +158 -0
  46. data/spec/mock_responses/register_sales/2e658bce-9627-bc27-d77d-6c9ba2e8216e.json +158 -0
  47. data/spec/mock_responses/registers.json +32 -0
  48. data/spec/mock_responses/taxes.json +7 -0
  49. data/spec/mock_responses/users.json +17 -0
  50. data/spec/spec_helper.rb +16 -0
  51. data/spec/support/matchers/have_attributes.rb +11 -0
  52. data/spec/support/shared_examples/integration.rb +79 -0
  53. data/spec/support/shared_examples/logger.rb +25 -0
  54. data/spec/vend/base_factory_spec.rb +48 -0
  55. data/spec/vend/base_spec.rb +348 -0
  56. data/spec/vend/client_spec.rb +93 -0
  57. data/spec/vend/http_client_spec.rb +129 -0
  58. data/spec/vend/null_logger_spec.rb +5 -0
  59. data/spec/vend/pagination_info_spec.rb +48 -0
  60. data/spec/vend/resource/register_sale_product_spec.rb +27 -0
  61. data/spec/vend/resource/register_sale_spec.rb +24 -0
  62. data/spec/vend/resource_collection_spec.rb +312 -0
  63. data/spec/vend/scope_spec.rb +41 -0
  64. data/vend.gemspec +26 -0
  65. metadata +179 -0
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ doc/*
6
+ .rvmrc
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rake'
4
+
5
+ # Specify your gem's dependencies in vend.gemspec
6
+ gemspec
@@ -0,0 +1,8 @@
1
+ = Vend API Gem
2
+
3
+ This gem provides access to Vend's REST API.
4
+
5
+ == Vend API Documentation
6
+
7
+ At the time of writing, this is located at
8
+ https://docs.google.com/document/pub?id=13JiV6771UcrkmawFRxzvVl3tf5PGSTBH56RYy0-5cps
@@ -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
@@ -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
@@ -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'
@@ -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
@@ -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