require 'set' require 'time' require 'yaml' require 'forwardable' require 'riak/util/translation' require 'riak/serializers' module Riak # Represents single (potentially-conflicted) value stored against a # key in the Riak database. This includes the raw value as well as # metadata. # @since 1.1.0 class RContent extend Forwardable include Util::Translation # @return [String] the MIME content type of the value attr_accessor :content_type # @return [Set] a Set of {Riak::Link} objects for relationships between this object and other resources attr_accessor :links # @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading attr_accessor :etag # @return [Time] the Last-Modified header from the most recent HTTP response, useful for caching and reloading attr_accessor :last_modified # @return [Hash] a hash of any X-Riak-Meta-* headers that were in the HTTP response, keyed on the trailing portion attr_accessor :meta # @return [Hash] a hash of secondary indexes, where the # key is the index name and the value is a Set of index # entries for that index attr_accessor :indexes # @return [Riak::RObject] the RObject to which this sibling belongs attr_accessor :robject def_delegators :robject, :bucket, :key, :vclock # Creates a new object value. This should not normally need to be # called by users of the client. Normal, single-value use can rely # on the delegating accessors on {Riak::RObject}. # @param [RObject] robject the object that this value belongs to # @yield self the new RContent def initialize(robject) @robject = robject @links, @meta = Set.new, {} @indexes = new_index_hash yield self if block_given? end def indexes=(hash) @indexes = hash.inject(new_index_hash) do |h, (k,v)| h[k].merge([*v]) h end end # @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key def data if @raw_data && !@data raw = @raw_data.respond_to?(:read) ? @raw_data.read : @raw_data @data = deserialize(raw) @raw_data = nil end @data end # @param [Object] new_data unmarshaled form of the data to be stored in # Riak. Object will be serialized using {#serialize} if a known # content_type is used. Setting this overrides values stored with # {#raw_data=} # @return [Object] the object stored def data=(new_data) if new_data.respond_to?(:read) raise ArgumentError.new(t("invalid_io_object")) end @raw_data = nil @data = new_data end # @return [String] raw data stored in riak for this object's key def raw_data if @data && !@raw_data @raw_data = serialize(@data) @data = nil end @raw_data end # @param [String, IO-like] new_raw_data the raw data to be stored in Riak # at this key, will not be marshaled or manipulated prior to storage. # Overrides any data stored by {#data=} # @return [String] the data stored def raw_data=(new_raw_data) @data = nil @raw_data = new_raw_data end # Serializes the internal object data for sending to Riak. Differs based on the content-type. # This method is called internally when storing the object. # Automatically serialized formats: # * JSON (application/json) # * YAML (text/yaml) # * Marshal (application/x-ruby-marshal) # When given an IO-like object (e.g. File), no serialization will # be done. # @param [Object] payload the data to serialize def serialize(payload) Serializers.serialize(@content_type, payload) end # Deserializes the internal object data from a Riak response. Differs based on the content-type. # This method is called internally when loading the object. # Automatically deserialized formats: # * JSON (application/json) # * YAML (text/yaml) # * Marshal (application/x-ruby-marshal) # @param [String] body the serialized response body def deserialize(body) Serializers.deserialize(@content_type, body) end # @return [String] A representation suitable for IRB and debugging output def inspect body = if @data || Serializers[content_type] data.inspect else @raw_data && "(#{@raw_data.size} bytes)" end "#<#{self.class.name} [#{@content_type}]:#{body}>" end # @api private def load_map_reduce_value(hash) metadata = hash['metadata'] extract_if_present(metadata, 'X-Riak-VTag', :etag) extract_if_present(metadata, 'content-type', :content_type) extract_if_present(metadata, 'X-Riak-Last-Modified', :last_modified) { |v| Time.httpdate( v ) } extract_if_present(metadata, 'index', :indexes) do |entries| Hash[ entries.map {|k,v| [k, Set.new(Array(v))] } ] end extract_if_present(metadata, 'Links', :links) do |links| Set.new( links.map { |l| Link.new(*l) } ) end extract_if_present(metadata, 'X-Riak-Meta', :meta) do |meta| Hash[ meta.map do |k,v| [k.sub(%r{^x-riak-meta-}i, ''), [v]] end ] end extract_if_present(hash, 'data', :data) { |v| deserialize(v) } end private def extract_if_present(hash, key, attribute=nil) if hash[key].present? attribute ||= key value = block_given? ? yield(hash[key]) : hash[key] send("#{attribute}=", value) end end def new_index_hash Hash.new {|h,k| h[k] = Set.new } end end end