staticky-files 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,984 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ class Files # rubocop:disable Metrics/ClassLength
5
+ require_relative "files/version"
6
+ require_relative "files/error"
7
+ require_relative "files/adapter"
8
+
9
+ OPEN_MODE = ::File::RDWR
10
+ WRITE_MODE = (::File::CREAT | ::File::WRONLY | ::File::TRUNC).freeze
11
+
12
+ # Creates a new instance
13
+ #
14
+ # Memory file system is experimental
15
+ #
16
+ # @param memory [TrueClass,FalseClass] use in-memory, ephemeral file system
17
+ # @param adapter [Staticky::FileSystem]
18
+ #
19
+ # @return [Staticky::Files] a new files instance
20
+ def initialize(memory: false, adapter: Adapter.call(memory:))
21
+ @adapter = adapter
22
+ end
23
+
24
+ # Read file content
25
+ #
26
+ # @param path [String,Pathname] the path to file
27
+ #
28
+ # @return [String] the file contents
29
+ #
30
+ # @raise [Staticky::Files::IOError] in case of I/O error
31
+ # TODO: allow buffered read
32
+ def read(path)
33
+ adapter.read(path)
34
+ end
35
+
36
+ # Creates an empty file for the given path.
37
+ # All the intermediate directories are created.
38
+ # If the path already exists, it doesn't change the contents
39
+ #
40
+ # @param path [String,Pathname] the path to file
41
+ #
42
+ # @raise [Staticky::Files::IOError] in case of I/O error
43
+ def touch(path)
44
+ adapter.touch(path)
45
+ end
46
+
47
+ # Creates a new file or rewrites the contents
48
+ # of an existing file for the given path and content
49
+ # All the intermediate directories are created.
50
+ #
51
+ # @param path [String,Pathname] the path to file
52
+ # @param content [String, Array<String>] the content to write
53
+ #
54
+ # @raise [Staticky::Files::IOError] in case of I/O error
55
+ def write(path, *content)
56
+ adapter.write(path, *content)
57
+ end
58
+
59
+ # Sets UNIX permissions of the file at the given path.
60
+ #
61
+ # Accepts permissions in numeric mode only, best provided as octal numbers matching the
62
+ # standard UNIX octal permission modes, such as `0o544` for a file writeable by its owner and
63
+ # readable by others, or `0o755` for a file writeable by its owner and executable by everyone.
64
+ #
65
+ # @param path [String,Pathname] the path to the file
66
+ # @param mode [Integer] the UNIX permissions mode
67
+ #
68
+ # @raise [Staticky::Files::IOError] in case of I/O error
69
+ def chmod(path, mode)
70
+ unless mode.is_a?(Integer)
71
+ raise Staticky::Files::Error,
72
+ "mode should be an integer (e.g. 0o755)"
73
+ end
74
+
75
+ adapter.chmod(path, mode)
76
+ end
77
+
78
+ # Returns a new string formed by joining the strings using Operating
79
+ # System path separator
80
+ #
81
+ # @param path [Array<String,Pathname>] path tokens
82
+ #
83
+ # @return [String] the joined path
84
+ def join(*path)
85
+ adapter.join(*path)
86
+ end
87
+
88
+ # Converts a path to an absolute path.
89
+ #
90
+ # Relative paths are referenced from the current working directory of
91
+ # the process unless `dir` is given.
92
+ #
93
+ # @param path [String,Pathname] the path to the file
94
+ # @param dir [String,Pathname] the base directory
95
+ #
96
+ # @return [String] the expanded path
97
+ def expand_path(path, dir = pwd)
98
+ adapter.expand_path(path, dir)
99
+ end
100
+
101
+ # Returns the name of the current working directory.
102
+ #
103
+ # @return [String] the current working directory.
104
+ def pwd
105
+ adapter.pwd
106
+ end
107
+
108
+ # Opens (or creates) a new file for both read/write operations
109
+ #
110
+ # @param path [String] the target file
111
+ # @param mode [String,Integer] Ruby file open mode
112
+ # @param args [Array<Object>] ::File.open args
113
+ # @param blk [Proc] the block to yield
114
+ #
115
+ # @yieldparam [File,Staticky::Files::MemoryFileSystem::Node] the opened file
116
+ #
117
+ # @return [File,Staticky::Files::MemoryFileSystem::Node] the opened file
118
+ #
119
+ # @raise [Staticky::Files::IOError] in case of I/O error
120
+ def open(path, mode = OPEN_MODE, ...)
121
+ adapter.open(path, mode, ...)
122
+ end
123
+
124
+ # Temporary changes the current working directory of the process to the
125
+ # given path and yield the given block.
126
+ #
127
+ # @param path [String,Pathname] the target directory
128
+ # @param blk [Proc] the code to execute with the target directory
129
+ #
130
+ # @raise [Staticky::Files::IOError] in case of I/O error
131
+ def chdir(path, &blk)
132
+ adapter.chdir(path, &blk)
133
+ end
134
+
135
+ # Creates a directory for the given path.
136
+ # It assumes that all the tokens in `path` are meant to be a directory.
137
+ # All the intermediate directories are created.
138
+ #
139
+ # @param path [String,Pathname] the path to directory
140
+ #
141
+ # @raise [Staticky::Files::IOError] in case of I/O error
142
+ #
143
+ # @since 0.1.0
144
+ # @api public
145
+ #
146
+ # @see #mkdir_p
147
+ #
148
+ # @example
149
+ # require "staticky/files"
150
+ #
151
+ # Staticky::Files.new.mkdir("path/to/directory")
152
+ # # => creates the `path/to/directory` directory
153
+ #
154
+ # # WRONG this isn't probably what you want, check `.mkdir_p`
155
+ # Staticky::Files.new.mkdir("path/to/file.rb")
156
+ # # => creates the `path/to/file.rb` directory
157
+ def mkdir(path)
158
+ adapter.mkdir(path)
159
+ end
160
+
161
+ # Creates a directory for the given path.
162
+ # It assumes that all the tokens, but the last, in `path` are meant to be
163
+ # a directory, whereas the last is meant to be a file.
164
+ # All the intermediate directories are created.
165
+ #
166
+ # @param path [String,Pathname] the path to directory
167
+ #
168
+ # @raise [Staticky::Files::IOError] in case of I/O error
169
+ #
170
+ # @since 0.1.0
171
+ # @api public
172
+ #
173
+ # @see #mkdir
174
+ #
175
+ # @example
176
+ # require "staticky/files"
177
+ #
178
+ # Staticky::Files.new.mkdir_p("path/to/file.rb")
179
+ # # => creates the `path/to` directory, but NOT `file.rb`
180
+ #
181
+ # # WRONG it doesn't create the last directory, check `.mkdir`
182
+ # Staticky::Files.new.mkdir_p("path/to/directory")
183
+ # # => creates the `path/to` directory
184
+ def mkdir_p(path)
185
+ adapter.mkdir_p(path)
186
+ end
187
+
188
+ # Copies source into destination.
189
+ # All the intermediate directories are created.
190
+ # If the destination already exists, it overrides the contents.
191
+ #
192
+ # @param source [String,Pathname] the path to the source file
193
+ # @param destination [String,Pathname] the path to the destination file
194
+ #
195
+ # @raise [Staticky::Files::IOError] in case of I/O error
196
+ #
197
+ # @since 0.1.0
198
+ # @api public
199
+ def cp(source, destination)
200
+ adapter.cp(source, destination)
201
+ end
202
+
203
+ # Deletes given path (file).
204
+ #
205
+ # @param path [String,Pathname] the path to file
206
+ #
207
+ # @raise [Staticky::Files::IOError] in case of I/O error
208
+ #
209
+ # @since 0.1.0
210
+ # @api public
211
+ def delete(path)
212
+ adapter.rm(path)
213
+ end
214
+
215
+ # Deletes given path (directory).
216
+ #
217
+ # @param path [String,Pathname] the path to file
218
+ #
219
+ # @raise [Staticky::Files::IOError] in case of I/O error
220
+ #
221
+ # @since 0.1.0
222
+ # @api public
223
+ def delete_directory(path)
224
+ adapter.rm_rf(path)
225
+ end
226
+
227
+ # Checks if `path` exist
228
+ #
229
+ # @param path [String,Pathname] the path to file
230
+ #
231
+ # @return [TrueClass,FalseClass] the result of the check
232
+ #
233
+ # @since 0.1.0
234
+ # @api public
235
+ #
236
+ # @example
237
+ # require "staticky/files"
238
+ #
239
+ # Staticky::Files.new.exist?(__FILE__) # => true
240
+ # Staticky::Files.new.exist?(__dir__) # => true
241
+ #
242
+ # Staticky::Files.new.exist?("missing_file") # => false
243
+ def exist?(path)
244
+ adapter.exist?(path)
245
+ end
246
+
247
+ # Checks if `path` is a directory
248
+ #
249
+ # @param path [String,Pathname] the path to directory
250
+ #
251
+ # @return [TrueClass,FalseClass] the result of the check
252
+ #
253
+ # @since 0.1.0
254
+ # @api public
255
+ #
256
+ # @example
257
+ # require "staticky/files"
258
+ #
259
+ # Staticky::Files.new.directory?(__dir__) # => true
260
+ # Staticky::Files.new.directory?(__FILE__) # => false
261
+ #
262
+ # Staticky::Files.new.directory?("missing_directory") # => false
263
+ def directory?(path)
264
+ adapter.directory?(path)
265
+ end
266
+
267
+ # Checks if `path` is an executable
268
+ #
269
+ # @param path [String,Pathname] the path to file
270
+ #
271
+ # @return [TrueClass,FalseClass] the result of the check
272
+ #
273
+ # @since 0.1.0
274
+ # @api public
275
+ #
276
+ # @example
277
+ # require "staticky/files"
278
+ #
279
+ # Staticky::Files.new.executable?("/path/to/ruby") # => true
280
+ # Staticky::Files.new.executable?(__FILE__) # => false
281
+ #
282
+ # Staticky::Files.new.directory?("missing_file") # => false
283
+ def executable?(path)
284
+ adapter.executable?(path)
285
+ end
286
+
287
+ # Adds a new line at the top of the file
288
+ #
289
+ # @param path [String,Pathname] the path to file
290
+ # @param line [String] the line to add
291
+ #
292
+ # @raise [Staticky::Files::IOError] in case of I/O error
293
+ #
294
+ # @see #append
295
+ #
296
+ # @since 0.1.0
297
+ # @api public
298
+ def unshift(path, line)
299
+ content = adapter.readlines(path)
300
+ content.unshift(newline(line))
301
+
302
+ write(path, content)
303
+ end
304
+
305
+ # Adds a new line at the bottom of the file
306
+ #
307
+ # @param path [String,Pathname] the path to file
308
+ # @param contents [String] the contents to add
309
+ #
310
+ # @raise [Staticky::Files::IOError] in case of I/O error
311
+ #
312
+ # @see #unshift
313
+ #
314
+ # @since 0.1.0
315
+ # @api public
316
+ def append(path, contents)
317
+ mkdir_p(path)
318
+ touch(path)
319
+
320
+ content = adapter.readlines(path)
321
+ content << newline unless newline?(content.last)
322
+ content << newline(contents)
323
+
324
+ write(path, content)
325
+ end
326
+
327
+ # Replace first line in `path` that contains `target` with `replacement`.
328
+ #
329
+ # @param path [String,Pathname] the path to file
330
+ # @param target [String,Regexp] the target to replace
331
+ # @param replacement [String] the replacement
332
+ #
333
+ # @raise [Staticky::Files::IOError] in case of I/O error
334
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
335
+ #
336
+ # @see #replace_last_line
337
+ #
338
+ # @since 0.1.0
339
+ # @api public
340
+ def replace_first_line(path, target, replacement)
341
+ content = adapter.readlines(path)
342
+ content[index(content, path, target)] = newline(replacement)
343
+
344
+ write(path, content)
345
+ end
346
+
347
+ # Replace last line in `path` that contains `target` with `replacement`.
348
+ #
349
+ # @param path [String,Pathname] the path to file
350
+ # @param target [String,Regexp] the target to replace
351
+ # @param replacement [String] the replacement
352
+ #
353
+ # @raise [Staticky::Files::IOError] in case of I/O error
354
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
355
+ #
356
+ # @see #replace_first_line
357
+ #
358
+ # @since 0.1.0
359
+ # @api public
360
+ def replace_last_line(path, target, replacement)
361
+ content = adapter.readlines(path)
362
+ content[-index(content.reverse, path, target) - CONTENT_OFFSET] =
363
+ newline(replacement)
364
+
365
+ write(path, content)
366
+ end
367
+
368
+ # Inject `contents` in `path` before `target`.
369
+ #
370
+ # @param path [String,Pathname] the path to file
371
+ # @param target [String,Regexp] the target to replace
372
+ # @param contents [String] the contents to inject
373
+ #
374
+ # @raise [Staticky::Files::IOError] in case of I/O error
375
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
376
+ #
377
+ # @see #inject_line_after
378
+ # @see #inject_line_before_last
379
+ # @see #inject_line_after_last
380
+ #
381
+ # @since 0.1.0
382
+ # @api public
383
+ def inject_line_before(path, target, contents)
384
+ _inject_line_before(path, target, contents, method(:index))
385
+ end
386
+
387
+ # Inject `contents` in `path` after last `target`.
388
+ #
389
+ # @param path [String,Pathname] the path to file
390
+ # @param target [String,Regexp] the target to replace
391
+ # @param contents [String] the contents to inject
392
+ #
393
+ # @raise [Staticky::Files::IOError] in case of I/O error
394
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
395
+ #
396
+ # @see #inject_line_before
397
+ # @see #inject_line_after
398
+ # @see #inject_line_after_last
399
+ #
400
+ # @since 0.1.0
401
+ # @api public
402
+ def inject_line_before_last(path, target, contents)
403
+ _inject_line_before(path, target, contents, method(:rindex))
404
+ end
405
+
406
+ # Inject `contents` in `path` after `target`.
407
+ #
408
+ # @param path [String,Pathname] the path to file
409
+ # @param target [String,Regexp] the target to replace
410
+ # @param contents [String] the contents to inject
411
+ #
412
+ # @raise [Staticky::Files::IOError] in case of I/O error
413
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
414
+ #
415
+ # @see #inject_line_before
416
+ # @see #inject_line_before_last
417
+ # @see #inject_line_after_last
418
+ #
419
+ # @since 0.1.0
420
+ # @api public
421
+ def inject_line_after(path, target, contents)
422
+ _inject_line_after(path, target, contents, method(:index))
423
+ end
424
+
425
+ # Inject `contents` in `path` after last `target`.
426
+ #
427
+ # @param path [String,Pathname] the path to file
428
+ # @param target [String,Regexp] the target to replace
429
+ # @param contents [String] the contents to inject
430
+ #
431
+ # @raise [Staticky::Files::IOError] in case of I/O error
432
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
433
+ #
434
+ # @see #inject_line_before
435
+ # @see #inject_line_after
436
+ # @see #inject_line_before_last
437
+ #
438
+ # @since 0.1.0
439
+ # @api public
440
+ def inject_line_after_last(path, target, contents)
441
+ _inject_line_after(path, target, contents, method(:rindex))
442
+ end
443
+
444
+ # Inject `contents` in `path` within the first Ruby block that matches `target`.
445
+ # The given `contents` will appear at the TOP of the Ruby block.
446
+ #
447
+ # @param path [String,Pathname] the path to file
448
+ # @param target [String,Regexp] the target matcher for Ruby block
449
+ # @param contents [String,Array<String>] the contents to inject
450
+ #
451
+ # @raise [Staticky::Files::IOError] in case of I/O error
452
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
453
+ #
454
+ # @since 0.1.0
455
+ # @api public
456
+ #
457
+ # @example Inject a single line
458
+ # require "staticky/files"
459
+ #
460
+ # files = Staticky::Files.new
461
+ # path = "config/application.rb"
462
+ #
463
+ # File.read(path)
464
+ # # # frozen_string_literal: true
465
+ # #
466
+ # # class Application
467
+ # # configure do
468
+ # # root __dir__
469
+ # # end
470
+ # # end
471
+ #
472
+ # # inject a single line
473
+ # files.inject_line_at_block_top(path, /configure/, %(load_path.unshift("lib")))
474
+ #
475
+ # File.read(path)
476
+ # # # frozen_string_literal: true
477
+ # #
478
+ # # class Application
479
+ # # configure do
480
+ # # load_path.unshift("lib")
481
+ # # root __dir__
482
+ # # end
483
+ # # end
484
+ #
485
+ # @example Inject multiple lines
486
+ # require "staticky/files"
487
+ #
488
+ # files = Staticky::Files.new
489
+ # path = "config/application.rb"
490
+ #
491
+ # File.read(path)
492
+ # # # frozen_string_literal: true
493
+ # #
494
+ # # class Application
495
+ # # configure do
496
+ # # root __dir__
497
+ # # end
498
+ # # end
499
+ #
500
+ # # inject multiple lines
501
+ # files.inject_line_at_block_top(path,
502
+ # /configure/,
503
+ # [%(load_path.unshift("lib")), "settings.load!"])
504
+ #
505
+ # File.read(path)
506
+ # # # frozen_string_literal: true
507
+ # #
508
+ # # class Application
509
+ # # configure do
510
+ # # load_path.unshift("lib")
511
+ # # settings.load!
512
+ # # root __dir__
513
+ # # end
514
+ # # end
515
+ #
516
+ # @example Inject a block
517
+ # require "staticky/files"
518
+ #
519
+ # files = Staticky::Files.new
520
+ # path = "config/application.rb"
521
+ #
522
+ # File.read(path)
523
+ # # # frozen_string_literal: true
524
+ # #
525
+ # # class Application
526
+ # # configure do
527
+ # # root __dir__
528
+ # # end
529
+ # # end
530
+ #
531
+ # # inject a block
532
+ # block = <<~BLOCK
533
+ # settings do
534
+ # load!
535
+ # end
536
+ # BLOCK
537
+ # files.inject_line_at_block_top(path, /configure/, block)
538
+ #
539
+ # File.read(path)
540
+ # # # frozen_string_literal: true
541
+ # #
542
+ # # class Application
543
+ # # configure do
544
+ # # settings do
545
+ # # load!
546
+ # # end
547
+ # # root __dir__
548
+ # # end
549
+ # # end
550
+ def inject_line_at_block_top(path, target, *contents)
551
+ content = adapter.readlines(path)
552
+ starting = index(content, path, target)
553
+ offset = SPACE * (content[starting][SPACE_MATCHER].bytesize + INDENTATION)
554
+
555
+ contents = Array(contents).flatten
556
+ contents = _offset_block_lines(contents, offset)
557
+
558
+ content.insert(starting + CONTENT_OFFSET, contents)
559
+ write(path, content)
560
+ end
561
+
562
+ # Inject `contents` in `path` within the first Ruby block that matches `target`.
563
+ # The given `contents` will appear at the BOTTOM of the Ruby block.
564
+ #
565
+ # @param path [String,Pathname] the path to file
566
+ # @param target [String,Regexp] the target matcher for Ruby block
567
+ # @param contents [String,Array<String>] the contents to inject
568
+ #
569
+ # @raise [Staticky::Files::IOError] in case of I/O error
570
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
571
+ #
572
+ # @since 0.1.0
573
+ # @api public
574
+ #
575
+ # @example Inject a single line
576
+ # require "staticky/files"
577
+ #
578
+ # files = Staticky::Files.new
579
+ # path = "config/application.rb"
580
+ #
581
+ # File.read(path)
582
+ # # # frozen_string_literal: true
583
+ # #
584
+ # # class Application
585
+ # # configure do
586
+ # # root __dir__
587
+ # # end
588
+ # # end
589
+ #
590
+ # # inject a single line
591
+ # files.inject_line_at_block_bottom(path, /configure/, %(load_path.unshift("lib")))
592
+ #
593
+ # File.read(path)
594
+ # # # frozen_string_literal: true
595
+ # #
596
+ # # class Application
597
+ # # configure do
598
+ # # root __dir__
599
+ # # load_path.unshift("lib")
600
+ # # end
601
+ # # end
602
+ #
603
+ # @example Inject multiple lines
604
+ # require "staticky/files"
605
+ #
606
+ # files = Staticky::Files.new
607
+ # path = "config/application.rb"
608
+ #
609
+ # File.read(path)
610
+ # # # frozen_string_literal: true
611
+ # #
612
+ # # class Application
613
+ # # configure do
614
+ # # root __dir__
615
+ # # end
616
+ # # end
617
+ #
618
+ # # inject multiple lines
619
+ # files.inject_line_at_block_bottom(path,
620
+ # /configure/,
621
+ # [%(load_path.unshift("lib")), "settings.load!"])
622
+ #
623
+ # File.read(path)
624
+ # # # frozen_string_literal: true
625
+ # #
626
+ # # class Application
627
+ # # configure do
628
+ # # root __dir__
629
+ # # load_path.unshift("lib")
630
+ # # settings.load!
631
+ # # end
632
+ # # end
633
+ #
634
+ # @example Inject a block
635
+ # require "staticky/files"
636
+ #
637
+ # files = Staticky::Files.new
638
+ # path = "config/application.rb"
639
+ #
640
+ # File.read(path)
641
+ # # # frozen_string_literal: true
642
+ # #
643
+ # # class Application
644
+ # # configure do
645
+ # # root __dir__
646
+ # # end
647
+ # # end
648
+ #
649
+ # # inject a block
650
+ # block = <<~BLOCK
651
+ # settings do
652
+ # load!
653
+ # end
654
+ # BLOCK
655
+ # files.inject_line_at_block_bottom(path, /configure/, block)
656
+ #
657
+ # File.read(path)
658
+ # # # frozen_string_literal: true
659
+ # #
660
+ # # class Application
661
+ # # configure do
662
+ # # root __dir__
663
+ # # settings do
664
+ # # load!
665
+ # # end
666
+ # # end
667
+ # # end
668
+ def inject_line_at_block_bottom(path, target, *contents)
669
+ content = adapter.readlines(path)
670
+ starting = index(content, path, target)
671
+ line = content[starting]
672
+ delimiter = if line.match?(INLINE_OPEN_BLOCK_MATCHER)
673
+ INLINE_BLOCK_DELIMITER
674
+ else
675
+ BLOCK_DELIMITER
676
+ end
677
+ target = content[starting..]
678
+ ending = closing_block_index(target, starting, path, line, delimiter)
679
+ offset = SPACE * (content[ending][SPACE_MATCHER].bytesize + INDENTATION)
680
+
681
+ contents = Array(contents).flatten
682
+ contents = _offset_block_lines(contents, offset)
683
+
684
+ content.insert(ending, contents)
685
+ write(path, content)
686
+ end
687
+
688
+ # Inject `contents` in `path` at the bottom of the Ruby class that matches `target`.
689
+ # The given `contents` will appear at the BOTTOM of the Ruby class.
690
+ #
691
+ # @param path [String,Pathname] the path to file
692
+ # @param target [String,Regexp] the target matcher for Ruby class
693
+ # @param contents [String,Array<String>] the contents to inject
694
+ #
695
+ # @raise [Staticky::Files::IOError] in case of I/O error
696
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
697
+ #
698
+ # @since 0.4.0
699
+ # @api public
700
+ #
701
+ # @example Inject a single line
702
+ # require "staticky/files"
703
+ #
704
+ # files = Staticky::Files.new
705
+ # path = "config/application.rb"
706
+ #
707
+ # File.read(path)
708
+ # # # frozen_string_literal: true
709
+ # #
710
+ # # class Application
711
+ # # end
712
+ #
713
+ # # inject a single line
714
+ # files.inject_line_at_class_bottom(path, /Application/, %(attr_accessor :name))
715
+ #
716
+ # File.read(path)
717
+ # # # frozen_string_literal: true
718
+ # #
719
+ # # class Application
720
+ # # attr_accessor :name
721
+ # # end
722
+ #
723
+ # @example Inject multiple lines
724
+ # require "staticky/files"
725
+ #
726
+ # files = Staticky::Files.new
727
+ # path = "math.rb"
728
+ #
729
+ # File.read(path)
730
+ # # # frozen_string_literal: true
731
+ # #
732
+ # # class Math
733
+ # # end
734
+ #
735
+ # # inject multiple lines
736
+ # files.inject_line_at_class_bottom(path,
737
+ # /Math/,
738
+ # ["def sum(a, b)", " a + b", "end"])
739
+ #
740
+ # File.read(path)
741
+ # # # frozen_string_literal: true
742
+ # #
743
+ # # class Math
744
+ # # def sum(a, b)
745
+ # # a + b
746
+ # # end
747
+ # # end
748
+ def inject_line_at_class_bottom(path, target, *contents)
749
+ content = adapter.readlines(path)
750
+ starting = index(content, path, target)
751
+ line = content[starting]
752
+ target = content[starting..]
753
+ ending = closing_class_index(
754
+ target,
755
+ starting,
756
+ path,
757
+ line,
758
+ BLOCK_DELIMITER
759
+ )
760
+ offset = SPACE * (content[ending][SPACE_MATCHER].bytesize + INDENTATION)
761
+
762
+ contents = Array(contents).flatten
763
+ contents = _offset_block_lines(contents, offset)
764
+
765
+ content.insert(ending, contents)
766
+ write(path, content)
767
+ end
768
+
769
+ # Removes line from `path`, matching `target`.
770
+ #
771
+ # @param path [String,Pathname] the path to file
772
+ # @param target [String,Regexp] the target to remove
773
+ #
774
+ # @raise [Staticky::Files::IOError] in case of I/O error
775
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
776
+ #
777
+ # @since 0.1.0
778
+ # @api public
779
+ def remove_line(path, target)
780
+ content = adapter.readlines(path)
781
+ i = index(content, path, target)
782
+
783
+ content.delete_at(i)
784
+ write(path, content)
785
+ end
786
+
787
+ # Removes `target` block from `path`
788
+ #
789
+ # @param path [String,Pathname] the path to file
790
+ # @param target [String] the target block to remove
791
+ #
792
+ # @raise [Staticky::Files::IOError] in case of I/O error
793
+ # @raise [Staticky::Files::MissingTargetError] if `target` cannot be found in `path`
794
+ #
795
+ # @since 0.1.0
796
+ # @api public
797
+ #
798
+ # @example
799
+ # require "staticky/files"
800
+ #
801
+ # puts File.read("app.rb")
802
+ #
803
+ # # class App
804
+ # # configure do
805
+ # # root __dir__
806
+ # # end
807
+ # # end
808
+ #
809
+ # Staticky::Files.new.remove_block("app.rb", "configure")
810
+ #
811
+ # puts File.read("app.rb")
812
+ #
813
+ # # class App
814
+ # # end
815
+ def remove_block(path, target)
816
+ content = adapter.readlines(path)
817
+ starting = index(content, path, target)
818
+ line = content[starting]
819
+ size = line[SPACE_MATCHER].bytesize
820
+ closing = (SPACE * size) +
821
+ (target.match?(INLINE_OPEN_BLOCK_MATCHER) ? INLINE_CLOSE_BLOCK : CLOSE_BLOCK)
822
+ ending = starting + index(
823
+ content[starting..-CONTENT_OFFSET],
824
+ path,
825
+ closing
826
+ )
827
+
828
+ content.slice!(starting..ending)
829
+ write(path, content)
830
+
831
+ remove_block(path, target) if match?(content, target)
832
+ end
833
+
834
+ # Reads entries from a directory
835
+ #
836
+ # @param path [String,Pathname] the path to file
837
+ #
838
+ # @raise [Staticky::Files::IOError] in case of I/O error
839
+ #
840
+ # @since 1.0.1
841
+ # @api public
842
+ def entries(path)
843
+ adapter.entries(path)
844
+ end
845
+
846
+ private
847
+
848
+ class Delimiter
849
+ SPACE_MATCHER_GENERAL = /[[:space:]]*/
850
+
851
+ attr_reader :opening, :closing
852
+
853
+ def initialize(name, opening, closing)
854
+ @name = name
855
+ @opening = opening
856
+ @closing = closing
857
+ freeze
858
+ end
859
+
860
+ def opening_matcher
861
+ matcher(opening)
862
+ end
863
+
864
+ def closing_matcher
865
+ matcher(closing)
866
+ end
867
+
868
+ private
869
+
870
+ def matcher(delimiter)
871
+ /#{SPACE_MATCHER_GENERAL}\b#{delimiter}\b(?:#{SPACE_MATCHER_GENERAL}|#{NEW_LINE_MATCHER})/
872
+ end
873
+ end
874
+
875
+ NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
876
+ NEW_LINE_MATCHER = /#{NEW_LINE}\z/
877
+ EMPTY_LINE = /\A\z/
878
+ CONTENT_OFFSET = 1
879
+ SPACE = " "
880
+ INDENTATION = 2
881
+ SPACE_MATCHER = /\A[[:space:]]*/
882
+ INLINE_OPEN_BLOCK = "{"
883
+ INLINE_CLOSE_BLOCK = "}"
884
+ OPEN_BLOCK = "do"
885
+ CLOSE_BLOCK = "end"
886
+ INLINE_OPEN_BLOCK_MATCHER = INLINE_CLOSE_BLOCK
887
+ INLINE_BLOCK_DELIMITER = Delimiter.new(
888
+ "InlineBlockDelimiter",
889
+ INLINE_OPEN_BLOCK,
890
+ INLINE_CLOSE_BLOCK
891
+ )
892
+ BLOCK_DELIMITER = Delimiter.new("BlockDelimiter", OPEN_BLOCK, CLOSE_BLOCK)
893
+
894
+ attr_reader :adapter
895
+
896
+ def newline(line = nil)
897
+ return line if line.to_s.end_with?(NEW_LINE)
898
+
899
+ "#{line}#{NEW_LINE}"
900
+ end
901
+
902
+ def newline?(content)
903
+ content&.end_with?(NEW_LINE)
904
+ end
905
+
906
+ def match?(content, target)
907
+ !line_number(content, target).nil?
908
+ end
909
+
910
+ def index(content, path, target)
911
+ line_number(content, target) or
912
+ raise MissingTargetError.new(target, path)
913
+ end
914
+
915
+ def rindex(content, path, target)
916
+ line_number(content, target, finder: content.method(:rindex)) or
917
+ raise MissingTargetError.new(target, path)
918
+ end
919
+
920
+ def closing_block_index(
921
+ content,
922
+ starting,
923
+ path,
924
+ target,
925
+ delimiter,
926
+ count_offset = 0
927
+ )
928
+ blocks_count = content.count { |line|
929
+ line.match?(delimiter.opening_matcher)
930
+ } + count_offset
931
+ matching_line = content.find do |line|
932
+ blocks_count -= 1 if line.match?(delimiter.closing_matcher)
933
+ line if blocks_count.zero?
934
+ end
935
+
936
+ (content.index(matching_line) or
937
+ raise MissingTargetError.new(target, path)) + starting
938
+ end
939
+
940
+ def closing_class_index(content, starting, path, target, delimiter)
941
+ closing_block_index(content, starting, path, target, delimiter, 1)
942
+ end
943
+
944
+ def _inject_line_before(path, target, contents, finder)
945
+ content = adapter.readlines(path)
946
+ i = finder.call(content, path, target)
947
+
948
+ content.insert(i, newline(contents))
949
+ write(path, content)
950
+ end
951
+
952
+ def _inject_line_after(path, target, contents, finder)
953
+ content = adapter.readlines(path)
954
+ i = finder.call(content, path, target)
955
+
956
+ content.insert(i + CONTENT_OFFSET, newline(contents))
957
+ write(path, content)
958
+ end
959
+
960
+ def _offset_block_lines(contents, offset)
961
+ contents.map do |line|
962
+ if line.match?(NEW_LINE)
963
+ line = line.split(NEW_LINE)
964
+ _offset_block_lines(line, offset)
965
+ elsif line.match?(EMPTY_LINE)
966
+ line + NEW_LINE
967
+ else
968
+ offset + line + NEW_LINE
969
+ end
970
+ end.join
971
+ end
972
+
973
+ def line_number(content, target, finder: content.method(:index))
974
+ finder.call do |l|
975
+ case target
976
+ when ::String
977
+ l.include?(target)
978
+ when Regexp
979
+ l =~ target
980
+ end
981
+ end
982
+ end
983
+ end
984
+ end