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,242 @@
1
+ module RiakRest
2
+
3
+ # Schema for data objects placed in a Riak bucket. Riak performs basic checks
4
+ # for storing and retrieving data objects in a bucket by ensuring:
5
+ #
6
+ # * Only allowed fields are used.
7
+ # * Required fields are included.
8
+ # * Only writable fields are stored.
9
+ # * Only readable fields are retrieved.
10
+ #
11
+ # The schema for a bucket can be changed dynamically so this doesn't lock
12
+ # storage of data objects to a static structure. To store, say, an expanded
13
+ # data object in an existing bucket, add the new field to the schema and
14
+ # reset the bucket schema before storing objects of the new structure. Note
15
+ # that dynamic changes to a bucket schema do not affect the data objects
16
+ # already stored by Jiak. Schema designations only affect structured Jiak
17
+ # interaction, not the data itself.
18
+ #
19
+ # The fields are kept as symbols or strings in four attribute arrays:
20
+ # <code>allowed_fields</code>:: Allowed in Jiak interaction.
21
+ # <code>required_fields</code>:: Required during Jiak interaction.
22
+ # <code>write_mask</code>:: Allowed to be written during a Jiak store.
23
+ # <code>read_mask</code>:: Returned by Jiak on a retrieval.
24
+ #
25
+ # Since Jiak interaction is JSON, duplicate fields names within an array are
26
+ # not meaningful, including a symbol that "equals" a string. Duplicates
27
+ # raise an exception.
28
+ #
29
+ # ===Usage
30
+ # <pre>
31
+ # schema = JiakSchema.new({:allowed_fields => [:foo,:bar,:baz],
32
+ # :required_fields => [:foo,:bar],
33
+ # :read_mask => [:foo,:baz],
34
+ # :write_mask => [:foo,:bar] })
35
+ #
36
+ # schema.required_fields # => [:foo,:bar]
37
+ #
38
+ #
39
+ # schema = JiakSchema.new({:allowed_fields => [:foo,:bar,:baz],
40
+ # :required_fields => [:foo,:bar]})
41
+ #
42
+ # schema.read_mask # => [:foo,:bar,:baz]
43
+ # schema.required_fields # => [:foo,:bar]
44
+ # schema.required_fields = [:foo,:bar,:baz]
45
+ # schema.required_fields # => [:foo,:bar,:baz]
46
+ #
47
+ #
48
+ # schema = JiakSchema.new([:foo,:bar,:baz])
49
+ #
50
+ # schema.write_mask # => [:foo,:bar,:baz)
51
+ #
52
+ # </pre>
53
+ class JiakSchema
54
+
55
+ attr_accessor :allowed_fields, :required_fields, :read_mask, :write_mask
56
+
57
+ # call-seq:
58
+ # JiakSchema.new(arg) -> JiakSchema
59
+ #
60
+ # New schema from either a hash or an single-element array.
61
+ #
62
+ # ====Hash structure
63
+ # <em>required</em>
64
+ # <code>allowed_fields</code>:: Fields that can be stored.
65
+ # <em>optional</em>
66
+ # <code>required_fields</code>:: Fields that must be provided on storage.
67
+ # <code>read_mask</code>:: Fields returned on retrieval.
68
+ # <code>write_mask</code>:: Fields that can be changed and stored.
69
+ # The value for key must be an array.
70
+ #
71
+ # =====OR
72
+ # <code>schema</code>: A hash whose value is the above hash structure.
73
+ #
74
+ # Notes
75
+ # * Keys can either be symbols or strings.
76
+ # * Array fields must be symbols or strings.
77
+ # * Required fields defaults to an empty array.
78
+ # * Masks default to the <code>allowed_fields</code> array.
79
+ #
80
+ # ====Array structure
81
+ # <code>[:f1,...,fn]</code>:: Allowed fields as symbols or strings.
82
+ #
83
+ # All other fields take the same value as the <code>allowed_fields</code>
84
+ # element. The array structure is provided for simplicity but does not
85
+ # provide the finer-grained control of the hash structure.
86
+ #
87
+ # Raise JiakSchemaException if:
88
+ # * the method argument is not either a hash or array
89
+ # * The fields are not either symbols or strings
90
+ # * The fields elements are not unique
91
+ def initialize(arg)
92
+ case arg
93
+ when Hash
94
+ # Jiak returns a JSON structure with a single key 'schema' whose value
95
+ # is a hash. If the arg hash has a schema key, set the opts hash to
96
+ # that; otherwise use the arg as the opts hash.
97
+ opts = arg[:schema] || arg['schema'] || arg
98
+
99
+ opts[:allowed_fields] ||= opts['allowed_fields']
100
+ check_arr("allowed_fields",opts[:allowed_fields])
101
+
102
+ # Use required if provided, otherwise set to empty array
103
+ opts[:required_fields] ||= opts['required_fields'] || []
104
+ check_arr("required_fields",opts[:required_fields])
105
+
106
+ # Use masks if provided, otherwise set to allowed_fields
107
+ [:read_mask,:write_mask].each do |key|
108
+ opts[key] ||= opts[key.to_s] || opts[:allowed_fields]
109
+ check_arr(key.to_s,opts[key])
110
+ end
111
+ when Array
112
+ # An array arg must be a single-element array of the allowed
113
+ # fields. Required fields is set to an empty array and the masks are
114
+ # set to the allowed fields array.
115
+ check_arr("allowed_fields",arg)
116
+ opts = {
117
+ :allowed_fields => arg,
118
+ :required_fields => [],
119
+ :read_mask => arg,
120
+ :write_mask => arg
121
+ }
122
+ else
123
+ raise JiakSchemaException, "Initialize arg must be either hash or array"
124
+ end
125
+
126
+ @allowed_fields = opts[:allowed_fields]
127
+ @required_fields = opts[:required_fields]
128
+ @read_mask = opts[:read_mask]
129
+ @write_mask = opts[:write_mask]
130
+ end
131
+
132
+ # call-seq:
133
+ # JiakSchema.from_json(json) -> JiakSchema
134
+ #
135
+ # Create a JiakSchema from parsed JSON returned by the Jiak server.
136
+ def self.from_jiak(jiak)
137
+ new(jiak)
138
+ end
139
+
140
+ # call-seq:
141
+ # schema.to_jiak -> JSON
142
+ #
143
+ # Create a representation suitable for sending to a Jiak server. Called by
144
+ # JiakClient when transporting a schema to Jiak.
145
+ def to_jiak
146
+ { :schema =>
147
+ { :allowed_fields => @allowed_fields,
148
+ :required_fields => @required_fields,
149
+ :read_mask => @read_mask,
150
+ :write_mask => @write_mask
151
+ }
152
+ }.to_json
153
+ end
154
+
155
+ def allowed_fields=(arr) # :nodoc:
156
+ check_arr('allowed_fields',arr)
157
+ @allowed_fields = arr
158
+ end
159
+ def required_fields=(arr) # :nodoc:
160
+ check_arr('required_fields',arr)
161
+ @required_fields = arr
162
+ end
163
+ def read_mask=(arr) # :nodoc:
164
+ check_arr('read_mask',arr)
165
+ @read_mask = arr
166
+ end
167
+ def write_mask=(arr) # :nodoc:
168
+ check_arr('write_mask',arr)
169
+ @write_mask = arr
170
+ end
171
+
172
+ # :call-seq:
173
+ # schema.readwrite = [:f1,...,:fn]
174
+ #
175
+ # Set the read and write masks for a JiakSchema.
176
+ def readwrite=(arr) # :nodoc:
177
+ check_arr('readwrite',arr)
178
+ @read_mask = arr
179
+ @write_mask = arr
180
+ end
181
+
182
+ # call-seq:
183
+ # schema == other -> true or false
184
+ #
185
+ # Equality -- Two schemas are equal if they contain the same array elements
186
+ # for all attributes, irrespective of order.
187
+ def ==(other)
188
+ (@allowed_fields.same_fields?(other.allowed_fields) &&
189
+ @required_fields.same_fields?(other.required_fields) &&
190
+ @read_mask.same_fields?(other.read_mask) &&
191
+ @write_mask.same_fields?(other.write_mask)) rescue false
192
+ end
193
+
194
+ # call-seq:
195
+ # schema.eql?(other) -> true or false
196
+ #
197
+ # Returns <code>true</code> if <code>other</code> is a JiakSchema with the
198
+ # same array elements, irrespective of order.
199
+ def eql?(other)
200
+ other.is_a?(JiakSchema) &&
201
+ @allowed_fields.same_fields?(other.allowed_fields) &&
202
+ @required_fields.same_fields?(other.required_fields) &&
203
+ @read_mask.same_fields?(other.read_mask) &&
204
+ @write_mask.same_fields?(other.write_mask)
205
+ end
206
+
207
+ def hash # :nodoc:
208
+ @allowed_fields.name.hash + @required_fields.hash +
209
+ @read_mask.hash + @write_mask.hash
210
+ end
211
+
212
+ # String representation of this schema.
213
+ def to_s
214
+ 'allowed_fields="'+@allowed_fields.inspect+
215
+ '",required_fields="'+@required_fields.inspect+
216
+ '",read_mask="'+@read_mask.inspect+
217
+ '",write_mask="'+@write_mask.inspect+'"'
218
+ end
219
+
220
+ # Each option must be an array of symbol or string elements.
221
+ def check_arr(desc,arr)
222
+ if(arr.eql?("*"))
223
+ raise(JiakSchemaException,
224
+ "RiakRest does not support wildcard schemas at this time.")
225
+ end
226
+ unless arr.is_a?(Array)
227
+ raise JiakSchemaException, "#{desc} must be an array"
228
+ end
229
+ arr.each do |field|
230
+ unless(field.is_a?(String) || field.is_a?(Symbol))
231
+ raise JiakSchemaException, "#{desc} must be strings or symbols"
232
+ end
233
+ end
234
+ unless arr.map{|f| f.to_s}.uniq.size == arr.size
235
+ raise JiakSchemaException, "#{desc} must have unique elements."
236
+ end
237
+ end
238
+ private :check_arr
239
+
240
+ end
241
+
242
+ end
@@ -0,0 +1,156 @@
1
+ module RiakRest
2
+
3
+ # Represents a link used to query linked objects in Jiak. Links are
4
+ # established using a JiakLink and queried using a QueryLink. The structures
5
+ # are very similar but significantly different.
6
+ #
7
+ # ===Usage
8
+ # <code>
9
+ # link = QueryLink.new('people','parent')
10
+ # link = QueryLink.new(['children','odd','_'])
11
+ # link = QueryLink.new('blogs',nil,QueryLink::ANY)
12
+ # </code>
13
+ class QueryLink
14
+
15
+ attr_accessor :bucket, :tag, :acc
16
+
17
+ # Jiak (erlang) wildcard character (atom)
18
+ ANY = '_'
19
+
20
+ # :call-seq:
21
+ # QueryLink.new(*args) -> QueryLink
22
+ #
23
+ # Create a link from argument array. Missing, nil, or empty string values
24
+ # are set to QueryLink::ANY.
25
+ #
26
+ # ====Examples
27
+ # The following create QueryLinks with the shown equivalent array structure:
28
+ # <code>
29
+ # QueryLink.new # => ['_','_','_']
30
+ # QueryLink.new 'b' # => ['b','_','_']
31
+ # QueryLink.new 'b','t' # => ['b','t','_']
32
+ # QueryLink.new 'b','t','a' # => ['b','t','a']
33
+ #
34
+ # QueryLink.new [] # => ['_','_','_']
35
+ # QueryLink.new ['b'] # => ['b','_','_']
36
+ # QueryLink.new ['b','t'] # => ['b','t','_']
37
+ # QueryLink.new ['b','t','a'] # => ['b','t','a']
38
+ #
39
+ # QueryLink.new ['',nil,' '] # => ['_','_','_']
40
+ # </code>
41
+ #
42
+ # Passing another QueryLink as an argument makes a copy of that
43
+ # link. Passing a JiakBucket in the first (bucket) position uses the name
44
+ # field of that JiakBucket.
45
+ def initialize(*args)
46
+ case args.size
47
+ when 0
48
+ bucket = tag = acc = ANY
49
+ when 1
50
+ if args[0].is_a? String
51
+ bucket = args[0]
52
+ tag = acc = ANY
53
+ elsif args[0].is_a? QueryLink
54
+ bucket, tag, acc = args[0].bucket, args[0].tag, args[0].acc
55
+ elsif args[0].is_a? Array
56
+ bucket, tag, acc = args[0][0], args[0][1], args[0][2]
57
+ else
58
+ raise QueryLinkException, "argument error"
59
+ end
60
+ when 2
61
+ bucket, tag = args[0], args[1]
62
+ acc = ANY
63
+ when 3
64
+ bucket, tag, acc = args[0], args[1], args[2]
65
+ else
66
+ raise QueryLinkException, "too many arguments, (#{args.size} for 3)"
67
+ end
68
+
69
+ @bucket, @tag, @acc = transform_args(bucket,tag,acc)
70
+ end
71
+
72
+ # :call-seq
73
+ # link.bucket = bucket
74
+ #
75
+ # Set the bucket field.
76
+ def bucket=(bucket)
77
+ bucket = bucket.name if bucket.is_a? JiakBucket
78
+ @bucket = transform_arg(bucket)
79
+ end
80
+
81
+ # :call-seq:
82
+ # link.tag = tag
83
+ #
84
+ # Set the tag field.
85
+ def tag=(tag)
86
+ @tag = transform_arg(tag)
87
+ end
88
+
89
+ # :call-seq:
90
+ # link.acc = acc
91
+ #
92
+ # Set the acc field.
93
+ def acc=(acc)
94
+ @acc = transform_arg(acc)
95
+ end
96
+
97
+ # :call-seq:
98
+ # link.for_uri -> URI encoded string
99
+ #
100
+ # URI represent this QueryLink, i.e, a string suitable for inclusion in an
101
+ # URI.
102
+ def for_uri
103
+ URI.encode([@bucket,@tag,@acc].join(','))
104
+ end
105
+
106
+ # :call-seq:
107
+ # link == other -> true or false
108
+ #
109
+ # Equality -- QueryLinks are equal if they contain the same attribute
110
+ # values.
111
+ def ==(other)
112
+ (@bucket == other.bucket &&
113
+ @tag == other.tag &&
114
+ @acc == other.acc) rescue false
115
+ end
116
+
117
+ # :call-seq:
118
+ # link.eql?(other) -> true or false
119
+ #
120
+ # Returns <code>true</code> if <code>other</code> is a QueryLink with the
121
+ # same attribute values.
122
+ def eql?(other)
123
+ other.is_a?(QueryLink) &&
124
+ @bucket.eql?(other.bucket) &&
125
+ @tag.eql?(other.tag) &&
126
+ @acc.eql?(other.acc)
127
+ end
128
+
129
+ def hash # :nodoc:
130
+ @bucket.name.hash + @tag.hash + @acc.hash
131
+ end
132
+
133
+ # String representation of this QueryLink.
134
+ def to_s
135
+ '["'+@bucket+'","'+@tag+'","'+@acc+'"]'
136
+ end
137
+
138
+ def transform_args(b,t,a)
139
+ b = b.name if b.is_a? JiakBucket
140
+ [transform_arg(b),transform_arg(t),transform_arg(a)]
141
+ end
142
+ private :transform_args
143
+
144
+ def transform_arg(arg)
145
+ arg = ANY if arg.nil?
146
+ unless arg.is_a? String
147
+ raise QueryLinkException, "Link elements must be Strings."
148
+ end
149
+ value = arg.dup
150
+ value.strip!
151
+ value.empty? ? ANY : value
152
+ end
153
+ private :transform_arg
154
+ end
155
+ end
156
+
@@ -0,0 +1,182 @@
1
+ module RiakRest
2
+ # JiakDataHash provides a easy-to-use default JiakData implementation. See
3
+ # JiakData for a discussion on creating user-defined data classes.
4
+ #
5
+ # JiakDataHash creates a JiakData from a list of user-data fields. Note
6
+ # JiakDataHash, like JiakData, is not used to create instances of user data;
7
+ # rather it is used to create the class for the user data. That class is then
8
+ # used to create instances of the user data itself.
9
+ #
10
+ # The class created by JiakDataHash is anonymous and takes the name of the
11
+ # constant to which it is assigned. See the example below.
12
+ #
13
+ # Object instances of the class created by JiakDataHash have read and write
14
+ # accessors for each the fields used in creating the class. Instance values
15
+ # can also be accessed via [] and []=. Other hash sematics are not
16
+ # provided. The created class does have a <code>to_hash</code> method that
17
+ # returns a hash of the instance data fields and values.
18
+ #
19
+ # The class created by JiakDataHash includes an initialize method that takes
20
+ # as argument a hash of the field/value pairs for the instance. The instance
21
+ # still has all of the field attributes, this simply provides a easy way to
22
+ # initialize some or all of the values for the fields.
23
+ #
24
+ # The class created by JiakDataHash also provides a <code>keygen</code> class
25
+ # method that allows specifying a list of fields for use in generating the
26
+ # key for instance data. See JiakDataHash#keygen.
27
+ #
28
+ # Note the created class has methods provided by JiakData to inspect or
29
+ # manipulate the structure Jiak interaction for instances of the class. See
30
+ # JiakData#ClassMethods for those methods.
31
+ #
32
+ # ===Usage
33
+ # <code>
34
+ # Dog = JiakDataHash.create(:name,:weight)
35
+ # Dog.keygen :name
36
+ #
37
+ # addie = Dog.new(:name => "Adelaide", :weight => 45)
38
+ # addie.name # => "Adeliade"
39
+ # addie.weight # => 45
40
+ #
41
+ # addie.weight = 47 # => 47
42
+ # </code>
43
+ #
44
+ class JiakDataHash
45
+
46
+ # :call-seq:
47
+ # JiakDataHash.create(:f1,...,fn) -> JiakDataHash
48
+ # JiakDataHash.create([:f1,...,fn]) -> JiakDataHash
49
+ # JiakDataHash.create(schema) -> JiakDataHash
50
+ #
51
+ # Creates a JiakDataHash class that can be used to create JiakData objects
52
+ # containing the specified fields.
53
+ def self.create(*args)
54
+ Class.new do
55
+ include JiakData
56
+
57
+ if(args.size == 1)
58
+ case args[0]
59
+ when Symbol, Array
60
+ allowed *args[0]
61
+ when JiakSchema
62
+ allowed *args[0].allowed_fields
63
+ required *args[0].required_fields
64
+ readable *args[0].read_mask
65
+ writable *args[0].write_mask
66
+ end
67
+ else
68
+ allowed *args
69
+ end
70
+
71
+ # :call-seq:
72
+ # DataClass.keygen(*fields)
73
+ #
74
+ # The key generation for the data class will be a concatenation of the
75
+ # to_s result of calling each of the listed data class fields.
76
+ def self.keygen(*fields)
77
+ define_method(:keygen) do
78
+ fields.inject("") {|key,field| key += send("#{field}").to_s}
79
+ end
80
+ end
81
+
82
+ # :call-seq:
83
+ # data.new({}) -> JiakDataHash
84
+ #
85
+ # Create an instance of the user-defined JiakDataHash using the provide
86
+ # hash as initial values.
87
+ def initialize(hsh={})
88
+ hsh.each {|key,value| send("#{key}=",value)}
89
+ end
90
+
91
+ # :call-seq:
92
+ # data.jiak_create(jiak) -> JiakDataHash
93
+ #
94
+ # Used by RiakRest to create an instance of the user-defined data class
95
+ # from the values returned by the Jiak server.
96
+ def self.jiak_create(jiak)
97
+ new(jiak)
98
+ end
99
+
100
+ # :call-seq:
101
+ # data[field] -> value
102
+ #
103
+ # Get the value of a field.
104
+ #
105
+ # Returns <code>nil</code> if <code>field</code> was not declared for
106
+ # this class. <code>field</code> can be in either string or symbol
107
+ # form.
108
+ def [](key)
109
+ send("#{key}") rescue nil
110
+ end
111
+
112
+ # :call-seq:
113
+ # data[field] = value
114
+ #
115
+ # Set the value of a field.
116
+ #
117
+ # Returns the value set, or <code>nil</code> if <code>field</code> was
118
+ # not declared for this class.
119
+ def []=(key,value)
120
+ send("#{key}=",value) rescue nil
121
+ end
122
+
123
+ # :call-seq:
124
+ # data.for_jiak -> hash
125
+ #
126
+ # Return a hash of the writable fields and their values. Used by
127
+ # RiakRest to prepare the data for transport to the Jiak server.
128
+ def for_jiak
129
+ self.class.schema.write_mask.inject({}) do |build,field|
130
+ val = send("#{field}")
131
+ build[field] = val unless val.nil?
132
+ build
133
+ end
134
+ end
135
+
136
+ # :call-seq:
137
+ # data.to_hash
138
+ #
139
+ # Return a hash of the allowed fields and their values.
140
+ def to_hash
141
+ self.class.schema.allowed_fields.inject({}) do |build,field|
142
+ val = send("#{field}")
143
+ build[field] = val
144
+ build
145
+ end
146
+ end
147
+
148
+
149
+ # call-seq:
150
+ # jiak_data == other -> true or false
151
+ #
152
+ # Equality -- Two JiakDataHash objects are equal if they contain the
153
+ # same values for all attributes.
154
+ def ==(other)
155
+ self.class.schema.allowed_fields.reduce(true) do |same,field|
156
+ same && (other.send("#{field}") == (send("#{field}")))
157
+ end
158
+ end
159
+
160
+ # call-seq:
161
+ # data.eql?(other) -> true or false
162
+ #
163
+ # Returns <code>true</code> if <code>other</code> is a JiakObject with
164
+ # the same the same attribute values for all allowed fields.
165
+ def eql?(other)
166
+ other.is_a?(self.class) &&
167
+ self.class.schema.allowed_fields.reduce(true) do |same,field|
168
+ same && other.send("#{field}").eql?(send("#{field}"))
169
+ end
170
+ end
171
+
172
+ def hash # :nodoc:
173
+ self.class.schema.allowed_fields.inject(0) do |hsh,field|
174
+ hsh += send("#{field}").hash
175
+ end
176
+ end
177
+
178
+ end
179
+ end
180
+
181
+ end
182
+ end