Application of Regex in my message templating webapp

Last year, I started building a messaging system/webapp which generates dynamic text messages with personalized content which helps Veterinarian clinics send appointments or announcement to their clients at a given time.

The project has released into production in last month (2023 Jan), I'd like to take note of my implementation of Regex in a real-life project.

Introduction

Template

Template is an entity within the system which is defined as a blueprint for a message and it includes placeholders, for dynamic data that can be filled in at runtime.

For example, a template may look like this

template = "Hello <ClientName>. This is <ClinicName>. We wanted to follow-up with you to confirm <PatientName>'s appointment on tomorrow. Please call <LocationPhone> if you need to reschedule.\n"

And the actual message we want receiver (client) to see:

actual message content

Implementation

To replace every placeholder with desired values, we rely on regular expression. Here is our implementation:

import re

class Renderer:
    """Class to implement regex."""

    def __init__(self, placeholder_to_value_map: dict):
        self.placeholder_to_value_map = placeholder_to_value_map
        self.re_pattern: re.Pattern = self.build_pattern()

    def build_pattern(self) -> re.Pattern:
        """Return regex pattern object."""
        return re.compile(
            "|".join(map(re.escape, self.placeholder_to_value_map))
        )

    def repl_placeholders(self, match_obj: re.match) -> str:
        """Return actual values if match."""
        return self.placeholder_to_value_map[match_obj.group(0)]

    def render(self, template: str) -> str:
        """Replace placeholders with actual values."""
        return self.re_pattern.sub(self.repl_placeholders, template)


if __name__ == "__main__":

    template = "Hello <ClientName>. This is <ClinicName>."\
                " We wanted to follow-up with "\
                "you to confirm <PatientName>'s appointment"\
                " on tomorrow. Please call"\
                "<ClinicPhoneNumber> if you need to reschedule.\n"
    placeholder_to_value_map = {
        "<ClientName>": "Michael",
         "<ClinicName>": "Saritasa",
        "<PatientName>": "Luca",
        "<ClinicPhoneNumber>":  "123456789"
    }
    renderer = Renderer(
        placeholder_to_value_map=placeholder_to_value_map,
    )
    msg = renderer.render(template)
    print(msg)

Output

hunghoang@localhost project4 % python re_revise.py
Hello Michael. This is Saritasa. We wanted to follow-up with you
to confirm Luca's appointment on tomorrow.
Please call123456789 if you need to reschedule.
  • Firstly, we prepare a placeholder-to-real-value map called placeholder_to_value_map.

  • Then, we construct re_pattern which helps us do matching and substitution later on. Operator | (A | B) features in this pattern to tell that it will match either A or B.

  • Thirdly, we will make substitution by calling .sub(self.repl_placeholders, template). In arguments, we have a callable repl_placeholders which returns replacement strings so that re_pattern substitutes placeholders present in template for them. This method is called for every occurrence of pattern. Let's say we have 4 matches. repl_placeholders should be executed 4 times.

Let's try to print it out to see if it's true

...
class Renderer:
    """Class to implement regex."""
    time = 0
...
    def repl_placeholders(self, match_obj: re.match) -> str:
        """Return actual values if match."""
        self.time += 1
        print(f"run {self.time}")
        return self.placeholder_to_value_map[match_obj.group(0)]
...

Result as we expected :

hunghoang@localhost project4 % python re_revise.py
run 1
run 2
run 3
run 4
Hello Michael. This is Saritasa. We wanted to follow-up with you to confirm Luca's appointment on tomorrow. Please call123456789 if you need to reschedule.

Conclusion

A few key takeaways:

  • Special character |: A|B, where A and B can be arbitrary REs, creates a regular expression that will match either A or B. An arbitrary number of REs can be separated by the '|' in this way.
  • re.compile(pattern) function: Compile a regular expression pattern into a regular expression object, which can be used for matching using its match(), search() and other methods.
  • Pattern.sub(repl, string, count=0) function: replace compiled pattern in string by repl callable