Skip to content
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

Newlines revamp (reading and writing) #7

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
55 changes: 24 additions & 31 deletions lib/dry/files.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "English" # Required to load $INPUT_RECORD_SEPARATOR

# dry-rb is a collection of next-generation Ruby libraries
#
# @api public
Expand All @@ -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
Expand Down Expand Up @@ -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 == newline # Leave it empty
adapter.write(path, joined_lines)
end

# Returns a new string formed by joining the strings using Operating
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
15 changes: 12 additions & 3 deletions lib/dry/files/file_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -117,13 +126,13 @@ 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
#
# @since 0.1.0
# @api private
def write(path, *content)
def write(path, content)
mkdir_p(path)

self.open(path, WRITE_MODE) do |f|
Expand Down
10 changes: 5 additions & 5 deletions lib/dry/files/memory_file_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Files
class MemoryFileSystem
# @since 0.1.0
# @api private
EMPTY_CONTENT = nil
EMPTY_CONTENT = ""
private_constant :EMPTY_CONTENT

require_relative "./memory_file_system/node"
Expand Down Expand Up @@ -104,20 +104,20 @@ def touch(path)
# of an existing file for the given path and content
# All the intermediate directories are created.
#
# @param path [String, Array<String>] the target path
# @param content [String, Array<String>] the content to write
# @param path [Array<String>] the target path
# @param content [String] the content to write
#
# @since 0.1.0
# @api private
def write(path, *content)
def write(path, content)
path = Path[path]
node = @root

for_each_segment(path) do |segment|
node = node.set(segment)
end

node.write(*content)
node.write(content)
node
end

Expand Down
9 changes: 4 additions & 5 deletions lib/dry/files/memory_file_system/node.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require "English"
require "stringio"

module Dry
Expand Down Expand Up @@ -203,20 +202,20 @@ def readlines
raise NotMemoryFileError, segment unless file?

@content.rewind
@content.readlines
@content.readlines(chomp: true)
end

# Write file contents
# IMPORTANT: This operation turns a node into a file
#
# @param content [String, Array<String>] the file content
# @param content [String] the file content
#
# @raise [Dry::Files::NotMemoryFileError] if node isn't a file
#
# @since 0.1.0
# @api private
def write(*content)
@content = StringIO.new(content.join($RS))
def write(content)
@content = StringIO.new(content)
@mode = DEFAULT_FILE_MODE
end

Expand Down
33 changes: 23 additions & 10 deletions spec/integration/dry/files_spec.rb
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) }
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect($INPUT_RECORD_SEPARATOR).not_to be_nil
expected = if windows?
"\r\n"
else
"\n"
end
expect($INPUT_RECORD_SEPARATOR).to eq(expected)

Then we can try to run CI on windows as well, if GHA allows that.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 require "english" from this library (which is what this PR sought to fix in the first place). We don't care about the value, just that it's non-nil. And Windows CI sounds like something we should avoid if we can :)

end
end

describe "#touch" do
it "creates an empty file" do
path = root.join("touch")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spec/support/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
end

failure_message do |actual|
"expected that `#{actual}' would have content '#{expected}', but it has '#{subject.read(actual)}'" # rubocop:disable Layout/LineLength
"expected that `#{actual}' would have content '#{expected}', but it has '#{subject.read(actual)}'"
end
end
2 changes: 1 addition & 1 deletion spec/support/os.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def with_operating_system(os, &blk)
when /darwin/ then :macos
when /win32|mingw|bccwin|cygwin/ then :windows
else
raise "unkwnown OS: `#{host_os}'"
raise "unknown OS: `#{host_os}'"
end

blk.call if result == os
Expand Down
3 changes: 1 addition & 2 deletions spec/unit/dry/files/file_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

require "dry/files/file_system"
require "securerandom"
require "English"

RSpec.describe Dry::Files::FileSystem do
let(:root) { Pathname.new(Dir.pwd).join("tmp", SecureRandom.uuid).tap(&:mkpath) }
Expand Down Expand Up @@ -87,7 +86,7 @@
path = root.join("readlines-file")
subject.write(path, "hello#{newline}world")

expect(subject.readlines(path)).to eq(%W[hello#{newline} world])
expect(subject.readlines(path)).to eq(%w[hello world])
end

it "reads empty file and returns empty array" do
Expand Down
Loading