nbio-cloudquery 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +74 -0
- data/Rakefile +75 -0
- data/VERSION.yml +4 -0
- data/lib/cloudquery.rb +453 -0
- data/spec/cloudquery_spec.rb +437 -0
- data/spec/example_schema.xml +26 -0
- data/spec/spec_helper.rb +11 -0
- metadata +92 -0
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,74 @@
|
|
1
|
+
cloudquery
|
2
|
+
==========
|
3
|
+
|
4
|
+
Client for Xoopit's cloudquery API
|
5
|
+
|
6
|
+
Install
|
7
|
+
-------
|
8
|
+
|
9
|
+
sudo gem install xoopit-cloudquery
|
10
|
+
|
11
|
+
|
12
|
+
Simple contacts application example
|
13
|
+
-----------------------------------
|
14
|
+
|
15
|
+
> require 'cloudquery'
|
16
|
+
=> true
|
17
|
+
> include Cloudquery
|
18
|
+
=> Object
|
19
|
+
> secret = Client.get_secret(<account_name>, <password>)
|
20
|
+
=> "your secret appears here"
|
21
|
+
> c = Client.new(:account => '<account_name>', :secret => secret)
|
22
|
+
=> #<Cloudquery::Client:0x10b1b24 @secure=true, @secret="your secret appears here", @account="<account_name>", @document_id_method=nil>
|
23
|
+
> c.add_indexes('superheroes')
|
24
|
+
=> {"result"=>["kMzzzybpqpY"], "size"=>1, "STATUS"=>200}
|
25
|
+
> c.add_schema(File.open('simple.contact.xml'))
|
26
|
+
=> {"result"=>["ubKme0EX3H2ud7VhBU7qngk3........."], "size"=>1, "STATUS"=>201}
|
27
|
+
> doc = {
|
28
|
+
'simple.contact.name' => 'Steve Rogers',
|
29
|
+
'simple.contact.email' => ['steve.rogers@example.com','captain.america@marvel.com'],
|
30
|
+
'simple.contact.telephone' => ['555-555-5555','123-456-6789'],
|
31
|
+
'simple.contact.address' => ['Lower East Side, NY NY'],
|
32
|
+
'simple.contact.birthday' => Date.parse('July 4, 1917'),
|
33
|
+
'simple.contact.note' => 'Captain America!',
|
34
|
+
}
|
35
|
+
=> {"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"}
|
36
|
+
> c.add_documents('superheroes', doc, 'simple.contact')
|
37
|
+
=> {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "size"=>1, "STATUS"=>201}
|
38
|
+
> docs = [
|
39
|
+
{
|
40
|
+
'simple.contact.name' => 'Clark Kent',
|
41
|
+
'simple.contact.email' => ['clark.kent@example.com','superman@dc.com'],
|
42
|
+
'simple.contact.telephone' => ['555-123-1234','555-456-6789'],
|
43
|
+
'simple.contact.address' => ['344 Clinton St., Apt. #3B, Metropolis', 'The Fortess of Solitude, North Pole'],
|
44
|
+
'simple.contact.birthday' => Date.parse('June 18, 1938'),
|
45
|
+
'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.'
|
46
|
+
},
|
47
|
+
{
|
48
|
+
'simple.contact.name' => 'Bruce Wayne',
|
49
|
+
'simple.contact.email' => ['bruce.wayne@example.com','batman@dc.com'],
|
50
|
+
'simple.contact.telephone' => ['555-123-6666','555-456-6666'],
|
51
|
+
'simple.contact.address' => ['1007 Mountain Drive, Gotham', 'The Batcave, Gotham'],
|
52
|
+
'simple.contact.birthday' => Date.parse('February 19, 1939'),
|
53
|
+
'simple.contact.note' => 'Sidekick is Robin. Has problems with the Joker. Member of e justice league.'
|
54
|
+
}
|
55
|
+
]
|
56
|
+
> c.add_documents('superheroes', docs, 'simple.contact')
|
57
|
+
=> {"result"=>["lQgByVSvJk1skHtKpMYX40kMzzzybpqpY", "weJF4uDPJrlvrETTJQNibFkMzzzybpqpY"], "size"=>2, "STATUS"=>201}
|
58
|
+
> c.count_documents('superheroes', '*', 'simple.contact')
|
59
|
+
=> {"result"=>3, "matches"=>3, "STATUS"=>200}
|
60
|
+
> c.get_documents('superheroes', '*', {:fields => 'simple.contact.name'}, 'simple.contact')
|
61
|
+
=> {"result"=>[{"simple.contact.name"=>"Steve Rogers"}, {"simple.contact.name"=>"Clark Kent"}, {"simple.contact.name"=>"Bruce Wayne"}], "matches"=>3, "size"=>3, "STATUS"=>200}
|
62
|
+
> c.get_documents('superheroes', 'name:Steve', {:fields => 'simple.contact.name'}, 'simple.contact')
|
63
|
+
=> {"result"=>[{"simple.contact.name"=>"Steve Rogers"}], "matches"=>1, "size"=>1, "STATUS"=>200}
|
64
|
+
> c.get_documents('superheroes', ':@:justice', {:fields => 'simple.contact.name'}, 'simple.contact')
|
65
|
+
=> {"result"=>[{"simple.contact.name"=>"Clark Kent"}, {"simple.contact.name"=>"Bruce Wayne"}], "matches"=>2, "size"=>2, "STATUS"=>200}
|
66
|
+
> c.modify_documents('superheroes', 'name:steve', {'simple.contact.note' => 'His name is STEVE!'}, 'simple.contact')
|
67
|
+
=> {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "matches"=>1, "size"=>1, "STATUS"=>200}
|
68
|
+
> c.delete_documents('superheroes', 'name:steve', 'simple.contact') => {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "matches"=>2, "size"=>1, "STATUS"=>200}
|
69
|
+
|
70
|
+
|
71
|
+
Copyright
|
72
|
+
---------
|
73
|
+
|
74
|
+
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
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>
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nbio-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
|