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



411
412
413
# File 'lib/scaffolding/routes_file_manipulator.rb', line 411

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)



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/scaffolding/routes_file_manipulator.rb', line 421

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



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

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_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)
      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
      find_or_create_resource([child_resource], options: routing_options, 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)
      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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/scaffolding/routes_file_manipulator.rb', line 161

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_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



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/scaffolding/routes_file_manipulator.rb', line 280

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

#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



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/scaffolding/routes_file_manipulator.rb', line 201

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



275
276
277
278
# File 'lib/scaffolding/routes_file_manipulator.rb', line 275

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



194
195
196
197
198
199
# File 'lib/scaffolding/routes_file_manipulator.rb', line 194

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

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



186
187
188
189
190
191
192
# File 'lib/scaffolding/routes_file_manipulator.rb', line 186

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

#formatted_concernsObject



415
416
417
418
# File 'lib/scaffolding/routes_file_manipulator.rb', line 415

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

#insert(lines_to_add, within) ⇒ Object

TODO: Remove this and use the BlockManipulator



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

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.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/scaffolding/routes_file_manipulator.rb', line 260

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



437
438
439
# File 'lib/scaffolding/routes_file_manipulator.rb', line 437

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.



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

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