-module(discovery_fetch).
-export([make_fetch_fn/1,
fetch/2,
actor_doc_url/2,
decode_body/1,
accept_header/0]).
%% Live peer-actor-doc fetch for peer_actors — Step 10c per design
%% §13.6. The peer_actors gen_server already exposes
%% lookup_or_fetch_srv/2(PeerId, FetchFn) where FetchFn is a
%% 1-arity closure that returns {ok, PeerAS} | {error, Reason} on
%% cache miss. For tests we wire a fake FetchFn that returns a
%% pre-baked AS; for live federation we wire the closure this
%% module produces — it GETs /actors/ with an Accept
%% header that asks for the actor_doc format
%% (http_server.erl Step 10c), decodes the response body via
%% term_codec, and returns the AS proplist.
%%
%% Cfg shape (reuses dispatch_http's peer URL resolution so a
%% single Cfg threads through both delivery and discovery):
%% {peer_url, [{PeerId, BaseUrl}, ...]}
%% {peer_url_fn, fun ((PeerId) -> {ok, BaseUrl} | not_found)}
%%
%% BaseUrl shape: <<"http://host:port">> (no trailing slash; this
%% module appends the path). PeerId is the actor atom.
%%
%% Outcomes:
%% 2xx + decodable body -> {ok, PeerAS}
%% 2xx + bad body -> {error, bad_actor_doc}
%% non-2xx -> {error, {status, N}}
%% resolver miss -> {error, no_peer_url}
%% transport -> {error, Reason}
%%
%% Cache write semantics live in peer_actors:lookup_or_fetch/3 —
%% successful fetches store; errors do NOT poison so callers can
%% retry on transients.
%% ── Accept header ────────────────────────────────────────────
%% "application/vnd.fed-sx.actor-doc" — same MIME the http_server
%% content_type_for(actor_doc) emits, so the Accept negotiation
%% in accept_format/1 routes the peer's response to the term_codec
%% serializer arm.
accept_header() ->
<<97,112,112,108,105,99,97,116,105,111,110,47,
118,110,100,46,102,101,100,45,115,120,46,
97,99,116,111,114,45,100,111,99>>.
%% ── public API ───────────────────────────────────────────────
make_fetch_fn(Cfg) ->
fun (PeerId) ->
case dispatch_http:resolve_peer_url(PeerId, Cfg) of
{error, R} -> {error, R};
{ok, BaseUrl} -> fetch(actor_doc_url(BaseUrl, PeerId), Cfg)
end
end.
fetch(Url, _Cfg) ->
AcceptKey = <<97,99,99,101,112,116>>, % "accept"
Headers = [{AcceptKey, accept_header()}],
try httpc:request(Url, get, Headers, <<>>) of
{ok, Status, _H, Body} when Status >= 200, Status < 300 ->
decode_body(Body);
{ok, Status, _H, _B} ->
{error, {status, Status}};
Other ->
{error, {bad_response, Other}}
catch
error:Reason -> {error, Reason}
end.
%% actor_doc_url/2 — /actors/. PeerId is the actor
%% atom; rendered to a binary via its name (matches the same path
%% layout http_server.erl uses for the route registration at
%% prefix "/actors/").
actor_doc_url(BaseUrl, PeerId) when is_atom(PeerId) ->
PeerBin = list_to_binary(atom_to_list(PeerId)),
%% "/actors/" — 8 bytes
Prefix = <<47,97,99,116,111,114,115,47>>,
<>.
%% decode_body/1 — round the wire body back through term_codec.
%% Returns {ok, AS} on a proplist-shaped decode (matching the
%% peer-actor-state schema), {error, bad_actor_doc} otherwise.
decode_body(Body) ->
case term_codec:decode(Body) of
{ok, AS, _} when is_list(AS) -> {ok, AS};
_ -> {error, bad_actor_doc}
end.