Class: Scaffolding::RoutesFileManipulator

Inherits:
Object
  • Object
show all
Defined in:
lib/scaffolding/routes_file_manipulator.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename, child, parent, transformer_options = {}) ⇒ RoutesFileManipulator

Returns a new instance of RoutesFileManipulator.



7
8
9
10
11
12
13
14
15
# File 'lib/scaffolding/routes_file_manipulator.rb', line 7

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

Instance Attribute Details

#childObject

Returns the value of attribute child.



5
6
7
# File 'lib/scaffolding/routes_file_manipulator.rb', line 5

def child
  @child
end

#concernsObject

Returns the value of attribute concerns.



5
6
7
# File 'lib/scaffolding/routes_file_manipulator.rb', line 5

def concerns
  @concerns
end

#linesObject

Returns the value of attribute lines.



5
6
7
# File 'lib/scaffolding/routes_file_manipulator.rb', line 5

def lines
  @lines
end

#parentObject

Returns the value of attribute parent.



5
6
7
# File 'lib/scaffolding/routes_file_manipulator.rb', line 5

def parent
  @parent
end

#transformer_optionsObject

Returns the value of attribute transformer_options.



5
6
7
# File 'lib/scaffolding/routes_file_manipulator.rb', line 5

def transformer_options
  @transformer_options
end

Instance Method Details

#add_concern(concern) ⇒ Object



457
458
459
# File 'lib/scaffolding/routes_file_manipulator.rb', line 457

def add_concern(concern)
  @concerns.push(concern)
end

#add_concern_at_line(concern, line_number) ⇒ Object

Adds a concern to an existing resource at the given line number. (used by the audit logs gem)



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/scaffolding/routes_file_manipulator.rb', line 467

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

#apply(base_namespaces) ⇒ Object



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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/scaffolding/routes_file_manipulator.rb', line 353

def apply(base_namespaces)
  child_namespaces, child_resource, parent_namespaces, parent_resource = divergent_parts

  within = find_or_create_namespaces(base_namespaces)

  # Add any concerns passed as options.
  add_concern(:sortable) if transformer_options["sortable"]

  # 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_resource([parent_resource], within: within)

    # With deeply nested namespaces we end up with mutliple resource blocks and some things need
    # to go into the last block. See this issue for details:
    # https://github.com/bullet-train-co/bullet_train/issues/1655
    last_parent_within = find_or_convert_resource_block(parent_resource, within: within, find_last: true)

    # 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}/, last_parent_within))
      scope_within = insert([line, "end"], last_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)
      routing_options = "shallow: false"
      routing_options += ", #{formatted_concerns}" if formatted_concerns
      find_or_create_resource([child_resource], options: routing_options, within: deeply_nested_within)
    else
      routing_options = "only: collection_actions"
      routing_options += ", #{formatted_concerns}" if formatted_concerns
      # We find last here for this reason:
      # https://github.com/bullet-train-co/bullet_train/issues/1655
      find_or_create_resource([child_resource], options: routing_options, within: scope_within, find_last: true)

      # 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)
      routing_options = "except: collection_actions"
      routing_options += ", #{formatted_concerns}" if formatted_concerns
      find_or_create_resource([child_resource], options: routing_options, 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], options: formatted_concerns, 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

    find_or_create_resource(child_namespaces + [child_resource], options: formatted_concerns, within: within)

  end
end

#child_partsObject



17
18
19
# File 'lib/scaffolding/routes_file_manipulator.rb', line 17

def child_parts
  @child_parts ||= child.underscore.pluralize.split("/")
end

#common_namespacesObject



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/scaffolding/routes_file_manipulator.rb', line 25

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

#divergent_partsObject



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/scaffolding/routes_file_manipulator.rb', line 38

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

#find_in_namespace(needle, namespaces, within = nil, ignore = nil) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/scaffolding/routes_file_manipulator.rb', line 188

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

#find_last_in_namespace(needle, namespaces, within = nil, ignore = nil) ⇒ Object



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
# File 'lib/scaffolding/routes_file_manipulator.rb', line 161

def find_last_in_namespace(needle, namespaces, within = nil, ignore = nil)
  if namespaces.any?
    namespace_lines = find_namespaces(namespaces, within)
    within = namespace_lines[namespaces.last]
  end

  lines_within = Scaffolding::FileManipulator.lines_within(lines, within)
  lines_within.reverse.each_with_index do |line, line_number|
    reversed_line_number = lines_within.count - line_number - 1
    # + 2 because line_number starts from 0, and within starts one line after
    actual_line_number = (within + reversed_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) + reversed_line_number) if line.match?(needle)
  end

  nil
end

#find_namespaces(namespaces, within = nil) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/scaffolding/routes_file_manipulator.rb', line 54

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

#find_or_convert_resource_block(parent_resource, options = {}) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/scaffolding/routes_file_manipulator.rb', line 318

def find_or_convert_resource_block(parent_resource, options = {})
  resource_statement_line = find_resource([parent_resource], options)
  if resource_statement_line
    resource_statement = lines[resource_statement_line]
    if !resource_statement.match?(/ do(\s.*)?$/)
      lines[resource_statement_line].gsub!("\n", " do\n")
      insert_after(["end"], resource_statement_line)
    end
  else
    raise BulletTrain::SuperScaffolding::CannotFindParentResourceException.new("the parent resource (`#{parent_resource}`) doesn't appear to exist in `#{@filename}`.")
  end

  # capture the line number of 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

#find_or_create_namespaces(namespaces, within = nil) ⇒ Object



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
# File 'lib/scaffolding/routes_file_manipulator.rb', line 106

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

#find_or_create_resource(parts, options = {}) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/scaffolding/routes_file_manipulator.rb', line 239

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

#find_or_create_resource_block(parts, options = {}) ⇒ Object



313
314
315
316
# File 'lib/scaffolding/routes_file_manipulator.rb', line 313

def find_or_create_resource_block(parts, options = {})
  find_or_create_resource(parts, options)
  find_or_convert_resource_block(parts.last, options)
end

#find_resource(parts, options = {}) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/scaffolding/routes_file_manipulator.rb', line 227

def find_resource(parts, options = {})
  parts = parts.dup
  resource = parts.pop
  needle = /resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/({)(.*)(})/, '{\2}')}" : ""}(,?\s.*)?$/

  if options[:find_last]
    find_last_in_namespace(needle, parts, options[:within], options[:ignore])
  else
    find_in_namespace(needle, parts, options[:within], options[:ignore])
  end
end

#find_resource_block(parts, options = {}) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/scaffolding/routes_file_manipulator.rb', line 213

def find_resource_block(parts, options = {})
  within = options[:within]
  parts = parts.dup
  resource = parts.pop
  needle = /resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/({)(.*)(})/, '{\2}')}" : ""}(,?\s.*)? do(\s.*)?$/

  # TODO this doesn't take into account any options like we do in `find_resource`.
  if options[:find_last]
    find_last_in_namespace(needle, parts, within)
  else
    find_in_namespace(needle, parts, within)
  end
end

#formatted_concernsObject



461
462
463
464
# File 'lib/scaffolding/routes_file_manipulator.rb', line 461

def formatted_concerns
  return if @concerns.empty?
  "concerns: #{@concerns}"
end

#insert(lines_to_add, within) ⇒ Object

TODO: Remove this and use the BlockManipulator



339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/scaffolding/routes_file_manipulator.rb', line 339

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

#insert_after(new_lines, line_number, options = {}) ⇒ Object

TODO: Remove this and use the BlockManipulator



87
88
89
90
91
92
93
# File 'lib/scaffolding/routes_file_manipulator.rb', line 87

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

#insert_before(new_lines, line_number, options = {}) ⇒ Object

TODO: Remove this and use the BlockManipulator



78
79
80
81
82
83
84
# File 'lib/scaffolding/routes_file_manipulator.rb', line 78

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

#insert_in_namespace(namespaces, new_lines, within = nil) ⇒ Object



95
96
97
98
99
100
101
102
103
104
# File 'lib/scaffolding/routes_file_manipulator.rb', line 95

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

#namespace_blocks_directly_under_parent(within) ⇒ Object

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.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/scaffolding/routes_file_manipulator.rb', line 298

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

#parent_partsObject



21
22
23
# File 'lib/scaffolding/routes_file_manipulator.rb', line 21

def parent_parts
  @parent_parts ||= parent.underscore.pluralize.split("/")
end

#reinstantiate_masamune_objectObject



483
484
485
# File 'lib/scaffolding/routes_file_manipulator.rb', line 483

def reinstantiate_masamune_object
  @msmn = Masamune::AbstractSyntaxTree.new(lines.join)
end

#scope_namespace_to_parent(namespace, within) ⇒ Object

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.



153
154
155
156
157
158
159
# File 'lib/scaffolding/routes_file_manipulator.rb', line 153

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

#top_level_namespace_block_lines(within) ⇒ Object

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.



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
# File 'lib/scaffolding/routes_file_manipulator.rb', line 257

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