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

View file

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

View file

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

View file

@ -1,5 +1,20 @@
use crate::error::{Error, ParseError}; 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 { macro_rules! basic_string_type {
($name:ident, $regex:literal, $max_len:literal) => { ($name:ident, $regex:literal, $max_len:literal) => {
pub struct $name { value: String, } 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; mod authority;
pub use authority::Authority; mod cid;
mod datetime; mod datetime;
pub use datetime::Datetime; mod did;
mod record_key; mod record_key;
pub use record_key::RecordKey; mod strong_ref;
mod uri; 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; pub use uri::Uri;
basic_string_type!(Handle, basic_string_type!(Handle,
@ -63,22 +82,3 @@ basic_string_type!(Tid,
r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$", r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$",
13 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::{ use crate::{
types::{Did, Handle}, types::{
Did, Handle
},
error::{Error, ParseError}, error::{Error, ParseError},
}; };
use serde::Deserialize;
use std::{ use std::{
fmt::{Display, Formatter, Result as FmtResult}, fmt::{Display, Formatter, Result as FmtResult},
str::FromStr, str::FromStr,
}; };
#[derive(Deserialize)]
pub enum Authority { pub enum Authority {
Did(Did), Did(Did),
Handle(Handle), Handle(Handle),

View file

@ -1,4 +1,7 @@
use crate::error::Error; use crate::{
basic_deserializer,
error::Error
};
pub struct Cid { value: String, } pub struct Cid { value: String, }
@ -14,3 +17,5 @@ impl std::str::FromStr for Cid {
Ok(Self { value: s.to_string() }) 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::{ use time::{
UtcDateTime, UtcDateTime,
format_description::well_known::{Rfc3339, Iso8601}, format_description::well_known::{Rfc3339, Iso8601},
@ -61,3 +64,5 @@ impl FromStr for Datetime {
Ok(Datetime { time: datetime, derived_string: s.to_string() }) 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::{ use std::{
fmt::{Display, Formatter, Result as FmtResult}, fmt::{Display, Formatter, Result as FmtResult},
str::FromStr, str::FromStr,
@ -31,6 +34,8 @@ impl FromStr for DidMethod {
} }
} }
basic_deserializer!(DidMethod);
pub struct Did { pub struct Did {
method: DidMethod, method: DidMethod,
identifier: String, 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())) 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::{ use crate::{
basic_deserializer,
types::{Did, Authority, Nsid, RecordKey}, types::{Did, Authority, Nsid, RecordKey},
error::{Error, ParseError}, error::{Error, ParseError},
}; };
@ -33,47 +34,78 @@ impl Display for Uri {
impl FromStr for Uri { impl FromStr for Uri {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() > 8000 { Self::check_length(s)?;
return Err(Error::Parse {
err: ParseError::Length { max: 8000, got: s.len() },
object: "Did".to_string(),
});
}
let Some(( let Some((
_whole, unchecked_authority, unchecked_collection, unchecked_rkey _whole, unchecked_authority, unchecked_collection, unchecked_rkey
)) = regex_captures!( )): Option<(&str, &str, &str, &str)> = regex_captures!(
r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i", r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i",
s, s,
) else { ) else {
return Err(Error::Parse { return Err(Error::Parse {
err: ParseError::Format, err: ParseError::Format,
object: "Uri".to_string(), object: "Uri".to_string(),
}); });
}; };
let did = match Authority::from_str(unchecked_authority)? { let did = Self::check_authority(unchecked_authority.to_string())?;
Authority::Handle(h) =>
return Err(Error::Parse {
err: ParseError::ForceDid { handle: h.to_string() },
object: "Uri".to_string(),
}),
Authority::Did(d) => d,
};
let collection = if unchecked_collection.is_empty() { None } 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 } 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 }) Ok(Uri { authority: did, collection, rkey })
} }
} }
impl Uri { 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 { pub fn authority_as_did(&self) -> &Did {
&self.authority &self.authority
} }
} }
basic_deserializer!(Uri);