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 = [
"bincode",
"chrono",
"derive_more",
"either",
"fetch",
"store",
]

View file

@ -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::<Follows>(riley, linen)? {
println!("follow relation does not exist yet");
if !db.exists::<FollowRequest>(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());
}

View file

@ -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),

View file

@ -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),

View file

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

View file

@ -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<String>,
/// The "bio", a freeform "about me" field.
pub about_string: Option<String>,
/// Arbitrary custom metadata fields.
pub about_fields: Vec<(String, String)>,
}
#[derive(Mixin, Encode, Decode)]
pub struct Content {
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)]
/// 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::<Profile>()
.has::<Content>()
.has::<Status>()
// Aliases
.has::<Username>()
// 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> {
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<Key> {
})
}
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
db.run(|tx| {
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 mod bites {
//! The most essential feature of any social network.
pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> {
use store::{Arrow, Key, Store};
/// *Bites you*
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Bite {
#[identity]
pub id: Key,
#[origin]
pub biter: Key,
#[target]
pub victim: Key,
}
pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> {
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>> {
db.incoming::<Bite>(victim).try_collect()
}
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 author: Key,
pub content: Content,
}
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
db.run(|tx| {
let iter = tx.range::<Content>(..);
/// 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)| {
let AuthorOf { author, .. } =
tx.incoming::<AuthorOf>(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<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 {
//! 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<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| {
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::<FollowRequest>(requester, target)?;
// TODO: This logic is a little broken but it'll do for now. i'll fix it later.
let fr = tx
.between::<FollowRequest>(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::<FollowRequest>(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<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>> {
db.outgoing::<Follows>(actor)
.map_ok(|a| a.followed)
.collect()
}
/// Get all actors following `actor`.
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
db.incoming::<Follows>(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::<FollowRequest>(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)?;

View file

@ -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<Type = AliasSpec> + From<String> + AsRef<str> {}
pub trait Alias: DataType<Type = AliasSpec> + From<String> + AsRef<str> {}
impl Transaction<'_> {
/// Look up the key associated with the alias.

View file

@ -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<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.
type Kind: ArrowKind = Basic;
}
@ -115,6 +115,13 @@ impl Store {
{
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<'_> {
@ -191,6 +198,13 @@ impl Transaction<'_> {
{
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 {
@ -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<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.
pub fn create<A>(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

View file

@ -157,10 +157,11 @@ mod cx {
impl Context for Store {
fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> 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<str>) -> 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 }
}
}

View file

@ -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<Type = MixinSpec> + Encode + Decode {}
pub trait Mixin: DataType<Type = MixinSpec> + 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<M>(
pub fn join_on<M, T>(
&self,
iter: impl IntoIterator<Item = Result<Key>>,
f: impl Fn(T) -> Key,
iter: impl IntoIterator<Item = Result<T>>,
) -> Result<Vec<(Key, Option<M>)>>
where
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
//! 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<C>(mut self) -> Schema
where
C: Value,
C: DataType,
{
self.add(C::SPEC);
self
@ -94,7 +94,7 @@ impl AsRef<str> 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<Keyspace>);

View file

@ -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