staticky-files 0.1.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,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