vuzitruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,101 @@
1
+ = VuzitRuby - Vuzit Web Services library
2
+
3
+ == INTRODUCTION
4
+
5
+ This is a library that allows developers to directly access the Vuzit Web
6
+ Service API through a simple Ruby script:
7
+
8
+ http://vuzit.com/developer/documents_api
9
+
10
+ Below is a basic upload example:
11
+
12
+ require "vuzitruby"
13
+
14
+ Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
15
+ Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
16
+
17
+ doc = Vuzit::Document.upload("c:/path/to/document.pdf")
18
+
19
+ puts "Document id: " + doc.id
20
+
21
+ To get started all you need to do is download the code, sign up for a free
22
+ account (https://ssl.vuzit.com/signup) and replace the public and
23
+ private keys with the keys from your account.
24
+
25
+ == SETUP
26
+
27
+ The client library is a RubyGem called *vuzitruby*. To install, type:
28
+
29
+ sudo gem install vuzitruby
30
+
31
+ == GETTING STARTED
32
+
33
+ * Download the code - http://github.com/vuzit/vuzitruby/downloads
34
+ * Sign up for a free Vuzit account - https://ssl.vuzit.com/signup
35
+ * Code Examples - http://wiki.github.com/vuzit/vuzitruby/code-examples
36
+ * Vuzit API Reference - http://doc.vuzit.com/vuzitruby
37
+
38
+ == EXAMPLES
39
+
40
+ Find Document Example - how to load a document:
41
+
42
+ require "vuzitruby"
43
+
44
+ Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
45
+ Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
46
+
47
+ doc = Vuzit::Document::find("DOCUMENT_ID")
48
+ puts "Document id: " + doc.id
49
+ puts "Document title: " + doc.title
50
+
51
+ Delete (destroy) Document Example:
52
+
53
+ require "vuzitruby"
54
+
55
+ Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
56
+ Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
57
+
58
+ doc = Vuzit::Document::destroy("DOCUMENT_ID")
59
+
60
+ Upload and View with the JavaScript API Example for a Rails RHTML file:
61
+
62
+ <%
63
+ require "vuzitruby"
64
+ require 'cgi'
65
+
66
+ Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
67
+ Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
68
+
69
+ doc = Vuzit::Document.upload("c:/path/to/document.pdf")
70
+ timestamp = Time.now
71
+ sig = Vuzit::Service.get_signature("show", doc.id, timestamp)
72
+ %>
73
+ <html>
74
+ <head>
75
+ <link href="http://vuzit.com/stylesheets/Vuzit-2.6.css" rel="Stylesheet" type="text/css" />
76
+ <script src="http://vuzit.com/javascripts/Vuzit-2.6.js" type="text/javascript"></script>
77
+ <script type="text/javascript">
78
+ // Called when the page is loaded.
79
+ function initialize() {
80
+ vuzit.Base.PublicKeySet("<%= Vuzit::Service.public_key %>");
81
+ var options = {signature: '<%= CGI.escape(sig) %>',
82
+ timestamp: '<%= timestamp %>', ssl: true}
83
+ var viewer = vuzit.Viewer.fromId("<%= doc.id %>", options);
84
+
85
+ viewer.display(document.getElementById("vuzit_viewer"), { zoom: 1 });
86
+ }
87
+ </script>
88
+ </head>
89
+ <body onload="initialize()">
90
+ <div id="vuzit_viewer" style="width: 650px; height: 500px;"></div>
91
+ </body>
92
+ </html>
93
+
94
+ == LICENSE
95
+
96
+ Released under the MIT license:
97
+
98
+ http://www.opensource.org/licenses/mit-license.php
99
+
100
+ This means you can use it in proprietary products. See LICENSE file.
101
+
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.name = 'vuzitruby'
6
+ s.version = "1.0.0"
7
+ s.homepage = 'http://vuzit.com/'
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.authors = ["Brent Matzelle"]
10
+ s.date = %q{2009-3-30}
11
+ s.description = %q{This is a library for the Vuzit Web Services API. For
12
+ more information on the platform, visit
13
+ http://vuzit.com/developer}
14
+ s.email = %q{support@vuzit.com}
15
+ s.extra_rdoc_files = ["README"]
16
+ s.files = ["README", "Rakefile", "lib/vuzitruby.rb",
17
+ "lib/vuzitruby/document.rb", "lib/vuzitruby/exception.rb",
18
+ "lib/vuzitruby/service.rb"]
19
+ s.has_rdoc = true
20
+ #s.rdoc_options = ["--main", "README"]
21
+ s.require_paths = ["lib"]
22
+ s.rubyforge_project = %q{vuzitruby}
23
+ s.rubygems_version = %q{1.0.0}
24
+ s.summary = %q{Ruby client library for the Vuzit Web Services API}
25
+ s.extra_rdoc_files = ["README"]
26
+ end
27
+
28
+ namespace :github do
29
+ desc "Prepare for GitHub gem packaging"
30
+ task :prepare do
31
+ `rake debug_gem > vuzitruby.gemspec`
32
+ end
33
+ end
34
+
35
+ require 'rake/testtask'
36
+ Rake::TestTask.new(:test) do |test|
37
+ test.libs << 'lib' << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ end
41
+
@@ -0,0 +1,276 @@
1
+ require 'rexml/document'
2
+ require 'uri'
3
+ require 'cgi'
4
+
5
+ class Hash #:nodoc:all
6
+ # Taken from Rails, with appreciation to DHH
7
+ def stringify_keys
8
+ inject({}) do |options, (key, value)|
9
+ options[key.to_s] = value
10
+ options
11
+ end
12
+ end unless method_defined?(:stringify_keys)
13
+ end
14
+
15
+ module Vuzit
16
+
17
+ # Class for uploading, loading, and deleting documents using the Vuzit Web
18
+ # Service API: http://vuzit.com/developer/documents_api
19
+ #
20
+ # To use this class you need to {sign up}[http://vuzit.com/signup] for
21
+ # Vuzit first.
22
+ class Document
23
+ # The document ID
24
+ attr_accessor :id
25
+
26
+ # The document title
27
+ attr_accessor :title
28
+
29
+ # The document subject
30
+ attr_accessor :subject
31
+
32
+ # Number of pages of the document
33
+ attr_accessor :page_count
34
+
35
+ # Page width of the document in pixels
36
+ attr_accessor :page_width
37
+
38
+ # Page height of the document in pixels
39
+ attr_accessor :page_height
40
+
41
+ # File size of the original document bytes
42
+ attr_accessor :file_size
43
+
44
+ TRIES = 3 #:nodoc:
45
+
46
+ # Constructor.
47
+ def initialize #:nodoc:
48
+ # Set the defaults
49
+ @id = @title = @subject = nil
50
+ @page_count = @page_width = @page_height = @file_size = -1
51
+ end
52
+
53
+ # Deletes a document by the ID. Returns true if it succeeded. It throws
54
+ # a Vuzit::Exception on failure. It returns _true_ on success.
55
+ #
56
+ # Example:
57
+ #
58
+ # Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
59
+ # Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
60
+ #
61
+ # result = Vuzit::Document.destroy("DOCUMENT_ID")
62
+ def self.destroy(id)
63
+ timestamp = Time.now
64
+ sig = CGI.escape(Vuzit::Service::get_signature('destroy', id, timestamp))
65
+
66
+ # Create the connection
67
+ uri = URI.parse(Vuzit::Service.service_url)
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+
70
+ query = "/documents/#{id}.xml?key=#{Vuzit::Service.public_key}" +
71
+ "&signature=#{sig}&timestamp=#{timestamp.to_i}"
72
+ request = Net::HTTP::Delete.new(query)
73
+ response = http.start { http.request(request) }
74
+
75
+ if response.code.to_i != 200
76
+ # Some type of error ocurred
77
+ begin
78
+ doc = REXML::Document.new(response.body)
79
+ rescue Exception => ex
80
+ raise Vuzit::Exception.new("XML error: #{ex.message}")
81
+ end
82
+
83
+ if doc.root != nil
84
+ code = doc.root.elements['code']
85
+ if code != nil
86
+ raise Vuzit::Exception.new(doc.root.elements['msg'].text, code.text.to_i);
87
+ end
88
+ end
89
+
90
+ # At this point we don't know what the error is
91
+ raise Vuzit::Exception.new("Unknown error occurred #{response.message}", response.code)
92
+ end
93
+
94
+ debug(response.code + " " + response.message + "\n")
95
+
96
+ return true
97
+ end
98
+
99
+ # Finds a document by the ID. It throws a Vuzit::Exception on failure.
100
+ #
101
+ # Example:
102
+ #
103
+ # Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
104
+ # Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
105
+ #
106
+ # doc = Vuzit::Document.find("DOCUMENT_ID")
107
+ # puts doc.id
108
+ # puts doc.title
109
+ def self.find(id)
110
+ timestamp = Time.now
111
+ sig = CGI.escape(Vuzit::Service::get_signature('show', id, timestamp))
112
+
113
+ # Create the connection
114
+ uri = URI.parse(Vuzit::Service.service_url)
115
+ http = Net::HTTP.new(uri.host, uri.port)
116
+
117
+ query = "/documents/#{id}.xml?key=#{Vuzit::Service.public_key}" +
118
+ "&signature=#{sig}&timestamp=#{timestamp.to_i}"
119
+ request = Net::HTTP::Get.new(query)
120
+ response = http.start { http.request(request) }
121
+
122
+ # TODO: Check if response.code.to_i != 200
123
+
124
+ begin
125
+ doc = REXML::Document.new(response.body)
126
+ rescue Exception => ex
127
+ raise Vuzit::Exception.new("XML error: #{ex.message}")
128
+ end
129
+
130
+ if doc.root == nil
131
+ raise Vuzit::Exception.new("No response from server");
132
+ end
133
+
134
+ debug(response.code + " " + response.message + "\n" + response.body)
135
+
136
+ code = doc.root.elements['code']
137
+ if code != nil
138
+ raise Vuzit::Exception.new(doc.root.elements['msg'].text, code.text.to_i);
139
+ end
140
+
141
+ id = doc.root.elements['web_id']
142
+ if id == nil
143
+ raise Vuzit::Exception.new("Unknown error occurred");
144
+ end
145
+
146
+ result = Vuzit::Document.new
147
+ result.id = id.text
148
+ result.title = doc.root.elements['title'].text
149
+ result.subject = doc.root.elements['subject'].text
150
+ result.page_count = doc.root.elements['page_count'].text.to_i
151
+ result.page_width = doc.root.elements['width'].text.to_i
152
+ result.page_height = doc.root.elements['height'].text.to_i
153
+ result.file_size = doc.root.elements['file_size'].text.to_i
154
+
155
+ return result
156
+ end
157
+
158
+ # Uploads a file to Vuzit. It throws a Vuzit::Exception on failure.
159
+ #
160
+ # Example:
161
+ #
162
+ # Vuzit::Service.public_key = 'YOUR_PUBLIC_API_KEY'
163
+ # Vuzit::Service.private_key = 'YOUR_PRIVATE_API_KEY'
164
+ #
165
+ # doc = Vuzit::Document.upload("c:/path/to/document.pdf", :secure => true)
166
+ # puts doc.id
167
+ def self.upload(file, options = {})
168
+ raise ArgumentError, "Options must be a hash" unless options.kind_of? Hash
169
+
170
+ timestamp = Time.now
171
+ sig = Vuzit::Service::get_signature('create', nil, timestamp)
172
+ # Make a request form
173
+ fields = Hash.new
174
+ fields[:format] = 'xml'
175
+ fields[:key] = Vuzit::Service::public_key
176
+
177
+ if options[:secure] != nil
178
+ fields[:secure] = options[:secure] == true ? '1' : '0'
179
+ else
180
+ fields[:secure] = '1'
181
+ end
182
+
183
+ fields[:signature] = sig
184
+ fields[:timestamp] = timestamp.to_i
185
+ fields[:file_type] = options[:file_type]
186
+ response = nil
187
+
188
+ File.open(file, "rb") do |f|
189
+ fields[:upload] = f
190
+ response = send_request 'create', fields
191
+ end
192
+
193
+ debug(response.code + " " + response.message + "\n" + response.body)
194
+
195
+ # TODO: check the response.code.to_i to make sure it's 201
196
+
197
+ begin
198
+ doc = REXML::Document.new(response.body)
199
+ rescue Exception => ex
200
+ raise Vuzit::Exception.new("XML error: #{ex.message}")
201
+ end
202
+
203
+ if doc.root == nil
204
+ raise Vuzit::Exception.new("No response from server");
205
+ end
206
+
207
+ code = doc.root.elements['code']
208
+ if code != nil
209
+ raise Vuzit::Exception.new(doc.root.elements['msg'].text, code.text.to_i);
210
+ end
211
+
212
+ id = doc.root.elements['web_id']
213
+ if id == nil
214
+ raise Vuzit::Exception.new("Unknown error occurred");
215
+ end
216
+
217
+ result = Vuzit::Document.new
218
+ result.id = id.text
219
+
220
+ return result
221
+ end
222
+
223
+ private
224
+
225
+ # Sends debug messages if activated
226
+ def self.debug(text)
227
+ $stderr.puts(text) if Vuzit::Service::debug == true
228
+ end
229
+
230
+ # Makes a HTTP POST.
231
+ def self.send_request(method, fields = {})
232
+ # See if method is given
233
+ raise ArgumentError, "Method should be given" if method.nil? || method.empty?
234
+
235
+ debug("** Remote method call: #{method}; fields: #{fields.inspect}")
236
+
237
+ # replace pesky hashes to prevent accidents
238
+ fields = fields.stringify_keys
239
+
240
+ # Complete fields with the method name
241
+ fields['method'] = method
242
+
243
+ fields.reject! { |k, v| v.nil? }
244
+
245
+ debug("** POST parameters: #{fields.inspect}")
246
+
247
+ # Create the connection
248
+ uri = URI.parse(Vuzit::Service.service_url)
249
+ http = Net::HTTP.new(uri.host, uri.port)
250
+
251
+ # API methods can be SLOW. Make sure this is set to something big to prevent spurious timeouts
252
+ http.read_timeout = 15 * 60
253
+
254
+ request = Net::HTTP::Post.new('/documents')
255
+ request.multipart_params = fields
256
+
257
+ tries = TRIES
258
+ begin
259
+ tries -= 1
260
+ res = http.request(request)
261
+ rescue Exception
262
+ $stderr.puts "Request encountered error, will retry #{tries} more."
263
+ if tries > 0
264
+ # Retrying several times with sleeps is recommended.
265
+ # This will prevent temporary downtimes at Scribd from breaking API applications
266
+ sleep(20)
267
+ retry
268
+ end
269
+ raise $!
270
+ end
271
+ return res
272
+ end
273
+
274
+ end
275
+
276
+ end
@@ -0,0 +1,33 @@
1
+
2
+ module Vuzit
3
+ # Vuzit library exception handler class.
4
+ class Exception < StandardError
5
+ # The web service error message
6
+ attr_reader :message
7
+
8
+ # The web service error code
9
+ attr_reader :code
10
+
11
+ # Constructor for errors.
12
+ #
13
+ # Example:
14
+ #
15
+ # begin
16
+ # doc = Vuzit::Document.find("DOCUMENT_ID")
17
+ # rescue Vuzit::Exception => ex
18
+ # puts "Error code: #{ex.code}, message: #{ex.message}"
19
+ # end
20
+ def initialize(message, code = 0)
21
+ @message = message
22
+ @code = code
23
+ end
24
+
25
+ # Returns the string representation of the error in this format:
26
+ #
27
+ # Vuzit::Exception: [CODE]: MESSAGE
28
+ def to_s
29
+ return "Vuzit::Exception: [#{@code}]: #{@message}";
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,149 @@
1
+
2
+ require 'net/https'
3
+ require "rubygems"
4
+ require "mime/types" # Requires gem install mime-types
5
+ require "base64"
6
+ require 'cgi'
7
+ require 'md5'
8
+
9
+ module Net #:nodoc:all
10
+ # Enhances the HTTP::Post class for multi-part post transactions
11
+ class HTTP::Post
12
+ def multipart_params=(param_hash={})
13
+ boundary_token = [Array.new(8) {rand(256)}].join
14
+ self.content_type = "multipart/form-data; boundary=#{boundary_token}"
15
+ boundary_marker = "--#{boundary_token}\r\n"
16
+ self.body = param_hash.map { |param_name, param_value|
17
+ boundary_marker + case param_value
18
+ when File
19
+ file_to_multipart(param_name, param_value)
20
+ else
21
+ text_to_multipart(param_name, param_value.to_s)
22
+ end
23
+ }.join('') + "--#{boundary_token}--\r\n"
24
+ end
25
+
26
+ protected
27
+ def file_to_multipart(key,file)
28
+ filename = File.basename(file.path)
29
+ mime_types = MIME::Types.of(filename)
30
+ mime_type = mime_types.empty? ? "application/octet-stream" : mime_types.first.content_type
31
+ part = %Q|Content-Disposition: form-data; name="#{key}"; filename="#{filename}"\r\n|
32
+ part += "Content-Transfer-Encoding: binary\r\n"
33
+ part += "Content-Type: #{mime_type}\r\n\r\n#{file.read}\r\n"
34
+ end
35
+
36
+ def text_to_multipart(key,value)
37
+ "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n#{value}\r\n"
38
+ end
39
+ end
40
+ end
41
+
42
+ module Vuzit
43
+
44
+ # Global data class.
45
+ class Service
46
+ @@public_key = nil
47
+ @@private_key = nil
48
+ @@service_url = 'http://vuzit.com'
49
+ @@debug = false
50
+
51
+ # TODO: For all of the set variables do not allow nil values
52
+
53
+ # Sets the Vuzit public API key
54
+ def self.public_key=(value)
55
+ @@public_key = value
56
+ end
57
+
58
+ # Returns the Vuzit public API key
59
+ def self.public_key
60
+ @@public_key
61
+ end
62
+
63
+ # Sets Vuzit private API key. Do NOT share this with anyone!
64
+ def self.private_key=(value)
65
+ @@private_key = value
66
+ end
67
+
68
+ # Returns the Vuzit private API key. Do NOT share this with anyone!
69
+ def self.private_key
70
+ @@private_key
71
+ end
72
+
73
+ # Sets the URL of the Vuzit web service. This only needs to be changed if
74
+ # you are running Vuzit Enterprise on your own server.
75
+ # The default value is "http://vuzit.com"
76
+ def self.service_url=(value)
77
+ @@service_url = value
78
+ end
79
+
80
+ # Returns the URL of the Vuzit web service.
81
+ def self.service_url
82
+ @@service_url
83
+ end
84
+
85
+ # Switch this to _true_ if you would like to see debug messages in the
86
+ # output.
87
+ def self.debug=(value)
88
+ @@debug = value
89
+ end
90
+
91
+ # Returns whether debugging is turned on or off.
92
+ def self.debug
93
+ @@debug
94
+ end
95
+
96
+ # Returns The signature string. NOTE: If you are going to use this
97
+ # with the Vuzit Javascript API then the value must be encoded with the
98
+ # CGI.escape function. See the Wiki example for more information:
99
+ #
100
+ # http://wiki.github.com/vuzit/vuzitruby/code-samples
101
+ #
102
+ # Example:
103
+ #
104
+ # timestamp = Time.now
105
+ # sig = Vuzit::Service.get_signature("show", "DOCUMENT_ID", timestamp)
106
+ def self.get_signature(service, id = '', time = nil)
107
+ if Vuzit::Service.public_key == nil || Vuzit::Service.private_key == nil
108
+ raise Vuzit::Exception.new("The public_key or private_key variables are nil")
109
+ end
110
+ time = (time == nil) ? Time.now.to_i : time.to_i
111
+
112
+ if @@public_key == nil
113
+ raise Vuzit::Exception.new("Vuzit::Service.public_key not set")
114
+ end
115
+
116
+ if @@private_key == nil
117
+ raise Vuzit::Exception.new("Vuzit::Service.private_key not set")
118
+ end
119
+
120
+ msg = "#{service}#{id}#{@@public_key}#{time}"
121
+ hmac = hmac_sha1(@@private_key, msg)
122
+ result = Base64::encode64(hmac).chomp
123
+
124
+ return result
125
+ end
126
+
127
+ private
128
+
129
+ # Creates the SHA1 key.
130
+ def self.hmac_sha1(key, s)
131
+ ipad = [].fill(0x36, 0, 64)
132
+ opad = [].fill(0x5C, 0, 64)
133
+ key = key.unpack("C*")
134
+ key += [].fill(0, 0, 64-key.length) if key.length < 64
135
+
136
+ inner = []
137
+ 64.times { |i| inner.push(key[i] ^ ipad[i]) }
138
+ inner += s.unpack("C*")
139
+
140
+ outer = []
141
+ 64.times { |i| outer.push(key[i] ^ opad[i]) }
142
+ outer = outer.pack("c*")
143
+ outer += Digest::SHA1.digest(inner.pack("c*"))
144
+
145
+ return Digest::SHA1.digest(outer)
146
+ end
147
+ end
148
+ end
149
+
data/lib/vuzitruby.rb ADDED
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ # This prevents the "require 'some_gem'" error
4
+ require 'rubygems'
5
+
6
+ require 'vuzitruby/service'
7
+ require 'vuzitruby/document'
8
+ require 'vuzitruby/exception'
9
+
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vuzitruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brent Matzelle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-30 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: This is a library for the Vuzit Web Services API. For more information on the platform, visit http://vuzit.com/developer
17
+ email: support@vuzit.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - README
26
+ - Rakefile
27
+ - lib/vuzitruby.rb
28
+ - lib/vuzitruby/document.rb
29
+ - lib/vuzitruby/exception.rb
30
+ - lib/vuzitruby/service.rb
31
+ has_rdoc: true
32
+ homepage: http://vuzit.com/
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --main
36
+ - README
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project: vuzitruby
54
+ rubygems_version: 1.3.1
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: Ruby client library for the Vuzit Web Services API
58
+ test_files: []
59
+