use crate::config::ValueFormat;
use faucet_core::FaucetError;
use serde_json::{Map, Value, json};
use std::collections::HashMap;
pub(crate) fn decode_payload(
data: &[u8],
format: ValueFormat,
message_id: &str,
) -> Result<Value, FaucetError> {
match format {
ValueFormat::Json => serde_json::from_slice(data).map_err(|e| {
FaucetError::Source(format!(
"pubsub: message {message_id} payload is not valid JSON: {e}"
))
}),
ValueFormat::String => match std::str::from_utf8(data) {
Ok(s) => Ok(Value::String(s.to_string())),
Err(e) => Err(FaucetError::Source(format!(
"pubsub: message {message_id} payload is not valid UTF-8: {e}"
))),
},
ValueFormat::Bytes => {
use base64::Engine as _;
Ok(Value::String(
base64::engine::general_purpose::STANDARD.encode(data),
))
}
}
}
pub(crate) fn attributes_to_value(attributes: &HashMap<String, String>) -> Value {
let mut map = Map::with_capacity(attributes.len());
for (k, v) in attributes {
map.insert(k.clone(), Value::String(v.clone()));
}
Value::Object(map)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn message_to_record(
data: &[u8],
attributes: &HashMap<String, String>,
message_id: &str,
ordering_key: &str,
publish_time_millis: Option<i64>,
format: ValueFormat,
attributes_key: &str,
) -> Result<Value, FaucetError> {
let payload = decode_payload(data, format, message_id)?;
let mut record = Map::new();
record.insert("data".to_string(), payload);
record.insert(attributes_key.to_string(), attributes_to_value(attributes));
record.insert("message_id".to_string(), json!(message_id));
if !ordering_key.is_empty() {
record.insert("ordering_key".to_string(), json!(ordering_key));
}
if let Some(ms) = publish_time_millis {
record.insert("publish_time_millis".to_string(), json!(ms));
}
Ok(Value::Object(record))
}
pub(crate) fn timestamp_millis(seconds: i64, nanos: i32) -> i64 {
seconds
.saturating_mul(1000)
.saturating_add(i64::from(nanos) / 1_000_000)
}
#[cfg(test)]
mod tests {
use super::*;
fn attrs(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
#[test]
fn payload_decodes_per_format() {
let j = decode_payload(br#"{"a":1}"#, ValueFormat::Json, "m1").unwrap();
assert_eq!(j["a"], 1);
let err = decode_payload(b"not json", ValueFormat::Json, "m-bad")
.unwrap_err()
.to_string();
assert!(err.contains("m-bad"), "{err}");
let s = decode_payload("héllo".as_bytes(), ValueFormat::String, "m1").unwrap();
assert_eq!(s, Value::String("héllo".into()));
assert!(decode_payload(&[0xff, 0xfe], ValueFormat::String, "m1").is_err());
let b = decode_payload(&[1, 2, 3], ValueFormat::Bytes, "m1").unwrap();
assert_eq!(b, Value::String("AQID".into()));
}
#[test]
fn attributes_map_to_string_object() {
let v = attributes_to_value(&attrs(&[("k1", "v1"), ("k2", "v2")]));
assert_eq!(v["k1"], "v1");
assert_eq!(v["k2"], "v2");
assert_eq!(attributes_to_value(&HashMap::new()), json!({}));
}
#[test]
fn record_shape_full() {
let r = message_to_record(
br#"{"x":1}"#,
&attrs(&[("origin", "eu")]),
"msg-7",
"order-42",
Some(1_716_700_000_123),
ValueFormat::Json,
"__attributes",
)
.unwrap();
assert_eq!(r["data"]["x"], 1);
assert_eq!(r["__attributes"]["origin"], "eu");
assert_eq!(r["message_id"], "msg-7");
assert_eq!(r["ordering_key"], "order-42");
assert_eq!(r["publish_time_millis"], 1_716_700_000_123i64);
}
#[test]
fn record_omits_absent_optionals_and_honours_attributes_key() {
let r = message_to_record(
b"raw text",
&HashMap::new(),
"msg-8",
"", None,
ValueFormat::String,
"attrs",
)
.unwrap();
assert_eq!(r["data"], "raw text");
assert!(r.get("attrs").is_some());
assert!(r.get("__attributes").is_none());
assert!(r.get("ordering_key").is_none());
assert!(r.get("publish_time_millis").is_none());
}
#[test]
fn record_propagates_decode_error() {
let err = message_to_record(
b"not json",
&HashMap::new(),
"msg-9",
"",
None,
ValueFormat::Json,
"__attributes",
)
.unwrap_err();
assert!(err.to_string().contains("msg-9"), "{err}");
}
#[test]
fn timestamp_millis_math() {
assert_eq!(
timestamp_millis(1_716_700_000, 123_000_000),
1_716_700_000_123
);
assert_eq!(timestamp_millis(0, 0), 0);
assert_eq!(timestamp_millis(i64::MAX, 0), i64::MAX);
}
}