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,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