xoopit-cloudquery 0.1.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 nb.io
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,73 @@
1
+ cloudquery
2
+ ==========
3
+
4
+ Client for Xoopit's cloudquery API
5
+
6
+ Install
7
+ -------
8
+
9
+ sudo gem install xoopit-cloudquery-ruby
10
+
11
+ Simple contacts application example
12
+ -----------------------------------
13
+
14
+ > require 'cloudquery'
15
+ => true
16
+ > include Cloudquery
17
+ => Object
18
+ > secret = Client.get_secret(<account_name>, <password>)
19
+ => "your secret appears here"
20
+ > c = Client.new(:account => '<account_name>', :secret => secret)
21
+ => #<Cloudquery::Client:0x10b1b24 @secure=true, @secret="your secret appears here", @account="<account_name>", @document_id_method=nil>
22
+ > c.add_indexes('superheroes')
23
+ => {"result"=>["kMzzzybpqpY"], "size"=>1, "STATUS"=>200}
24
+ > c.add_schema(File.open('simple.contact.xml'))
25
+ => {"result"=>["ubKme0EX3H2ud7VhBU7qngk3........."], "size"=>1, "STATUS"=>201}
26
+ > doc = {
27
+ 'simple.contact.name' => 'Steve Rogers',
28
+ 'simple.contact.email' => ['steve.rogers@example.com','captain.america@marvel.com'],
29
+ 'simple.contact.telephone' => ['555-555-5555','123-456-6789'],
30
+ 'simple.contact.address' => ['Lower East Side, NY NY'],
31
+ 'simple.contact.birthday' => Date.parse('July 4, 1917'),
32
+ 'simple.contact.note' => 'Captain America!',
33
+ }
34
+ => {"simple.contact.birthday"=>#<Date: 4842827/2,0,2299161>, "simple.contact.address"=>["Lower East Side, NY NY"], "simple.contact.telephone"=>["555-555-5555", "123-456-6789"], "simple.contact.note"=>"Captain America!", "simple.contact.email"=>["steve.rogers@example.com", "captain.america@marvel.com"], "simple.contact.name"=>"Steve Rogers"}
35
+ > c.add_documents('superheroes', doc, 'simple.contact')
36
+ => {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "size"=>1, "STATUS"=>201}
37
+ > docs = [
38
+ {
39
+ 'simple.contact.name' => 'Clark Kent',
40
+ 'simple.contact.email' => ['clark.kent@example.com','superman@dc.com'],
41
+ 'simple.contact.telephone' => ['555-123-1234','555-456-6789'],
42
+ 'simple.contact.address' => ['344 Clinton St., Apt. #3B, Metropolis', 'The Fortess of Solitude, North Pole'],
43
+ 'simple.contact.birthday' => Date.parse('June 18, 1938'),
44
+ 'simple.contact.note' => 'Superhuman strength, speed, stamina, durability, senses, intelligence, regeneration, and longevity; super breath, heat vision, x-ray vision and flight. Member of the justice league.'
45
+ },
46
+ {
47
+ 'simple.contact.name' => 'Bruce Wayne',
48
+ 'simple.contact.email' => ['bruce.wayne@example.com','batman@dc.com'],
49
+ 'simple.contact.telephone' => ['555-123-6666','555-456-6666'],
50
+ 'simple.contact.address' => ['1007 Mountain Drive, Gotham', 'The Batcave, Gotham'],
51
+ 'simple.contact.birthday' => Date.parse('February 19, 1939'),
52
+ 'simple.contact.note' => 'Sidekick is Robin. Has problems with the Joker. Member of e justice league.'
53
+ }
54
+ ]
55
+ > c.add_documents('superheroes', docs, 'simple.contact')
56
+ => {"result"=>["lQgByVSvJk1skHtKpMYX40kMzzzybpqpY", "weJF4uDPJrlvrETTJQNibFkMzzzybpqpY"], "size"=>2, "STATUS"=>201}
57
+ > c.count_documents('superheroes', '*', 'simple.contact')
58
+ => {"result"=>3, "matches"=>3, "STATUS"=>200}
59
+ > c.get_documents('superheroes', '*', {:fields => 'simple.contact.name'}, 'simple.contact')
60
+ => {"result"=>[{"simple.contact.name"=>"Steve Rogers"}, {"simple.contact.name"=>"Clark Kent"}, {"simple.contact.name"=>"Bruce Wayne"}], "matches"=>3, "size"=>3, "STATUS"=>200}
61
+ > c.get_documents('superheroes', 'name:Steve', {:fields => 'simple.contact.name'}, 'simple.contact')
62
+ => {"result"=>[{"simple.contact.name"=>"Steve Rogers"}], "matches"=>1, "size"=>1, "STATUS"=>200}
63
+ > c.get_documents('superheroes', ':@:justice', {:fields => 'simple.contact.name'}, 'simple.contact')
64
+ => {"result"=>[{"simple.contact.name"=>"Clark Kent"}, {"simple.contact.name"=>"Bruce Wayne"}], "matches"=>2, "size"=>2, "STATUS"=>200}
65
+ > c.modify_documents('superheroes', 'name:steve', {'simple.contact.note' => 'His name is STEVE!'}, 'simple.contact')
66
+ => {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "matches"=>1, "size"=>1, "STATUS"=>200}
67
+ > c.delete_documents('superheroes', 'name:steve', 'simple.contact') => {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "matches"=>2, "size"=>1, "STATUS"=>200}
68
+
69
+
70
+ Copyright
71
+ ---------
72
+
73
+ Copyright (c) 2009 nb.io, LLC and Xoopit, Inc. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,75 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ gem.name = "cloudquery"
7
+ gem.summary = "Client for Xoopit's cloudquery API"
8
+ gem.email = "us@nb.io"
9
+ gem.homepage = "http://github.com/nbio/cloudquery"
10
+ gem.description = "Client for Xoopit's cloudquery API"
11
+ gem.authors = ["Cameron Walters", "nb.io"]
12
+ gem.files = FileList["[A-Z]*", "{lib,spec}/**/*"]
13
+ # gem.rubyforge_project = "cloudquery"
14
+
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+
34
+ task :default => :spec
35
+
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask.new do |rdoc|
38
+ if File.exist?('VERSION.yml')
39
+ config = YAML.load(File.read('VERSION.yml'))
40
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
41
+ else
42
+ version = ""
43
+ end
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "cloudquery #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
50
+
51
+ # begin
52
+ # require 'rake/contrib/sshpublisher'
53
+ # namespace :rubyforge do
54
+ #
55
+ # desc "Release gem and RDoc documentation to RubyForge"
56
+ # task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
57
+ #
58
+ # namespace :release do
59
+ # desc "Publish RDoc to RubyForge."
60
+ # task :docs => [:rdoc] do
61
+ # config = YAML.load(
62
+ # File.read(File.expand_path('~/.rubyforge/user-config.yml'))
63
+ # )
64
+ #
65
+ # host = "#{config['username']}@rubyforge.org"
66
+ # remote_dir = "/var/www/gforge-projects/cloudquery/"
67
+ # local_dir = 'rdoc'
68
+ #
69
+ # Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
70
+ # end
71
+ # end
72
+ # end
73
+ # rescue LoadError
74
+ # puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
75
+ # end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
data/lib/cloudquery.rb ADDED
@@ -0,0 +1,453 @@
1
+ require "rubygems"
2
+ require "uri"
3
+ require "digest/sha1"
4
+ require "base64"
5
+ require "rack/utils"
6
+ require "curl"
7
+ require "json"
8
+
9
+ module Cloudquery
10
+ SCHEME = "https".freeze
11
+ HOST = "api.xoopit.com".freeze
12
+ PATH = "/v0".freeze
13
+
14
+ API_PATHS = {
15
+ :account => "account".freeze,
16
+ :schema => "schema".freeze,
17
+ :indexes => "i".freeze,
18
+ :documents => "i".freeze,
19
+ }.freeze
20
+
21
+ # Standard Content-Types for requests
22
+ CONTENT_TYPES = {
23
+ :json => 'application/json;charset=utf-8'.freeze,
24
+ :form => 'application/x-www-form-urlencoded'.freeze,
25
+ :xml => 'application/xml;charset=utf-8'.freeze,
26
+ }.freeze
27
+
28
+
29
+ SIGNING_METHOD = "SHA1".freeze
30
+ COOKIE_JAR = ".cookies.lwp".freeze
31
+
32
+ class Request
33
+ attr_accessor :method, :headers, :scheme, :host, :port, :path, :params, :body
34
+
35
+ def initialize(options={})
36
+ @method = options[:method] || 'POST'
37
+ @headers = options[:headers] || {}
38
+ @scheme = options[:scheme] || SCHEME
39
+ @host = options[:host] || HOST
40
+ @port = options[:port] || (@scheme == 'https' ? URI::HTTPS::DEFAULT_PORT : URI::HTTP::DEFAULT_PORT)
41
+ @path = options[:path] || PATH
42
+ @params = options[:params] || {}
43
+ if ['PUT', 'DELETE'].include?(@method)
44
+ @params['_method'] = @method
45
+ @method = 'POST'
46
+ end
47
+ @body = options[:body]
48
+
49
+ @account = options[:account]
50
+ @secret = options[:secret]
51
+ end
52
+
53
+ def request_uri(account=@account, secret=@secret)
54
+ query = query_str(signature_params(account))
55
+ uri = if query.empty?
56
+ @path.dup
57
+ else
58
+ "#{@path}?#{query}"
59
+ end
60
+ uri = append_signature(uri, secret) if secret
61
+ uri
62
+ end
63
+
64
+ def url(account=@account, secret=@secret)
65
+ base_uri.merge(request_uri(account, secret)).to_s
66
+ end
67
+
68
+ private
69
+ def append_signature(uri, secret)
70
+ sig = Crypto::URLSafeSHA1.sign(secret, uri)
71
+ x_sig = Rack::Utils.build_query("x_sig" => sig)
72
+ "#{uri}&#{x_sig}"
73
+ end
74
+
75
+ def signature_params(account=@account)
76
+ return {} unless account
77
+ {
78
+ 'x_name' => account,
79
+ 'x_time' => Time.now.to_i_with_milliseconds,
80
+ 'x_nonce' => Cloudquery::Crypto::Random.nonce,
81
+ 'x_method' => SIGNING_METHOD,
82
+ }
83
+ end
84
+
85
+ def query_str(additional_params={})
86
+ Rack::Utils.build_query(@params.dup.merge(additional_params))
87
+ end
88
+
89
+ def base_uri
90
+ uri_class = (@scheme == 'https' ? URI::HTTPS : URI::HTTP)
91
+ uri_class.build(:scheme => @scheme, :host => @host, :port => @port)
92
+ end
93
+
94
+ end
95
+
96
+ module Crypto
97
+ module Random
98
+ extend self
99
+
100
+ SecureRandom = (defined?(::SecureRandom) && ::SecureRandom) || (defined?(::ActiveSupport::SecureRandom) && ::ActiveSupport::SecureRandom)
101
+ if SecureRandom
102
+ def nonce
103
+ "#{SecureRandom.random_number}.#{Time.now.to_i}"[2..-1]
104
+ end
105
+ else
106
+ def nonce
107
+ "#{rand.to_s}.#{Time.now.to_i}"[2..-1]
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ module URLSafeSHA1
114
+ extend self
115
+
116
+ def sign(*tokens)
117
+ tokens = tokens.flatten
118
+ digest = Digest::SHA1.digest(tokens.join)
119
+ Base64.encode64(digest).chomp.tr('+/', '-_')
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ class Client
126
+ attr_reader :account
127
+ attr_writer :secret
128
+
129
+ # Create a new instance of the client
130
+ # +options = {}+ Acceptable options:
131
+ # +:account+ => <account name> (default => nil)
132
+ # +:secret+ => <API secret> (default => nil)
133
+ #
134
+ # +:document_id_method+ => <method name> (default => nil)
135
+ # will call +:document_id_method+ during +add_documents+
136
+ # and +update_documents+ which should inject an +'#.id'+
137
+ # key-value pair as a simple way to tie app PKs to doc ids.
138
+ #
139
+ # +:secure+ => Boolean (default => true, uses HTTPS)
140
+ # +:secure => false+ will use HTTP
141
+ def initialize(options={})
142
+ # unless options[:account] && options[:secret]
143
+ # raise "Client requires :account => <account name> and :secret => <secret>"
144
+ # end
145
+
146
+ @account = options[:account]
147
+ @secret = options[:secret]
148
+
149
+ @secure = options[:secure] != false # must pass false for insecure
150
+
151
+ @document_id_method = options[:document_id_method]
152
+ end
153
+
154
+
155
+ ## Account management
156
+
157
+ # Retrieve the API secret for an account, using the password (uses HTTPS)
158
+ def self.get_secret(account, password)
159
+ auth = Request.new(:path => "#{PATH}/auth")
160
+ curl = Curl::Easy.new(auth.url) do |c|
161
+ c.enable_cookies = true
162
+ c.cookiejar = COOKIE_JAR
163
+ end
164
+ params = Rack::Utils.build_query({"name" => account, "password" => password})
165
+ curl.http_post(params)
166
+
167
+ if curl.response_code == 200
168
+ curl.url = Request.new(:path => "#{PATH}/#{API_PATHS[:account]}/#{account}").url
169
+ curl.http_get
170
+ response = JSON.parse(curl.body_str)
171
+ response['result']['secret']
172
+ else
173
+ STDERR.puts "Error: #{curl.response_code} #{Rack::Utils::HTTP_STATUS_CODES[curl.response_code]}"
174
+ end
175
+ end
176
+
177
+ # Get the account document
178
+ def get_account
179
+ send_request get(account_path)
180
+ end
181
+
182
+ # Update the account document.
183
+ # For example, you can use this method to change the API secret:
184
+ # update_account({'secret' => 'your-new-secret'})
185
+ def update_account(account_doc={})
186
+ body = JSON.generate(account_doc)
187
+ send_request put(account_path, body)
188
+ end
189
+
190
+ # Delete the account. BEWARE: THIS WILL ACTUALLY DELETE YOUR ACCOUNT.
191
+ def delete_account
192
+ send_request delete(account_path)
193
+ end
194
+
195
+
196
+ ## Schema management
197
+
198
+ # Add a schema to the account. xml can be a String
199
+ # or File-like (responds to read)
200
+ def add_schema(xml)
201
+ body = xml.respond_to?(:read) ? xml.read : xml
202
+ request = post(build_path(API_PATHS[:schema]), body)
203
+ send_request(request, CONTENT_TYPES[:xml])
204
+ end
205
+
206
+ # Delete a schema from the account, by name
207
+ def delete_schema(schema_name)
208
+ send_request delete(build_path(
209
+ API_PATHS[:schema],
210
+ Rack::Utils.escape("xfs.schema.name:\"#{schema_name}\"")
211
+ ))
212
+ end
213
+
214
+ # Get the schemas for the account.
215
+ # NOTE: returned format is not the same as accepted for input
216
+ def get_schemas
217
+ send_request get(build_path(API_PATHS[:schema]))
218
+ end
219
+
220
+
221
+ ## Index management
222
+
223
+ # Add one or more indexes to the account, by name or id
224
+ def add_indexes(*indexes)
225
+ body = JSON.generate(indexes.flatten)
226
+ send_request post(build_path(API_PATHS[:indexes]), body)
227
+ end
228
+
229
+ # Delete one or more indexes from the account, by name or id
230
+ # +indexes = '*'+ will delete all indexes
231
+ def delete_indexes(*indexes)
232
+ indexes = url_pipe_join(indexes)
233
+ send_request delete(build_path(API_PATHS[:indexes], indexes))
234
+ end
235
+
236
+ # Get the indexes from the account. Returns a list of ids
237
+ def get_indexes
238
+ send_request get(build_path(API_PATHS[:indexes]))
239
+ end
240
+
241
+
242
+ ## Document management
243
+
244
+ # Add documents to the specified +index+
245
+ # +index = name or id+, +docs = {}+ or Array of {}.
246
+ #
247
+ # Documents with key +'#.id'+ and an existing value will be updated.
248
+ #
249
+ # If +schemas+ is not nil, ensures existence of the
250
+ # specified schemas on each document.
251
+ def add_documents(index, docs, *schemas)
252
+ request = post(
253
+ build_path(API_PATHS[:documents], index, url_pipe_join(schemas)),
254
+ JSON.generate(identify_documents(docs))
255
+ )
256
+ send_request request
257
+ end
258
+
259
+ # Update documents in the specified +index+
260
+ # +index = name or id+, +docs = {}+ or Array of {}.
261
+ #
262
+ # Documents lacking the key +'#.id'+ will be created.
263
+ #
264
+ # If +schemas+ is not nil, ensures existence of the
265
+ # specified schemas on each document.
266
+ def update_documents(index, docs, *schemas)
267
+ request = put(
268
+ build_path(API_PATHS[:documents], index, url_pipe_join(schemas)),
269
+ JSON.generate(identify_documents(docs))
270
+ )
271
+ send_request request
272
+ end
273
+
274
+ # Modify documents in the +index+ matching +query+
275
+ # +modifications = {}+ to update all matching
276
+ # documents.
277
+ #
278
+ # If +schemas+ is not nil, ensures existence of the
279
+ # specified schemas on each document.
280
+ def modify_documents(index, query, modifications, *schemas)
281
+ request = put(
282
+ build_path(API_PATHS[:documents], index, url_pipe_join(schemas), Rack::Utils.escape(query)),
283
+ JSON.generate(modifications)
284
+ )
285
+ send_request request
286
+ end
287
+
288
+ # Delete documents in the +index+ matching +query+
289
+ #
290
+ # +query+ defaults to +'*'+
291
+ # BEWARE: If +query = nil+ this will delete ALL documents in +index+.
292
+ #
293
+ # +index+ may be an id, index name, or Array of ids or names.
294
+ # Operates on all indexes if +index = nil+ or +'*'+
295
+ #
296
+ # If +schemas+ is not nil, ensures existence of the
297
+ # specified schemas on each document.
298
+ def delete_documents(index, query, *schemas)
299
+ request = delete(
300
+ build_path(API_PATHS[:documents],
301
+ url_pipe_join(index),
302
+ url_pipe_join(schemas),
303
+ Rack::Utils.escape(query)
304
+ )
305
+ )
306
+ send_request request
307
+ end
308
+
309
+ # Get documents matching +query+
310
+ #
311
+ # +query+ defaults to +'*'+
312
+ # +index+ may be an id, index name, or Array of ids or names.
313
+ # Operates on all indexes if +index = nil+ or +'*'+
314
+ #
315
+ # +options = {}+ Acceptable options:
316
+ # +:fields+ => a field name, a prefix match (e.g. +'trans*'+), or a list thereof (default => +'*'+)
317
+ # +:sort+ => a string ("[+|-]schema.field"), or a list thereof (default => +'+#.number'+)
318
+ # +:offset+ => integer offset into the result set (default => +0+)
319
+ # +:limit+ => integer limit on number of documents returned per index (default => <no limit>)
320
+ #
321
+ # If +schemas+ is not nil, ensures existence of the
322
+ # specified schemas on each document.
323
+ def get_documents(index, query, options={}, *schemas)
324
+ if fields = options.delete(:fields)
325
+ fields = url_pipe_join(fields)
326
+ end
327
+
328
+ if options[:sort]
329
+ options[:sort] = Array(options[:sort]).flatten.join(',')
330
+ end
331
+
332
+ request = get(
333
+ build_path(API_PATHS[:documents],
334
+ url_pipe_join(index),
335
+ url_pipe_join(schemas),
336
+ url_pipe_join(query),
337
+ fields
338
+ ),
339
+ options
340
+ )
341
+ send_request request
342
+ end
343
+
344
+ # Count documents matching +query+
345
+ #
346
+ # +query+ defaults to +'*'+
347
+ # +index+ may be an id, index name, or Array of ids or names.
348
+ # Operates on all indexes if +index = nil+ or +'*'+
349
+ #
350
+ # If +schemas+ is not nil, ensures existence of the
351
+ # specified schemas on each document.
352
+ def count_documents(index, query, *schemas)
353
+ get_documents(index, query, {:fields => '@count'}, *schemas)
354
+ end
355
+
356
+ private
357
+ def build_path(*path_elements)
358
+ path_elements.flatten.compact.unshift(PATH).join('/')
359
+ end
360
+
361
+ def account_path
362
+ build_path(API_PATHS[:account], @account)
363
+ end
364
+
365
+ def build_request(options={})
366
+ Request.new default_request_params.merge(options)
367
+ end
368
+
369
+ def get(path, params={})
370
+ build_request(:method => 'GET', :path => path, :params => params)
371
+ end
372
+
373
+ def delete(path, params={})
374
+ build_request(:method => 'DELETE', :path => path, :params => params)
375
+ end
376
+
377
+ def post(path, doc, params={})
378
+ build_request(:method => 'POST', :path => path, :body => doc, :params => params)
379
+ end
380
+
381
+ def put(path, doc, params={})
382
+ build_request(:method => 'PUT', :path => path, :body => doc, :params => params)
383
+ end
384
+
385
+ def default_request_params
386
+ {
387
+ :account => @account,
388
+ :secret => @secret,
389
+ :scheme => @secure ? 'https' : 'http',
390
+ }
391
+ end
392
+
393
+ def send_request(request, content_type=nil)
394
+ response = execute_request(request.method, request.url, request.headers, request.body, content_type)
395
+ status_code = response.first
396
+ if (200..299).include?(status_code)
397
+ begin
398
+ result = JSON.parse(response.last)
399
+ rescue JSON::ParserError => e
400
+ result = {"REASON" => e.message}
401
+ end
402
+ else
403
+ result = {"REASON" => "Error: #{status_code} #{Rack::Utils::HTTP_STATUS_CODES[status_code]}"}
404
+ end
405
+ result.merge!({'STATUS' => status_code})
406
+ end
407
+
408
+ def execute_request(method, url, headers, body, content_type=nil)
409
+ content_type ||= CONTENT_TYPES[:json]
410
+ curl = Curl::Easy.new(url) do |c|
411
+ c.headers = headers
412
+ c.headers['Content-Type'] = content_type
413
+ c.encoding = 'gzip'
414
+ end
415
+ case method
416
+ when 'GET'
417
+ curl.http_get
418
+ when 'DELETE'
419
+ curl.http_delete
420
+ when 'POST'
421
+ curl.http_post(body)
422
+ when 'PUT'
423
+ curl.http_put(body)
424
+ end
425
+
426
+ [curl.response_code, curl.header_str, curl.body_str]
427
+ end
428
+
429
+ def url_pipe_join(arr, default_value='*')
430
+ arr = Array(arr).flatten
431
+ if arr.empty?
432
+ default_value
433
+ else
434
+ Rack::Utils.escape(arr.join('|'))
435
+ end
436
+ end
437
+
438
+ def identify_documents(docs)
439
+ [docs] if docs.is_a?(Hash)
440
+ if @document_id_method
441
+ docs.each { |d| d.send(@document_id_method) }
442
+ end
443
+ docs
444
+ end
445
+ end
446
+ end
447
+
448
+
449
+ class Time
450
+ def to_i_with_milliseconds
451
+ (to_f * 1000).to_i
452
+ end
453
+ end
@@ -0,0 +1,437 @@
1
+ require 'spec_helper'
2
+
3
+ if ENV["TEST_REAL_HTTP"]
4
+ # Create a config.yml file containing the following:
5
+ # :account: <your account name>
6
+ # :secret: <your secret>
7
+ # then run the specs with TEST_REAL_HTTP=true
8
+ describe "CloudQuery account" do
9
+ before(:each) do
10
+ @config = YAML.load(File.read('config.yml'))
11
+ @client = Cloudquery::Client.new(@config)
12
+ end
13
+
14
+ it "gets your account information from the server" do
15
+ response = @client.get_account
16
+ response['STATUS'].should be_between(200, 299)
17
+
18
+ account = response["result"]
19
+ account["secret"].should == @config[:secret]
20
+
21
+ account.should have_key("name")
22
+ account["name"].should == @config[:account]
23
+
24
+ account.should have_key("preferences")
25
+ end
26
+
27
+ it "updates your account on the server" do
28
+ account = @client.get_account["result"]
29
+ response = @client.update_account(account)
30
+ response['STATUS'].should be_between(200, 299)
31
+ end
32
+
33
+ it "adds a schema to your account on the server" do
34
+ response = @client.add_schema(File.open('spec/example_schema.xml'))
35
+ response['STATUS'].should be_between(200, 299)
36
+ end
37
+
38
+ it "gets the schemas for your account from the server" do
39
+ response = @client.get_schemas
40
+ response['STATUS'].should be_between(200, 299)
41
+ response['result'].should be_an_instance_of(Array)
42
+ response['result'].should have_at_least(1).item
43
+ end
44
+
45
+ it "deletes a schema from your account on the server" do
46
+ response = @client.delete_schema("spec.example")
47
+ response['STATUS'].should be_between(200, 299)
48
+ end
49
+
50
+ it "adds a single index to your account on the server" do
51
+ response = @client.add_indexes('spec_index')
52
+ response['STATUS'].should be_between(200, 299)
53
+ response['result'].should be_an_instance_of(Array)
54
+ response['result'].should have(1).item
55
+ end
56
+
57
+ it "adds multiple indexes to your account on the server" do
58
+ response = @client.add_indexes %w( spec_index_1 spec_index_2 spec_index_3 )
59
+ response['STATUS'].should be_between(200, 299)
60
+ response['result'].should be_an_instance_of(Array)
61
+ response['result'].should have(3).items
62
+ end
63
+
64
+ it "gets the indexes for your account from the server" do
65
+ response = @client.get_indexes
66
+ response['STATUS'].should be_between(200, 299)
67
+ response['result'].should be_an_instance_of(Array)
68
+ response['result'].should have_at_least(4).items
69
+ end
70
+
71
+ it "deletes a single index from your account on the server" do
72
+ response = @client.delete_indexes('spec_index')
73
+ response['STATUS'].should be_between(200, 299)
74
+ response['result'].should be_an_instance_of(Array)
75
+ response['result'].should have(1).item
76
+ end
77
+
78
+ it "deletes multiple indexes from your account on the server" do
79
+ response = @client.delete_indexes %w( spec_index_1 spec_index_2 spec_index_3 )
80
+ response['STATUS'].should be_between(200, 299)
81
+ response['result'].should be_an_instance_of(Array)
82
+ response['result'].should have(3).items
83
+ end
84
+
85
+ describe "document support" do
86
+ def valid_document
87
+ {
88
+ 'spec.example.name' => 'Steve Rogers',
89
+ 'spec.example.email' => ['steve.rogers@example.com','captain.america@marvel.com'],
90
+ 'spec.example.telephone' => ['555-555-5555','123-456-6789'],
91
+ 'spec.example.address' => ['Lower East Side, NY NY'],
92
+ 'spec.example.birthday' => ParseDate.parsedate('July 4, 1917'),
93
+ 'spec.example.note' => 'Captain America!',
94
+ }
95
+ end
96
+
97
+ def add_valid_document(index=nil)
98
+ index ||= 'spec_index'
99
+ response = @client.add_documents(index, valid_document, 'spec.example')
100
+ response['result'].first
101
+ end
102
+
103
+ before(:each) do
104
+ @client.add_indexes('spec_index')
105
+ @client.add_schema(File.open('spec/example_schema.xml'))
106
+ end
107
+
108
+ after(:each) do
109
+ @client.delete_schema("spec.example")
110
+ @client.delete_indexes('spec_index')
111
+ end
112
+
113
+ it "adds a document to an index on the server" do
114
+ response = @client.add_documents('spec_index', valid_document, 'spec.example')
115
+ response['STATUS'].should == 201
116
+ response['result'].should have(1).item
117
+ end
118
+
119
+ it "adds multiple documents to an index on the server" do
120
+ documents = [
121
+ valid_document,
122
+ {
123
+ 'spec.example.name' => 'Clark Kent',
124
+ 'spec.example.email' => ['clark.kent@example.com','superman@dc.com'],
125
+ 'spec.example.telephone' => ['555-123-1234', '555-456-6789'],
126
+ 'spec.example.address' =>
127
+ ['344 Clinton St., Apt. #3B, Metropolis', 'The Fortess of Solitude, North Pole'],
128
+ 'spec.example.birthday' => ParseDate.parsedate('June 18, 1938'),
129
+ 'spec.example.note' =>
130
+ 'Superhuman strength, speed, stamina, durability, senses, intelligence, regeneration, and longevity; super breath, heat vision, x-ray vision and flight. Member of the justice league.',
131
+ },
132
+ {
133
+ 'spec.example.name' => 'Bruce Wayne',
134
+ 'spec.example.email' => ['bruce.wayne@example.com','batman@dc.com'],
135
+ 'spec.example.telephone' => ['555-123-6666', '555-456-6666'],
136
+ 'spec.example.address' =>
137
+ ['1007 Mountain Drive, Gotham', 'The Batcave, Gotham'],
138
+ 'spec.example.birthday' => ParseDate.parsedate('February 19, 1939'),
139
+ 'spec.example.note' =>
140
+ 'Sidekick is Robin. Has problems with the Joker. Member of the justice league.',
141
+ },
142
+ ]
143
+
144
+ response = @client.add_documents('spec_index', documents, 'spec.example')
145
+ response['STATUS'].should == 201
146
+ response['result'].should have(3).items
147
+ end
148
+
149
+ it "updates a document on the server" do
150
+ doc = valid_document
151
+ doc['#.#'] = add_valid_document
152
+ doc['spec.example.note'] = "Document modified!"
153
+
154
+ response = @client.update_documents('spec_index', doc, 'spec.example')
155
+ response['STATUS'].should == 200
156
+ response['result'].should have(1).item
157
+ end
158
+
159
+ it "modifies documents on the server" do
160
+ add_valid_document
161
+ mods = {'spec.example.note' => 'Document modified!'}
162
+ response = @client.modify_documents(
163
+ "spec_index",
164
+ "name:#{valid_document['spec.example.name']}",
165
+ mods,
166
+ "spec.example"
167
+ )
168
+ response['STATUS'].should == 200 # OK
169
+ response['result'].should have(1).item
170
+ end
171
+
172
+ it "gets a document from the server" do
173
+ add_valid_document
174
+ response = @client.get_documents('spec_index', nil, {}, 'spec.example')
175
+ response['STATUS'].should == 200
176
+ response['result'].should have(1).item
177
+ stored_document = response['result'].first
178
+ valid_document.each { |key, value| stored_document.should have_key(key) }
179
+ end
180
+
181
+ it "gets a document from multiple indexes on the server" do
182
+ @client.add_indexes('spec_index_2')
183
+ @client.delete_documents(nil, nil)
184
+ add_valid_document
185
+ add_valid_document('spec_index_2')
186
+
187
+ response = @client.get_documents(nil, nil, {}, 'spec.example')
188
+ response['STATUS'].should == 200
189
+ response['result'].should have(2).items
190
+ stored_document_1 = response['result'].first
191
+ stored_document_2 = response['result'].last
192
+
193
+ valid_document.each { |key, value| stored_document_1.should have_key(key) }
194
+ valid_document.each { |key, value| stored_document_2.should have_key(key) }
195
+
196
+ @client.delete_indexes('spec_index_2')
197
+ end
198
+
199
+ it "counts documents from the server" do
200
+ @client.delete_documents(nil, nil)
201
+ add_valid_document
202
+
203
+ response = @client.count_documents('spec_index', '*', 'spec.example')
204
+ response['STATUS'].should == 200
205
+ response['result'].should == 1
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ describe Cloudquery::Client do
212
+ before(:each) do
213
+ @valid_options = {
214
+ :account => 'account',
215
+ :secret => 'secret'
216
+ }
217
+ end
218
+
219
+ def client(options={})
220
+ return @client if defined?(@client)
221
+ @client = Cloudquery::Client.new(@valid_options.merge(options))
222
+ @client.stub!(:execute_request)
223
+ @client
224
+ end
225
+
226
+ it "instantiates when passed valid arguments" do
227
+ lambda { client }.should_not raise_error
228
+ end
229
+
230
+ end
231
+
232
+ describe Cloudquery::Request do
233
+ before(:each) do
234
+ @valid_options = {
235
+ :scheme => 'http',
236
+ :host => 'example.com',
237
+ :path => '/super/duper/path',
238
+ }
239
+ end
240
+
241
+ def request(additional_options={})
242
+ return @request if defined?(@request)
243
+ @request = Cloudquery::Request.new(@valid_options.merge(additional_options))
244
+ end
245
+
246
+ it "instantiates with valid options" do
247
+ lambda { request }.should_not raise_error
248
+ end
249
+
250
+ describe "request_uri" do
251
+ describe "without an account or secret" do
252
+ it "appends the query_str to the path after '?'" do
253
+ request.should_receive(:query_str).at_least(:once).and_return("query=string&more=params")
254
+ request.request_uri.should == "#{request.path}?#{request.send(:query_str)}"
255
+ end
256
+
257
+ it "doesn't append a '?' when query_str is empty" do
258
+ request.should_receive(:query_str).at_least(:once).and_return("")
259
+ request.request_uri.should == request.path
260
+ request.request_uri.should_not equal(request.path) #ensure we don't accidentally modify request's instance variable
261
+ end
262
+ end
263
+
264
+ describe "with an account" do
265
+ it "should append the signature_params" do
266
+ params = request(:account => 'account').request_uri.sub(/^[^?]+\?/, '').split('&')
267
+ params.select { |n| n.match(/^x_/) }.should have(4).items
268
+ end
269
+
270
+ describe "and a secret" do
271
+ it "should append the signature when the secret is provided" do
272
+ params = request(:account => 'account', :secret => 'secret').request_uri.sub(/^[^?]+\?/, '').split('&')
273
+ x_params = params.select { |n| n.match(/^x_/) }
274
+ x_params.should have(5).items
275
+ x_params.last.should match(/^x_sig=[0-9a-zA-Z\-._%]+/)
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ describe "url" do
282
+ it "constructs a full URL from the scheme, host, and request_uri" do
283
+ request.url.should ==
284
+ "#{request.scheme}://#{request.host}#{request.request_uri}"
285
+ end
286
+
287
+ it "constructs a url using a port override" do
288
+ request(:port => 8080).url.should ==
289
+ "#{request.scheme}://#{request.host}:8080#{request.request_uri}"
290
+ end
291
+
292
+ it "constructs a url using a path override" do
293
+ request(:path => '/another/path').url.should ==
294
+ "#{request.scheme}://#{request.host}#{request.request_uri}"
295
+ end
296
+
297
+ it "constructs a url with default query parameters" do
298
+ request(:params => {'these' => 'params'}).url.should ==
299
+ "#{request.scheme}://#{request.host}#{request.request_uri}"
300
+ request.url.should match(/these=params$/)
301
+ end
302
+
303
+ describe "without an account or secret" do
304
+ it "does not append the x_<params>" do
305
+ request.url.should_not match(/x_/)
306
+ end
307
+ end
308
+
309
+ describe "with an account" do
310
+ it "appends the signature params" do
311
+ url = request(:account => 'account').url
312
+ query = Rack::Utils.parse_query(url.split('?').last)
313
+ request.send(:signature_params).keys.each do |param_name|
314
+ query.should have_key(param_name)
315
+ end
316
+ end
317
+
318
+ describe "and a secret" do
319
+ it "appends the signature params and x_sig with the signature" do
320
+ url = request(:account => 'account', :secret => 'secret').url
321
+ query = Rack::Utils.parse_query(url.split('?').last)
322
+ signature_params = request.send(:signature_params).keys
323
+ signature_params.each do |param_name|
324
+ query.should have_key(param_name)
325
+ end
326
+ query.should have_key('x_sig')
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ describe "private methods" do
333
+
334
+ describe "append_signature" do
335
+ it "should append the signature as the x_sig parameter at the end of the query string" do
336
+ url = 'http://example.com/path?query=string'
337
+ signed_url = request.send(:append_signature, url, 'secret')
338
+ signed_url.should match(/^#{url.sub(/\?/, '\\?')}/)
339
+ signed_url.should match(/x_sig=[-\w]+(?:%3D)*$/)
340
+ end
341
+ end
342
+
343
+ describe "signature_params" do
344
+ describe "without an account present" do
345
+ it "should return an empty hash" do
346
+ request.send(:signature_params).should == {}
347
+ end
348
+ end
349
+
350
+ describe "with an account present" do
351
+ before(:each) do
352
+ @params = request(:account => 'account').send(:signature_params)
353
+ end
354
+
355
+ it "should return a hash with the x_name parameter with the account name" do
356
+ @params.should have_key('x_name')
357
+ @params['x_name'].should == 'account'
358
+ end
359
+
360
+ it "should return a hash with the x_time parameter with the current milliseconds since epoch" do
361
+ @params.should have_key('x_time')
362
+ @params['x_time'].should be_close(Time.now.to_i_with_milliseconds, 100)
363
+ end
364
+
365
+ it "should return a hash with the x_nonce parameter of the format \d+.\d+" do
366
+ @params.should have_key('x_nonce')
367
+ @params['x_nonce'].should match(/^\d+.\d+$/)
368
+ end
369
+
370
+ it "should return a hash with the x_method parameter with the signing method name" do
371
+ @params.should have_key('x_method')
372
+ @params['x_method'].should == Cloudquery::SIGNING_METHOD
373
+ end
374
+ end
375
+ end
376
+
377
+ describe "query_str" do
378
+ it "builds a query string from the request params" do
379
+ request(:params => {'these' => 'params'})
380
+ request.send(:query_str).should == 'these=params'
381
+ end
382
+
383
+ it "url-encodes params with non alphanumeric characters (outside [ a-zA-Z0-9-._])" do
384
+ request(:params => {'weird' => 'values=here'})
385
+ request.send(:query_str).should == 'weird=values%3Dhere'
386
+ end
387
+
388
+ it "returns an empty string when no params are present" do
389
+ request(:params => {}).send(:query_str) == ""
390
+ end
391
+ end
392
+
393
+ describe "base_uri" do
394
+ it "returns an http url when the scheme is http" do
395
+ request(:scheme => 'http').send(:base_uri).should be_an_instance_of(URI::HTTP)
396
+ end
397
+ it "returns an https url when the scheme is https" do
398
+ request(:scheme => 'https').send(:base_uri).should be_an_instance_of(URI::HTTPS)
399
+ end
400
+ end
401
+ end
402
+
403
+ end
404
+
405
+ describe Cloudquery::Crypto::Random do
406
+ describe "nonce generation" do
407
+ it "generates a nonce with a random number, a dot, and the current time" do
408
+ nonce = Cloudquery::Crypto::Random.nonce
409
+ nonce.should match(/^\d+.\d+$/)
410
+ random_digits, time = nonce.split('.')
411
+ time.to_i.should be_close(Time.now.to_i, 1)
412
+ random_digits.should match(/^\d+$/)
413
+ end
414
+ end
415
+ end
416
+
417
+ describe Cloudquery::Crypto::URLSafeSHA1 do
418
+ describe "sign" do
419
+ it "takes an arbitrary number of tokens to encrypt" do
420
+ lambda { Cloudquery::Crypto::URLSafeSHA1.sign }.should_not raise_error
421
+ lambda { Cloudquery::Crypto::URLSafeSHA1.sign('a') }.should_not raise_error
422
+ lambda { Cloudquery::Crypto::URLSafeSHA1.sign('a', 'b', 'c') }.should_not raise_error
423
+ end
424
+
425
+ it "produces a url-safe base64 encoded SHA1 digest of tokens" do
426
+ 20.times do
427
+ token = Cloudquery::Crypto::Random.nonce
428
+ signature = Cloudquery::Crypto::URLSafeSHA1.sign(token)
429
+ signature.should_not include('+')
430
+ signature.should_not include('/')
431
+
432
+ b64_digest = Base64.encode64(Digest::SHA1.digest(token)).chomp.tr('+/', '-_')
433
+ signature.should == b64_digest
434
+ end
435
+ end
436
+ end
437
+ end
@@ -0,0 +1,26 @@
1
+ <schema name="spec.example" store="yes">
2
+ <!-- The full name of the contact -->
3
+ <field name="name"
4
+ type="string"
5
+ analyzer="LCWhitespaceAnalyzer"
6
+ usage="user" />
7
+ <!-- The email addresses. A json array: email address -->
8
+ <field name="email"
9
+ type="string"
10
+ usage="user" />
11
+ <!-- The phone numbers. A json array: phone number -->
12
+ <field name="telephone"
13
+ type="string"
14
+ usage="user" />
15
+ <!-- The addresses. A json array: address -->
16
+ <field name="address"
17
+ type="string"
18
+ usage="user" />
19
+ <!-- The birthday of the contact-->
20
+ <field name="birthday"
21
+ type="date" />
22
+ <!-- A note for the contact-->
23
+ <field name="note"
24
+ type="text"
25
+ usage="user" />
26
+ </schema>
@@ -0,0 +1,11 @@
1
+ require 'spec'
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'cloudquery'
6
+ require 'parsedate'
7
+ require 'pp'
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xoopit-cloudquery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Walters
8
+ - nb.io
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-05-03 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rack
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "1.0"
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 1.1.4
35
+ version:
36
+ - !ruby/object:Gem::Dependency
37
+ name: taf2-curb
38
+ type: :runtime
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 0.2.8.0
45
+ version:
46
+ description: Client for Xoopit's cloudquery API
47
+ email: us@nb.io
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - LICENSE
54
+ - README.markdown
55
+ files:
56
+ - LICENSE
57
+ - README.markdown
58
+ - Rakefile
59
+ - VERSION.yml
60
+ - lib/cloudquery.rb
61
+ - spec/cloudquery_spec.rb
62
+ - spec/example_schema.xml
63
+ - spec/spec_helper.rb
64
+ has_rdoc: true
65
+ homepage: http://github.com/nbio/cloudquery
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --charset=UTF-8
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.2.0
87
+ signing_key:
88
+ specification_version: 2
89
+ summary: Client for Xoopit's cloudquery API
90
+ test_files:
91
+ - spec/cloudquery_spec.rb
92
+ - spec/spec_helper.rb