Global, mono-binary to libraries and binaries

This separates the previous mono-binary setup into separate libraries
and binaries. Specifically it split the old since api/ingestor binary
into an Atproto, and DB library, as well as an api, and ingestor binary.

Atproto Lib
Was mostly untouched. The original URI implementation was changed to use
FromStr, otherwise only imports were changed.

DB Lib
Is mostly unused, so there wasn't much that needed to be changed. Some
new files were added so that future work on it can hit the ground
running.

Api Binary
Is almost entirely the same. Imports were changed and the ingestor code
of main was removed.

Ingestor Binary
Was almost entirely refactored. An interface to made injestors was
added, and it was modularized. The only shared code is in
Ingestor.start(), and collections.rs's macros, but that is mostly
boilerplate.
This commit is contained in:
Julia Lange 2025-05-22 15:22:43 -07:00
parent 45acaaa601
commit eb28549a0f
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto
31 changed files with 582 additions and 636 deletions

14
api/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "api"
version = "0.1.0"
edition = "2024"
[dependencies]
atproto.workspace = true
axum = { version = "0.8.3", features = ["json"] }
http = "1.3.1"
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true

35
api/src/main.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::router::{
Router,
Endpoint,
xrpc::{
QueryInput,
ProcedureInput,
Response,
error,
},
};
use atproto::Nsid;
use http::status::StatusCode;
mod router;
#[tokio::main]
async fn main() {
let subscriber = tracing_subscriber::FmtSubscriber::new();
let _ = tracing::subscriber::set_global_default(subscriber);
let mut router = Router::new();
let get_nsid = Nsid::new(String::from("me.woach.get")).expect("me.woach.get is a valid nsid");
let post_nsid = Nsid::new(String::from("me.woach.post")).expect("me.woach.post is a valid nsid");
router = router.add_endpoint(Endpoint::new_xrpc_query(get_nsid, test));
router = router.add_endpoint(Endpoint::new_xrpc_procedure(post_nsid, test2));
router.serve().await;
}
async fn test(_data: QueryInput) -> Response {
error(StatusCode::OK, "error", "message")
}
async fn test2(_data: ProcedureInput) -> Response {
error(StatusCode::OK, "error", "message")
}

59
api/src/router.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::router::xrpc::{
XrpcEndpoint,
XrpcHandler,
QueryInput,
ProcedureInput,
};
use atproto::Nsid;
use axum::Router as AxumRouter;
use core::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr};
use tokio::net::TcpListener;
pub struct Router {
addr: SocketAddr,
router: AxumRouter,
}
// In case server ever needs to support more than just XRPC
pub enum Endpoint {
Xrpc(XrpcEndpoint),
}
impl Endpoint {
pub fn new_xrpc_query<Q>(nsid: Nsid, query: Q) -> Self
where
Q: XrpcHandler<QueryInput> + Clone
{
Endpoint::Xrpc(XrpcEndpoint::new_query(nsid,query))
}
pub fn new_xrpc_procedure<P>(nsid: Nsid, procedure: P) -> Self
where
P: XrpcHandler<ProcedureInput> + Clone
{
Endpoint::Xrpc(XrpcEndpoint::new_procedure(nsid,procedure))
}
}
pub mod xrpc;
impl Router {
pub fn new() -> Self {
let mut router = AxumRouter::new();
router = XrpcEndpoint::not_implemented().add_to_router(router);
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127,0,0,1)), 6702);
Router { router, addr }
}
pub fn add_endpoint(mut self, endpoint: Endpoint) -> Self {
match endpoint {
Endpoint::Xrpc(ep) => self.router = ep.add_to_router(self.router),
};
self
}
pub async fn serve(self) {
let listener = TcpListener::bind(self.addr).await.unwrap();
axum::serve(listener, self.router).await.unwrap();
}
}

181
api/src/router/xrpc.rs Normal file
View file

@ -0,0 +1,181 @@
use std::{
collections::HashMap,
pin::Pin,
future::Future,
};
use atproto::Nsid;
use axum::{
extract::{
Json,
Query,
Request,
FromRequest,
FromRequestParts,
rejection::QueryRejection,
},
body::Bytes,
routing::{
get,
post,
method_routing::MethodRouter,
},
http::{
StatusCode,
request::Parts,
},
Router as axumRouter,
};
use serde_json::{Value, json};
enum Path {
Nsid(Nsid),
NotImplemented,
}
pub struct XrpcEndpoint {
path: Path,
resolver: MethodRouter,
}
pub type Response = (StatusCode, Json<Value>);
pub fn error(code: StatusCode, error: &str, message: &str) -> Response {
(
code,
Json(json!({
"error": error,
"message": message
}))
)
}
pub fn response(code: StatusCode, message: &str) -> Response {
error(code, "", message)
}
pub struct QueryInput {
parameters: HashMap<String, String>,
}
impl<S> FromRequestParts<S> for QueryInput
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S)
-> Result<Self, Self::Rejection> {
let query_params: Result<Query<HashMap<String, String>>, QueryRejection> = Query::try_from_uri(&parts.uri);
match query_params {
Ok(p) => Ok(QueryInput { parameters: p.0 }),
Err(e) => Err(error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text())),
}
}
}
pub struct ProcedureInput {
parameters: HashMap<String, String>,
input: Json<Value>,
}
impl<S> FromRequest<S> for ProcedureInput
where
Bytes: FromRequest<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, state: &S)
-> Result<Self, Self::Rejection> {
let query_params: Result<Query<HashMap<String, String>>, QueryRejection> = Query::try_from_uri(req.uri());
let parameters = match query_params {
Ok(p) => p.0,
Err(e) => return Err(error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text())),
};
let json_value = Json::<Value>::from_request(req, state).await;
let input: Json<Value> = match json_value {
Ok(v) => v,
Err(e) => return Err(error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text())),
};
Ok(ProcedureInput { parameters, input })
}
}
pub trait XrpcHandler<Input>: Send + Sync + 'static {
fn call(&self, input: Input)
-> Pin<Box<dyn Future<Output = Response> + Send>>;
}
impl<F, Fut> XrpcHandler<QueryInput> for F
where
F: Fn(QueryInput) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
fn call(&self, input: QueryInput)
-> Pin<Box<dyn Future<Output = Response>+ Send>> {
Box::pin((self)(input))
}
}
impl<F, Fut> XrpcHandler<ProcedureInput> for F
where
F: Fn(ProcedureInput) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
fn call(&self, input: ProcedureInput)
-> Pin<Box<dyn Future<Output = Response>+ Send>> {
Box::pin((self)(input))
}
}
impl XrpcEndpoint {
pub fn new_query<Q>(nsid: Nsid, query: Q) -> Self
where
Q: XrpcHandler<QueryInput> + Clone
{
XrpcEndpoint {
path: Path::Nsid(nsid),
resolver: get(async move | mut parts: Parts | -> Response {
match QueryInput::from_request_parts(&mut parts, &()).await {
Ok(qi) => query.call(qi).await,
Err(e) => e
}
})
}
}
pub fn new_procedure<P>(nsid: Nsid, procedure: P) -> Self
where
P: XrpcHandler<ProcedureInput> + Clone
{
XrpcEndpoint {
path: Path::Nsid(nsid),
resolver: post(async move | req: Request | -> Response {
match ProcedureInput::from_request(req, &()).await {
Ok(pi) => procedure.call(pi).await,
Err(e) => e
}
})
}
}
pub fn add_to_router(self, router: axumRouter) -> axumRouter {
let path = match self.path {
Path::Nsid(nsid) => &("/xrpc/".to_owned() + nsid.as_str()),
Path::NotImplemented => "/xrpc/{*nsid}",
};
router.route(path, self.resolver)
}
pub fn not_implemented() -> Self {
let resolver = (
StatusCode::NOT_IMPLEMENTED,
Json(json!({
"error": "MethodNotImplemented",
"message": "Method Not Implemented"
}))
);
XrpcEndpoint {
path: Path::NotImplemented,
resolver: get(resolver.clone()).post(resolver),
}
}
}