blob: 010177d65f4ef3d4d6d668e2c0fd83c662b82420 [file] [log] [blame]
#--
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#++
module Qpid::Proton::Codec
# +DataError+ is raised when an error occurs while encoding
# or decoding data.
class DataError < Exception; end
# The +Data+ class provides an interface for decoding, extracting,
# creating, and encoding arbitrary AMQP data. A +Data+ object
# contains a tree of AMQP values. Leaf nodes in this tree correspond
# to scalars in the AMQP type system such as INT or STRING. Interior
# nodes in this tree correspond to compound values in the AMQP type
# system such as *LIST*,*MAP*, *ARRAY*, or *DESCRIBED*. The root node
# of the tree is the +Data+ object itself and can have an arbitrary
# number of children.
#
# A +Data+ object maintains the notion of the current sibling node
# and a current parent node. Siblings are ordered within their parent.
# Values are accessed and/or added by using the #next, #prev,
# #enter, and #exit methods to navigate to the desired location in
# the tree and using the supplied variety of mutator and accessor
# methods to access or add a value of the desired type.
#
# The mutator methods will always add a value _after_ the current node
# in the tree. If the current node has a next sibling the mutator method
# will overwrite the value on this node. If there is no current node
# or the current node has no next sibling then one will be added. The
# accessor methods always set the added/modified node to the current
# node. The accessor methods read the value of the current node and do
# not change which node is current.
#
# The following types of scalar values are supported:
#
# * NULL
# * BOOL
# * UBYTE
# * BYTE
# * USHORT
# * SHORT
# * UINT
# * INT
# * CHAR
# * ULONG
# * LONG
# * TIMESTAMP
# * FLOAT
# * DOUBLE
# * DECIMAL32
# * DECIMAL64
# * DECIMAL128
# * UUID
# * BINARY
# * STRING
# * SYMBOL
#
# The following types of compound values are supported:
#
# * DESCRIBED
# * ARRAY
# * LIST
# * MAP
#
class Data
# Creates a new instance with the specified capacity.
#
# @param capacity [Fixnum, Object] The initial capacity or content.
#
def initialize(capacity = 16)
if (!capacity.nil?) &&
(capacity.is_a?(Fixnum) ||
capacity.is_a?(Bignum))
@data = Cproton.pn_data(capacity)
@free = true
else
@data = capacity
@free = false
end
# destructor
ObjectSpace.define_finalizer(self, self.class.finalize!(@data, @free))
end
# @private
def self.finalize!(data, free)
proc {
Cproton.pn_data_free(data) if free
}
end
# @private
def to_s
tmp = Cproton.pn_string("")
Cproton.pn_inspect(@data, tmp)
result = Cproton.pn_string_get(tmp)
Cproton.pn_free(tmp)
return result
end
# Clears the object.
#
def clear
Cproton.pn_data_clear(@data)
end
# Clears the current node and sets the parent to the root node.
#
# Clearing the current node sets it *before* the first node, calling
# #next will advance to the first node.
#
def rewind
Cproton.pn_data_rewind(@data)
end
# Advances the current node to its next sibling and returns its types.
#
# If there is no next sibling the current node remains unchanged
# and nil is returned.
#
def next
Cproton.pn_data_next(@data)
end
# Advances the current node to its previous sibling and returns its type.
#
# If there is no previous sibling then the current node remains unchanged
# and nil is return.
#
def prev
return Cproton.pn_data_prev(@data) ? type : nil
end
# Sets the parent node to the current node and clears the current node.
#
# Clearing the current node sets it _before_ the first child.
#
def enter
Cproton.pn_data_enter(@data)
end
# Sets the current node to the parent node and the parent node to its own
# parent.
#
def exit
Cproton.pn_data_exit(@data)
end
# Returns the numeric type code of the current node.
#
# @return [Fixnum] The current node type.
# @return [nil] If there is no current node.
#
def type_code
dtype = Cproton.pn_data_type(@data)
return (dtype == -1) ? nil : dtype
end
# Return the type object for the current node
#
# @param [Fixnum] The object type.
#
# @see #type_code
#
def type
Mapping.for_code(type_code)
end
# Returns a representation of the data encoded in AMQP format.
#
# @return [String] The context of the Data as an AMQP data string.
#
# @example
#
# @data.string = "This is a test."
# @encoded = @data.encode
#
# # @encoded now contains the text "This is a test." encoded for
# # AMQP transport.
#
def encode
buffer = "\0"*1024
loop do
cd = Cproton.pn_data_encode(@data, buffer, buffer.length)
if cd == Cproton::PN_OVERFLOW
buffer *= 2
elsif cd >= 0
return buffer[0...cd]
else
check(cd)
end
end
end
# Decodes the first value from supplied AMQP data and returns the number
# of bytes consumed.
#
# @param encoded [String] The encoded data.
#
# @example
#
# # SCENARIO: A string of encoded data, @encoded, contains the text
# # of "This is a test." and is passed to an instance of Data
# # for decoding.
#
# @data.decode(@encoded)
# @data.string #=> "This is a test."
#
def decode(encoded)
check(Cproton.pn_data_decode(@data, encoded, encoded.length))
end
# Puts a list value.
#
# Elements may be filled by entering the list node and putting element
# values.
#
# @example
#
# data = Qpid::Proton::Codec::Data.new
# data.put_list
# data.enter
# data.int = 1
# data.int = 2
# data.int = 3
# data.exit
#
def put_list
check(Cproton.pn_data_put_list(@data))
end
# If the current node is a list, this returns the number of elements.
# Otherwise, it returns zero.
#
# List elements can be accessed by entering the list.
#
# @example
#
# count = @data.list
# @data.enter
# (0...count).each
# type = @data.next
# puts "Value: #{@data.string}" if type == STRING
# # ... process other node types
# end
def list
Cproton.pn_data_get_list(@data)
end
# Puts a map value.
#
# Elements may be filled by entering the map node and putting alternating
# key/value pairs.
#
# @example
#
# data = Qpid::Proton::Codec::Data.new
# data.put_map
# data.enter
# data.string = "key"
# data.string = "value"
# data.exit
#
def put_map
check(Cproton.pn_data_put_map(@data))
end
# If the current node is a map, this returns the number of child
# elements. Otherwise, it returns zero.
#
# Key/value pairs can be accessed by entering the map.
#
# @example
#
# count = @data.map
# @data.enter
# (0...count).each do
# type = @data.next
# puts "Key=#{@data.string}" if type == STRING
# # ... process other key types
# type = @data.next
# puts "Value=#{@data.string}" if type == STRING
# # ... process other value types
# end
# @data.exit
def map
Cproton.pn_data_get_map(@data)
end
# @private
def get_map
::Hash.proton_data_get(self)
end
# Puts an array value.
#
# Elements may be filled by entering the array node and putting the
# element values. The values must all be of the specified array element
# type.
#
# If an array is *described* then the first child value of the array
# is the descriptor and may be of any type.
#
# @param described [Boolean] True if the array is described.
# @param element_type [Fixnum] The AMQP type for each element of the array.
#
# @example
#
# # create an array of integer values
# data = Qpid::Proton::Codec::Data.new
# data.put_array(false, INT)
# data.enter
# data.int = 1
# data.int = 2
# data.int = 3
# data.exit
#
# # create a described array of double values
# data.put_array(true, DOUBLE)
# data.enter
# data.symbol = "array-descriptor"
# data.double = 1.1
# data.double = 1.2
# data.double = 1.3
# data.exit
#
def put_array(described, element_type)
check(Cproton.pn_data_put_array(@data, described, element_type.code))
end
# If the current node is an array, returns a tuple of the element count, a
# boolean indicating whether the array is described, and the type of each
# element. Otherwise it returns +(0, false, nil).
#
# Array data can be accessed by entering the array.
#
# @example
#
# # get the details of thecurrent array
# count, described, array_type = @data.array
#
# # enter the node
# data.enter
#
# # get the next node
# data.next
# puts "Descriptor: #{data.symbol}" if described
# (0...count).each do
# @data.next
# puts "Element: #{@data.string}"
# end
def array
count = Cproton.pn_data_get_array(@data)
described = Cproton.pn_data_is_array_described(@data)
array_type = Cproton.pn_data_get_array_type(@data)
return nil if array_type == -1
[count, described, Mapping.for_code(array_type) ]
end
# @private
def get_array
::Array.proton_get(self)
end
# Puts a described value.
#
# A described node has two children, the descriptor and the value.
# These are specified by entering the node and putting the
# desired values.
#
# @example
#
# data = Qpid::Proton::Codec::Data.new
# data.put_described
# data.enter
# data.symbol = "value-descriptor"
# data.string = "the value"
# data.exit
#
def put_described
check(Cproton.pn_data_put_described(@data))
end
# @private
def get_described
raise TypeError, "not a described type" unless self.described?
self.enter
self.next
type = self.type
descriptor = type.get(self)
self.next
type = self.type
value = type.get(self)
self.exit
Qpid::Proton::Types::Described.new(descriptor, value)
end
# Checks if the current node is a described value.
#
# The described and value may be accessed by entering the described value.
#
# @example
#
# if @data.described?
# @data.enter
# puts "The symbol is #{@data.symbol}"
# puts "The value is #{@data.string}"
# end
def described?
Cproton.pn_data_is_described(@data)
end
# Puts a null value.
#
def null
check(Cproton.pn_data_put_null(@data))
end
# Utility method for Qpid::Proton::Codec::Mapping
#
# @private
#
def null=(value)
null
end
# Puts an arbitrary object type.
#
# The Data instance will determine which AMQP type is appropriate and will
# use that to encode the object.
#
# @param object [Object] The value.
#
def object=(object)
Mapping.for_class(object.class).put(self, object)
end
# Gets the current node, based on how it was encoded.
#
# @return [Object] The current node.
#
def object
type = self.type
return nil if type.nil?
type.get(data)
end
# Checks if the current node is null.
#
# @return [Boolean] True if the node is null.
#
def null?
Cproton.pn_data_is_null(@data)
end
# Puts a boolean value.
#
# @param value [Boolean] The boolean value.
#
def bool=(value)
check(Cproton.pn_data_put_bool(@data, value))
end
# If the current node is a boolean, then it returns the value. Otherwise,
# it returns false.
#
# @return [Boolean] The boolean value.
#
def bool
Cproton.pn_data_get_bool(@data)
end
# Puts an unsigned byte value.
#
# @param value [Fixnum] The unsigned byte value.
#
def ubyte=(value)
check(Cproton.pn_data_put_ubyte(@data, value))
end
# If the current node is an unsigned byte, returns its value. Otherwise,
# it returns 0.
#
# @return [Fixnum] The unsigned byte value.
#
def ubyte
Cproton.pn_data_get_ubyte(@data)
end
# Puts a byte value.
#
# @param value [Fixnum] The byte value.
#
def byte=(value)
check(Cproton.pn_data_put_byte(@data, value))
end
# If the current node is an byte, returns its value. Otherwise,
# it returns 0.
#
# @return [Fixnum] The byte value.
#
def byte
Cproton.pn_data_get_byte(@data)
end
# Puts an unsigned short value.
#
# @param value [Fixnum] The unsigned short value
#
def ushort=(value)
check(Cproton.pn_data_put_ushort(@data, value))
end
# If the current node is an unsigned short, returns its value. Otherwise,
# it returns 0.
#
# @return [Fixnum] The unsigned short value.
#
def ushort
Cproton.pn_data_get_ushort(@data)
end
# Puts a short value.
#
# @param value [Fixnum] The short value.
#
def short=(value)
check(Cproton.pn_data_put_short(@data, value))
end
# If the current node is a short, returns its value. Otherwise,
# returns a 0.
#
# @return [Fixnum] The short value.
#
def short
Cproton.pn_data_get_short(@data)
end
# Puts an unsigned integer value.
#
# @param value [Fixnum] the unsigned integer value
#
def uint=(value)
raise TypeError if value.nil?
raise RangeError, "invalid uint: #{value}" if value < 0
check(Cproton.pn_data_put_uint(@data, value))
end
# If the current node is an unsigned int, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The unsigned integer value.
#
def uint
Cproton.pn_data_get_uint(@data)
end
# Puts an integer value.
#
# ==== Options
#
# * value - the integer value
def int=(value)
check(Cproton.pn_data_put_int(@data, value))
end
# If the current node is an integer, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The integer value.
#
def int
Cproton.pn_data_get_int(@data)
end
# Puts a character value.
#
# @param value [Fixnum] The character value.
#
def char=(value)
check(Cproton.pn_data_put_char(@data, value))
end
# If the current node is a character, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The character value.
#
def char
Cproton.pn_data_get_char(@data)
end
# Puts an unsigned long value.
#
# @param value [Fixnum] The unsigned long value.
#
def ulong=(value)
raise TypeError if value.nil?
raise RangeError, "invalid ulong: #{value}" if value < 0
check(Cproton.pn_data_put_ulong(@data, value))
end
# If the current node is an unsigned long, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The unsigned long value.
#
def ulong
Cproton.pn_data_get_ulong(@data)
end
# Puts a long value.
#
# @param value [Fixnum] The long value.
#
def long=(value)
check(Cproton.pn_data_put_long(@data, value))
end
# If the current node is a long, returns its value. Otherwise, returns 0.
#
# @return [Fixnum] The long value.
def long
Cproton.pn_data_get_long(@data)
end
# Puts a timestamp value.
#
# @param value [Fixnum] The timestamp value.
#
def timestamp=(value)
value = value.to_i if (!value.nil? && value.is_a?(Time))
check(Cproton.pn_data_put_timestamp(@data, value))
end
# If the current node is a timestamp, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The timestamp value.
#
def timestamp
Cproton.pn_data_get_timestamp(@data)
end
# Puts a float value.
#
# @param value [Float] The floating point value.
#
def float=(value)
check(Cproton.pn_data_put_float(@data, value))
end
# If the current node is a float, returns its value. Otherwise,
# returns 0.
#
# @return [Float] The floating point value.
#
def float
Cproton.pn_data_get_float(@data)
end
# Puts a double value.
#
# @param value [Float] The double precision floating point value.
#
def double=(value)
check(Cproton.pn_data_put_double(@data, value))
end
# If the current node is a double, returns its value. Otherwise,
# returns 0.
#
# @return [Float] The double precision floating point value.
#
def double
Cproton.pn_data_get_double(@data)
end
# Puts a decimal32 value.
#
# @param value [Fixnum] The decimal32 value.
#
def decimal32=(value)
check(Cproton.pn_data_put_decimal32(@data, value))
end
# If the current node is a decimal32, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The decimal32 value.
#
def decimal32
Cproton.pn_data_get_decimal32(@data)
end
# Puts a decimal64 value.
#
# @param value [Fixnum] The decimal64 value.
#
def decimal64=(value)
check(Cproton.pn_data_put_decimal64(@data, value))
end
# If the current node is a decimal64, returns its value. Otherwise,
# it returns 0.
#
# @return [Fixnum] The decimal64 value.
#
def decimal64
Cproton.pn_data_get_decimal64(@data)
end
# Puts a decimal128 value.
#
# @param value [Fixnum] The decimal128 value.
#
def decimal128=(value)
raise TypeError, "invalid decimal128 value: #{value}" if value.nil?
value = value.to_s(16).rjust(32, "0")
bytes = []
value.scan(/(..)/) {|v| bytes << v[0].to_i(16)}
check(Cproton.pn_data_put_decimal128(@data, bytes))
end
# If the current node is a decimal128, returns its value. Otherwise,
# returns 0.
#
# @return [Fixnum] The decimal128 value.
#
def decimal128
value = ""
Cproton.pn_data_get_decimal128(@data).each{|val| value += ("%02x" % val)}
value.to_i(16)
end
# Puts a +UUID+ value.
#
# The UUID is expected to be in the format of a string or else a 128-bit
# integer value.
#
# @param value [String, Numeric] A string or numeric representation of the UUID.
#
# @example
#
# # set a uuid value from a string value
# require 'securerandom'
# @data.uuid = SecureRandom.uuid
#
# # or
# @data.uuid = "fd0289a5-8eec-4a08-9283-81d02c9d2fff"
#
# # set a uuid value from a 128-bit value
# @data.uuid = 0 # sets to 00000000-0000-0000-0000-000000000000
#
def uuid=(value)
raise ::ArgumentError, "invalid uuid: #{value}" if value.nil?
# if the uuid that was submitted was numeric value, then translated
# it into a hex string, otherwise assume it was a string represtation
# and attempt to decode it
if value.is_a? Numeric
value = "%032x" % value
else
raise ::ArgumentError, "invalid uuid: #{value}" if !valid_uuid?(value)
value = (value[0, 8] +
value[9, 4] +
value[14, 4] +
value[19, 4] +
value[24, 12])
end
bytes = []
value.scan(/(..)/) {|v| bytes << v[0].to_i(16)}
check(Cproton.pn_data_put_uuid(@data, bytes))
end
# If the current value is a +UUID+, returns its value. Otherwise,
# it returns nil.
#
# @return [String] The string representation of the UUID.
#
def uuid
value = ""
Cproton.pn_data_get_uuid(@data).each{|val| value += ("%02x" % val)}
value.insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-")
end
# Puts a binary value.
#
# A binary string is encoded as an ASCII 8-bit string value. This is in
# contranst to other strings, which are treated as UTF-8 encoded.
#
# @param value [String] An arbitrary string value.
#
# @see #string=
#
def binary=(value)
check(Cproton.pn_data_put_binary(@data, value))
end
# If the current node is binary, returns its value. Otherwise, it returns
# an empty string ("").
#
# @return [String] The binary string.
#
# @see #string
#
def binary
Qpid::Proton::Types::BinaryString.new(Cproton.pn_data_get_binary(@data))
end
# Puts a UTF-8 encoded string value.
#
# *NOTE:* A nil value is stored as an empty string rather than as a nil.
#
# @param value [String] The UTF-8 encoded string value.
#
# @see #binary=
#
def string=(value)
check(Cproton.pn_data_put_string(@data, value))
end
# If the current node is a string, returns its value. Otherwise, it
# returns an empty string ("").
#
# @return [String] The UTF-8 encoded string.
#
# @see #binary
#
def string
Qpid::Proton::Types::UTFString.new(Cproton.pn_data_get_string(@data))
end
# Puts a symbolic value.
#
# @param value [String] The symbolic string value.
#
def symbol=(value)
check(Cproton.pn_data_put_symbol(@data, value))
end
# If the current node is a symbol, returns its value. Otherwise, it
# returns an empty string ("").
#
# @return [String] The symbolic string value.
#
def symbol
Cproton.pn_data_get_symbol(@data)
end
# Get the current value as a single object.
#
# @return [Object] The current node's object.
#
# @see #type_code
# @see #type
#
def get
type.get(self);
end
# Puts a new value with the given type into the current node.
#
# @param value [Object] The value.
# @param type_code [Mapping] The value's type.
#
# @private
#
def put(value, type_code);
type_code.put(self, value);
end
private
def valid_uuid?(value)
# ensure that the UUID is in the right format
# xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
value =~ /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
end
# @private
def check(err)
if err < 0
raise DataError, "[#{err}]: #{Cproton.pn_data_error(@data)}"
else
return err
end
end
end
end