I work on ruby > 3.0 and i need to replace a text content (in-line html text with smartcode). this text can be long and for example it's like this : "Hello, {{viewer_name}} ! How are you ?"
I have a method to replace theses smartcodes :
def populate_smartcodes(content)
content.gsub(/\{{(.*?)\}}/).each do |value|
smartcode = value[/#{Regexp.escape('{{')}(.*?)#{Regexp.escape('}}')}/m, 1]
str_smartcode = "{{#{smartcode}}}"
case smartcode
when 'viewer_name'
content = content.gsub(str_smartcode, viewer.name)
when 'company_city'
content = content.gsub(str_smartcode, company.city )
end
content
end
company_city
and viewer_name
are variables i need to provide with viewer an instance of User::Viewer.
And i have a lot of smartcodes to replace ... I think this is not a good way to do it but it's working.
Can you help me to improve the performance or the way to do it ?
CodePudding user response:
Here you're running a new gsub inside an existing gsub block, which isn't necessary, since the gsub block will substitute each match with the return value of the block anyway. The same result could be achieved simply with 2 lines of code:
content.gsub!(/{{viewer_name}}/, viewer.name)
content.gsub!(/{{company_city}}/, company.city)
...but that is still analyzing the whole string twice. If you know all of the smartcodes beforehand, you could do something like this instead:
def populate_smartcodes(content)
content.gsub(/{{\w*}}/) do |smartcode|
case smartcode
when '{{viewer_name}}'
viewer.name
when '{{company_city}}'
company.city
else
smartcode
end
end
end
This will return a copy of the input string with all instances of {{viewer_name}}
and {{company_city}}
replaced. You could also use gsub!
instead of gsub
if you want to modify the original string in place, so you can call it like populate_smartcodes(original_string)
instead of original_string = populate_smartcodes(original_string)
.
CodePudding user response:
It is possible to extract smartcodes from the template in a much simpler (cleaner?) way. We know for sure, that they always contain 4 parentheses - 2 from each side. So we can just take what gsub
matches, and take 2..-3
chars as the actual binding name:
content = "Hello, {{viewer_name}}! How are you?"
content.gsub!(/{{\w ?)}}/) do |matched_chunk|
matched_chunk[2..-3] # => viewer_name
end
Next, we would like to resolve these matched smartcodes into something meaningful. And we would like to do it dynamically to avoid hardcoding of many-many individual smartcodes, correct?
In your example you resolve viewer_name
into viewer.name
and company_city
into company.city
- it looks like some pattern <receiver>.<property>
. If this is the case, we could use this "generality" to resolve things dynamically without the need to hardcode resolution for each "smartcode":
content.gsub!(/{{\w ?)}}/) do |matched_chunk|
recv, prop = matched_chunk[2..-3].split("_", 2)
instance_eval("#{recv}.#{prop}") # the receiver should be defined in the current scope, obviously
rescue
matched_chunk
end
Well, it could work. Probably :) But it smells as hell:
I limit the split by 2, to ensure we always stick with
receiver.property
pattern. So, something likeuser_first_name
will be resolved asuser.first_name
. But this assumption might be just wrong in general! It might be in fact something likeuser.first.name
, or evenfoo_bar
that should be resolved intosomething_else
- so this solution is fragile at minimum...instance_eval
is a Pandora box, and we should be extremely careful with it. Idealy, We should call it in an isolated, safe context (some blank slate object)
Both of these problems are solvable: we can create a special template builder object as a safe blank late context for our instance_eval
; we could then inject the necessary bindings so that instance_eval could resolve our smartcodes properly (in this case we could even replace instance_eval with safer magic or maybe no magic at all - hardcoding things sometimes is not that bad idea) etc...
But following this rabbit hole, we would sooner or later recreate something like liquid
or mustache
, just way less mature and more error-prone.
So my proposal is don't play around regexes, try to adopt some battle-tested template engine first (and roll back to the custom implementation only if you have really strong reasons) :)