Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions lib/thor/actions/inject_into_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

class Thor
module Actions
WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: moving this is good anyway because it was the subject of the method comment intended for insert_into_file 😅


# Injects the given content into a file, raising an error if the contents of
# the file are not changed. Different from gsub_file, this method is reversible.
#
# ==== Parameters
# destination<String>:: Relative path to the destination root
# data<String>:: Data to add to the file. Can be given as a block.
# config<Hash>:: give :verbose => false to not log the status and the flag
# for injection (:after or :before) or :force => true for
# insert two or more times the same content.
#
# ==== Examples
#
# insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n"
#
# insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
# gems = ask "Which gems would you like to add?"
# gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
# end
#
def insert_into_file!(destination, *args, &block)
data = block_given? ? block : args.shift

config = args.shift || {}
config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
config = config.merge({error_on_no_change: true})

action InjectIntoFile.new(self, destination, data, config)
end
alias_method :inject_into_file!, :insert_into_file!

# Injects the given content into a file. Different from gsub_file, this
# method is reversible.
#
Expand All @@ -21,8 +53,6 @@ module Actions
# gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
# end
#
WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}

def insert_into_file(destination, *args, &block)
data = block_given? ? block : args.shift

Expand All @@ -47,6 +77,7 @@ def initialize(base, destination, data, config)

@replacement = data.is_a?(Proc) ? data.call : data
@flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp)
@error_on_no_change = @config.fetch(:error_on_no_change, false)
end

def invoke!
Expand All @@ -59,6 +90,8 @@ def invoke!
if exists?
if replace!(/#{flag}/, content, config[:force])
say_status(:invoke)
elsif @error_on_no_change
raise Thor::Error, "The content of #{destination} did not change"
elsif replacement_present?
say_status(:unchanged, color: :blue)
else
Expand Down
162 changes: 162 additions & 0 deletions spec/actions/inject_into_file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def invoke!(*args, &block)
capture(:stdout) { invoker.insert_into_file(*args, &block) }
end

def invoke_with_a_bang!(*args, &block)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: this seemed like the most straightforward, if a little whimsical, way of handling the tests

Happy to go with an alternative structure though if desired

capture(:stdout) { invoker.insert_into_file!(*args, &block) }
end

def revoke!(*args, &block)
capture(:stdout) { revoker.insert_into_file(*args, &block) }
end
Expand Down Expand Up @@ -170,6 +174,164 @@ def file
end
end
end

context "with a bang" do
it "changes the file adding content after the flag" do
invoke_with_a_bang! "doc/README", "\nmore content", after: "__start__"
expect(File.read(file)).to eq("__start__\nmore content\nREADME\n__end__\n")
end

it "changes the file adding content before the flag" do
invoke_with_a_bang! "doc/README", "more content\n", before: "__end__"
expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")
end

it "appends content to the file if before and after arguments not provided" do
invoke_with_a_bang!("doc/README", "more content\n")
expect(File.read(file)).to eq("__start__\nREADME\n__end__\nmore content\n")
end

it "does not change the file and raises an error if replacement present in the file" do
invoke_with_a_bang!("doc/README", "more specific content\n")
expect do
invoke_with_a_bang!("doc/README", "more specific content\n")
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")
end

it "does not change the file and raises an error if flag not found in the file" do
expect do
invoke_with_a_bang!("doc/README", "more content\n", after: "whatever")
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")
end

it "accepts data as a block" do
invoke_with_a_bang! "doc/README", before: "__end__" do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")
end

it "logs status" do
expect(invoke_with_a_bang!("doc/README", "\nmore content", after: "__start__")).to eq(" insert doc/README\n")
end

it "logs status if pretending" do
invoker(pretend: true)
expect(invoke_with_a_bang!("doc/README", "\nmore content", after: "__start__")).to eq(" insert doc/README\n")
end

it "does not change the file if pretending" do
invoker pretend: true
invoke_with_a_bang! "doc/README", "\nmore content", after: "__start__"
expect(File.read(file)).to eq("__start__\nREADME\n__end__\n")
end

it "does not change the file and raises an error if already includes content" do
invoke_with_a_bang! "doc/README", before: "__end__" do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")

expect do
invoke_with_a_bang! "doc/README", before: "__end__" do
"more content\n"
end
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")
end

it "does not change the file and raises an error if already includes content using before with capture" do
invoke_with_a_bang! "doc/README", before: /(__end__)/ do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")

expect do
invoke_with_a_bang! "doc/README", before: /(__end__)/ do
"more content\n"
end
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")
end

it "does not change the file and raises an error if already includes content using after with capture" do
invoke_with_a_bang! "doc/README", after: /(README\n)/ do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")

expect do
invoke_with_a_bang! "doc/README", after: /(README\n)/ do
"more content\n"
end
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")
end

it "does not attempt to change the file if it doesn't exist - instead raises Thor::Error" do
expect do
invoke_with_a_bang! "idontexist", before: "something" do
"any content"
end
end.to raise_error(Thor::Error, /does not appear to exist/)
expect(File.exist?("idontexist")).to be_falsey
end

it "does not attempt to change the file if it doesn't exist and pretending" do
expect do
invoker pretend: true
invoke_with_a_bang! "idontexist", before: "something" do
"any content"
end
end.not_to raise_error
expect(File.exist?("idontexist")).to be_falsey
end

it "does change the file if already includes content and :force is true" do
invoke_with_a_bang! "doc/README", before: "__end__" do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n")

invoke_with_a_bang! "doc/README", before: "__end__", force: true do
"more content\n"
end

expect(File.read(file)).to eq("__start__\nREADME\nmore content\nmore content\n__end__\n")
end

it "can insert chinese" do
encoding_original = Encoding.default_external

begin
silence_warnings do
Encoding.default_external = Encoding.find("UTF-8")
end
invoke_with_a_bang! "doc/README.zh", "\n中文", after: "__start__"
expect(File.read(File.join(destination_root, "doc/README.zh"))).to eq("__start__\n中文\n说明\n__end__\n")
ensure
silence_warnings do
Encoding.default_external = encoding_original
end
end
end

it "does not mutate the provided config" do
config = {after: "__start__"}
invoke_with_a_bang! "doc/README", "\nmore content", config
expect(File.read(file)).to eq("__start__\nmore content\nREADME\n__end__\n")

expect(config).to eq({after: "__start__"})
end
end
end

describe "#revoke!" do
Expand Down