bullet_train-super_scaffolding 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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