Repository navigation
First class support for collections (heavily inspired by RabbitMQ's use of Cuttlefish) - #73
Conversation
|
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.
843c271 to
652646c
Compare
|
The forced push was a rebase on top of |
There was a problem hiding this comment.
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:
- Keep the current behavior (absent when empty) and document it prominently as a known difference from the translation form.
- Change
collect_fuzzy_valuesto write an empty collection ([]forlist/{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.
|
Let me look into the empty collection case. I wouldn't mind fixing |
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
|
@lukebakken ready for another round. |
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 speciallyand instead of going over the standard translation,
collect_fuzzy_values/7is 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.