PiiCatalog (pii_catalog v0.1.0)

Copy Markdown View Source

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

Types

A pattern name, as an atom or a string.

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

name()

@type name() :: atom() | String.t()

A pattern name, as an atom or a string.

Functions

compile(patterns_or_names \\ names())

@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.

fetch!(name)

@spec fetch!(name()) :: PiiCatalog.Pattern.t()

The pattern with this name. Raises ArgumentError for an unknown name.

find(scanner, text)

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}]

get(name)

@spec get(name()) :: PiiCatalog.Pattern.t() | nil

The pattern with this name, or nil. Builds only that pattern.

groups()

@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.

in_region(region)

@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.

names()

@spec names() :: [atom(), ...]

Every pattern name, in catalog order.

patterns()

@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.

region_label(region)

@spec region_label(atom()) :: String.t()

A region's display name: region_label(:se) is "Sweden". Raises ArgumentError for a region not in regions/0.

regions()

@spec regions() :: [{atom(), String.t()}, ...]

Every region a pattern is tagged with, in display order, as [{region, label}]: [global: "Global", eu: "European Union", ...].

scan(scanner, text)

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.

valid?(name, value)

@spec valid?(name(), binary()) :: boolean()

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")
false

Raises ArgumentError for an unknown name.