lib/bindata/sanitize.rb



require 'bindata/registry'

module BinData

  # Subclasses of this are sanitized
  class SanitizedParameter; end

  class SanitizedPrototype < SanitizedParameter
    def initialize(obj_type, obj_params, endian)
      endian = endian.endian if endian.respond_to? :endian
      obj_params ||= {}

      @obj_class  = RegisteredClasses.lookup(obj_type, endian)
      @obj_params = SanitizedParameters.new(obj_params, @obj_class, endian)
    end

    def instantiate(value = nil, parent = nil)
      @factory ||= @obj_class.new(@obj_params)

      @factory.new(value, parent)
    end
  end
  #----------------------------------------------------------------------------

  class SanitizedField < SanitizedParameter
    def initialize(name, field_type, field_params, endian)
      @name      = name
      @prototype = SanitizedPrototype.new(field_type, field_params, endian)
    end

    attr_reader :prototype

    def name_as_sym
      @name.nil? ? nil : @name.to_sym
    end

    def name
      @name
    end

    def instantiate(value = nil, parent = nil)
      @prototype.instantiate(value, parent)
    end
  end
  #----------------------------------------------------------------------------

  class SanitizedFields < SanitizedParameter
    def initialize(endian)
      @fields = []
      @endian = endian
    end
    attr_reader :fields

    def add_field(type, name, params)
      name = nil if name == ""

      @fields << SanitizedField.new(name, type, params, @endian)
    end

    def [](idx)
      @fields[idx]
    end

    def empty?
      @fields.empty?
    end

    def length
      @fields.length
    end

    def each(&block)
      @fields.each(&block)
    end

    def collect(&block)
      @fields.collect(&block)
    end

    def field_names
      @fields.collect { |field| field.name_as_sym }
    end

    def has_field_name?(name)
      @fields.detect { |f| f.name_as_sym == name.to_sym }
    end

    def all_field_names_blank?
      @fields.all? { |f| f.name == nil }
    end

    def no_field_names_blank?
      @fields.all? { |f| f.name != nil }
    end

    def copy_fields(other)
      @fields.concat(other.fields)
    end
  end
  #----------------------------------------------------------------------------

  class SanitizedChoices < SanitizedParameter
    def initialize(choices, endian)
      @choices = {}
      choices.each_pair do |key, val|
        if SanitizedParameter === val
          prototype = val
        else
          type, param = val
          prototype = SanitizedPrototype.new(type, param, endian)
        end

        if key == :default
          @choices.default = prototype
        else
          @choices[key] = prototype
        end
      end
    end

    def [](key)
      @choices[key]
    end
  end
  #----------------------------------------------------------------------------

  class SanitizedBigEndian < SanitizedParameter
    def endian
      :big
    end
  end

  class SanitizedLittleEndian < SanitizedParameter
    def endian
      :little
    end
  end
  #----------------------------------------------------------------------------

  # BinData objects are instantiated with parameters to determine their
  # behaviour.  These parameters must be sanitized to ensure their values
  # are valid.  When instantiating many objects with identical parameters,
  # such as an array of records, there is much duplicated sanitizing.
  #
  # The purpose of the sanitizing code is to eliminate the duplicated
  # validation.
  #
  # SanitizedParameters is a hash-like collection of parameters.  Its purpose
  # is to recursively sanitize the parameters of an entire BinData object chain
  # at a single time.
  class SanitizedParameters < Hash

    # Memoized constants
    BIG_ENDIAN    = SanitizedBigEndian.new
    LITTLE_ENDIAN = SanitizedLittleEndian.new

    class << self
      def sanitize(parameters, the_class)
        if SanitizedParameters === parameters
          parameters
        else
          SanitizedParameters.new(parameters, the_class, nil)
        end
      end
    end

    def initialize(parameters, the_class, endian)
      parameters.each_pair { |key, value| self[key.to_sym] = value }

      @the_class = the_class
      @endian    = endian

      sanitize!
    end

    alias_method :has_parameter?, :has_key?

    def needs_sanitizing?(key)
      parameter = self[key]

      parameter and not parameter.is_a?(SanitizedParameter)
    end

    def warn_replacement_parameter(bad_key, suggested_key)
      if has_parameter?(bad_key)
        warn ":#{bad_key} is not used with #{@the_class}.  " +
        "You probably want to change this to :#{suggested_key}"
      end
    end

    def warn_renamed_parameter(old_key, new_key)
      val = delete(old_key)
      if val
        self[new_key] = val
        warn ":#{old_key} has been renamed to :#{new_key} in #{@the_class}.  " +
        "Using :#{old_key} is now deprecated and will be removed in the future"
      end
    end

    def endian
      @endian || self[:endian]
    end
    attr_writer :endian

    def create_sanitized_endian(endian)
      if endian == :big
        BIG_ENDIAN
      elsif endian == :little
        LITTLE_ENDIAN
      else
        raise ArgumentError, "unknown value for endian '#{endian}'"
      end
    end

    def create_sanitized_params(params, the_class)
      SanitizedParameters.new(params, the_class, self.endian)
    end

    def create_sanitized_choices(choices)
      SanitizedChoices.new(choices, self.endian)
    end

    def create_sanitized_fields
      SanitizedFields.new(self.endian)
    end

    def create_sanitized_object_prototype(obj_type, obj_params)
      SanitizedPrototype.new(obj_type, obj_params, self.endian)
    end

    #---------------
    private

    def sanitize!
      ensure_no_nil_values
      merge_default_parameters!

      @the_class.sanitize_parameters!(self)

      ensure_mandatory_parameters_exist
      ensure_mutual_exclusion_of_parameters
    end

    def ensure_no_nil_values
      each do |key, value|
        if value.nil?
          raise ArgumentError,
                "parameter '#{key}' has nil value in #{@the_class}"
        end
      end
    end

    def merge_default_parameters!
      @the_class.default_parameters.each do |key, value|
        self[key] ||= value
      end
    end

    def ensure_mandatory_parameters_exist
      @the_class.mandatory_parameters.each do |key|
        unless has_parameter?(key)
          raise ArgumentError,
                  "parameter '#{key}' must be specified in #{@the_class}"
        end
      end
    end

    def ensure_mutual_exclusion_of_parameters
      return if length < 2

      @the_class.mutually_exclusive_parameters.each do |key1, key2|
        if has_parameter?(key1) and has_parameter?(key2)
          raise ArgumentError, "params '#{key1}' and '#{key2}' " +
                               "are mutually exclusive in #{@the_class}"
        end
      end
    end
  end
  #----------------------------------------------------------------------------

end