aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/code_generation.rb
blob: d2d7deb212aeee196dc4a6c29cbd336c30923b77 (plain) (tree)




















































































































                                                                                                                                        
                        




















































































































                                                                                                   
module ActionController
  module CodeGeneration #:nodoc
    class GenerationError < StandardError
    end
  
    class Source
      attr_reader :lines, :indentation_level
      IndentationString = '  '
      def initialize
        @lines, @indentation_level = [], 0
      end
      def line(line)
        @lines << (IndentationString * @indentation_level + line)
      end
      alias :<< :line
    
      def indent
        @indentation_level += 1
        yield
        ensure
        @indentation_level -= 1
      end
    
      def to_s() lines.join("\n") end
    end

    class CodeGenerator
      attr_accessor :source, :locals
      def initialize(source = nil)
        @locals = []
        @source = source || Source.new
      end
    
      BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
      ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
      Keywords = BeginKeywords + ResumeKeywords
    
      def method_missing(keyword, *text)
        if Keywords.include? keyword
          if ResumeKeywords.include? keyword
            raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/ 
            source.lines.pop # Remove the 'end'
          end
      
          line "#{keyword} #{text.join ' '}"
          begin source.indent { yield(self.dup) }
          ensure line 'end'
          end
        else
          super(keyword, *text)
        end
      end
    
      def line(*args) self.source.line(*args) end
      alias :<< :line
      def indent(*args, &block) source(*args, &block) end
      def to_s() source.to_s end
    
      def share_locals_with(other)
        other.locals = self.locals = (other.locals | locals) 
      end
    
      FieldsToDuplicate = [:locals]
      def dup
        copy = self.class.new(source)
        self.class::FieldsToDuplicate.each do |sym|
          value = self.send(sym)
          value = value.dup unless value.nil? || value.is_a?(Numeric)
          copy.send("#{sym}=", value)
        end
        return copy
      end
    end

    class RecognitionGenerator < CodeGenerator
      Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
      attr_accessor(*Attributes)
      FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
    
      def initialize(*args)
        super(*args)
        @after, @before = [], []
        @current = nil
        @results, @constants = {}, {}
        @depth = 0
        @move_ahead = nil
        @finish_statement = Proc.new {|hash_expr| hash_expr}
      end
    
      def if_next_matches(string, &block)
        test = Routing.test_condition(next_segment(true), string)
        self.if(test, &block)
      end
    
      def move_forward(places = 1)
        dup = self.dup
        dup.depth += 1
        dup.move_ahead = places
        yield dup
      end
    
      def next_segment(assign_inline = false, default = nil)
        if locals.include?(segment_name)
          code = segment_name
        else
          code = "#{segment_name} = #{path_name}[#{index_name}]"
          if assign_inline
            code = "(#{code})"
          else
            line(code)
            code = segment_name
          end
        
          locals << segment_name
        end
        code = "(#{code} || #{default.inspect})" if default 
      
        return code.to_s
      end
    
      def segment_name() "segment#{depth}".to_sym end
      def path_name() :path end
      def index_name
        move_ahead, @move_ahead = @move_ahead, nil
        move_ahead ? "index += #{move_ahead}" : 'index'
      end
    
      def continue
        dup = self.dup
        dup.before << dup.current
        dup.current = dup.after.shift
        dup.go
      end
    
      def go
        if current then current.write_recognition(self)
        else self.finish
        end
      end 
    
      def result(key, expression, delay = false)
        unless delay
          line "#{key}_value = #{expression}"
          expression = "#{key}_value"
        end
        results[key] = expression
      end
      def constant_result(key, object)
        constants[key] = object
      end
  
      def finish(ensure_traversal_finished = true)
        pairs = [] 
        (results.keys + constants.keys).uniq.each do |key|
          pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
        end
        hash_expr = "{#{pairs.join(', ')}}"
      
        statement = finish_statement.call(hash_expr)
        if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
        else self << statement
        end
      end
    end
  
    class GenerationGenerator < CodeGenerator
      Attributes = [:after, :before, :current, :segments]
      attr_accessor(*Attributes)
      FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
    
      def initialize(*args)
        super(*args)
        @after, @before = [], []
        @current = nil
        @segments = []
      end
    
      def hash_name() 'hash' end
      def local_name(key) "#{key}_value" end
    
      def hash_value(key, assign = true, default = nil)
        if locals.include?(local_name(key)) then code = local_name(key)
        else
          code = "hash[#{key.to_sym.inspect}]"
          if assign
            code = "(#{local_name(key)} = #{code})"
            locals << local_name(key)
          end
        end
        code = "(#{code} || #{default.inspect})" if default
        return code
      end 
    
      def expire_for_keys(*keys)
        return if keys.empty?
        conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
        line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
      end
    
      def add_segment(*segments)
        d = dup
        d.segments.concat segments
        yield d
      end
    
      def go
        if current then current.write_generation(self)
        else self.finish
        end
      end
    
      def continue
        d = dup
        d.before << d.current
        d.current = d.after.shift
        d.go
      end
    
      def finish
        line %("#{segments.join('/')}")
      end

      def check_conditions(conditions)
        tests = []
        generator = nil
        conditions.each do |key, condition|
          tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
          tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
          generator = self.dup unless generator
        end
        return tests.join(' && ')
      end
    end
  end
end