use chrono::{DateTime, Utc}; use derive_builder::Builder; use friendly_id; use shrinkwraprs::Shrinkwrap; use sqlx::{self, FromRow}; // Kasten is German for "Box," and is used both because this is // supposed to be a Zettlekasten, and because "Box" is a heavily // reserved word in Rust. So, for that matter, are "crate" and // "cargo," "cell," and so forth. If I'd wanted to go the Full // Noguchi, I guess I could have used "envelope." // In order to prevent arbitrary enumeration tokens from getting into // the database, the private layer takes a very hard line on insisting // that everything sent TO the datastore come in the enumerated // format, and everything coming OUT of the database be converted back // into an enumeration. These macros instantiate those objects // and their conversions to/from strings. macro_rules! build_conversion_enums { ( $ty:ident, $( $s:literal => $x:ident, )*) => { #[derive(Clone, Debug, PartialEq, Eq)] pub enum $ty { $( $x ), * } impl From for $ty { fn from(kind: String) -> Self { match &kind[..] { $( $s => $ty::$x, )* _ => panic!("Illegal value in $ty database: {}", kind), } } } impl From<$ty> for String { fn from(kind: $ty) -> Self { match kind { $( $ty::$x => $s ),* } .to_string() } } impl $ty { pub fn to_string(&self) -> String { String::from(self.clone()) } } }; } #[derive(Shrinkwrap, Clone)] pub(crate) struct NoteId(pub String); #[derive(Shrinkwrap, Clone)] pub(crate) struct ParentId(pub String); // The different kinds of objects we support. build_conversion_enums!( NoteKind, "box" => Kasten, "note" => Note, "resource" => Resource, ); // The different kinds of relationships we support. I do not yet // know how to ensure that there is a maximum of one (a -> // b)::Direct, and that for any (a -> b) there is no (b <- a), that // is, nor, for that matter, do I know how to prevent cycles. build_conversion_enums!( RelationshipKind, "direct" => Direct, "reference" => Reference, "embed" => Embed, ); build_conversion_enums!( KastenRelationshipKind, "kasten" => Kasten, "unacked" => Unacked, "cancelled" => Cancelled, ); // A Note is the base construct of our system. It represents a // single note and contains information about its parent and location. // This is the object *retrieved* from the database. #[derive(Clone, Debug, FromRow)] pub(crate) struct RowNote { pub id: String, pub parent_id: Option, pub content: String, pub kind: String, pub location: i64, pub creation_date: DateTime, pub updated_date: DateTime, pub lastview_date: DateTime, pub deleted_date: Option>, } /// A Note as it's returned from the private layer. This is /// provided to ensure that the NoteKind is an enum, and that we /// control the list of possible values stored in the database. #[derive(Clone, Debug)] pub struct Note { pub id: String, pub parent_id: Option, pub content: String, pub kind: NoteKind, pub location: i64, pub creation_date: DateTime, pub updated_date: DateTime, pub lastview_date: DateTime, pub deleted_date: Option>, } impl From for Note { fn from(note: RowNote) -> Self { Self { id: note.id, parent_id: note.parent_id, content: note.content, kind: NoteKind::from(note.kind), location: note.location, creation_date: note.creation_date, updated_date: note.updated_date, lastview_date: note.lastview_date, deleted_date: note.deleted_date, } } } /// A new Note object as it's inserted into the system. It has no /// parent or location information; those are data relative to the /// parent, and must be provided by the client. In the case of a /// Kasten, no location or parent is necessary. #[derive(Clone, Debug, Builder)] pub struct NewNote { #[builder(default = r#"friendly_id::create()"#)] pub id: String, pub content: String, #[builder(default = r#"NoteKind::Note"#)] pub kind: NoteKind, #[builder(default = r#"chrono::Utc::now()"#)] pub creation_date: DateTime, #[builder(default = r#"chrono::Utc::now()"#)] pub updated_date: DateTime, #[builder(default = r#"chrono::Utc::now()"#)] pub lastview_date: DateTime, #[builder(default = r#"None"#)] pub deleted_date: Option>, } impl From for Note { /// Only used for building new kastens, so the decision- making is /// limited to kasten-level things, like pointing to self and /// having a location of zero. fn from(note: NewNote) -> Self { Self { id: note.id, parent_id: None, content: note.content, kind: note.kind, location: 0, creation_date: note.creation_date, updated_date: note.updated_date, lastview_date: note.lastview_date, deleted_date: note.deleted_date, } } } #[derive(Clone, Debug, FromRow)] pub(crate) struct JustId { pub id: String, } #[derive(Clone, Debug, FromRow)] pub(crate) struct PageTitle { pub id: String, pub content: String, } #[derive(Clone, Debug, FromRow)] pub(crate) struct RowCount { pub count: i64, } #[derive(Clone, Debug, FromRow)] pub(crate) struct NoteRelationshipRow { pub parent_id: String, pub note_id: String, pub location: i64, pub kind: String, } #[derive(Clone, Debug)] pub struct NoteRelationship { pub parent_id: String, pub note_id: String, pub location: i64, pub kind: RelationshipKind, } impl From for NoteRelationship { fn from(rel: NoteRelationshipRow) -> Self { Self { parent_id: rel.parent_id, note_id: rel.note_id, location: rel.location, kind: RelationshipKind::from(rel.kind), } } } #[derive(Clone, Debug, FromRow)] pub(crate) struct KastenRelationshipRow { pub note_id: String, pub kasten_id: String, pub kind: String, } #[derive(Clone, Debug)] pub struct KastenRelationship { pub note_id: String, pub kasten_id: String, pub kind: KastenRelationshipKind, } impl From for KastenRelationship { fn from(rel: KastenRelationshipRow) -> Self { Self { kasten_id: rel.kasten_id, note_id: rel.note_id, kind: KastenRelationshipKind::from(rel.kind), } } } #[cfg(test)] mod tests { use super::*; #[test] fn can_build_new_note() { let now = chrono::Utc::now(); let newnote = NewNoteBuilder::default().content("bar".to_string()).build().unwrap(); assert!(newnote.id.len() > 4); assert!((newnote.creation_date - now).num_minutes() < 1); assert!((newnote.updated_date - now).num_minutes() < 1); assert!((newnote.lastview_date - now).num_minutes() < 1); assert!(newnote.deleted_date.is_none()); } }