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.
- 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
|