Documented some stuff, improved follow request logic

This commit is contained in:
Riley Apeldoorn 2024-04-26 23:56:46 +02:00
parent 29f90ad918
commit 7ea8938c49
12 changed files with 418 additions and 175 deletions

2
Cargo.lock generated
View file

@ -1142,6 +1142,8 @@ version = "0.0.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"chrono", "chrono",
"derive_more",
"either",
"fetch", "fetch",
"store", "store",
] ]

View file

@ -1,7 +1,7 @@
use puppy::{ use puppy::{
model::{schema, Bite, FollowRequest, Follows, Profile, Username}, model::{schema, Bite, FollowRequest, Follows, Profile, Username},
post::Author,
store::{self, Error}, store::{self, Error},
tl::Post,
Key, Store, Key, Store,
}; };
@ -13,49 +13,49 @@ fn main() -> store::Result<()> {
let linen = get_or_create_actor(&db, "linen")?; let linen = get_or_create_actor(&db, "linen")?;
if true { if true {
println!("creating posts"); println!("creating posts");
puppy::create_post(&db, riley, "@linen <3")?; puppy::post::create_post(&db, riley, "@linen <3")?;
puppy::create_post(&db, linen, "@riley <3")?; puppy::post::create_post(&db, linen, "@riley <3")?;
} }
if true { 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::<FollowRequest>(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::follows::request(&db, riley, linen)?;
} else { } else {
println!("accepting the pending follow request"); println!("accepting the pending follow request");
puppy::fr::accept(&db, riley, linen)?; puppy::follows::accept(&db, riley, linen)?;
} }
} else { } else {
println!("riley already follows linen"); println!("riley already follows linen");
} }
} }
println!("\nPosts on the instance:"); println!("\nPosts on the instance:");
for Post { for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
id, let Author { ref handle, .. } = post.author;
content, let content = post.content.content.as_ref().unwrap();
author, println!("- {} by {handle}:\n{content}", post.id)
} in puppy::tl::fetch_all(&db)?
{
let Profile { account_name, .. } = db.get_mixin(author)?.unwrap();
let content = content.content.unwrap();
println!("- {id} by @{account_name} ({author}):\n{content}",)
} }
println!("\nLinen's followers:"); println!("\nLinen's followers:");
for id in puppy::fr::followers_of(&db, linen)? { for id in puppy::follows::followers_of(&db, linen)? {
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
println!("- @{account_name} ({id})"); println!("- @{account_name} ({id})");
} }
println!("\nRiley's following:"); println!("\nRiley's following:");
for id in puppy::fr::following_of(&db, riley)? { for id in puppy::follows::following_of(&db, riley)? {
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
println!("- @{account_name} ({id})"); println!("- @{account_name} ({id})");
} }
if false { if false {
println!("Biting riley"); println!("Biting riley");
puppy::bite_actor(&db, linen, riley).unwrap(); puppy::bites::bite_actor(&db, linen, riley).unwrap();
for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() { for Bite { id, biter, .. } in puppy::bites::bites_on(&db, riley).unwrap() {
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
println!("riley was bitten by @{account_name} at {}", id.timestamp()); println!("riley was bitten by @{account_name} at {}", id.timestamp());
} }

View file

@ -114,7 +114,7 @@ fn gen_spec(name: &Ident) -> impl ToTokens {
let by_origin = format!("{prefix}/by-origin"); let by_origin = format!("{prefix}/by-origin");
let by_target = format!("{prefix}/by-target"); let by_target = format!("{prefix}/by-target");
quote! { quote! {
impl store::types::Value for #name { impl store::types::DataType for #name {
type Type = store::types::ArrowSpec; type Type = store::types::ArrowSpec;
const SPEC: Self::Type = store::types::ArrowSpec { const SPEC: Self::Type = store::types::ArrowSpec {
by_origin: store::types::Keyspace(#by_origin), by_origin: store::types::Keyspace(#by_origin),

View file

@ -28,7 +28,7 @@ fn make_alias_impl(name: &syn::Ident, field: &syn::Field) -> TokenStream {
let keyspace = format!("{prefix}/keyspace"); let keyspace = format!("{prefix}/keyspace");
let reversed = format!("{prefix}/reversed"); let reversed = format!("{prefix}/reversed");
let spec = quote::quote! { let spec = quote::quote! {
impl store::types::Value for #name { impl store::types::DataType for #name {
type Type = store::types::AliasSpec; type Type = store::types::AliasSpec;
const SPEC: Self::Type = store::types::AliasSpec { const SPEC: Self::Type = store::types::AliasSpec {
keyspace: store::types::Keyspace(#keyspace), keyspace: store::types::Keyspace(#keyspace),
@ -58,7 +58,7 @@ pub fn mixin(item: TokenStream) -> TokenStream {
let keyspace = format!("{prefix}/main"); let keyspace = format!("{prefix}/main");
let spec = quote::quote! { let spec = quote::quote! {
impl store::types::Value for #name { impl store::types::DataType for #name {
type Type = store::types::MixinSpec; type Type = store::types::MixinSpec;
const SPEC: Self::Type = store::types::MixinSpec { const SPEC: Self::Type = store::types::MixinSpec {
keyspace: store::types::Keyspace(#keyspace), keyspace: store::types::Keyspace(#keyspace),

View file

@ -10,3 +10,5 @@ store = { path = "../store" }
fetch = { path = "../fetch" } fetch = { path = "../fetch" }
bincode = "2.0.0-rc.3" bincode = "2.0.0-rc.3"
chrono = "*" chrono = "*"
either = "*"
derive_more = "*"

View file

@ -1,62 +1,38 @@
#![feature(iterator_try_collect, try_blocks)] #![feature(iterator_try_collect, try_blocks)]
use model::{AuthorOf, Bite, Content, Profile, Username}; use model::{Profile, Username};
use store::util::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 bincode::{Decode, Encode};
use store::{types::Schema, Alias, Arrow, Key, Mixin}; use derive_more::Display;
use store::{types::Schema, Alias, Mixin};
#[derive(Mixin, Encode, Decode)] use crate::follows::Status;
pub use crate::{
bites::Bite,
follows::{FollowRequest, Follows},
post::{AuthorOf, Content},
};
/// A "profile" in the social media sense.
///
/// Contains all presentation information about someone making posts.
#[derive(Mixin, Encode, Decode, Debug, Clone)]
pub struct Profile { pub struct Profile {
/// How many posts has this user made?
pub post_count: usize, pub post_count: usize,
pub account_name: String, /// The name used for the profile's handle.
pub account_name: Username,
/// The name displayed above their posts.
pub display_name: Option<String>, pub display_name: Option<String>,
/// The "bio", a freeform "about me" field.
pub about_string: Option<String>, pub about_string: Option<String>,
/// Arbitrary custom metadata fields.
pub about_fields: Vec<(String, String)>, pub about_fields: Vec<(String, String)>,
} }
#[derive(Mixin, Encode, Decode)] /// A unique name for an actor that is part of their "handle".
pub struct Content { #[derive(Alias, Encode, Decode, Clone, PartialEq, Eq, Debug, Hash, Display)]
pub content: Option<String>,
pub summary: Option<String>,
}
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct AuthorOf {
#[origin]
pub author: Key,
#[target]
pub object: Key,
}
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Follows {
#[origin]
pub follower: Key,
#[target]
pub followed: Key,
}
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Bite {
#[identity]
pub id: Key,
#[origin]
pub biter: Key,
#[target]
pub victim: Key,
}
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct FollowRequest {
#[identity]
pub id: Key,
pub origin: Key,
pub target: Key,
}
#[derive(Alias)]
pub struct Username(pub String); pub struct Username(pub String);
/// Construct the schema. /// Construct the schema.
@ -65,6 +41,7 @@ pub mod model {
// Mixins // Mixins
.has::<Profile>() .has::<Profile>()
.has::<Content>() .has::<Content>()
.has::<Status>()
// Aliases // Aliases
.has::<Username>() .has::<Username>()
// Arrows // Arrows
@ -75,32 +52,14 @@ pub mod model {
} }
} }
pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result<Key> {
let key = Key::gen();
db.run(|tx| {
tx.update::<Profile>(author, |mut profile| {
profile.post_count += 1;
profile
})?;
tx.add_mixin(key, Content {
content: Some(content.to_string()),
summary: None,
})?;
tx.create(AuthorOf {
author,
object: key,
})?;
Ok(key)
})
}
pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> { pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
let key = Key::gen(); let key = Key::gen();
db.run(|tx| { db.run(|tx| {
tx.add_alias(key, Username(username.to_string()))?; let username: Username = username.to_string().into();
tx.add_alias(key, username.clone())?;
tx.add_mixin(key, Profile { tx.add_mixin(key, Profile {
post_count: 0, post_count: 0,
account_name: username.to_string(), account_name: username,
display_name: None, display_name: None,
about_string: None, about_string: None,
about_fields: Vec::new(), about_fields: Vec::new(),
@ -109,68 +68,298 @@ 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 mod bites {
db.run(|tx| { //! The most essential feature of any social network.
let keys = tx.outgoing::<AuthorOf>(author).map_ok(|a| a.object);
let posts = tx
.join_on(keys)?
.into_iter()
.filter_map(|(k, opt)| try { (k, opt?) })
.collect();
Ok(posts)
})
}
pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> { use store::{Arrow, Key, Store};
db.run(|tx| {
let id = Key::gen();
tx.create(Bite { id, biter, victim })?;
Ok(id)
})
}
pub fn bites_on(db: &Store, victim: Key) -> store::Result<Vec<Bite>> { /// *Bites you*
db.incoming::<Bite>(victim).try_collect() #[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
} pub struct Bite {
#[identity]
pub mod tl {
//! Timelines
use store::{util::IterExt as _, Error, Key, Result, Store};
use crate::model::{AuthorOf, Content};
pub struct Post {
pub id: Key, pub id: Key,
pub author: Key, #[origin]
pub content: Content, pub biter: Key,
#[target]
pub victim: Key,
} }
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> { pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> {
db.run(|tx| { db.run(|tx| {
let iter = tx.range::<Content>(..); let id = Key::gen();
tx.create(Bite { id, biter, victim })?;
Ok(id)
})
}
/// Who has bitten `victim`?
pub fn bites_on(db: &Store, victim: Key) -> store::Result<Vec<Bite>> {
db.incoming::<Bite>(victim).try_collect()
}
}
pub mod post {
//! Timelines: where you go to view the posts.
use std::ops::RangeBounds;
use bincode::{Decode, Encode};
use chrono::{DateTime, Utc};
use either::Either::{Left, Right};
use store::{util::IterExt as _, Arrow, Error, Key, Mixin, Result, Store, Transaction};
use crate::model::Profile;
/// The contents of a post.
#[derive(Mixin, Encode, Decode, Debug, Clone, Default)]
pub struct Content {
/// Main post body.
pub content: Option<String>,
/// Content warning for the post.
pub warning: Option<String>,
}
impl From<&str> for Content {
fn from(value: &str) -> Self {
value.to_string().into()
}
}
impl From<String> for Content {
fn from(value: String) -> Self {
Content {
content: Some(value),
warning: None,
}
}
}
/// The relation that `author` has constructed and published `object`.
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct AuthorOf {
#[origin]
pub author: Key,
#[target]
pub object: Key,
}
/// A piece of content posted by someone.
#[derive(Clone, Debug)]
pub struct Post {
/// The post's internal ID.
pub id: Key,
/// The actual post contents.
pub content: Content,
/// Metadata about the post's author.
pub author: Author,
}
/// Information about a [`Post`]'s author.
#[derive(Clone, Debug)]
pub struct Author {
/// The identifier of the author.
pub id: Key,
/// The name to display along with the post.
pub display_name: String,
/// An informal identifier for a particular author.
pub handle: String,
}
/// An ordered list of [`Post`]s for viewing.
#[derive(Debug)]
pub struct Timeline {
items: Vec<Item>,
}
/// Discrete events that can be displayed to a user as part of a timeline.
#[derive(Debug)]
enum Item {
Post(Post),
}
impl Item {
/// Get the timeline item if it is a [`Post`].
pub fn as_post(&self) -> Option<&Post> {
match self {
Item::Post(ref post) => Some(post),
}
}
}
impl Timeline {
/// Get all the posts in the timeline.
pub fn posts(&self) -> impl Iterator<Item = &Post> {
self.items.iter().filter_map(|x| x.as_post())
}
}
/// Gets at most `limit` of the posts known to the instance that were inserted within `time_range`.
pub fn fetch_timeline(
db: &Store,
time_range: impl RangeBounds<DateTime<Utc>>,
limit: Option<usize>,
) -> Result<Timeline> {
let posts = db.run(|tx| {
// Get all post content entries (the argument passed here is a range of chrono datetimes).
let iter = tx.range::<Content>(time_range);
let iter = match limit {
Some(n) => Left(iter.take(n)),
None => Right(iter),
};
// Then, we're gonna map each of them to their author, and get the profile information needed to
// render the post (mostly display name and handle).
iter.bind_results(|(id, content)| { iter.bind_results(|(id, content)| {
let AuthorOf { author, .. } = // Take the first author. There is nothing stopping a post from having multiple authors, but
tx.incoming::<AuthorOf>(id).next_or(Error::Missing)?; // let's take it one step at a time.
Ok(Post { let (author, Some(Profile { display_name, account_name, .. })) = tx
.join_on(|a: AuthorOf| a.author, tx.incoming(id))?
.swap_remove(0)
else {
// We expect all posts to have at least one author, so we should complain if there is one
// that doesn't (for now). For robustness, the `.collect()` down there should be replaced
// with a strategy where we log a warning instead of failing, but in the current state of
// the project, failing fast is a good thing.
return Err(Error::Missing);
};
Ok(Item::Post(Post {
id, id,
author, author: Author {
id: author,
handle: format!("@{account_name}"),
display_name: display_name.unwrap_or(account_name.0),
},
content, content,
}) }))
}) })
.collect() .collect()
})?;
Ok(Timeline { items: posts })
}
/// Create a new post.
pub fn create_post(db: &Store, author: Key, content: impl Into<Content>) -> store::Result<Key> {
db.run(|tx| mixin_post(tx, Key::gen(), author, content))
}
/// Add a post's mixins and predicates to an existing `node`.
pub fn mixin_post(
tx: &Transaction<'_>,
node: Key,
author: Key,
content: impl Into<Content>,
) -> store::Result<Key> {
tx.update::<Profile>(author, |mut profile| {
profile.post_count += 1;
profile
})?;
tx.add_mixin(node, content.into())?;
tx.create(AuthorOf { author, object: node })?;
Ok(node)
}
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
db.run(|tx| {
let posts = tx
.join_on(|a: AuthorOf| a.object, tx.outgoing(author))?
.into_iter()
.filter_map(|(k, opt)| try { (k, opt?) })
.collect();
Ok(posts)
}) })
} }
} }
pub mod fr { pub mod follows {
//! Follow requests //! Follow requests and related stuff.
use store::{util::IterExt as _, Key, Store, OK}; use bincode::{Decode, Encode};
use store::{util::IterExt, Arrow, Error, Key, Mixin, Store, OK};
use crate::model::{FollowRequest, Follows}; /// A predicate; `follower` "follows" `followed`.
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Follows {
#[origin]
pub follower: Key,
#[target]
pub followed: Key,
}
pub fn create(db: &Store, requester: Key, target: Key) -> store::Result<FollowRequest> { /// An instance of a request from some `origin` user to follow a `target` user.
///
/// This should not be used to determine whether two actors are following each other. For that, use
/// [`Follows`], a basic arrow for exactly this purpose. *This* arrow is used to identify specific
/// instances of *requests*, and serves mostly as a historical reference and for synchronizing with
/// other servers.
///
/// Mixins always present for the `id`:
///
/// - [`Status`], carrying the status of the request.
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct FollowRequest {
/// The unique ID of this particular request.
#[identity]
pub id: Key,
/// The "follower", the user that made the request.
pub origin: Key,
/// The one the request is made to.
pub target: Key,
}
impl FollowRequest {
/// Determine if this follow request is pending.
pub fn is_pending(&self, db: &Store) -> store::Result<bool> {
// The status is stored as a mixin, so we need to get it.
let Some(st) = db.get_mixin::<Status>(self.id)? else {
// If we don't have a status for a follow request, something is borked.
return Err(Error::Missing);
};
// If the status of the follow request is pending, it can't also be true that the follows
// relation already exists.
debug_assert! {
!(st == Status::Pending)
|| db.exists::<Follows>(self.origin, self.target).map(|x| !x)?,
"fr.is_pending -> !(fr.origin follows fr.target)"
};
Ok(st == Status::Pending)
}
}
/// The status of a [`FollowRequest`].
///
/// Valid state transitions:
///
/// ```text
/// ┌──────────────▶ Rejected
/// │
/// │
/// │
///
/// None ─────────▶ Pending ────────▶ Accepted
///
/// │ │
/// │ │
/// │ │
/// ▼ │
/// Withdrawn ◀────────────┘
/// ```
///
/// In addition, a follow request will be deleted if either endpoint is removed from the graph.
#[derive(Mixin, Encode, Decode, Eq, PartialEq, Clone)]
pub enum Status {
/// The follow request was previously pending or accepted, but since withdrawn.
///
/// This can happen when someone cancels their follow request or unfollows the target.
Withdrawn,
/// The follow request was accepted.
Accepted,
/// The follow request was denied.
Rejected,
/// The follow request is still under review.
Pending,
}
/// Request to follow another actor.
pub fn request(db: &Store, requester: Key, target: Key) -> store::Result<FollowRequest> {
db.run(|tx| { db.run(|tx| {
let req = FollowRequest { let req = FollowRequest {
id: Key::gen(), id: Key::gen(),
@ -178,38 +367,56 @@ pub mod fr {
target, target,
}; };
tx.create(req)?; tx.create(req)?;
tx.add_mixin(req.id, Status::Pending)?;
Ok(req) Ok(req)
}) })
} }
/// Accept the open follow request from `requester` to `target`, if one exists.
pub fn accept(db: &Store, requester: Key, target: Key) -> store::Result<()> { pub fn accept(db: &Store, requester: Key, target: Key) -> store::Result<()> {
db.run(|tx| { db.run(|tx| {
tx.delete_all::<FollowRequest>(requester, target)?; // TODO: This logic is a little broken but it'll do for now. i'll fix it later.
tx.create(Follows { let fr = tx
follower: requester, .between::<FollowRequest>(requester, target)
followed: target, .filter(|fr| fr.as_ref().is_ok_and(|f| f.target == target))
})?; // We'll want the latest one, because that one was inserted last so it'll be the most
// recent
.last()
.ok_or_else(|| Error::Missing)??;
// Only apply the update if the last follow request is still in a pending state.
if let Some(Status::Pending) = db.get_mixin(fr.id)? {
tx.update(fr.id, |_| Status::Accepted)?;
tx.create(Follows {
follower: requester,
followed: target,
})?;
}
OK OK
}) })
} }
pub fn reject(db: &Store, requester: Key, target: Key) -> store::Result<()> { pub fn reject(db: &Store, request: Key) -> store::Result<()> {
db.run(|tx| { db.run(|tx| {
tx.delete_all::<FollowRequest>(requester, target)?; tx.update(request, |_| Status::Rejected)?;
OK OK
}) })
} }
/// List all pending follow requests for a user.
pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<FollowRequest>> { pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<FollowRequest>> {
db.incoming::<FollowRequest>(target).collect() db.incoming::<FollowRequest>(target)
.filter_bind_results(|req| Ok(if req.is_pending(db)? { Some(req) } else { None }))
.collect()
} }
/// Get all actors followed by `actor`.
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.outgoing::<Follows>(actor) db.outgoing::<Follows>(actor)
.map_ok(|a| a.followed) .map_ok(|a| a.followed)
.collect() .collect()
} }
/// Get all actors following `actor`.
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.incoming::<Follows>(actor) db.incoming::<Follows>(actor)
.map_ok(|a| a.follower) .map_ok(|a| a.follower)
@ -236,7 +443,7 @@ pub mod fr {
fn create_fr() -> store::Result<()> { fn create_fr() -> store::Result<()> {
Store::test(schema(), |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::request(&db, alice, bob)?;
assert!( assert!(
db.exists::<FollowRequest>(alice, bob)?, db.exists::<FollowRequest>(alice, bob)?,
"(alice -> bob) ∈ follow-requested" "(alice -> bob) ∈ follow-requested"
@ -258,7 +465,7 @@ pub mod fr {
fn accept_fr() -> store::Result<()> { fn accept_fr() -> store::Result<()> {
Store::test(schema(), |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::request(&db, alice, bob)?;
super::accept(&db, alice, bob)?; super::accept(&db, alice, bob)?;
assert!( assert!(
@ -284,7 +491,7 @@ pub mod fr {
fn listing_follow_relations() -> store::Result<()> { fn listing_follow_relations() -> store::Result<()> {
Store::test(schema(), |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::request(&db, alice, bob)?;
super::accept(&db, alice, bob)?; super::accept(&db, alice, bob)?;
let followers_of_bob = super::followers_of(&db, bob)?; let followers_of_bob = super::followers_of(&db, bob)?;

View file

@ -2,13 +2,13 @@
pub use r#macro::Alias; pub use r#macro::Alias;
use super::{ use super::{
types::{AliasSpec, Value}, types::{AliasSpec, DataType},
Batch, Store, Transaction, Batch, Store, Transaction,
}; };
use crate::{Key, Result}; use crate::{Key, Result};
/// An alternative unique identifier for a node. /// An alternative unique identifier for a node.
pub trait Alias: Value<Type = AliasSpec> + From<String> + AsRef<str> {} pub trait Alias: DataType<Type = AliasSpec> + From<String> + AsRef<str> {}
impl Transaction<'_> { impl Transaction<'_> {
/// Look up the key associated with the alias. /// Look up the key associated with the alias.

View file

@ -42,7 +42,7 @@
pub use self::kinds::{Basic, Multi}; pub use self::kinds::{Basic, Multi};
use super::{ use super::{
types::{ArrowSpec, Value}, types::{ArrowSpec, DataType},
Batch, Store, Transaction, Batch, Store, Transaction,
}; };
use crate::{util::IterExt as _, Key, Result}; use crate::{util::IterExt as _, Key, Result};
@ -50,7 +50,7 @@ use crate::{util::IterExt as _, Key, Result};
/// A directed edge. /// A directed edge.
/// ///
/// See the [module docs][self] for an introduction. /// See the [module docs][self] for an introduction.
pub trait Arrow: Value<Type = ArrowSpec> + From<Self::Kind> + Into<Self::Kind> { pub trait Arrow: DataType<Type = ArrowSpec> + From<Self::Kind> + Into<Self::Kind> {
/// The representation of this arrow, which also determines whether parallel edges are allowed. /// The representation of this arrow, which also determines whether parallel edges are allowed.
type Kind: ArrowKind = Basic; type Kind: ArrowKind = Basic;
} }
@ -115,6 +115,13 @@ impl Store {
{ {
op::outgoing::<A>(self, origin).map_ok(A::from) op::outgoing::<A>(self, origin).map_ok(A::from)
} }
/// List all arrows between `a` and `b`, in either direction.
pub fn between<'a, A>(&'a self, a: Key, b: Key) -> impl Iterator<Item = Result<A>> + 'a
where
A: Arrow<Kind = Multi> + 'a,
{
op::between::<A>(self, a, b).map_ok(A::from)
}
} }
impl Transaction<'_> { impl Transaction<'_> {
@ -191,6 +198,13 @@ impl Transaction<'_> {
{ {
op::delete_one::<A>(self, arrow.into()) op::delete_one::<A>(self, arrow.into())
} }
/// List all arrows between `a` and `b`, in either direction.
pub fn between<'a, A>(&'a self, a: Key, b: Key) -> impl Iterator<Item = Result<A>> + 'a
where
A: Arrow<Kind = Multi> + 'a,
{
op::between::<A>(self, a, b).map_ok(A::from)
}
} }
impl Batch { impl Batch {
@ -274,17 +288,29 @@ mod op {
.map_ok(|(ref k, _)| A::Kind::dec(k)) .map_ok(|(ref k, _)| A::Kind::dec(k))
} }
/// Get all arrows between the two endpoints (in either direction)
pub fn between<'db, A>(
cx: &'db impl Query,
origin: Key,
target: Key,
) -> impl Iterator<Item = Result<A::Kind>> + 'db
where
A: Arrow,
A::Kind: 'db,
{
let ks = cx.open(A::SPEC.by_origin);
ks.scan(origin.fuse(target))
.chain(ks.scan(target.fuse(origin)))
.map_ok(|(ref k, _)| A::Kind::dec(k))
}
/// Create a new arrow. /// Create a new arrow.
pub fn create<A>(cx: &impl Write, arrow: A::Kind) -> Result<()> pub fn create<A>(cx: &impl Write, arrow: A::Kind) -> Result<()>
where where
A: Arrow, A: Arrow,
{ {
if A::Kind::IS_MULTI { if A::Kind::IS_MULTI {
let Multi { let Multi { identity, origin, target } = unsafe { arrow.raw().multi };
identity,
origin,
target,
} = unsafe { arrow.raw().multi };
cx.open(MULTIEDGE_HEADERS) cx.open(MULTIEDGE_HEADERS)
.set(identity, origin.fuse(target))?; .set(identity, origin.fuse(target))?;
} }
@ -471,7 +497,7 @@ mod kinds {
/// Derive [`Arrow`] for a struct. /// Derive [`Arrow`] for a struct.
/// ///
/// This will generate the required [`Into`] and [`From`] impls, as well as an [`Arrow`](trait@Arrow) impl and /// This will generate the required [`Into`] and [`From`] impls, as well as an [`Arrow`](trait@Arrow) impl and
/// a [`Value`] impl with the namespaces derived from the name of the struct. The macro works on structs with /// a [`DataType`] impl with the namespaces derived from the name of the struct. The macro works on structs with
/// specific fields, or newtypes of any arrow kind. /// specific fields, or newtypes of any arrow kind.
/// ///
/// # Attributes /// # Attributes

View file

@ -157,10 +157,11 @@ mod cx {
impl Context for Store { impl Context for Store {
fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> { fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> {
Keyspace { let name = cf.as_ref();
cf: self.inner.cf_handle(cf.as_ref()).unwrap(), let Some(cf) = self.inner.cf_handle(name) else {
context: &self, panic!("unregistered keyspace {name}! is it in the schema?")
} };
Keyspace { context: &self, cf }
} }
} }
@ -205,10 +206,11 @@ mod cx {
impl Context for Transaction<'_> { impl Context for Transaction<'_> {
fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> { fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> {
Keyspace { let name = cf.as_ref();
cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(), let Some(cf) = self.store.inner.cf_handle(name) else {
context: &self, panic!("unregistered keyspace {name}! is it in the schema?")
} };
Keyspace { context: &self, cf }
} }
} }
@ -271,10 +273,11 @@ mod cx {
where where
Self: Sized, Self: Sized,
{ {
Keyspace { let name = cf.as_ref();
cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(), let Some(cf) = self.store.inner.cf_handle(name) else {
context: &self, panic!("unregistered keyspace {name}! is it in the schema?")
} };
Keyspace { context: &self, cf }
} }
} }

View file

@ -4,13 +4,13 @@ use bincode::{Decode, Encode};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use super::{ use super::{
types::{MixinSpec, Value}, types::{DataType, MixinSpec},
Batch, Store, Transaction, Batch, Store, Transaction,
}; };
use crate::{Error, Key, Result}; use crate::{util::IterExt as _, 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: DataType<Type = MixinSpec> + Encode + Decode {}
/// Derive a [`Mixin`] implementation. /// Derive a [`Mixin`] implementation.
/// ///
@ -111,14 +111,15 @@ impl Transaction<'_> {
op::get_range(self, range) op::get_range(self, range)
} }
/// Think "LEFT JOIN". In goes an iterator over keys, out come all the associated results. /// Think "LEFT JOIN". In goes an iterator over keys, out come all the associated results.
pub fn join_on<M>( pub fn join_on<M, T>(
&self, &self,
iter: impl IntoIterator<Item = Result<Key>>, f: impl Fn(T) -> Key,
iter: impl IntoIterator<Item = Result<T>>,
) -> Result<Vec<(Key, Option<M>)>> ) -> Result<Vec<(Key, Option<M>)>>
where where
M: Mixin, M: Mixin,
{ {
op::join_on(self, iter) op::join_on(self, iter.into_iter().map_ok(f))
} }
} }

View file

@ -2,7 +2,7 @@
//! //!
//! There is a lot of complicated machinery here to make it so that you have to write very little code to //! There is a lot of complicated machinery here to make it so that you have to write very little code to
//! define new types. Basically, if you want to define a thing to store, you need to implement the trait //! define new types. Basically, if you want to define a thing to store, you need to implement the trait
//! for it (e.g. [`Arrow`]), and also implement [`Value`], where you create a specification describing which //! for it (e.g. [`Arrow`]), and also implement [`DataType`], where you create a specification describing which
//! namespaces store records of that type. //! namespaces store records of that type.
//! //!
//! Then, when you construct a new `Store`, you need to pass in a [`Schema`], or the database won't be able //! Then, when you construct a new `Store`, you need to pass in a [`Schema`], or the database won't be able
@ -66,7 +66,7 @@ impl Schema {
/// Add the component to the schema. /// Add the component to the schema.
pub fn has<C>(mut self) -> Schema pub fn has<C>(mut self) -> Schema
where where
C: Value, C: DataType,
{ {
self.add(C::SPEC); self.add(C::SPEC);
self self
@ -94,7 +94,7 @@ impl AsRef<str> for Keyspace {
/// [mixin](MixinSpec). /// [mixin](MixinSpec).
/// ///
/// All namespaces must be unique, and added to the [`Schema`]. /// All namespaces must be unique, and added to the [`Schema`].
pub trait Value { pub trait DataType {
type Type: TypeSpec; type Type: TypeSpec;
const SPEC: Self::Type; const SPEC: Self::Type;
} }
@ -126,7 +126,7 @@ pub struct MixinSpec {
pub keyspace: Keyspace, pub keyspace: Keyspace,
} }
/// Describes how to add a [`Value`] to a [`Schema`]. /// Describes how to add a [`DataType`] to a [`Schema`].
pub trait TypeSpec { pub trait TypeSpec {
/// Register the namespaces. /// Register the namespaces.
fn register(&self, set: &mut HashSet<Keyspace>); fn register(&self, set: &mut HashSet<Keyspace>);

View file

@ -2,3 +2,5 @@ unstable_features = true
overflow_delimited_expr = true overflow_delimited_expr = true
group_imports = "StdExternalCrate" group_imports = "StdExternalCrate"
use_field_init_shorthand = true use_field_init_shorthand = true
reorder_modules = false
struct_lit_width = 30