I’m working on a small library (GitHub - fuelen/mold: A tiny, zero-dependency parsing library for external payloads · GitHub) where schemas are plain data. Type options live in the 2nd element of the tuple: {:string, min_length: 2, max_length: 50}.I’d like to officially support custom keys here, so companion libraries can extend these opts.
Here’s what the typespec for :string looks like today:
@type string_type() ::
{:string,
trim: boolean(),
nilable: boolean(),
default: default(),
format: Regex.t(),
min_length: non_neg_integer(),
max_length: non_neg_integer(),
in: Enumerable.t(),
transform: transform(),
validate: validate()}
| :string
This renders well in docs. I didn’t even factor the common opts (nilable, default, transform, validate) into a shared type, because then I’d have to write:
{:string, [
{:trim, boolean()} |
{:format, Regex.t()} |
{:min_length, non_neg_integer()} |
{:max_length, non_neg_integer()} | shared_option()
]}
It renders not as nice as the inlined keyword typespec, but I’m fine with it if needed.
Technically, I can already write
{:string, min_length: 2, my_custom_opt_for_another_library: :something}
and the library swallows unknown options. But the typespec says you can’t add custom options.
Here are the options I’m considering:
Option 1. Open keyword
@type string_type() ::
{:string,
[
{:trim, boolean()}
| {:format, Regex.t()}
| {:min_length, non_neg_integer()}
| ...
| {atom(), any()} # <-- custom option
]}
| :string
In this case, the list of known opts reads more like a hint than a contract.
Probably, it would be interesting to have an ability to write something like
{custom_option_name :: atom(), any()} when custom_option_name not in [:trim, :format, :min_length, ...]
but it actually means I want a map with the syntax of keyword list ![]()
Option 2. Explicit :ext namespace
{:string, min_length: 2, ext: [some_ext_opt: ...]}
Verbose, but core opts stay strictly typed. Everything inside :ext is keyword() and available to extensions. But feels a bit artificial, like a pattern grabbed from other programming languages where type system doesn’t allow anything else.
Option 3. Just keyword()
Give up on typing opts and simply document allowed keys in @typedoc.
I think the first option is a good mix between 2nd and 3rd.
What would you pick? Is there an idiomatic shape at all?






















