dashbitco / nimble_options

A tiny library for validating and documenting high-level options. 💽
Apache License 2.0
507 stars 38 forks source link

Support `in` with documentation #121

Closed billylanchantin closed 10 months ago

billylanchantin commented 10 months ago

Problem

A list of choices is already supported via {:in, ...}. Unfortunately, there's no easy way to document the choices.

Example:

@pets [:cat, dog]

@schema [
  pet: [
    type: {:in, @pets},
    doc: "Type of pet."
  ]
]

@doc """
## Options

#{NimbleOptions.docs(@schema)}
"""

If you do this, there is no reference in the docs to the actual values the :pet key can assume. If you want to document that, you need to do so manually:

@schema [
  pet: [
    type: {:in, @pets},
    doc: ~s"""
      Type of pet.

      Must be one of:

        * `:cat` - feline
        * `:dog` - canine
      """
  ]
]

This isn't ideal. And it has a habit of growing out of sync with the actual values.

Proposal

We add a struct of some sort that allows choices to self-document. For example:

@pets [
  %NimbleOptions.Choice{
    value: :cat,
    doc: "feline"
  },
  %NimbleOptions.Choice{
    value: :dog,
    doc: "canine"
  },
]

@schema [
  pet: [
    type: {:in, @pets},
    doc: "Type of pet."
  ]
]

@doc """
## Options

#{NimbleOptions.docs(@schema)}
"""

Then if the type is {:in, ...} and the element is a %NimbleOptions.Choice{} (or whatever name makes sense), then the docs builder can add the "Must be one of..." part automatically.

whatyouhide commented 10 months ago

You can basically already do this with just a few more lines of code from what you have above. Something like:

@pets [
  %{value: :cat, doc: "feline"},
  # ...
]

@schema [
  pet: [
    type: {:in, Enum.map(@pets, & &1.value)},
    doc: "Type of pet. Must be one of #{Enum.map_join(@pets, ", ", & &1.doc)}"
  ]
]

In order for this library to stay nimble, I'd say something like the code above is probably the best approach. If we introduce NimbleOptions.Choice, the slope gets pretty slippery and we might start adding a ton of similar things.

Thanks for the proposal anyways! 💟

billylanchantin commented 10 months ago

I hear you on remaining nimble! So no worries is this is still rejected. But do note that your proposed solution is not quite what I was going for.

You'd want something more like:

@schema [
  pet: [
    type: {:in, Enum.map(@pets, & &1.value)},
    doc: """
      Type of pet.

      Must be one of:

      #{Enum.map_join(@pets, "\n\n", &"  * `#{inspect(&1.value)}` - #{&1.doc}")}
      """
  ]
]

But then you start running into indentation issues and it gets messy.

And it also feels like I'm reinventing what ought to be fairly fundamental -- I think I want this literally every time I have a list of atoms as options. If an element of a schema can self-document via :doc, why not an element of an {:in, ...}?

However, again, I do hear you on remaining Nimble. I can definitely make the map_join version work if need be :)

whatyouhide commented 10 months ago

@billylanchantin this sets a "dangerous" precedent though. We don't change documentation for any other type. For example, we don't say something like "must be an integer" if you use :integer as the type. So, this would be a first, and would start what I think would be a slippery slope. I suggest to add helpers to your application that do something like the code you showed above.

Thanks again!

billylanchantin commented 10 months ago

Sure thing! And thanks for the explanation :)