-module(caffeine_lang@codegen@relay). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/caffeine_lang/codegen/relay.gleam"). -export([generate/1]). -export_type([signal_entry/0, signal_kind/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type signal_entry() :: {signal_entry, binary(), signal_kind(), binary(), gleam@dict:dict(binary(), caffeine_lang@value:value()), gleam@dict:dict(binary(), binary()), gleam@option:option(binary())}. -type signal_kind() :: count | distribution. -file("src/caffeine_lang/codegen/relay.gleam", 125). ?DOC( " Serialize the `tags` dict — already `Dict(String, String)` because every\n" " codegen-emitted tag is a string. Sorted by key for deterministic output.\n" ). -spec tags_dict_json(gleam@dict:dict(binary(), binary())) -> gleam@json:json(). tags_dict_json(Tags) -> _pipe = Tags, _pipe@1 = maps:to_list(_pipe), _pipe@2 = gleam@list:sort( _pipe@1, fun(A, B) -> gleam@string:compare(erlang:element(1, A), erlang:element(1, B)) end ), _pipe@3 = gleam@list:map( _pipe@2, fun(Pair) -> {erlang:element(1, Pair), gleam@json:string(erlang:element(2, Pair))} end ), gleam@json:object(_pipe@3). -file("src/caffeine_lang/codegen/relay.gleam", 154). -spec match_value_json(caffeine_lang@value:value()) -> gleam@json:json(). match_value_json(V) -> case V of {string_value, S} -> gleam@json:string(S); {int_value, I} -> gleam@json:int(I); {float_value, F} -> gleam@json:float(F); {bool_value, B} -> gleam@json:bool(B); _ -> gleam@json:null() end. -file("src/caffeine_lang/codegen/relay.gleam", 146). ?DOC( " Serialize a `match` dict. Match values are runtime-untyped at the relay,\n" " so we emit each as its nearest JSON shape. Strings stay strings; numeric\n" " and boolean values keep their JSON-native form. Other shapes fall back\n" " to `null` (shouldn't appear post-validate). Entries are sorted by key for\n" " deterministic output across compile targets — `dict.to_list` doesn't\n" " guarantee insertion order on JavaScript.\n" ). -spec match_dict_json(gleam@dict:dict(binary(), caffeine_lang@value:value())) -> gleam@json:json(). match_dict_json(Match) -> _pipe = Match, _pipe@1 = maps:to_list(_pipe), _pipe@2 = gleam@list:sort( _pipe@1, fun(A, B) -> gleam@string:compare(erlang:element(1, A), erlang:element(1, B)) end ), _pipe@3 = gleam@list:map( _pipe@2, fun(Pair) -> {erlang:element(1, Pair), match_value_json(erlang:element(2, Pair))} end ), gleam@json:object(_pipe@3). -file("src/caffeine_lang/codegen/relay.gleam", 133). -spec signal_kind_to_string(signal_kind()) -> binary(). signal_kind_to_string(Kind) -> case Kind of count -> <<"count"/utf8>>; distribution -> <<"distribution"/utf8>> end. -file("src/caffeine_lang/codegen/relay.gleam", 109). -spec signal_entry_json(signal_entry()) -> gleam@json:json(). signal_entry_json(Entry) -> gleam@json:object( [{<<"metric"/utf8>>, gleam@json:string(erlang:element(2, Entry))}, {<<"kind"/utf8>>, gleam@json:string( signal_kind_to_string(erlang:element(3, Entry)) )}, {<<"source"/utf8>>, gleam@json:string(erlang:element(4, Entry))}, {<<"match"/utf8>>, match_dict_json(erlang:element(5, Entry))}, {<<"tags"/utf8>>, tags_dict_json(erlang:element(6, Entry))}, {<<"value_path"/utf8>>, case erlang:element(7, Entry) of none -> gleam@json:null(); {some, P} -> gleam@json:string(P) end}] ). -file("src/caffeine_lang/codegen/relay.gleam", 102). ?DOC( " Wrap a list of entries in the top-level `signals.json` object: a version\n" " tag plus the routing array. Schema version 2 adds the per-entry `tags`\n" " field; relay builds before 2026-05-23 read v1 and don't know about tags.\n" ). -spec signals_json(list(signal_entry())) -> gleam@json:json(). signals_json(Entries) -> gleam@json:object( [{<<"version"/utf8>>, gleam@json:int(2)}, {<<"signals"/utf8>>, gleam@json:array(Entries, fun signal_entry_json/1)}] ). -file("src/caffeine_lang/codegen/relay.gleam", 66). ?DOC( " Build the list of signal entries from a single resolved IR by walking\n" " `slo.indicators` for `ExternalSignal` variants. Literal-query indicators\n" " are skipped — they don't need relay routing. Entries are sorted by\n" " indicator name for deterministic output across compile targets.\n" ). -spec ir_to_signal_entries( caffeine_lang@linker@ir:intermediate_representation(caffeine_lang@linker@ir:resolved()) ) -> list(signal_entry()). ir_to_signal_entries(Ir) -> _pipe = erlang:element(3, erlang:element(5, Ir)), _pipe@1 = maps:to_list(_pipe), _pipe@2 = gleam@list:sort( _pipe@1, fun(A, B) -> gleam@string:compare(erlang:element(1, A), erlang:element(1, B)) end ), gleam@list:filter_map( _pipe@2, fun(Pair) -> {Name, Src} = Pair, case Src of {external_signal, Source, Match, Value_extraction} -> {ok, {signal_entry, <<"caffeine."/utf8, (caffeine_lang@codegen@generator_utils:dd_metric_safe( erlang:element(3, Ir) ))/binary>>, case Value_extraction of none -> count; {some, _} -> distribution end, Source, Match, maps:from_list( [{<<"indicator"/utf8>>, caffeine_lang@codegen@generator_utils:dd_metric_safe( Name )}] ), gleam@option:map( Value_extraction, fun(Ve) -> erlang:element(2, Ve) end )}}; _ -> {error, nil} end end ). -file("src/caffeine_lang/codegen/relay.gleam", 52). ?DOC(false). -spec generate( list(caffeine_lang@linker@ir:intermediate_representation(caffeine_lang@linker@ir:resolved())) ) -> gleam@option:option(binary()). generate(Irs) -> Entries = gleam@list:flat_map(Irs, fun ir_to_signal_entries/1), case Entries of [] -> none; _ -> {some, gleam@json:to_string(signals_json(Entries))} end.