Skip to main content

lib/rpc_elixir/codegen/shared.ex

defmodule RpcElixir.Codegen.Shared do
  @moduledoc false
  # Small helpers shared across the codegen submodules: naming-key construction,
  # property-key emission, IR unwrapping, PaginatedResponse detection, and
  # custom `ts_type/0` resolution.

  @ts_identifier ~r/^[a-zA-Z_$][a-zA-Z0-9_$]*$/

  def proc_base_key(proc), do: "#{inspect(proc.handler_mod)}.#{proc.handler_fun}"

  @doc """
  Error `code`s contributed by a procedure's middleware chain, in attach order
  and de-duplicated. A middleware opts in by implementing
  `c:RpcElixir.Middleware.rpc_error_codes/1`; one that does not implement it contributes none.

  These are folded into the procedure's generated error type and its runtime
  `.isError` codes so cross-cutting errors (e.g. `:unauthorized` from an auth
  middleware) are visible to the client without being repeated in handler specs.
  """
  def middleware_error_codes(%{middleware: middleware}) do
    middleware
    |> Enum.flat_map(fn {mod, opts} ->
      if function_exported?(mod, :rpc_error_codes, 1), do: mod.rpc_error_codes(opts), else: []
    end)
    |> Enum.uniq()
  end

  def middleware_error_codes(_proc), do: []

  def require_name!(name_map, key) do
    case Map.fetch(name_map, key) do
      {:ok, name} -> name
      :error -> raise "Procedure references unknown $defs key: #{key}"
    end
  end

  def emit_prop_key(name) do
    if Regex.match?(@ts_identifier, name), do: name, else: JSON.encode!(name)
  end

  def unwrap_optional(%{kind: "optional", inner: inner}), do: {inner, true}
  def unwrap_optional(ir), do: {ir, false}

  def last_segment(module), do: module |> Module.split() |> List.last()

  def sanitize_doc(text), do: String.replace(text, "*/", "* /")

  def paginated_item_type(%{
        items: %{kind: "list", inner: inner},
        next_cursor: %{kind: "nullable", inner: %{kind: "primitive", type: "string"}},
        has_more: %{kind: "primitive", type: "boolean"}
      }),
      do: {:ok, inner}

  def paginated_item_type(_), do: :not_paginated

  def custom_ts_type(mod) do
    Code.ensure_compiled(mod)
    if function_exported?(mod, :ts_type, 0), do: validate_ts_type!(mod, mod.ts_type()), else: nil
  end

  defp validate_ts_type!(mod, name) when is_binary(name) do
    if Regex.match?(@ts_identifier, name) do
      name
    else
      raise "Codegen: #{inspect(mod)}.ts_type/0 returned #{inspect(name)}, which is not a " <>
              "valid TypeScript identifier (expected something like \"Int64String\")."
    end
  end

  defp validate_ts_type!(mod, other) do
    raise "Codegen: #{inspect(mod)}.ts_type/0 must return a String, got #{inspect(other)}."
  end
end