poslavu 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.rdoc +106 -0
- data/Rakefile +15 -0
- data/lib/poslavu.rb +12 -0
- data/lib/poslavu/client.rb +110 -0
- data/lib/poslavu/query_scope.rb +160 -0
- data/lib/poslavu/row.rb +63 -0
- data/lib/poslavu/version.rb +4 -0
- data/poslavu.gemspec +38 -0
- data/spec/aliases_spec.rb +12 -0
- data/spec/live_spec.rb +182 -0
- data/spec/poslavu/client_spec.rb +65 -0
- data/spec/poslavu/query_scope_spec.rb +138 -0
- data/spec/poslavu/row_spec.rb +56 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/poslavu_api_stub.rb +29 -0
- metadata +247 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.rdoc
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/lib/poslavu.rb
ADDED
@@ -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
|
data/lib/poslavu/row.rb
ADDED
@@ -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
|
data/poslavu.gemspec
ADDED
@@ -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
|
data/spec/live_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|