diff --git a/Cargo.lock b/Cargo.lock index 89d6af6..0e68bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,6 +1142,8 @@ version = "0.0.0" dependencies = [ "bincode", "chrono", + "derive_more", + "either", "fetch", "store", ] diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 95c19c1..06041a1 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -1,7 +1,7 @@ use puppy::{ model::{schema, Bite, FollowRequest, Follows, Profile, Username}, + post::Author, store::{self, Error}, - tl::Post, Key, Store, }; @@ -13,49 +13,49 @@ fn main() -> store::Result<()> { let linen = get_or_create_actor(&db, "linen")?; if true { println!("creating posts"); - puppy::create_post(&db, riley, "@linen <3")?; - puppy::create_post(&db, linen, "@riley <3")?; + puppy::post::create_post(&db, riley, "@linen <3")?; + puppy::post::create_post(&db, linen, "@riley <3")?; } + if true { println!("making riley follow linen"); if !db.exists::(riley, linen)? { println!("follow relation does not exist yet"); if !db.exists::(riley, linen)? { println!("no pending follow request; creating"); - puppy::fr::create(&db, riley, linen)?; + puppy::follows::request(&db, riley, linen)?; } else { println!("accepting the pending follow request"); - puppy::fr::accept(&db, riley, linen)?; + puppy::follows::accept(&db, riley, linen)?; } } else { println!("riley already follows linen"); } } + println!("\nPosts on the instance:"); - for Post { - id, - content, - author, - } 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}",) + for post in puppy::post::fetch_timeline(&db, .., None)?.posts() { + let Author { ref handle, .. } = post.author; + let content = post.content.content.as_ref().unwrap(); + println!("- {} by {handle}:\n{content}", post.id) } + 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(); println!("- @{account_name} ({id})"); } + 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(); println!("- @{account_name} ({id})"); } + if false { println!("Biting riley"); - puppy::bite_actor(&db, linen, riley).unwrap(); - for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() { + puppy::bites::bite_actor(&db, linen, riley).unwrap(); + for Bite { id, biter, .. } in puppy::bites::bites_on(&db, riley).unwrap() { let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap(); println!("riley was bitten by @{account_name} at {}", id.timestamp()); } diff --git a/lib/macro/src/arrow.rs b/lib/macro/src/arrow.rs index 34a8aad..192c6d0 100644 --- a/lib/macro/src/arrow.rs +++ b/lib/macro/src/arrow.rs @@ -114,7 +114,7 @@ fn gen_spec(name: &Ident) -> impl ToTokens { let by_origin = format!("{prefix}/by-origin"); let by_target = format!("{prefix}/by-target"); quote! { - impl store::types::Value for #name { + impl store::types::DataType for #name { type Type = store::types::ArrowSpec; const SPEC: Self::Type = store::types::ArrowSpec { by_origin: store::types::Keyspace(#by_origin), diff --git a/lib/macro/src/lib.rs b/lib/macro/src/lib.rs index 6bf0643..fef6117 100644 --- a/lib/macro/src/lib.rs +++ b/lib/macro/src/lib.rs @@ -28,7 +28,7 @@ fn make_alias_impl(name: &syn::Ident, field: &syn::Field) -> TokenStream { let keyspace = format!("{prefix}/keyspace"); let reversed = format!("{prefix}/reversed"); let spec = quote::quote! { - impl store::types::Value for #name { + impl store::types::DataType for #name { type Type = store::types::AliasSpec; const SPEC: Self::Type = store::types::AliasSpec { keyspace: store::types::Keyspace(#keyspace), @@ -58,7 +58,7 @@ pub fn mixin(item: TokenStream) -> TokenStream { let keyspace = format!("{prefix}/main"); let spec = quote::quote! { - impl store::types::Value for #name { + impl store::types::DataType for #name { type Type = store::types::MixinSpec; const SPEC: Self::Type = store::types::MixinSpec { keyspace: store::types::Keyspace(#keyspace), diff --git a/lib/puppy/Cargo.toml b/lib/puppy/Cargo.toml index da89aae..e3cbdd3 100644 --- a/lib/puppy/Cargo.toml +++ b/lib/puppy/Cargo.toml @@ -10,3 +10,5 @@ store = { path = "../store" } fetch = { path = "../fetch" } bincode = "2.0.0-rc.3" chrono = "*" +either = "*" +derive_more = "*" diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index e5689e0..03cfc40 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -1,62 +1,38 @@ #![feature(iterator_try_collect, try_blocks)] -use model::{AuthorOf, Bite, Content, Profile, Username}; -use store::util::IterExt as _; +use model::{Profile, Username}; pub use store::{self, Key, Store}; pub mod model { 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 { + /// How many posts has this user made? 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, + /// The "bio", a freeform "about me" field. pub about_string: Option, + /// Arbitrary custom metadata fields. pub about_fields: Vec<(String, String)>, } - #[derive(Mixin, Encode, Decode)] - pub struct Content { - pub content: Option, - pub summary: Option, - } - - #[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)] + /// A unique name for an actor that is part of their "handle". + #[derive(Alias, Encode, Decode, Clone, PartialEq, Eq, Debug, Hash, Display)] pub struct Username(pub String); /// Construct the schema. @@ -65,6 +41,7 @@ pub mod model { // Mixins .has::() .has::() + .has::() // Aliases .has::() // Arrows @@ -75,32 +52,14 @@ pub mod model { } } -pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result { - let key = Key::gen(); - db.run(|tx| { - tx.update::(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 { let key = Key::gen(); 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 { post_count: 0, - account_name: username.to_string(), + account_name: username, display_name: None, about_string: None, about_fields: Vec::new(), @@ -109,68 +68,298 @@ pub fn create_actor(db: &Store, username: impl ToString) -> store::Result { }) } -pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result> { - db.run(|tx| { - let keys = tx.outgoing::(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 mod bites { + //! The most essential feature of any social network. -pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result { - db.run(|tx| { - let id = Key::gen(); - tx.create(Bite { id, biter, victim })?; - Ok(id) - }) -} + use store::{Arrow, Key, Store}; -pub fn bites_on(db: &Store, victim: Key) -> store::Result> { - db.incoming::(victim).try_collect() -} - -pub mod tl { - //! Timelines - - use store::{util::IterExt as _, Error, Key, Result, Store}; - - use crate::model::{AuthorOf, Content}; - - pub struct Post { + /// *Bites you* + #[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)] + pub struct Bite { + #[identity] pub id: Key, - pub author: Key, - pub content: Content, + #[origin] + pub biter: Key, + #[target] + pub victim: Key, } - pub fn fetch_all(db: &Store) -> Result> { + pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result { db.run(|tx| { - let iter = tx.range::(..); + 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> { + db.incoming::(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, + /// Content warning for the post. + pub warning: Option, + } + + impl From<&str> for Content { + fn from(value: &str) -> Self { + value.to_string().into() + } + } + + impl From 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, + } + + /// 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 { + 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>, + limit: Option, + ) -> Result { + let posts = db.run(|tx| { + // Get all post content entries (the argument passed here is a range of chrono datetimes). + let iter = tx.range::(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)| { - let AuthorOf { author, .. } = - tx.incoming::(id).next_or(Error::Missing)?; - Ok(Post { + // Take the first author. There is nothing stopping a post from having multiple authors, but + // let's take it one step at a time. + 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, - author, + author: Author { + id: author, + handle: format!("@{account_name}"), + display_name: display_name.unwrap_or(account_name.0), + }, content, - }) + })) }) .collect() + })?; + Ok(Timeline { items: posts }) + } + + /// Create a new post. + pub fn create_post(db: &Store, author: Key, content: impl Into) -> store::Result { + 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, + ) -> store::Result { + tx.update::(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> { + 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 { - //! Follow requests +pub mod follows { + //! 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 { + /// 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 { + // The status is stored as a mixin, so we need to get it. + let Some(st) = db.get_mixin::(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::(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 { db.run(|tx| { let req = FollowRequest { id: Key::gen(), @@ -178,38 +367,56 @@ pub mod fr { target, }; tx.create(req)?; + tx.add_mixin(req.id, Status::Pending)?; 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<()> { db.run(|tx| { - tx.delete_all::(requester, target)?; - tx.create(Follows { - follower: requester, - followed: target, - })?; + // TODO: This logic is a little broken but it'll do for now. i'll fix it later. + let fr = tx + .between::(requester, 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 }) } - pub fn reject(db: &Store, requester: Key, target: Key) -> store::Result<()> { + pub fn reject(db: &Store, request: Key) -> store::Result<()> { db.run(|tx| { - tx.delete_all::(requester, target)?; + tx.update(request, |_| Status::Rejected)?; OK }) } + /// List all pending follow requests for a user. pub fn list_pending(db: &Store, target: Key) -> store::Result> { - db.incoming::(target).collect() + db.incoming::(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> { db.outgoing::(actor) .map_ok(|a| a.followed) .collect() } + /// Get all actors following `actor`. pub fn followers_of(db: &Store, actor: Key) -> store::Result> { db.incoming::(actor) .map_ok(|a| a.follower) @@ -236,7 +443,7 @@ pub mod fr { fn create_fr() -> store::Result<()> { Store::test(schema(), |db| { let (alice, bob) = make_test_actors(&db)?; - super::create(&db, alice, bob)?; + super::request(&db, alice, bob)?; assert!( db.exists::(alice, bob)?, "(alice -> bob) ∈ follow-requested" @@ -258,7 +465,7 @@ pub mod fr { fn accept_fr() -> store::Result<()> { Store::test(schema(), |db| { let (alice, bob) = make_test_actors(&db)?; - super::create(&db, alice, bob)?; + super::request(&db, alice, bob)?; super::accept(&db, alice, bob)?; assert!( @@ -284,7 +491,7 @@ pub mod fr { fn listing_follow_relations() -> store::Result<()> { Store::test(schema(), |db| { let (alice, bob) = make_test_actors(&db)?; - super::create(&db, alice, bob)?; + super::request(&db, alice, bob)?; super::accept(&db, alice, bob)?; let followers_of_bob = super::followers_of(&db, bob)?; diff --git a/lib/store/src/alias.rs b/lib/store/src/alias.rs index c495821..5890e07 100644 --- a/lib/store/src/alias.rs +++ b/lib/store/src/alias.rs @@ -2,13 +2,13 @@ pub use r#macro::Alias; use super::{ - types::{AliasSpec, Value}, + types::{AliasSpec, DataType}, Batch, Store, Transaction, }; use crate::{Key, Result}; /// An alternative unique identifier for a node. -pub trait Alias: Value + From + AsRef {} +pub trait Alias: DataType + From + AsRef {} impl Transaction<'_> { /// Look up the key associated with the alias. diff --git a/lib/store/src/arrow.rs b/lib/store/src/arrow.rs index 67b0095..8ce0ce9 100644 --- a/lib/store/src/arrow.rs +++ b/lib/store/src/arrow.rs @@ -42,7 +42,7 @@ pub use self::kinds::{Basic, Multi}; use super::{ - types::{ArrowSpec, Value}, + types::{ArrowSpec, DataType}, Batch, Store, Transaction, }; use crate::{util::IterExt as _, Key, Result}; @@ -50,7 +50,7 @@ use crate::{util::IterExt as _, Key, Result}; /// A directed edge. /// /// See the [module docs][self] for an introduction. -pub trait Arrow: Value + From + Into { +pub trait Arrow: DataType + From + Into { /// The representation of this arrow, which also determines whether parallel edges are allowed. type Kind: ArrowKind = Basic; } @@ -115,6 +115,13 @@ impl Store { { op::outgoing::(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> + 'a + where + A: Arrow + 'a, + { + op::between::(self, a, b).map_ok(A::from) + } } impl Transaction<'_> { @@ -191,6 +198,13 @@ impl Transaction<'_> { { op::delete_one::(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> + 'a + where + A: Arrow + 'a, + { + op::between::(self, a, b).map_ok(A::from) + } } impl Batch { @@ -274,17 +288,29 @@ mod op { .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> + '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. pub fn create(cx: &impl Write, arrow: A::Kind) -> Result<()> where A: Arrow, { if A::Kind::IS_MULTI { - let Multi { - identity, - origin, - target, - } = unsafe { arrow.raw().multi }; + let Multi { identity, origin, target } = unsafe { arrow.raw().multi }; cx.open(MULTIEDGE_HEADERS) .set(identity, origin.fuse(target))?; } @@ -471,7 +497,7 @@ mod kinds { /// Derive [`Arrow`] for a struct. /// /// 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. /// /// # Attributes diff --git a/lib/store/src/internal.rs b/lib/store/src/internal.rs index 7680cef..0b3538b 100644 --- a/lib/store/src/internal.rs +++ b/lib/store/src/internal.rs @@ -157,10 +157,11 @@ mod cx { impl Context for Store { fn open<'cx>(&'cx self, cf: impl AsRef) -> Keyspace<'cx, Self> { - Keyspace { - cf: self.inner.cf_handle(cf.as_ref()).unwrap(), - context: &self, - } + let name = cf.as_ref(); + let Some(cf) = self.inner.cf_handle(name) else { + panic!("unregistered keyspace {name}! is it in the schema?") + }; + Keyspace { context: &self, cf } } } @@ -205,10 +206,11 @@ mod cx { impl Context for Transaction<'_> { fn open<'cx>(&'cx self, cf: impl AsRef) -> Keyspace<'cx, Self> { - Keyspace { - cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(), - context: &self, - } + let name = cf.as_ref(); + let Some(cf) = self.store.inner.cf_handle(name) else { + panic!("unregistered keyspace {name}! is it in the schema?") + }; + Keyspace { context: &self, cf } } } @@ -271,10 +273,11 @@ mod cx { where Self: Sized, { - Keyspace { - cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(), - context: &self, - } + let name = cf.as_ref(); + let Some(cf) = self.store.inner.cf_handle(name) else { + panic!("unregistered keyspace {name}! is it in the schema?") + }; + Keyspace { context: &self, cf } } } diff --git a/lib/store/src/mixin.rs b/lib/store/src/mixin.rs index 6ef8fa8..c58de89 100644 --- a/lib/store/src/mixin.rs +++ b/lib/store/src/mixin.rs @@ -4,13 +4,13 @@ use bincode::{Decode, Encode}; use chrono::{DateTime, Utc}; use super::{ - types::{MixinSpec, Value}, + types::{DataType, MixinSpec}, 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. -pub trait Mixin: Value + Encode + Decode {} +pub trait Mixin: DataType + Encode + Decode {} /// Derive a [`Mixin`] implementation. /// @@ -111,14 +111,15 @@ impl Transaction<'_> { op::get_range(self, range) } /// Think "LEFT JOIN". In goes an iterator over keys, out come all the associated results. - pub fn join_on( + pub fn join_on( &self, - iter: impl IntoIterator>, + f: impl Fn(T) -> Key, + iter: impl IntoIterator>, ) -> Result)>> where M: Mixin, { - op::join_on(self, iter) + op::join_on(self, iter.into_iter().map_ok(f)) } } diff --git a/lib/store/src/types.rs b/lib/store/src/types.rs index 9f482ca..141d193 100644 --- a/lib/store/src/types.rs +++ b/lib/store/src/types.rs @@ -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 //! 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. //! //! 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. pub fn has(mut self) -> Schema where - C: Value, + C: DataType, { self.add(C::SPEC); self @@ -94,7 +94,7 @@ impl AsRef for Keyspace { /// [mixin](MixinSpec). /// /// All namespaces must be unique, and added to the [`Schema`]. -pub trait Value { +pub trait DataType { type Type: TypeSpec; const SPEC: Self::Type; } @@ -126,7 +126,7 @@ pub struct MixinSpec { pub keyspace: Keyspace, } -/// Describes how to add a [`Value`] to a [`Schema`]. +/// Describes how to add a [`DataType`] to a [`Schema`]. pub trait TypeSpec { /// Register the namespaces. fn register(&self, set: &mut HashSet); diff --git a/rustfmt.toml b/rustfmt.toml index 9c177f5..65c3426 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,3 +2,5 @@ unstable_features = true overflow_delimited_expr = true group_imports = "StdExternalCrate" use_field_init_shorthand = true +reorder_modules = false +struct_lit_width = 30