A catalog of personal identifiers, each a regex plus, where the identifier has one, a check-digit or structure validator, and a scanner that finds them in a text, most of them in one shared regex pass.
iex> scanner = PiiCatalog.compile()
iex> PiiCatalog.find(scanner, "Pay to DE89 3704 0044 0532 0130 00 by Friday")
[%PiiCatalog.Match{name: :iban, value: "DE89 3704 0044 0532 0130 00", offset: 7, length: 27}]No processes, no state and no runtime dependencies: nothing is started,
cached or logged. Three validators read today's date (see
PiiCatalog.Validators), so their answer for a value can change over
time. Compile a scanner once with compile/1 and keep it; scan/2 and
find/2 take it as their first argument.
Pattern names are atoms (:iban, :personnummer_se) and never change
once released. Every function that takes a name also takes it as a
string, without creating atoms.
Summary
Functions
Compiles a scanner. Takes pattern names, PiiCatalog.Pattern structs or
a mix; with no argument, every pattern. Matches are reported grouped by
pattern in the order given. Raises ArgumentError for an unknown name.
The pattern with this name. Raises ArgumentError for an unknown name.
The matches in text that pass their pattern's validator, in the order
of scan/2. A pattern without a validator keeps every match.
The pattern with this name, or nil. Builds only that pattern.
The patterns grouped by the region they are listed under (the first of
their regions), in display order: [{region, label, [pattern]}]. Each
pattern appears once.
The patterns tagged with a region, in catalog order. :eu includes the
identifiers listed under another group that are used in the EU, such as
:iban (listed under :global) and :personnummer_se (under
:nordics). Raises ArgumentError for a region not in regions/0.
Every pattern name, in catalog order.
Every pattern, in catalog order, with its regex compiled.
A region's display name: region_label(:se) is "Sweden". Raises
ArgumentError for a region not in regions/0.
Every region a pattern is tagged with, in display order, as
[{region, label}]: [global: "Global", eu: "European Union", ...].
Every candidate match in text: values of the right shape, before any
validator runs. Grouped by pattern in the scanner's order, and left to
right within each pattern, with no match starting inside the previous
match of the same pattern. Matches of different patterns may overlap.
Whether a value passes the validator of the named pattern, taken as the
whole of a match. It does not check the format: that is the regex's job,
and a pattern without a validator (:email, :phone_intl,
:date_of_birth) accepts every value. To check text, use find/2.
Types
Functions
@spec compile([name() | PiiCatalog.Pattern.t()]) :: PiiCatalog.Scanner.t()
Compiles a scanner. Takes pattern names, PiiCatalog.Pattern structs or
a mix; with no argument, every pattern. Matches are reported grouped by
pattern in the order given. Raises ArgumentError for an unknown name.
PiiCatalog.compile([:email, :iban, :phone_intl])
PiiCatalog.compile(PiiCatalog.in_region(:nordics))A struct is taken as it is, so a pattern of your own can be scanned too;
its validator must be nil or one of PiiCatalog.Validators.names/0
(ArgumentError otherwise), and see PiiCatalog.Scanner for what a regex in the shared pass must
meet.
@spec fetch!(name()) :: PiiCatalog.Pattern.t()
The pattern with this name. Raises ArgumentError for an unknown name.
@spec find(PiiCatalog.Scanner.t(), binary()) :: [PiiCatalog.Match.t()]
The matches in text that pass their pattern's validator, in the order
of scan/2. A pattern without a validator keeps every match.
Raises PiiCatalog.ScanError, with the patterns being scanned and the
engine's reason, when the regex engine gives up before the end of the
text: at its match or recursion limit. The result is never partial: the
whole text was scanned and every match is returned, or the call raises.
The caller decides what to do with a text that raised, such as refusing
it or redacting it whole. scan/2 raises the same way.
iex> scanner = PiiCatalog.compile([:credit_card])
iex> PiiCatalog.find(scanner, "Card 4111 1111 1111 1111, not 4111 1111 1111 1112")
[%PiiCatalog.Match{name: :credit_card, value: "4111 1111 1111 1111", offset: 5, length: 19}]
@spec get(name()) :: PiiCatalog.Pattern.t() | nil
The pattern with this name, or nil. Builds only that pattern.
@spec groups() :: [{atom(), String.t(), [PiiCatalog.Pattern.t(), ...]}]
The patterns grouped by the region they are listed under (the first of
their regions), in display order: [{region, label, [pattern]}]. Each
pattern appears once.
@spec in_region(atom()) :: [PiiCatalog.Pattern.t()]
The patterns tagged with a region, in catalog order. :eu includes the
identifiers listed under another group that are used in the EU, such as
:iban (listed under :global) and :personnummer_se (under
:nordics). Raises ArgumentError for a region not in regions/0.
@spec names() :: [atom(), ...]
Every pattern name, in catalog order.
@spec patterns() :: [PiiCatalog.Pattern.t()]
Every pattern, in catalog order, with its regex compiled.
Builds the list on each call: compile a scanner once rather than calling
this for every scan. From OTP 28 a compiled regex is a reference, so
patterns from two calls are not ==; compare them by name.
A region's display name: region_label(:se) is "Sweden". Raises
ArgumentError for a region not in regions/0.
Every region a pattern is tagged with, in display order, as
[{region, label}]: [global: "Global", eu: "European Union", ...].
@spec scan(PiiCatalog.Scanner.t(), binary()) :: [PiiCatalog.Match.t()]
Every candidate match in text: values of the right shape, before any
validator runs. Grouped by pattern in the scanner's order, and left to
right within each pattern, with no match starting inside the previous
match of the same pattern. Matches of different patterns may overlap.
Each pattern gives what Regex.scan/3 on its own regex gives, though
most patterns share one pass over the text, except that an error from
the regex engine raises PiiCatalog.ScanError (see find/2) where
Regex.scan/3 would return the matches before it as if the text ended
there.
Whether a value passes the validator of the named pattern, taken as the
whole of a match. It does not check the format: that is the regex's job,
and a pattern without a validator (:email, :phone_intl,
:date_of_birth) accepts every value. To check text, use find/2.
iex> PiiCatalog.valid?(:iban, "DE89 3704 0044 0532 0130 00")
true
iex> PiiCatalog.valid?(:iban, "DE88 3704 0044 0532 0130 00")
falseRaises ArgumentError for an unknown name.