ripple 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +26 -0
- data/LICENSE +13 -0
- data/README.textile +126 -0
- data/RELEASE_NOTES.textile +24 -0
- data/Rakefile +61 -0
- data/VERSION +1 -0
- data/lib/riak.rb +45 -0
- data/lib/riak/bucket.rb +105 -0
- data/lib/riak/client.rb +138 -0
- data/lib/riak/client/curb_backend.rb +63 -0
- data/lib/riak/client/http_backend.rb +209 -0
- data/lib/riak/client/net_http_backend.rb +49 -0
- data/lib/riak/failed_request.rb +37 -0
- data/lib/riak/i18n.rb +15 -0
- data/lib/riak/invalid_response.rb +25 -0
- data/lib/riak/link.rb +54 -0
- data/lib/riak/locale/en.yml +37 -0
- data/lib/riak/map_reduce.rb +240 -0
- data/lib/riak/map_reduce_error.rb +20 -0
- data/lib/riak/robject.rb +234 -0
- data/lib/riak/util/headers.rb +44 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/translation.rb +29 -0
- data/lib/riak/walk_spec.rb +113 -0
- data/lib/ripple.rb +48 -0
- data/lib/ripple/core_ext/casting.rb +96 -0
- data/lib/ripple/document.rb +60 -0
- data/lib/ripple/document/attribute_methods.rb +111 -0
- data/lib/ripple/document/attribute_methods/dirty.rb +52 -0
- data/lib/ripple/document/attribute_methods/query.rb +49 -0
- data/lib/ripple/document/attribute_methods/read.rb +38 -0
- data/lib/ripple/document/attribute_methods/write.rb +36 -0
- data/lib/ripple/document/bucket_access.rb +38 -0
- data/lib/ripple/document/finders.rb +84 -0
- data/lib/ripple/document/persistence.rb +93 -0
- data/lib/ripple/document/persistence/callbacks.rb +48 -0
- data/lib/ripple/document/properties.rb +85 -0
- data/lib/ripple/document/validations.rb +44 -0
- data/lib/ripple/embedded_document.rb +38 -0
- data/lib/ripple/embedded_document/persistence.rb +46 -0
- data/lib/ripple/i18n.rb +15 -0
- data/lib/ripple/locale/en.yml +16 -0
- data/lib/ripple/property_type_mismatch.rb +23 -0
- data/lib/ripple/translation.rb +24 -0
- data/ripple.gemspec +159 -0
- data/spec/fixtures/cat.jpg +0 -0
- data/spec/fixtures/multipart-blank.txt +7 -0
- data/spec/fixtures/multipart-with-body.txt +16 -0
- data/spec/riak/bucket_spec.rb +141 -0
- data/spec/riak/client_spec.rb +169 -0
- data/spec/riak/curb_backend_spec.rb +50 -0
- data/spec/riak/headers_spec.rb +34 -0
- data/spec/riak/http_backend_spec.rb +136 -0
- data/spec/riak/link_spec.rb +50 -0
- data/spec/riak/map_reduce_spec.rb +347 -0
- data/spec/riak/multipart_spec.rb +36 -0
- data/spec/riak/net_http_backend_spec.rb +28 -0
- data/spec/riak/object_spec.rb +444 -0
- data/spec/riak/walk_spec_spec.rb +208 -0
- data/spec/ripple/attribute_methods_spec.rb +149 -0
- data/spec/ripple/bucket_access_spec.rb +48 -0
- data/spec/ripple/callbacks_spec.rb +86 -0
- data/spec/ripple/document_spec.rb +35 -0
- data/spec/ripple/embedded_document_spec.rb +52 -0
- data/spec/ripple/finders_spec.rb +146 -0
- data/spec/ripple/persistence_spec.rb +89 -0
- data/spec/ripple/properties_spec.rb +195 -0
- data/spec/ripple/ripple_spec.rb +43 -0
- data/spec/ripple/validations_spec.rb +64 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/http_backend_implementation_examples.rb +215 -0
- data/spec/support/mock_server.rb +58 -0
- metadata +221 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
en:
|
15
|
+
riak:
|
16
|
+
client_type: "invalid argument {{client}} is not a Riak::Client"
|
17
|
+
string_type: "invalid_argument {{string}} is not a String"
|
18
|
+
loading_bucket: "while loading bucket '{{name}}'"
|
19
|
+
failed_request: "Expected {{expected}} from Riak but received {{code}}. {{body}}"
|
20
|
+
hash_type: "invalid argument {{hash}} is not a Hash"
|
21
|
+
path_and_body_required: "You must supply both a resource path and a body."
|
22
|
+
request_body_type: "Request body must be a string or IO."
|
23
|
+
resource_path_short: "Resource path too short"
|
24
|
+
missing_host_and_port: "You must specify a host and port, or use the defaults of 127.0.0.1:8098"
|
25
|
+
invalid_client_id: "Invalid client ID, must be a string or between 0 and {{max_id}}"
|
26
|
+
hostname_invalid: "host must be a valid hostname"
|
27
|
+
port_invalid: "port must be an integer between 0 and 65535"
|
28
|
+
install_curb: "curb library not found! Please `gem install curb` for better performance."
|
29
|
+
bucket_link_conversion: "Can't convert a bucket link to a walk spec"
|
30
|
+
invalid_phase_type: "type must be :map, :reduce, or :link"
|
31
|
+
module_function_pair_required: "function must have two elements when an array"
|
32
|
+
stored_function_invalid: "function must have :bucket and :key when a hash"
|
33
|
+
walk_spec_invalid_unless_link: "WalkSpec is only valid for a function when the type is :link"
|
34
|
+
invalid_function_value: "invalid value for function: {{value}}"
|
35
|
+
content_type_undefined: "content_type is not defined!"
|
36
|
+
too_few_arguments: "too few arguments: {{params}}"
|
37
|
+
wrong_argument_count_walk_spec: "wrong number of arguments (one Hash or bucket,tag,keep required)"
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
# Class for invoking map-reduce jobs using the HTTP interface.
|
18
|
+
class MapReduce
|
19
|
+
include Util::Translation
|
20
|
+
# @return [Array<[bucket,key]>,String] The bucket/keys for input to the job, or the bucket (all keys).
|
21
|
+
# @see #add
|
22
|
+
attr_accessor :inputs
|
23
|
+
|
24
|
+
# @return [Array<Phase>] The map and reduce phases that will be executed
|
25
|
+
# @see #map
|
26
|
+
# @see #reduce
|
27
|
+
# @see #link
|
28
|
+
attr_accessor :query
|
29
|
+
|
30
|
+
# Creates a new map-reduce job.
|
31
|
+
# @param [Client] client the Riak::Client interface
|
32
|
+
# @yield [self] helpful for initializing the job
|
33
|
+
def initialize(client)
|
34
|
+
@client, @inputs, @query = client, [], []
|
35
|
+
yield self if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add or replace inputs for the job.
|
39
|
+
# @overload add(bucket)
|
40
|
+
# Run the job across all keys in the bucket. This will replace any other inputs previously added.
|
41
|
+
# @param [String, Bucket] bucket the bucket to run the job on
|
42
|
+
# @overload add(bucket,key)
|
43
|
+
# Add a bucket/key pair to the job.
|
44
|
+
# @param [String,Bucket] bucket the bucket of the object
|
45
|
+
# @param [String] key the key of the object
|
46
|
+
# @overload add(object)
|
47
|
+
# Add an object to the job (by its bucket/key)
|
48
|
+
# @param [RObject] object the object to add to the inputs
|
49
|
+
# @overload add(bucket, key, keydata)
|
50
|
+
# @param [String,Bucket] bucket the bucket of the object
|
51
|
+
# @param [String] key the key of the object
|
52
|
+
# @param [String] keydata extra data to pass along with the object to the job
|
53
|
+
# @return [MapReduce] self
|
54
|
+
def add(*params)
|
55
|
+
params = params.dup.flatten
|
56
|
+
case params.size
|
57
|
+
when 1
|
58
|
+
p = params.first
|
59
|
+
case p
|
60
|
+
when Bucket
|
61
|
+
@inputs = p.name
|
62
|
+
when RObject
|
63
|
+
@inputs << [p.bucket.name, p.key]
|
64
|
+
when String
|
65
|
+
@inputs = p
|
66
|
+
end
|
67
|
+
when 2..3
|
68
|
+
bucket = params.shift
|
69
|
+
bucket = bucket.name if Bucket === bucket
|
70
|
+
@inputs << params.unshift(bucket)
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
alias :<< :add
|
75
|
+
alias :include :add
|
76
|
+
|
77
|
+
# Add a map phase to the job.
|
78
|
+
# @overload map(function)
|
79
|
+
# @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module,function] pair
|
80
|
+
# @overload map(function?, options)
|
81
|
+
# @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module, function] pair
|
82
|
+
# @param [Hash] options extra options for the phase (see {Phase#initialize})
|
83
|
+
# @return [MapReduce] self
|
84
|
+
# @see Phase#initialize
|
85
|
+
def map(*params)
|
86
|
+
options = params.extract_options!
|
87
|
+
@query << Phase.new({:type => :map, :function => params.shift}.merge(options))
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Add a reduce phase to the job.
|
92
|
+
# @overload reduce(function)
|
93
|
+
# @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module,function] pair
|
94
|
+
# @overload reduce(function?, options)
|
95
|
+
# @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module, function] pair
|
96
|
+
# @param [Hash] options extra options for the phase (see {Phase#initialize})
|
97
|
+
# @return [MapReduce] self
|
98
|
+
# @see Phase#initialize
|
99
|
+
def reduce(*params)
|
100
|
+
options = params.extract_options!
|
101
|
+
@query << Phase.new({:type => :reduce, :function => params.shift}.merge(options))
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add a link phase to the job. Link phases follow links attached to objects automatically (a special case of map).
|
106
|
+
# @overload link(walk_spec, options={})
|
107
|
+
# @param [WalkSpec] walk_spec a WalkSpec that represents the types of links to follow
|
108
|
+
# @param [Hash] options extra options for the phase (see {Phase#initialize})
|
109
|
+
# @overload link(bucket, tag, keep, options={})
|
110
|
+
# @param [String, nil] bucket the bucket to limit links to
|
111
|
+
# @param [String, nil] tag the tag to limit links to
|
112
|
+
# @param [Boolean] keep whether to keep results of this phase (overrides the phase options)
|
113
|
+
# @param [Hash] options extra options for the phase (see {Phase#initialize})
|
114
|
+
# @overload link(options)
|
115
|
+
# @param [Hash] options options for both the walk spec and link phase
|
116
|
+
# @see WalkSpec#initialize
|
117
|
+
# @return [MapReduce] self
|
118
|
+
# @see Phase#initialize
|
119
|
+
def link(*params)
|
120
|
+
options = params.extract_options!
|
121
|
+
walk_spec_options = options.slice!(:type, :function, :language, :arg) unless params.first
|
122
|
+
walk_spec = WalkSpec.normalize(params.shift || walk_spec_options).first
|
123
|
+
@query << Phase.new({:type => :link, :function => walk_spec}.merge(options))
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
# Convert the job to JSON for submission over the HTTP interface.
|
128
|
+
# @return [String] the JSON representation
|
129
|
+
def to_json(options={})
|
130
|
+
ActiveSupport::JSON.encode({"inputs" => inputs, "query" => query.map(&:as_json)}, options)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Executes this map-reduce job.
|
134
|
+
# @return [Array<Array>] similar to link-walking, each element is an array of results from a phase where "keep" is true. If there is only one "keep" phase, only the results from that phase will be returned.
|
135
|
+
def run
|
136
|
+
response = @client.http.post(200, @client.mapred, to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
|
137
|
+
if response.try(:[], :headers).try(:[],'content-type').include?("application/json")
|
138
|
+
JSON.parse(response[:body])
|
139
|
+
else
|
140
|
+
response
|
141
|
+
end
|
142
|
+
rescue FailedRequest => fr
|
143
|
+
if fr.code == 500 && fr.headers['content-type'].include?("application/json")
|
144
|
+
raise MapReduceError.new(fr.body)
|
145
|
+
else
|
146
|
+
raise fr
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Represents an individual phase in a map-reduce pipeline. Generally you'll want to call
|
151
|
+
# methods of {MapReduce} instead of using this directly.
|
152
|
+
class Phase
|
153
|
+
include Util::Translation
|
154
|
+
# @return [Symbol] the type of phase - :map, :reduce, or :link
|
155
|
+
attr_accessor :type
|
156
|
+
|
157
|
+
# @return [String, Array<String, String>, Hash, WalkSpec] For :map and :reduce types, the Javascript function to run (as a string or hash with bucket/key), or the module + function in Erlang to run. For a :link type, a {Riak::WalkSpec} or an equivalent hash.
|
158
|
+
attr_accessor :function
|
159
|
+
|
160
|
+
# @return [String] the language of the phase's function - "javascript" or "erlang". Meaningless for :link type phases.
|
161
|
+
attr_accessor :language
|
162
|
+
|
163
|
+
# @return [Boolean] whether results of this phase will be returned
|
164
|
+
attr_accessor :keep
|
165
|
+
|
166
|
+
# @return [Array] any extra static arguments to pass to the phase
|
167
|
+
attr_accessor :arg
|
168
|
+
|
169
|
+
# Creates a phase in the map-reduce pipeline
|
170
|
+
# @param [Hash] options options for the phase
|
171
|
+
# @option options [Symbol] :type one of :map, :reduce, :link
|
172
|
+
# @option options [String] :language ("javascript") "erlang" or "javascript"
|
173
|
+
# @option options [String, Array, Hash] :function In the case of Javascript, a literal function in a string, or a hash with :bucket and :key. In the case of Erlang, an Array of [module, function]. For a :link phase, a hash including any of :bucket, :tag or a WalkSpec.
|
174
|
+
# @option options [Boolean] :keep (false) whether to return the results of this phase
|
175
|
+
# @option options [Array] :arg (nil) any extra static arguments to pass to the phase
|
176
|
+
def initialize(options={})
|
177
|
+
self.type = options[:type]
|
178
|
+
self.language = options[:language] || "javascript"
|
179
|
+
self.function = options[:function]
|
180
|
+
self.keep = options[:keep] || false
|
181
|
+
self.arg = options[:arg]
|
182
|
+
end
|
183
|
+
|
184
|
+
def type=(value)
|
185
|
+
raise ArgumentError, t("invalid_phase_type") unless value.to_s =~ /^(map|reduce|link)$/i
|
186
|
+
@type = value.to_s.downcase.to_sym
|
187
|
+
end
|
188
|
+
|
189
|
+
def function=(value)
|
190
|
+
case value
|
191
|
+
when Array
|
192
|
+
raise ArgumentError, t("module_function_pair_required") unless value.size == 2
|
193
|
+
@language = "erlang"
|
194
|
+
when Hash
|
195
|
+
raise ArgumentError, t("stored_function_invalid") unless type == :link || value.has_key?(:bucket) && value.has_key?(:key)
|
196
|
+
@language = "javascript"
|
197
|
+
when String
|
198
|
+
@language = "javascript"
|
199
|
+
when WalkSpec
|
200
|
+
raise ArgumentError, t("walk_spec_invalid_unless_link") unless type == :link
|
201
|
+
else
|
202
|
+
raise ArgumentError, t("invalid_function_value", :value => value.inspect)
|
203
|
+
end
|
204
|
+
@function = value
|
205
|
+
end
|
206
|
+
|
207
|
+
# Converts the phase to JSON for use while invoking a job.
|
208
|
+
# @return [String] a JSON representation of the phase
|
209
|
+
def to_json(options=nil)
|
210
|
+
ActiveSupport::JSON.encode(as_json, options)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Converts the phase to its JSON-compatible representation for job invocation.
|
214
|
+
# @return [Hash] a Hash-equivalent of the phase
|
215
|
+
def as_json(options=nil)
|
216
|
+
obj = case type
|
217
|
+
when :map, :reduce
|
218
|
+
defaults = {"language" => language, "keep" => keep}
|
219
|
+
case function
|
220
|
+
when Hash
|
221
|
+
defaults.merge(function)
|
222
|
+
when String
|
223
|
+
if function =~ /\s*function/
|
224
|
+
defaults.merge("source" => function)
|
225
|
+
else
|
226
|
+
defaults.merge("name" => function)
|
227
|
+
end
|
228
|
+
when Array
|
229
|
+
defaults.merge("module" => function[0], "function" => function[1])
|
230
|
+
end
|
231
|
+
when :link
|
232
|
+
spec = WalkSpec.normalize(function).first
|
233
|
+
{"bucket" => spec.bucket, "tag" => spec.tag, "keep" => spec.keep || keep}
|
234
|
+
end
|
235
|
+
obj["arg"] = arg if arg
|
236
|
+
{ type => obj }
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
# Raised when an error occurred in the Javascript map-reduce chain.
|
18
|
+
# The message will be the body of the JSON error response.
|
19
|
+
class MapReduceError < StandardError; end
|
20
|
+
end
|
data/lib/riak/robject.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
# Parent class of all object types supported by ripple. {Riak::RObject} represents
|
18
|
+
# the data and metadata stored in a bucket/key pair in the Riak database.
|
19
|
+
class RObject
|
20
|
+
include Util
|
21
|
+
include Util::Translation
|
22
|
+
|
23
|
+
# @return [Bucket] the bucket in which this object is contained
|
24
|
+
attr_accessor :bucket
|
25
|
+
|
26
|
+
# @return [String] the key of this object within its bucket
|
27
|
+
attr_accessor :key
|
28
|
+
|
29
|
+
# @return [String] the MIME content type of the object
|
30
|
+
attr_accessor :content_type
|
31
|
+
|
32
|
+
# @return [String] the Riak vector clock for the object
|
33
|
+
attr_accessor :vclock
|
34
|
+
alias_attribute :vector_clock, :vclock
|
35
|
+
|
36
|
+
# @return [Object] the data stored in Riak at this object's key. Varies in format by content-type, defaulting to String from the response body.
|
37
|
+
attr_accessor :data
|
38
|
+
|
39
|
+
# @return [Array<Link>] an array of {Riak::Link} objects for relationships between this object and other resources
|
40
|
+
attr_accessor :links
|
41
|
+
|
42
|
+
# @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading
|
43
|
+
attr_accessor :etag
|
44
|
+
|
45
|
+
# @return [Time] the Last-Modified header from the most recent HTTP response, useful for caching and reloading
|
46
|
+
attr_accessor :last_modified
|
47
|
+
|
48
|
+
# @return [Hash] a hash of any X-Riak-Meta-* headers that were in the HTTP response, keyed on the trailing portion
|
49
|
+
attr_accessor :meta
|
50
|
+
|
51
|
+
# Create a new object manually
|
52
|
+
# @param [Bucket] bucket the bucket in which the object exists
|
53
|
+
# @param [String] key the key at which the object resides. If nil, a key will be assigned when the object is saved.
|
54
|
+
# @see Bucket#get
|
55
|
+
def initialize(bucket, key=nil)
|
56
|
+
@bucket, @key = bucket, key
|
57
|
+
@links, @meta = [], {}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Load object data from an HTTP response
|
61
|
+
# @param [Hash] response a response from {Riak::Client::HTTPBackend}
|
62
|
+
def load(response)
|
63
|
+
extract_header(response, "location", :key) {|v| URI.unescape(v.split("/").last) }
|
64
|
+
extract_header(response, "content-type", :content_type)
|
65
|
+
extract_header(response, "x-riak-vclock", :vclock)
|
66
|
+
extract_header(response, "link", :links) {|v| Link.parse(v) }
|
67
|
+
extract_header(response, "etag", :etag)
|
68
|
+
extract_header(response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
|
69
|
+
@meta = response[:headers].inject({}) do |h,(k,v)|
|
70
|
+
if k =~ /x-riak-meta-(.*)/
|
71
|
+
h[$1] = v
|
72
|
+
end
|
73
|
+
h
|
74
|
+
end
|
75
|
+
@data = deserialize(response[:body]) if response[:body].present?
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
# HTTP header hash that will be sent along when storing the object
|
80
|
+
# @return [Hash] hash of HTTP Headers
|
81
|
+
def store_headers
|
82
|
+
{}.tap do |hash|
|
83
|
+
hash["Content-Type"] = @content_type
|
84
|
+
hash["X-Riak-Vclock"] = @vclock if @vclock
|
85
|
+
unless @links.blank?
|
86
|
+
hash["Link"] = @links.reject {|l| l.rel == "up" }.map(&:to_s).join(", ")
|
87
|
+
end
|
88
|
+
unless @meta.blank?
|
89
|
+
@meta.each do |k,v|
|
90
|
+
hash["X-Riak-Meta-#{k}"] = v.to_s
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# HTTP header hash that will be sent along when reloading the object
|
97
|
+
# @return [Hash] hash of HTTP headers
|
98
|
+
def reload_headers
|
99
|
+
{}.tap do |h|
|
100
|
+
h['If-None-Match'] = @etag if @etag.present?
|
101
|
+
h['If-Modified-Since'] = @last_modified.httpdate if @last_modified.present?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Store the object in Riak
|
106
|
+
# @param [Hash] options query parameters
|
107
|
+
# @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
|
108
|
+
# @option options [Fixnum] :w the "w" parameter (Write quorum)
|
109
|
+
# @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
|
110
|
+
# @option options [Boolean] :returnbody (true) whether to return the result of a successful write in the body of the response. Set to false for fire-and-forget updates, set to true to immediately have access to the object's stored representation.
|
111
|
+
# @return [Riak::RObject] self
|
112
|
+
# @raise [ArgumentError] if the content_type is not defined
|
113
|
+
def store(options={})
|
114
|
+
raise ArgumentError, t("content_type_undefined") unless @content_type.present?
|
115
|
+
params = {:returnbody => true}.merge(options)
|
116
|
+
method, codes, path = @key.present? ? [:put, [200,204], "#{@bucket.name}/#{@key}"] : [:post, 201, @bucket.name]
|
117
|
+
response = @bucket.client.http.send(method, codes, @bucket.client.prefix, path, params, serialize(data), store_headers)
|
118
|
+
load(response)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Reload the object from Riak. Will use conditional GETs when possible.
|
122
|
+
# @param [Hash] options query parameters
|
123
|
+
# @option options [Fixnum] :r the "r" parameter (Read quorum)
|
124
|
+
# @option options [Boolean] :force will force a reload request if the vclock is not present, useful for reloading the object after a store (not passed in the query params)
|
125
|
+
# @return [Riak::RObject] self
|
126
|
+
def reload(options={})
|
127
|
+
force = options.delete(:force)
|
128
|
+
return self unless @key && (@vclock || force)
|
129
|
+
response = @bucket.client.http.get([200, 304], @bucket.client.prefix, @bucket.name, @key, options, reload_headers)
|
130
|
+
load(response) if response[:code] == 200
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
alias :fetch :reload
|
135
|
+
|
136
|
+
# Delete the object from Riak and freeze this instance. Will work whether or not the object actually
|
137
|
+
# exists in the Riak database.
|
138
|
+
def delete
|
139
|
+
return if key.blank?
|
140
|
+
@bucket.client.http.delete([204,404], @bucket.client.prefix, @bucket.name, key)
|
141
|
+
freeze
|
142
|
+
end
|
143
|
+
|
144
|
+
# Serializes the internal object data for sending to Riak. Differs based on the content-type.
|
145
|
+
# This method is called internally when storing the object.
|
146
|
+
# Automatically serialized formats:
|
147
|
+
# * JSON (application/json)
|
148
|
+
# * YAML (text/yaml)
|
149
|
+
# * Marshal (application/octet-stream if meta['ruby-serialization'] == "Marshal")
|
150
|
+
# @param [Object] payload the data to serialize
|
151
|
+
def serialize(payload)
|
152
|
+
return payload if IO === payload
|
153
|
+
case @content_type
|
154
|
+
when /json/
|
155
|
+
ActiveSupport::JSON.encode(payload)
|
156
|
+
when /yaml/
|
157
|
+
YAML.dump(payload)
|
158
|
+
when "application/octet-stream"
|
159
|
+
if @meta['ruby-serialization'] == "Marshal"
|
160
|
+
Marshal.dump(payload)
|
161
|
+
else
|
162
|
+
payload.to_s
|
163
|
+
end
|
164
|
+
else
|
165
|
+
payload.to_s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Deserializes the internal object data from a Riak response. Differs based on the content-type.
|
170
|
+
# This method is called internally when loading the object.
|
171
|
+
# Automatically deserialized formats:
|
172
|
+
# * JSON (application/json)
|
173
|
+
# * YAML (text/yaml)
|
174
|
+
# * Marshal (application/octet-stream if meta['ruby-serialization'] == "Marshal")
|
175
|
+
# @param [String] body the serialized response body
|
176
|
+
def deserialize(body)
|
177
|
+
case @content_type
|
178
|
+
when /json/
|
179
|
+
ActiveSupport::JSON.decode(body)
|
180
|
+
when /yaml/
|
181
|
+
YAML.load(body)
|
182
|
+
when "application/octet-stream"
|
183
|
+
if @meta['ruby-serialization'] == "Marshal"
|
184
|
+
Marshal.load(body)
|
185
|
+
else
|
186
|
+
body
|
187
|
+
end
|
188
|
+
else
|
189
|
+
body
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [String] A representation suitable for IRB and debugging output
|
194
|
+
def inspect
|
195
|
+
"#<#{self.class.name} #{@bucket.client.http.path(@bucket.client.prefix, @bucket.name, @key).to_s} [#{@content_type}]:#{@data.inspect}>"
|
196
|
+
end
|
197
|
+
|
198
|
+
# Walks links from this object to other objects in Riak.
|
199
|
+
def walk(*params)
|
200
|
+
specs = WalkSpec.normalize(*params)
|
201
|
+
response = @bucket.client.http.get(200, @bucket.client.prefix, @bucket.name, @key, specs.join("/"))
|
202
|
+
if boundary = Multipart.extract_boundary(response[:headers]['content-type'].first)
|
203
|
+
Multipart.parse(response[:body], boundary).map do |group|
|
204
|
+
map_walk_group(group)
|
205
|
+
end
|
206
|
+
else
|
207
|
+
[]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Converts the object to a link suitable for linking other objects to it
|
212
|
+
def to_link(tag=nil)
|
213
|
+
Link.new(@bucket.client.http.path(@bucket.client.prefix, @bucket.name, @key).path, tag)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
def extract_header(response, name, attribute=nil)
|
218
|
+
if response[:headers][name].present?
|
219
|
+
value = response[:headers][name].try(:first)
|
220
|
+
value = yield value if block_given?
|
221
|
+
send("#{attribute}=", value) if attribute
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def map_walk_group(group)
|
226
|
+
group.map do |obj|
|
227
|
+
if obj[:headers] && obj[:body] && obj[:headers]['location']
|
228
|
+
bucket, key = $1, $2 if obj[:headers]['location'].first =~ %r{/.*/(.*)/(.*)$}
|
229
|
+
RObject.new(@bucket.client.bucket(bucket, :keys => false), key).load(obj)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|