Skip to main content

src/aion/codec.gleam

//// 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))
  }
}