lib/bindata/dsl.rb



module BinData
  module DSLMixin
    def self.included(base) #:nodoc:
      base.extend ClassMethods
    end

    module ClassMethods

      def dsl_parser(parser_type = nil)
        unless defined? @dsl_parser
          parser_type = superclass.dsl_parser.parser_type if parser_type.nil?
          @dsl_parser = DSLParser.new(self, parser_type)
        end
        @dsl_parser
      end

      def method_missing(symbol, *args, &block) #:nodoc:
        dsl_parser.__send__(symbol, *args, &block)
      end

      # Assert object is not an array or string.
      def to_ary; nil; end
      def to_str; nil; end
    end

    # A DSLParser parses and accumulates field definitions of the form
    #
    #   type name, params
    #
    # where:
    #   * +type+ is the under_scored name of a registered type
    #   * +name+ is the (possible optional) name of the field
    #   * +params+ is a hash containing any parameters
    #
    class DSLParser
      def initialize(the_class, parser_type)
        @the_class   = the_class
        @parser_type = parser_type
        @endian      = parent_attribute(:endian, nil)
      end

      attr_reader :parser_type

      def endian(endian = nil)
        if endian.nil?
          @endian
        elsif endian == :big or endian == :little
          @endian = endian
        else
          dsl_raise ArgumentError, "unknown value for endian '#{endian}'"
        end
      end

      def hide(*args)
        if option?(:hidden_fields)
          hidden = args.collect do |name|
                     unless Symbol === name
                       warn "Hidden field '#{name}' should be provided as a symbol.  Using strings is deprecated"
                     end
                     name.to_sym
                   end

          unless defined? @hide
            @hide = parent_attribute(:hide, []).dup
          end

          @hide.concat(hidden.compact)
          @hide
        end
      end

      def fields
        unless defined? @fields
          fields = parent_attribute(:fields, nil)
          klass = option?(:sanitize_fields) ? SanitizedFields : UnSanitizedFields
          @fields = klass.new(endian)
          @fields.copy_fields(fields) if fields
        end

        @fields
      end

      def dsl_params
        case @parser_type
        when :struct
          to_struct_params
        when :array
          to_array_params
        when :choice
          to_choice_params
        when :primitive
          to_struct_params
        when :wrapper
          raise "Wrapper is deprecated"
        else
          raise "unknown parser type #{@parser_type}"
        end
      end

      def method_missing(symbol, *args, &block) #:nodoc:
        type   = symbol
        name   = name_from_field_declaration(args)
        params = params_from_field_declaration(type, args, &block)

        append_field(type, name, params)
      end

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

      def option?(opt)
        options.include?(opt)
      end

      def options
        case @parser_type
        when :struct
          [:multiple_fields, :optional_fieldnames, :sanitize_fields, :hidden_fields]
        when :array
          [:multiple_fields, :optional_fieldnames, :sanitize_fields]
        when :choice
          [:multiple_fields, :all_or_none_fieldnames, :sanitize_fields, :fieldnames_are_values]
        when :primitive
          [:multiple_fields, :optional_fieldnames, :sanitize_fields]
        when :wrapper
          [:only_one_field, :no_fieldnames]
        else
          raise "unknown parser type #{parser_type}"
        end
      end

      def parent_attribute(attr, default = nil)
        parent = @the_class.superclass.respond_to?(:dsl_parser) ? @the_class.superclass.dsl_parser : nil
        if parent and parent.respond_to?(attr)
          parent.send(attr)
        else
          default
        end
      end

      def name_from_field_declaration(args)
        name, params = args
        if name == "" or name.is_a?(Hash)
          nil
        else
          name
        end
      end

      def params_from_field_declaration(type, args, &block)
        params = params_from_args(args)

        if block_given?
          params.merge(params_from_block(type, &block))
        else
          params
        end
      end

      def params_from_args(args)
        name, params = args
        params = name if name.is_a?(Hash)

        params || {}
      end

      def params_from_block(type, &block)
        bindata_classes = {
          :array  => BinData::Array,
          :choice => BinData::Choice,
          :struct => BinData::Struct
        }

        if bindata_classes.include?(type)
          parser = DSLParser.new(bindata_classes[type], type)
          parser.endian(endian)
          parser.instance_eval(&block)

          parser.dsl_params
        else
          {}
        end
      end

      def append_field(type, name, params)
        ensure_valid_field(name)

        fields.add_field(type, name, params)
      rescue ArgumentError => err
        dsl_raise ArgumentError, err.message
      rescue UnRegisteredTypeError => err
        dsl_raise TypeError, "unknown type '#{err.message}'"
      end

      def ensure_valid_field(field_name)
        if too_many_fields?
          dsl_raise SyntaxError, "attempting to wrap more than one type"
        end

        if must_not_have_a_name_failed?(field_name)
          dsl_raise SyntaxError, "field must not have a name"
        end

        if all_or_none_names_failed?(field_name)
          dsl_raise SyntaxError, "fields must either all have names, or none must have names"
        end

        if must_have_a_name_failed?(field_name)
          dsl_raise SyntaxError, "field must have a name"
        end

        ensure_valid_name(field_name)
      end

      def ensure_valid_name(name)
        if name and not option?(:fieldnames_are_values)
          if malformed_name?(name)
            dsl_raise NameError.new("", name), "field '#{name}' is an illegal fieldname"
          end

          if duplicate_name?(name)
            dsl_raise SyntaxError, "duplicate field '#{name}'"
          end

          if name_shadows_method?(name)
            dsl_raise NameError.new("", name), "field '#{name}' shadows an existing method"
          end

          if name_is_reserved?(name)
            dsl_raise NameError.new("", name), "field '#{name}' is a reserved name"
          end
        end
      end

      def too_many_fields?
        option?(:only_one_field) and not fields.empty?
      end

      def must_not_have_a_name_failed?(name)
        option?(:no_fieldnames) and name != nil
      end

      def must_have_a_name_failed?(name)
        option?(:mandatory_fieldnames) and name.nil?
      end

      def all_or_none_names_failed?(name)
        if option?(:all_or_none_fieldnames) and not fields.empty?
          all_names_blank = fields.all_field_names_blank?
          no_names_blank = fields.no_field_names_blank?

          (name != nil and all_names_blank) or (name == nil and no_names_blank)
        else
          false
        end
      end

      def malformed_name?(name)
        /^[a-z_]\w*$/ !~ name.to_s
      end

      def duplicate_name?(name)
        fields.has_field_name?(name)
      end

      def name_shadows_method?(name)
        @the_class.method_defined?(name)
      end

      def name_is_reserved?(name)
        BinData::Struct::RESERVED.include?(name.to_sym)
      end

      def dsl_raise(exception, message)
        backtrace = caller
        backtrace.shift while %r{bindata/dsl.rb} =~ backtrace.first

        raise exception, message + " in #{@the_class}", backtrace
      end

      def to_array_params
        case fields.length
        when 0
          {}
        when 1
          {:type => fields[0].prototype}
        else
          {:type => [:struct, to_struct_params]}
        end
      end

      def to_choice_params
        if fields.length == 0
          {}
        elsif fields.all_field_names_blank?
          {:choices => fields.collect { |f| f.prototype }}
        else
          choices = {}
          fields.each { |f| choices[f.name] = f.prototype }
          {:choices => choices}
        end
      end

      def to_struct_params
        result = {:fields => fields}
        if not endian.nil?
          result[:endian] = endian
        end
        if option?(:hidden_fields) and not hide.empty?
          result[:hide] = hide
        end

        result
      end
    end

    # An array containing a field definition of the form
    # expected by BinData::Struct.
    class UnSanitizedField < ::Array
      def initialize(type, name, params)
        super()
        self << type << name << params
      end
      def type
        self[0]
      end
      def name
        self[1]
      end
      def params
        self[2]
      end
    end

    class UnSanitizedFields < ::Array
      def initialize(endian)
        @endian = endian
      end

      def add_field(type, name, params)
        normalized_endian = @endian.respond_to?(:endian) ? @endian.endian : @endian
        normalized_type = RegisteredClasses.normalize_name(type, normalized_endian)
        self << UnSanitizedField.new(normalized_type, name, params)
      end

      def copy_fields(other)
        concat(other)
      end
    end
  end
end