# `PiiCatalog`
[🔗](https://github.com/signalvaultio/pii_catalog/blob/v0.1.0/lib/pii_catalog.ex#L1)

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.

# `name`

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

A pattern name, as an atom or a string.

# `compile`

```elixir
@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!`

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

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

# `find`

```elixir
@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}]

# `get`

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

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

# `groups`

```elixir
@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`

```elixir
@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`

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

Every pattern name, in catalog order.

# `patterns`

```elixir
@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`

```elixir
@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`

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

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

# `scan`

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

# `valid?`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
