Skip to content

First class support for collections (heavily inspired by RabbitMQ's use of Cuttlefish) - #73

Merged
lukebakken merged 4 commits into
mainfrom
mk-native-collection-grouped-keys-support
Apr 14, 2026
Merged

lukebakken merged 4 commits into
mainfrom
mk-native-collection-grouped-keys-support

Conversation

@michaelklishin

@michaelklishin michaelklishin commented Apr 13, 2026 •

Copy link
Copy Markdown
Collaborator

A good frakking half of LoCs across RabbitMQ
schema files is taken by collection keys
that look like this:

{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
    {datatype, atom}
]}.

{translation, "rabbit.auth_mechanisms",
fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_mechanisms", Conf),
    Sorted = lists:keysort(1, Settings),
    [V || {_, V} <- Sorted]
end}.

This is repetitive and also highly standardized,
so we can introduce a first class feature that would make the above snippets look like this:

{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
    {datatype, atom},
    {collect, list}
]}.

which is much harder to get wrong.

With this change, the keys with this {collect, list} tuple and fuzzy variables are treated specially
and instead of going over the standard translation, collect_fuzzy_values/7 is used instead.

The following collection "types" are supported:

  • list
  • {map, atom} (keys are converted to atoms)
  • {map, binary} (keys are converted to binaries)
  • {proplist, atom}

{proplist, binary} is not a case used in RabbitMQ, so I figured it can be left out.

There is a good number of more complex collections in RabbitMQ that this feature cannot cover but it can remove some 30-40% of repetitive lines (and β‰ˆ 20% in terms of keys) across our schemas, so it's already a win.

Note that integration tests literally borrow a few real world examples from RabbitMQ.

@michaelklishin michaelklishin added this to the 3.7.0 milestone Apr 13, 2026
@michaelklishin

Copy link
Copy Markdown
Collaborator Author

For those wondering why this cannot be done at the data type level: data types work on individual values, and here we by definition need a collection.

In other words, trying to squeeze this idea into the real of Cuttlefish data types would be much less elegant, not more. I've tried that first.

A good frakking half of LoCs across RabbitMQ
schema files is taken by collection keys
that look like this:

```erl
{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
    {datatype, atom}
]}.

{translation, "rabbit.auth_mechanisms",
fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_mechanisms", Conf),
    Sorted = lists:keysort(1, Settings),
    [V || {_, V} <- Sorted]
end}.
```

This is repetitive and also highly standardized,
so we can introduce a first class feature that would
make the above snippets look like this:

```erl
{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
    {datatype, atom},
    {collect, list}
]}.
```

which is much harder to get wrong.

With this change, the keys with this `{collect, list}` tuple
*and* fuzzy variables are treated specially
and instead of going over the standard translation,
`collect_fuzzy_values/7` is used instead.

The following collection "types" are supported:

 * `list`
 * `{map, atom}` (keys are converted to atoms)
 * `{map, binary}` (keys are converted to binaries)
 * `{proplist, atom}`

`{proplist, binary}` is not a case used in RabbitMQ,
so I figured it can be left out.

There is a good number of more complex collections
in RabbitMQ that this feature cannot cover but it can remove
some 30-40% of repetitive lines across our schemas,
so it's already a win.
@michaelklishin
michaelklishin force-pushed the mk-native-collection-grouped-keys-support branch from 843c271 to 652646c Compare April 13, 2026 20:53
@michaelklishin

Copy link
Copy Markdown
Collaborator Author

The forced push was a rebase on top of main.

@lukebakken lukebakken left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS - this is me and the 🧞 , of course 😸


The implementation is correct and well-tested. 442 tests pass, zero dialyzer warnings. A few items worth discussing before merge.

Behavioral difference: empty collection vs absent key

When no conf values match the wildcard pattern, {collect, list} produces no output - the key is absent from app.config. The equivalent translation pattern (filter_by_prefix + list comprehension) returns [], which cuttlefish writes to app.config as a valid empty list.

The integration tests acknowledge this with rabbit_ssl_versions_absent_assert/1, which accepts both outcomes. But for a drop-in replacement, applications that rely on the key being present with an empty list will behave differently after migration.

Two options:

  1. Keep the current behavior (absent when empty) and document it prominently as a known difference from the translation form.
  2. Change collect_fuzzy_values to write an empty collection ([] for list/{proplist, atom}, #{} for {map, atom}/{map, binary}) when no matches are found, making it a true drop-in replacement.

Which behavior is intended?

{proplist, binary} produces a parse error

parse_collect({proplist, binary}) returns {error, {invalid_collect_type, {proplist, binary}}}. The PR description says this case was deliberately left out, which is fine - but a schema author who tries it will get a parse error. Worth noting explicitly in the description or a code comment so it reads as intentional rather than an oversight.

I've added a REFERENCE.md (comprehensive schema author reference) and a CHANGELOG.md entry for v3.7.0 in a commit on this branch.

@michaelklishin

michaelklishin commented Apr 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Let me look into the empty collection case. I wouldn't mind fixing {proplist, binary}, too, just not entirely sure what cuttlefish can do beyond throwing an error of sorts.

1. Make sure that an empty collection is returned when no keys match
2. `{proplist, binary}` (an unsupported collection type) now
   produces a more descriptive error
@michaelklishin

Copy link
Copy Markdown
Collaborator Author

@lukebakken ready for another round.

@lukebakken
lukebakken merged commit 848e361 into main Apr 14, 2026
6 checks passed
@lukebakken
lukebakken deleted the mk-native-collection-grouped-keys-support branch April 14, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants