aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/code_generation.rb
blob: a4800037203d0e6a933a215ad3cb105fc0fe647f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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