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,265 @@
1
+ module RiakRest
2
+
3
+ # All end-user data stored via RiakRest is contained in user-defined data
4
+ # objects. To make a user-defined data object, include module JiakData into
5
+ # your class definition. This allows creating a class that can be used to
6
+ # create instances of your user-defined data. Note JiakData does create
7
+ # user-data instances, rather it facilitates creating the class you use to
8
+ # create user-data instances.
9
+ #
10
+ # The four class methods <code>allowed, required, readable, writable</code>
11
+ # defined in JiakData#ClassMethods are used to declare the schema for
12
+ # structured Jiak interaction with user-defined data. The <code>allowed</code>
13
+ # method is mandatory; the other methods take the defaults as described in
14
+ # JiakSchema. See JiakSchema for discussion on structured data interaction.
15
+ #
16
+ # User-defined data classes must override JiakData#ClassMethods#jiak_create
17
+ # (creating your user-defined data from the information returned by Jiak) and
18
+ # JiakData#for_jiak (providing the information to be sent to Jiak). The
19
+ # default implementations of these methods throw JiakDataException to
20
+ # enforce this override.
21
+ #
22
+ # JiakData provides a default data key generator that returns nil, which
23
+ # instructs the Jiak server to generate a random key on first data
24
+ # storage. To explicitly set the key override JiakData#keygen to return
25
+ # whatever string you want to use for the key. Keys need to be unique within
26
+ # each bucket on the Jiak server but can be the same across distinct buckets.
27
+ #
28
+ # ===Example
29
+ # <code>
30
+ # class FooBarBaz
31
+ # include JiakData
32
+ #
33
+ # allowed :foo, :bar, :baz
34
+ # required :foo
35
+ # readable :foo, :bar
36
+ # writable :foo, :baz
37
+ #
38
+ # def initialize(foo,bar,baz)
39
+ # @foo = foo
40
+ # @bar = bar
41
+ # @baz = baz
42
+ # end
43
+ #
44
+ # def self.jiak_create(jiak)
45
+ # new(jiak['foo'],jiak['bar'])
46
+ # end
47
+ #
48
+ # def for_jiak
49
+ # { :foo => @foo,
50
+ # :baz => @baz
51
+ # }
52
+ # end
53
+ #
54
+ # def keygen
55
+ # "#{foo}"
56
+ # end
57
+ # end
58
+ # </code>
59
+ #
60
+ # Note that FooBarBaz <code>bar</code> is readable but not writable and
61
+ # <code>baz</code> is writable but not readable. Also note
62
+ # <code>for_jiak</code> only provides the <code>writable</code> fields for
63
+ # writing to the Jiak server and <code>jiak_create</code> only initializes
64
+ # from the <code>readable</code> fields returned by the Jiak server. The
65
+ # above definition means a user of FooBarBaz could change <code>baz</code>
66
+ # but not see that change and could access <code>bar</code> but not change
67
+ # it. This could be useful if either another JiakData class (with a different
68
+ # schema) created access into the same data, allowing <code>bar</code> writes
69
+ # and <code>baz</code> reads, or if Riak server-side manipulation affected
70
+ # those fields. The constraints declared in FooBarBaz simply provide
71
+ # a particular structured interaction of data on a Jiak server.
72
+ #
73
+ # If only one JiakData will be used for a particular type of data on the Jiak
74
+ # server it is desirable to have the <code>readable</code> and
75
+ # <code>writable</code> fields be the same as <code>allowed</code>. Setting
76
+ # only <code>allowed</code> fields provide this reasonable default, hence only
77
+ # that call is mandatory.
78
+ module JiakData
79
+
80
+ # ----------------------------------------------------------------------
81
+ # Class methods
82
+ # ----------------------------------------------------------------------
83
+
84
+ # Class methods for use in creating a user-defined JiakData. The methods
85
+ # <code>allowed, required, readable, writable</code> define the JiakSchema
86
+ # for this JiakData. See JiakSchema for discussion on the use of schemas in
87
+ # Riak.
88
+ module ClassMethods
89
+
90
+ # :call-seq:
91
+ # allowed :f1, ..., :fn -> array
92
+ #
93
+ # Fields allowed in Jiak interactions. Returns an array of the allowed
94
+ # fields.
95
+ #
96
+ def allowed(*fields)
97
+ arr_fields = create_array(fields)
98
+ fields.each {|field| attr_accessor field}
99
+ @schema = JiakSchema.new(arr_fields)
100
+ arr_fields
101
+ end
102
+
103
+ # :call-seq:
104
+ # required :f1, ..., :fn -> array
105
+ #
106
+ # Fields required during in Jiak interactions. Returns an array of the
107
+ # required fields.
108
+ #
109
+ def required(*fields)
110
+ set_fields('required_fields',*fields)
111
+ end
112
+
113
+ # :call-seq:
114
+ # readable :f1, ..., :fn -> array
115
+ #
116
+ # Fields returned by Jiak on retrieval. Returns an array of the fields in
117
+ # the read mask.
118
+ #
119
+ def readable(*fields)
120
+ set_fields('read_mask',*fields)
121
+ end
122
+
123
+ # :call-seq:
124
+ # writable :f1, ..., :fn -> arry
125
+ #
126
+ # Fields that can be written during Jiak interaction. Returns an array of
127
+ # the fields in the write mask.
128
+ #
129
+ def writable(*fields)
130
+ set_fields('write_mask',*fields)
131
+ end
132
+
133
+ # :call-seq:
134
+ # readwrite :f1, ..., :fn -> array
135
+ #
136
+ # Set the read and write masks to the same fields. Returns an array of
137
+ # the fields in the masks.
138
+ def readwrite(*fields)
139
+ arr_fields = set_fields('readwrite',*fields)
140
+ arr_fields
141
+ end
142
+
143
+ def set_fields(which,*fields)
144
+ arr_fields = create_array(fields)
145
+ check_allowed(arr_fields)
146
+ @schema.send("#{which}=",arr_fields)
147
+ arr_fields
148
+ end
149
+ private :set_fields
150
+
151
+ # :call-seq:
152
+ # JiakData.schema -> JiakSchema
153
+ #
154
+ # Get a JiakSchema representation determined by
155
+ # <code>allowed, required, readable, writable</code>.
156
+ #
157
+ def schema
158
+ @schema
159
+ end
160
+
161
+ # :call-seq:
162
+ # JiakData.jiak_create(jiak) -> JiakData
163
+ #
164
+ # Create an instance of user-defined data object from the fields read
165
+ # by Jiak. These fields are determined by the read mask of the
166
+ # structured Jiak interaction. See JiakSchema for read mask discussion.
167
+ #
168
+ # User-defined data classes must override this method. The method is
169
+ # called during the creation of a JiakObject from information returned by
170
+ # Jiak. The JiakObject contains the user-defined data itself. You do not
171
+ # call this method explicitly.
172
+ #
173
+ # ====Example
174
+ # <code>
175
+ # def initialize(f1,f2)
176
+ # @f1 = f1
177
+ # @f2 = f2
178
+ # end
179
+ # def jiak_create(jiak)
180
+ # new(jiak['f1'], jiak['f2'])
181
+ # end
182
+ # </code>
183
+ #
184
+ # Raise JiakDataException if not explicitly defined by user-defined data class.
185
+ def jiak_create(json)
186
+ raise JiakDataException, "#{self} must define jiak_create"
187
+ end
188
+
189
+ def create_array(*fields)
190
+ if(fields.size == 1 && fields[0].is_a?(Array))
191
+ array = fields[0]
192
+ else
193
+ array = fields
194
+ end
195
+ array.map {|field| field}
196
+ array
197
+ end
198
+ private :create_array
199
+
200
+ def check_allowed(fields)
201
+ allowed_fields = @schema.allowed_fields
202
+ fields.each do |field|
203
+ unless allowed_fields.include?(field)
204
+ raise JiakDataException, "field '#{field}' not allowed"
205
+ end
206
+ end
207
+ end
208
+ private :check_allowed
209
+ end
210
+
211
+ def self.included(including_class) # :nodoc:
212
+ including_class.extend(ClassMethods)
213
+ end
214
+
215
+ # ----------------------------------------------------------------------
216
+ # Instance methods
217
+ # ----------------------------------------------------------------------
218
+
219
+ # :call-seq:
220
+ # for_jiak -> hash
221
+ #
222
+ # Provide a hash structure of the data to write to Jiak. The fields for
223
+ # this structure should come from the write mask of the structured Jiak
224
+ # interaction. See JiakSchema for write mask discussion.
225
+ #
226
+ # User-defined data classes must override this method. The method is called
227
+ # during the creation of a JiakObject to send information to Jiak. The
228
+ # JiakObject contains the user-defined data itself. You do not call this
229
+ # method explicitly.
230
+ #
231
+ # ====Example
232
+ # <code>
233
+ # def for_jiak
234
+ # { :writable_f1 => @writable_f1,
235
+ # :writable_f2 => @writable_f2
236
+ # }
237
+ # end
238
+ # </code>
239
+ #
240
+ # Raise JiakDataException if not explicitly defined by user-defined data class.
241
+ def for_jiak
242
+ raise JiakDataException, "#{self} must define for_jiak"
243
+ end
244
+
245
+ # :call-seq:
246
+ # keygen -> string
247
+ #
248
+ # Generate Jiak key for data. Default implementation returns
249
+ # <code>nil</code> which instructs the Jiak server to generate a random
250
+ # key. Override for user-defined data behaviour.
251
+ #
252
+ # ====Example
253
+ # A simple implementation would look like:
254
+ # <code>
255
+ # def keygen
256
+ # f1.to_s
257
+ # end
258
+ # </code>
259
+ def keygen
260
+ nil
261
+ end
262
+
263
+ end
264
+ end
265
+
@@ -0,0 +1,131 @@
1
+ module RiakRest
2
+
3
+ # Represents a link between object in Jiak. JiakLinks are used to link a
4
+ # JiakObject to another JiakObject at a bucket/key. The tag allows the link
5
+ # to be traversed later using a QueryLink.
6
+ #
7
+ # ===Usage
8
+ # <code>
9
+ # link = JiakLink.new('people','callie','sister')
10
+ # </code>
11
+ # If the above link were added to a JiakObject in the same bucket and keyed
12
+ # by the string 'remy', the link from remy to the sister callie would be
13
+ # retrieve using JiakClient#walk and the query link
14
+ # <code>QueryLink.new('people','sister')</code>
15
+ #
16
+ class JiakLink
17
+
18
+ attr_accessor :bucket, :key, :tag
19
+
20
+ # :call-seq:
21
+ # JiakLink.new(bucket,key,tag) -> JiakLink
22
+ # JiakLink.new([bucket,key,tag]) -> JiakLink
23
+ #
24
+ # Create a link (designated by tag) to an object at bucket/key. Bucket can
25
+ # be either a JiakBucket or a string bucket name; key and tag must both be
26
+ # strings.
27
+ #
28
+ def initialize(*args)
29
+ case args.size
30
+ when 1
31
+ if args[0].is_a? Array
32
+ bucket, key, tag = args[0][0], args[0][1], args[0][2]
33
+ elsif args[0].is_a? JiakLink
34
+ bucket, key, tag = args[0].bucket, args[0].key, args[0].tag
35
+ else
36
+ raise JiakLinkException, "argument error"
37
+ end
38
+ when 3
39
+ bucket, key, tag = args[0], args[1], args[2]
40
+ else
41
+ raise JiakLinkException, "argument error"
42
+ end
43
+
44
+ @bucket, @key, @tag = transform_args(bucket,key,tag)
45
+ end
46
+
47
+ # :call-seq:
48
+ # link.bucket = bucket
49
+ #
50
+ # Set the bucket field.
51
+ def bucket=(bucket)
52
+ bucket = bucket.name if bucket.is_a? JiakBucket
53
+ @bucket = transform_arg(bucket)
54
+ end
55
+
56
+ # :call-seq:
57
+ # link.key = key
58
+ #
59
+ # Set the key field.
60
+ def key=(key)
61
+ @key = transform_arg(key)
62
+ end
63
+
64
+ # :call-seq:
65
+ # link.tag = tag
66
+ #
67
+ # Set the tag field.
68
+ def tag=(tag)
69
+ @tag = transform_arg(tag)
70
+ end
71
+
72
+ # :call-seq:
73
+ # link.for_jiak -> JSON
74
+ #
75
+ # Representation of this JiakLink for transport to Jiak.
76
+ def for_jiak
77
+ [@bucket, @key, @tag]
78
+ end
79
+
80
+ # :call-seq:
81
+ # link == other -> true or false
82
+ #
83
+ # Equality -- JiakLinks are equal if they contain the same attribute values.
84
+ def ==(other)
85
+ (@bucket == other.bucket &&
86
+ @key == other.key &&
87
+ @tag == other.tag) rescue false
88
+ end
89
+
90
+ # :call-seq:
91
+ # eql?(other) -> true or false
92
+ #
93
+ # Returns <code>true</code> if <code>other</code> is a JiakLink with the
94
+ # same attribute values.
95
+ def eql?(other)
96
+ other.is_a?(JiakLink) &&
97
+ @bucket.eql?(other.bucket) &&
98
+ @key.eql?(other.key) &&
99
+ @tag.eql?(other.tag)
100
+ end
101
+
102
+ def hash # :nodoc:
103
+ @bucket.hash + @key.hash + @tag.hash
104
+ end
105
+
106
+ # String representation of this JiakLink.
107
+ def to_s
108
+ "[#{bucket},#{key},#{tag}]"
109
+ end
110
+
111
+ def transform_args(b,k,t)
112
+ b = b.name if b.is_a? JiakBucket
113
+ [transform_arg(b),transform_arg(k),transform_arg(t)]
114
+ end
115
+ private :transform_args
116
+
117
+ def transform_arg(arg)
118
+ unless arg.is_a? String
119
+ raise JiakLinkException, "Link elements must be Strings."
120
+ end
121
+ value = arg.dup
122
+ value.strip!
123
+ if value.empty?
124
+ raise JiakLinkException, "Link elements can't be empty."
125
+ end
126
+ value
127
+ end
128
+ private :transform_arg
129
+
130
+ end
131
+ end
@@ -0,0 +1,233 @@
1
+ module RiakRest
2
+
3
+ # Wrapper for JiakData.
4
+ class JiakObject
5
+
6
+ attr_accessor :bucket, :key, :data, :links, :riak
7
+
8
+ # :call-seq:
9
+ # JiakObject.new(opts) -> JiakObject
10
+ #
11
+ # Create a object for Jiak storage. Valid options:
12
+ # <code>:bucket</code> :: JiakBucket for storage.
13
+ # <code>:data</code> :: Object JiakData to be stored.
14
+ # <code>:key</code> :: Object key.
15
+ # <code>:links</code> :: Object JiakLink array.
16
+ #
17
+ # The bucket and data options are required.
18
+ #
19
+ # The key and links options are optional. If no key is provided, the
20
+ # <code>keygen</code> method of <code>data</code> is used to provide the
21
+ # key. The default implementation of JiakData#keygen is an empty string,
22
+ # which instructs the Jiak server to generate a random key. If no links are
23
+ # provided, the default uses an empty array.
24
+ #
25
+ # There are other options used by the system to maintain the context of the
26
+ # JiakObject on the Riak cluster. These options should not be manually
27
+ # altered and are purposely not described here.
28
+ def initialize(opts)
29
+ opts[:links] ||= []
30
+ check_opts(opts)
31
+
32
+ @bucket = check_bucket(opts[:bucket])
33
+ @data = check_data(opts[:data])
34
+ @key = transform_key(opts[:key] || @data.keygen)
35
+ @links = check_links(opts[:links])
36
+
37
+ # The Riak context for the object if provided.
38
+ if opts[:vclock]
39
+ @riak = opts.select {|k,v| [:vclock,:vtag,:lastmod].include?(k)}
40
+ end
41
+ end
42
+
43
+ # :call-seq:
44
+ # JiakObject.from_jiak(jiak) -> JiakObject
45
+ #
46
+ # Create a JiakObject from parsed JSON returned by the Jiak server. Calls
47
+ # the <code>jiak_create</code> of the JiakData class passed as the second
48
+ # argument to inflate the data into the user-defined data class.
49
+ def self.from_jiak(jiak,klass)
50
+ jiak[:bucket] = JiakBucket.new(jiak.delete('bucket'),klass)
51
+ jiak[:data] = klass.jiak_create(jiak.delete('object'))
52
+ jiak[:links] = jiak.delete('links').map {|link| JiakLink.new(link)}
53
+
54
+ new(jiak.inject({}) do |build, (key, value)|
55
+ build[key.to_sym] = value
56
+ build
57
+ end)
58
+ end
59
+
60
+ # :call-seq:
61
+ # jiak_object.to_jiak -> JSON
62
+ #
63
+ # Create a representation suitable for sending to a Jiak server. Calls the
64
+ # <code>for_jiak</code> method of the wrapped JiakData. Called by
65
+ # JiakClient when transporting an object to Jiak.
66
+ def to_jiak
67
+ jiak = {
68
+ :bucket => @bucket.name,
69
+ :key => @key,
70
+ :object => @data.for_jiak,
71
+ :links => @links.map {|link| link.for_jiak}
72
+ }
73
+ if(@riak)
74
+ jiak[:vclock] = @riak[:vclock]
75
+ jiak[:vtag] = @riak[:vtag]
76
+ jiak[:lastmod] = @riak[:lastmod]
77
+ end
78
+ jiak.to_json
79
+ end
80
+
81
+ # :call-seq:
82
+ # jiak_object.bucket = bucket
83
+ #
84
+ # Set the bucket for a JiakObject. Bucket must be a JiakBucket.
85
+ def bucket=(bucket)
86
+ @bucket = check_bucket(bucket)
87
+ end
88
+
89
+ # :call-seq:
90
+ # jiak_object.key = string or nil
91
+ #
92
+ # Set the key for a JiakObject. Key string is stripped of leading and
93
+ # trailing blanks. A nil key is interpreted as an empty string.
94
+ def key=(key)
95
+ @key = transform_key(key)
96
+ end
97
+
98
+ # :call-seq:
99
+ # jiak_object.data = {}
100
+ #
101
+ # Set the data wrapped by a JiakObject. The data must be a JiakData object.
102
+ def data=(data)
103
+ @data = check_data(data)
104
+ end
105
+
106
+ # :call-seq:
107
+ # jiak_object.links = []
108
+ #
109
+ # Set the links array for JiakObject. Each array element must be a
110
+ # JiakLink.
111
+ def links=(links)
112
+ @links = check_links(links)
113
+ end
114
+
115
+ # :call-seq:
116
+ # jiak_object << JiakLink -> JiakObject
117
+ #
118
+ # Convenience method for adding a JiakLink to the links for a
119
+ # JiakObject. Duplicate links are ignored. Returns the JiakObject for
120
+ # chaining.
121
+ def <<(link)
122
+ link = check_link(link)
123
+ @links << link unless @links.include?(link)
124
+ self
125
+ end
126
+
127
+ # :call-seq:
128
+ # jiak_object.riak = riak
129
+ #
130
+ # Set the Riak context for a JiakObject.
131
+ def riak=(riak)
132
+ @riak = check_riak(riak)
133
+ end
134
+
135
+ # :call-seq:
136
+ # jiak_object == other -> true or false
137
+ #
138
+ # Equality -- Two JiakObjects are equal if they contain the same values
139
+ # for all attributes.
140
+ def ==(other)
141
+ (@bucket == other.bucket &&
142
+ @key == other.key &&
143
+ @data == other.data &&
144
+ @links == other.links
145
+ ) rescue false
146
+ end
147
+
148
+ # :call-seq:
149
+ # jiak_object.eql?(other) -> true or false
150
+ #
151
+ # Returns <code>true</code> if <code>other</code> is a JiakObject with the
152
+ # same the same attribute values.
153
+ def eql?(other)
154
+ other.is_a?(JiakObject) &&
155
+ @bucket.eql?(other.bucket) &&
156
+ @key.eql?(other.key) &&
157
+ @data.eql?(other.data) &&
158
+ @links.eql?(other.links)
159
+ end
160
+
161
+ def hash # :nodoc:
162
+ @bucket.name.hash + @key.hash + @data.hash + @links.hash + @riak.hash
163
+ end
164
+
165
+ def check_opts(opts)
166
+ err = opts.select {|k,v| !VALID_OPTS.include?(k)}
167
+ unless err.empty?
168
+ raise JiakObjectException, "unrecognized options: #{err.keys}"
169
+ end
170
+ opts
171
+ end
172
+ private :check_opts
173
+
174
+ def check_bucket(bucket)
175
+ unless bucket.is_a?(JiakBucket)
176
+ raise JiakObjectException, "Bucket must be a JiakBucket."
177
+ end
178
+ bucket
179
+ end
180
+ private :check_bucket
181
+
182
+ def transform_key(key)
183
+ # Change nil key to empty
184
+ o_key = key.nil? ? '' : key.dup
185
+ unless o_key.is_a?(String)
186
+ raise JiakObjectException, "Key must be a String"
187
+ end
188
+ o_key.strip!
189
+ o_key
190
+ end
191
+ private :transform_key
192
+
193
+ def check_data(data)
194
+ unless data.is_a?(JiakData)
195
+ raise JiakObjectException, "Data must be a JiakData."
196
+ end
197
+ data
198
+ end
199
+ private :check_data
200
+
201
+ def check_links(links)
202
+ unless links.is_a? Array
203
+ raise JiakObjectException, "Links must be an array"
204
+ end
205
+ links.each do |link|
206
+ check_link(link)
207
+ end
208
+ links
209
+ end
210
+ private :check_links
211
+
212
+ def check_link(link)
213
+ unless link.is_a? JiakLink
214
+ raise JiakObjectException, "Each link must be a JiakLink"
215
+ end
216
+ link
217
+ end
218
+ private :check_link
219
+
220
+ def check_riak(riak)
221
+ err = riak.select {|k,v| !VALID_RIAK.include?(k)}
222
+ unless err.empty?
223
+ raise JiakObjectException, "unrecognized options: #{err.keys}"
224
+ end
225
+ riak
226
+ end
227
+
228
+ private
229
+ VALID_RIAK = [:vclock,:vtag,:lastmod]
230
+ VALID_OPTS = [:bucket,:key,:data,:links] + VALID_RIAK
231
+ end
232
+
233
+ end