fidothe/saxon-rb

View on GitHub
lib/saxon/xdm/atomic_value.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
require_relative 'sequence_like'
require_relative '../s9api'
require_relative '../qname'
require_relative '../item_type'

module Saxon
  module XDM
    # An XPath Data Model Node object, representing an XML document, or an element
    # or one of the other node chunks in the XDM.
    class AtomicValue
      include XDM::SequenceLike
      include XDM::ItemSequenceLike

      # Error thrown when an attempt to create QName-holding XDM::AtomicValue is
      # made using anything other than a {Saxon::QName} or s9api.QName instance.
      #
      # QNames are dependent on the namespace URI, which isn't present in the
      # lexical string you normally see (e.g. <tt>prefix:name</tt>). Prefixes are
      # only bound to a URI in the context of a particular document, so creating
      # them imlpicitly through the XDM::AtomicValue creation process doesn't really
      # work. They need to be created explicitly and then handed in to be wrapped.
      class CannotCreateQNameFromString < StandardError
        # returns an error message
        def to_s
          "QName XDM::AtomicValues must be created using an instance of Saxon::QName, not a string like 'prefix:name': Prefix URI binding is undefined at this point"
        end
      end

      # xs:NOTATION is another QName-holding XDM type. Unlike xs:QName, there
      # isn't a way to create these outside of parsing an XML document within
      # Saxon, so attempting to do so raises this error.
      class NotationCannotBeDirectlyCreated < StandardError
         # returns an error message
         def to_s
          "xs:NOTATION XDM::AtomicValues cannot be directly created outside of XML parsing."
        end
      end

      class << self
        # ItemType representing QNames
        def xs_qname
          @xs_qname ||= ItemType.get_type('xs:QName')
        end

        # ItemType representing NOTATION
        def xs_notation
          @xs_notation ||= ItemType.get_type('xs:NOTATION')
        end

        # Convert a single Ruby value into an XDM::AtomicValue
        #
        # If no explicit {ItemType} is passed, the correct type is guessed based
        # on the class of the value. (e.g. <tt>xs:date</tt> for {::Date}.)
        #
        # Values are converted based on Ruby idioms and operations, so an explicit
        # {ItemType} of <tt>xs:boolean</tt> will use truthyness to evaluate the
        # value. This means that the Ruby string <tt>'false'</tt> will produce an
        # <tt>xs:boolean</tt> whose value is <tt>true</tt>, because all strings
        # are truthy. If you need to pass lexical strings unchanged, use
        # {XDM::AtomicValue.from_lexical_string}.
        #
        # @param value [Object] the value to convert
        #
        # @param item_type [Saxon::ItemType, String] the value's type, as either
        #   an {ItemType} or a name (<tt>xs:date</tt>)
        #
        # @return [Saxon::XDM::AtomicValue]
        def create(value, item_type = nil)
          case value
          when Saxon::S9API::XdmAtomicValue
            new(value)
          when XDM::AtomicValue
            value
          else
            return create_implying_item_type(value) if item_type.nil?

            item_type = ItemType.get_type(item_type)

            return new(Saxon::S9API::XdmAtomicValue.new(value.to_java)) if item_type == xs_qname && value_is_qname?(value)
            raise NotationCannotBeDirectlyCreated if item_type == xs_notation

            value_lexical_string = item_type.lexical_string(value)
            new(new_s9_xdm_atomic_value(value_lexical_string, item_type))
          end
        end

        # convert a lexical string representation of an XDM Atomic Value into an
        # XDM::AtomicValue.
        #
        # Note that this skips all conversion and checking of the string before
        # handing it off to Saxon.
        #
        # @param value [String] the lexical string representation of the value
        # @param item_type [Saxon::ItemType, String] the value's type, as either
        #   an {ItemType} or a name (<tt>xs:date</tt>)
        # @return [Saxon::XDM::AtomicValue]
        def from_lexical_string(value, item_type)
          item_type = ItemType.get_type(item_type)
          raise CannotCreateQNameFromString if item_type == xs_qname
          new(new_s9_xdm_atomic_value(value.to_s, item_type))
        end

        private

        def new_s9_xdm_atomic_value(value, item_type)
          Saxon::S9API::XdmAtomicValue.new(value.to_java, item_type.to_java)
        end

        def create_implying_item_type(value)
          return new(Saxon::S9API::XdmAtomicValue.new(value.to_java)) if value_is_qname?(value)

          item_type = ItemType.get_type(value.class)

          new(new_s9_xdm_atomic_value(item_type.lexical_string(value), item_type))
        end

        def value_is_qname?(value)
          QName === value || S9API::QName === value
        end
      end

      attr_reader :s9_xdm_atomic_value
      private :s9_xdm_atomic_value

      # @api private
      def initialize(s9_xdm_atomic_value)
        @s9_xdm_atomic_value = s9_xdm_atomic_value
      end

      # Return a {QName} representing the type of the value
      #
      # @return [Saxon::QName] the {QName} of the value's type
      def type_name
        @type_name ||= Saxon::QName.new(s9_xdm_atomic_value.getTypeName)
      end

      # @return [Saxon::S9API::XdmAtomicValue] The underlying Saxon Java XDM
      # atomic value object.
      def to_java
        s9_xdm_atomic_value
      end

      # Return the value as a String. Like calling XPath's <tt>string()</tt>
      # function.
      #
      # @return [String] The string representation of the value
      def to_s
        s9_xdm_atomic_value.toString
      end

      # @return [Saxon::ItemType] The ItemType of the value
      def item_type
        @item_type ||= Saxon::ItemType.get_type(Saxon::QName.resolve(s9_xdm_atomic_value.getTypeName))
      end

      # Return the value as an instance of the Ruby class that best represents the
      # type of the value. Types with no sensible equivalent are returned as their
      # lexical string form
      #
      # @return [Object] A Ruby object representation of the value
      def to_ruby
        @ruby_value ||= item_type.ruby_value(self).freeze
      end

      # compares two {XDM::AtomicValue}s using the underlying Saxon and XDM
      # comparision rules
      #
      # @param other [Saxon::XDM::AtomicValue]
      # @return [Boolean]
      def ==(other)
        return false unless other.is_a?(XDM::AtomicValue)
        s9_xdm_atomic_value.equals(other.to_java)
      end

      alias_method :eql?, :==

      # Compute a hash-code for this {AtomicValue}.
      #
      # Two {AtomicValue}s with the same content will have the same hash code (and will compare using eql?).
      # @see Object#hash
      def hash
        @hash ||= s9_xdm_atomic_value.hashCode
      end
    end
  end
end