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,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
@@ -0,0 +1,9 @@
1
+ module Vend
2
+ module Logable
3
+ attr_accessor :logger
4
+
5
+ def logger
6
+ @logger ||= NullLogger.new
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Vend
2
+ class NullLogger
3
+ def debug(*args) ; end
4
+ def info(*args) ; end
5
+ def warn(*args) ; end
6
+ def error(*args) ; end
7
+ def fatal(*args) ; end
8
+ end
9
+ end
@@ -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,11 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class Customer < Vend::Base
5
+ url_scope :since
6
+ findable_by :email
7
+ findable_by :name, :as => :q
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class Outlet < Vend::Base; end
5
+
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class PaymentType < Vend::Base; end
5
+
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class Product < Vend::Base
5
+ url_scope :since
6
+ url_scope :active
7
+
8
+ cast_attribute :supply_price, Float
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class Register < Vend::Base; end
5
+
6
+ end
7
+ 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,7 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class Tax < Vend::Base; end
5
+
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Vend
2
+ module Resource
3
+
4
+ class User < Vend::Base; end
5
+
6
+ end
7
+ 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
@@ -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