import gleam/bit_array import gleam/dict import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode import gleam/json import gleam/option.{type Option, None, Some} /// One JSON field, or nothing when the value is absent. Keeps optional /// properties out of the encoded object entirely rather than emitting null. pub fn opt( name: String, value: Option(a), to_json: fn(a) -> json.Json, ) -> List(#(String, json.Json)) { case value { Some(v) -> [#(name, to_json(v))] None -> [] } } /// A `cid-link` is rendered in JSON as `{ "$link": }`. pub fn encode_cid_link(cid: String) -> json.Json { json.object([#("$link", json.string(cid))]) } pub fn cid_link_decoder() -> decode.Decoder(String) { decode.at(["$link"], decode.string) } /// `bytes` are rendered in JSON as `{ "$bytes": }`. pub fn encode_bytes(bytes: BitArray) -> json.Json { json.object([#("$bytes", json.string(bit_array.base64_encode(bytes, False)))]) } pub fn bytes_decoder() -> decode.Decoder(BitArray) { use encoded <- decode.then(decode.at(["$bytes"], decode.string)) case bit_array.base64_decode(encoded) { Ok(bytes) -> decode.success(bytes) Error(_) -> decode.failure(<<>>, "bytes") } } /// Re-encode an already-decoded `unknown` value back to JSON so records that /// carry one round-trip. Reconstructs the JSON value from the decoded `Dynamic`. pub fn dynamic_to_json(value: Dynamic) -> json.Json { case decode.run(value, json_value_decoder()) { Ok(rendered) -> rendered Error(_) -> json.null() } } fn json_value_decoder() -> decode.Decoder(json.Json) { use <- decode.recursive decode.one_of(decode.bool |> decode.map(json.bool), [ decode.int |> decode.map(json.int), decode.float |> decode.map(json.float), decode.string |> decode.map(json.string), decode.list(json_value_decoder()) |> decode.map(json.preprocessed_array), decode.dict(decode.string, json_value_decoder()) |> decode.map(fn(entries) { json.object(dict.to_list(entries)) }), decode.success(json.null()), ]) }