phcurado / parameter

Schema creation, validation, serialization and deserialization for input data
https://hexdocs.pm/parameter
Apache License 2.0
57 stars 1 forks source link

add validate/2, load/2, and dump/2 function generation to nested schema modules #63

Closed punchcafe closed 8 months ago

punchcafe commented 8 months ago

Hello!

This is a Pull Request to add some small Quality of Life improvements to the nested schema module generation.

phcurado commented 8 months ago

@punchcafe thanks for the PR. Could you elaborate what problem you are facing that requires to have these functions on the nested module? The PR looks good tho, needs formatting which was flagged on the CI.

punchcafe commented 8 months ago

Thank you! I've updated that now, so hopefully it should be green!

phcurado commented 8 months ago

thanks! Could you elaborate what problem you are facing that requires to have these functions on the nested module? I just want to understand your problem and see if this is the best solution.

punchcafe commented 8 months ago

Sure! It was mostly just a matter of ergonomics. Having to call the main param each time with the Module felt a bit verbose, so we were wondering if we could make it more succinct by using the GenServer kind of approach.

phcurado commented 8 months ago

I don't see much gain on using

MyParam.load(%{})
# instead of 
Parameter.load(MyParam, %{})

I think the second approach is more explicit that we are using the parameter library to load parameters from your module. The first approach we are injecting a function which will be hidden in the nested macro. Also when you mention the GenServer, seems Parameter is consistent with their implementation: https://hexdocs.pm/elixir/1.12/GenServer.html Example:

{:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)
GenServer.call(MyStack, :pop)

So on genserver, same as parameter, you have to use the module for calling the module that implements the behaviour. The documentation also mention that we can wrap the gen server functions (example in the Client/Server API). I would recommend the same for parameter:

defmodule MyParam do
  use Parameter.Schema

  param do
    field :my_param, :string
  end

  def load(inputs, opts \\ []) do
    Parameter.load(__MODULE__, inputs, opts)
  end
end

Now if you still want that all your param modules to have these injected functions, I recommend doing the following:

defmodule MyApp.Param do
  defmacro param(module, block) do
    quote do
      import Parameter.Schema, except: [param: 2]

      Parameter.Schema.param unquote(module) do
        unquote(block)

        def load(params, opts \\ []) do
          Parameter.load(__MODULE__, params, opts)
        end

        def dump(struct) do
          Parameter.dump(__MODULE__, struct)
        end
      end
    end
  end
end

## Then you can use your own parameter version in your modules
defmodule MyApp.UserController do
  import MyApp.Param

  param UserParam do
    field :first_name, :string
  end
end

Now if we test this out:

iex> MyApp.UserController.UserParam.load(%{"first_name" => "Name"})
...> {:ok, %{first_name: "Name"}}

This way you can also extend the param/1 macro to inject these functions instead of only the nested part param/2.

For me this approach is more clear that you are injecting these functions into your nested module, you can even extend to offer more capabilities like forcing every load to be a struct and so on.

punchcafe commented 8 months ago

Thanks for your feedback. I see your concerns about adding this as it brings in implicit function declarations.

I appreciate the solutions you've offered above but these don't really meet our use case, as our priority is to be able to declaratively define request schemas with minimal boiler plate and no extra internal macros.

Thank you for your time!