xoopit-cloud_query 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +10 -10
- data/lib/cloud_query/client.rb +370 -0
- data/lib/cloud_query/crypto.rb +28 -0
- data/lib/cloud_query/field.rb +22 -0
- data/lib/cloud_query/request.rb +64 -0
- data/lib/cloud_query/schema.rb +18 -0
- data/lib/cloud_query/x64.rb +42 -0
- data/lib/cloud_query.rb +3 -1
- data/spec/cloudquery_spec.rb +3 -3
- data/spec/lib/cloud_query/field_spec.rb +43 -0
- data/spec/lib/cloud_query/schema_spec.rb +34 -0
- data/spec/lib/cloud_query/x64_spec.rb +14 -0
- data/tasks/distribution.rb +38 -0
- data/tasks/testing.rb +14 -0
- metadata +40 -13
- data/Rakefile +0 -80
- data/VERSION.yml +0 -4
- data/spec/example_schema.xml +0 -26
- data/spec/spec_helper.rb +0 -11
data/README.markdown
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
CloudQuery
|
2
2
|
==========
|
3
3
|
|
4
|
-
Client for Xoopit's
|
4
|
+
Client for Xoopit's CloudQuery API
|
5
5
|
|
6
6
|
Install
|
7
7
|
-------
|
@@ -11,21 +11,21 @@ care of it. If not, `sudo gem install json rack taf2-curb` will do it.
|
|
11
11
|
|
12
12
|
Be sure you've run `gem sources -a http://gems.github.com` once on your system. Then:
|
13
13
|
|
14
|
-
sudo gem install xoopit-cloudquery
|
14
|
+
sudo gem install xoopit-cloudquery-ruby
|
15
15
|
|
16
16
|
Simple contacts application example
|
17
17
|
-----------------------------------
|
18
18
|
|
19
19
|
> require 'rubygems'
|
20
20
|
=> true
|
21
|
-
> require '
|
21
|
+
> require 'cloud_query'
|
22
22
|
=> true
|
23
|
-
> include
|
23
|
+
> include CloudQuery
|
24
24
|
=> Object
|
25
25
|
> secret = Client.get_secret(<account_name>, <password>)
|
26
26
|
=> "your secret appears here"
|
27
27
|
> c = Client.new(:account => '<account_name>', :secret => secret)
|
28
|
-
=> #<
|
28
|
+
=> #<CloudQuery::Client:0x10b1b24 @secure=true, @secret="your secret appears here", @account="<account_name>", @document_id_method=nil>
|
29
29
|
> c.add_indexes('superheroes')
|
30
30
|
=> {"result"=>["kMzzzybpqpY"], "size"=>1, "STATUS"=>200}
|
31
31
|
> c.add_schema(File.open('simple.contact.xml'))
|
@@ -35,10 +35,10 @@ Simple contacts application example
|
|
35
35
|
'simple.contact.email' => ['steve.rogers@example.com','captain.america@marvel.com'],
|
36
36
|
'simple.contact.telephone' => ['555-555-5555','123-456-6789'],
|
37
37
|
'simple.contact.address' => ['Lower East Side, NY NY'],
|
38
|
-
'simple.contact.birthday' =>
|
38
|
+
'simple.contact.birthday' => Time.parse('July 4, 1917').to_i_milliseconds,
|
39
39
|
'simple.contact.note' => 'Captain America!',
|
40
40
|
}
|
41
|
-
=> {"simple.contact.birthday"
|
41
|
+
=> {"simple.contact.birthday"=>-1656604800000, "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"}
|
42
42
|
> c.add_documents('superheroes', doc, 'simple.contact')
|
43
43
|
=> {"result"=>["nDLCNLPo3oHtxANzG4YBn5kMzzzybpqpY"], "size"=>1, "STATUS"=>201}
|
44
44
|
> docs = [
|
@@ -47,7 +47,7 @@ Simple contacts application example
|
|
47
47
|
'simple.contact.email' => ['clark.kent@example.com','superman@dc.com'],
|
48
48
|
'simple.contact.telephone' => ['555-123-1234','555-456-6789'],
|
49
49
|
'simple.contact.address' => ['344 Clinton St., Apt. #3B, Metropolis', 'The Fortess of Solitude, North Pole'],
|
50
|
-
'simple.contact.birthday' =>
|
50
|
+
'simple.contact.birthday' => Time.parse('June 18, 1938').to_i_milliseconds,
|
51
51
|
'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.'
|
52
52
|
},
|
53
53
|
{
|
@@ -55,7 +55,7 @@ Simple contacts application example
|
|
55
55
|
'simple.contact.email' => ['bruce.wayne@example.com','batman@dc.com'],
|
56
56
|
'simple.contact.telephone' => ['555-123-6666','555-456-6666'],
|
57
57
|
'simple.contact.address' => ['1007 Mountain Drive, Gotham', 'The Batcave, Gotham'],
|
58
|
-
'simple.contact.birthday' =>
|
58
|
+
'simple.contact.birthday' => Time.parse('February 19, 1939').to_i_milliseconds,
|
59
59
|
'simple.contact.note' => 'Sidekick is Robin. Has problems with the Joker. Member of e justice league.'
|
60
60
|
}
|
61
61
|
]
|
@@ -0,0 +1,370 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
class Client
|
3
|
+
attr_reader :account
|
4
|
+
attr_writer :secret
|
5
|
+
|
6
|
+
# Create a new instance of the client
|
7
|
+
#
|
8
|
+
# It's highly recommended to set options <tt>:account</tt>
|
9
|
+
# and <tt>:secret</tt>. Creating a client without an account
|
10
|
+
# and secret isn't very useful.
|
11
|
+
#
|
12
|
+
# ==== Acceptable options:
|
13
|
+
# :account => <account name> (default => nil)
|
14
|
+
# :secret => <API secret> (default => nil)
|
15
|
+
# :document_id_method => <method name> (default => nil)
|
16
|
+
# :secure => Boolean (use HTTPS, default => true)
|
17
|
+
#
|
18
|
+
# If +document_id_method+ is set, it will be called on each
|
19
|
+
# document as a part of +add_documents+ and +update_documents+
|
20
|
+
# which should inject an <tt>'#.#'</tt> key-value pair as a
|
21
|
+
# simple way to tie app PKs to doc ids.
|
22
|
+
def initialize(options={})
|
23
|
+
# unless options[:account] && options[:secret]
|
24
|
+
# raise "Client requires :account => <account name> and :secret => <secret>"
|
25
|
+
# end
|
26
|
+
|
27
|
+
@account = options[:account]
|
28
|
+
@secret = options[:secret]
|
29
|
+
|
30
|
+
@secure = options[:secure] != false # must pass false for insecure
|
31
|
+
|
32
|
+
@document_id_method = options[:document_id_method]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
## Account management
|
37
|
+
|
38
|
+
# Retrieve the API secret for an +account+, using the +password+ (uses HTTPS)
|
39
|
+
def self.get_secret(account, password)
|
40
|
+
auth = Request.new(:path => "#{PATH}/auth")
|
41
|
+
curl = Curl::Easy.new(auth.url) do |c|
|
42
|
+
c.enable_cookies = true
|
43
|
+
c.cookiejar = COOKIE_JAR
|
44
|
+
end
|
45
|
+
params = Rack::Utils.build_query({"name" => account, "password" => password})
|
46
|
+
curl.http_post(params)
|
47
|
+
|
48
|
+
if (curl.response_code/100) == 2
|
49
|
+
curl.url = Request.new(:path => "#{PATH}/#{API_PATHS[:account]}/#{account}").url
|
50
|
+
curl.http_get
|
51
|
+
response = JSON.parse(curl.body_str)
|
52
|
+
response['result']['secret']
|
53
|
+
else
|
54
|
+
STDERR.puts "Error: #{curl.response_code} #{Rack::Utils::HTTP_STATUS_CODES[curl.response_code]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# change password
|
59
|
+
# optionally change the secret
|
60
|
+
def self.change_password(account, old_password, new_password, new_secret=nil)
|
61
|
+
secret = get_secret(account, old_password)
|
62
|
+
c = Client.new(:account => account, :secret => secret, :secure => true)
|
63
|
+
a = c.get_account()['result']
|
64
|
+
a['password'] = new_password
|
65
|
+
a.delete('secret')
|
66
|
+
if new_secret != nil
|
67
|
+
a['secret'] = new_secret
|
68
|
+
end
|
69
|
+
c.update_account(a)
|
70
|
+
return (new_secret != nil ? new_secret : secret);
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get the account document
|
74
|
+
def get_account
|
75
|
+
send_request get(account_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Update the account document.
|
79
|
+
#
|
80
|
+
# Use this method to change the API secret:
|
81
|
+
# update_account({'secret' => 'your-new-secret'})
|
82
|
+
def update_account(account_doc={})
|
83
|
+
body = JSON.generate(account_doc)
|
84
|
+
send_request put(account_path, body)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Delete the account.
|
88
|
+
#
|
89
|
+
# ==== BEWARE: THIS WILL ACTUALLY DELETE YOUR ACCOUNT.
|
90
|
+
def delete_account!
|
91
|
+
send_request delete(account_path)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
## Schema management
|
96
|
+
|
97
|
+
# Add a schema to the account. +xml+ can be a +String+
|
98
|
+
# or +File+-like (responds to <tt>:read</tt>)
|
99
|
+
def add_schema(xml)
|
100
|
+
body = xml.respond_to?(:read) ? xml.read : xml
|
101
|
+
request = post(build_path(API_PATHS[:schema]), body)
|
102
|
+
send_request(request, CONTENT_TYPES[:xml])
|
103
|
+
end
|
104
|
+
|
105
|
+
# Delete a schema from the account, by name
|
106
|
+
# if cascade is true, all documents with the specified schema will be deleted
|
107
|
+
# (use with care)
|
108
|
+
def delete_schema(schema_name, cascade=nil)
|
109
|
+
c = cascade ? {:cascade => 'true'} : nil
|
110
|
+
send_request delete(build_path(
|
111
|
+
API_PATHS[:schema],
|
112
|
+
Rack::Utils.escape("$.name:\"#{schema_name}\"")
|
113
|
+
), c)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the schemas for the account.
|
117
|
+
#
|
118
|
+
# NOTE: returned format is not the same as accepted for input
|
119
|
+
def get_schemas
|
120
|
+
send_request get(build_path(API_PATHS[:schema]))
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
## Index management
|
125
|
+
|
126
|
+
# Add one or more indexes to the account, by name or id
|
127
|
+
def add_indexes(*indexes)
|
128
|
+
body = JSON.generate(indexes.flatten)
|
129
|
+
send_request post(build_path(API_PATHS[:indexes]), body)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Delete one or more indexes from the account, by name or id
|
133
|
+
# <tt>indexes = '*'</tt> will delete all indexes
|
134
|
+
def delete_indexes(*indexes)
|
135
|
+
indexes = url_pipe_join(indexes)
|
136
|
+
send_request delete(build_path(API_PATHS[:indexes], indexes))
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get the indexes from the account. Returns a list of ids
|
140
|
+
def get_indexes
|
141
|
+
send_request get(build_path(API_PATHS[:indexes]))
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
## Document management
|
146
|
+
|
147
|
+
# Add documents to the specified +index+
|
148
|
+
#
|
149
|
+
# <tt>index = name</tt> or +id+, <tt>docs = {}</tt> or +Array+ of <tt>{}</tt>.
|
150
|
+
#
|
151
|
+
# Documents with key <tt>'#.#'</tt> and an existing value will be updated.
|
152
|
+
#
|
153
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
154
|
+
# specified schemas on each document.
|
155
|
+
def add_documents(index, docs, schemas=[], fieldmode=nil)
|
156
|
+
fm = fieldmode != nil ? {:fieldmode => fieldmode} : nil
|
157
|
+
request = post(
|
158
|
+
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), fm),
|
159
|
+
JSON.generate(identify_documents(docs))
|
160
|
+
)
|
161
|
+
send_request request
|
162
|
+
end
|
163
|
+
|
164
|
+
# Update documents in the specified +index+
|
165
|
+
|
166
|
+
# <tt>index = name</tt> or +id+, <tt>docs = {}</tt> or +Array+ of <tt>{}</tt>.
|
167
|
+
#
|
168
|
+
# Documents lacking the key <tt>'#.#'</tt> will be created.
|
169
|
+
#
|
170
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
171
|
+
# specified schemas on each document.
|
172
|
+
def update_documents(index, docs, schemas=[], fieldmode=nil)
|
173
|
+
fm = fieldmode != nil ? {:fieldmode => fieldmode} : nil
|
174
|
+
request = put(
|
175
|
+
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), fm),
|
176
|
+
JSON.generate(identify_documents(docs))
|
177
|
+
)
|
178
|
+
send_request request
|
179
|
+
end
|
180
|
+
|
181
|
+
# Modify documents in the +index+ matching +query+
|
182
|
+
#
|
183
|
+
# <tt>modifications = {...data...}</tt> to update all matching
|
184
|
+
# documents.
|
185
|
+
#
|
186
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
187
|
+
# specified schemas on each document.
|
188
|
+
def modify_documents(index, query, modifications, schemas=[], fieldmode=nil)
|
189
|
+
fm = fieldmode != nil ? "?fieldmode=#{fieldmode}" : ""
|
190
|
+
request = put(
|
191
|
+
build_path(API_PATHS[:documents], index, url_pipe_join(schemas), Rack::Utils.escape(query), fm),
|
192
|
+
JSON.generate(modifications)
|
193
|
+
)
|
194
|
+
send_request request
|
195
|
+
end
|
196
|
+
|
197
|
+
# Delete documents in the +index+ matching +query+
|
198
|
+
#
|
199
|
+
# query => defaults to '*'
|
200
|
+
# index => may be an id, index name, or Array of ids or names.
|
201
|
+
#
|
202
|
+
# Operates on all indexes if +index+ = +nil+ or <tt>'*'</tt>
|
203
|
+
#
|
204
|
+
# ==== BEWARE: If +query+ = +nil+ this will delete ALL documents in +index+.
|
205
|
+
#
|
206
|
+
# ==== Acceptable options:
|
207
|
+
# :sort => a string ("[+|-]schema.field"), or a list thereof (default => index-order)
|
208
|
+
# :offset => integer offset into the result set (default => 0)
|
209
|
+
# :limit => integer limit on number of documents returned per index (default => <no limit>)
|
210
|
+
#
|
211
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
212
|
+
# specified schemas on each document.
|
213
|
+
def delete_documents(index, query, options={}, schemas=[])
|
214
|
+
if options[:sort]
|
215
|
+
options[:sort] = Array(options[:sort]).flatten.join(',')
|
216
|
+
end
|
217
|
+
request = delete(
|
218
|
+
build_path(API_PATHS[:documents],
|
219
|
+
url_pipe_join(index),
|
220
|
+
url_pipe_join(schemas),
|
221
|
+
Rack::Utils.escape(query)
|
222
|
+
),
|
223
|
+
options
|
224
|
+
)
|
225
|
+
send_request request
|
226
|
+
end
|
227
|
+
|
228
|
+
# Get documents matching +query+
|
229
|
+
#
|
230
|
+
# query => defaults to '*'
|
231
|
+
# index => may be an id, index name, or Array of ids or names.
|
232
|
+
#
|
233
|
+
# Operates on all indexes if +index+ = +nil+ or <tt>'*'</tt>
|
234
|
+
#
|
235
|
+
# ==== Acceptable options:
|
236
|
+
# :fields => a field name, a prefix match (e.g. 'trans*'), or Array of fields (default => '*')
|
237
|
+
# :sort => a string ("[+|-]schema.field"), or a list thereof (default => index-order)
|
238
|
+
# :offset => integer offset into the result set (default => 0)
|
239
|
+
# :limit => integer limit on number of documents returned per index (default => <no limit>)
|
240
|
+
# :fieldmode => 'short' or 'long'/nil, if 'short' then field names will be returned in their
|
241
|
+
# short form (no schema name prefix), this can cause naming collisions depending
|
242
|
+
# on use schema definition and data etc.
|
243
|
+
#
|
244
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
245
|
+
# specified schemas on each document.
|
246
|
+
def get_documents(index, query, options={}, schemas=[])
|
247
|
+
if fields = options.delete(:fields)
|
248
|
+
fields = url_pipe_join(fields)
|
249
|
+
end
|
250
|
+
|
251
|
+
if options[:sort]
|
252
|
+
options[:sort] = Array(options[:sort]).flatten.join(',')
|
253
|
+
end
|
254
|
+
|
255
|
+
request = get(
|
256
|
+
build_path(API_PATHS[:documents],
|
257
|
+
url_pipe_join(index),
|
258
|
+
url_pipe_join(schemas),
|
259
|
+
url_pipe_join(query),
|
260
|
+
fields
|
261
|
+
),
|
262
|
+
options
|
263
|
+
)
|
264
|
+
send_request request
|
265
|
+
end
|
266
|
+
|
267
|
+
# Count documents matching +query+
|
268
|
+
#
|
269
|
+
# query => defaults to '*'
|
270
|
+
# index => may be an id, index name, or Array of ids or names.
|
271
|
+
#
|
272
|
+
# Operates on all indexes if +index+ = +nil+ or <tt>'*'</tt>
|
273
|
+
#
|
274
|
+
# If +schemas+ is not +nil+, ensures existence of the
|
275
|
+
# specified schemas on each document.
|
276
|
+
def count_documents(index, query, schemas)
|
277
|
+
get_documents(index, query, {:fields => '@count'}, schemas)
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
def build_path(*path_elements)
|
282
|
+
path_elements.flatten.compact.unshift(PATH).join('/')
|
283
|
+
end
|
284
|
+
|
285
|
+
def account_path
|
286
|
+
build_path(API_PATHS[:account], @account)
|
287
|
+
end
|
288
|
+
|
289
|
+
def build_request(options={})
|
290
|
+
Request.new default_request_params.merge(options)
|
291
|
+
end
|
292
|
+
|
293
|
+
def get(path, params={})
|
294
|
+
build_request(:method => 'GET', :path => path, :params => params)
|
295
|
+
end
|
296
|
+
|
297
|
+
def delete(path, params={})
|
298
|
+
build_request(:method => 'DELETE', :path => path, :params => params)
|
299
|
+
end
|
300
|
+
|
301
|
+
def post(path, doc, params={})
|
302
|
+
build_request(:method => 'POST', :path => path, :body => doc, :params => params)
|
303
|
+
end
|
304
|
+
|
305
|
+
def put(path, doc, params={})
|
306
|
+
build_request(:method => 'PUT', :path => path, :body => doc, :params => params)
|
307
|
+
end
|
308
|
+
|
309
|
+
def default_request_params
|
310
|
+
{
|
311
|
+
:account => @account,
|
312
|
+
:secret => @secret,
|
313
|
+
:scheme => @secure ? 'https' : 'http',
|
314
|
+
}
|
315
|
+
end
|
316
|
+
|
317
|
+
def send_request(request, content_type=nil)
|
318
|
+
response = execute_request(request.method, request.url, request.headers, request.body, content_type)
|
319
|
+
status_code = response.first
|
320
|
+
if (200..299).include?(status_code)
|
321
|
+
begin
|
322
|
+
result = JSON.parse(response.last)
|
323
|
+
rescue JSON::ParserError => e
|
324
|
+
result = {"REASON" => e.message}
|
325
|
+
end
|
326
|
+
else
|
327
|
+
result = {"REASON" => "Error: #{status_code} #{Rack::Utils::HTTP_STATUS_CODES[status_code]}"}
|
328
|
+
end
|
329
|
+
result.merge!({'STATUS' => status_code})
|
330
|
+
end
|
331
|
+
|
332
|
+
def execute_request(method, url, headers, body, content_type=nil)
|
333
|
+
content_type ||= CONTENT_TYPES[:json]
|
334
|
+
curl = Curl::Easy.new(url) do |c|
|
335
|
+
c.headers = headers
|
336
|
+
c.headers['Content-Type'] = content_type
|
337
|
+
c.encoding = 'gzip'
|
338
|
+
end
|
339
|
+
case method
|
340
|
+
when 'GET'
|
341
|
+
curl.http_get
|
342
|
+
when 'DELETE'
|
343
|
+
curl.http_delete
|
344
|
+
when 'POST'
|
345
|
+
curl.http_post(body)
|
346
|
+
when 'PUT'
|
347
|
+
curl.http_put(body)
|
348
|
+
end
|
349
|
+
|
350
|
+
[curl.response_code, curl.header_str, curl.body_str]
|
351
|
+
end
|
352
|
+
|
353
|
+
def url_pipe_join(arr, default_value='*')
|
354
|
+
arr = Array(arr).flatten
|
355
|
+
if arr.empty?
|
356
|
+
default_value
|
357
|
+
else
|
358
|
+
Rack::Utils.escape(arr.join('|'))
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def identify_documents(docs)
|
363
|
+
[docs] if docs.is_a?(Hash)
|
364
|
+
if @document_id_method
|
365
|
+
docs.each { |d| d.send(@document_id_method) }
|
366
|
+
end
|
367
|
+
docs
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
module Crypto
|
3
|
+
module Random
|
4
|
+
extend self
|
5
|
+
|
6
|
+
SecureRandom = (defined?(::SecureRandom) && ::SecureRandom) || (defined?(::ActiveSupport::SecureRandom) && ::ActiveSupport::SecureRandom)
|
7
|
+
if SecureRandom
|
8
|
+
def nonce
|
9
|
+
"#{SecureRandom.random_number}.#{Time.now.to_i}"[2..-1]
|
10
|
+
end
|
11
|
+
else
|
12
|
+
def nonce
|
13
|
+
"#{rand.to_s}.#{Time.now.to_i}"[2..-1]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module URLSafeSHA1
|
19
|
+
extend self
|
20
|
+
|
21
|
+
def sign(*tokens)
|
22
|
+
tokens = tokens.flatten
|
23
|
+
digest = Digest::SHA1.digest(tokens.join)
|
24
|
+
Base64.encode64(digest).chomp.tr('+/', '-_')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
class Field
|
3
|
+
attr_accessor :name, :type, :usage
|
4
|
+
|
5
|
+
def initialize(name, type)
|
6
|
+
@name, @type = name, type
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_node(builder)
|
10
|
+
builder.field attributes
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
hash = { :name => @name, :type => @type }
|
17
|
+
hash[:usage] = @usage unless @usage.nil?
|
18
|
+
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
class Request
|
3
|
+
attr_accessor :method, :headers, :scheme, :host, :port, :path, :params, :body
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
@method = options[:method] || 'POST'
|
7
|
+
@headers = options[:headers] || {}
|
8
|
+
@scheme = options[:scheme] || SCHEME
|
9
|
+
@host = options[:host] || HOST
|
10
|
+
@port = options[:port] || (@scheme == 'https' ? URI::HTTPS::DEFAULT_PORT : URI::HTTP::DEFAULT_PORT)
|
11
|
+
@path = options[:path] || PATH
|
12
|
+
@params = options[:params] || {}
|
13
|
+
if ['PUT', 'DELETE'].include?(@method)
|
14
|
+
@params['_method'] = @method
|
15
|
+
@method = 'POST'
|
16
|
+
end
|
17
|
+
@body = options[:body]
|
18
|
+
|
19
|
+
@account = options[:account]
|
20
|
+
@secret = options[:secret]
|
21
|
+
end
|
22
|
+
|
23
|
+
def request_uri(account=@account, secret=@secret)
|
24
|
+
query = query_str(signature_params(account))
|
25
|
+
uri = if query.empty?
|
26
|
+
@path.dup
|
27
|
+
else
|
28
|
+
"#{@path}?#{query}"
|
29
|
+
end
|
30
|
+
uri = append_signature(uri, secret) if secret
|
31
|
+
uri
|
32
|
+
end
|
33
|
+
|
34
|
+
def url(account=@account, secret=@secret)
|
35
|
+
base_uri.merge(request_uri(account, secret)).to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def append_signature(uri, secret)
|
40
|
+
sig = Crypto::URLSafeSHA1.sign(secret, uri)
|
41
|
+
x_sig = Rack::Utils.build_query("x_sig" => sig)
|
42
|
+
"#{uri}&#{x_sig}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def signature_params(account=@account)
|
46
|
+
return {} unless account
|
47
|
+
{
|
48
|
+
'x_name' => account,
|
49
|
+
'x_time' => Time.now.to_i_milliseconds,
|
50
|
+
'x_nonce' => CloudQuery::Crypto::Random.nonce,
|
51
|
+
'x_method' => SIGNING_METHOD,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def query_str(additional_params={})
|
56
|
+
Rack::Utils.build_query(@params.dup.merge(additional_params))
|
57
|
+
end
|
58
|
+
|
59
|
+
def base_uri
|
60
|
+
uri_class = (@scheme == 'https' ? URI::HTTPS : URI::HTTP)
|
61
|
+
uri_class.build(:scheme => @scheme, :host => @host, :port => @port)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
class Schema
|
3
|
+
attr_accessor :name, :fields
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
@fields = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_xml
|
11
|
+
Nokogiri::XML::Builder.new { |doc|
|
12
|
+
doc.schema(:name => @name, :store => 'yes') {
|
13
|
+
@fields.each { |field| field.to_node(doc) }
|
14
|
+
}
|
15
|
+
}.to_xml
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module CloudQuery
|
2
|
+
class X64
|
3
|
+
Shift = 6
|
4
|
+
Mask = 0x3f
|
5
|
+
Munge = 0x30
|
6
|
+
NumberOfChars = (64 + Shift - 1)/Shift
|
7
|
+
Maximum = 0xffffffffffffffff
|
8
|
+
Compliment = 0x8000000000000000
|
9
|
+
Characters = '.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
|
10
|
+
|
11
|
+
def self.encode(object, characters = Characters, munge = Munge)
|
12
|
+
int = integer_from_object(object)
|
13
|
+
|
14
|
+
chars = []
|
15
|
+
while chars.length == 0 || int != 0
|
16
|
+
byte = int & Maximum
|
17
|
+
|
18
|
+
(0...NumberOfChars).each do |i|
|
19
|
+
index = byte & Mask
|
20
|
+
index |= munge if i == (NumberOfChars - 1)
|
21
|
+
chars.unshift characters[index, 1]
|
22
|
+
byte = (byte >> Shift) & Maximum
|
23
|
+
end
|
24
|
+
|
25
|
+
int = int >> 64
|
26
|
+
end
|
27
|
+
|
28
|
+
chars.join
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def self.integer_from_object(object)
|
34
|
+
case object
|
35
|
+
when String
|
36
|
+
UUIDTools::UUID.parse_raw(Digest::MD5.digest(object)).to_i
|
37
|
+
else
|
38
|
+
object.to_i
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/cloud_query.rb
CHANGED
@@ -6,12 +6,14 @@ require "rack/utils"
|
|
6
6
|
require "curl"
|
7
7
|
require "json"
|
8
8
|
require 'nokogiri'
|
9
|
+
require 'uuidtools'
|
9
10
|
|
10
11
|
require 'cloud_query/client'
|
11
12
|
require 'cloud_query/crypto'
|
12
13
|
require 'cloud_query/field'
|
13
14
|
require 'cloud_query/request'
|
14
15
|
require 'cloud_query/schema'
|
16
|
+
require 'cloud_query/x64'
|
15
17
|
|
16
18
|
module CloudQuery
|
17
19
|
SCHEME = "https".freeze
|
@@ -37,7 +39,7 @@ module CloudQuery
|
|
37
39
|
end
|
38
40
|
|
39
41
|
class Time
|
40
|
-
def
|
42
|
+
def to_i_milliseconds
|
41
43
|
(to_f * 1000).to_i
|
42
44
|
end
|
43
45
|
end
|
data/spec/cloudquery_spec.rb
CHANGED
@@ -89,7 +89,7 @@ if ENV["TEST_REAL_HTTP"]
|
|
89
89
|
'spec.example.email' => ['steve.rogers@example.com','captain.america@marvel.com'],
|
90
90
|
'spec.example.telephone' => ['555-555-5555','123-456-6789'],
|
91
91
|
'spec.example.address' => ['Lower East Side, NY NY'],
|
92
|
-
'spec.example.birthday' =>
|
92
|
+
'spec.example.birthday' => Date.parse('July 4, 1917'),
|
93
93
|
'spec.example.note' => 'Captain America!',
|
94
94
|
}
|
95
95
|
end
|
@@ -125,7 +125,7 @@ if ENV["TEST_REAL_HTTP"]
|
|
125
125
|
'spec.example.telephone' => ['555-123-1234', '555-456-6789'],
|
126
126
|
'spec.example.address' =>
|
127
127
|
['344 Clinton St., Apt. #3B, Metropolis', 'The Fortess of Solitude, North Pole'],
|
128
|
-
'spec.example.birthday' =>
|
128
|
+
'spec.example.birthday' => Date.parse('June 18, 1938'),
|
129
129
|
'spec.example.note' =>
|
130
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
131
|
},
|
@@ -135,7 +135,7 @@ if ENV["TEST_REAL_HTTP"]
|
|
135
135
|
'spec.example.telephone' => ['555-123-6666', '555-456-6666'],
|
136
136
|
'spec.example.address' =>
|
137
137
|
['1007 Mountain Drive, Gotham', 'The Batcave, Gotham'],
|
138
|
-
'spec.example.birthday' =>
|
138
|
+
'spec.example.birthday' => Date.parse('February 19, 1939'),
|
139
139
|
'spec.example.note' =>
|
140
140
|
'Sidekick is Robin. Has problems with the Joker. Member of the justice league.',
|
141
141
|
},
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe CloudQuery::Field do
|
4
|
+
it "should initialize with a name and type" do
|
5
|
+
field = CloudQuery::Field.new(:name, :string)
|
6
|
+
|
7
|
+
field.name.should == :name
|
8
|
+
field.type.should == :string
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#.to_node" do
|
12
|
+
before :each do
|
13
|
+
@field = CloudQuery::Field.new(:name, :string)
|
14
|
+
@builder = Nokogiri::XML::Builder.new { |doc|
|
15
|
+
@field.to_node(doc)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should append a field element" do
|
20
|
+
@builder.doc.at('field').should_not be_nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should set the name attribute" do
|
24
|
+
@builder.doc.at('field')['name'].should == 'name'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should set the type attribute" do
|
28
|
+
@builder.doc.at('field')['type'].should == 'string'
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should set the usage attribute if one is provided" do
|
32
|
+
@field.usage = 'user'
|
33
|
+
@builder = Nokogiri::XML::Builder.new { |doc|
|
34
|
+
@field.to_node(doc)
|
35
|
+
}
|
36
|
+
@builder.doc.at('field')['usage'].should == 'user'
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not have a usage attribute if not set" do
|
40
|
+
@builder.doc.at('field')['usage'].should be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe CloudQuery::Schema do
|
4
|
+
describe "#.to_xml" do
|
5
|
+
before :each do
|
6
|
+
@schema = CloudQuery::Schema.new('user.schema')
|
7
|
+
@schema.fields << CloudQuery::Field.new(:name, :string)
|
8
|
+
@schema.fields << CloudQuery::Field.new(:type, :literal)
|
9
|
+
@schema.fields << CloudQuery::Field.new(:description, :text)
|
10
|
+
|
11
|
+
@document = Nokogiri::XML(@schema.to_xml)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should output a schema element" do
|
15
|
+
@document.at('schema').should_not be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should output the schema name as an attribute" do
|
19
|
+
@document.at('schema')['name'].should == 'user.schema'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should output the store attribute set to yes" do
|
23
|
+
@document.at('schema')['store'].should == 'yes'
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should output each of the fields as elements" do
|
27
|
+
@schema.fields.each do |field|
|
28
|
+
@document.at('schema').css('field').detect { |element|
|
29
|
+
element['name'] == field.name.to_s
|
30
|
+
}.should_not be_nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe CloudQuery::X64 do
|
4
|
+
describe '.encode' do
|
5
|
+
# Built with the expectation of parity with the Python equivalent.
|
6
|
+
it "should return expected values from strings" do
|
7
|
+
CloudQuery::X64.encode('test').should == 'kaEPwp57SCnwfTIcBa8vIq'
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return expected values from integers" do
|
11
|
+
CloudQuery::X64.encode(111).should == 'k........0j'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
require 'yard'
|
3
|
+
|
4
|
+
desc 'Generate documentation'
|
5
|
+
YARD::Rake::YardocTask.new do |t|
|
6
|
+
config = YAML.load(File.read('VERSION.yml'))
|
7
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
8
|
+
|
9
|
+
t.name = "CloudQuery #{version}"
|
10
|
+
t.files = ['lib/**/*.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
task :rdoc => :yardoc
|
14
|
+
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "cloud_query"
|
17
|
+
gem.summary = "Client for Xoopit's CloudQuery API"
|
18
|
+
gem.description = "Client for Xoopit's CloudQuery API"
|
19
|
+
gem.email = "cloudquery@xoopit.com"
|
20
|
+
gem.homepage = "http://github.com/xoopit/cloudquery_ruby"
|
21
|
+
gem.authors = ["Xoopit", "Cameron Walters", "nb.io", "Pat Allan"]
|
22
|
+
gem.files = FileList[
|
23
|
+
"lib/**/*.rb",
|
24
|
+
"LICENCE",
|
25
|
+
"README.markdown",
|
26
|
+
"tasks/**/*.rb"
|
27
|
+
]
|
28
|
+
gem.test_files = FileList["spec/**/*_spec.rb"]
|
29
|
+
# gem.rubyforge_project = "cloudquery"
|
30
|
+
|
31
|
+
gem.add_dependency 'rack', '>= 1.0'
|
32
|
+
gem.add_dependency 'json', '>= 1.1.4'
|
33
|
+
gem.add_dependency 'taf2-curb', '>= 0.2.8.0'
|
34
|
+
gem.add_dependency 'nokogiri', '>= 1.3.1'
|
35
|
+
gem.add_dependency 'uuidtools', '>= 2.0.0'
|
36
|
+
|
37
|
+
# See http://www.rubygems.org/read/chapter/20 for additional gemspec settings
|
38
|
+
end
|
data/tasks/testing.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec/rake/spectask'
|
2
|
+
|
3
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
4
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
5
|
+
t.spec_opts << "-c"
|
6
|
+
end
|
7
|
+
|
8
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
t.spec_opts << "-c"
|
11
|
+
|
12
|
+
t.rcov_opts = ['--exclude', 'spec', '--exclude', 'gems']
|
13
|
+
t.rcov = true
|
14
|
+
end
|
metadata
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xoopit-cloud_query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
+
- Xoopit
|
7
8
|
- Cameron Walters
|
8
9
|
- nb.io
|
10
|
+
- Pat Allan
|
9
11
|
autorequire:
|
10
12
|
bindir: bin
|
11
13
|
cert_chain: []
|
12
14
|
|
13
|
-
date: 2009-
|
15
|
+
date: 2009-07-16 00:00:00 -07:00
|
14
16
|
default_executable:
|
15
17
|
dependencies:
|
16
18
|
- !ruby/object:Gem::Dependency
|
@@ -43,8 +45,28 @@ dependencies:
|
|
43
45
|
- !ruby/object:Gem::Version
|
44
46
|
version: 0.2.8.0
|
45
47
|
version:
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: nokogiri
|
50
|
+
type: :runtime
|
51
|
+
version_requirement:
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 1.3.1
|
57
|
+
version:
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: uuidtools
|
60
|
+
type: :runtime
|
61
|
+
version_requirement:
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 2.0.0
|
67
|
+
version:
|
46
68
|
description: Client for Xoopit's CloudQuery API
|
47
|
-
email:
|
69
|
+
email: cloudquery@xoopit.com
|
48
70
|
executables: []
|
49
71
|
|
50
72
|
extensions: []
|
@@ -53,17 +75,20 @@ extra_rdoc_files:
|
|
53
75
|
- LICENSE
|
54
76
|
- README.markdown
|
55
77
|
files:
|
56
|
-
- LICENSE
|
57
78
|
- README.markdown
|
58
|
-
- Rakefile
|
59
|
-
- VERSION.yml
|
60
|
-
- lib/cloudquery.rb
|
61
79
|
- lib/cloud_query.rb
|
62
|
-
-
|
63
|
-
-
|
64
|
-
-
|
80
|
+
- lib/cloud_query/client.rb
|
81
|
+
- lib/cloud_query/crypto.rb
|
82
|
+
- lib/cloud_query/field.rb
|
83
|
+
- lib/cloud_query/request.rb
|
84
|
+
- lib/cloud_query/schema.rb
|
85
|
+
- lib/cloud_query/x64.rb
|
86
|
+
- lib/cloudquery.rb
|
87
|
+
- tasks/distribution.rb
|
88
|
+
- tasks/testing.rb
|
89
|
+
- LICENSE
|
65
90
|
has_rdoc: true
|
66
|
-
homepage: http://github.com/
|
91
|
+
homepage: http://github.com/xoopit/cloudquery_ruby
|
67
92
|
post_install_message:
|
68
93
|
rdoc_options:
|
69
94
|
- --charset=UTF-8
|
@@ -87,7 +112,9 @@ rubyforge_project:
|
|
87
112
|
rubygems_version: 1.2.0
|
88
113
|
signing_key:
|
89
114
|
specification_version: 2
|
90
|
-
summary: Client for Xoopit's
|
115
|
+
summary: Client for Xoopit's CloudQuery API
|
91
116
|
test_files:
|
92
117
|
- spec/cloudquery_spec.rb
|
93
|
-
- spec/
|
118
|
+
- spec/lib/cloud_query/field_spec.rb
|
119
|
+
- spec/lib/cloud_query/schema_spec.rb
|
120
|
+
- spec/lib/cloud_query/x64_spec.rb
|
data/Rakefile
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
require 'rake'
|
2
|
-
|
3
|
-
begin
|
4
|
-
require 'jeweler'
|
5
|
-
Jeweler::Tasks.new do |gem|
|
6
|
-
gem.name = "cloud_query"
|
7
|
-
gem.summary = "Client for Xoopit's CloudQuery API"
|
8
|
-
gem.email = "us@nb.io"
|
9
|
-
gem.homepage = "http://github.com/xoopit/cloudquery_ruby"
|
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
|
-
gem.add_dependency('rack', ">= 1.0")
|
15
|
-
gem.add_dependency('json', ">= 1.1.4")
|
16
|
-
gem.add_dependency('taf2-curb', ">= 0.2.8.0")
|
17
|
-
|
18
|
-
|
19
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
20
|
-
end
|
21
|
-
rescue LoadError
|
22
|
-
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
23
|
-
end
|
24
|
-
|
25
|
-
require 'spec/rake/spectask'
|
26
|
-
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
-
spec.libs << 'lib' << 'spec'
|
28
|
-
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
-
spec.spec_opts << "-c"
|
30
|
-
end
|
31
|
-
|
32
|
-
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
-
spec.libs << 'lib' << 'spec'
|
34
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
-
spec.rcov = true
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
task :default => :spec
|
40
|
-
|
41
|
-
require 'rake/rdoctask'
|
42
|
-
Rake::RDocTask.new do |rdoc|
|
43
|
-
if File.exist?('VERSION.yml')
|
44
|
-
config = YAML.load(File.read('VERSION.yml'))
|
45
|
-
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
46
|
-
else
|
47
|
-
version = ""
|
48
|
-
end
|
49
|
-
|
50
|
-
rdoc.rdoc_dir = 'rdoc'
|
51
|
-
rdoc.title = "CloudQuery #{version}"
|
52
|
-
rdoc.rdoc_files.include('README*')
|
53
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
-
end
|
55
|
-
|
56
|
-
# begin
|
57
|
-
# require 'rake/contrib/sshpublisher'
|
58
|
-
# namespace :rubyforge do
|
59
|
-
#
|
60
|
-
# desc "Release gem and RDoc documentation to RubyForge"
|
61
|
-
# task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
62
|
-
#
|
63
|
-
# namespace :release do
|
64
|
-
# desc "Publish RDoc to RubyForge."
|
65
|
-
# task :docs => [:rdoc] do
|
66
|
-
# config = YAML.load(
|
67
|
-
# File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
68
|
-
# )
|
69
|
-
#
|
70
|
-
# host = "#{config['username']}@rubyforge.org"
|
71
|
-
# remote_dir = "/var/www/gforge-projects/cloudquery/"
|
72
|
-
# local_dir = 'rdoc'
|
73
|
-
#
|
74
|
-
# Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
75
|
-
# end
|
76
|
-
# end
|
77
|
-
# end
|
78
|
-
# rescue LoadError
|
79
|
-
# puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
80
|
-
# end
|
data/VERSION.yml
DELETED
data/spec/example_schema.xml
DELETED
@@ -1,26 +0,0 @@
|
|
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
DELETED