diff --git a/lib/a2a/json.ex b/lib/a2a/json.ex index b83b2ae..8680be0 100644 --- a/lib/a2a/json.ex +++ b/lib/a2a/json.ex @@ -259,19 +259,20 @@ defmodule A2A.JSON do - `:signatures` — list of JWS signature maps (each `%{"protected" => ..., "signature" => ..., "header" => ...}`) """ - @spec encode_agent_card(A2A.Agent.card(), keyword()) :: map() + @spec encode_agent_card(A2A.AgentCard.t(), keyword()) :: map() def encode_agent_card(card, opts \\ []) do url = Keyword.fetch!(opts, :url) - capabilities = Keyword.get(opts, :capabilities, %{}) - input_modes = Keyword.get(opts, :default_input_modes, ["text/plain"]) - output_modes = Keyword.get(opts, :default_output_modes, ["text/plain"]) + capabilities = card_field(opts, card, :capabilities, %{}) + input_modes = card_field(opts, card, :default_input_modes, ["text/plain"]) + output_modes = card_field(opts, card, :default_output_modes, ["text/plain"]) interfaces = Keyword.get(opts, :supported_interfaces) || + non_empty(Map.get(card, :supported_interfaces)) || [%{url: url, protocol_binding: "JSONRPC", protocol_version: "2.0"}] - security_schemes = Keyword.get(opts, :security_schemes, %{}) - security = Keyword.get(opts, :security, []) + security_schemes = card_field(opts, card, :security_schemes, %{}) + security = card_field(opts, card, :security, []) skills = Enum.map(card.skills, fn skill -> @@ -296,16 +297,29 @@ defmodule A2A.JSON do "defaultOutputModes" => output_modes, "supportedInterfaces" => encode_interfaces(interfaces) } - |> put_unless_nil("provider", encode_provider(Keyword.get(opts, :provider))) - |> put_unless_nil("documentationUrl", Keyword.get(opts, :documentation_url)) - |> put_unless_nil("iconUrl", Keyword.get(opts, :icon_url)) + |> put_unless_nil("provider", encode_provider(card_field(opts, card, :provider, nil))) + |> put_unless_nil("documentationUrl", card_field(opts, card, :documentation_url, nil)) + |> put_unless_nil("iconUrl", card_field(opts, card, :icon_url, nil)) |> put_unless_empty("securitySchemes", encode_security_schemes(security_schemes)) |> put_unless_empty("security", security) - |> put_unless_empty("signatures", Keyword.get(opts, :signatures, [])) + |> put_unless_empty("signatures", card_field(opts, card, :signatures, [])) map end + # Resolves a field from caller opts first (explicit override), then from the + # card struct/map, then the given default. Lets callers pass a fully-populated + # %A2A.AgentCard{} OR supply fields via opts (backward compatible). + defp card_field(opts, card, key, default) do + case Keyword.fetch(opts, key) do + {:ok, value} -> value + :error -> Map.get(card, key, default) || default + end + end + + defp non_empty([]), do: nil + defp non_empty(value), do: value + @doc """ Decodes a JSON map into an `%A2A.AgentCard{}` struct. diff --git a/test/a2a/json_test.exs b/test/a2a/json_test.exs index 00cc864..c90727b 100644 --- a/test/a2a/json_test.exs +++ b/test/a2a/json_test.exs @@ -1134,6 +1134,46 @@ defmodule A2A.JSONTest do JSON.encode_agent_card(card, []) end end + + test "encodes rich fields from a populated %A2A.AgentCard{} struct" do + card = %A2A.AgentCard{ + name: "struct-agent", + description: "Built from the struct", + url: "https://example.com/a2a", + version: "2.0.0", + skills: [%{id: "s", name: "S", description: "d", tags: ["t"]}], + capabilities: %{streaming: true, push_notifications: true}, + provider: %{organization: "Acme", url: "https://acme.example.com"}, + documentation_url: "https://docs.example.com" + } + + map = JSON.encode_agent_card(card, url: "https://example.com/a2a") + + # Without these struct values being read, all three would be empty/absent. + assert map["capabilities"] == %{"streaming" => true, "pushNotifications" => true} + assert map["provider"] == %{"organization" => "Acme", "url" => "https://acme.example.com"} + assert map["documentationUrl"] == "https://docs.example.com" + end + + test "opts override struct fields (backward compatible)" do + card = %A2A.AgentCard{ + name: "struct-agent", + description: "d", + url: "https://example.com/a2a", + version: "1.0.0", + skills: [], + capabilities: %{streaming: true} + } + + map = + JSON.encode_agent_card(card, + url: "https://example.com/a2a", + capabilities: %{push_notifications: true} + ) + + # opts win over the struct value + assert map["capabilities"] == %{"pushNotifications" => true} + end end # -------------------------------------------------------------------