Compare commits
2 commits
9d8fb730ba
...
781a56028f
| Author | SHA1 | Date | |
|---|---|---|---|
| 781a56028f | |||
| 1abdb7f133 |
19 changed files with 504 additions and 116 deletions
|
|
@ -7,6 +7,7 @@ async-trait = "0.1.88"
|
||||||
atproto = { path = "./atproto" }
|
atproto = { path = "./atproto" }
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@ atrium-api = { version = "0.25.3", default-features = false }
|
||||||
lazy-regex = "3.4.1"
|
lazy-regex = "3.4.1"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
time = { version = "0.3.41", features = ["parsing", "formatting"] }
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
|
||||||
31
atproto/src/error.rs
Normal file
31
atproto/src/error.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use thiserror::Error as ThisError;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, ThisError)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Error while parsing")]
|
||||||
|
Parse { err: ParseError, object: String },
|
||||||
|
#[error("Error while formatting")]
|
||||||
|
Format { err: FormatError, object: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, ThisError)]
|
||||||
|
pub enum FormatError {
|
||||||
|
#[error("Time Parse Error: {0}")]
|
||||||
|
Datetime(#[from] time::error::Format),
|
||||||
|
}
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, ThisError)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("Time Parse Error: {0}")]
|
||||||
|
Datetime(#[from] time::error::Parse),
|
||||||
|
#[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:?}")]
|
||||||
|
ForceDid { handle: String },
|
||||||
|
#[error("Incorrectly formatted")]
|
||||||
|
Format,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
@ -1,80 +1,3 @@
|
||||||
use lazy_regex::regex_captures;
|
|
||||||
use core::str::FromStr;
|
|
||||||
use std::fmt::{
|
|
||||||
Display, Formatter, Result as FmtResult
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use atrium_api::types::{
|
|
||||||
Collection,
|
|
||||||
string::{
|
|
||||||
AtIdentifier as Authority,
|
|
||||||
Datetime,
|
|
||||||
Did,
|
|
||||||
Nsid,
|
|
||||||
RecordKey,
|
|
||||||
Tid,
|
|
||||||
Handle,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod lexicons;
|
pub mod lexicons;
|
||||||
|
pub mod types;
|
||||||
pub struct Cid(String);
|
pub mod error;
|
||||||
|
|
||||||
impl Display for Cid {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StrongRef<T> {
|
|
||||||
pub content: T,
|
|
||||||
pub cid: Cid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Uri {
|
|
||||||
whole: String,
|
|
||||||
// These fields could be useful in the future,
|
|
||||||
// so I'm leaving the code for them.
|
|
||||||
// authority: Authority,
|
|
||||||
// collection: Option<Nsid>,
|
|
||||||
// rkey: Option<RecordKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Uri {
|
|
||||||
type Err = &'static str;
|
|
||||||
fn from_str(uri: &str) -> Result<Self, Self::Err> {
|
|
||||||
if uri.len() > 8000 {
|
|
||||||
return Err("Uri too long")
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((
|
|
||||||
whole, unchecked_authority, unchecked_collection, unchecked_rkey
|
|
||||||
)) = regex_captures!(
|
|
||||||
r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i",
|
|
||||||
uri,
|
|
||||||
) else {
|
|
||||||
return Err("Invalid Uri");
|
|
||||||
};
|
|
||||||
|
|
||||||
// This parsing is required, but the values don't need to be used yet.
|
|
||||||
// No compute cost to use them, just storage cost
|
|
||||||
let _authority = Authority::from_str(unchecked_authority)?;
|
|
||||||
|
|
||||||
let _collection = if unchecked_collection.is_empty() { None }
|
|
||||||
else { Some(Nsid::new(unchecked_collection.to_string())?) };
|
|
||||||
|
|
||||||
let _rkey = if unchecked_rkey.is_empty() { None }
|
|
||||||
else { Some(RecordKey::new(unchecked_rkey.to_string())?) };
|
|
||||||
|
|
||||||
// Ok(Uri{ whole: whole.to_string(), authority, collection, rkey })
|
|
||||||
Ok(Uri { whole: whole.to_string() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Uri {
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
self.whole.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
70
atproto/src/types.rs
Normal file
70
atproto/src/types.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::error::{Error, ParseError};
|
||||||
|
|
||||||
|
macro_rules! basic_string_type {
|
||||||
|
($name:ident, $regex:literal, $max_len:literal) => {
|
||||||
|
pub struct $name { value: String, }
|
||||||
|
|
||||||
|
impl std::fmt::Display for $name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for $name {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.len() > $max_len {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Length { max: s.len(), got: $max_len },
|
||||||
|
object: stringify!($name).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! lazy_regex::regex_is_match!(
|
||||||
|
$regex,
|
||||||
|
s
|
||||||
|
) {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Format,
|
||||||
|
object: stringify!($name).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
value: s.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod did;
|
||||||
|
pub use did::Did;
|
||||||
|
mod cid;
|
||||||
|
pub use cid::Cid;
|
||||||
|
mod authority;
|
||||||
|
pub use authority::Authority;
|
||||||
|
mod datetime;
|
||||||
|
pub use datetime::Datetime;
|
||||||
|
mod record_key;
|
||||||
|
pub use record_key::RecordKey;
|
||||||
|
mod uri;
|
||||||
|
pub use uri::Uri;
|
||||||
|
|
||||||
|
basic_string_type!(Handle,
|
||||||
|
r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$",
|
||||||
|
253
|
||||||
|
);
|
||||||
|
basic_string_type!(Nsid,
|
||||||
|
r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$",
|
||||||
|
317
|
||||||
|
);
|
||||||
|
basic_string_type!(Tid,
|
||||||
|
r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$",
|
||||||
|
13
|
||||||
|
);
|
||||||
|
|
||||||
|
pub struct StrongRef<T> {
|
||||||
|
content: T,
|
||||||
|
cid: Cid,
|
||||||
|
}
|
||||||
38
atproto/src/types/authority.rs
Normal file
38
atproto/src/types/authority.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::{
|
||||||
|
types::{Did, Handle},
|
||||||
|
error::{Error, ParseError},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum Authority {
|
||||||
|
Did(Did),
|
||||||
|
Handle(Handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Authority {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
Authority::Did(did) => did.to_string(),
|
||||||
|
Authority::Handle(handle) => handle.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Authority {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(did) = s.parse::<Did>() {
|
||||||
|
return Ok(Authority::Did(did));
|
||||||
|
}
|
||||||
|
if let Ok(did) = s.parse::<Handle>() {
|
||||||
|
return Ok(Authority::Handle(did));
|
||||||
|
}
|
||||||
|
Err(Error::Parse {
|
||||||
|
err: ParseError::Format,
|
||||||
|
object: "Authority".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
atproto/src/types/cid.rs
Normal file
16
atproto/src/types/cid.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub struct Cid { value: String, }
|
||||||
|
|
||||||
|
impl std::fmt::Display for Cid {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Cid {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self { value: s.to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
63
atproto/src/types/datetime.rs
Normal file
63
atproto/src/types/datetime.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::error::{Error, ParseError, FormatError};
|
||||||
|
use time::{
|
||||||
|
UtcDateTime,
|
||||||
|
format_description::well_known::{Rfc3339, Iso8601},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
pub use time::{Date, Time};
|
||||||
|
|
||||||
|
pub struct Datetime {
|
||||||
|
time: UtcDateTime,
|
||||||
|
derived_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Datetime {
|
||||||
|
pub fn now() -> Result<Self, Error> {
|
||||||
|
Datetime::from_utc(UtcDateTime::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(date: Date, time: Time) -> Result<Self, Error> {
|
||||||
|
Datetime::from_utc(UtcDateTime::new(date, time))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_utc(utc: UtcDateTime) -> Result<Self, Error> {
|
||||||
|
Ok(Datetime {
|
||||||
|
time: utc,
|
||||||
|
derived_string: match utc.format(&Rfc3339) {
|
||||||
|
Ok(ds) => ds,
|
||||||
|
Err(e) => return Err(Error::Format {
|
||||||
|
err: FormatError::Datetime(e),
|
||||||
|
object: "Datetime".to_string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Datetime {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{}", self.derived_string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Datetime {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// Parse as both Rfc3339 and Iso8601 to ensure intersection
|
||||||
|
let dt_rfc3339 = UtcDateTime::parse(s, &Rfc3339);
|
||||||
|
let dt_iso8601 = UtcDateTime::parse(s, &Iso8601::DEFAULT);
|
||||||
|
|
||||||
|
let datetime = dt_iso8601
|
||||||
|
.and(dt_rfc3339)
|
||||||
|
.map_err(|e| Error::Parse {
|
||||||
|
err: ParseError::Datetime(e),
|
||||||
|
object: "Datetime".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Datetime { time: datetime, derived_string: s.to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
72
atproto/src/types/did.rs
Normal file
72
atproto/src/types/did.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
use crate::error::{Error, ParseError};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use lazy_regex::regex_captures;
|
||||||
|
|
||||||
|
enum DidMethod {
|
||||||
|
Web,
|
||||||
|
Plc,
|
||||||
|
}
|
||||||
|
impl Display for DidMethod {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
DidMethod::Web => String::from("web"),
|
||||||
|
DidMethod::Plc => String::from("plc"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for DidMethod {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"web" => Ok(DidMethod::Web),
|
||||||
|
"plc" => Ok(DidMethod::Plc),
|
||||||
|
_ => Err(Error::Parse {
|
||||||
|
err: ParseError::Format,
|
||||||
|
object: "DidMethod".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Did {
|
||||||
|
method: DidMethod,
|
||||||
|
identifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Did {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "did:{}:{}", self.method, self.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Did {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.len() > 2048 {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Length { max: s.len(), got: 2048 },
|
||||||
|
object: "Did".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((
|
||||||
|
_whole, unchecked_method, identifier
|
||||||
|
)) = regex_captures!(
|
||||||
|
r"^did:([a-z]+):([a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$",
|
||||||
|
s,
|
||||||
|
) else {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Format,
|
||||||
|
object: "Did".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
method: unchecked_method.parse::<DidMethod>()?,
|
||||||
|
identifier: identifier.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
63
atproto/src/types/record_key.rs
Normal file
63
atproto/src/types/record_key.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::{
|
||||||
|
types::{Nsid, Tid},
|
||||||
|
error::{Error, ParseError},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use lazy_regex::regex_is_match;
|
||||||
|
|
||||||
|
pub enum RecordKey {
|
||||||
|
Tid(Tid),
|
||||||
|
Nsid(Nsid),
|
||||||
|
Literal(String),
|
||||||
|
Any(String),
|
||||||
|
}
|
||||||
|
impl Display for RecordKey {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
RecordKey::Tid(tid) => tid.to_string(),
|
||||||
|
RecordKey::Nsid(nsid) => nsid.to_string(),
|
||||||
|
RecordKey::Literal(literal) => literal.to_string(),
|
||||||
|
RecordKey::Any(any) => any.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for RecordKey {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.len() > 512 {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Length { max: 512, got: s.len() },
|
||||||
|
object: "RecordKey".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(
|
||||||
|
regex_is_match!(
|
||||||
|
r"^[a-zA-Z0-9`.-_:~]+$",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
&& s != "."
|
||||||
|
&& s != ".."
|
||||||
|
) {
|
||||||
|
return Err(Error::Parse {
|
||||||
|
err: ParseError::Format,
|
||||||
|
object: "RecordKey".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid record key, now decide type
|
||||||
|
if s.starts_with("literal:") {
|
||||||
|
return Ok(RecordKey::Literal(s.to_string()));
|
||||||
|
}
|
||||||
|
if let Ok(tid) = s.parse::<Tid>() {
|
||||||
|
return Ok(RecordKey::Tid(tid));
|
||||||
|
}
|
||||||
|
if let Ok(nsid) = s.parse::<Nsid>() {
|
||||||
|
return Ok(RecordKey::Nsid(nsid));
|
||||||
|
}
|
||||||
|
Ok(RecordKey::Any(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
79
atproto/src/types/uri.rs
Normal file
79
atproto/src/types/uri.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::{
|
||||||
|
types::{Did, Authority, Nsid, RecordKey},
|
||||||
|
error::{Error, ParseError},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use lazy_regex::regex_captures;
|
||||||
|
|
||||||
|
pub struct Uri {
|
||||||
|
authority: Did,
|
||||||
|
collection: Option<Nsid>,
|
||||||
|
rkey: Option<RecordKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Uri {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "at://{}", self.authority)?;
|
||||||
|
|
||||||
|
if let Some(collection) = &self.collection {
|
||||||
|
write!(f, "/{}", collection)?;
|
||||||
|
|
||||||
|
if let Some(rkey) = &self.rkey {
|
||||||
|
write!(f, "/{}", rkey)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((
|
||||||
|
_whole, unchecked_authority, unchecked_collection, unchecked_rkey
|
||||||
|
)) = regex_captures!(
|
||||||
|
r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i",
|
||||||
|
s,
|
||||||
|
) 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 collection = if unchecked_collection.is_empty() { None }
|
||||||
|
else { Some(unchecked_collection.parse::<Nsid>()?) };
|
||||||
|
|
||||||
|
let rkey = if unchecked_rkey.is_empty() { None }
|
||||||
|
else { Some(unchecked_rkey.parse::<RecordKey>()?) };
|
||||||
|
|
||||||
|
Ok(Uri { authority: did, collection, rkey })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uri {
|
||||||
|
pub fn authority_as_did(&self) -> &Did {
|
||||||
|
&self.authority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "2.0.12"
|
|
||||||
atproto.workspace = true
|
atproto.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio"] }
|
sqlx.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
|
||||||
45
db/migrations/20250612223204_initial_schema.sql
Normal file
45
db/migrations/20250612223204_initial_schema.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
CREATE TABLE actor (
|
||||||
|
did VARCHAR PRIMARY KEY,
|
||||||
|
handle VARCHAR UNIQUE,
|
||||||
|
indexed_at VARCHAR NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX actor_handle_trgm_idx ON actor USING gist (handle gist_trgm_ops);
|
||||||
|
|
||||||
|
CREATE TABLE session (
|
||||||
|
uri VARCHAR PRIMARY KEY,
|
||||||
|
cid VARCHAR NOT NULL,
|
||||||
|
owner VARCHAR NOT NULL,
|
||||||
|
|
||||||
|
content VARCHAR NOT NULL,
|
||||||
|
contentcid VARCHAR NOT NULL,
|
||||||
|
label VARCHAR,
|
||||||
|
-- Participants in participant
|
||||||
|
|
||||||
|
created_at VARCHAR NOT NULL,
|
||||||
|
indexed_at VARCHAR NOT NULL,
|
||||||
|
sort_at VARCHAR GENERATED ALWAYS AS (LEAST(created_at,indexed_at)) STORED NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE activity (
|
||||||
|
uri VARCHAR PRIMARY KEY,
|
||||||
|
cid VARCHAR NOT NULL,
|
||||||
|
|
||||||
|
session VARCHAR,
|
||||||
|
sessioncid VARCHAR,
|
||||||
|
-- Progress in progress
|
||||||
|
|
||||||
|
performed_at VARCHAR,
|
||||||
|
created_at VARCHAR NOT NULL,
|
||||||
|
indexed_at VARCHAR NOT NULL,
|
||||||
|
sort_at VARCHAR GENERATED ALWAYS AS (LEAST(created_at,indexed_at)) STORED NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE participant (
|
||||||
|
participantdid VARCHAR NOT NULL,
|
||||||
|
session VARCHAR NOT NULL,
|
||||||
|
role VARCHAR NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -1,15 +1,3 @@
|
||||||
use sqlx::{
|
|
||||||
query,
|
|
||||||
Database,
|
|
||||||
Pool,
|
|
||||||
Postgres,
|
|
||||||
pool::PoolOptions,
|
|
||||||
postgres::{
|
|
||||||
PgConnectOptions,
|
|
||||||
PgSslMode,
|
|
||||||
},
|
|
||||||
Result,
|
|
||||||
};
|
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
pub struct Db<Db: Database> {
|
pub struct Db<Db: Database> {
|
||||||
|
|
@ -39,13 +27,6 @@ pub struct Session {
|
||||||
|
|
||||||
impl Db<Postgres> {
|
impl Db<Postgres> {
|
||||||
async fn connect() -> Result<Self> {
|
async fn connect() -> Result<Self> {
|
||||||
let conn = PgConnectOptions::new()
|
|
||||||
.host("localhost")
|
|
||||||
.port(5432)
|
|
||||||
.username("postgres")
|
|
||||||
.password("062217")
|
|
||||||
.database("anisky")
|
|
||||||
.ssl_mode(PgSslMode::Disable);
|
|
||||||
|
|
||||||
let pool = match PoolOptions::new().connect_with(conn).await {
|
let pool = match PoolOptions::new().connect_with(conn).await {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
|
|
|
||||||
8
db/src/error.rs
Normal file
8
db/src/error.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Database Implementation Error: {0}")]
|
||||||
|
Backend(#[from] sqlx::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::DbError;
|
use crate::Error;
|
||||||
use atproto::{
|
use atproto::{
|
||||||
Cid,
|
Cid,
|
||||||
Uri,
|
Uri,
|
||||||
|
|
@ -56,15 +56,15 @@ pub enum Progress {
|
||||||
|
|
||||||
pub async fn ingest_session(
|
pub async fn ingest_session(
|
||||||
db: PgPool, session: Session
|
db: PgPool, session: Session
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), Error> {
|
||||||
let mut transaction = db.begin().await?;
|
let mut transaction = db.begin().await?;
|
||||||
write_session(&mut transaction, session).await?;
|
write_session(&mut transaction, session).await?;
|
||||||
transaction.commit().await.map_err(DbError::Backend)
|
transaction.commit().await.map_err(Error::Backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_session(
|
async fn write_session(
|
||||||
tr: &mut PgTransaction<'_>, session: Session
|
tr: &mut PgTransaction<'_>, session: Session
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
let (contenturi, contentcid): (Option<Content>, String) =
|
let (contenturi, contentcid): (Option<Content>, String) =
|
||||||
match session.content {
|
match session.content {
|
||||||
|
|
@ -94,7 +94,7 @@ async fn write_session(
|
||||||
|
|
||||||
async fn write_participant(
|
async fn write_participant(
|
||||||
tr: &mut PgTransaction<'_>, participant: Participant, sessionuri: Uri
|
tr: &mut PgTransaction<'_>, participant: Participant, sessionuri: Uri
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), Error> {
|
||||||
let (participantType, user): (String, User) = match participant {
|
let (participantType, user): (String, User) = match participant {
|
||||||
Participant::Owner(user) => ("Owner".to_string(), user),
|
Participant::Owner(user) => ("Owner".to_string(), user),
|
||||||
Participant::Added(user) => ("Participant".to_string(), user),
|
Participant::Added(user) => ("Participant".to_string(), user),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub mod interfaces;
|
pub mod interfaces;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use crate::error::Error;
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum DbError {
|
|
||||||
#[error("Database Implementation Error: {0}")]
|
|
||||||
Backend(#[from] sqlx::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub struct db;
|
// pub struct db;
|
||||||
|
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -41,11 +41,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1746585402,
|
"lastModified": 1749695868,
|
||||||
"narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=",
|
"narHash": "sha256-debjTLOyqqsYOUuUGQsAHskFXH5+Kx2t3dOo/FCoNRA=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "72dd969389583664f87aa348b3458f2813693617",
|
"rev": "55f914d5228b5c8120e9e0f9698ed5b7214d09cd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
packages = (with pkgs; [
|
packages = (with pkgs; [
|
||||||
# The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt,
|
# The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt,
|
||||||
# rustdoc, rustfmt, and other tools.
|
# rustdoc, rustfmt, and other tools.
|
||||||
|
sqlx-cli
|
||||||
rustToolchain
|
rustToolchain
|
||||||
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]);
|
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue