poslavu 0.0.1

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.
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .env
19
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Will Glynn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ = POSLavu
2
+
3
+ POSLavu[http://www.poslavu.com/] is a hosted point-of-sale system. They
4
+ provide an API.
5
+
6
+ The POSLavu API is, franky, disgusting. It feels like something someone might
7
+ have come up with their first week using MySQL and PHP. There's POST parameters
8
+ and XML fragments and JSON scattered about. Error handling and input
9
+ sanitization are afterthoughts. There's no direction or cohesiveness. Tell me:
10
+ would *you* expose an "order" table with 91 different columns? POSLavu did.
11
+
12
+ This gem wraps that API into something that's more reasonable than using their
13
+ API directly. A gem can't fix the data model, but it can add some sanity to the
14
+ access methods.
15
+
16
+ Naturally, you'll need a POSLavu account to do anything useful.
17
+
18
+ == Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'poslavu'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install poslavu
31
+
32
+ == Usage
33
+
34
+ All usage starts by instantiating a client object.
35
+
36
+ client = POSLavu::Client.new('dataname', 'token', 'key')
37
+
38
+ From here, you can invoke API methods directly:
39
+
40
+ client.invoke('command', 'parameter' => 'value')
41
+
42
+ This is the low-level interface. Hopefully you'll never need it.
43
+
44
+ The POSLavu gem provides a higher-level query interface. Say you want to
45
+ iterate over all your orders:
46
+
47
+ client.table('orders').each { |order|
48
+ # ...
49
+ }
50
+
51
+ Done. This will issue multiple API calls as needed, traversing the list of
52
+ orders one page at a time. Naturally, the resulting object is +Enumerable+,
53
+ so you can call +.map+ or +.inject+ or whatever other normal things you
54
+ might want to do.
55
+
56
+ Now, say you want a list of orders that have produced 4 checks:
57
+
58
+ client.table('orders').where('no_of_checks' => 4).each { |order|
59
+ # ...
60
+ }
61
+
62
+ Or maybe you want to restrict by date:
63
+
64
+ client.table('orders').filter('opened', :between, '2012-10-01', '2012-10-02')
65
+
66
+ It also supports pagination, in case you'd like to handle that yourself:
67
+
68
+ client.table('orders').page(1, 50)
69
+ client.table('orders').page(2, 50)
70
+ client.table('orders').page(3, 50)
71
+
72
+ Client#table returns a POSLavu::QueryScope, which lets you chain various
73
+ conditions and lazily retrieve the results. Records are encapsulated by
74
+ POSLavu::Row, which is just a Hash that came from the POSLavu API.
75
+
76
+ == Development
77
+
78
+ POSLavu uses rspec and WebMock to validate functionality.
79
+
80
+ There is a component of the test suite that runs read-only queries against
81
+ the live POSLavu API. This is intended as a smoke test, principally exercising
82
+ the RPC mechanism, although it can also identify changes in the server-side
83
+ data model.
84
+
85
+ Running the live component of the test suite requires POSLavu API credentials.
86
+ This is safe to run against a live site; it does not modify any data. You can
87
+ pass in your credentials using environment variables or by creating a +.env+
88
+ file with the following:
89
+
90
+ POSLAVU_DATANAME=foobar
91
+ POSLAVU_KEY=q834SCx...
92
+ POSLAVU_TOKEN=EZcWR0n...
93
+
94
+ You can determine the proper values in the
95
+ API[http://admin.poslavu.com/cp/index.php?mode=api] tab of the POSLavu Control
96
+ Panel. Once you're ready, say:
97
+
98
+ $ bundle exec rake live
99
+
100
+ == Contributing
101
+
102
+ 1. Fork it
103
+ 2. Create your feature branch (<tt>git checkout -b my-new-feature</tt>)
104
+ 3. Commit your changes (<tt>git commit -am 'Added some feature'</tt>)
105
+ 4. Push to the branch (<tt>git push origin my-new-feature</tt>)
106
+ 5. Create new Pull Request
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.rspec_opts = ['--color --format documentation --tag ~live']
7
+ end
8
+
9
+ task :default => :spec
10
+ task :test => :spec
11
+
12
+ desc "Run tests against the live POSLavu servers"
13
+ task :live do
14
+ sh "bundle exec rspec --tag live --color --format documentation"
15
+ end
@@ -0,0 +1,12 @@
1
+ require "poslavu/version"
2
+
3
+ require "poslavu/row"
4
+ require "poslavu/client"
5
+ require "poslavu/query_scope"
6
+
7
+ module POSLavu
8
+ end
9
+
10
+ # Add some aliases so as to not be picky about capitalization
11
+ Object.const_set(:POSlavu, POSLavu)
12
+ Object.const_set(:Poslavu, POSLavu)
@@ -0,0 +1,110 @@
1
+ require "faraday"
2
+ require "nokogiri"
3
+
4
+ # POSLavu::Client communicates with the POSLavu API over HTTPS.
5
+ #
6
+ # You must provide a +dataname+, +token+, and +key+ to create a Client.
7
+ # From here, you can call Client#invoke to run arbitrary API commands.
8
+ #
9
+ # Clients don't hold any state except for a Faraday::Connection, which
10
+ # may or may not use persistent connections depending on your default
11
+ # Faraday adapter.
12
+ class POSLavu::Client
13
+
14
+ # Encapsulates an error thrown by the POSLavu API. All failing API calls
15
+ # via Client#invoke raise a subclass of this exception.
16
+ #
17
+ # See also:
18
+ # * CommandFailedError
19
+ # * CommunicationError
20
+ class Error < RuntimeError; end
21
+
22
+ # Represents an error returned by the API. POSLavu couldn't even be bothered to
23
+ # use HTTP status codes, so there's nothing machine-readable here. If you need
24
+ # to distinguish various types of errors, you'll have to do string matching and
25
+ # hope they don't change anything.
26
+ class CommandFailedError < Error; end
27
+
28
+ # Represents an error outside the scope of normal API failures, including all IP-,
29
+ # TCP-, and HTTP-level errors.
30
+ class CommunicationError < Error; end
31
+
32
+ # The API endpoint as a string
33
+ URL = "https://admin.poslavu.com/cp/reqserv/"
34
+
35
+ # Create a new Client with the specified credentials. These values can be
36
+ # retrieved from this[http://admin.poslavu.com/cp/index.php?mode=api] page as
37
+ # required.
38
+ def initialize(dataname, token, key)
39
+ @parameters = {
40
+ 'dataname' => dataname,
41
+ 'token' => token,
42
+ 'key' => key
43
+ }
44
+ end
45
+
46
+ # Returns this Client's +dataname+.
47
+ def dataname
48
+ @parameters['dataname']
49
+ end
50
+
51
+ def inspect #:nodoc:
52
+ "#<POSLavu::Client dataname=#{@parameters['dataname'].inspect}>"
53
+ end
54
+
55
+ # Returns an object that allows you to access the specified table.
56
+ #
57
+ # # Find all orders for a given table
58
+ # client.table('orders').where('table_id' => 5).each { |row|
59
+ # # ...
60
+ # }
61
+ #
62
+ # See POSLavu::QueryScope for the query syntax.
63
+ def table(table)
64
+ POSLavu::QueryScope.new(self, table)
65
+ end
66
+
67
+ # Invokes a command, accepting a +parameters+ hash, and returning an array of
68
+ # POSLavu::Row objects.
69
+ #
70
+ # The POSLavu API flattens all these parameters into a single POST request.
71
+ # +command+ is broken out as a convenience because #table handles querying, and
72
+ # specifying <tt>'cmd' => 'foo'</tt> repeatedly doesn't feel necessary.
73
+ def invoke(command, parameters = {})
74
+ final_parameters = @parameters.merge(parameters).merge('cmd' => command)
75
+
76
+ response = connection.post URL, final_parameters
77
+
78
+ fragment = Nokogiri::XML.fragment(response.body)
79
+ elements = fragment.children.select(&:element?) # .element_children doesn't work
80
+
81
+ if elements.empty?
82
+ if fragment.to_s.strip.empty?
83
+ # did we actually get no data?
84
+ return []
85
+ else
86
+ # this is apparently how errors are signalled
87
+ raise CommandFailedError, fragment.to_s
88
+ end
89
+ else
90
+ # assume all the elements are <row>s, and let Row explode if we're wrong
91
+ elements.map { |element|
92
+ POSLavu::Row.from_nokogiri(element)
93
+ }
94
+ end
95
+
96
+ rescue Faraday::Error::ClientError
97
+ raise CommunicationError, $!.to_s
98
+ end
99
+
100
+ protected
101
+ def connection #:nodoc:
102
+ @connection ||= Faraday.new(:url => URL) { |faraday|
103
+ faraday.request :url_encoded
104
+ faraday.response :raise_error
105
+ faraday.adapter Faraday.default_adapter
106
+ }.tap { |connection|
107
+ connection.headers[:user_agent] = "POSLavu Ruby #{POSLavu::VERSION}"
108
+ }
109
+ end
110
+ end
@@ -0,0 +1,160 @@
1
+ require 'multi_json'
2
+
3
+ # QueryScope represents a retrievable set of records. You can obtain one by
4
+ # calling POSLavu::Client#table() for the table in question.
5
+ #
6
+ # Query scopes are chainable. Given an initial scope representing all records
7
+ # in a table, you may further restrict the records of interest by with #filter,
8
+ # #page, and #where.
9
+ #
10
+ # Query scopes are Enumerable. #each is the obvious access method, but #to_a,
11
+ # #map, #inject, and all your friends are also available.
12
+ #
13
+ # Query scopes are lazy loading. You can manipulate them as much as you want
14
+ # without performing any API calls. The request is actually performed once you
15
+ # call #each or any other Enumerable method. If you've called #page, the results
16
+ # are held in memory. If not, #each issues multiple requests (internally
17
+ # paginating) and does not hold the result in memory.
18
+ class POSLavu::QueryScope
19
+ include Enumerable
20
+
21
+ # The name of the table, as passed to POSLavu::Client#table
22
+ attr_reader :table
23
+
24
+ # The list of operators supported by the POSLavu API (and thus supported by #filter).
25
+ Operators = ['<=', '>=', '<', '>', '=', '<>', '!=', 'BETWEEN', 'LIKE', 'NOT LIKE']
26
+
27
+ # Returns a new QueryScope with the specified filter applied.
28
+ #
29
+ # The POSLavu API has a basic query language modeled after SQL, probably because
30
+ # they're shoveling the filter straight into SQL. It supports restricting +field+s
31
+ # using a set of Operators. All of them require a value for comparison, except
32
+ # +BETWEEN+, which requires two values.
33
+ #
34
+ # +LIKE+ and +NOT LIKE+ accept +'%'+ as a wildcard. There is no mechanism for
35
+ # pattern-matching strings containing a percent sign.
36
+ def filter(field, operator, value, value2=nil)
37
+ operator = operator.to_s.upcase
38
+ raise ArgumentError, "invalid operator" unless Operators.include?(operator)
39
+
40
+ chain { |x|
41
+ filter = { 'field' => field, 'operator' => operator, 'value1' => value.to_s }
42
+ filter['value2'] = value2.to_s if operator == 'BETWEEN'
43
+ x.filters << filter
44
+ }
45
+ end
46
+
47
+ # Returns a new QueryScope, restricting results to the specified page number.
48
+ #
49
+ # Pages are 1-indexed: the first page is page 1, not page 0.
50
+ def page(number, records_per_page=40)
51
+ raise ArgumentError, "the first page number is 1 (got #{number})" if number < 1
52
+
53
+ chain { |x|
54
+ x.start_record = (number - 1) * records_per_page
55
+ x.record_count = records_per_page
56
+ }
57
+ end
58
+
59
+ # Returns a new QueryScope, restricting results to rows matching the specified
60
+ # hash. It is a convenience method around #filter. The following two statements
61
+ # are exactly equivalent:
62
+ #
63
+ # client.table('orders').where('table_id' => 4, 'no_of_checks' => 2)
64
+ # client.table('orders').filter('table_id', '=', 4).filter('no_of_checks', '=', 2)
65
+ def where(hash)
66
+ scope = self
67
+ hash.each { |key,value|
68
+ scope = scope.filter(key, '=', value)
69
+ }
70
+ scope
71
+ end
72
+
73
+ # Iterate over the records represented by this query scope.
74
+ #
75
+ # If this scope has an explicit #page set, the results will be retrieved and
76
+ # memoized. Otherwise, this scope will internally paginate and make successive
77
+ # requests, yielding each row in turn, and the results will not be memoized.
78
+ def each(&block)
79
+ if @rows
80
+ # we've been memoized
81
+ @rows.each(&block)
82
+
83
+ elsif start_record
84
+ # we represent a single page
85
+ # do the fetching and iterate
86
+ @rows = fetch_rows
87
+
88
+ @rows.each(&block)
89
+ else
90
+ # we represent the whole set of possible records
91
+ # fetch repeatedly, in pages
92
+ page_number = 1
93
+ records_per_page = 100
94
+
95
+ loop {
96
+ # create a scope for this page
97
+ inner_scope = page(page_number, records_per_page)
98
+
99
+ # fetch the records as an array
100
+ records = inner_scope.to_a
101
+
102
+ # pass them to the caller
103
+ records.each(&block)
104
+
105
+ # is this the last page?
106
+ if records.size < records_per_page
107
+ # was this the first page?
108
+ if page_number == 1
109
+ # this is the only page
110
+ # memoize
111
+ @rows = records
112
+ end
113
+
114
+ # regardless, we're done
115
+ break
116
+ end
117
+
118
+ page_number += 1
119
+ }
120
+
121
+ end
122
+
123
+ self
124
+ end
125
+
126
+ #:nodoc:
127
+ protected
128
+ attr_accessor :filters, :start_record, :record_count
129
+ attr_accessor :rows
130
+
131
+ def initialize(client, table)
132
+ @client = client
133
+ @table = table
134
+
135
+ @filters = []
136
+ @start_record = nil
137
+ @record_count = nil
138
+ @rows = nil
139
+ end
140
+
141
+ def chain(&block)
142
+ dup.tap { |copy|
143
+ copy.rows = nil
144
+ yield(copy)
145
+ }
146
+ end
147
+
148
+ def fetch_rows
149
+ @client.invoke('list', to_params)
150
+ end
151
+
152
+ def to_params
153
+ {
154
+ 'table' => @table,
155
+ 'limit' => "#{start_record},#{record_count}"
156
+ }.tap do |params|
157
+ params['filters'] = MultiJson.dump(filters) unless filters.empty?
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,63 @@
1
+ require "nokogiri"
2
+
3
+ # The POSLavu API principally operates on database rows exchanged in
4
+ # XML fragments. These are encapsulated as POSLavu::Row objects, which
5
+ # is really just a Hash with some additional methods.
6
+ class POSLavu::Row < Hash
7
+ # Instantiate a Row, optionally copying an existing Hash.
8
+ def initialize(hash_to_copy = nil)
9
+ if hash_to_copy
10
+ hash_to_copy.each { |key,value|
11
+ self[key.to_sym] = value.to_s
12
+ }
13
+ end
14
+ end
15
+
16
+ # Instantiate a Row given a string containing a <tt><row/></tt> XML fragment.
17
+ # This XML fragment must contain exactly one <tt><row></tt> element at the root.
18
+ def self.from_xml(string)
19
+ fragment = Nokogiri::XML.fragment(string)
20
+ from_nokogiri(fragment)
21
+ end
22
+
23
+ # Instantiate a Row from a Nokogiri::XML::Node or similar. If you're using
24
+ # the public interface, you shouldn't ever need to call this.
25
+ def self.from_nokogiri(xml) # :nodoc:
26
+ raise ArgumentError, "argument is not a Nokogiri node" unless xml.kind_of?(Nokogiri::XML::Node)
27
+
28
+ if xml.element? && xml.name == 'row'
29
+ xml_row = xml
30
+ else
31
+ rows = xml.xpath('./row')
32
+ raise ArgumentError, "argument does not directly contain a <row> element" if rows.empty?
33
+ raise ArgumentError, "argument contains more than one <row> element" if rows.size > 1
34
+
35
+ xml_row = rows.first
36
+ end
37
+
38
+ new.tap { |row|
39
+ xml_row.element_children.each { |element|
40
+ row[element.name.to_sym] = element.text
41
+ }
42
+ }
43
+ end
44
+
45
+ # Adds this Row to a Nokogiri::XML::Node. If you're using the public
46
+ # interface, you shouldn't ever need to call this.
47
+ def to_nokogiri(doc) # :nodoc:
48
+ row = doc.create_element('row'); doc.add_child(row)
49
+ each { |key,value|
50
+ element = doc.create_element(key.to_s)
51
+ element.add_child(doc.create_text_node(value.to_s))
52
+ row.add_child(element)
53
+ }
54
+ row
55
+ end
56
+
57
+ # Transform this Row into a string containing a <tt><row/></tt> XML fragment
58
+ def to_xml
59
+ doc = Nokogiri::XML::Document.new
60
+ element = to_nokogiri(doc)
61
+ element.to_s
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ module POSLavu
2
+ # The version of the POSLavu gem.
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/poslavu/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Will Glynn"]
6
+ gem.email = ["will@willglynn.com"]
7
+ gem.description = %q{POSLavu is a hosted point-of-sale system. The `poslavu` gem provides access to the API.}
8
+ gem.summary = %q{POSLavu API client}
9
+ gem.homepage = "http://github.com/willglynn/poslavu"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "poslavu"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = POSLavu::VERSION
17
+
18
+ gem.rdoc_options += [
19
+ '--title', 'POSLavu API Client',
20
+ '--main', 'README.rdoc',
21
+ '--exclude', 'spec',
22
+ '--exclude', 'Gemfile',
23
+ '--exclude', 'Rakefile'
24
+ ]
25
+
26
+ gem.add_dependency "nokogiri", "~> 1.5"
27
+ gem.add_dependency "faraday", "~> 0.8"
28
+ gem.add_dependency "multi_json", "~> 1.3"
29
+
30
+ gem.add_development_dependency "bundler", "~> 1.1"
31
+ gem.add_development_dependency "dotenv", "~> 0.2"
32
+ gem.add_development_dependency "guard", "~> 1.4"
33
+ gem.add_development_dependency "rspec", "~> 2.11"
34
+ gem.add_development_dependency "rake", "~> 0.9.2"
35
+ gem.add_development_dependency "webmock", "~> 1.8"
36
+
37
+ gem.add_development_dependency "pry"
38
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Global aliases" do
4
+ describe Poslavu do
5
+ it { should eql(POSlavu) }
6
+ end
7
+
8
+ describe POSlavu do
9
+ it { should eql(POSLavu) }
10
+ end
11
+ end
12
+
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Live tests", :live => true do
4
+ if ENV['POSLAVU_DATANAME'] && ENV['POSLAVU_KEY'] && ENV['POSLAVU_TOKEN']
5
+ before(:all) { WebMock.allow_net_connect! }
6
+ after(:all) { WebMock.disable_net_connect! }
7
+ let!(:client) { POSLavu::Client.new(ENV['POSLAVU_DATANAME'], ENV['POSLAVU_TOKEN'], ENV['POSLAVU_KEY']) }
8
+ subject { client }
9
+
10
+ describe("tables") do
11
+ let(:response) { client.table(table).page(1,1).to_a }
12
+ subject { response }
13
+
14
+ describe("locations") do
15
+ let(:table) { 'locations' }
16
+ it { should_not be_empty }
17
+
18
+ describe("#first") {
19
+ subject { response.first }
20
+ it { should be_kind_of(POSLavu::Row) }
21
+ its(:keys) { should eq [:id, :title, :address, :city, :state, :zip, :phone, :website, :manager, :taxrate, :menu_id, :ag1, :ag2, :PINwait, :integrateCC, :seat_numbers, :PINwaitM, :country, :monitary_symbol, :left_or_right, :gratuity_label, :auto_send_at_checkout, :allow_resend, :lock_orders, :lock_order_override, :exit_after_save, :exit_after_send, :exit_after_print_check, :exit_after_checkout, :exit_after_void, :admin_PIN_discount, :admin_PIN_void, :lock_net_path, :tab_view, :tabs_and_tables, :hide_tabs, :allow_tab_view_toggle, :display_forced_modifier_prices, :component_package, :ask4email_at_checkout, :component_package2, :allow_custom_items, :default_receipt_print, :product_level, :exit_order_after_save, :exit_order_after_send, :exit_order_after_print_check, :use_direct_printing, :debug_mode, :allow_deposits, :cc_signatures, :gateway, :cc_transtype, :admin_PIN_terminal_set, :admin_PIN_till_report, :ml_type, :ml_un, :ml_pw, :ml_listid, :disable_decimal, :individual_cc_receipts, :server_manage_tips, :day_start_end_time, :require_cvn, :display_order_sent_in_ipod, :print_forced_mods_on_receipt, :print_optional_mods_on_receipt, :return_after_add_item_on_ipod, :kitchen_ticket_font_size, :receipt_font_size, :modifiers_in_red, :market_type, :allow_debug_menu, :customer_cc_receipt, :verify_swipe_amount, :itemize_payments_on_receipt, :debug_console, :allow_cc_returns, :allow_voice_auth, :allow_partial_auth, :allow_signature_tip, :mute_register_bell, :group_equivalent_items, :cc_signatures_ipod, :string_encoding, :tax_inclusion, :rounding_factor, :round_up_or_down, :tax_auto_gratuity, :level_to_open_register, :append_cct_details, :html_email_receipts, :verify_remaining_payment, :multiple_quantities_in_red, :verify_quickpay, :use_language_pack, :print_item_notes_on_receipt, :verify_entered_payment, :other_transactions_open_drawer, :credit_transactions_open_drawer, :allow_to_rate, :gateway_debug, :level_to_open_register_at_checkout, :ask_for_guest_count, :level_to_edit_sent_items, :default_dining_room_background, :admin_PIN_void_payments, :admin_PIN_refund, :order_pad_font, :email, :raster_mode_font_size1, :raster_mode_font_size2, :print_logo_on_receipts, :allow_tax_exempt, :level_to_grant_tax_exempt, :timezone, :clockin_overlay, :item_icon_color, :component_package3, :save_device_console_log, :order_pad_font_ipod, :default_preauth_amount, :decimal_char, :thousands_char, :get_card_info, :bypass_checkout_message, :hide_item_titles, :allow_RFID, :display_seat_course_icons, :cash_transactions_open_drawer, :component_package_code, :vf_enrollment_id] }
22
+ }
23
+ end
24
+
25
+ describe("orders") do
26
+ let(:table) { 'orders' }
27
+ it { should_not be_empty }
28
+
29
+ describe("#first") {
30
+ subject { response.first }
31
+ it { should be_kind_of(POSLavu::Row) }
32
+ its(:keys) { should eq [:id, :order_id, :location, :location_id, :opened, :closed, :subtotal, :taxrate, :tax, :total, :server, :server_id, :tablename, :send_status, :discount, :discount_sh, :gratuity, :gratuity_percent, :card_gratuity, :cash_paid, :card_paid, :gift_certificate, :change_amount, :reopen_refund, :void, :cashier, :cashier_id, :auth_by, :auth_by_id, :guests, :email, :permission, :check_has_printed, :no_of_checks, :card_desc, :transaction_id, :multiple_tax_rates, :tab, :original_id, :deposit_status, :register, :refunded, :refund_notes, :refunded_cc, :refund_notes_cc, :refunded_by, :refunded_by_cc, :cash_tip, :discount_value, :reopened_datetime, :discount_type, :deposit_amount, :subtotal_without_deposit, :togo_status, :togo_phone, :togo_time, :cash_applied, :reopen_datetime, :rounding_amount, :auto_gratuity_is_taxed, :discount_id, :refunded_gc, :register_name, :opening_device, :closing_device, :alt_paid, :alt_refunded, :last_course_sent, :tax_exempt, :reclosed_datetime, :reopening_device, :reclosing_device, :exemption_id, :exemption_name, :recloser, :recloser_id, :void_reason, :alt_tablename, :checked_out, :idiscount_amount, :past_names, :itax, :togo_name, :merges, :active_device, :tabname, :last_modified, :last_mod_device, :discount_info, :last_mod_register_name, :force_closed] }
33
+ }
34
+ end
35
+
36
+ describe("order_contents") do
37
+ let(:table) { 'order_contents' }
38
+ it { should_not be_empty }
39
+
40
+ describe("#first") {
41
+ subject { response.first }
42
+ its(:keys) { should eq [:id, :loc_id, :order_id, :item, :price, :quantity, :options, :special, :modify_price, :print, :check, :seat, :item_id, :printer, :apply_taxrate, :custom_taxrate, :modifier_list_id, :forced_modifier_group_id, :forced_modifiers_price, :course, :print_order, :open_item, :subtotal, :allow_deposit, :deposit_info, :discount_amount, :discount_value, :discount_type, :after_discount, :subtotal_with_mods, :tax_amount, :notes, :total_with_tax, :itax_rate, :itax, :tax_rate1, :tax1, :tax_rate2, :tax2, :tax_rate3, :tax3, :tax_subtotal1, :tax_subtotal2, :tax_subtotal3, :after_gratuity, :void, :discount_id, :server_time, :device_time, :idiscount_id, :idiscount_sh, :idiscount_value, :idiscount_type, :idiscount_amount, :split_factor, :hidden_data1, :hidden_data2, :hidden_data3, :hidden_data4, :tax_inclusion, :tax_name1, :tax_name2, :tax_name3, :sent, :tax_exempt, :exemption_id, :exemption_name, :itax_name, :checked_out, :hidden_data5, :hidden_data6, :price_override, :original_price, :override_id, :auto_saved, :idiscount_info, :category_id] }
43
+ }
44
+ end
45
+
46
+ describe("order_payments") do
47
+ let(:table) { 'order_payments' }
48
+ it { should_not be_empty }
49
+
50
+ describe("#first") {
51
+ subject { response.first }
52
+ its(:keys) { should eq [:id, :order_id, :check, :amount, :card_desc, :transaction_id, :refunded, :refund_notes, :refunded_by, :refund_pnref, :tip_amount, :auth, :loc_id, :processed, :auth_code, :card_type, :datetime, :pay_type, :voided, :void_notes, :voided_by, :void_pnref, :register, :got_response, :transtype, :split_tender_id, :temp_data, :change, :total_collected, :record_no, :server_name, :action, :ref_data, :process_data, :voice_auth, :server_id, :preauth_id, :tip_for_id, :swipe_grade, :batch_no, :register_name, :pay_type_id, :first_four, :mpshc_pid, :server_time, :info, :signature, :info_label, :for_deposit, :more_info, :customer_id, :is_deposit, :device_udid, :internal_id] }
53
+ }
54
+ end
55
+
56
+ describe("menu_groups") do
57
+ let(:table) { 'menu_groups' }
58
+ it { should_not be_empty }
59
+
60
+ describe("#first") {
61
+ subject { response.first }
62
+ its(:keys) { should eq [:id, :menu_id, :group_name, :orderby] }
63
+ }
64
+ end
65
+
66
+ describe("menu_categories") do
67
+ let(:table) { 'menu_categories' }
68
+ it { should_not be_empty }
69
+
70
+ describe("#first") {
71
+ subject { response.first }
72
+ its(:keys) { should eq [:id, :menu_id, :group_id, :name, :image, :description, :active, :print, :last_modified_date, :printer, :modifier_list_id, :apply_taxrate, :custom_taxrate, :forced_modifier_group_id, :print_order, :super_group_id, :tax_inclusion, :ltg_display] }
73
+ }
74
+ end
75
+
76
+ describe("menu_items") do
77
+ let(:table) { 'menu_items' }
78
+ it { should_not be_empty }
79
+
80
+ describe("#first") {
81
+ subject { response.first }
82
+ its(:keys) { should eq [:id, :category_id, :menu_id, :name, :price, :description, :image, :options1, :options2, :options3, :active, :print, :quick_item, :last_modified_date, :printer, :apply_taxrate, :custom_taxrate, :modifier_list_id, :forced_modifier_group_id, :image2, :image3, :misc_content, :ingredients, :open_item, :hidden_value, :hidden_value2, :allow_deposit, :UPC, :hidden_value3, :inv_count, :show_in_app, :super_group_id, :tax_inclusion, :ltg_display] }
83
+ }
84
+ end
85
+
86
+ describe("tables") do
87
+ let(:table) { 'tables' }
88
+ it { should_not be_empty }
89
+
90
+ describe("#first") {
91
+ subject { response.first }
92
+ its(:keys) { should eq [:id, :loc_id, :coord_x, :coord_y, :shapes, :widths, :heights, :names, :title, :rotate, :centerX, :centerY] }
93
+ }
94
+ end
95
+
96
+ describe("clock_punches") do
97
+ let(:table) { 'clock_punches' }
98
+ it { should_not be_empty }
99
+
100
+ describe("#first") {
101
+ subject { response.first }
102
+ its(:keys) { should eq [:id, :location, :location_id, :punch_type, :server, :server_id, :time, :hours, :punched_out, :time_out, :server_time, :server_time_out, :punch_id, :udid_in, :udid_out, :ip_in, :ip_out, :notes] }
103
+ }
104
+ end
105
+
106
+ describe("modifiers") do
107
+ let(:table) { 'modifiers' }
108
+ it { should_not be_empty }
109
+
110
+ describe("#first") {
111
+ subject { response.first }
112
+ its(:keys) { should eq [:title, :cost, :category, :id, :ingredients] }
113
+ }
114
+ end
115
+
116
+ describe("modifiers_used") do
117
+ let(:table) { 'modifiers_used' }
118
+ it { should_not be_empty }
119
+
120
+ describe("#first") {
121
+ subject { response.first }
122
+ its(:keys) { should eq [:id, :loc_id, :order_id, :mod_id, :qty, :type, :row, :cost, :unit_cost] }
123
+ }
124
+ end
125
+
126
+ describe("forced_modifiers") do
127
+ let(:table) { 'forced_modifiers' }
128
+ it { should_not be_empty }
129
+
130
+ describe("#first") {
131
+ subject { response.first }
132
+ its(:keys) { should eq [:title, :cost, :list_id, :id, :detour, :extra, :extra2, :ingredients, :extra3, :extra4, :extra5] }
133
+ }
134
+ end
135
+
136
+ describe("ingredients") do
137
+ let(:table) { 'ingredients' }
138
+ it { should_not be_empty }
139
+
140
+ describe("#first") {
141
+ subject { response.first }
142
+ its(:keys) { should eq [:title, :qty, :unit, :low, :high, :id, :category, :cost, :loc_id] }
143
+ }
144
+ end
145
+
146
+ describe("ingredient_categories") do
147
+ let(:table) { 'ingredient_categories' }
148
+ it { should_not be_empty }
149
+
150
+ describe("#first") {
151
+ subject { response.first }
152
+ its(:keys) { should eq [:title, :description, :id, :loc_id] }
153
+ }
154
+ end
155
+
156
+ describe("ingredient_usage") do
157
+ let(:table) { 'ingredient_usage' }
158
+ it { should_not be_empty }
159
+
160
+ describe("#first") {
161
+ subject { response.first }
162
+ its(:keys) { should eq [:ts, :date, :orderid, :itemid, :ingredientid, :qty, :id, :loc_id, :server_time, :cost, :content_id] }
163
+ }
164
+ end
165
+
166
+ describe("users") do
167
+ let(:table) { 'users' }
168
+ it { should_not be_empty }
169
+
170
+ describe("#first") {
171
+ subject { response.first }
172
+ its(:keys) { should eq [:id, :company_code, :username, :f_name, :l_name, :email, :access_level, :quick_serve, :loc_id, :service_type, :address, :phone, :mobile, :role_id, :deleted_date, :created_date] }
173
+ }
174
+ end
175
+ end
176
+
177
+ else
178
+ it("API credentials") {
179
+ pending "none provided; see README.md"
180
+ }
181
+ end
182
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe POSLavu::Client do
4
+ let(:dataname) { 'dataname' }
5
+ let(:token) { 'token' }
6
+ let(:key) { 'key' }
7
+ let(:client) { POSLavu::Client.new(dataname, token, key) }
8
+ subject { client }
9
+
10
+ describe "invoke" do
11
+ describe "HTTP requests" do
12
+ before {
13
+ poslavu_api_stub
14
+ client.invoke('command', 'abc' => 'def') rescue nil
15
+ }
16
+
17
+ specify("should request with 'dataname' parameter") { WebMock.should have_requested_poslavu_api('dataname' => dataname) }
18
+ specify("should request with 'token' parameter") { WebMock.should have_requested_poslavu_api('token' => token) }
19
+ specify("should request with 'key' parameter") { WebMock.should have_requested_poslavu_api('key' => key) }
20
+ specify("should request with 'cmd' parameter") { WebMock.should have_requested_poslavu_api('cmd' => 'command') }
21
+ specify("should request with hash parameter") { WebMock.should have_requested_poslavu_api('abc' => 'def') }
22
+ specify("should request with User-Agent header") { WebMock.should have_requested_poslavu_api.with(:headers => { 'User-Agent' => "POSLavu Ruby #{POSLavu::VERSION}"}) }
23
+ end
24
+
25
+ describe "error handling" do
26
+ it "raises errors returned as body text" do
27
+ poslavu_api_stub { 'this is an error' }
28
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error, "this is an error"
29
+ end
30
+
31
+ # not yet observed in the wild
32
+ it "raises errors for 500 responses" do
33
+ poslavu_api_stub.to_return(:status => 500)
34
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error
35
+ end
36
+
37
+ it "raises errors for timeouts" do
38
+ poslavu_api_stub.to_timeout
39
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error
40
+ end
41
+ end
42
+
43
+ describe "response parsing" do
44
+ before { poslavu_api_stub { response } }
45
+ subject { client.invoke('command') }
46
+
47
+ describe "empty string" do
48
+ let(:response) { "" }
49
+ it { should be_kind_of Array }
50
+ it { should be_empty }
51
+ end
52
+
53
+ describe "single row" do
54
+ let(:response) { [POSLavu::Row.new(:foo => 'bar', :baz => 'quxx')] }
55
+ it { should be_kind_of Array }
56
+ it("should match the expected value") { subject.should eql response }
57
+ end
58
+
59
+ describe "multiple rows" do
60
+ let(:response) { (1..10).map { |n| POSLavu::Row.new(:counter => n) } }
61
+ it("should match the expected value") { subject.should eql response }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,138 @@
1
+ describe POSLavu::QueryScope do
2
+ let(:dataname) { 'dataname' }
3
+ let(:token) { 'token' }
4
+ let(:key) { 'key' }
5
+ let(:table_name) { 'table' }
6
+ let(:client) { POSLavu::Client.new(dataname, token, key) }
7
+ let(:table) { client.table(table_name) }
8
+
9
+ it "should be returned by POSLavu::Client#table" do
10
+ table.should be_kind_of POSLavu::QueryScope
11
+ end
12
+
13
+ describe "#filter" do
14
+ before { poslavu_api_stub { POSLavu::Row.new } }
15
+ ['<=', '>=', '<', '>', '=', '<>', '!=', 'LIKE', 'NOT LIKE'].each { |operator|
16
+ specify "filters using #{operator}" do
17
+ table.filter('a', operator, 'b').page(1, 1).each {}
18
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"' + operator + '","value1":"b"}]')
19
+ end
20
+ }
21
+
22
+ specify "filters using BETWEEN" do
23
+ table.filter('a', 'BETWEEN', 'b', 'c').page(1, 1).each {}
24
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"BETWEEN","value1":"b","value2":"c"}]')
25
+ end
26
+
27
+
28
+ it "chains multiple filters" do
29
+ table.filter('a', '=', 'b').filter('c', '=', 'd').page(1, 1).each {}
30
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"},{"field":"c","operator":"=","value1":"d"}]')
31
+ end
32
+
33
+ it "preserves pagination when filtering" do
34
+ base = table.page(2, 47)
35
+ filtered = base.filter('field', '=', 'value')
36
+ filtered.instance_variable_get(:@start_record).should eq 47
37
+ filtered.instance_variable_get(:@record_count).should eq 47
38
+ end
39
+ end
40
+
41
+ describe "#where" do
42
+ before { poslavu_api_stub { POSLavu::Row.new } }
43
+
44
+ it "creates proper JSON filter" do
45
+ table.where('a' => 'b', 'c' => 'd').page(1, 1).each {}
46
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"},{"field":"c","operator":"=","value1":"d"}]')
47
+ end
48
+ end
49
+
50
+ describe "#each" do
51
+ describe "for a single page" do
52
+ let(:page_size) { 40 }
53
+ before { poslavu_api_stub { (1..page_size).map { |n| POSLavu::Row.new('counter' => n) } } }
54
+ subject { table.page(1, page_size) }
55
+
56
+ it "returns rows" do
57
+ collected = []
58
+ subject.each { |value| collected << value }
59
+ collected.size.should eq 40
60
+ end
61
+
62
+ it "memoizes results" do
63
+ subject.each { }
64
+ subject.each { }
65
+ WebMock.should have_requested_poslavu_api.once
66
+ end
67
+
68
+ it "re-requests when chained" do
69
+ subject.each { }
70
+ child = subject.filter('foo', '=', 'bar')
71
+ child.each { }
72
+ WebMock.should have_requested_poslavu_api.twice
73
+ end
74
+
75
+ it "passes filter as JSON" do
76
+ subject.filter('a', '=', 'b').each {}
77
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"}]')
78
+ end
79
+ end
80
+
81
+ describe "for all pages when there is only one page" do
82
+ before {
83
+ poslavu_api_stub('limit' => '0,100') { (1..50).map { |n| POSLavu::Row.new('counter' => n) } }
84
+ }
85
+
86
+ subject { table }
87
+
88
+ it "returns all results" do
89
+ rows = 0
90
+ subject.each { rows += 1 }
91
+ rows.should eq 50
92
+ end
93
+
94
+ it "requests all pages" do
95
+ subject.each {}
96
+ WebMock.should have_requested_poslavu_api.once
97
+ end
98
+
99
+ it "memoizes results" do
100
+ subject.each {}
101
+ subject.each {}
102
+ WebMock.should have_requested_poslavu_api.once
103
+ end
104
+ end
105
+
106
+ describe "for all pages when there is more than one" do
107
+ let(:full_page) { (1..100).map { |n| POSLavu::Row.new('counter' => n) } }
108
+ let(:half_page) { full_page[0,50] }
109
+
110
+ before {
111
+ poslavu_api_stub('limit' => '0,100') { full_page }
112
+ poslavu_api_stub('limit' => '100,100') { full_page }
113
+ poslavu_api_stub('limit' => '200,100') { half_page }
114
+ }
115
+
116
+ subject { table }
117
+
118
+ it "returns all results" do
119
+ rows = 0
120
+ subject.each { rows += 1 }
121
+ rows.should eq 250
122
+ end
123
+
124
+ it "requests all pages" do
125
+ subject.each {}
126
+ WebMock.should have_requested_poslavu_api.times(3)
127
+ end
128
+
129
+ it "does not memoize results" do
130
+ subject.each {}
131
+ subject.each {}
132
+ WebMock.should have_requested_poslavu_api.times(6)
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe POSLavu::Row do
4
+ subject { POSLavu::Row.new }
5
+
6
+ it { should be_kind_of Hash }
7
+
8
+ describe "new" do
9
+ subject { POSLavu::Row.new(hash) }
10
+
11
+ describe "hash with symbol key and string value" do
12
+ let(:hash) do { :a => 'b' } end
13
+ its(:keys) { should eql [:a] }
14
+ its(:values) { should eql ['b'] }
15
+ end
16
+
17
+ describe "hash with string key and integer value" do
18
+ let(:hash) do { 'a' => 0 } end
19
+ its(:keys) { should eql [:a] }
20
+ its(:values) { should eql ['0'] }
21
+ end
22
+ end
23
+
24
+ describe "#from_xml" do
25
+ subject { POSLavu::Row.from_xml(string) }
26
+
27
+ describe "<row/>" do
28
+ let(:string) { "<row/>" }
29
+ it { should be_empty }
30
+ end
31
+
32
+ describe "<row><foo>bar</foo></row>" do
33
+ let(:string) { "<row><foo>bar</foo></row>" }
34
+ its(:keys) { should eq [:foo] }
35
+ its(:values) { should eq ['bar'] }
36
+
37
+ it("to_xml should match") {
38
+ subject.to_xml.gsub(/\s/, '').should eq(string.gsub(/\s/, ''))
39
+ }
40
+ end
41
+
42
+ describe "failure cases" do
43
+ def self.should_fail(string)
44
+ subject { lambda { POSLavu::Row.from_xml(string) } }
45
+ it("#{string.inspect} should raise ArgumentError") { should raise_exception ArgumentError }
46
+ end
47
+
48
+ should_fail nil
49
+ should_fail ""
50
+ should_fail "<not_a_row_element/>"
51
+ should_fail "<row/><row/>"
52
+ should_fail "<result><row/></result>"
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ require "poslavu"
2
+ Bundler.require :development
3
+
4
+ require 'webmock/rspec'
5
+
6
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}
7
+
8
+ RSpec.configure { |config|
9
+ config.include POSLavu::APIStub
10
+ }
11
+
12
+ Dotenv.load
@@ -0,0 +1,29 @@
1
+ module POSLavu::APIStub
2
+ def poslavu_api_stub(params = {}, &block)
3
+ stub = stub_http_request(:post, POSLavu::Client::URL).with(:body => hash_including(params))
4
+
5
+ if block
6
+ stub.to_return { |request|
7
+ response = block.call(request)
8
+
9
+ if response.instance_of?(Hash)
10
+ response
11
+ else
12
+ body = case response
13
+ when Array then response.map(&:to_xml).join # assume array of rows
14
+ when POSLavu::Row then response.to_xml
15
+ else response.to_s
16
+ end
17
+
18
+ { :body => body, :status => 200 }
19
+ end
20
+ }
21
+ end
22
+
23
+ stub
24
+ end
25
+
26
+ def have_requested_poslavu_api(params = {})
27
+ have_requested(:post, POSLavu::Client::URL).with(:body => hash_including(params))
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: poslavu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Will Glynn
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: faraday
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.8'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.8'
46
+ - !ruby/object:Gem::Dependency
47
+ name: multi_json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.1'
78
+ - !ruby/object:Gem::Dependency
79
+ name: dotenv
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '0.2'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '0.2'
94
+ - !ruby/object:Gem::Dependency
95
+ name: guard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.4'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '2.11'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '2.11'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rake
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 0.9.2
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 0.9.2
142
+ - !ruby/object:Gem::Dependency
143
+ name: webmock
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: '1.8'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: '1.8'
158
+ - !ruby/object:Gem::Dependency
159
+ name: pry
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description: POSLavu is a hosted point-of-sale system. The `poslavu` gem provides
175
+ access to the API.
176
+ email:
177
+ - will@willglynn.com
178
+ executables: []
179
+ extensions: []
180
+ extra_rdoc_files: []
181
+ files:
182
+ - .gitignore
183
+ - Gemfile
184
+ - LICENSE
185
+ - README.rdoc
186
+ - Rakefile
187
+ - lib/poslavu.rb
188
+ - lib/poslavu/client.rb
189
+ - lib/poslavu/query_scope.rb
190
+ - lib/poslavu/row.rb
191
+ - lib/poslavu/version.rb
192
+ - poslavu.gemspec
193
+ - spec/aliases_spec.rb
194
+ - spec/live_spec.rb
195
+ - spec/poslavu/client_spec.rb
196
+ - spec/poslavu/query_scope_spec.rb
197
+ - spec/poslavu/row_spec.rb
198
+ - spec/spec_helper.rb
199
+ - spec/support/poslavu_api_stub.rb
200
+ homepage: http://github.com/willglynn/poslavu
201
+ licenses: []
202
+ post_install_message:
203
+ rdoc_options:
204
+ - --title
205
+ - POSLavu API Client
206
+ - --main
207
+ - README.rdoc
208
+ - --exclude
209
+ - spec
210
+ - --exclude
211
+ - Gemfile
212
+ - --exclude
213
+ - Rakefile
214
+ require_paths:
215
+ - lib
216
+ required_ruby_version: !ruby/object:Gem::Requirement
217
+ none: false
218
+ requirements:
219
+ - - ! '>='
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ segments:
223
+ - 0
224
+ hash: -4247689874090540976
225
+ required_rubygems_version: !ruby/object:Gem::Requirement
226
+ none: false
227
+ requirements:
228
+ - - ! '>='
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ segments:
232
+ - 0
233
+ hash: -4247689874090540976
234
+ requirements: []
235
+ rubyforge_project:
236
+ rubygems_version: 1.8.24
237
+ signing_key:
238
+ specification_version: 3
239
+ summary: POSLavu API client
240
+ test_files:
241
+ - spec/aliases_spec.rb
242
+ - spec/live_spec.rb
243
+ - spec/poslavu/client_spec.rb
244
+ - spec/poslavu/query_scope_spec.rb
245
+ - spec/poslavu/row_spec.rb
246
+ - spec/spec_helper.rb
247
+ - spec/support/poslavu_api_stub.rb