-
Notifications
You must be signed in to change notification settings - Fork 40
/
routes_file_manipulator.rb
432 lines (374 loc) · 17.6 KB
/
routes_file_manipulator.rb
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
require "scaffolding/block_manipulator"
require "masamune"
class Scaffolding::RoutesFileManipulator
attr_accessor :child, :parent, :lines, :transformer_options, :concerns
def initialize(filename, child, parent, transformer_options = {})
@concerns = []
self.child = child
self.parent = parent
@filename = filename
self.lines = File.readlines(@filename)
self.transformer_options = transformer_options
@msmn = Masamune::AbstractSyntaxTree.new(lines.join)
end
def child_parts
@child_parts ||= child.underscore.pluralize.split("/")
end
def parent_parts
@parent_parts ||= parent.underscore.pluralize.split("/")
end
def common_namespaces
unless @common_namespaces
@common_namespaces ||= []
child_parts_copy = child_parts.dup
parent_parts_copy = parent_parts.dup
while child_parts_copy.first == parent_parts_copy.first && child_parts_copy.count > 1 && parent_parts_copy.count > 1
@common_namespaces << child_parts_copy.shift
parent_parts_copy.shift
end
end
@common_namespaces
end
def divergent_parts
unless @divergent_namespaces
@divergent_namespaces ||= []
child_parts_copy = child_parts.dup
parent_parts_copy = parent_parts.dup
while child_parts_copy.first == parent_parts_copy.first && child_parts_copy.count > 1 && parent_parts_copy.count > 1
child_parts_copy.shift
parent_parts_copy.shift
end
child_resource = child_parts_copy.pop
parent_resource = parent_parts_copy.pop
@divergent_namespaces = [child_parts_copy, child_resource, parent_parts_copy, parent_resource]
end
@divergent_namespaces
end
def find_namespaces(namespaces, within = nil)
namespaces = namespaces.dup
results = {}
reinstantiate_masamune_object
# `within` can refer to either a `resources`, `namespace`, `scope`, or `shallow` block.
blocks = @msmn.method_calls.select { |node| node.token_value.match?(/resources|namespace|scope|shallow/) }
namespace_nodes = blocks.select { |node| node.token_value.match?(/namespace/) }
if within
starting_block = blocks.find { |block| block.line_number - 1 == within }
block_range = (starting_block.location.start_line)..(starting_block.location.end_line)
namespace_nodes.select! { |node| block_range.cover?(node.line_number) }
end
namespace_nodes.each do |node|
name = node.arguments.child_nodes.first.unescaped
results[namespaces.shift] = node.line_number - 1 if namespaces.first.to_s == name
end
results
end
# TODO: Remove this and use the BlockManipulator
def insert_before(new_lines, line_number, options = {})
options[:indent] ||= false
before = lines[0..(line_number - 1)]
new_lines = new_lines.map { |line| (Scaffolding::BlockManipulator.indentation_of(line_number, lines) + (options[:indent] ? " " : "") + line).gsub(/\s+$/, "") + "\n" }
after = lines[line_number..]
self.lines = before + (options[:prepend_newline] ? ["\n"] : []) + new_lines + after
end
# TODO: Remove this and use the BlockManipulator
def insert_after(new_lines, line_number, options = {})
options[:indent] ||= false
before = lines[0..line_number]
new_lines = new_lines.map { |line| (Scaffolding::BlockManipulator.indentation_of(line_number, lines) + (options[:indent] ? " " : "") + line).gsub(/\s+$/, "") + "\n" }
after = lines[(line_number + 1)..]
self.lines = before + new_lines + (options[:append_newline] ? ["\n"] : []) + after
end
def insert_in_namespace(namespaces, new_lines, within = nil)
namespace_lines = find_namespaces(namespaces, within)
if namespace_lines[namespaces.last]
block_start = namespace_lines[namespaces.last]
insertion_point = Scaffolding::BlockManipulator.find_block_end(starting_from: block_start, lines: lines)
insert_before(new_lines, insertion_point, indent: true, prepend_newline: (insertion_point > block_start + 1))
else
raise "we weren't able to insert the following lines into the namespace block for #{namespaces.join(" -> ")}:\n\n#{new_lines.join("\n")}"
end
end
def find_or_create_namespaces(namespaces, within = nil)
namespaces = namespaces.dup
created_namespaces = []
current_namespace = nil
while namespaces.any?
current_namespace = namespaces.shift
namespace_lines = if within.nil?
find_namespaces(created_namespaces + [current_namespace], within)
else
scope_namespace_to_parent(current_namespace, within)
end
unless namespace_lines[current_namespace]
lines_to_add = ["namespace :#{current_namespace} do", "end"]
if created_namespaces.any?
insert_in_namespace(created_namespaces, lines_to_add, within)
else
insert(lines_to_add, within)
end
end
created_namespaces << current_namespace
end
namespace_lines = find_namespaces(created_namespaces + [current_namespace], within)
namespace_lines ? namespace_lines[current_namespace] : nil
end
# Since it's possible for multiple namespaces to exist on different levels,
# We scope the namespace we're trying to scaffold to its proper parent before processing it.
#
# i.e:
# Parent: Insight => Child: Personality::CharacterTrait
# Parent: Team => Child: Personality::Disposition
# In this case, the :personality namespace under :insights should be
# ignored when Super Scaffolding Personality::Dispositon.
#
# resources do :insights do
# namespace :personality do
# resources :character_traits
# end
# end
#
# namespace :personality do
# resources :dispositions
# end
#
# In this case, Personality::CharacterTrait is under Team just like Personality::Disposition,
# but Personality::CharacterTrait's DIRECT parent is Insight so we shouldn't scaffold its routes there.
def scope_namespace_to_parent(namespace, within)
namespace_block_start = namespace_blocks_directly_under_parent(within).map do |namespace_block|
namespace_line_number = namespace_block.begin
namespace_line_number if lines[namespace_line_number].match?(/ +namespace :#{namespace}/)
end.compact
namespace_block_start.present? ? {namespace => namespace_block_start} : {}
end
def find_in_namespace(needle, namespaces, within = nil, ignore = nil)
if namespaces.any?
namespace_lines = find_namespaces(namespaces, within)
within = namespace_lines[namespaces.last]
end
Scaffolding::FileManipulator.lines_within(lines, within).each_with_index do |line, line_number|
# + 2 because line_number starts from 0, and within starts one line after
actual_line_number = (within + line_number + 2)
# The lines we want to ignore may be a a series of blocks, so we check each Range here.
ignore_line = false
if ignore.present?
ignore.each do |lines_to_ignore|
ignore_line = true if lines_to_ignore.include?(actual_line_number)
end
end
next if ignore_line
return (within + (within ? 1 : 0) + line_number) if line.match?(needle)
end
nil
end
def find_resource_block(parts, options = {})
within = options[:within]
parts = parts.dup
resource = parts.pop
# TODO this doesn't take into account any options like we do in `find_resource`.
find_in_namespace(/resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/({)(.*)(})/, '{\2}')}" : ""}(,?\s.*)? do(\s.*)?$/, parts, within)
end
def find_resource(parts, options = {})
parts = parts.dup
resource = parts.pop
needle = /resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/({)(.*)(})/, '{\2}')}" : ""}(,?\s.*)?$/
find_in_namespace(needle, parts, options[:within], options[:ignore])
end
def find_or_create_resource(parts, options = {})
parts = parts.dup
resource = parts.pop
namespaces = parts
namespace_within = find_or_create_namespaces(namespaces, options[:within])
# The namespaces that the developer has declared are captured above in `namespace_within`,
# so all other namespaces nested inside the resource's parent should be ignored.
options[:ignore] = top_level_namespace_block_lines(options[:within]) || []
unless (result = find_resource([resource], options))
result = insert(["resources :#{resource}" + (options[:options] ? ", #{options[:options]}" : "")], namespace_within || options[:within])
end
result
end
# Finds namespace blocks no matter how many levels deep they are nested in resource blocks, etc.
# However, will not find namespace blocks inside namespace blocks.
def top_level_namespace_block_lines(within)
namespaces = @msmn.method_calls(token_value: "namespace")
namespace_line_numbers = namespaces.map(&:line_number)
local_namespace_blocks = []
Scaffolding::FileManipulator.lines_within(lines, within).each do |line|
# Masamune gets the actual line number, whereas File.readlines etc. start at 0.
line_index = lines.index(line) + 1
# Since we only want top-level namespace blocks, we ensure that
# all other namespace blocks INSIDE the top-level namespace blocks are skipped
if namespace_line_numbers.include?(line_index)
# Grab the first symbol token on the same line as the namespace.
reinstantiate_masamune_object
namespace_name = @msmn.symbols.find { |sym| sym.line_number == line_index }.token_value
local_namespace = find_namespaces([namespace_name], within)
starting_line_number = local_namespace[namespace_name]
local_namespace_block = ((starting_line_number + 1)..(Scaffolding::BlockManipulator.find_block_end(starting_from: starting_line_number, lines: lines) + 1))
if local_namespace_blocks.empty?
local_namespace_blocks << local_namespace_block
else
skip_block = false
local_namespace_blocks.each do |block_range|
if block_range.include?(local_namespace_block.first)
skip_block = true
else
next
end
end
local_namespace_blocks << local_namespace_block unless skip_block
end
end
end
local_namespace_blocks
end
# Whereas top_level_namespace_block_lines grabs all namespace blocks that
# appear first no matter how many resource blocks they're nested in,
# this method grabs namespace blocks that are only indented one level deep.
def namespace_blocks_directly_under_parent(within)
blocks = []
if lines[within].match?(/do$/)
parent_indentation_size = Scaffolding::BlockManipulator.indentation_of(within, lines).length
within_block_end = Scaffolding::BlockManipulator.find_block_end(starting_from: within, lines: lines)
within.upto(within_block_end) do |line_number|
if lines[line_number].match?(/^#{" " * (parent_indentation_size + 2)}namespace/)
namespace_block_lines = line_number..Scaffolding::BlockManipulator.find_block_end(starting_from: line_number, lines: lines)
blocks << namespace_block_lines
end
end
end
blocks
end
def find_or_create_resource_block(parts, options = {})
find_or_create_resource(parts, options)
find_or_convert_resource_block(parts.last, options)
end
def find_or_convert_resource_block(parent_resource, options = {})
unless find_resource_block([parent_resource], options)
if (resource_line_number = find_resource([parent_resource], options))
# convert it.
lines[resource_line_number].gsub!("\n", " do\n")
insert_after(["end"], resource_line_number)
else
raise BulletTrain::SuperScaffolding::CannotFindParentResourceException.new("the parent resource (`#{parent_resource}`) doesn't appear to exist in `#{@filename}`.")
end
end
# update the block of code we're working within.
unless (within = find_resource_block([parent_resource], options))
raise "tried to convert the parent resource to a block, but failed?"
end
within
end
# TODO: Remove this and use the BlockManipulator
def insert(lines_to_add, within)
insertion_line = Scaffolding::BlockManipulator.find_block_end(starting_from: within, lines: lines)
result_line = insertion_line
unless insertion_line == within + 1
# only put the extra space if we're adding this line after a block
if /^\s*end\s*$/.match?(lines[insertion_line - 1])
lines_to_add.unshift("")
result_line += 1
end
end
insert_before(lines_to_add, insertion_line, indent: true)
result_line
end
def apply(base_namespaces)
child_namespaces, child_resource, parent_namespaces, parent_resource = divergent_parts
within = find_or_create_namespaces(base_namespaces)
# e.g. Project and Projects::Deliverable
if parent_namespaces.empty? && child_namespaces.any? && parent_resource == child_namespaces.first
# resources :projects do
# scope module: 'projects' do
# resources :deliverables, only: collection_actions
# end
# end
parent_within = find_or_convert_resource_block(parent_resource, within: within)
# add the new resource within that namespace.
line = "scope module: '#{parent_resource}' do"
# TODO you haven't tested this yet.
unless (scope_within = Scaffolding::FileManipulator.find(lines, /#{line}/, parent_within))
scope_within = insert([line, "end"], parent_within)
end
if child_namespaces.size > 1
# If a model has multiple namespaces, we have to account for that here.
# For example, this creates the :issues namespace here when `SendAction`
# and the `parent_resource` is `newsletters`.
#
# resources :newsletters do
# scope module: 'newsletters' do
# resources :issues, only: collection_actions
# namespace :issues do
# resources :send_actions, shallow: false
# end
# end
# end
# TODO: We should be able to just do `child_namespaces.shift`.
child_namespaces_without_parent = child_namespaces.dup
child_namespaces_without_parent.shift
deeply_nested_within = find_or_create_namespaces(child_namespaces_without_parent, scope_within)
find_or_create_resource([child_resource], options: "shallow: false", within: deeply_nested_within)
else
find_or_create_resource([child_resource], options: "only: collection_actions", within: scope_within)
# namespace :projects do
# resources :deliverables, except: collection_actions
# end
# We want to see if there are any namespaces one level above the parent itself,
# because namespaces with the same name as the resource can exist on the same level.
parent_block_start = Scaffolding::BlockManipulator.find_block_parent(parent_within, lines)
namespace_line_within = find_or_create_namespaces(child_namespaces, parent_block_start)
find_or_create_resource([child_resource], options: "except: collection_actions", within: namespace_line_within)
unless find_namespaces(child_namespaces, within)[child_namespaces.last]
raise "tried to insert `namespace :#{child_namespaces.last}` but it seems we failed"
end
end
# e.g. Projects::Deliverable and Objective Under It, Abstract::Concept and Concrete::Thing
elsif parent_namespaces.any?
# namespace :projects do
# resources :deliverables
# end
top_parent_namespace = find_namespaces(parent_namespaces, within)[parent_namespaces.first]
find_or_create_resource(child_namespaces + [child_resource], within: top_parent_namespace)
# resources :projects_deliverables, path: 'projects/deliverables' do
# resources :objectives
# end
block_parent_within = Scaffolding::BlockManipulator.find_block_parent(top_parent_namespace, lines)
parent_namespaces_and_resource = (parent_namespaces + [parent_resource]).join("_")
parent_within = find_or_create_resource_block([parent_namespaces_and_resource], options: "path: '#{parent_namespaces_and_resource.tr("_", "/")}'", within: block_parent_within)
find_or_create_resource(child_namespaces + [child_resource], within: parent_within)
else
begin
within = find_or_convert_resource_block(parent_resource, within: within)
rescue
within = find_or_convert_resource_block(parent_resource, options: "except: collection_actions", within: within)
end
add_concern(:sortable) if transformer_options["sortable"]
find_or_create_resource(child_namespaces + [child_resource], options: formatted_concerns, within: within)
end
end
def add_concern(concern)
@concerns.push(concern)
end
def formatted_concerns
return if @concerns.empty?
"concerns: #{@concerns}"
end
# Adds a concern to an existing resource at the given line number. (used by the audit logs gem)
def add_concern_at_line(concern, line_number)
line = lines[line_number]
existing_concerns = line.match(/concerns: \[(.*)\]/).to_a[1].to_s.split(",")
existing_concerns.map! { |e| e.tr(":", "").tr("\"", "").squish&.to_sym }
existing_concerns.filter! { |e| e.present? }
existing_concerns << concern
existing_concerns.uniq!
if line.include?("concerns:")
lines[line_number].gsub!(/concerns: \[(.*)\]/, "concerns: [#{existing_concerns.map { |e| ":#{e}" }.join(", ")}]")
elsif line.squish.ends_with?(" do")
lines[line_number].gsub!(/ do$/, ", concerns: [#{existing_concerns.map { |e| ":#{e}" }.join(", ")}] do")
else
lines[line_number].gsub!(/resources :(.*)$/, "resources :\\1, concerns: [#{existing_concerns.map { |e| ":#{e}" }.join(", ")}]")
end
end
def reinstantiate_masamune_object
@msmn = Masamune::AbstractSyntaxTree.new(lines.join)
end
end