//// Typed codecs for values crossing the Aion workflow boundary. import gleam/dynamic/decode import gleam/json /// A typed encoder/decoder pair over the string form consumed by the FFI /// boundary. /// /// Encoders produce the canonical string payload sent to the engine. Decoders /// turn that string payload back into the expected Gleam value and report a /// typed `DecodeError` on malformed input or schema mismatch. pub type Codec(a) { Codec(encode: fn(a) -> String, decode: fn(String) -> Result(a, DecodeError)) } /// A typed boundary decode failure. /// /// `reason` describes the failing expectation and `path` points at the nested /// JSON field or index when the underlying decoder can provide one. pub type DecodeError { DecodeError(reason: String, path: List(String)) } /// Build a `Codec` from a `gleam_json` encoder and decoder. /// /// Malformed JSON and decoder mismatches are mapped to `DecodeError` values; /// decode failures are returned as data. pub fn json_codec( encoder: fn(a) -> json.Json, decoder: decode.Decoder(a), ) -> Codec(a) { Codec( encode: fn(value) { value |> encoder |> json.to_string }, decode: fn(input) { input |> json.parse(decoder) |> result_map_error(json_decode_error) }, ) } fn json_decode_error(error: json.DecodeError) -> DecodeError { case error { json.UnexpectedEndOfInput -> DecodeError("Unexpected end of input", []) json.UnexpectedByte(byte) -> DecodeError("Unexpected byte: " <> byte, []) json.UnexpectedSequence(sequence) -> DecodeError("Unexpected sequence: " <> sequence, []) json.UnableToDecode(errors) -> dynamic_decode_error(errors) } } fn dynamic_decode_error(errors: List(decode.DecodeError)) -> DecodeError { case errors { [] -> DecodeError("Unable to decode value", []) [decode.DecodeError(expected: expected, found: found, path: path), ..] -> DecodeError("Expected " <> expected <> ", found " <> found, path) } } fn result_map_error( result: Result(a, error), mapper: fn(error) -> mapped_error, ) -> Result(a, mapped_error) { case result { Ok(value) -> Ok(value) Error(error) -> Error(mapper(error)) } }