defmodule Jido.Chat.WebhookRequest do
@moduledoc """
Typed webhook request envelope used for adapter verification and parsing.
"""
alias Jido.Chat.Wire
@schema Zoi.struct(
__MODULE__,
%{
adapter_name: Zoi.atom() |> Zoi.nullish(),
method: Zoi.string() |> Zoi.default("POST"),
path: Zoi.string() |> Zoi.nullish(),
headers: Zoi.map() |> Zoi.default(%{}),
payload: Zoi.map() |> Zoi.default(%{}),
query: Zoi.map() |> Zoi.default(%{}),
raw: Zoi.any() |> Zoi.nullish(),
metadata: Zoi.map() |> Zoi.default(%{})
},
coerce: true
)
@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
@doc "Returns the Zoi schema for WebhookRequest."
def schema, do: @schema
@doc "Creates a typed webhook request from raw map or HTTP-style request fields."
@spec new(map()) :: t()
def new(attrs) when is_map(attrs) do
attrs
|> normalize_shape()
|> normalize_headers()
|> then(&Jido.Chat.Schema.parse!(__MODULE__, @schema, &1))
end
@doc "Gets a normalized request header value."
@spec header(t(), String.t()) :: String.t() | nil
def header(%__MODULE__{} = request, key) when is_binary(key) do
request.headers[String.downcase(key)]
end
@doc "Serializes webhook request into plain map with type marker."
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = request) do
request
|> Map.from_struct()
|> Wire.to_plain()
|> Map.put("__type__", "webhook_request")
end
@doc "Builds webhook request from serialized data."
@spec from_map(map()) :: t()
def from_map(map) when is_map(map), do: new(map)
defp normalize_shape(attrs) do
payload = attrs[:payload] || attrs["payload"]
if is_map(payload) do
attrs
else
%{
adapter_name: attrs[:adapter_name] || attrs["adapter_name"],
method: attrs[:method] || attrs["method"] || "POST",
path: attrs[:path] || attrs["path"],
headers: attrs[:headers] || attrs["headers"] || %{},
payload: attrs,
query: attrs[:query] || attrs["query"] || %{},
raw: attrs,
metadata: attrs[:metadata] || attrs["metadata"] || %{}
}
end
end
defp normalize_headers(attrs) do
headers = attrs[:headers] || attrs["headers"] || %{}
normalized =
headers
|> Enum.map(fn {key, value} -> {key |> to_string() |> String.downcase(), value} end)
|> Map.new()
Map.put(attrs, :headers, normalized)
end
end