notesmachine/server/nm-store/src/structs.rs

258 lines
7.2 KiB
Rust

use chrono::{DateTime, Utc};
use derive_builder::Builder;
use friendly_id;
use shrinkwraprs::Shrinkwrap;
use sqlx::{self, FromRow};
// Page is German for "Box," and is used both because this is
// supposed to be a Page, 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<String> 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" => Page,
"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!(
PageRelationshipKind,
"page" => Page,
"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<String>,
pub content: String,
pub kind: String,
pub location: i64,
pub creation_date: DateTime<Utc>,
pub updated_date: DateTime<Utc>,
pub lastview_date: DateTime<Utc>,
pub deleted_date: Option<DateTime<Utc>>,
}
/// 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<String>,
pub content: String,
pub kind: NoteKind,
pub location: i64,
pub creation_date: DateTime<Utc>,
pub updated_date: DateTime<Utc>,
pub lastview_date: DateTime<Utc>,
pub deleted_date: Option<DateTime<Utc>>,
}
impl From<RowNote> 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
/// Page, 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<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub updated_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub lastview_date: DateTime<Utc>,
#[builder(default = r#"None"#)]
pub deleted_date: Option<DateTime<Utc>>,
}
impl From<NewNote> for Note {
/// Only used for building new pages, so the decision- making is
/// limited to page-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<NoteRelationshipRow> 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 PageRelationshipRow {
pub note_id: String,
pub page_id: String,
pub kind: String,
}
#[derive(Clone, Debug)]
pub struct PageRelationship {
pub note_id: String,
pub page_id: String,
pub kind: PageRelationshipKind,
}
impl From<PageRelationshipRow> for PageRelationship {
fn from(rel: PageRelationshipRow) -> Self {
Self {
page_id: rel.page_id,
note_id: rel.note_id,
kind: PageRelationshipKind::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());
}
}