bullet_train-super_scaffolding 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,384 @@
1
+ class Scaffolding::RoutesFileManipulator
2
+ attr_accessor :child, :parent, :lines, :transformer_options
3
+
4
+ def initialize(filename, child, parent, transformer_options = {})
5
+ self.child = child
6
+ self.parent = parent
7
+ @filename = filename
8
+ self.lines = File.readlines(@filename)
9
+ self.transformer_options = transformer_options
10
+ end
11
+
12
+ def child_parts
13
+ @child_parts ||= child.underscore.pluralize.split("/")
14
+ end
15
+
16
+ def parent_parts
17
+ @parent_parts ||= parent.underscore.pluralize.split("/")
18
+ end
19
+
20
+ def common_namespaces
21
+ unless @common_namespaces
22
+ @common_namespaces ||= []
23
+ child_parts_copy = child_parts.dup
24
+ parent_parts_copy = parent_parts.dup
25
+ while child_parts_copy.first == parent_parts_copy.first && child_parts_copy.count > 1 && parent_parts_copy.count > 1
26
+ @common_namespaces << child_parts_copy.shift
27
+ parent_parts_copy.shift
28
+ end
29
+ end
30
+ @common_namespaces
31
+ end
32
+
33
+ # def divergent_namespaces
34
+ # unless @divergent_namespaces
35
+ # @divergent_namespaces ||= []
36
+ # child_parts_copy = child_parts.dup
37
+ # parent_parts_copy = parent_parts.dup
38
+ # while child_parts_copy.first == parent_parts_copy.first
39
+ # child_parts_copy.shift
40
+ # parent_parts_copy.shift
41
+ # end
42
+ # child_parts_copy.pop
43
+ # parent_parts_copy.pop
44
+ # @divergent_namespaces = [child_parts_copy, parent_parts_copy]
45
+ # end
46
+ # @divergent_namespaces
47
+ # end
48
+
49
+ def divergent_parts
50
+ unless @divergent_namespaces
51
+ @divergent_namespaces ||= []
52
+ child_parts_copy = child_parts.dup
53
+ parent_parts_copy = parent_parts.dup
54
+ while child_parts_copy.first == parent_parts_copy.first && child_parts_copy.count > 1 && parent_parts_copy.count > 1
55
+ child_parts_copy.shift
56
+ parent_parts_copy.shift
57
+ end
58
+ child_resource = child_parts_copy.pop
59
+ parent_resource = parent_parts_copy.pop
60
+ @divergent_namespaces = [child_parts_copy, child_resource, parent_parts_copy, parent_resource]
61
+ end
62
+ @divergent_namespaces
63
+ end
64
+
65
+ def find_namespaces(namespaces, within = nil)
66
+ namespaces = namespaces.dup
67
+ results = {}
68
+ block_end = find_block_end(within) if within
69
+ lines.each_with_index do |line, line_number|
70
+ if within
71
+ next unless line_number > within
72
+ return results if line_number >= block_end
73
+ end
74
+ if line.include?("namespace :#{namespaces.first} do")
75
+ results[namespaces.shift] = line_number
76
+ end
77
+ return results unless namespaces.any?
78
+ end
79
+ results
80
+ end
81
+
82
+ def indentation_of(line_number)
83
+ lines[line_number].match(/^( +)/)[1]
84
+ rescue
85
+ nil
86
+ end
87
+
88
+ def find_block_parent(starting_line_number)
89
+ return nil unless indentation_of(starting_line_number)
90
+ cursor = starting_line_number
91
+ while cursor >= 0
92
+ unless lines[cursor].match?(/^#{indentation_of(starting_line_number)}/) || !lines[cursor].present?
93
+ return cursor
94
+ end
95
+ cursor -= 1
96
+ end
97
+ nil
98
+ end
99
+
100
+ def find_block_end(starting_line_number)
101
+ return nil unless indentation_of(starting_line_number)
102
+ lines.each_with_index do |line, line_number|
103
+ next unless line_number > starting_line_number
104
+ if /^#{indentation_of(starting_line_number)}end\s+/.match?(line)
105
+ return line_number
106
+ end
107
+ end
108
+ nil
109
+ end
110
+
111
+ def insert_before(new_lines, line_number, options = {})
112
+ options[:indent] ||= false
113
+ before = lines[0..(line_number - 1)]
114
+ new_lines = new_lines.map { |line| (indentation_of(line_number) + (options[:indent] ? " " : "") + line).gsub(/\s+$/, "") + "\n" }
115
+ after = lines[line_number..]
116
+ self.lines = before + (options[:prepend_newline] ? ["\n"] : []) + new_lines + after
117
+ end
118
+
119
+ def insert_after(new_lines, line_number, options = {})
120
+ options[:indent] ||= false
121
+ before = lines[0..line_number]
122
+ new_lines = new_lines.map { |line| (indentation_of(line_number) + (options[:indent] ? " " : "") + line).gsub(/\s+$/, "") + "\n" }
123
+ after = lines[(line_number + 1)..]
124
+ self.lines = before + new_lines + (options[:append_newline] ? ["\n"] : []) + after
125
+ end
126
+
127
+ def insert_in_namespace(namespaces, new_lines, within = nil)
128
+ namespace_lines = find_namespaces(namespaces, within)
129
+ if namespace_lines[namespaces.last]
130
+ block_start = namespace_lines[namespaces.last]
131
+ insertion_point = find_block_end(block_start)
132
+ insert_before(new_lines, insertion_point, indent: true, prepend_newline: (insertion_point > block_start + 1))
133
+ else
134
+ raise "we weren't able to insert the following lines into the namespace block for #{namespaces.join(" -> ")}:\n\n#{new_lines.join("\n")}"
135
+ end
136
+ end
137
+
138
+ def find_or_create_namespaces(namespaces, within = nil)
139
+ namespaces = namespaces.dup
140
+ created_namespaces = []
141
+ current_namespace = nil
142
+ while namespaces.any?
143
+ current_namespace = namespaces.shift
144
+ namespace_lines = find_namespaces(created_namespaces + [current_namespace], within)
145
+ unless namespace_lines[current_namespace]
146
+ lines_to_add = ["namespace :#{current_namespace} do", "end"]
147
+ if created_namespaces.any?
148
+ insert_in_namespace(created_namespaces, lines_to_add, within)
149
+ else
150
+ insert(lines_to_add, within)
151
+ end
152
+ end
153
+ created_namespaces << current_namespace
154
+ end
155
+ namespace_lines = find_namespaces(created_namespaces + [current_namespace], within)
156
+ namespace_lines ? namespace_lines[current_namespace] : nil
157
+ end
158
+
159
+ def find(needle, within = nil)
160
+ lines_within(within).each_with_index do |line, line_number|
161
+ return (within + (within ? 1 : 0) + line_number) if line.match?(needle)
162
+ end
163
+
164
+ nil
165
+ end
166
+
167
+ def find_in_namespace(needle, namespaces, within = nil, ignore = nil)
168
+ if namespaces.any?
169
+ namespace_lines = find_namespaces(namespaces, within)
170
+ within = namespace_lines[namespaces.last]
171
+ end
172
+
173
+ lines_within(within).each_with_index do |line, line_number|
174
+ # + 2 because line_number starts from 0, and within starts one line after
175
+ actual_line_number = (within + line_number + 2)
176
+
177
+ # The lines we want to ignore may be a a series of blocks, so we check each Range here.
178
+ ignore_line = false
179
+ if ignore.present?
180
+ ignore.each do |lines_to_ignore|
181
+ ignore_line = true if lines_to_ignore.include?(actual_line_number)
182
+ end
183
+ end
184
+
185
+ next if ignore_line
186
+ return (within + (within ? 1 : 0) + line_number) if line.match?(needle)
187
+ end
188
+
189
+ nil
190
+ end
191
+
192
+ def find_resource_block(parts, options = {})
193
+ within = options[:within]
194
+ parts = parts.dup
195
+ resource = parts.pop
196
+ # TODO this doesn't take into account any options like we do in `find_resource`.
197
+ find_in_namespace(/resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/(\[)(.*)(\])/, '\[\2\]')}" : ""}(,?\s.*)? do(\s.*)?$/, parts, within)
198
+ end
199
+
200
+ def find_resource(parts, options = {})
201
+ parts = parts.dup
202
+ resource = parts.pop
203
+ needle = /resources :#{resource}#{options[:options] ? ", #{options[:options].gsub(/(\[)(.*)(\])/, '\[\2\]')}" : ""}(,?\s.*)?$/
204
+ find_in_namespace(needle, parts, options[:within], options[:ignore])
205
+ end
206
+
207
+ def find_or_create_resource(parts, options = {})
208
+ parts = parts.dup
209
+ resource = parts.pop
210
+ namespaces = parts
211
+ namespace_within = find_or_create_namespaces(namespaces, options[:within])
212
+
213
+ # The namespaces that the developer has declared are captured above in `namespace_within`,
214
+ # so all other namespaces nested inside the resource's parent should be ignored.
215
+ options[:ignore] = top_level_namespace_block_lines(options[:within]) || []
216
+
217
+ unless (result = find_resource([resource], options))
218
+ result = insert(["resources :#{resource}" + (options[:options] ? ", #{options[:options]}" : "")], namespace_within || options[:within])
219
+ end
220
+ result
221
+ end
222
+
223
+ def top_level_namespace_block_lines(within)
224
+ local_namespace_blocks = []
225
+ lines_within(within).each do |line|
226
+ # i.e. - Retrieve "foo" from "namespace :foo do"
227
+ match_data = line.match(/(\s*namespace\s:)(.*)(\sdo$)/)
228
+
229
+ # Since we only want top-level namespace blocks, we ensure that
230
+ # all other namespace blocks INSIDE the top-level namespace blocks are skipped
231
+ if match_data.present?
232
+ namespace_name = match_data[2]
233
+ local_namespace = find_namespaces([namespace_name], within)
234
+ starting_line_number = local_namespace[namespace_name]
235
+ local_namespace_block = ((starting_line_number + 1)..(find_block_end(starting_line_number) + 1))
236
+
237
+ if local_namespace_blocks.empty?
238
+ local_namespace_blocks << local_namespace_block
239
+ else
240
+ skip_block = false
241
+ local_namespace_blocks.each do |block_range|
242
+ if block_range.include?(local_namespace_block.first)
243
+ skip_block = true
244
+ else
245
+ next
246
+ end
247
+ end
248
+ local_namespace_blocks << local_namespace_block unless skip_block
249
+ end
250
+ end
251
+ end
252
+
253
+ local_namespace_blocks
254
+ end
255
+
256
+ def find_or_create_resource_block(parts, options = {})
257
+ find_or_create_resource(parts, options)
258
+ find_or_convert_resource_block(parts.last, options)
259
+ end
260
+
261
+ def lines_within(within)
262
+ return lines unless within
263
+ lines[(within + 1)..(find_block_end(within) + 1)]
264
+ end
265
+
266
+ def find_or_convert_resource_block(parent_resource, options = {})
267
+ unless find_resource_block([parent_resource], options)
268
+ if (resource_line_number = find_resource([parent_resource], options))
269
+ # convert it.
270
+ lines[resource_line_number].gsub!("\n", " do\n")
271
+ insert_after(["end"], resource_line_number)
272
+ else
273
+ raise "the parent resource (`#{parent_resource}`) doesn't appear to exist in `#{@filename}`."
274
+ end
275
+ end
276
+
277
+ # update the block of code we're working within.
278
+ unless (within = find_resource_block([parent_resource], options))
279
+ raise "tried to convert the parent resource to a block, but failed?"
280
+ end
281
+
282
+ within
283
+ end
284
+
285
+ def insert(lines_to_add, within)
286
+ insertion_line = find_block_end(within)
287
+ result_line = insertion_line
288
+ unless insertion_line == within + 1
289
+ # only put the extra space if we're adding this line after a block
290
+ if /^\s*end\s*$/.match?(lines[insertion_line - 1])
291
+ lines_to_add.unshift("")
292
+ result_line += 1
293
+ end
294
+ end
295
+ insert_before(lines_to_add, insertion_line, indent: true)
296
+ result_line
297
+ end
298
+
299
+ def apply(base_namespaces)
300
+ child_namespaces, child_resource, parent_namespaces, parent_resource = divergent_parts
301
+
302
+ within = find_or_create_namespaces(base_namespaces)
303
+ within = find_or_create_namespaces(common_namespaces, within) if common_namespaces.any?
304
+
305
+ # e.g. Project and Projects::Deliverable
306
+ if parent_namespaces.empty? && child_namespaces.one? && parent_resource == child_namespaces.first
307
+
308
+ # resources :projects do
309
+ # scope module: 'projects' do
310
+ # resources :deliverables, only: collection_actions
311
+ # end
312
+ # end
313
+
314
+ parent_within = find_or_convert_resource_block(parent_resource, within: within)
315
+
316
+ # add the new resource within that namespace.
317
+ line = "scope module: '#{parent_resource}' do"
318
+ # TODO you haven't tested this yet.
319
+ unless (scope_within = find(/#{line}/, parent_within))
320
+ scope_within = insert([line, "end"], parent_within)
321
+ end
322
+
323
+ find_or_create_resource([child_resource], options: "only: collection_actions", within: scope_within)
324
+
325
+ # namespace :projects do
326
+ # resources :deliverables, except: collection_actions
327
+ # end
328
+
329
+ unless find_namespaces(child_namespaces, within)[child_namespaces.last]
330
+ insert_after(["", "namespace :#{child_namespaces.last} do", "end"], find_block_end(scope_within))
331
+ unless find_namespaces(child_namespaces, within)[child_namespaces.last]
332
+ raise "tried to insert `namespace :#{child_namespaces.last}` but it seems we failed"
333
+ end
334
+ end
335
+
336
+ find_or_create_resource(child_namespaces + [child_resource], options: "except: collection_actions", within: within)
337
+
338
+ # e.g. Projects::Deliverable and Objective Under It, Abstract::Concept and Concrete::Thing
339
+ elsif parent_namespaces.any?
340
+
341
+ # namespace :projects do
342
+ # resources :deliverables
343
+ # end
344
+ #
345
+ # resources :projects_deliverables, path: 'projects/deliverables' do
346
+ # resources :objectives
347
+ # end
348
+
349
+ find_resource(parent_namespaces + [parent_resource], within: within)
350
+ top_parent_namespace = find_namespaces(parent_namespaces, within)[parent_namespaces.first]
351
+ block_parent_within = find_block_parent(top_parent_namespace)
352
+ parent_namespaces_and_resource = (parent_namespaces + [parent_resource]).join("_")
353
+ parent_within = find_or_create_resource_block([parent_namespaces_and_resource], options: "path: '#{parent_namespaces_and_resource.tr("_", "/")}'", within: block_parent_within)
354
+ find_or_create_resource(child_namespaces + [child_resource], within: parent_within)
355
+
356
+ else
357
+
358
+ begin
359
+ within = find_or_convert_resource_block(parent_resource, within: within)
360
+ rescue
361
+ within = find_or_convert_resource_block(parent_resource, options: "except: collection_actions", within: within)
362
+ end
363
+
364
+ find_or_create_resource(child_namespaces + [child_resource], options: define_concerns, within: within)
365
+
366
+ end
367
+ end
368
+
369
+ # Pushing custom concerns here will add them to the routes file when Super Scaffolding.
370
+ def define_concerns
371
+ concerns = []
372
+ concerns.push(:sortable) if transformer_options["sortable"]
373
+
374
+ return if concerns.empty?
375
+ "concerns: #{concerns}"
376
+ end
377
+
378
+ def write
379
+ puts "Updating '#{@filename}'."
380
+ File.open(@filename, "w+") do |file|
381
+ file.puts(lines.join.strip + "\n")
382
+ end
383
+ end
384
+ end