A compiled set of patterns, ready to scan text. Build one with
PiiCatalog.compile/1 and keep it: compiling every pattern takes about
as long as scanning a few kilobytes of text, which is worth paying once rather
than per call. A scanner is an immutable value, so it can be kept in a process, an ETS
table or :persistent_term. Build it at runtime rather than in a module
attribute: from OTP 28 a compiled regex is a reference, which cannot be
stored at compile time.
How a text is scanned
The regex engine pays a cost at every position of the text for each pass, so scanning patterns one after another costs one such pass each. Most catalog patterns start the same way: no ASCII word character before, then a digit or a capital letter. Those share one pass. Patterns of your own never do: each is scanned in a pass of its own, so it cannot change what the catalog patterns find, and an error from the engine on it names it alone. At each position where such a match can start, each of them is tried as a lookahead with a named capture, and the position is reported only if one of them matched.
For each pattern the result is what Regex.scan/3 gives on its own
regex: matches left to right, none starting inside the previous one. A
pattern scanned in the shared pass matches at a position exactly when its
own regex does, so the only step left is dropping a match that starts
inside the previous one.
A catalog pattern shares the pass when shared?/1 is true: its regex starts with
(?<![A-Za-z0-9_]), has no flags, has no | outside parentheses, and
uses none of \K, a backtracking verb such as (*SKIP) (any (* in
its source), a named group, a backreference (\1 to \9, \g,
\k, (?P=), a subroutine call ((?1), (?+1), (?-1), (?R),
(?&name), (?P>name)) or a condition other than an assertion ((?(1),
(?(R), (?(DEFINE)). Inside the shared regex a verb would act on the
other patterns' positions too, and the others would point at the wrong
group, as group numbers count from the start of the whole regex. In a regex such as (?<![A-Za-z0-9_])ORD\d{4}|INV\d{4}
the lookbehind applies to the first branch only, but in the shared regex
it would apply to both. The rest are scanned on
their own: those starting with a literal (@, +) or a keyword, which
the engine finds quickly, and those with flags or any of the above. A
pattern scanned on its own is skipped when the text lacks what its
requires field names: a literal, or a match for one of a list of
regexes.
A pattern in the shared pass must also begin every match with a digit or a capital letter. The catalog's tests check this for every catalog pattern that shares the pass.
When the regex engine stops
The engine stops a match that takes too many steps and returns an error
(:match_limit). Regex.scan/3 reads that error as no more matches, so
the rest of the text would go unscanned without a sign. The scanner reads
the error and raises PiiCatalog.ScanError instead. A pattern skipped
on a text that lacks its requires is still skipped; a requires regex
that stops with an error counts as present, so the pattern is scanned.
Summary
Functions
The matches of scan/2 that pass their pattern's validator, in the same
order. The validator sees the matched bytes alone. Raises
PiiCatalog.ScanError as scan/2 does.
Compiles a scanner for the given patterns, scanned in the order given.
A second pattern with a name already in the list is dropped. Raises
ArgumentError for a pattern whose validator is neither nil nor one
of PiiCatalog.Validators.names/0.
The patterns a scanner holds, in the order it reports them.
Every candidate match of every pattern in text, before validation.
Grouped by pattern in the scanner's order, and left to right within each.
Raises PiiCatalog.ScanError when the regex engine stops before the end
of the text.
Whether a pattern is scanned in the shared pass. Only catalog patterns
are: a pattern whose name is not in the catalog, or whose regex is not
the catalog's (same source and options), is scanned on its own. Of the
catalog patterns, those whose regex starts with (?<![A-Za-z0-9_]),
has no flags and no | outside parentheses, and has no \K,
backtracking verb, named group, backreference, subroutine call or
condition other than an assertion share the pass.
Types
Functions
@spec find(t(), binary()) :: [PiiCatalog.Match.t()]
The matches of scan/2 that pass their pattern's validator, in the same
order. The validator sees the matched bytes alone. Raises
PiiCatalog.ScanError as scan/2 does.
@spec new([PiiCatalog.Pattern.t()]) :: t()
Compiles a scanner for the given patterns, scanned in the order given.
A second pattern with a name already in the list is dropped. Raises
ArgumentError for a pattern whose validator is neither nil nor one
of PiiCatalog.Validators.names/0.
@spec patterns(t()) :: [PiiCatalog.Pattern.t()]
The patterns a scanner holds, in the order it reports them.
@spec scan(t(), binary()) :: [PiiCatalog.Match.t()]
Every candidate match of every pattern in text, before validation.
Grouped by pattern in the scanner's order, and left to right within each.
Raises PiiCatalog.ScanError when the regex engine stops before the end
of the text.