///|
/// DynamoDB's wire attribute types. Numbers stay as decimal text to avoid
/// losing precision when they pass through JSON.
#derive(Eq, Debug)
pub(all) enum AttributeValue {
Text(String)
Number(String)
Binary(Bytes)
BoolValue(Bool)
NullValue
ListValue(Array[AttributeValue])
ObjectValue(Map[String, AttributeValue])
StringSet(Array[String])
NumberSet(Array[String])
BinarySet(Array[Bytes])
}
///|
pub extend AttributeValue with Eq::{not_equal, equal}
///|
pub extend AttributeValue with @debug.Debug::{to_repr}
///|
pub type Item = Map[String, AttributeValue]
///|
pub(all) suberror CodecError {
InvalidAttribute(String)
}
///|
fn tagged(tag : String, value : Json) -> Json {
Json::object(Map([(tag, value)]))
}
///|
pub fn encode_attribute(value : AttributeValue) -> Json {
match value {
Text(text) => tagged("S", Json::string(text))
Number(number) => tagged("N", Json::string(number))
Binary(bytes) => tagged("B", Json::string(@base64.encode(bytes)))
BoolValue(true) => tagged("BOOL", Json::boolean(true))
BoolValue(false) => tagged("BOOL", Json::boolean(false))
NullValue => tagged("NULL", Json::boolean(true))
ListValue(items) => tagged("L", Json::array(items.map(encode_attribute)))
ObjectValue(fields) => tagged("M", encode_item(fields))
StringSet(values) =>
tagged("SS", Json::array(values.map(fn(s) { Json::string(s) })))
NumberSet(values) =>
tagged("NS", Json::array(values.map(fn(s) { Json::string(s) })))
BinarySet(values) =>
tagged(
"BS",
Json::array(values.map(fn(b) { Json::string(@base64.encode(b)) })),
)
}
}
///|
pub fn encode_item(item : Item) -> Json {
let fields : Map[String, Json] = Map([])
for key, value in item {
fields[key] = encode_attribute(value)
}
Json::object(fields)
}
///|
fn string_value(value : Json, tag : String) -> String raise CodecError {
match value {
String(text) => text
_ => raise InvalidAttribute("\{tag} must contain a string")
}
}
///|
fn binary_value(value : Json, tag : String) -> Bytes raise CodecError {
let text = string_value(value, tag)
@base64.decode(text) catch {
_ => raise InvalidAttribute("\{tag} must contain base64")
}
}
///|
fn string_array(value : Json, tag : String) -> Array[String] raise CodecError {
match value {
Array(values) => {
let result : Array[String] = []
for value in values {
result.push(string_value(value, tag))
}
result
}
_ => raise InvalidAttribute("\{tag} must contain an array")
}
}
///|
pub fn decode_attribute(json : Json) -> AttributeValue raise CodecError {
let fields = match json {
Object(fields) => fields
_ => raise InvalidAttribute("attribute must be an object")
}
if fields.length() != 1 {
raise InvalidAttribute("attribute must have exactly one type tag")
}
match fields.iter().next() {
Some(("S", value)) => Text(string_value(value, "S"))
Some(("N", value)) => Number(string_value(value, "N"))
Some(("B", value)) => Binary(binary_value(value, "B"))
Some(("BOOL", True)) => BoolValue(true)
Some(("BOOL", False)) => BoolValue(false)
Some(("NULL", True)) => NullValue
Some(("L", Array(values))) => {
let result : Array[AttributeValue] = []
for value in values {
result.push(decode_attribute(value))
}
ListValue(result)
}
Some(("M", value)) => ObjectValue(decode_item(value))
Some(("SS", value)) => StringSet(string_array(value, "SS"))
Some(("NS", value)) => NumberSet(string_array(value, "NS"))
Some(("BS", Array(values))) => {
let result : Array[Bytes] = []
for value in values {
result.push(binary_value(value, "BS"))
}
BinarySet(result)
}
Some((tag, _)) => raise InvalidAttribute("invalid value for tag \{tag}")
None => raise InvalidAttribute("attribute is empty")
}
}
///|
pub fn decode_item(json : Json) -> Item raise CodecError {
let fields = match json {
Object(fields) => fields
_ => raise InvalidAttribute("item must be an object")
}
let item : Item = Map([])
for name, value in fields {
item[name] = decode_attribute(value)
}
item
}