Skip to content

Custom Patterns and Name Patterns

daniele frisanco edited this page Jul 5, 2026 · 1 revision

Custom Patterns & Name Patterns

The 89 built-ins can't know your org's internal ID formats or your customers' names. Two APIs close that gap: add_pattern (any regex) and name_pattern (a generated regex for a person's name across all its written variations).

Custom patterns — add_pattern

Register at boot, or at runtime from any thread (registration is thread-safe):

# String (POSIX ERE) or Regexp — both accepted
DataRedactor.add_pattern(name: "employee_id", regex: "EMP-[0-9]{6}")
DataRedactor.add_pattern(name: "ticket_ref",  regex: /TICKET-[A-Z]{2}[0-9]{4}/, boundary: true)

# :custom by default; pass any built-in tag to group differently
DataRedactor.add_pattern(name: "internal_key", regex: "INT-[A-Z]{3}", tag: :credentials)

DataRedactor.redact(text)                                  # all patterns incl. custom
DataRedactor.redact(text, only: [:custom])                 # only your patterns
DataRedactor.redact(text, only: [:custom, :credentials])   # mix

DataRedactor.custom_patterns   # => [{name:, source:, tag:, boundary:}, ...]
DataRedactor.remove_pattern("employee_id")
DataRedactor.clear_custom_patterns!                        # mostly for test suites

Regex rules (POSIX ERE)

Custom patterns use the same POSIX ERE engine as the built-ins. Not supported — and rejected at registration with DataRedactor::InvalidPatternError, never at redaction time:

Don't use Use instead
\d [0-9]
\s [[:space:]]
\w [[:alnum:]_]
\b, lookahead/behind, non-greedy, named groups — (unsupported)

boundary: true

Wraps the pattern with (^|[^0-9A-Za-z])(PATTERN)([^0-9A-Za-z]|$) so it only fires when the token isn't embedded in a longer alphanumeric string. Incompatible with patterns that contain capture groups. Use it for generic digit/alphanumeric formats (the same rule the built-ins follow — see Pattern Catalogue and C Engine Internals).

Name patterns — name_pattern

Personal names can't ship as built-ins, but the regex to match a name across its written variations is the same every time. name_pattern generates that regex, ready for add_pattern:

DataRedactor.add_pattern(
  name:  "person_mario_rossi",
  regex: DataRedactor.name_pattern("Mario", "Rossi"),
  tag:   :contact
)

DataRedactor.redact("ticket from Mario Rossi about ...")
# => "ticket from [REDACTED] about ..."

One generated pattern matches all of these:

Variation Matches
Case Mario Rossi, mario rossi, MARIO ROSSI
Order Mario Rossi, Rossi Mario, Rossi, Mario, Rossi,Mario
Initials M. Rossi, M Rossi, Mario R., M.R., MR
Diacritics name_pattern("Jose", "Munoz") also matches José Muñoz (and vice versa)
Separators spaces/hyphens interchangeable — Anne-Marie, Anne Marie, AnneMarie; each half alone (Anne Berg, Marie Berg); multi-word parts like "Van der Berg" tolerate any separator

Add a middle name — both the no-middle and with-middle forms match:

DataRedactor.name_pattern("Mario", "Rossi", middle: "Luigi")
# matches "Mario Rossi" AND "Mario Luigi Rossi" AND "Rossi Mario Luigi"

⚠️ Register with the default boundary: false

The returned string is already boundary-wrapped — it won't fire inside a longer word (Mario won't match inside Mariolino). So register it with the default boundary: false. Passing boundary: true would double-wrap and be rejected because of the baked-in capture groups.

Runnable examples

See also

Clone this wiki locally