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 +20 -0
- data/README.markdown +73 -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,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
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: 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
|