riakrest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +41 -0
  3. data/PostInstall.txt +2 -0
  4. data/README.rdoc +51 -0
  5. data/Rakefile +24 -0
  6. data/examples/auto_update_data.rb +50 -0
  7. data/examples/auto_update_links.rb +48 -0
  8. data/examples/basic_client.rb +33 -0
  9. data/examples/basic_resource.rb +34 -0
  10. data/examples/json_data_resource.rb +32 -0
  11. data/examples/linked_resource.rb +113 -0
  12. data/examples/multiple_resources.rb +43 -0
  13. data/lib/riakrest/core/exceptions.rb +73 -0
  14. data/lib/riakrest/core/jiak_bucket.rb +146 -0
  15. data/lib/riakrest/core/jiak_client.rb +316 -0
  16. data/lib/riakrest/core/jiak_data.rb +265 -0
  17. data/lib/riakrest/core/jiak_link.rb +131 -0
  18. data/lib/riakrest/core/jiak_object.rb +233 -0
  19. data/lib/riakrest/core/jiak_schema.rb +242 -0
  20. data/lib/riakrest/core/query_link.rb +156 -0
  21. data/lib/riakrest/data/jiak_data_hash.rb +182 -0
  22. data/lib/riakrest/resource/jiak_resource.rb +628 -0
  23. data/lib/riakrest/version.rb +7 -0
  24. data/lib/riakrest.rb +164 -0
  25. data/riakrest.gemspec +38 -0
  26. data/script/console +10 -0
  27. data/script/destroy +14 -0
  28. data/script/generate +14 -0
  29. data/spec/core/exceptions_spec.rb +18 -0
  30. data/spec/core/jiak_bucket_spec.rb +103 -0
  31. data/spec/core/jiak_client_spec.rb +358 -0
  32. data/spec/core/jiak_link_spec.rb +77 -0
  33. data/spec/core/jiak_object_spec.rb +210 -0
  34. data/spec/core/jiak_schema_spec.rb +184 -0
  35. data/spec/core/query_link_spec.rb +128 -0
  36. data/spec/data/jiak_data_hash_spec.rb +14 -0
  37. data/spec/resource/jiak_resource_spec.rb +128 -0
  38. data/spec/riakrest_spec.rb +17 -0
  39. data/spec/spec.opts +5 -0
  40. data/spec/spec_helper.rb +12 -0
  41. data/tasks/rspec.rake +21 -0
  42. metadata +113 -0
@@ -0,0 +1,146 @@
1
+ module RiakRest
2
+
3
+ # Data is stored on the Jiak server by key under a bucket. During Jiak
4
+ # interaction, the bucket on the server has an associated schema which
5
+ # determines permissible data interaction. See JiakSchema for a discussion of
6
+ # schemas in Jiak. Since the bucket schema can be changed dynamically,
7
+ # schemas can be viewed more as a loose type system rather than an onerous
8
+ # restriction.
9
+ #
10
+ # In RiakRest buckets have an associated JiakData class, and each JiakData
11
+ # class has an associated JiakSchema. These associations facility setting and
12
+ # maintaining the current schema in use for a Jiak bucket. Dynamically
13
+ # changing the bucket schema means you can have either homogeneous (simplest)
14
+ # or heterogenous data in a single Jiak server bucket. It also means you can
15
+ # define multiple JiakData classes that effectively present different "views"
16
+ # (via schemas) into the same data stored on the Jiak server. These classes
17
+ # act like types that can determine which fields are accessible for reading
18
+ # and writing data. The JiakData class associated with a bucket is also used
19
+ # to marshal user-defined data going to and from the Jiak server.
20
+ #
21
+ # JiakResource greatly eases the bookkeeping necessary for heterogenous, as
22
+ # well as homogenous, data interaction with the Jiak server.
23
+ class JiakBucket
24
+
25
+ attr_reader :schema
26
+ attr_accessor :name, :data_class, :params
27
+
28
+ # :call-seq:
29
+ # JiakBucket.new(name,data_class,params={}) -> JiakBucket
30
+ #
31
+ # Create a bucket for use in Jiak interaction.
32
+ #
33
+ # Valid optional parameters are <code>params</code> hash are <code>:reads,
34
+ # :writes, :durable_writes, :waits</code>. See JiakClient#store,
35
+ # JiakClient#get, and JiakClient#delete for discriptions of these
36
+ # parameters.
37
+ #
38
+ # Raise JiakBucketException if the bucket name is not a non-empty string or
39
+ # the data class has not included JiakData.
40
+ def initialize(name,data_class,params={})
41
+ @name = transform_name(name)
42
+ @data_class = check_data_class(data_class)
43
+ @params = check_params(params)
44
+ end
45
+
46
+ # :call-seq:
47
+ # name = gname
48
+ #
49
+ # Set the name of the Jiak bucket.
50
+ #
51
+ # Raise JiakBucketException if not a non-empty string.
52
+ def name=(gname)
53
+ @name = transform_name(gname)
54
+ end
55
+
56
+ # :call-seq:
57
+ # data_class = klass
58
+ #
59
+ # Set the class for the data to be stored or retrieved from the bucket.
60
+ #
61
+ # Raise JiakBucketException if the data class has not included JiakData.
62
+ def data_class=(data_class)
63
+ @data_class = check_data_class(data_class)
64
+ end
65
+
66
+ # :call-seq:
67
+ # bucket.params = params
68
+ #
69
+ # Set default params for Jiak client requests. See JiakBucket#new for
70
+ # valid parameters.
71
+ #
72
+ def params=(params)
73
+ @params = check_params(params)
74
+ end
75
+
76
+ # :call-seq:
77
+ # bucket.schema -> JiakSchema
78
+ #
79
+ # Gets the data schema for this bucket. This call does not access the
80
+ # server, but rather returns the schema of the current data class
81
+ # associated with the bucket. This association is required to establish the
82
+ # Jiak server schema in the first place, so as long as the Jiak server
83
+ # schema has not be altered by another call to JiakClient#set_schema the
84
+ # information returned via this call will be current.
85
+ def schema
86
+ data_class.schema
87
+ end
88
+
89
+ # :call-seq:
90
+ # bucket == other -> true or false
91
+ #
92
+ # Equality -- JiakBuckets are equal if they contain the same attribute
93
+ # values.
94
+ def ==(other)
95
+ (@name == other.name &&
96
+ @data_class == other.data_class &&
97
+ @params == other.params) rescue false
98
+ end
99
+
100
+ # :call-seq:
101
+ # jiak_bucket.eql?(other) -> true or false
102
+ #
103
+ # Returns <code>true</code> if <i>jiak_bucket</i> and <i>other</i> contain
104
+ # the same attribute values.
105
+ def eql?(other)
106
+ (@name.eql?(other.name) &&
107
+ @data_class.eql?(other.data_class) &&
108
+ @params.eql?(other.params)) rescue false
109
+ end
110
+
111
+ def hash # :nodoc:
112
+ @name.hash + @data_class.hash + @params.hash
113
+ end
114
+
115
+ def transform_name(name)
116
+ unless name.is_a?(String)
117
+ raise JiakBucketException, "Name must be a string"
118
+ end
119
+ b_name = name.dup
120
+ b_name.strip!
121
+ raise JiakBucketException, "Name cannot be empty" if b_name.empty?
122
+ b_name
123
+ end
124
+ private :transform_name
125
+
126
+ def check_data_class(data_class)
127
+ unless data_class.include?(JiakData)
128
+ raise JiakBucketException, "Data class must be type of JiakData."
129
+ end
130
+ data_class
131
+ end
132
+ private :check_data_class
133
+
134
+ def check_params(params)
135
+ valid = [:reads,:writes,:durable_writes,:waits]
136
+ err = params.select {|k,v| !valid.include?(k)}
137
+ unless err.empty?
138
+ raise JiakBucketException, "unrecognized request params: #{err.keys}"
139
+ end
140
+ params
141
+ end
142
+ private :check_params
143
+
144
+ end
145
+
146
+ end
@@ -0,0 +1,316 @@
1
+ module RiakRest
2
+
3
+ # Restful client interaction with a Riak document store via a JSON
4
+ # interface. See RiakRest for a basic example usage.
5
+ class JiakClient
6
+
7
+ # :stopdoc:
8
+ APP_JSON = 'application/json'
9
+ JSON_DATA = 'json'
10
+
11
+ RETURN_BODY = 'returnbody'
12
+ READS = 'r'
13
+ WRITES = 'w'
14
+ DURABLE_WRITES = 'dw'
15
+ RESPONSE_WAITS = 'rw'
16
+
17
+ KEYS='keys'
18
+ SCHEMA='schema'
19
+ # :startdoc:
20
+
21
+ # :call-seq:
22
+ # JiakClient.new(uri) -> uri
23
+ #
24
+ # Create a new client for Riak RESTful (Jiak) interaction with the server at
25
+ # the specified URI.
26
+ #
27
+ # Raise JiakClientException if the server URI is not a string.
28
+ #
29
+ def initialize(uri='http://127.0.0.1:8002/jiak/')
30
+ unless uri.is_a?(String)
31
+ raise JiakClientException, "Jiak server URI shoud be a String."
32
+ end
33
+ @uri = uri
34
+ @uri += '/' unless @uri.end_with?('/')
35
+ @uri
36
+ end
37
+
38
+ # :call-seq:
39
+ # set_schema(bucket) -> nil
40
+ #
41
+ # Set the Jiak server schema for a bucket. The schema is determined by the
42
+ # JiakData associated with the JiakBucket.
43
+ #
44
+ # Raise JiakClientException if the bucket is not a JiakBucket.
45
+ #
46
+ def set_schema(bucket)
47
+ unless bucket.is_a?(JiakBucket)
48
+ raise JiakClientException, "Bucket must be a JiakBucket."
49
+ end
50
+ resp = RestClient.put(jiak_uri(bucket),
51
+ bucket.schema.to_jiak,
52
+ :content_type => APP_JSON,
53
+ :data_type => JSON_DATA,
54
+ :accept => APP_JSON)
55
+ end
56
+
57
+ # :call-seq:
58
+ # schema(bucket) -> JiakSchema
59
+ #
60
+ # Get the data schema for a bucket on a Jiak server. This involves a call
61
+ # to the Jiak server. See JiakBucket#schema for a way to get this
62
+ # information without server access.
63
+ def schema(bucket)
64
+ JiakSchema.from_jiak(bucket_info(bucket,SCHEMA))
65
+ end
66
+
67
+ # :call-seq:
68
+ # client.keys(bucket) -> array
69
+ #
70
+ # Get an Array of all known keys for the specified bucket. Since key lists
71
+ # are updated asynchronously the returned array can be out of date
72
+ # immediately after a put or delete.
73
+ def keys(bucket)
74
+ bucket_info(bucket,KEYS)
75
+ end
76
+
77
+ # :call-seq:
78
+ # store(object,opts={}) -> JiakObject or key
79
+ #
80
+ # Stores user-defined data (wrapped in a JiakObject) on the Jiak
81
+ # server. JiakData#to_jiak is used to prepare user-defined data for JSON
82
+ # transport to Jiak. That call is expected to return a Ruby hash
83
+ # representation of the writable JiakData fields that are JSONized for HTTP
84
+ # transport. Successful server writes return either the storage key or the
85
+ # stored JiakObject depending on the option <code>key</code>. The object
86
+ # for storage must be JiakObject. Valid options are:
87
+ #
88
+ # <code>:object</code> :: If <code>true</code>, on success return the stored JiakObject (which includes Jiak metadata); otherwise return just the key. Default is <code>false</code>, which returns the key.
89
+ # <code>:writes</code> :: The number of Riak nodes that must successfully store the data.
90
+ # <code>:durable_writes</code> :: The number of Riak nodes (<code>< writes</code>) that must successfully store the data in a durable manner.
91
+ # <code>:reads</code> :: The number of Riak nodes that must successfully read data if a JiakObject is being returned.
92
+ #
93
+ # If any of the request parameters <code>:writes, :durable_writes,
94
+ # :reads</code> are not set, each first defaults to the value set for the
95
+ # JiakBucket in the JiakObject, then to the value set on the Riak
96
+ # cluster. In general the values set on the Riak cluster should suffice.
97
+ #
98
+ # Raise JiakClientException if object not a JiakObject or illegal options
99
+ # are passed.<br/>
100
+ # Raise JiakResourceException on RESTful HTTP errors.
101
+ #
102
+ def store(jobj,opts={})
103
+ params = jobj.bucket.params
104
+ req_params = {
105
+ WRITES => opts[:writes] || params[:writes],
106
+ DURABLE_WRITES => opts[:durable_writes] || params[:durable_writes],
107
+ READS => opts[:reads] || params[:reads]
108
+ }
109
+ req_params[RETURN_BODY] = opts[:object] if opts[:object]
110
+
111
+ begin
112
+ uri = jiak_uri(jobj.bucket,jobj.key,req_params)
113
+ payload = jobj.to_jiak
114
+ headers = {
115
+ :content_type => APP_JSON,
116
+ :data_type => JSON_DATA,
117
+ :accept => APP_JSON }
118
+ # Decision tree:
119
+ # If key empty POST
120
+ # Else PUT
121
+ # If object true, return JiakObject
122
+ # Else
123
+ # POST - parse key from location header
124
+ # PUT - return the given key
125
+ key_empty = jobj.key.empty?
126
+ if(key_empty)
127
+ resp = RestClient.post(uri,payload,headers)
128
+ else
129
+ resp = RestClient.put(uri,payload,headers)
130
+ end
131
+
132
+ if(req_params[RETURN_BODY])
133
+ JiakObject.from_jiak(JSON.parse(resp),jobj.bucket.data_class)
134
+ elsif(key_empty)
135
+ resp.headers[:location].split('/').last
136
+ else
137
+ jobj.key
138
+ end
139
+ rescue RestClient::ExceptionWithResponse => err
140
+ fail_with_response("put", err)
141
+ rescue RestClient::Exception => err
142
+ fail_with_message("put", err)
143
+ end
144
+ end
145
+
146
+ # :call-seq:
147
+ # get(bucket,key,opts={}) -> JiakObject
148
+ #
149
+ # Get data stored on a Jiak server at a bucket/key. The user-defined data
150
+ # stored on the Jiak server is inflated inside a JiakObject that also
151
+ # includes Riak storage information. Data inflation is controlled by the
152
+ # data class associated with the bucket of this call.
153
+ #
154
+ # Since the data schema validation that occurs on the Jiak server validates
155
+ # to the last schema set for that Jiak server bucket, it is imperative that
156
+ # schema be the same (or at least consistent) with the schema associated
157
+ # with this bucket at retrieval time. If you only store homogeneous objects
158
+ # in a bucket this will not be an issue.
159
+ #
160
+ # The bucket must be a JiakBucket and the key must be a non-empty
161
+ # String. Valid options are:
162
+ #
163
+ # <code>:reads</code> --- The number of Riak nodes that must successfully
164
+ # reply with the data. If not set, defaults first to the value set for the
165
+ # JiakBucket, then to the value set on the Riak cluster. In general the
166
+ # values set on the Riak cluster should suffice.
167
+ #
168
+ # Raise JiakClientException if bucket not a JiakBucket.<br/>
169
+ # Raise JiakResourceNotFound if resource not found on Jiak server.<br/>
170
+ # Raise JiakResourceException on other HTTP RESTful errors.
171
+ #
172
+ def get(bucket,key,opts={})
173
+ unless bucket.is_a?(JiakBucket)
174
+ raise JiakClientException, "Bucket must be a JiakBucket."
175
+ end
176
+ req_params = {READS => opts[:reads] || bucket.params[:reads]}
177
+
178
+ begin
179
+ uri = jiak_uri(bucket,key,req_params)
180
+ resp = RestClient.get(uri, :accept => APP_JSON)
181
+ JiakObject.from_jiak(JSON.parse(resp),bucket.data_class)
182
+ rescue RestClient::ResourceNotFound => err
183
+ raise JiakResourceNotFound, "failed get: #{err.message}"
184
+ rescue RestClient::ExceptionWithResponse => err
185
+ fail_with_response("get", err)
186
+ rescue RestClient::Exception => err
187
+ fail_with_message("get",err)
188
+ end
189
+ end
190
+
191
+ # :call-seq:
192
+ # delete(bucket,key,opts={}) -> true or false
193
+ #
194
+ # Delete the JiakObject stored at the bucket/key. Valid options are:
195
+ #
196
+ # <code>:waits</code> --- The number of Riak nodes that must reply the
197
+ # delete has occurred before success. If not set, defaults first to the
198
+ # value set for the JiakBucket, then to the value set on the Riak
199
+ # cluster. In general the values set on the Riak cluster should suffice.
200
+ #
201
+ # Raise JiakResourceException on RESTful HTTP errors.
202
+ #
203
+ def delete(bucket,key,opts={})
204
+ begin
205
+ req_params = {RESPONSE_WAITS => opts[:waits] || bucket.params[:waits]}
206
+ uri = jiak_uri(bucket,key,req_params)
207
+ RestClient.delete(uri, :accept => APP_JSON)
208
+ true
209
+ rescue RestClient::ExceptionWithResponse => err
210
+ fail_with_response("delete", err)
211
+ rescue RestClient::Exception => err
212
+ fail_with_message("delete", err)
213
+ end
214
+ end
215
+
216
+ # :call-seq:
217
+ # walk(bucket,key,query,data_class) -> array
218
+ #
219
+ # Return an array of JiakObjects retrieved by walking a query links
220
+ # array starting with the links for the object at bucket/key. The
221
+ # data_class is used to inflate the data objects returned in the
222
+ # JiakObject array.
223
+ #
224
+ # See QueryLink for a description of the <code>query</code> structure.
225
+ def walk(bucket,key,query,data_class)
226
+ begin
227
+ start = jiak_uri(bucket,key)
228
+ case query
229
+ when QueryLink
230
+ uri = start+'/'+query.for_uri
231
+ when Array
232
+ uri = query.inject(start) {|build,link| build+'/'+link.for_uri}
233
+ else
234
+ raise QueryLinkException, 'failed: query must be '+
235
+ 'a QueryLink or an Array of QueryLink objects'
236
+ end
237
+ resp = RestClient.get(uri, :accept => APP_JSON)
238
+ # JSON.parse(resp)['results'][0]
239
+ JSON.parse(resp)['results'][0].map do |jiak|
240
+ JiakObject.from_jiak(jiak,data_class)
241
+ end
242
+ rescue RestClient::ExceptionWithResponse => err
243
+ fail_with_response("put", err)
244
+ rescue RestClient::Exception => err
245
+ fail_with_message("put", err)
246
+ end
247
+ end
248
+
249
+ # :call-seq:
250
+ # client.uri -> string
251
+ #
252
+ # String representation of the base URI of the Jiak server.
253
+ def uri
254
+ @uri
255
+ end
256
+
257
+ # :call-seq:
258
+ # client == other -> true or false
259
+ #
260
+ # Equality -- JiakClients are equal if they have the same URI
261
+ def ==(other)
262
+ (@uri == other.uri) rescue false
263
+ end
264
+
265
+ # :call-seq:
266
+ # eql?(other) -> true or false
267
+ #
268
+ # Returns <code>true</code> if <code>other</code> is a JiakClint with the
269
+ # same URI.
270
+ def eql?(other)
271
+ other.is_a?(JiakClient) &&
272
+ @uri.eql?(other.uri)
273
+ end
274
+
275
+ def hash # :nodoc:
276
+ @uri.hash
277
+ end
278
+
279
+ # Build the URI for accessing the Jiak server.
280
+ def jiak_uri(bucket,key="",params={})
281
+ uri = "#{@uri}#{URI.encode(bucket.name)}"
282
+ uri += "/#{URI.encode(key)}" unless key.empty?
283
+ qstring = params.reject {|k,v| v.nil?}.map{|k,v| "#{k}=#{v}"}.join('&')
284
+ uri += "?#{URI.encode(qstring)}" unless qstring.empty?
285
+ uri
286
+ end
287
+ private :jiak_uri
288
+
289
+ # Get either the schema or keys for the bucket.
290
+ def bucket_info(bucket,info)
291
+ ignore = (info == SCHEMA) ? KEYS : SCHEMA
292
+ begin
293
+ uri = jiak_uri(bucket,"",{ignore => false})
294
+ JSON.parse(RestClient.get(uri, :accept => APP_JSON))[info]
295
+ rescue RestClient::ExceptionWithResponse => err
296
+ fail_with_response("get", err)
297
+ rescue RestClient::Exception => err
298
+ fail_with_message("get", err)
299
+ end
300
+ end
301
+ private :bucket_info
302
+
303
+ def fail_with_response(action,err)
304
+ raise( JiakResourceException,
305
+ "failed #{action}: HTTP code #{err.http_code}: #{err.http_body}")
306
+ end
307
+ private :fail_with_response
308
+
309
+ def fail_with_message(action,err)
310
+ raise JiakResourceException, "failed #{action}: #{err.message}"
311
+ end
312
+ private :fail_with_message
313
+
314
+ end
315
+
316
+ end