Packages

Generic Elixir models and portable validation for RelayMark documents and manifests

Current section

Files

Jump to
relay_mark_elixir lib relay_mark agent_answer.ex
Raw

lib/relay_mark/agent_answer.ex

defmodule RelayMark.AgentAnswer do
@moduledoc """
Portable validation for one transported Agent Answer document and manifest.
The returned digests bind the exact accepted UTF-8 JSON bytes. They are host
envelope metadata and are never inserted into the RelayMark document.
"""
alias RelayMark.{
Diagnostic,
Document,
JSON,
Manifest,
ManifestProjection,
Schema,
SemanticValidator
}
@profile "relaymark.agent_answer.v1"
@type validated_pair :: %{
document: Document.t(),
manifest: Manifest.t(),
document_digest: String.t(),
manifest_digest: String.t()
}
@spec validate_transported_pair(binary(), binary(), map()) ::
{:ok, validated_pair()} | {:error, [Diagnostic.t()]}
def validate_transported_pair(document_json, manifest_json, validation_context \\ %{})
when is_binary(document_json) and is_binary(manifest_json) and
is_map(validation_context) do
with {:ok, document} <- decode_json(document_json, "document"),
{:ok, manifest} <- decode_json(manifest_json, "manifest") do
diagnostics =
case Schema.validate_document_schema(document) do
{:error, _schema_error} ->
[
Diagnostic.error(
"relaymark.schema.invalid",
"Document does not conform to the RelayMark document schema."
)
]
:ok ->
if get_in(document, ["frontmatter", "profile"]) == @profile do
SemanticValidator.validate(document, validation_context)
else
[
Diagnostic.error(
"relaymark.agent_answer.profile",
"Transported Agent Answer document must declare frontmatter.profile relaymark.agent_answer.v1."
)
]
end
end
diagnostics =
if diagnostics == [] and ManifestProjection.project(document, []) != manifest do
[
Diagnostic.error(
"relaymark.agent_answer.manifest_mismatch",
"Transported relaymark.agent_answer.v1 manifest must exactly match the canonical projection of its document."
)
]
else
diagnostics
end
if diagnostics == [] do
{:ok,
%{
document: Document.from_map(document),
manifest: Manifest.from_map(manifest),
document_digest: digest(document_json),
manifest_digest: digest(manifest_json)
}}
else
{:error, diagnostics}
end
end
end
defp decode_json(json, label) do
case JSON.decode(json) do
{:ok, value} when is_map(value) ->
{:ok, value}
_error ->
{:error,
[
Diagnostic.error(
"relaymark.transport.#{label}_json",
"Transported RelayMark #{label} must be valid UTF-8 JSON."
)
]}
end
end
defp digest(bytes) do
"sha256:" <> (:crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower))
end
end