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



405
406
407
# File 'lib/scaffolding/routes_file_manipulator.rb', line 405

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)



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/scaffolding/routes_file_manipulator.rb', line 415

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



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

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

#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



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

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
76
77
78
# File 'lib/scaffolding/routes_file_manipulator.rb', line 54

def find_namespaces(namespaces, within = nil)
  results = {}
  reinstantiate_masamune_object
  namespace_nodes = @msmn.method_calls(token_value: "namespace")
  namespace_nodes.map do |node|
    # Get the first argument (a Symbol) without the colon from the list of arguments.
    name = node.arguments.child_nodes.first.unescaped

    # TODO: For some reason we use both strings and symbols for namespaces.
    # i.e. - ["account"] and [:v1].
    if namespaces.include?(name.to_sym)
      results[name.to_sym] = node.line_number - 1
    elsif namespaces.include?(name)
      results[name] = node.line_number - 1
    end
  end

  # `within` uses an Array index whereas Masamune nodes use the actual line number, so we write `within + 1` here.
  if within
    block_end = @msmn.method_calls.find { |node| node.line_number == within + 1 }.location.end_line
    results.reject! { |name, line_number| line_number >= block_end }
  end

  results
end

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



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

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



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

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



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/scaffolding/routes_file_manipulator.rb', line 204

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



277
278
279
280
# File 'lib/scaffolding/routes_file_manipulator.rb', line 277

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



197
198
199
200
201
202
# File 'lib/scaffolding/routes_file_manipulator.rb', line 197

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



189
190
191
192
193
194
195
# File 'lib/scaffolding/routes_file_manipulator.rb', line 189

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



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

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

#insert(lines_to_add, within) ⇒ Object

TODO: Remove this and use the BlockManipulator



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

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



90
91
92
93
94
95
96
# File 'lib/scaffolding/routes_file_manipulator.rb', line 90

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



81
82
83
84
85
86
87
# File 'lib/scaffolding/routes_file_manipulator.rb', line 81

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



98
99
100
101
102
103
104
105
106
107
# File 'lib/scaffolding/routes_file_manipulator.rb', line 98

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.



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

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

We have to do this because the ‘lines` object is constantly changing, so we reinstantiate this object wherever necessary.



433
434
435
# File 'lib/scaffolding/routes_file_manipulator.rb', line 433

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.



156
157
158
159
160
161
162
# File 'lib/scaffolding/routes_file_manipulator.rb', line 156

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.



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

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