dry-files 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4d22584a76b85c83b8d1d50e0b805335347f205f7ac7da1ea31dd538a5b459fd
4
+ data.tar.gz: 4054a4f0bf0daa95cc3e23937d5acb9726606cf7974159146cd21787a186549b
5
+ SHA512:
6
+ metadata.gz: d7881004a31c1106fb0a42b11015169fe1deac588aa1609ab3dd13c28c945f83cea7b722fb43250de665d862ec27f8f76071f88eac3b1567b2ea95a201cb8a4d
7
+ data.tar.gz: 3d2c8d0f875b00e889d0020a5023443b686529548bf1b28ca5007085bd88d4581affc6ad1a2d5319f861dfc768cef8e486f45959204478f05670121a239db124
data/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
+
3
+ ## 0.1.0 2021-05-04
4
+
5
+ Initial release
6
+
7
+ ### Added
8
+
9
+ - Introduced `Dry::Files`
10
+ - Introduced `Dry::Files#initialize` which accepts an optional `memory: true/false` argument to use the in-memory adapter (@jodosha)
11
+ - Introduced `Dry::Files#read` to read the file all at once (@jodosha)
12
+ - Introduced `Dry::Files#touch` to touch a file and create all the intermediate directories, if needed (@jodosha)
13
+ - Introduced `Dry::Files#write` to write/replace a file and create all the intermediate directories, if needed (@jodosha)
14
+ - Introduced `Dry::Files#join` to join the given path tokens (@jodosha)
15
+ - Introduced `Dry::Files#expand_path` to make the relative path absolute, starting from the current directory of from a custom one (@jodosha)
16
+ - Introduced `Dry::Files#pwd` to return the current directory (@jodosha)
17
+ - Introduced `Dry::Files#chdir` to temporary change the current directory (@jodosha)
18
+ - Introduced `Dry::Files#mkdir` to create intermediate directories for the given directory name (@jodosha)
19
+ - Introduced `Dry::Files#mkdir_p` to create intermediate directories for the given file name (@jodosha)
20
+ - Introduced `Dry::Files#cp` to copy source file into destination and create intermediate destination directories, if needed (@jodosha)
21
+ - Introduced `Dry::Files#delete` to delete a file (@jodosha)
22
+ - Introduced `Dry::Files#delete_directory` to delete a directory (@jodosha)
23
+ - Introduced `Dry::Files#exist?` to check if a path exists (@jodosha)
24
+ - Introduced `Dry::Files#directory?` to check if a path is a directory (@jodosha)
25
+ - Introduced `Dry::Files#executable?` to check if a path is an executable (@jodosha)
26
+ - Introduced `Dry::Files#unshift` to add a new line at the top of the file (@jodosha)
27
+ - Introduced `Dry::Files#append` to add a new line at the bottom of the file (@jodosha)
28
+ - Introduced `Dry::Files#replace_first_line` to replace first line that match target (@jodosha)
29
+ - Introduced `Dry::Files#replace_last_line` to replace last line that match target (@jodosha)
30
+ - Introduced `Dry::Files#inject_line_before` to inject content before the first match of target (@jodosha)
31
+ - Introduced `Dry::Files#inject_line_before_last` to inject content before the last match of target (@jodosha)
32
+ - Introduced `Dry::Files#inject_line_after` to inject content after the first match of target (@jodosha)
33
+ - Introduced `Dry::Files#inject_line_after_last` to inject content after the last match of target (@jodosha)
34
+ - Introduced `Dry::Files#inject_line_at_block_top` to inject content as the first line of the matching code block (@jodosha)
35
+ - Introduced `Dry::Files#inject_line_at_block_bottom` to inject content as the last line of the matching code block (@jodosha)
36
+ - Introduced `Dry::Files#remove_line` to remove the first matching line (@jodosha)
37
+ - Introduced `Dry::Files#remove_block` remove the first matching block (@jodosha)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2021 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ [gem]: https://rubygems.org/gems/dry-files
2
+ [actions]: https://github.com/dry-rb/dry-files/actions
3
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-files
4
+ [chat]: https://dry-rb.zulipchat.com
5
+ [inchpages]: http://inch-ci.org/github/dry-rb/dry-files
6
+
7
+ # dry-files [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/dry-files.svg)][gem]
10
+ [![CI Status](https://github.com/dry-rb/dry-files/workflows/ci/badge.svg)][actions]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/71200ee8d70b412c9e21c20b8b3b3688)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/71200ee8d70b412c9e21c20b8b3b3688)][codacy]
13
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-files.svg?branch=master)][inchpages]
14
+
15
+ ## Links
16
+
17
+ * [User documentation](http://dry-rb.org/gems/dry-files)
18
+ * [API documentation](http://rubydoc.info/gems/dry-files)
19
+
20
+ ## Supported Ruby versions
21
+
22
+ This library officially supports the following Ruby versions:
23
+
24
+ * MRI >= `2.5`
25
+ * jruby >= `9.2`
26
+
27
+ ## License
28
+
29
+ See `LICENSE` file.
data/dry-files.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ # this file is managed by dry-rb/devtools project
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'dry/files/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'dry-files'
10
+ spec.authors = ["Luca Guidi"]
11
+ spec.email = ["me@lucaguidi.com"]
12
+ spec.license = 'MIT'
13
+ spec.version = Dry::Files::VERSION.dup
14
+
15
+ spec.summary = "file utilities"
16
+ spec.description = spec.summary
17
+ spec.homepage = 'https://dry-rb.org/gems/dry-files'
18
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-files.gemspec", "lib/**/*"]
19
+ spec.bindir = 'bin'
20
+ spec.executables = []
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-files/blob/master/CHANGELOG.md'
25
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-files'
26
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-files/issues'
27
+
28
+ spec.required_ruby_version = ">= 2.5.0"
29
+
30
+ # to update dependencies edit project.yml
31
+ spec.add_development_dependency "rake", "~> 13.0"
32
+ spec.add_development_dependency "rspec", "~> 3.10"
33
+ spec.add_development_dependency "rubocop", "~> 1.12"
34
+ end
data/lib/dry-files.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/files"
data/lib/dry/files.rb ADDED
@@ -0,0 +1,860 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dry-rb is a collection of next-generation Ruby libraries
4
+ #
5
+ # @api public
6
+ # @since 0.1.0
7
+ module Dry
8
+ # File manipulations
9
+ #
10
+ # @since 0.1.0
11
+ # @api public
12
+ class Files
13
+ require_relative "files/version"
14
+ require_relative "files/error"
15
+ require_relative "files/adapter"
16
+
17
+ # Creates a new instance
18
+ #
19
+ # Memory file system is experimental
20
+ #
21
+ # @param memory [TrueClass,FalseClass] use in-memory, ephemeral file system
22
+ # @param adapter [Dry::FileSystem]
23
+ #
24
+ # @return [Dry::Files] a new files instance
25
+ #
26
+ # @since 0.1.0
27
+ # @api public
28
+ def initialize(memory: false, adapter: Adapter.call(memory: memory))
29
+ @adapter = adapter
30
+ end
31
+
32
+ # Read file content
33
+ #
34
+ # @param path [String,Pathname] the path to file
35
+ #
36
+ # @return [String] the file contents
37
+ #
38
+ # @raise [Dry::Files::IOError] in case of I/O error
39
+ #
40
+ # @since 0.1.0
41
+ # @api public
42
+ #
43
+ # TODO: allow buffered read
44
+ def read(path)
45
+ adapter.read(path)
46
+ end
47
+
48
+ # Creates an empty file for the given path.
49
+ # All the intermediate directories are created.
50
+ # If the path already exists, it doesn't change the contents
51
+ #
52
+ # @param path [String,Pathname] the path to file
53
+ #
54
+ # @raise [Dry::Files::IOError] in case of I/O error
55
+ #
56
+ # @since 0.1.0
57
+ # @api public
58
+ def touch(path)
59
+ adapter.touch(path)
60
+ end
61
+
62
+ # Creates a new file or rewrites the contents
63
+ # of an existing file for the given path and content
64
+ # All the intermediate directories are created.
65
+ #
66
+ # @param path [String,Pathname] the path to file
67
+ # @param content [String, Array<String>] the content to write
68
+ #
69
+ # @raise [Dry::Files::IOError] in case of I/O error
70
+ #
71
+ # @since 0.1.0
72
+ # @api public
73
+ def write(path, *content)
74
+ adapter.write(path, *content)
75
+ end
76
+
77
+ # Returns a new string formed by joining the strings using Operating
78
+ # System path separator
79
+ #
80
+ # @param path [Array<String,Pathname>] path tokens
81
+ #
82
+ # @return [String] the joined path
83
+ #
84
+ # @since 0.1.0
85
+ # @api public
86
+ def join(*path)
87
+ adapter.join(*path)
88
+ end
89
+
90
+ # Converts a path to an absolute path.
91
+ #
92
+ # Relative paths are referenced from the current working directory of
93
+ # the process unless `dir` is given.
94
+ #
95
+ # @param path [String,Pathname] the path to the file
96
+ # @param dir [String,Pathname] the base directory
97
+ #
98
+ # @return [String] the expanded path
99
+ #
100
+ # @since 0.1.0
101
+ def expand_path(path, dir = pwd)
102
+ adapter.expand_path(path, dir)
103
+ end
104
+
105
+ # Returns the name of the current working directory.
106
+ #
107
+ # @return [String] the current working directory.
108
+ #
109
+ # @since 0.1.0
110
+ def pwd
111
+ adapter.pwd
112
+ end
113
+
114
+ # Temporary changes the current working directory of the process to the
115
+ # given path and yield the given block.
116
+ #
117
+ # @param path [String,Pathname] the target directory
118
+ # @param blk [Proc] the code to execute with the target directory
119
+ #
120
+ # @raise [Dry::Files::IOError] in case of I/O error
121
+ #
122
+ # @since 0.1.0
123
+ def chdir(path, &blk)
124
+ adapter.chdir(path, &blk)
125
+ end
126
+
127
+ # Creates a directory for the given path.
128
+ # It assumes that all the tokens in `path` are meant to be a directory.
129
+ # All the intermediate directories are created.
130
+ #
131
+ # @param path [String,Pathname] the path to directory
132
+ #
133
+ # @raise [Dry::Files::IOError] in case of I/O error
134
+ #
135
+ # @since 0.1.0
136
+ # @api public
137
+ #
138
+ # @see #mkdir_p
139
+ #
140
+ # @example
141
+ # require "dry/files"
142
+ #
143
+ # Dry::Files.new.mkdir("path/to/directory")
144
+ # # => creates the `path/to/directory` directory
145
+ #
146
+ # # WRONG this isn't probably what you want, check `.mkdir_p`
147
+ # Dry::Files.new.mkdir("path/to/file.rb")
148
+ # # => creates the `path/to/file.rb` directory
149
+ def mkdir(path)
150
+ adapter.mkdir(path)
151
+ end
152
+
153
+ # Creates a directory for the given path.
154
+ # It assumes that all the tokens, but the last, in `path` are meant to be
155
+ # a directory, whereas the last is meant to be a file.
156
+ # All the intermediate directories are created.
157
+ #
158
+ # @param path [String,Pathname] the path to directory
159
+ #
160
+ # @raise [Dry::Files::IOError] in case of I/O error
161
+ #
162
+ # @since 0.1.0
163
+ # @api public
164
+ #
165
+ # @see #mkdir
166
+ #
167
+ # @example
168
+ # require "dry/files"
169
+ #
170
+ # Dry::Files.new.mkdir_p("path/to/file.rb")
171
+ # # => creates the `path/to` directory, but NOT `file.rb`
172
+ #
173
+ # # WRONG it doesn't create the last directory, check `.mkdir`
174
+ # Dry::Files.new.mkdir_p("path/to/directory")
175
+ # # => creates the `path/to` directory
176
+ def mkdir_p(path)
177
+ adapter.mkdir_p(path)
178
+ end
179
+
180
+ # Copies source into destination.
181
+ # All the intermediate directories are created.
182
+ # If the destination already exists, it overrides the contents.
183
+ #
184
+ # @param source [String,Pathname] the path to the source file
185
+ # @param destination [String,Pathname] the path to the destination file
186
+ #
187
+ # @raise [Dry::Files::IOError] in case of I/O error
188
+ #
189
+ # @since 0.1.0
190
+ # @api public
191
+ def cp(source, destination)
192
+ adapter.cp(source, destination)
193
+ end
194
+
195
+ # Deletes given path (file).
196
+ #
197
+ # @param path [String,Pathname] the path to file
198
+ #
199
+ # @raise [Dry::Files::IOError] in case of I/O error
200
+ #
201
+ # @since 0.1.0
202
+ # @api public
203
+ def delete(path)
204
+ adapter.rm(path)
205
+ end
206
+
207
+ # Deletes given path (directory).
208
+ #
209
+ # @param path [String,Pathname] the path to file
210
+ #
211
+ # @raise [Dry::Files::IOError] in case of I/O error
212
+ #
213
+ # @since 0.1.0
214
+ # @api public
215
+ def delete_directory(path)
216
+ adapter.rm_rf(path)
217
+ end
218
+
219
+ # Checks if `path` exist
220
+ #
221
+ # @param path [String,Pathname] the path to file
222
+ #
223
+ # @return [TrueClass,FalseClass] the result of the check
224
+ #
225
+ # @since 0.1.0
226
+ # @api public
227
+ #
228
+ # @example
229
+ # require "dry/files"
230
+ #
231
+ # Dry::Files.new.exist?(__FILE__) # => true
232
+ # Dry::Files.new.exist?(__dir__) # => true
233
+ #
234
+ # Dry::Files.new.exist?("missing_file") # => false
235
+ def exist?(path)
236
+ adapter.exist?(path)
237
+ end
238
+
239
+ # Checks if `path` is a directory
240
+ #
241
+ # @param path [String,Pathname] the path to directory
242
+ #
243
+ # @return [TrueClass,FalseClass] the result of the check
244
+ #
245
+ # @since 0.1.0
246
+ # @api public
247
+ #
248
+ # @example
249
+ # require "dry/files"
250
+ #
251
+ # Dry::Files.new.directory?(__dir__) # => true
252
+ # Dry::Files.new.directory?(__FILE__) # => false
253
+ #
254
+ # Dry::Files.new.directory?("missing_directory") # => false
255
+ def directory?(path)
256
+ adapter.directory?(path)
257
+ end
258
+
259
+ # Checks if `path` is an executable
260
+ #
261
+ # @param path [String,Pathname] the path to file
262
+ #
263
+ # @return [TrueClass,FalseClass] the result of the check
264
+ #
265
+ # @since 0.1.0
266
+ # @api public
267
+ #
268
+ # @example
269
+ # require "dry/files"
270
+ #
271
+ # Dry::Files.new.executable?("/path/to/ruby") # => true
272
+ # Dry::Files.new.executable?(__FILE__) # => false
273
+ #
274
+ # Dry::Files.new.directory?("missing_file") # => false
275
+ def executable?(path)
276
+ adapter.executable?(path)
277
+ end
278
+
279
+ # Adds a new line at the top of the file
280
+ #
281
+ # @param path [String,Pathname] the path to file
282
+ # @param line [String] the line to add
283
+ #
284
+ # @raise [Dry::Files::IOError] in case of I/O error
285
+ #
286
+ # @see #append
287
+ #
288
+ # @since 0.1.0
289
+ # @api public
290
+ def unshift(path, line)
291
+ content = adapter.readlines(path)
292
+ content.unshift(newline(line))
293
+
294
+ write(path, content)
295
+ end
296
+
297
+ # Adds a new line at the bottom of the file
298
+ #
299
+ # @param path [String,Pathname] the path to file
300
+ # @param contents [String] the contents to add
301
+ #
302
+ # @raise [Dry::Files::IOError] in case of I/O error
303
+ #
304
+ # @see #unshift
305
+ #
306
+ # @since 0.1.0
307
+ # @api public
308
+ def append(path, contents)
309
+ mkdir_p(path)
310
+
311
+ content = adapter.readlines(path)
312
+ content << newline unless newline?(content.last)
313
+ content << newline(contents)
314
+
315
+ write(path, content)
316
+ end
317
+
318
+ # Replace first line in `path` that contains `target` with `replacement`.
319
+ #
320
+ # @param path [String,Pathname] the path to file
321
+ # @param target [String,Regexp] the target to replace
322
+ # @param replacement [String] the replacement
323
+ #
324
+ # @raise [Dry::Files::IOError] in case of I/O error
325
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
326
+ #
327
+ # @see #replace_last_line
328
+ #
329
+ # @since 0.1.0
330
+ # @api public
331
+ def replace_first_line(path, target, replacement)
332
+ content = adapter.readlines(path)
333
+ content[index(content, path, target)] = newline(replacement)
334
+
335
+ write(path, content)
336
+ end
337
+
338
+ # Replace last line in `path` that contains `target` with `replacement`.
339
+ #
340
+ # @param path [String,Pathname] the path to file
341
+ # @param target [String,Regexp] the target to replace
342
+ # @param replacement [String] the replacement
343
+ #
344
+ # @raise [Dry::Files::IOError] in case of I/O error
345
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
346
+ #
347
+ # @see #replace_first_line
348
+ #
349
+ # @since 0.1.0
350
+ # @api public
351
+ def replace_last_line(path, target, replacement)
352
+ content = adapter.readlines(path)
353
+ content[-index(content.reverse, path, target) - CONTENT_OFFSET] = newline(replacement)
354
+
355
+ write(path, content)
356
+ end
357
+
358
+ # Inject `contents` in `path` before `target`.
359
+ #
360
+ # @param path [String,Pathname] the path to file
361
+ # @param target [String,Regexp] the target to replace
362
+ # @param contents [String] the contents to inject
363
+ #
364
+ # @raise [Dry::Files::IOError] in case of I/O error
365
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
366
+ #
367
+ # @see #inject_line_after
368
+ # @see #inject_line_before_last
369
+ # @see #inject_line_after_last
370
+ #
371
+ # @since 0.1.0
372
+ # @api public
373
+ def inject_line_before(path, target, contents)
374
+ _inject_line_before(path, target, contents, method(:index))
375
+ end
376
+
377
+ # Inject `contents` in `path` after last `target`.
378
+ #
379
+ # @param path [String,Pathname] the path to file
380
+ # @param target [String,Regexp] the target to replace
381
+ # @param contents [String] the contents to inject
382
+ #
383
+ # @raise [Dry::Files::IOError] in case of I/O error
384
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
385
+ #
386
+ # @see #inject_line_before
387
+ # @see #inject_line_after
388
+ # @see #inject_line_after_last
389
+ #
390
+ # @since 0.1.0
391
+ # @api public
392
+ def inject_line_before_last(path, target, contents)
393
+ _inject_line_before(path, target, contents, method(:rindex))
394
+ end
395
+
396
+ # Inject `contents` in `path` after `target`.
397
+ #
398
+ # @param path [String,Pathname] the path to file
399
+ # @param target [String,Regexp] the target to replace
400
+ # @param contents [String] the contents to inject
401
+ #
402
+ # @raise [Dry::Files::IOError] in case of I/O error
403
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
404
+ #
405
+ # @see #inject_line_before
406
+ # @see #inject_line_before_last
407
+ # @see #inject_line_after_last
408
+ #
409
+ # @since 0.1.0
410
+ # @api public
411
+ def inject_line_after(path, target, contents)
412
+ _inject_line_after(path, target, contents, method(:index))
413
+ end
414
+
415
+ # Inject `contents` in `path` after last `target`.
416
+ #
417
+ # @param path [String,Pathname] the path to file
418
+ # @param target [String,Regexp] the target to replace
419
+ # @param contents [String] the contents to inject
420
+ #
421
+ # @raise [Dry::Files::IOError] in case of I/O error
422
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
423
+ #
424
+ # @see #inject_line_before
425
+ # @see #inject_line_after
426
+ # @see #inject_line_before_last
427
+ #
428
+ # @since 0.1.0
429
+ # @api public
430
+ def inject_line_after_last(path, target, contents)
431
+ _inject_line_after(path, target, contents, method(:rindex))
432
+ end
433
+
434
+ # Inject `contents` in `path` within the first Ruby block that matches `target`.
435
+ # The given `contents` will appear at the TOP of the Ruby block.
436
+ #
437
+ # @param path [String,Pathname] the path to file
438
+ # @param target [String,Regexp] the target matcher for Ruby block
439
+ # @param contents [String,Array<String>] the contents to inject
440
+ #
441
+ # @raise [Dry::Files::IOError] in case of I/O error
442
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
443
+ #
444
+ # @since 0.1.0
445
+ # @api public
446
+ #
447
+ # @example Inject a single line
448
+ # require "dry/files"
449
+ #
450
+ # files = Dry::Files.new
451
+ # path = "config/application.rb"
452
+ #
453
+ # File.read(path)
454
+ # # # frozen_string_literal: true
455
+ # #
456
+ # # class Application
457
+ # # configure do
458
+ # # root __dir__
459
+ # # end
460
+ # # end
461
+ #
462
+ # # inject a single line
463
+ # files.inject_line_at_block_top(path, /configure/, %(load_path.unshift("lib")))
464
+ #
465
+ # File.read(path)
466
+ # # # frozen_string_literal: true
467
+ # #
468
+ # # class Application
469
+ # # configure do
470
+ # # load_path.unshift("lib")
471
+ # # root __dir__
472
+ # # end
473
+ # # end
474
+ #
475
+ # @example Inject multiple lines
476
+ # require "dry/files"
477
+ #
478
+ # files = Dry::Files.new
479
+ # path = "config/application.rb"
480
+ #
481
+ # File.read(path)
482
+ # # # frozen_string_literal: true
483
+ # #
484
+ # # class Application
485
+ # # configure do
486
+ # # root __dir__
487
+ # # end
488
+ # # end
489
+ #
490
+ # # inject multiple lines
491
+ # files.inject_line_at_block_top(path,
492
+ # /configure/,
493
+ # [%(load_path.unshift("lib")), "settings.load!"])
494
+ #
495
+ # File.read(path)
496
+ # # # frozen_string_literal: true
497
+ # #
498
+ # # class Application
499
+ # # configure do
500
+ # # load_path.unshift("lib")
501
+ # # settings.load!
502
+ # # root __dir__
503
+ # # end
504
+ # # end
505
+ #
506
+ # @example Inject a block
507
+ # require "dry/files"
508
+ #
509
+ # files = Dry::Files.new
510
+ # path = "config/application.rb"
511
+ #
512
+ # File.read(path)
513
+ # # # frozen_string_literal: true
514
+ # #
515
+ # # class Application
516
+ # # configure do
517
+ # # root __dir__
518
+ # # end
519
+ # # end
520
+ #
521
+ # # inject a block
522
+ # block = <<~BLOCK
523
+ # settings do
524
+ # load!
525
+ # end
526
+ # BLOCK
527
+ # files.inject_line_at_block_top(path, /configure/, block)
528
+ #
529
+ # File.read(path)
530
+ # # # frozen_string_literal: true
531
+ # #
532
+ # # class Application
533
+ # # configure do
534
+ # # settings do
535
+ # # load!
536
+ # # end
537
+ # # root __dir__
538
+ # # end
539
+ # # end
540
+ def inject_line_at_block_top(path, target, *contents)
541
+ content = adapter.readlines(path)
542
+ starting = index(content, path, target)
543
+ offset = SPACE * (content[starting][SPACE_MATCHER].bytesize + INDENTATION)
544
+
545
+ contents = Array(contents).flatten
546
+ contents = _offset_block_lines(contents, offset)
547
+
548
+ content.insert(starting + CONTENT_OFFSET, contents)
549
+ write(path, content)
550
+ end
551
+
552
+ # Inject `contents` in `path` within the first Ruby block that matches `target`.
553
+ # The given `contents` will appear at the BOTTOM of the Ruby block.
554
+ #
555
+ # @param path [String,Pathname] the path to file
556
+ # @param target [String,Regexp] the target matcher for Ruby block
557
+ # @param contents [String,Array<String>] the contents to inject
558
+ #
559
+ # @raise [Dry::Files::IOError] in case of I/O error
560
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
561
+ #
562
+ # @since 0.1.0
563
+ # @api public
564
+ #
565
+ # @example Inject a single line
566
+ # require "dry/files"
567
+ #
568
+ # files = Dry::Files.new
569
+ # path = "config/application.rb"
570
+ #
571
+ # File.read(path)
572
+ # # # frozen_string_literal: true
573
+ # #
574
+ # # class Application
575
+ # # configure do
576
+ # # root __dir__
577
+ # # end
578
+ # # end
579
+ #
580
+ # # inject a single line
581
+ # files.inject_line_at_block_bottom(path, /configure/, %(load_path.unshift("lib")))
582
+ #
583
+ # File.read(path)
584
+ # # # frozen_string_literal: true
585
+ # #
586
+ # # class Application
587
+ # # configure do
588
+ # # root __dir__
589
+ # # load_path.unshift("lib")
590
+ # # end
591
+ # # end
592
+ #
593
+ # @example Inject multiple lines
594
+ # require "dry/files"
595
+ #
596
+ # files = Dry::Files.new
597
+ # path = "config/application.rb"
598
+ #
599
+ # File.read(path)
600
+ # # # frozen_string_literal: true
601
+ # #
602
+ # # class Application
603
+ # # configure do
604
+ # # root __dir__
605
+ # # end
606
+ # # end
607
+ #
608
+ # # inject multiple lines
609
+ # files.inject_line_at_block_bottom(path,
610
+ # /configure/,
611
+ # [%(load_path.unshift("lib")), "settings.load!"])
612
+ #
613
+ # File.read(path)
614
+ # # # frozen_string_literal: true
615
+ # #
616
+ # # class Application
617
+ # # configure do
618
+ # # root __dir__
619
+ # # load_path.unshift("lib")
620
+ # # settings.load!
621
+ # # end
622
+ # # end
623
+ #
624
+ # @example Inject a block
625
+ # require "dry/files"
626
+ #
627
+ # files = Dry::Files.new
628
+ # path = "config/application.rb"
629
+ #
630
+ # File.read(path)
631
+ # # # frozen_string_literal: true
632
+ # #
633
+ # # class Application
634
+ # # configure do
635
+ # # root __dir__
636
+ # # end
637
+ # # end
638
+ #
639
+ # # inject a block
640
+ # block = <<~BLOCK
641
+ # settings do
642
+ # load!
643
+ # end
644
+ # BLOCK
645
+ # files.inject_line_at_block_bottom(path, /configure/, block)
646
+ #
647
+ # File.read(path)
648
+ # # # frozen_string_literal: true
649
+ # #
650
+ # # class Application
651
+ # # configure do
652
+ # # root __dir__
653
+ # # settings do
654
+ # # load!
655
+ # # end
656
+ # # end
657
+ # # end
658
+ def inject_line_at_block_bottom(path, target, *contents)
659
+ content = adapter.readlines(path)
660
+ starting = index(content, path, target)
661
+ line = content[starting]
662
+ size = line[SPACE_MATCHER].bytesize
663
+ closing = (SPACE * size) +
664
+ (target.match?(INLINE_OPEN_BLOCK_MATCHER) ? INLINE_CLOSE_BLOCK : CLOSE_BLOCK)
665
+ ending = starting + index(content[starting..-CONTENT_OFFSET], path, closing)
666
+ offset = SPACE * (content[ending][SPACE_MATCHER].bytesize + INDENTATION)
667
+
668
+ contents = Array(contents).flatten
669
+ contents = _offset_block_lines(contents, offset)
670
+
671
+ content.insert(ending, contents)
672
+ write(path, content)
673
+ end
674
+
675
+ # Removes line from `path`, matching `target`.
676
+ #
677
+ # @param path [String,Pathname] the path to file
678
+ # @param target [String,Regexp] the target to remove
679
+ #
680
+ # @raise [Dry::Files::IOError] in case of I/O error
681
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
682
+ #
683
+ # @since 0.1.0
684
+ # @api public
685
+ def remove_line(path, target)
686
+ content = adapter.readlines(path)
687
+ i = index(content, path, target)
688
+
689
+ content.delete_at(i)
690
+ write(path, content)
691
+ end
692
+
693
+ # Removes `target` block from `path`
694
+ #
695
+ # @param path [String,Pathname] the path to file
696
+ # @param target [String] the target block to remove
697
+ #
698
+ # @raise [Dry::Files::IOError] in case of I/O error
699
+ # @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
700
+ #
701
+ # @since 0.1.0
702
+ # @api public
703
+ #
704
+ # @example
705
+ # require "dry/files"
706
+ #
707
+ # puts File.read("app.rb")
708
+ #
709
+ # # class App
710
+ # # configure do
711
+ # # root __dir__
712
+ # # end
713
+ # # end
714
+ #
715
+ # Dry::Files.new.remove_block("app.rb", "configure")
716
+ #
717
+ # puts File.read("app.rb")
718
+ #
719
+ # # class App
720
+ # # end
721
+ def remove_block(path, target)
722
+ content = adapter.readlines(path)
723
+ starting = index(content, path, target)
724
+ line = content[starting]
725
+ size = line[SPACE_MATCHER].bytesize
726
+ closing = (SPACE * size) +
727
+ (target.match?(INLINE_OPEN_BLOCK_MATCHER) ? INLINE_CLOSE_BLOCK : CLOSE_BLOCK)
728
+ ending = starting + index(content[starting..-CONTENT_OFFSET], path, closing)
729
+
730
+ content.slice!(starting..ending)
731
+ write(path, content)
732
+
733
+ remove_block(path, target) if match?(content, target)
734
+ end
735
+
736
+ private
737
+
738
+ # @since 0.1.0
739
+ # @api private
740
+ NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
741
+ private_constant :NEW_LINE
742
+
743
+ # @since 0.1.0
744
+ # @api private
745
+ CONTENT_OFFSET = 1
746
+ private_constant :CONTENT_OFFSET
747
+
748
+ # @since 0.1.0
749
+ # @api private
750
+ SPACE = " "
751
+ private_constant :SPACE
752
+
753
+ # @since 0.1.0
754
+ # @api private
755
+ INDENTATION = 2
756
+ private_constant :INDENTATION
757
+
758
+ # @since 0.1.0
759
+ # @api private
760
+ SPACE_MATCHER = /\A[[:space:]]*/.freeze
761
+ private_constant :SPACE_MATCHER
762
+
763
+ # @since 0.1.0
764
+ # @api private
765
+ INLINE_OPEN_BLOCK_MATCHER = "{"
766
+ private_constant :INLINE_OPEN_BLOCK_MATCHER
767
+
768
+ # @since 0.1.0
769
+ # @api private
770
+ INLINE_CLOSE_BLOCK = "}"
771
+ private_constant :INLINE_CLOSE_BLOCK
772
+
773
+ # @since 0.1.0
774
+ # @api private
775
+ CLOSE_BLOCK = "end"
776
+ private_constant :CLOSE_BLOCK
777
+
778
+ # @since 0.1.0
779
+ # @api private
780
+ attr_reader :adapter
781
+
782
+ # @since 0.1.0
783
+ # @api private
784
+ def newline(line = nil)
785
+ "#{line}#{NEW_LINE}"
786
+ end
787
+
788
+ # @since 0.1.0
789
+ # @api private
790
+ def newline?(content)
791
+ content.end_with?(NEW_LINE)
792
+ end
793
+
794
+ # @since 0.1.0
795
+ # @api private
796
+ def match?(content, target)
797
+ !line_number(content, target).nil?
798
+ end
799
+
800
+ # @since 0.1.0
801
+ # @api private
802
+ def index(content, path, target)
803
+ line_number(content, target) or
804
+ raise MissingTargetError.new(target, path)
805
+ end
806
+
807
+ # @since 0.1.0
808
+ # @api private
809
+ def rindex(content, path, target)
810
+ line_number(content, target, finder: content.method(:rindex)) or
811
+ raise MissingTargetError.new(target, path)
812
+ end
813
+
814
+ # @since 0.1.0
815
+ # @api private
816
+ def _inject_line_before(path, target, contents, finder)
817
+ content = adapter.readlines(path)
818
+ i = finder.call(content, path, target)
819
+
820
+ content.insert(i, newline(contents))
821
+ write(path, content)
822
+ end
823
+
824
+ # @since 0.1.0
825
+ # @api private
826
+ def _inject_line_after(path, target, contents, finder)
827
+ content = adapter.readlines(path)
828
+ i = finder.call(content, path, target)
829
+
830
+ content.insert(i + CONTENT_OFFSET, newline(contents))
831
+ write(path, content)
832
+ end
833
+
834
+ # @since 0.1.0
835
+ # @api private
836
+ def _offset_block_lines(contents, offset)
837
+ contents.map do |line|
838
+ if line.match?(NEW_LINE)
839
+ line = line.split(NEW_LINE)
840
+ _offset_block_lines(line, offset)
841
+ else
842
+ offset + line + NEW_LINE
843
+ end
844
+ end.join
845
+ end
846
+
847
+ # @since 0.1.0
848
+ # @api private
849
+ def line_number(content, target, finder: content.method(:index))
850
+ finder.call do |l|
851
+ case target
852
+ when ::String
853
+ l.include?(target)
854
+ when Regexp
855
+ l =~ target
856
+ end
857
+ end
858
+ end
859
+ end
860
+ end