Add derive macros to store
This commit is contained in:
parent
c5bd6a127e
commit
bcdd5e6059
14 changed files with 378 additions and 70 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -888,6 +888,12 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macro"
|
name = "macro"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.60",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
|
@ -1134,6 +1140,8 @@ dependencies = [
|
||||||
name = "puppy"
|
name = "puppy"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
|
"chrono",
|
||||||
"fetch",
|
"fetch",
|
||||||
"store",
|
"store",
|
||||||
]
|
]
|
||||||
|
@ -1494,6 +1502,7 @@ dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"either",
|
||||||
"macro",
|
"macro",
|
||||||
"rocksdb",
|
"rocksdb",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
|
@ -1,31 +1,26 @@
|
||||||
use puppy::{
|
use puppy::{
|
||||||
store::{
|
model::{schema, Bite, FollowRequest, Follows, Profile, Username},
|
||||||
self,
|
store::{self, Error},
|
||||||
alias::Username,
|
|
||||||
arrow::{FollowRequested, Follows},
|
|
||||||
mixin::Profile,
|
|
||||||
Error,
|
|
||||||
},
|
|
||||||
tl::Post,
|
tl::Post,
|
||||||
Bite, Key, Store,
|
Key, Store,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> store::Result<()> {
|
fn main() -> store::Result<()> {
|
||||||
// Store::nuke(".state")?;
|
// Store::nuke(".state")?;
|
||||||
let db = Store::open(".state")?;
|
let db = Store::open(".state", schema())?;
|
||||||
println!("creating actors");
|
println!("creating actors");
|
||||||
let riley = get_or_create_actor(&db, "riley")?;
|
let riley = get_or_create_actor(&db, "riley")?;
|
||||||
let linen = get_or_create_actor(&db, "linen")?;
|
let linen = get_or_create_actor(&db, "linen")?;
|
||||||
if false {
|
if true {
|
||||||
println!("creating posts");
|
println!("creating posts");
|
||||||
puppy::create_post(&db, riley, "@linen <3")?;
|
puppy::create_post(&db, riley, "@linen <3")?;
|
||||||
puppy::create_post(&db, linen, "@riley <3")?;
|
puppy::create_post(&db, linen, "@riley <3")?;
|
||||||
}
|
}
|
||||||
if false {
|
if true {
|
||||||
println!("making riley follow linen");
|
println!("making riley follow linen");
|
||||||
if !db.exists::<Follows>((riley, linen))? {
|
if !db.exists::<Follows>(riley, linen)? {
|
||||||
println!("follow relation does not exist yet");
|
println!("follow relation does not exist yet");
|
||||||
if !db.exists::<FollowRequested>((riley, linen))? {
|
if !db.exists::<FollowRequest>(riley, linen)? {
|
||||||
println!("no pending follow request; creating");
|
println!("no pending follow request; creating");
|
||||||
puppy::fr::create(&db, riley, linen)?;
|
puppy::fr::create(&db, riley, linen)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,44 +31,46 @@ fn main() -> store::Result<()> {
|
||||||
println!("riley already follows linen");
|
println!("riley already follows linen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Posts on the instance:");
|
println!("\nPosts on the instance:");
|
||||||
for Post {
|
for Post {
|
||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
author,
|
author,
|
||||||
} in puppy::tl::fetch_all(&db)?
|
} in puppy::tl::fetch_all(&db)?
|
||||||
{
|
{
|
||||||
let (_, Profile { account_name, .. }) = db.lookup(author)?;
|
let Profile { account_name, .. } = db.get_mixin(author)?.unwrap();
|
||||||
let content = content.content.unwrap();
|
let content = content.content.unwrap();
|
||||||
println!("- {id} by @{account_name} ({author}):\n{content}",)
|
println!("- {id} by @{account_name} ({author}):\n{content}",)
|
||||||
}
|
}
|
||||||
println!("Linen's followers:");
|
println!("\nLinen's followers:");
|
||||||
for id in puppy::fr::followers_of(&db, linen)? {
|
for id in puppy::fr::followers_of(&db, linen)? {
|
||||||
let (_, Profile { account_name, .. }) = db.lookup(id)?;
|
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
println!("- @{account_name} ({id})");
|
println!("- @{account_name} ({id})");
|
||||||
}
|
}
|
||||||
println!("Riley's following:");
|
println!("\nRiley's following:");
|
||||||
for id in puppy::fr::following_of(&db, riley)? {
|
for id in puppy::fr::following_of(&db, riley)? {
|
||||||
let (_, Profile { account_name, .. }) = db.lookup(id)?;
|
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
println!("- @{account_name} ({id})");
|
println!("- @{account_name} ({id})");
|
||||||
}
|
}
|
||||||
println!("Biting riley");
|
if false {
|
||||||
puppy::bite_actor(&db, linen, riley).unwrap();
|
println!("Biting riley");
|
||||||
for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() {
|
puppy::bite_actor(&db, linen, riley).unwrap();
|
||||||
let (_, Profile { account_name, .. }) = db.lookup(biter).unwrap();
|
for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() {
|
||||||
println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
||||||
|
println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
store::OK
|
store::OK
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> {
|
fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> {
|
||||||
let user = db.translate::<Username>(username);
|
let user = db.lookup(Username(username.to_string()));
|
||||||
match user {
|
match user {
|
||||||
Ok(key) => {
|
Ok(Some(key)) => {
|
||||||
println!("found '{username}' ({key})");
|
println!("found '{username}' ({key})");
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
Err(Error::Missing) => {
|
Ok(None) => {
|
||||||
println!("'{username}' doesn't exist yet, creating");
|
println!("'{username}' doesn't exist yet, creating");
|
||||||
let r = puppy::create_actor(&db, username);
|
let r = puppy::create_actor(&db, username);
|
||||||
if let Ok(ref key) = r {
|
if let Ok(ref key) = r {
|
||||||
|
|
|
@ -5,3 +5,9 @@ edition = "2021"
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = '2', features = ['full'] }
|
||||||
|
quote = '*'
|
||||||
|
proc-macro2 = '*'
|
||||||
|
heck = '*'
|
||||||
|
|
142
lib/macro/src/arrow.rs
Normal file
142
lib/macro/src/arrow.rs
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
use heck::AsKebabCase;
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use syn::{parse_macro_input, Data, DeriveInput, Field, Ident};
|
||||||
|
|
||||||
|
pub fn arrow(item: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(item as DeriveInput);
|
||||||
|
let Data::Struct(structure) = input.data else {
|
||||||
|
panic!("Only structs are supported as arrows")
|
||||||
|
};
|
||||||
|
match structure.fields {
|
||||||
|
syn::Fields::Named(fields) => from_named(&input.ident, fields),
|
||||||
|
syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => {
|
||||||
|
let first = f.unnamed.first().unwrap();
|
||||||
|
from_newtype(&input.ident, first)
|
||||||
|
}
|
||||||
|
_ => panic!(
|
||||||
|
"Only newtype structs and structs with named fields can have a derived arrow impl"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_named(name: &Ident, fields: syn::FieldsNamed) -> TokenStream {
|
||||||
|
let (origin, target, identity) = extract_idents(fields);
|
||||||
|
match identity {
|
||||||
|
Some(id) => make_multi_arrow(name, origin, target, id),
|
||||||
|
None => make_basic_arrow(name, origin, target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_basic_arrow(name: &Ident, origin: Ident, target: Ident) -> TokenStream {
|
||||||
|
let spec = gen_spec(name);
|
||||||
|
TokenStream::from(quote! {
|
||||||
|
#spec
|
||||||
|
impl store::arrow::Arrow for #name {}
|
||||||
|
impl From<store::arrow::Basic> for #name {
|
||||||
|
fn from(v: store::arrow::Basic) -> #name {
|
||||||
|
#name {
|
||||||
|
#origin: v.origin,
|
||||||
|
#target: v.target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<#name> for store::arrow::Basic {
|
||||||
|
fn from(v: #name) -> store::arrow::Basic {
|
||||||
|
store::arrow::Basic {
|
||||||
|
origin: v.#origin,
|
||||||
|
target: v.#target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_multi_arrow(name: &Ident, origin: Ident, target: Ident, id: Ident) -> TokenStream {
|
||||||
|
let spec = gen_spec(name);
|
||||||
|
TokenStream::from(quote! {
|
||||||
|
#spec
|
||||||
|
impl store::arrow::Arrow for #name {
|
||||||
|
type Kind = store::arrow::Multi;
|
||||||
|
}
|
||||||
|
impl From<store::arrow::Multi> for #name {
|
||||||
|
fn from(v: store::arrow::Multi) -> #name {
|
||||||
|
#name {
|
||||||
|
#id: v.identity,
|
||||||
|
#origin: v.origin,
|
||||||
|
#target: v.target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<#name> for store::arrow::Multi {
|
||||||
|
fn from(v: #name) -> store::arrow::Multi {
|
||||||
|
store::arrow::Multi {
|
||||||
|
identity: v.#id,
|
||||||
|
origin: v.#origin,
|
||||||
|
target: v.#target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_idents(fields: syn::FieldsNamed) -> (Ident, Ident, Option<Ident>) {
|
||||||
|
let origin = extract_ident("origin", &fields).unwrap();
|
||||||
|
let target = extract_ident("target", &fields).unwrap();
|
||||||
|
let id = extract_ident("identity", &fields);
|
||||||
|
(origin, target, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_ident(name: &str, fields: &syn::FieldsNamed) -> Option<Ident> {
|
||||||
|
// Prefer marked fields and default to correctly named fields.
|
||||||
|
fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.find(|field| {
|
||||||
|
field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|attr| attr.meta.path().get_ident())
|
||||||
|
.any(|id| id == name)
|
||||||
|
})
|
||||||
|
.and_then(|f| f.ident.clone())
|
||||||
|
.or_else(|| {
|
||||||
|
fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| f.ident.clone())
|
||||||
|
.find(|id| id == name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_spec(name: &Ident) -> impl ToTokens {
|
||||||
|
let prefix = AsKebabCase(name.to_string());
|
||||||
|
let by_origin = format!("{prefix}/by-origin");
|
||||||
|
let by_target = format!("{prefix}/by-target");
|
||||||
|
quote! {
|
||||||
|
impl store::types::Value for #name {
|
||||||
|
type Type = store::types::ArrowSpec;
|
||||||
|
const SPEC: Self::Type = store::types::ArrowSpec {
|
||||||
|
by_origin: store::types::Namespace(#by_origin),
|
||||||
|
by_target: store::types::Namespace(#by_target),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_newtype(name: &Ident, field: &Field) -> TokenStream {
|
||||||
|
let spec = gen_spec(name);
|
||||||
|
let typ = &field.ty;
|
||||||
|
TokenStream::from(quote! {
|
||||||
|
#spec
|
||||||
|
impl store::arrow::Arrow for #name {
|
||||||
|
type Kind = #typ;
|
||||||
|
}
|
||||||
|
impl From<#typ> for #name {
|
||||||
|
fn from(v: #typ) -> #name { #name(v) }
|
||||||
|
}
|
||||||
|
impl From<#name> for #typ {
|
||||||
|
fn from(v: #name) -> #typ { v.0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,16 +1,73 @@
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
mod arrow;
|
||||||
|
|
||||||
#[proc_macro_derive(Arrow, attributes(origin, target, identity))]
|
#[proc_macro_derive(Arrow, attributes(origin, target, identity))]
|
||||||
pub fn arrow(item: TokenStream) -> TokenStream {
|
pub fn arrow(item: TokenStream) -> TokenStream {
|
||||||
TokenStream::new()
|
arrow::arrow(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Alias)]
|
#[proc_macro_derive(Alias)]
|
||||||
pub fn alias(item: TokenStream) -> TokenStream {
|
pub fn alias(item: TokenStream) -> TokenStream {
|
||||||
TokenStream::new()
|
let input = syn::parse_macro_input!(item as syn::DeriveInput);
|
||||||
|
let syn::Data::Struct(structure) = input.data else {
|
||||||
|
panic!("Only structs are supported as aliases")
|
||||||
|
};
|
||||||
|
match structure.fields {
|
||||||
|
syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => {
|
||||||
|
let first = f.unnamed.first().unwrap();
|
||||||
|
make_alias_impl(&input.ident, first)
|
||||||
|
}
|
||||||
|
_ => panic!("Only string newtype structs are allowed as aliases"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_alias_impl(name: &syn::Ident, field: &syn::Field) -> TokenStream {
|
||||||
|
let typ = &field.ty;
|
||||||
|
let prefix = heck::AsKebabCase(name.to_string());
|
||||||
|
let keyspace = format!("{prefix}/keyspace");
|
||||||
|
let reversed = format!("{prefix}/reversed");
|
||||||
|
let spec = quote::quote! {
|
||||||
|
impl store::types::Value for #name {
|
||||||
|
type Type = store::types::AliasSpec;
|
||||||
|
const SPEC: Self::Type = store::types::AliasSpec {
|
||||||
|
keyspace: store::types::Namespace(#keyspace),
|
||||||
|
reversed: store::types::Namespace(#reversed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(quote::quote! {
|
||||||
|
#spec
|
||||||
|
impl store::Alias for #name {}
|
||||||
|
impl AsRef<str> for #name {
|
||||||
|
fn as_ref(&self) -> &str { self.0.as_ref() }
|
||||||
|
}
|
||||||
|
impl From<#typ> for #name {
|
||||||
|
fn from(v: #typ) -> #name { #name(v) }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Mixin)]
|
#[proc_macro_derive(Mixin)]
|
||||||
pub fn mixin(item: TokenStream) -> TokenStream {
|
pub fn mixin(item: TokenStream) -> TokenStream {
|
||||||
TokenStream::new()
|
let input = syn::parse_macro_input!(item as syn::DeriveInput);
|
||||||
|
|
||||||
|
let name = input.ident;
|
||||||
|
let prefix = heck::AsKebabCase(name.to_string());
|
||||||
|
let keyspace = format!("{prefix}/main");
|
||||||
|
|
||||||
|
let spec = quote::quote! {
|
||||||
|
impl store::types::Value for #name {
|
||||||
|
type Type = store::types::MixinSpec;
|
||||||
|
const SPEC: Self::Type = store::types::MixinSpec {
|
||||||
|
keyspace: store::types::Namespace(#keyspace),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(quote::quote! {
|
||||||
|
#spec
|
||||||
|
impl store::Mixin for #name {}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,5 @@ path = "src/lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
store = { path = "../store" }
|
store = { path = "../store" }
|
||||||
fetch = { path = "../fetch" }
|
fetch = { path = "../fetch" }
|
||||||
|
bincode = "2.0.0-rc.3"
|
||||||
|
chrono = "*"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
|
use model::{AuthorOf, Bite, Content, Profile, Username};
|
||||||
|
use store::util::{key, IterExt as _};
|
||||||
pub use store::{self, Key, Store};
|
pub use store::{self, Key, Store};
|
||||||
|
|
||||||
pub mod model {
|
pub mod model {
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use store::{types::Schema, Key};
|
use store::{types::Schema, Key};
|
||||||
|
|
||||||
#[derive(store::Mixin)]
|
#[derive(store::Mixin, Encode, Decode)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub post_count: usize,
|
pub post_count: usize,
|
||||||
pub account_name: String,
|
pub account_name: String,
|
||||||
|
@ -13,13 +16,13 @@ pub mod model {
|
||||||
pub about_fields: Vec<(String, String)>,
|
pub about_fields: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(store::Mixin)]
|
#[derive(store::Mixin, Encode, Decode)]
|
||||||
pub struct Content {
|
pub struct Content {
|
||||||
pub content: Option<String>,
|
pub content: Option<String>,
|
||||||
pub summary: Option<String>,
|
pub summary: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(store::Arrow, Clone, Copy)]
|
#[derive(store::Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct AuthorOf {
|
pub struct AuthorOf {
|
||||||
#[origin]
|
#[origin]
|
||||||
pub author: Key,
|
pub author: Key,
|
||||||
|
@ -27,7 +30,7 @@ pub mod model {
|
||||||
pub object: Key,
|
pub object: Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(store::Arrow, Clone, Copy)]
|
#[derive(store::Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct Follows {
|
pub struct Follows {
|
||||||
#[origin]
|
#[origin]
|
||||||
pub follower: Key,
|
pub follower: Key,
|
||||||
|
@ -35,7 +38,7 @@ pub mod model {
|
||||||
pub followed: Key,
|
pub followed: Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(store::Arrow, Clone, Copy)]
|
#[derive(store::Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct Bite {
|
pub struct Bite {
|
||||||
#[identity]
|
#[identity]
|
||||||
pub id: Key,
|
pub id: Key,
|
||||||
|
@ -45,7 +48,7 @@ pub mod model {
|
||||||
pub victim: Key,
|
pub victim: Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(store::Arrow, Clone, Copy)]
|
#[derive(store::Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct FollowRequest {
|
pub struct FollowRequest {
|
||||||
#[identity]
|
#[identity]
|
||||||
pub id: Key,
|
pub id: Key,
|
||||||
|
@ -109,7 +112,11 @@ pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
|
||||||
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
|
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
|
||||||
db.run(|tx| {
|
db.run(|tx| {
|
||||||
tx.outgoing::<AuthorOf>(author)
|
tx.outgoing::<AuthorOf>(author)
|
||||||
.bind_results(|arr| tx.get_mixin::<Content>(arr.object))
|
.map_ok(|a| a.object)
|
||||||
|
.filter_bind_results(|post| {
|
||||||
|
let thing = tx.get_mixin(post)?;
|
||||||
|
Ok(thing.map(key(post)))
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -129,7 +136,9 @@ pub fn bites_on(db: &Store, victim: Key) -> store::Result<Vec<Bite>> {
|
||||||
pub mod tl {
|
pub mod tl {
|
||||||
//! Timelines
|
//! Timelines
|
||||||
|
|
||||||
use store::{arrow::AuthorOf, mixin::Content, util::IterExt as _, Error, Key, Result, Store};
|
use store::{util::IterExt as _, Error, Key, Result, Store};
|
||||||
|
|
||||||
|
use crate::model::{AuthorOf, Content};
|
||||||
|
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: Key,
|
pub id: Key,
|
||||||
|
@ -139,9 +148,10 @@ pub mod tl {
|
||||||
|
|
||||||
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
|
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
|
||||||
db.run(|tx| {
|
db.run(|tx| {
|
||||||
let iter = tx.list::<Content>();
|
let iter = tx.range::<Content>(..);
|
||||||
iter.bind_results(|(id, content)| {
|
iter.bind_results(|(id, content)| {
|
||||||
let author = tx.incoming::<AuthorOf>(id).next_or(Error::Missing)?;
|
let AuthorOf { author, .. } =
|
||||||
|
tx.incoming::<AuthorOf>(id).next_or(Error::Missing)?;
|
||||||
Ok(Post {
|
Ok(Post {
|
||||||
id,
|
id,
|
||||||
author,
|
author,
|
||||||
|
@ -185,31 +195,35 @@ pub mod fr {
|
||||||
|
|
||||||
pub fn reject(db: &Store, requester: Key, target: Key) -> store::Result<()> {
|
pub fn reject(db: &Store, requester: Key, target: Key) -> store::Result<()> {
|
||||||
db.run(|tx| {
|
db.run(|tx| {
|
||||||
tx.remove_arrow::<FollowRequested>((requester, target))?;
|
tx.delete_all::<FollowRequest>(requester, target)?;
|
||||||
OK
|
OK
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<Key>> {
|
pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<FollowRequest>> {
|
||||||
db.transaction(|tx| tx.list_incoming::<FollowRequested>(target).keys().collect())
|
db.incoming::<FollowRequest>(target).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
||||||
db.transaction(|tx| tx.list_outgoing::<Follows>(actor).keys().collect())
|
db.outgoing::<Follows>(actor)
|
||||||
|
.map_ok(|a| a.followed)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
||||||
db.transaction(|tx| tx.list_incoming::<Follows>(actor).keys().collect())
|
db.incoming::<Follows>(actor)
|
||||||
|
.map_ok(|a| a.follower)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use store::{
|
use store::{Key, Store, OK};
|
||||||
arrow::{FollowRequested, Follows},
|
|
||||||
Key, Store, OK,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::create_actor;
|
use crate::{
|
||||||
|
create_actor,
|
||||||
|
model::{schema, FollowRequest, Follows},
|
||||||
|
};
|
||||||
|
|
||||||
fn make_test_actors(db: &Store) -> store::Result<(Key, Key)> {
|
fn make_test_actors(db: &Store) -> store::Result<(Key, Key)> {
|
||||||
let alice = create_actor(&db, "alice")?;
|
let alice = create_actor(&db, "alice")?;
|
||||||
|
@ -220,18 +234,21 @@ pub mod fr {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_fr() -> store::Result<()> {
|
fn create_fr() -> store::Result<()> {
|
||||||
Store::with_tmp(|db| {
|
Store::test(schema(), |db| {
|
||||||
let (alice, bob) = make_test_actors(&db)?;
|
let (alice, bob) = make_test_actors(&db)?;
|
||||||
super::create(&db, alice, bob)?;
|
super::create(&db, alice, bob)?;
|
||||||
assert!(
|
assert!(
|
||||||
db.exists::<FollowRequested>((alice, bob))?,
|
db.exists::<FollowRequest>(alice, bob)?,
|
||||||
"(alice -> bob) ∈ follow-requested"
|
"(alice -> bob) ∈ follow-requested"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!db.exists::<Follows>((alice, bob))?,
|
!db.exists::<Follows>(alice, bob)?,
|
||||||
"(alice -> bob) ∉ follows"
|
"(alice -> bob) ∉ follows"
|
||||||
);
|
);
|
||||||
let pending_for_bob = super::list_pending(&db, bob)?;
|
let pending_for_bob = super::list_pending(&db, bob)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|fr| fr.origin)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(pending_for_bob, vec![alice], "bob.pending = {{alice}}");
|
assert_eq!(pending_for_bob, vec![alice], "bob.pending = {{alice}}");
|
||||||
OK
|
OK
|
||||||
})
|
})
|
||||||
|
@ -239,17 +256,17 @@ pub mod fr {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accept_fr() -> store::Result<()> {
|
fn accept_fr() -> store::Result<()> {
|
||||||
Store::with_tmp(|db| {
|
Store::test(schema(), |db| {
|
||||||
let (alice, bob) = make_test_actors(&db)?;
|
let (alice, bob) = make_test_actors(&db)?;
|
||||||
super::create(&db, alice, bob)?;
|
super::create(&db, alice, bob)?;
|
||||||
super::accept(&db, alice, bob)?;
|
super::accept(&db, alice, bob)?;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
db.exists::<Follows>((alice, bob))?,
|
db.exists::<Follows>(alice, bob)?,
|
||||||
"(alice -> bob) ∈ follows"
|
"(alice -> bob) ∈ follows"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!db.exists::<Follows>((bob, alice))?,
|
!db.exists::<Follows>(bob, alice)?,
|
||||||
"(bob -> alice) ∉ follows"
|
"(bob -> alice) ∉ follows"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -265,7 +282,7 @@ pub mod fr {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn listing_follow_relations() -> store::Result<()> {
|
fn listing_follow_relations() -> store::Result<()> {
|
||||||
Store::with_tmp(|db| {
|
Store::test(schema(), |db| {
|
||||||
let (alice, bob) = make_test_actors(&db)?;
|
let (alice, bob) = make_test_actors(&db)?;
|
||||||
super::create(&db, alice, bob)?;
|
super::create(&db, alice, bob)?;
|
||||||
super::accept(&db, alice, bob)?;
|
super::accept(&db, alice, bob)?;
|
||||||
|
|
|
@ -13,3 +13,4 @@ bincode = "2.0.0-rc.3"
|
||||||
chrono = "*"
|
chrono = "*"
|
||||||
tempfile = "*"
|
tempfile = "*"
|
||||||
macro = { path = "../macro" }
|
macro = { path = "../macro" }
|
||||||
|
either = "*"
|
||||||
|
|
|
@ -93,20 +93,18 @@ impl Store {
|
||||||
op::exists::<A>(self, origin, target)
|
op::exists::<A>(self, origin, target)
|
||||||
}
|
}
|
||||||
/// Get all arrows of type `A` that point at `target`.
|
/// Get all arrows of type `A` that point at `target`.
|
||||||
pub fn incoming<'a, A>(&'a self, target: Key) -> impl Iterator<Item = Result<A::Kind>> + 'a
|
pub fn incoming<'a, A>(&'a self, target: Key) -> impl Iterator<Item = Result<A>> + 'a
|
||||||
where
|
where
|
||||||
A::Kind: 'a,
|
A: Arrow + 'a,
|
||||||
A: Arrow,
|
|
||||||
{
|
{
|
||||||
op::incoming::<A>(self, target)
|
op::incoming::<A>(self, target).map_ok(A::from)
|
||||||
}
|
}
|
||||||
/// Get all arrows of type `A` that point away from `origin`.
|
/// Get all arrows of type `A` that point away from `origin`.
|
||||||
pub fn outgoing<'a, A>(&'a self, origin: Key) -> impl Iterator<Item = Result<A::Kind>> + 'a
|
pub fn outgoing<'a, A>(&'a self, origin: Key) -> impl Iterator<Item = Result<A>> + 'a
|
||||||
where
|
where
|
||||||
A::Kind: 'a,
|
A: Arrow + 'a,
|
||||||
A: Arrow,
|
|
||||||
{
|
{
|
||||||
op::outgoing::<A>(self, origin)
|
op::outgoing::<A>(self, origin).map_ok(A::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,26 @@ where
|
||||||
.full_iterator(&self.cf, IteratorMode::Start)
|
.full_iterator(&self.cf, IteratorMode::Start)
|
||||||
.map_err(Error::Internal)
|
.map_err(Error::Internal)
|
||||||
}
|
}
|
||||||
|
/// Execute a range scan
|
||||||
|
pub fn range<const N: usize>(
|
||||||
|
&self,
|
||||||
|
lower: [u8; N],
|
||||||
|
upper: [u8; N],
|
||||||
|
) -> impl Iterator<Item = Result<(Box<[u8]>, Box<[u8]>)>> + 'db {
|
||||||
|
self.context
|
||||||
|
.full_iterator(&self.cf, IteratorMode::Start)
|
||||||
|
.skip_while(move |r| match r {
|
||||||
|
Ok((ref k, _)) => k.as_ref() < &lower,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
// The prefix iterator may "overshoot". This makes it stop when it reaches
|
||||||
|
// the end of the range that has the prefix.
|
||||||
|
.take_while(move |r| match r {
|
||||||
|
Ok((ref k, _)) => k.as_ref() < &upper,
|
||||||
|
_ => true,
|
||||||
|
})
|
||||||
|
.map_err(Error::Internal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> Keyspace<'_, C>
|
impl<C> Keyspace<'_, C>
|
||||||
|
|
|
@ -44,6 +44,11 @@ impl Key {
|
||||||
let head = Key::from_slice(&buf[16..]);
|
let head = Key::from_slice(&buf[16..]);
|
||||||
(tail, head)
|
(tail, head)
|
||||||
}
|
}
|
||||||
|
pub(crate) fn range(ts: DateTime<Utc>) -> ([u8; 16], [u8; 16]) {
|
||||||
|
let min = Ulid::from_parts(ts.timestamp_millis() as u64, u128::MIN).to_bytes();
|
||||||
|
let max = Ulid::from_parts(ts.timestamp_millis() as u64, u128::MAX).to_bytes();
|
||||||
|
(min, max)
|
||||||
|
}
|
||||||
fn to_ulid(self) -> Ulid {
|
fn to_ulid(self) -> Ulid {
|
||||||
Ulid::from_bytes(self.0)
|
Ulid::from_bytes(self.0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#![feature(iterator_try_collect, associated_type_defaults)]
|
#![feature(iterator_try_collect, associated_type_defaults)]
|
||||||
#![feature(marker_trait_attr)]
|
|
||||||
//! Data persistence for the ActivityPuppy social media server built on top of [rocksdb].
|
//! Data persistence for the ActivityPuppy social media server built on top of [rocksdb].
|
||||||
//!
|
//!
|
||||||
//! # Overview
|
//! # Overview
|
||||||
|
@ -158,4 +157,4 @@ pub enum Error {
|
||||||
Decoding(bincode::error::DecodeError),
|
Decoding(bincode::error::DecodeError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Backend = rocksdb::TransactionDB<rocksdb::MultiThreaded>;
|
type Backend = rocksdb::TransactionDB<rocksdb::MultiThreaded>;
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
|
use std::{
|
||||||
|
fmt::Pointer,
|
||||||
|
ops::{Bound, RangeBounds},
|
||||||
|
};
|
||||||
|
|
||||||
use bincode::{Decode, Encode};
|
use bincode::{Decode, Encode};
|
||||||
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
|
use either::Either;
|
||||||
/// Derive a [`Mixin`] implementation.
|
/// Derive a [`Mixin`] implementation.
|
||||||
pub use r#macro::Mixin;
|
pub use r#macro::Mixin;
|
||||||
|
|
||||||
|
@ -6,7 +13,7 @@ use super::{
|
||||||
types::{MixinSpec, Value},
|
types::{MixinSpec, Value},
|
||||||
Batch, Store, Transaction,
|
Batch, Store, Transaction,
|
||||||
};
|
};
|
||||||
use crate::{Error, Key, Result};
|
use crate::{internal::Query, util::IterExt, Error, Key, Result};
|
||||||
|
|
||||||
/// Mixins are the simplest pieces of data in the store.
|
/// Mixins are the simplest pieces of data in the store.
|
||||||
pub trait Mixin: Value<Type = MixinSpec> + Encode + Decode {}
|
pub trait Mixin: Value<Type = MixinSpec> + Encode + Decode {}
|
||||||
|
@ -73,6 +80,40 @@ impl Transaction<'_> {
|
||||||
{
|
{
|
||||||
op::has_mixin::<M>(self, node)
|
op::has_mixin::<M>(self, node)
|
||||||
}
|
}
|
||||||
|
/// Get all `M`s where the key's timestamp is within the `range`.
|
||||||
|
pub fn range<M>(
|
||||||
|
&self,
|
||||||
|
range: impl RangeBounds<DateTime<Utc>>,
|
||||||
|
) -> impl Iterator<Item = Result<(Key, M)>> + '_
|
||||||
|
where
|
||||||
|
M: Mixin,
|
||||||
|
{
|
||||||
|
use crate::internal::Context as _;
|
||||||
|
const MS: TimeDelta = TimeDelta::milliseconds(1);
|
||||||
|
let iter = match (range.start_bound(), range.end_bound()) {
|
||||||
|
(Bound::Unbounded, Bound::Unbounded) => {
|
||||||
|
Either::Left(self.open(M::SPEC.keyspace).list())
|
||||||
|
}
|
||||||
|
(min, max) => {
|
||||||
|
let lower = match min {
|
||||||
|
Bound::Unbounded => [u8::MIN; 16],
|
||||||
|
Bound::Included(inc) => Key::range(*inc).0,
|
||||||
|
Bound::Excluded(exc) => Key::range(*exc + MS).0,
|
||||||
|
};
|
||||||
|
let upper = match max {
|
||||||
|
Bound::Unbounded => [u8::MAX; 16],
|
||||||
|
Bound::Included(inc) => Key::range(*inc).1,
|
||||||
|
Bound::Excluded(exc) => Key::range(*exc - MS).1,
|
||||||
|
};
|
||||||
|
Either::Right(self.open(M::SPEC.keyspace).range(lower, upper))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
iter.bind_results(|(k, v)| {
|
||||||
|
let key = Key::from_slice(k.as_ref());
|
||||||
|
let val = op::decode(v)?;
|
||||||
|
Ok((key, val))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Batch {
|
impl Batch {
|
||||||
|
@ -136,7 +177,7 @@ mod op {
|
||||||
bincode::encode_to_vec(data, bincode::config::standard()).map_err(Error::Encoding)
|
bincode::encode_to_vec(data, bincode::config::standard()).map_err(Error::Encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode<T>(data: impl AsRef<[u8]>) -> Result<T>
|
pub(super) fn decode<T>(data: impl AsRef<[u8]>) -> Result<T>
|
||||||
where
|
where
|
||||||
T: bincode::Decode,
|
T: bincode::Decode,
|
||||||
{
|
{
|
||||||
|
|
|
@ -46,6 +46,20 @@ pub trait IterExt: Iterator + Sized {
|
||||||
{
|
{
|
||||||
self.next().ok_or(e)?
|
self.next().ok_or(e)?
|
||||||
}
|
}
|
||||||
|
/// `filter_map` meets `and_then`.
|
||||||
|
fn filter_bind_results<'a, I, O, E>(
|
||||||
|
self,
|
||||||
|
mut f: impl FnMut(I) -> Result<Option<O>, E> + 'a,
|
||||||
|
) -> impl Iterator<Item = Result<O, E>> + 'a
|
||||||
|
where
|
||||||
|
Self: Iterator<Item = Result<I, E>> + 'a,
|
||||||
|
{
|
||||||
|
self.filter_map(move |r| r.and_then(|x| f(x)).transpose())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> IterExt for I where I: Iterator {}
|
impl<I> IterExt for I where I: Iterator {}
|
||||||
|
|
||||||
|
pub fn key<K, V>(key: K) -> impl FnOnce(V) -> (K, V) {
|
||||||
|
move |val| (key, val)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue