-
-
Notifications
You must be signed in to change notification settings - Fork 8
Newlines revamp (reading and writing) #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
a7ff007
fbfaed8
b4e7106
182c874
6a5f92d
f1f4389
d456998
8d4dbc3
bcbac68
1c5053c
c8fa3d9
d4d36fc
d873b27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "english" | ||
|
|
||
| # dry-rb is a collection of next-generation Ruby libraries | ||
| # | ||
| # @api public | ||
|
|
@@ -25,8 +27,11 @@ class Files | |
| # | ||
| # @since 0.1.0 | ||
| # @api public | ||
| def initialize(memory: false, adapter: Adapter.call(memory: memory)) | ||
| def initialize(memory: false, | ||
| adapter: Adapter.call(memory: memory), | ||
| newline: $INPUT_RECORD_SEPARATOR) | ||
| @adapter = adapter | ||
| @newline = newline | ||
| end | ||
|
|
||
| # Read file content | ||
|
|
@@ -64,14 +69,16 @@ def touch(path) | |
| # All the intermediate directories are created. | ||
| # | ||
| # @param path [String,Pathname] the path to file | ||
| # @param content [String, Array<String>] the content to write | ||
| # @param lines [String, Array<String>] the content to write | ||
| # | ||
| # @raise [Dry::Files::IOError] in case of I/O error | ||
| # | ||
| # @since 0.1.0 | ||
| # @api public | ||
| def write(path, *content) | ||
| adapter.write(path, *content) | ||
| def write(path, lines) | ||
| joined_lines = Array(lines).flatten.map { |line| line.chomp.concat(newline) }.join | ||
| joined_lines = "" if joined_lines == "\n" # Leave it empty | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cllns Can we extract
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I switched to I can extract an |
||
| adapter.write(path, joined_lines) | ||
| end | ||
|
|
||
| # Returns a new string formed by joining the strings using Operating | ||
|
|
@@ -289,7 +296,7 @@ def executable?(path) | |
| # @api public | ||
| def unshift(path, line) | ||
| content = adapter.readlines(path) | ||
| content.unshift(newline(line)) | ||
| content.unshift(line) | ||
|
|
||
| write(path, content) | ||
| end | ||
|
|
@@ -309,8 +316,7 @@ def append(path, contents) | |
| mkdir_p(path) | ||
|
|
||
| content = adapter.readlines(path) | ||
| content << newline unless newline?(content.last) | ||
| content << newline(contents) | ||
| content << contents | ||
|
|
||
| write(path, content) | ||
| end | ||
|
|
@@ -330,7 +336,7 @@ def append(path, contents) | |
| # @api public | ||
| def replace_first_line(path, target, replacement) | ||
| content = adapter.readlines(path) | ||
| content[index(content, path, target)] = newline(replacement) | ||
| content[index(content, path, target)] = replacement | ||
|
|
||
| write(path, content) | ||
| end | ||
|
|
@@ -350,7 +356,7 @@ def replace_first_line(path, target, replacement) | |
| # @api public | ||
| def replace_last_line(path, target, replacement) | ||
| content = adapter.readlines(path) | ||
| content[-index(content.reverse, path, target) - CONTENT_OFFSET] = newline(replacement) | ||
| content[-index(content.reverse, path, target) - CONTENT_OFFSET] = replacement | ||
|
|
||
| write(path, content) | ||
| end | ||
|
|
@@ -735,11 +741,6 @@ def remove_block(path, target) | |
|
|
||
| private | ||
|
|
||
| # @since 0.1.0 | ||
| # @api private | ||
| NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars | ||
| private_constant :NEW_LINE | ||
|
|
||
| # @since 0.1.0 | ||
| # @api private | ||
| CONTENT_OFFSET = 1 | ||
|
|
@@ -779,17 +780,9 @@ def remove_block(path, target) | |
| # @api private | ||
| attr_reader :adapter | ||
|
|
||
| # @since 0.1.0 | ||
| # @since x.x.x | ||
| # @api private | ||
| def newline(line = nil) | ||
| "#{line}#{NEW_LINE}" | ||
| end | ||
|
|
||
| # @since 0.1.0 | ||
| # @api private | ||
| def newline?(content) | ||
| content.end_with?(NEW_LINE) | ||
| end | ||
| attr_reader :newline | ||
|
|
||
| # @since 0.1.0 | ||
| # @api private | ||
|
|
@@ -817,7 +810,7 @@ def _inject_line_before(path, target, contents, finder) | |
| content = adapter.readlines(path) | ||
| i = finder.call(content, path, target) | ||
|
|
||
| content.insert(i, newline(contents)) | ||
| content.insert(i, contents) | ||
| write(path, content) | ||
| end | ||
|
|
||
|
|
@@ -827,21 +820,21 @@ def _inject_line_after(path, target, contents, finder) | |
| content = adapter.readlines(path) | ||
| i = finder.call(content, path, target) | ||
|
|
||
| content.insert(i + CONTENT_OFFSET, newline(contents)) | ||
| content.insert(i + CONTENT_OFFSET, contents) | ||
| write(path, content) | ||
| end | ||
|
|
||
| # @since 0.1.0 | ||
| # @api private | ||
| def _offset_block_lines(contents, offset) | ||
| contents.map do |line| | ||
| if line.match?(NEW_LINE) | ||
| line = line.split(NEW_LINE) | ||
| _offset_block_lines(line, offset) | ||
| case line.lines | ||
| in [line] | ||
| offset + line | ||
| else | ||
| offset + line + NEW_LINE | ||
| _offset_block_lines(line.lines, offset) | ||
| end | ||
| end.join | ||
| end | ||
| end | ||
|
|
||
| # @since 0.1.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,7 +86,16 @@ def read(path, *args, **kwargs) | |
| # @api private | ||
| def readlines(path, *args) | ||
| with_error_handling do | ||
| file.readlines(path, *args) | ||
| file.readlines(path, *args, chomp: true).then do |lines| | ||
| # The last item will be an empty string if the file has a | ||
| # trailing newline. We don't want that in our contents, | ||
| # especially since we append a newline to every line during `write` | ||
| if lines.last && lines.last.empty? | ||
| lines[0..-2] | ||
| else | ||
| lines | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -117,13 +126,16 @@ def touch(path, **kwargs) | |
| # All the intermediate directories are created. | ||
| # | ||
| # @param path [String,Pathname] the path to file | ||
| # @param content [String, Array<String>] the content to write | ||
| # @param content [String] the content to write | ||
| # | ||
| # @raise [Dry::Files::IOError] in case of I/O error | ||
| # @raise [CanOnlyWriteStringError] if content param isn't a String | ||
| # | ||
| # @since 0.1.0 | ||
| # @api private | ||
| def write(path, *content) | ||
| def write(path, content) | ||
| raise CanOnlyWriteStringError unless content.is_a?(String) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cllns Given this is an adapter (private API), we control the arguments, this exception seems to be a redundant internal assertion. Is this necessary?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha. Was being defensive but you're right, it's not worth a custom Error |
||
|
|
||
| mkdir_p(path) | ||
|
|
||
| self.open(path, WRITE_MODE) do |f| | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,6 @@ | ||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||
|
|
||||||||||||||||||
| require "securerandom" | ||||||||||||||||||
| require "English" | ||||||||||||||||||
|
|
||||||||||||||||||
| RSpec.describe Dry::Files do | ||||||||||||||||||
| let(:root) { Pathname.new(Dir.pwd).join("tmp", SecureRandom.uuid).tap(&:mkpath) } | ||||||||||||||||||
|
|
@@ -11,6 +10,12 @@ | |||||||||||||||||
| FileUtils.remove_entry_secure(root) | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| describe "$INPUT_RECORD_SEPARATOR" do | ||||||||||||||||||
| it "is not nil" do | ||||||||||||||||||
| expect($INPUT_RECORD_SEPARATOR).not_to be_nil | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Then we can try to run CI on windows as well, if GHA allows that.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my research "$INPUT_RECORD_SEPARATOR does not give us \r\n on Windows" (from above). I don't have a Windows computer to confirm this but that is what I found. Besides, we don't need to test Ruby. This is a regression test to ensure we don't remove |
||||||||||||||||||
| end | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| describe "#touch" do | ||||||||||||||||||
| it "creates an empty file" do | ||||||||||||||||||
| path = root.join("touch") | ||||||||||||||||||
|
|
@@ -52,9 +57,9 @@ | |||||||||||||||||
| describe "#read" do | ||||||||||||||||||
| it "reads file" do | ||||||||||||||||||
| path = root.join("read") | ||||||||||||||||||
| subject.write(path, expected = "Hello#{newline}World") | ||||||||||||||||||
| subject.write(path, "Hello#{newline}World") | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(subject.read(path)).to eq(expected) | ||||||||||||||||||
| expect(subject.read(path)).to eq("Hello#{newline}World#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "raises error when path is a directory" do | ||||||||||||||||||
|
|
@@ -85,15 +90,23 @@ | |||||||||||||||||
| subject.write(path, "Hello#{newline}World") | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(path).to exist | ||||||||||||||||||
| expect(path).to have_content("Hello#{newline}World") | ||||||||||||||||||
| expect(path).to have_content("Hello#{newline}World#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "creates an empty file (without trailing newline)" do | ||||||||||||||||||
| path = root.join("write") | ||||||||||||||||||
| subject.write(path, "") | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(path).to exist | ||||||||||||||||||
| expect(path).to have_content("") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "creates intermediate directories" do | ||||||||||||||||||
| path = root.join("path", "to", "file", "write") | ||||||||||||||||||
| subject.write(path, ":)") | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(path).to exist | ||||||||||||||||||
| expect(path).to have_content(":)") | ||||||||||||||||||
| expect(path).to have_content(":)#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "overwrites file when it already exists" do | ||||||||||||||||||
|
|
@@ -102,7 +115,7 @@ | |||||||||||||||||
| subject.write(path, "new words") | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(path).to exist | ||||||||||||||||||
| expect(path).to have_content("new words") | ||||||||||||||||||
| expect(path).to have_content("new words#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "raises error when path isn't writeable" do | ||||||||||||||||||
|
|
@@ -138,7 +151,7 @@ | |||||||||||||||||
| subject.cp(source, destination) | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(destination).to exist | ||||||||||||||||||
| expect(destination).to have_content("the source") | ||||||||||||||||||
| expect(destination).to have_content("the source#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "creates intermediate directories" do | ||||||||||||||||||
|
|
@@ -149,7 +162,7 @@ | |||||||||||||||||
| subject.cp(source, destination) | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(destination).to exist | ||||||||||||||||||
| expect(destination).to have_content("the source for intermediate directories") | ||||||||||||||||||
| expect(destination).to have_content("the source for intermediate directories#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "overrides already existing file" do | ||||||||||||||||||
|
|
@@ -161,7 +174,7 @@ | |||||||||||||||||
| subject.cp(source, destination) | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(destination).to exist | ||||||||||||||||||
| expect(destination).to have_content("the source") | ||||||||||||||||||
| expect(destination).to have_content("the source#{newline}") | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| it "raises error when source cannot be found" do | ||||||||||||||||||
|
|
@@ -455,7 +468,7 @@ class Unshift | |||||||||||||||||
| subject.write(path, content) | ||||||||||||||||||
| subject.unshift(path, "root to: 'home#index'") | ||||||||||||||||||
|
|
||||||||||||||||||
| expected = "root to: 'home#index'#{newline}get '/tires', to: 'sunshine#index'" | ||||||||||||||||||
| expected = "root to: 'home#index'#{newline}get '/tires', to: 'sunshine#index'#{newline}" | ||||||||||||||||||
|
|
||||||||||||||||||
| expect(path).to have_content(expected) | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cllns Isn't this creating too many intermediate arrays? Can we use destructive counterparts (e.g.
#map!instead of#map?