xoopit-cloudquery 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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