Atproto, serde deserialize & move strong_ref

This adds Serde deserialization. It also needs to add an error for
handling the failure on these deserializations.

I broke strong_ref into its own file because it was starting to grow a
lot.
This commit is contained in:
Julia Lange 2025-07-02 10:47:26 -07:00
parent 34719e7d01
commit 5bc903b2fa
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto
11 changed files with 194 additions and 54 deletions

47
Cargo.lock generated
View file

@ -70,8 +70,8 @@ name = "api"
version = "0.1.0"
dependencies = [
"atproto",
"axum",
"http 1.3.1",
"router",
"serde",
"serde_json",
"tokio",
@ -123,6 +123,7 @@ dependencies = [
"time",
"tracing",
"tracing-subscriber",
"unicode-segmentation",
]
[[package]]
@ -293,9 +294,9 @@ dependencies = [
[[package]]
name = "bon"
version = "3.6.3"
version = "3.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced38439e7a86a4761f7f7d5ded5ff009135939ecb464a24452eaa4c1696af7d"
checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6"
dependencies = [
"bon-macros",
"rustversion",
@ -303,9 +304,9 @@ dependencies = [
[[package]]
name = "bon-macros"
version = "3.6.3"
version = "3.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce61d2d3844c6b8d31b2353d9f66cf5e632b3e9549583fe3cac2f4f6136725e"
checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca"
dependencies = [
"darling",
"ident_case",
@ -662,6 +663,20 @@ dependencies = [
"serde",
]
[[package]]
name = "entryway"
version = "0.1.0"
dependencies = [
"atproto",
"http 1.3.1",
"router",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -1183,6 +1198,8 @@ dependencies = [
"anyhow",
"async-trait",
"atproto",
"atrium-api",
"db",
"rocketman",
"serde",
"serde_json",
@ -1813,6 +1830,20 @@ dependencies = [
"zstd",
]
[[package]]
name = "router"
version = "0.1.0"
dependencies = [
"atproto",
"axum",
"http 1.3.1",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "rsa"
version = "0.9.8"
@ -2708,6 +2739,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unsigned-varint"
version = "0.8.0"

View file

@ -7,7 +7,7 @@ async-trait = "0.1.88"
atproto = { path = "./atproto" }
db = { path = "./db" }
router = { path = "./router" }
serde = "1.0.219"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio"] }
thiserror = "2.0.12"

View file

@ -20,6 +20,8 @@ pub enum FormatError {
pub enum ParseError {
#[error("Time Parse Error: {0}")]
Datetime(#[from] time::error::Parse),
#[error("Json Parse Error: {0}")]
Serde(#[from] serde_json::error::Error),
#[error("Length of parsed object too long, max: {max:?}, got: {got:?}.")]
Length { max: usize, got: usize },
#[error("Currently Did is enforced, cannot use handle, {handle:?}")]

View file

@ -1,5 +1,20 @@
use crate::error::{Error, ParseError};
#[macro_export]
macro_rules! basic_deserializer {
($name:ident) => {
impl<'de> serde::de::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value: String = serde::de::Deserialize::deserialize(deserializer)?;
value.parse::<$name>().map_err(<D::Error as serde::de::Error>::custom)
}
}
}
}
macro_rules! basic_string_type {
($name:ident, $regex:literal, $max_len:literal) => {
pub struct $name { value: String, }
@ -35,20 +50,24 @@ macro_rules! basic_string_type {
})
}
}
basic_deserializer!($name);
}
}
mod did;
pub use did::Did;
mod cid;
pub use cid::Cid;
mod authority;
pub use authority::Authority;
mod cid;
mod datetime;
pub use datetime::Datetime;
mod did;
mod record_key;
pub use record_key::RecordKey;
mod strong_ref;
mod uri;
pub use authority::Authority;
pub use cid::Cid;
pub use datetime::Datetime;
pub use did::Did;
pub use record_key::RecordKey;
pub use strong_ref::StrongRef;
pub use uri::Uri;
basic_string_type!(Handle,
@ -63,22 +82,3 @@ basic_string_type!(Tid,
r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$",
13
);
pub struct StrongRef<T> {
content: T,
cid: Cid,
}
impl<T> StrongRef<T> {
pub fn get_content(&self) -> &T {
&self.content
}
pub fn extract_content(self) -> (T, Cid) {
(self.content, self.cid)
}
pub fn get_cid(&self) -> &Cid {
&self.cid
}
}

View file

@ -1,12 +1,16 @@
use crate::{
types::{Did, Handle},
types::{
Did, Handle
},
error::{Error, ParseError},
};
use serde::Deserialize;
use std::{
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
#[derive(Deserialize)]
pub enum Authority {
Did(Did),
Handle(Handle),

View file

@ -1,4 +1,7 @@
use crate::error::Error;
use crate::{
basic_deserializer,
error::Error
};
pub struct Cid { value: String, }
@ -14,3 +17,5 @@ impl std::str::FromStr for Cid {
Ok(Self { value: s.to_string() })
}
}
basic_deserializer!(Cid);

View file

@ -1,4 +1,7 @@
use crate::error::{Error, ParseError, FormatError};
use crate::{
basic_deserializer,
error::{Error, ParseError, FormatError},
};
use time::{
UtcDateTime,
format_description::well_known::{Rfc3339, Iso8601},
@ -61,3 +64,5 @@ impl FromStr for Datetime {
Ok(Datetime { time: datetime, derived_string: s.to_string() })
}
}
basic_deserializer!(Datetime);

View file

@ -1,4 +1,7 @@
use crate::error::{Error, ParseError};
use crate::{
basic_deserializer,
error::{Error, ParseError},
};
use std::{
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
@ -31,6 +34,8 @@ impl FromStr for DidMethod {
}
}
basic_deserializer!(DidMethod);
pub struct Did {
method: DidMethod,
identifier: String,
@ -70,3 +75,5 @@ impl FromStr for Did {
})
}
}
basic_deserializer!(Did);

View file

@ -61,3 +61,5 @@ impl FromStr for RecordKey {
Ok(RecordKey::Any(s.to_string()))
}
}
basic_deserializer!(RecordKey);

View file

@ -0,0 +1,46 @@
use crate::{
basic_deserializer,
types::{Cid, Uri},
error::{Error, ParseError},
};
pub struct StrongRef<T> {
content: T,
cid: Cid,
}
impl StrongRef<Uri> {
pub fn from_atrium_api(strong_ref: atrium_api::com::atproto::repo::strong_ref::MainData) -> Result<Self, Error> {
let str_cid = serde_json::to_string(&strong_ref.cid).map_err(|e| {
Error::Parse { err: ParseError::Serde(e), object: "Uri".to_string() }
})?;
Ok(Self {
content: strong_ref.uri.parse::<Uri>()?,
cid: str_cid.parse::<Cid>()?,
})
}
}
impl<T> StrongRef<T> {
pub fn map_content<U, F>(self, f: F) -> StrongRef<U>
where
F: FnOnce(T) -> U,
{
StrongRef {
content: f(self.content),
cid: self.cid,
}
}
pub fn get_content(&self) -> &T {
&self.content
}
pub fn extract_content(self) -> (T, Cid) {
(self.content, self.cid)
}
pub fn get_cid(&self) -> &Cid {
&self.cid
}
}

View file

@ -1,4 +1,5 @@
use crate::{
basic_deserializer,
types::{Did, Authority, Nsid, RecordKey},
error::{Error, ParseError},
};
@ -33,47 +34,78 @@ impl Display for Uri {
impl FromStr for Uri {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() > 8000 {
return Err(Error::Parse {
err: ParseError::Length { max: 8000, got: s.len() },
object: "Did".to_string(),
});
}
Self::check_length(s)?;
let Some((
_whole, unchecked_authority, unchecked_collection, unchecked_rkey
)) = regex_captures!(
)): Option<(&str, &str, &str, &str)> = regex_captures!(
r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i",
s,
) else {
) else {
return Err(Error::Parse {
err: ParseError::Format,
object: "Uri".to_string(),
});
};
let did = match Authority::from_str(unchecked_authority)? {
Authority::Handle(h) =>
return Err(Error::Parse {
err: ParseError::ForceDid { handle: h.to_string() },
object: "Uri".to_string(),
}),
Authority::Did(d) => d,
};
let did = Self::check_authority(unchecked_authority.to_string())?;
let collection = if unchecked_collection.is_empty() { None }
else { Some(unchecked_collection.parse::<Nsid>()?) };
else { Some(Self::check_collection(unchecked_collection.to_string())?) };
let rkey = if unchecked_rkey.is_empty() { None }
else { Some(unchecked_rkey.parse::<RecordKey>()?) };
else { Some(Self::check_rkey(unchecked_rkey.to_string())?) };
Ok(Uri { authority: did, collection, rkey })
}
}
impl Uri {
pub fn from_components(
authority_str: String, collection_str: Option<String>,
rkey_str: Option<String>
) -> Result<Self, Error> {
let authority = Self::check_authority(authority_str)?;
let collection = collection_str.map(Self::check_collection).transpose()?;
let rkey = rkey_str.map(Self::check_rkey).transpose()?;
let uri = Uri { authority, collection, rkey };
Self::check_length(&uri.to_string())?;
Ok(uri)
}
fn check_length(s: &str) -> Result<(), Error> {
if s.len() > 8000 {
return Err(Error::Parse {
err: ParseError::Length { max: 8000, got: s.len() },
object: "Did".to_string(),
});
}
Ok(())
}
fn check_authority(authority: String) -> Result<Did, Error> {
Ok(match Authority::from_str(&authority)? {
Authority::Handle(h) =>
return Err(Error::Parse {
err: ParseError::ForceDid { handle: h.to_string() },
object: "Uri".to_string(),
}),
Authority::Did(d) => d,
})
}
fn check_collection(collection: String) -> Result<Nsid, Error> {
Ok(collection.parse::<Nsid>()?)
}
fn check_rkey(rkey: String) -> Result<RecordKey, Error> {
Ok(rkey.parse::<RecordKey>()?)
}
pub fn authority_as_did(&self) -> &Did {
&self.authority
}
}
basic_deserializer!(Uri);