use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Blob {
#[serde(rename = "$type")]
pub type_marker: String,
#[serde(rename = "ref")]
pub ref_link: Option<CIDLink>,
#[serde(rename = "mimeType")]
pub mime_type: String,
pub size: u64,
pub cid: Option<String>,
}
impl Blob {
pub fn is_legacy(&self) -> bool {
self.type_marker == "blob" && self.cid.is_some() && self.ref_link.is_none()
}
pub fn is_modern(&self) -> bool {
self.type_marker == "blob" && self.ref_link.is_some()
}
pub fn get_cid(&self) -> Option<&str> {
if let Some(ref link) = self.ref_link {
Some(&link.link)
} else {
self.cid.as_deref()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CIDLink {
#[serde(rename = "$link")]
pub link: String,
}
impl CIDLink {
pub fn new(cid: impl Into<String>) -> Self {
Self { link: cid.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bytes {
#[serde(rename = "$bytes")]
pub bytes: String,
}
impl Bytes {
pub fn new(base64: impl Into<String>) -> Self {
Self {
bytes: base64.into(),
}
}
pub fn decode(&self) -> Result<Vec<u8>, base64::DecodeError> {
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(&self.bytes)
}
pub fn decoded_len(&self) -> Option<usize> {
self.decode().ok().map(|v| v.len())
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum DataValue {
#[default]
Null,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
Bytes(Bytes),
Link(CIDLink),
Blob(Blob),
Array(Vec<DataValue>),
Object(IndexMap<String, DataValue>),
}
impl DataValue {
pub fn is_null(&self) -> bool {
matches!(self, DataValue::Null)
}
pub fn is_boolean(&self) -> bool {
matches!(self, DataValue::Boolean(_))
}
pub fn is_integer(&self) -> bool {
matches!(self, DataValue::Integer(_))
}
pub fn is_float(&self) -> bool {
matches!(self, DataValue::Float(_))
}
pub fn is_number(&self) -> bool {
matches!(self, DataValue::Integer(_) | DataValue::Float(_))
}
pub fn is_string(&self) -> bool {
matches!(self, DataValue::String(_))
}
pub fn is_bytes(&self) -> bool {
matches!(self, DataValue::Bytes(_))
}
pub fn is_link(&self) -> bool {
matches!(self, DataValue::Link(_))
}
pub fn is_blob(&self) -> bool {
matches!(self, DataValue::Blob(_))
}
pub fn is_array(&self) -> bool {
matches!(self, DataValue::Array(_))
}
pub fn is_object(&self) -> bool {
matches!(self, DataValue::Object(_))
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
DataValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
DataValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
DataValue::Float(f) => Some(*f),
DataValue::Integer(i) => Some(*i as f64),
_ => None,
}
}
pub fn as_string(&self) -> Option<&str> {
match self {
DataValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&Bytes> {
match self {
DataValue::Bytes(b) => Some(b),
_ => None,
}
}
pub fn as_link(&self) -> Option<&CIDLink> {
match self {
DataValue::Link(l) => Some(l),
_ => None,
}
}
pub fn as_blob(&self) -> Option<&Blob> {
match self {
DataValue::Blob(b) => Some(b),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[DataValue]> {
match self {
DataValue::Array(a) => Some(a),
_ => None,
}
}
pub fn as_object(&self) -> Option<&IndexMap<String, DataValue>> {
match self {
DataValue::Object(o) => Some(o),
_ => None,
}
}
pub fn as_array_mut(&mut self) -> Option<&mut Vec<DataValue>> {
match self {
DataValue::Array(a) => Some(a),
_ => None,
}
}
pub fn as_object_mut(&mut self) -> Option<&mut IndexMap<String, DataValue>> {
match self {
DataValue::Object(o) => Some(o),
_ => None,
}
}
pub fn type_name(&self) -> &'static str {
match self {
DataValue::Null => "null",
DataValue::Boolean(_) => "boolean",
DataValue::Integer(_) => "integer",
DataValue::Float(_) => "float",
DataValue::String(_) => "string",
DataValue::Bytes(_) => "bytes",
DataValue::Link(_) => "link",
DataValue::Blob(_) => "blob",
DataValue::Array(_) => "array",
DataValue::Object(_) => "object",
}
}
pub fn get_type(&self) -> Option<&str> {
self.as_object()
.and_then(|obj| obj.get("$type"))
.and_then(|v| v.as_string())
}
pub fn get(&self, key: &str) -> Option<&DataValue> {
self.as_object().and_then(|obj| obj.get(key))
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn test_data_value_types() {
assert!(DataValue::Null.is_null());
assert!(DataValue::Boolean(true).is_boolean());
assert!(DataValue::Integer(42).is_integer());
assert!(DataValue::Float(3.14).is_float());
assert!(DataValue::String("test".into()).is_string());
assert!(DataValue::Array(vec![]).is_array());
assert!(DataValue::Object(IndexMap::new()).is_object());
}
#[test]
fn test_data_value_accessors() {
assert_eq!(DataValue::Boolean(true).as_boolean(), Some(true));
assert_eq!(DataValue::Integer(42).as_integer(), Some(42));
assert_eq!(DataValue::Float(3.14).as_float(), Some(3.14));
assert_eq!(DataValue::String("test".into()).as_string(), Some("test"));
assert_eq!(DataValue::Integer(42).as_float(), Some(42.0));
}
#[test]
fn test_cid_link() {
let link = CIDLink::new("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi");
assert_eq!(
link.link,
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
);
}
#[test]
fn test_bytes() {
let bytes = Bytes::new("SGVsbG8gV29ybGQ="); let decoded = bytes.decode().unwrap();
assert_eq!(decoded, b"Hello World");
assert_eq!(bytes.decoded_len(), Some(11));
}
#[test]
fn test_blob_modern() {
let blob = Blob {
type_marker: "blob".to_string(),
ref_link: Some(CIDLink::new("bafytest")),
mime_type: "image/png".to_string(),
size: 1024,
cid: None,
};
assert!(blob.is_modern());
assert!(!blob.is_legacy());
assert_eq!(blob.get_cid(), Some("bafytest"));
}
#[test]
fn test_blob_legacy() {
let blob = Blob {
type_marker: "blob".to_string(),
ref_link: None,
mime_type: "image/png".to_string(),
size: 1024,
cid: Some("bafytest".to_string()),
};
assert!(!blob.is_modern());
assert!(blob.is_legacy());
assert_eq!(blob.get_cid(), Some("bafytest"));
}
#[test]
fn test_data_value_type_names() {
assert_eq!(DataValue::Null.type_name(), "null");
assert_eq!(DataValue::Boolean(true).type_name(), "boolean");
assert_eq!(DataValue::Integer(42).type_name(), "integer");
assert_eq!(DataValue::Float(3.14).type_name(), "float");
assert_eq!(DataValue::String("".into()).type_name(), "string");
assert_eq!(DataValue::Array(vec![]).type_name(), "array");
assert_eq!(DataValue::Object(IndexMap::new()).type_name(), "object");
}
#[test]
fn test_data_value_get_type() {
let mut obj = IndexMap::new();
obj.insert(
"$type".to_string(),
DataValue::String("app.bsky.feed.post".into()),
);
let value = DataValue::Object(obj);
assert_eq!(value.get_type(), Some("app.bsky.feed.post"));
let empty_obj = DataValue::Object(IndexMap::new());
assert_eq!(empty_obj.get_type(), None);
assert_eq!(DataValue::String("test".into()).get_type(), None);
}
}