String Interpolation with Templates Using String#
I was working on a feature for upcloudify that will use Slack notifications instead of email notifications. One of the challenges I faced was how to build-in flexibility for generating messages. I would want that the user be able to provide their own custom notification message, but at the same time be able to provide placeholders for items like the S3 download link.
I initially thought of using an ERB
template but then realized it will be far too overkill for this simple purpose. I actually just needed to have the caller be able to provide a template string, and then merge certain variables into this template string.
I started playing around with regexes when I came upon String#%
From the documentation:
Format—Uses str as a format specification, and returns the result of applying it to arg. If the format specification contains more than one substitution, then arg must be an
Array
orHash
containing the values to be substituted. SeeKernel::sprintf
for details of the format string.
"%05d" % 123 #=> "00123"
"%-5s: %08x" % [ "ID", self.object_id ] #=> "ID : 200e14d6"
"foo = %{foo}" % { :foo => 'bar' } #=> "foo = bar"
I thought this was very cool. Let me show you what I mean:
You can also use an array to feed the template:
For an even better template that doesn’t depend on the order of the elements, you can feed in a Hash
:
Note: Sadly, OpalBox doesn’t seem to work well with this particular usage of String#%
since Opal 0.7.1 has a bug where it doesn’t properly interpolate named parameters (I’ve already reported this to the opalbox author). I will update this article whenever the issues have been ironed out.
Runnning the code in IRB
works however:
puts template % {programming_paradigm: "Object Oriented", answer: "Inheritance"}
# Q: What's the Object Oriented way to become wealthy?
# A: Inheritance
The method is also written in C (at least for MRI) so it’s expected to be fast.
puts Benchmark.measure { "Hello %s" % "World" * 6_000_000}
# 0.010000 0.040000 0.050000 ( 0.051013)
In the end the code for the feature I was working on looked like this:
# gem source
def upload_and_notify(filename: nil, attachment: nil, message: "%s")
raise ArgumentError "filename cannot be nil" unless filename
raise ArgumentError "attachment cannot be nil" unless attachment
expiration = (Date.today + 7).to_time
file = @uploader.upload(filename, attachment)
@notifier.notify(text: message % file.url(expiration))
end
# spec file
context "the notification can merge the file url" do
When { expect(notifier).to receive(:notify).with({text: "your report <filename link>"}) }
Then {
expect {
instance.upload_and_notify(filename: 'abc', attachment: '123', message: "your report <%s>")
}.not_to raise_error
}
end
See also:
- http://ruby-doc.org/core-2.2.0/String.html#method-i-25
- http://ruby-doc.org/core-2.2.0/Kernel.html#method-i-sprintf
- http://blog.revathskumar.com/2013/01/ruby-multiple-string-substitution-in-string-template.html
- http://davebaker.me/articles/tip-ruby-string-interpolation-with-hashes
TLDR;
String#%
is a cool and flexible way to store a template in a string so you can defer the string interpolation.