Skip to content

Allow defining operations by module attributes #76

Closed
@hauleth

Description

@hauleth

Example implementation that I have used in my project (still lacks a little bit options, but overall idea can be seen):

defmodule KokonWeb.Rest do
  alias OpenApiSpex.Operation

  defmacro __using__(_opts) do
    quote do
      alias KokonWeb.Rest.Schema
      alias OpenApiSpex.Operation

      plug(OpenApiSpex.Plug.Cast)
      plug(OpenApiSpex.Plug.Validate)

      @on_definition KokonWeb.Rest
      @before_compile KokonWeb.Rest

      Module.register_attribute(__MODULE__, :parameter, accumulate: true)
      Module.register_attribute(__MODULE__, :response, accumulate: true)

      Module.register_attribute(__MODULE__, :open_api_operations, accumulate: true)
    end
  end

  def __on_definition__(_env, _type, :open_api_operation, _args, _guards, _body), do: nil
  def __on_definition__(_env, _type, :action, _args, _guards, _body), do: nil

  def __on_definition__(%Macro.Env{module: mod}, :def, name, _args, _guards, _body) do
    parameters = Module.delete_attribute(mod, :parameter)
    response = Module.delete_attribute(mod, :response)
    {summary, doc} = docs(Module.get_attribute(mod, :doc))

    operation =
      %Operation{
        summary: summary || "TODO",
        description: doc,
        operationId: module_name(mod) <> ".#{name}",
        parameters: parameters,
        responses: Map.new(response)
      }

    Module.put_attribute(mod, :open_api_operations, {name, operation})
  end

  def __on_definition__(_env, _type, _name, _args, _guards, _body), do: nil

  defmacro __before_compile__(_env) do
    quote unquote: false do
      for {name, operation} <- Module.delete_attribute(__MODULE__, :open_api_operations) do
        def open_api_operation(unquote(name)), do: unquote(Macro.escape(operation))
      end
    end
  end

  defp docs(nil), do: {nil, nil}

  defp docs({_, doc}) do
    [summary | _] = String.split(doc, ~r/\n\s*\n/, parts: 2)

    {summary, doc}
  end

  defp module_name(mod) when is_atom(mod), do: module_name(Atom.to_string(mod))
  defp module_name("Elixir." <> name), do: name
  defp module_name(name) when is_binary(name), do: name
end

This causes controller to look like this:

defmodule KokonWeb.Rest.Controllers.Schedule do
  use KokonWeb.Rest

  @doc "List schedule"
  @response {200, Operation.response(
    "Submissions",
    "application/json",
    KokonWeb.Rest.Schema.Submissions
  )}
  def index(conn, _params) do
    {:ok, submissions} = Kokon.Submissions.all()

    json(conn, submissions)
  end

  @doc "Create new submission"
  @parameter Operation.parameter(:title, :query, :string, "Submission title",
    required: true
  )
  @parameter Operation.parameter(
    :abstract,
    :query,
    :string,
    "Submission description",
    required: true
  )
  @response {200, Operation.response(
    "Submissions",
    "application/json",
    KokonWeb.Rest.Schema.Submission
  )}
  def create(conn, %{title: title, abstract: abstract}) do
    with {:ok, submission} <-
      Kokon.Submissions.create(%{title: title, abstract: abstract}) do
      json(conn, submission)
    end
  end
end

I am opening this as an issue instead of PR as I would like to know opinions about this beforehand.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions