vend 0.0.4

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