lib/rspec/core/metadata.rb



module RSpec
  module Core
    class Metadata < Hash

      def initialize(superclass_metadata=nil)
        @superclass_metadata = superclass_metadata
        if @superclass_metadata
          update(@superclass_metadata)
          example_group = {:example_group => @superclass_metadata[:example_group]}
        end

        store(:example_group, example_group || {})
        yield self if block_given?
      end

      RESERVED_KEYS = [
        :description,
        :example_group,
        :execution_result,
        :file_path,
        :full_description,
        :line_number,
        :location
      ]

      def process(*args)
        user_metadata = args.last.is_a?(Hash) ? args.pop : {}
        ensure_valid_keys(user_metadata)

        self[:example_group][:describes] = described_class_from(*args)
        self[:example_group][:description] = description_from(*args)
        self[:example_group][:full_description] = full_description_from(*args)

        self[:example_group][:block] = user_metadata.delete(:example_group_block)
        self[:example_group][:file_path], self[:example_group][:line_number] = file_and_line_number_from(caller)
        self[:example_group][:location] = location_from(self[:example_group])

        update(user_metadata)
      end

      def ensure_valid_keys(user_metadata)
        RESERVED_KEYS.each do |key|
          if user_metadata.keys.include?(key)
            raise <<-EOM
#{"*"*50}
:#{key} is not allowed

RSpec reserves some hash keys for its own internal use,
including :#{key}, which is used on:

  #{caller(0)[4]}.

Here are all of RSpec's reserved hash keys:

  #{RESERVED_KEYS.join("\n  ")}
#{"*"*50}
EOM
            raise ":#{key} is not allowed"
          end
        end
      end

      def for_example(description, options)
        dup.configure_for_example(description,options)
      end

      def configure_for_example(description, options)
        store(:description, description.to_s)
        store(:full_description, "#{self[:example_group][:full_description]} #{self[:description]}")
        store(:execution_result, {})
        self[:file_path], self[:line_number] = file_and_line_number_from(caller)
        self[:location] = location_from(self)
        update(options)
      end

      def all_apply?(filters)
        filters.all? do |key, value|
          apply_condition(key, value)
        end
      end

      def relevant_line_numbers(metadata)
        line_numbers = [metadata[:line_number]]
        if metadata[:example_group]
          line_numbers + relevant_line_numbers(metadata[:example_group])
        else
          line_numbers
        end
      end

      def apply_condition(key, value, metadata=nil)
        metadata ||= self
        case value
        when Hash
          value.all? { |k, v| apply_condition(k, v, metadata[key]) }
        when Regexp
          metadata[key] =~ value
        when Proc
          value.call(metadata[key]) rescue false
        when Fixnum
          if key == :line_number
            relevant_line_numbers(metadata).include?(world.preceding_declaration_line(value))
          else
            metadata[key] == value
          end
        else
          metadata[key] == value
        end
      end

    private

      def world
        RSpec.world
      end

      def superclass_metadata
        @superclass_metadata ||= { :example_group => {} }
      end

      def description_from(*args)
        args.inject("") do |result, a|
          a = a.to_s.strip
          if result == ""
            a
          elsif a =~ /^(#|::|\.)/
            "#{result}#{a}"
          else
            "#{result} #{a}"
          end
        end
      end

      def full_description_from(*args)
        if superclass_metadata[:example_group][:full_description]
          description_from(superclass_metadata[:example_group][:full_description], *args)
        else
          description_from(*args)
        end
      end

      def described_class_from(*args)
        if args.first.is_a?(String) || args.first.is_a?(Symbol)
          superclass_metadata[:example_group][:describes]
        else
          args.first
        end
      end

      def file_and_line_number_from(list)
        entry = first_caller_from_outside_rspec_from_caller(list)
        entry =~ /(.+?):(\d+)(|:\d+)/
        return [$1, $2.to_i]
      end

      def first_caller_from_outside_rspec_from_caller(list)
        list.detect {|l| l !~ /\/lib\/rspec\/core/}
      end

      def location_from(metadata)
        "#{metadata[:file_path]}:#{metadata[:line_number]}"
      end
    end
  end
end