Class: ADSL::Extract::Rails::RailsExtractor

Inherits:
Object
  • Object
show all
Includes:
CallbackChainSimulator, RailsSpecialGemInstrumentation
Defined in:
lib/adsl/extract/rails/rails_extractor.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from RailsSpecialGemInstrumentation

#instrument_gem_authlogic, #instrument_gem_cancan, #instrument_gem_devise, #instrument_gem_ransack, #instrument_gems

Methods included from CallbackChainSimulator

#halting_status_of, #interrupt_callback_chain_on_render, #split_into_callbacks, #split_into_paths_that_will_or_will_not_halt

Constructor Details

#initialize(options = {}) ⇒ RailsExtractor

Returns a new instance of RailsExtractor.



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
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 20

def initialize(options = {})
  options = Hash[
    :ar_classes => default_activerecord_models,
    :invariants => Dir['invariants/**/*_invs.rb'],
    :instrumentation_filters => []
  ].merge options
 
  @ar_classes = options[:ar_classes].map do |ar_class|
    generator = ActiveRecordMetaclassGenerator.new ar_class
    generator.generate_class
  end
  
  ar_class_names = @ar_classes.map(&:adsl_ast_class_name)
  
  @invariant_extractor = ADSL::Extract::Rails::InvariantExtractor.new ar_class_names
  @invariants = @invariant_extractor.extract(options[:invariants]).map{ |inv| inv.adsl_ast }
  @instrumentation_filters = @invariant_extractor.instrumentation_filters
  @instrumentation_filters += options[:instrumentation_filters]

  @action_instrumenter = ADSL::Extract::Rails::ActionInstrumenter.new ar_class_names
  @action_instrumenter.instrumentation_filters = @instrumentation_filters
  @actions = []
  all_routes.each do |route|
    translation = action_to_adsl_ast(route)
    @actions << translation unless translation.nil?
  end
end

Instance Attribute Details

#actionsObject

Returns the value of attribute actions.



18
19
20
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 18

def actions
  @actions
end

#ar_classesObject

Returns the value of attribute ar_classes.



18
19
20
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 18

def ar_classes
  @ar_classes
end

#instrumentation_filtersObject

Returns the value of attribute instrumentation_filters.



18
19
20
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 18

def instrumentation_filters
  @instrumentation_filters
end

#invariantsObject

Returns the value of attribute invariants.



18
19
20
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 18

def invariants
  @invariants
end

Instance Method Details

#action_name_for(route) ⇒ Object



71
72
73
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 71

def action_name_for(route)
  "#{route[:controller]}__#{route[:action]}"
end

#action_of(route) ⇒ Object



182
183
184
185
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 182

def action_of(route)
  return nil unless route.defaults.include? :action
  route.defaults[:action].to_sym
end

#action_to_adsl_ast(route) ⇒ Object



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
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 101

def action_to_adsl_ast(route)
  instrumentation_allows = @instrumentation_filters.map do |f|
    f.allow_instrumentation? route[:controller].new, route[:action]
  end
  return nil if instrumentation_allows.include? false

  action_name = action_name_for route
  potential_adsl_asts = @actions.select{ |action| action.name.text == action_name }
  raise "Multiple actions with identical names" if potential_adsl_asts.length > 1
  return potential_adsl_asts.first if potential_adsl_asts.length == 1

  prepare_instrumentation route[:controller], route[:action]

  session = ActionDispatch::Integration::Session.new(::Rails.application)
  ::Rails.application.config.action_dispatch.show_exceptions = false

  block = @action_instrumenter.exec_within do
    @action_instrumenter.exec_within do
      request_method = route[:request_method].to_s.downcase.split('|').first
      session.send request_method, route[:url]
    end
    @action_instrumenter.abb.root_lvl_adsl_ast 
  end

  interrupt_callback_chain_on_render block, route[:action]

  action = ADSL::Parser::ASTAction.new({
    :name => ADSL::Parser::ASTIdent.new(:text => action_name),
    :arg_cardinalities => [],
    :arg_names => [],
    :arg_types => [],
    :block => block
  })

  action = action.optimize
  action.prepend_global_variables_by_signatures /^at__.*/, /^atat__.*/

  action
end

#adsl_astObject



201
202
203
204
205
206
207
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 201

def adsl_ast
  ADSL::Parser::ASTSpec.new(
    :classes => @ar_classes.map(&:adsl_ast),
    :actions => @actions,
    :invariants => @invariants
  )
end

#all_routesObject



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 48

def all_routes
  ::Rails.application.routes.routes.map{ |route|
    {
      :request_method => request_method_for(route),
      :url => url_for(route),
      :controller => controller_of(route),
      :action => action_of(route)
    }
  }.select{ |route|
    !route[:action].nil? &&
    !route[:controller].nil? &&
    !route[:url].nil? &&
    !route[:request_method].nil? &&
    route[:controller].action_methods.include?(route[:action].to_s)
  }.uniq{ |a|
    [a[:controller], a[:action]]
  }.sort{ |a, b| [a[:controller].to_s, a[:action]] <=> [b[:controller].to_s, b[:action]] }
end

#callbacks(controller) ⇒ Object



75
76
77
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 75

def callbacks(controller)
  controller.respond_to?(:_process_action_callbacks) ? controller._process_action_callbacks : []
end

#controller_of(route) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 167

def controller_of(route)
  return nil unless route.defaults.include? :controller
  possible_names = [
    "#{route.defaults[:controller].pluralize}_controller".camelize,
    "#{route.defaults[:controller]}_controller".camelize, 
  ]
  possible_names.each do |name|
    begin
      return name.constantize
    rescue NameError
    end
  end
  raise "No controller class found for #{route.defaults}; attempted class names are #{possible_names}"
end

#default_activerecord_modelsObject



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
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 141

def default_activerecord_models
  models_dir = Rails.respond_to?(:root) ? Rails.root.join('app', 'models') : Pathname.new('app/models')
  classes = Dir[models_dir.join '**', '*.rb'].map{ |path|
    relative_path = /^#{Regexp.escape models_dir.to_s}\/(.*)\.rb$/.match(path)[1]
    klass = nil
    while klass.nil? && !relative_path.empty?
      begin
        klass = relative_path.camelize.constantize
      rescue NameError, LoadError
      end
      if klass.nil?
        relative_path = /^[^\/]+\/(.*)$/.match(relative_path)[1]
      end
    end
    raise "Could not find class corresponding to path #{path}" if klass.nil?
    klass
  }.select{ |klass| klass < ActiveRecord::Base }
  until_no_change(classes) do |classes|
    all = classes.dup
    classes.each do |c|
      all << c.superclass unless classes.include?(c.superclass) || c.superclass == ActiveRecord::Base
    end
    all
  end
end

#prepare_instrumentation(controller_class, action) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 79

def prepare_instrumentation(controller_class, action)
  controller_class.class_eval <<-ruby, __FILE__, __LINE__ + 1
    def default_render(*args); end
    def verify_authenticity_token; end
    def params
      ADSL::Extract::Rails::PartiallyUnknownHash.new(
        :controller => '#{ controller_class.controller_name }',
        :action => '#{ action }'
      )
    end
  ruby

  instrument_gems controller_class, action
  
  controller = controller_class.new
  @action_instrumenter.instrument controller, action
  callbacks(controller_class).each do |callback|
    next if callback.filter.is_a?(String)
    @action_instrumenter.instrument controller, callback.filter 
  end
end

#request_method_for(route) ⇒ Object



187
188
189
190
191
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 187

def request_method_for(route)
  method_s = route.verb.source.match(/^\^?(.*?)\$?$/)[1]
  return nil if method_s.empty?
  method_s.to_sym
end

#route_for(controller, action) ⇒ Object



67
68
69
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 67

def route_for(controller, action)
  all_routes.select{ |a| a[:controller] == controller && a[:action] == action.to_sym}.first
end

#url_for(route) ⇒ Object



193
194
195
196
197
198
199
# File 'lib/adsl/extract/rails/rails_extractor.rb', line 193

def url_for(route)
  params = {}
  route.required_parts.each do |part|
    params[part] = 0
  end
  route.format(params)
end