mogox/ruby-hl7

View on GitHub
lib/segment.rb

Summary

Maintainability
A
25 mins
Test Coverage
# Ruby Object representation of an hl7 2.x message segment
# The segments can be setup to provide aliases to specific fields with
# optional validation code that is run when the field is modified
# The segment field data is also accessible via the e<number> method.
#
# == Defining a New Segment
#  class HL7::Message::Segment::NK1 < HL7::Message::Segment
#    wieght 100 # segments are sorted ascendingly
#    add_field :something_you_want       # assumes :idx=>1
#    add_field :something_else, :idx=>6  # :idx=>6 and field count=6
#    add_field :something_more           # :idx=>7
#    add_field :block_example do |value|
#       raise HL7::InvalidDataError.new
#                                  unless value.to_i < 100 && value.to_i > 10
#      return value
#    end
#    # this block will be executed when seg.block_example= is called
#    # and when seg.block_example is called
#
class HL7::Message::Segment
  extend HL7::Message::SegmentListStorage
  include HL7::Message::SegmentFields

  attr :segment_parent, true
  attr :element_delim
  attr :item_delim
  attr :segment_weight

  METHOD_MISSING_FOR_INITIALIZER = <<-END
    def method_missing( sym, *args, &blk )
      __seg__.send( sym, args, blk )
    end
  END

  # setup a new HL7::Message::Segment
  # raw_segment:: is an optional String or Array which will be used as the
  #               segment's field data
  # delims:: an optional array of delimiters, where
  #               delims[0] = element delimiter
  #               delims[1] = item delimiter
  def initialize(raw_segment="", delims=[], &blk)
    @segments_by_name = {}
    @field_total = 0
    @is_child = false

    setup_delimiters delims

    @elements = elements_from_segment(raw_segment)

    if block_given?
      callctx = eval( "self", blk.binding )
      def callctx.__seg__(val=nil)
        @__seg_val__ ||= val
      end
      callctx.__seg__(self)
      # TODO: find out if this pollutes the calling namespace permanently...

      eval( METHOD_MISSING_FOR_INITIALIZER, blk.binding )
      yield self
      eval( "class << self; remove_method :method_missing;end", blk.binding )
    end
  end

  # Breaks the raw segment into elements
  # raw_segment:: is an optional String or Array which will be used as the
  #               segment's field data
  def elements_from_segment(raw_segment)
    if (raw_segment.kind_of? Array)
      elements = raw_segment
    else
      elements = HL7::MessageParser.split_by_delimiter( raw_segment,
                                                        @element_delim )
      if raw_segment == ""
        elements[0] = self.class.to_s.split( "::" ).last
        elements << ""
      end
    end
    elements
  end

  def to_info
    "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
  end

  # output the HL7 spec version of the segment
  def to_s
    @elements.join( @element_delim )
  end

  # at the segment level there is no difference between to_s and to_hl7
  alias :to_hl7 :to_s

  # handle the e<number> field accessor
  # and any aliases that didn't get added to the system automatically
  def method_missing( sym, *args, &blk )
    base_str = sym.to_s.gsub( "=", "" )
    base_sym = base_str.to_sym

    if self.class.fields.include?( base_sym )
      # base_sym is ok, let's move on
    elsif /e([0-9]+)/.match( base_str )
      # base_sym should actually be $1, since we're going by
      # element id number
      base_sym = $1.to_i
    else
      super
    end

    if sym.to_s.include?( "=" )
      write_field( base_sym, args )
    else
      if args.length > 0
        write_field( base_sym, args.flatten.select { |arg| arg } )
      else
        read_field( base_sym )
      end
    end
  end

  # sort-compare two Segments, 0 indicates equality
  def <=>( other )
    return nil unless other.kind_of?(HL7::Message::Segment)

    # per Comparable docs: http://www.ruby-doc.org/core/classes/Comparable.html
    diff = self.weight - other.weight
    return -1 if diff > 0
    return 1 if diff < 0
    return 0
  end

  # get the defined sort-weight of this segment class
  # an alias for self.weight
  def weight
    self.class.weight
  end

  # return true if the segment has a parent
  def is_child_segment?
    (@is_child_segment ||= false)
  end

  # indicate whether or not the segment has a parent
  def is_child_segment=(val)
    @is_child_segment = val
  end

  # get the length of the segment (number of fields it contains)
  def length
    0 unless @elements
    @elements.length
  end

  def has_children?
    self.respond_to?(:children)
  end

  private
  def self.singleton #:nodoc:
    class << self; self end
  end

  def setup_delimiters(delims)
    delims = [ delims ].flatten

    @element_delim = ( delims.length>0 ) ? delims[0] : "|"
    @item_delim = ( delims.length>1 ) ? delims[1] : "^"
  end

  # DSL element to define a segment's sort weight
  # returns the segment's current weight by default
  # segments are sorted ascending
  def self.weight(new_weight=nil)
    if new_weight
      singleton.module_eval do
        @my_weight = new_weight
      end
    end

    singleton.module_eval do
      return 999 unless @my_weight
      @my_weight
    end
  end

  def self.convert_to_ts(value) #:nodoc:
    value.respond_to?(:to_hl7) ? value.to_hl7 : value
  end

end