dry-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.
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