dashbitco / nimble_options

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

Error when using 3-element tuples in :arguments option for schema validation #138

Closed lucazulian closed 1 week ago

lucazulian commented 1 week ago

Whenever I try to pass a 3-element tuple as an argument in the configuration schema, I encounter the following error:

** (NimbleOptions.ValidationError) invalid list in :arguments option: invalid tuple in list element at position 0: invalid value for tuple element at position 0: expected string, got: :{}
    (nimble_options 1.1.1) lib/nimble_options.ex:359: NimbleOptions.validate!/2
    expanding macro: Consumer.__using__/1
    nimble_options_tuple.exs:31: ConsumerTest (module)
    (elixir 1.17.3) expanding macro: Kernel.use/2
    nimble_options_tuple.exs:31: ConsumerTest (module)

Code usefull to reproduce the issue

Mix.install([
  {:nimble_options, "~> 1.1"}
])

defmodule Consumer do
  @config_schema NimbleOptions.new!(
                   arguments: [
                     doc: "Optional queue arguments",
                     type: {:list, {:tuple, [:string, :atom, :any]}},
                     default: []
                   ]
                 )

  def config_schema, do: @config_schema

  defmacro __using__(args) do
    # credo:disable-for-next-line
    options = NimbleOptions.validate!(args, Consumer.config_schema())

    quote bind_quoted: [options: options] do
      # credo:disable-for-next-line
      Consumer.start_link(__MODULE__, options)
    end
  end

  def start_link(_consumer, _options) do
  end
end

defmodule ConsumerTest do
  use Consumer, arguments: [{"x-consumer-argument", :bool, true}]
end

Expected Behavior

The tuple provided in arguments: [{"x-consumer-argument", :bool, true}] should be validated correctly based on the schema, which specifies a tuple with types [:string, :atom, :any].

Actual Behavior

NimbleOptions raises an error that indicates the tuple is invalid, specifically for the first element, which expects a string but fails when provided with a valid string because it receives an AST representation.

Additional Context

If you think this is a valuable issue, I have a possible working solution.

whatyouhide commented 1 week ago

This is not a nimble_options issue. You are validating the options at compile time, not at runtime, so what is getting passed to NimbleOptions.validate!/2 there is

iex(1)> quote do: [{"x-consumer-argument", :bool, true}]
[{:{}, [], ["x-consumer-argument", :bool, true]}]

Hence the error. Validate the options after unquoting them:

quote bind_quoted: [options: options]
  options = NimbleOptions.validate!(...)
  Consumer.start_link(...)
end
lucazulian commented 1 week ago

This is not a nimble_options issue. You are validating the options at compile time, not at runtime, so what is getting passed to NimbleOptions.validate!/2 there is

iex(1)> quote do: [{"x-consumer-argument", :bool, true}]
[{:{}, [], ["x-consumer-argument", :bool, true]}]

Hence the error. Validate the options after unquoting them:

quote bind_quoted: [options: options]
  options = NimbleOptions.validate!(...)
  Consumer.start_link(...)
end

Hi @whatyouhide, thank you for you response. I see, and the compile-time validation was needed. I consider this a nimble_options issue because I wonder why it handles all the other tuples correctly and not that specific one, knowing that it's an elixir special case.

whatyouhide commented 1 week ago

@lucazulian it's not an Elixir special case. You are treating the AST (representation of the code) as the data structure you want to validate.

start_link/1 in your quoted code inside __using__ likely won't work anyway, because you'd start a process at compile time (which is when __using__ is invoked) and not at runtime. My guess is that you'd want something like:

defmacro __using__(opts) do
  quote bind_quoted: [opts: opts], unquote: false do
    # opts πŸ‘‡ here is injected as AST so `NimbleOptions.validate!` will
    # receive the value itself, not the representation of the code
    val_opts = NimbleOptions.validate!(opts, Consumer.config_schema())

    def start_link do
      Consumer.start_link(unquote(val_opts))
    end
  end
end

Validation still happens at compile-time this way.

lucazulian commented 1 week ago

@lucazulian it's not an Elixir special case. You are treating the AST (representation of the code) as the data structure you want to validate.

start_link/1 in your quoted code inside __using__ likely won't work anyway, because you'd start a process at compile time (which is when __using__ is invoked) and not at runtime. My guess is that you'd want something like:

defmacro __using__(opts) do
  quote bind_quoted: [opts: opts], unquote: false do
    # opts πŸ‘‡ here is injected as AST so `NimbleOptions.validate!` will
    # receive the value itself, not the representation of the code
    val_opts = NimbleOptions.validate!(opts, Consumer.config_schema())

    def start_link do
      Consumer.start_link(unquote(val_opts))
    end
  end
end

Validation still happens at compile-time this way.

Thanks for the clarification!