riakrest 0.0.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.
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