Home > Software engineering >  Replace smartcodes with gsub
Replace smartcodes with gsub

Time:05-25

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 like user_first_name will be resolved as user.first_name. But this assumption might be just wrong in general! It might be in fact something like user.first.name, or even foo_bar that should be resolved into something_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) :)

  • Related