Database basics

This commit is contained in:
Riley Apeldoorn 2022-07-21 00:28:28 +02:00
parent 7d392ed3fa
commit 0969bff603
6 changed files with 216 additions and 138 deletions

View File

@ -15,6 +15,21 @@
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
@ -61,11 +76,47 @@
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"oxalica": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1655606998,
"narHash": "sha256-6XIQEwmoldCE3lzI54VQxD2tFJoeRsjRMYWkthtGRRw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4b600525be94c23b44c42441f33460343fafe7a1",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs_2",
"oxalica": "oxalica"
}
}
},

View File

@ -1,33 +1,38 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
};
inputs = {
flake-utils.url = "github:numtide/flake-utils";
oxalica.url = "github:oxalica/rust-overlay";
naersk.url = "github:nix-community/naersk";
};
outputs = { self, nixpkgs, flake-utils, naersk }:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = nixpkgs.legacyPackages."${system}";
naersk-lib = naersk.lib."${system}";
in
rec {
# `nix build`
packages."hermit" = naersk-lib.buildPackage {
pname = "hermit";
root = ./.;
};
defaultPackage = packages."hermit";
outputs = { self, nixpkgs, flake-utils, naersk, oxalica }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; overlays = [ oxalica.overlay ]; };
naersk-lib = naersk.lib."${system}";
in rec {
# `nix build`
packages."hermit" = naersk-lib.buildPackage {
pname = "hermit";
root = ./.;
};
defaultPackage = packages."hermit";
# `nix run`
apps."hermit"= flake-utils.lib.mkApp {
drv = packages."hermit";
};
defaultApp = apps."hermit";
# `nix run`
apps."hermit"= flake-utils.lib.mkApp {
drv = packages."hermit";
};
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo openssl pkgconfig ];
};
}
);
defaultApp = apps."hermit";
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(rust-bin.selectLatestNightlyWith (t: t.default))
openssl
pkgconfig
];
};
}
);
}

View File

@ -1,24 +1,25 @@
//! ActivityPub implementation code and related abstractions.
use futures::prelude::*;
use serde::Serialize;
use crate::{ Id, Activity, err, Result, Error, sign, ctx::Context };
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub enum Create {
Note { object: Note }
}
impl From<Create> for Activity { fn from (a: Create) -> Activity { Activity::Create (a) } }
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub enum Follow {
Actor { object: Actor }
}
impl From<Follow> for Activity { fn from (a: Follow) -> Activity { Activity::Follow (a) } }
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub enum Accept {
Follow { object: Follow }
}
@ -26,12 +27,12 @@ pub enum Accept {
impl From<Accept> for Activity { fn from (a: Accept) -> Activity { Activity::Accept (a) } }
/// An entity that publishes activities.
#[derive(Clone)]
#[derive(Clone, Serialize, sqlx::FromRow)]
pub struct Actor {
id: Id,
}
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub struct Note {
id: Id,
}

View File

@ -1,13 +1,17 @@
//! Database abstraction layer used by Hermit.
use crate::{ Id, Result };
use std::borrow::Borrow;
use std::{marker::PhantomData, pin::Pin};
use crate::{ Id, Result, ap::Actor, err };
use futures::prelude::*;
use sqlx::{Executor, pool::PoolConnection};
use sqlx::{Executor, pool::PoolConnection, database::HasArguments};
use sqlx::{ FromRow, Either::Right };
/// `const ()` but in Rust
fn void <T> (_: T) -> () { () }
type Database = sqlx::Postgres;
pub(crate) type Database = sqlx::Postgres;
/// Specifies how to connect to the database.
pub struct Config {}
@ -28,32 +32,27 @@ impl Client {
todo!()
}
/// Fetch the data mapped to the given `key` from the database.
pub async fn get <T> (&self, key: T::Key) -> Result<Option<T>>
where
T: Get,
{
self.with_conn(|c| T::get(key, c))
.await
pub async fn query <'e, T: 'e> (&self, query: Query<'e, T>) -> Pin<Box<dyn Stream<Item = Result<T>> + Send + 'e>> {
let Query (q, f) = query;
let stream = q
.fetch_many(&self.pool)
.filter_map(async move |r| {
match r.map_err(err) {
Ok (Right (row)) => Some (f(row)),
Err (error) => Some (Err (error)),
_ => None,
}
});
Box::pin (stream)
}
/// Perfom an insertion on the database.
pub async fn insert <T> (&mut self, data: T) -> Result<()>
where
T: Insert,
{
self.with_conn(|c| data.set(c))
.await
.map(void)
}
/// Delete something from the database.
pub async fn delete <T> (&mut self, key: T::Key) -> Result<()>
where
T: Delete,
{
self.with_conn(|c| T::del(key, c))
.await
pub async fn get <'q, T: 'q> (&self, query: Query<'q, T>) -> Result<Option<T>> {
self.query(query)
.await
.next()
.await
.transpose()
}
/// Handles the getting-a-connection logic.
@ -62,8 +61,6 @@ impl Client {
F: FnOnce (&mut PoolConnection<Database>) -> O,
O: Future<Output = Result<T>>,
{
use crate::err;
self.pool
.acquire()
.map_err(err)
@ -75,52 +72,24 @@ impl Client {
}
pub trait Object: Sized {
type Key: Eq;
fn key (&self) -> &Self::Key;
}
pub trait Insert: Object {
type Future: Future<Output = Result<Self::Key>>;
fn set <'e, E> (self, exec: E) -> Self::Future
where
E: Executor<'e>;
}
pub trait Delete: Object {
type Future: Future<Output = Result<()>>;
fn del <'e, E> (key: Self::Key, exec: E) -> Self::Future where E: Executor<'e>;
}
pub trait Get: Object {
type Future: Future<Output = Result<Option<Self>>>;
fn get <'e, E> (key: Self::Key, exec: E) -> Self::Future where E: Executor<'e>;
}
pub mod ops {
//! Database operations (queries and updates).
use super::*;
pub struct Following {
pub from: Id,
pub to: Id,
pub id: Id,
}
impl Object for Following {
type Key = Id;
fn key (&self) -> &Self::Key { &self.id }
}
impl Insert for Following {
type Future = future::BoxFuture<'static, Result<Id>>;
fn set <'e, E> (self, exec: E) -> Self::Future
where
E: Executor<'e>
{
todo!()
}
}
type Q<'a> = sqlx::query::Query<'a, Database, <Database as HasArguments<'a>>::Arguments>;
type Row = <Database as sqlx::Database>::Row;
pub struct Query <'a, T> (Q<'a>, fn (Row) -> Result<T>);
/// Generate a query that gets an [`Actor`] by its [`Id`].
pub fn get_actor <'a> (id: &'a Id) -> Query<'a, Actor> {
// Prepare a query
let query = sqlx::query("select * from actors where id = $1")
.bind(id);
// Return an sql query which will result in a series of rows,
// and a decoder function that will translate each row to a
// value of type `Actor`.
Query (query, |row: Row| {
let data = Actor::from_row(&row)?;
Ok (data)
})
}

View File

@ -1,7 +1,11 @@
#![feature(async_closure)]
//! # The Hermit ActivityPub server
//!
//! This library contains the types and trait impls that make up the ActivityPub
//! support and database interaction for the Hermit ActivityPub server.
#![feature(generic_associated_types)]
use serde::Serialize;
// Expose the `Id` type in the crate root
pub use id::Id;
@ -14,7 +18,7 @@ pub mod db;
pub mod ap;
/// The Activity supertype used in abstractions over any kind of activity.
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub enum Activity {
/// Create a post.
Create (ap::Create),
@ -62,18 +66,19 @@ pub (crate) fn err (e: impl Into<Error>) -> Error { e.into() }
mod id {
use std::str::FromStr;
use std::{str::FromStr, error::Error};
use serde::{ Deserialize, Serialize };
use sqlx::database::{HasArguments, HasValueRef};
use url::ParseError;
use crate::db;
/// An ActivityPub identifier.
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Id (reqwest::Url);
impl crate::IntoUrl for Id {
fn into_url (self) -> Option<url::Url> { Some (self.0) }
}
impl FromStr for Id {
type Err = url::ParseError;
@ -82,6 +87,31 @@ mod id {
}
}
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_string())
}
}
impl sqlx::Type<db::Database> for Id {
fn type_info () -> <db::Database as sqlx::Database>::TypeInfo {
String::type_info()
}
}
impl<'q> sqlx::Encode<'q, db::Database> for Id {
fn encode_by_ref(&self, buf: &mut <db::Database as HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.0.to_string().encode_by_ref(buf)
}
}
impl<'r> sqlx::Decode<'r, db::Database> for Id {
fn decode(value: <db::Database as HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
<String as sqlx::Decode<db::Database>>::decode(value)
.map(|s| s.parse().expect("Failed to parse ID as URL"))
.map(Id)
}
}
}
mod ctx {
@ -89,9 +119,9 @@ mod ctx {
use std::sync::Arc;
use futures::prelude::*;
use serde_json::Value;
use serde_json::{Value, to_value};
use crate::{ conf::Config, db, Result, sign::Sign, ap, Activity };
use crate::{ conf::Config, db, Result, sign::Sign, ap::{self, Actor}, Activity, Id, err };
/// The context of a thread/task.
///
@ -224,6 +254,20 @@ mod ctx {
Ok (value)
}
/// Fetch a value from the database by trying all the ActivityPub
/// records.
async fn db_fetch (&self, id: Id) -> Result<Option<Value>> {
if let Some (data) = self.db.get(db::get_actor(&id)).await? {
return to_value(data)
.map_err(err)
.map(Some)
}
todo!()
}
/// Attempt to dereference to a [`Create`](ap::Create) activity.

View File

@ -1,11 +1,11 @@
use std::sync::Arc;
use futures::stream;
use hermit::{ Context, Error, db, sign, Activity, };
use hermit::conf::Config;
use tokio::sync::{mpsc, broadcast};
use task::Executor;
use tokio_stream::wrappers::{ReceiverStream, BroadcastStream};
use tokio_stream::wrappers::ReceiverStream;
/// Module that contains all the API endpoints and frontend pages
/// used by Hermit.
@ -68,6 +68,8 @@ async fn main () {
ctrl_tx,
});
// Redefine `ctrl_tx`: it is now the transmitter that transmits
// *from* `Ctrl`.
let (ctrl_tx, _rx) = broadcast::channel(256);
ctx.run (task::Auto {
@ -93,32 +95,28 @@ fn mk_channel <T> (size: usize) -> (mpsc::Sender<T>, ReceiverStream<T>) {
(tx, rx)
}
fn err (e: impl Into<Error>) -> Error { e.into() }
mod task {
//! Async tasks, communicating with each other across threads through generic
//! streams and sinks.
use std::pin::Pin;
use tokio::sync::{mpsc, broadcast};
use tokio::sync::{
broadcast,
mpsc,
};
use futures::prelude::*;
use serde_json::Value;
use crate::web;
use crate::sign::Sign;
use crate::{flow::Flow, Activity, ctrl::Message, Context};
use crate::{
flow::Flow,
ctrl::Message,
sign::Sign,
Activity,
Context,
web,
};
impl<S> Executor for Context<S>
where
S: Sign + Send + Sync + 'static
{
fn run (&self, task: impl Task) {
let ctx: Context<S> = self.clone();
tokio::spawn(task.run(ctx));
}
}
/// Something that can execute a task.
/// Something that can execute a [`Task`].
pub trait Executor {
/// Perform a [`Task`].
@ -137,6 +135,16 @@ mod task {
where
S: Sign + Send + Sync + 'static;
}
impl<S> Executor for Context<S>
where
S: Sign + Send + Sync + 'static
{
fn run (&self, task: impl Task) {
let ctx: Context<S> = self.clone();
tokio::spawn(task.run(ctx));
}
}
/// The main web server.