From bccc2a7acfef66ddd6a8de82c08c6822f941acc2 Mon Sep 17 00:00:00 2001
From: Tim Morgan <tim@timmorgan.org>
Date: Sun, 23 Jun 2024 11:17:27 -0500
Subject: [PATCH] Add specs for String#encode :fallback option

---
 core/string/shared/encode.rb | 116 +++++++++++++++++++++++++++++++++++
 1 file changed, 116 insertions(+)

diff --git a/core/string/shared/encode.rb b/core/string/shared/encode.rb
index 3776e0d70..91db3254d 100644
--- a/core/string/shared/encode.rb
+++ b/core/string/shared/encode.rb
@@ -194,6 +194,122 @@
     end
   end
 
+  describe "given the fallback option" do
+    context "given a hash" do
+      it "looks up the replacement value from the hash" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "bar" })
+        encoded.should == "Bbar"
+      end
+
+      it "raises an error if the key is not present in the hash" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: { "foo" => "bar" })
+        }.should raise_error(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII")
+      end
+
+      it "raises an error if the value is itself invalid" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "\uffee" })
+        }.should raise_error(ArgumentError, "too big fallback string")
+      end
+
+      it "uses the hash's default value if set" do
+        hash = {}
+        hash.default = "bar"
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash)
+        encoded.should == "Bbar"
+      end
+
+      it "uses the result of calling default_proc if set" do
+        hash = {}
+        hash.default_proc = -> _, _ { "bar" }
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash)
+        encoded.should == "Bbar"
+      end
+    end
+
+    context "given an object inheriting from Hash" do
+      before do
+        klass = Class.new(Hash)
+        @hash_like = klass.new
+        @hash_like["\ufffd"] = "bar"
+      end
+
+      it "looks up the replacement value from the object" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like)
+        encoded.should == "Bbar"
+      end
+    end
+
+    context "given an object responding to []" do
+      before do
+        klass = Class.new do
+          def [](c) = c.bytes.inspect
+        end
+        @hash_like = klass.new
+      end
+
+      it "calls [] on the object, passing the invalid character" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like)
+        encoded.should == "B[239, 191, 189]"
+      end
+    end
+
+    context "given an object not responding to []" do
+      before do
+        @non_hash_like = Object.new
+      end
+
+      it "raises an error" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: @non_hash_like)
+        }.should raise_error(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII")
+      end
+    end
+
+    context "given a proc" do
+      it "calls the proc to get the replacement value, passing in the invalid character" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| c.bytes.inspect })
+        encoded.should == "B[239, 191, 189]"
+      end
+
+      it "raises an error if the returned value is itself invalid" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" })
+        }.should raise_error(ArgumentError, "too big fallback string")
+      end
+    end
+
+    context "given a lambda" do
+      it "calls the lambda to get the replacement value, passing in the invalid character" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { c.bytes.inspect })
+        encoded.should == "B[239, 191, 189]"
+      end
+
+      it "raises an error if the returned value is itself invalid" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" })
+        }.should raise_error(ArgumentError, "too big fallback string")
+      end
+    end
+
+    context "given a method" do
+      def replace(c) = c.bytes.inspect
+      def replace_bad(c) = "\uffee"
+
+      it "calls the method to get the replacement value, passing in the invalid character" do
+        encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace))
+        encoded.should == "B[239, 191, 189]"
+      end
+
+      it "raises an error if the returned value is itself invalid" do
+        -> {
+          "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_bad))
+        }.should raise_error(ArgumentError, "too big fallback string")
+      end
+    end
+  end
+
   describe "given the xml: :text option" do
     it "replaces all instances of '&' with '&amp;'" do
       '& and &'.send(@method, "UTF-8", xml: :text).should == '&amp; and &amp;'