Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide an API to access documentation metadata at compile time #8095

Open
hauleth opened this issue Aug 16, 2018 · 11 comments
Open

Provide an API to access documentation metadata at compile time #8095

hauleth opened this issue Aug 16, 2018 · 11 comments

Comments

@hauleth
Copy link
Contributor

hauleth commented Aug 16, 2018

Environment

  • Elixir & Erlang/OTP versions (elixir --version): 1.7.2
  • Operating system: macOS

Current behavior

I wanted to create macro that would allow me to remove boilerplate over generating structures for events in my application. To document such structures I wanted to use @doc to simulate feeling that these are normal parts of the external module instead of being separate modules, ex.:

defmodule MyApp.Event do
  import Events

  @moduledoc false

  @doc """
  Creation of new submission
  """
  @doc deprecated: "Foo"
  defevent SubmissionCreated,
    id: any(),
    name: String.t(),
    author: [map()]
end

While basic implementation is dumb easy documentation is quite challenging. What I have achieved is:

defmodule Events do
  @moduledoc false

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      docs = Module.get_attribute(__MODULE__, :doc)
      deprecated = case Module.get_attribute(__MODULE__, :deprecated) do
        nil -> []
        value -> [deprecated: value]
      end

      {set, _} = :elixir_module.data_tables(__MODULE__)
      metadata = case :ets.lookup(set, {:doc, :meta}) do
        [{{:doc, :meta}, metadata, _}] -> deprecated ++ Keyword.new(metadata)
        [] -> deprecated
      end

      defmodule unquote(module) do
        if docs, do: Module.put_attribute(__MODULE__, :moduledoc, docs)
        if metadata != [], do: @moduledoc metadata

        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end

      :elixir_module.delete_definition_attributes(__ENV__, nil, nil, nil, nil, nil)
    end
  end
end

Which is fairly ok, except of the part where I need manually get data from ETS for documentation metadata. It cannot be mitigated by using Module.get_attribute/2 as it explicitly requires atom as a second argument while metadata are stored under {:doc, :meta}.

Expected behavior

Somehow allow fetching documentation metadata in macros. This would allow macro writers to utilise that metadata in some way like example above.

@fertapric
Copy link
Member

I would add "at compile time" to the title, because once compiled I think you can access the documentation using Code.fetch_docs/1 :)

@josevalim josevalim changed the title There is no way to programatically access to documentation metadata Provide an API to access documentation metadata at compile time Aug 16, 2018
@josevalim
Copy link
Member

The documentation metadata and related fields is stored as private fields on the table, that's why you can't access it and you shouldn't rely on the compiler internals for this. :)

The other thing is that, even if we provide a way to access metadata, then you would also need a way to remove the documentation because there is no actual function definition. It feels like the best way to go here is to use your own attribute, such as @eventdoc, especially because you won't pretend that you are defining a function when it is actually a module.

@josevalim
Copy link
Member

The proper solution here would be to make @doc and friends an accumulated attribute, so accessing @doc would return all previously set values:

@doc "foo"
@doc bar: 1
@doc #=> [{2, bar: 1}, {1, "foo"}]

This would also allow us to get rid of special cases in both Module and Kernel. The trouble with doing this now is that it will break code that expects @doc to always be a binary (or similar). So we can only do it on Elixir 2.0.

@hauleth
Copy link
Contributor Author

hauleth commented Aug 16, 2018

@josevalim I agree that in this particular situation it makes sense to use custom attribute instead, but imagine something like that:

defmacro defrecord(name, tag \\ nil, kv) do
  quote do
    def unquote(name)(), do: def unquote(name)(values), do: def unquote(name)(record, values), do: end
end

And while you maybe do not want to share most docs between them then currently supported tags makes sense for them (like :since or :deprecated).

@mguimas
Copy link
Contributor

mguimas commented May 9, 2019

@josevalim Instead of this

@doc "foo"
@doc bar: 1
@doc #=> [{2, bar: 1}, {1, "foo"}]

could you consider this

@doc "foo"
@doc bar: 1
@doc #=> [doc: "foo", bar: 1]

?

@josevalim
Copy link
Member

@mguimas unfortunately a metadata can be named :doc, so that would be ambiguous, we will probably use a tuple-based format, such as {"doc", meta: "data"}, or a map: %{doc: "...", metadata: [...]}.

@hauleth
Copy link
Contributor Author

hauleth commented Jun 23, 2020

@josevalim I would vote for the tuple + keyword list so:

@doc "foo:
@doc bar: 1
@doc bar: 2

Would result with:

{"foo", [bar: 1, bar: 2]}

@mguimas
Copy link
Contributor

mguimas commented Jun 23, 2020

@josevalim I believe that a good data structure is one that makes it is easy to use Enum to traverse the information associated with the @doc entries.

So in the example

@doc :foo
@doc bar: 1
@doc bar: 2
@doc :bar

perhaps a good solution is

[:foo, {:bar, 1}, {:bar, 2}, :bar]

which respects not only the contents, but also the order of the doc entries.

@Nezteb
Copy link
Contributor

Nezteb commented Dec 22, 2023

@hauleth @josevalim It's been 3 years; I don't think this issue has been resolved, but I figured I'd check in with both of you to verify. 😄

@wojtekmach
Copy link
Member

It’s not. It’s marked for Elixir v2.0.

@Nezteb
Copy link
Contributor

Nezteb commented Dec 22, 2023

It’s marked for Elixir v2.0.

Ah my bad. I'm on mobile and didn't scroll down to see the addition of the 2.0 milestone. 🤦‍♂️

Disregard me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

7 participants
@josevalim @wojtekmach @hauleth @fertapric @Nezteb @mguimas and others