From 76d27dfcec68fde3e793e3e5d100f1bd410d63a9 Mon Sep 17 00:00:00 2001 From: itsscb Date: Sat, 21 Jun 2025 21:09:56 +0200 Subject: [PATCH] feat: initial data model + server --- Cargo.toml | 5 +++ index.html | 78 +++++++++++++++++++++++++++++++++++ src/lib.rs | 62 ++++++++++++++++++++++++++++ src/main.rs | 5 +++ src/person.rs | 94 ++++++++++++++++++++++++++++++++++++++++++ src/stammbaum.rs | 84 +++++++++++++++++++++++++++++++++++++ templates/_layout.html | 60 +++++++++++++++++++++++++++ templates/person.html | 38 +++++++++++++++++ test.json | 49 ++++++++++++++++++++++ 9 files changed, 475 insertions(+) create mode 100644 index.html create mode 100644 src/main.rs create mode 100644 src/stammbaum.rs create mode 100644 templates/_layout.html create mode 100644 templates/person.html create mode 100644 test.json diff --git a/Cargo.toml b/Cargo.toml index 13ae1db..e804d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,10 @@ edition = "2024" [dependencies] askama = "0.14.0" axum = "0.8.4" +chrono = { version = "0.4.41", features = ["serde"] } +serde = { version = "1.0.219", features = ["derive", "rc"] } +serde_json = "1.0.140" +tokio = { version = "1.45.1", features = ["full", "tracing"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" +uuid = { version = "1.17.0", features = ["serde", "v4"] } diff --git a/index.html b/index.html new file mode 100644 index 0000000..653fbb8 --- /dev/null +++ b/index.html @@ -0,0 +1,78 @@ + + + + + + + Family Tree Node + + + + + +
+
+ Jane + Doe + (Musterfrau) +
+
+
Sex: Female
+
DOB: 1990-01-02
+
+ Parents: +
    + +
+
+
+
+ + + + diff --git a/src/lib.rs b/src/lib.rs index e31776d..f5f816b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,63 @@ +#![allow(clippy::expect_used)] +use askama::Template; +use axum::{ + Router, + extract::{Path, State}, + http::StatusCode, + response::{Html, IntoResponse}, + routing::get, + serve, +}; +use person::PersonID; +use stammbaum::Stammbaum; +use std::io::BufReader; +use tokio::runtime::Runtime; + mod person; +mod stammbaum; + +async fn get_person( + Path(id): Path, + State(stammbaum): State, +) -> Result { + let id = match PersonID::try_from(id) { + Ok(id) => id, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + match stammbaum.get(id) { + None => Err(StatusCode::NOT_FOUND), + Some(p) => Ok(Html( + p.render().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, + )), + } +} + +async fn list_stammbaum( + State(stammbaum): State, +) -> Result { + Ok::, StatusCode>(Html(stammbaum.render())) +} + +pub fn run() -> Result<(), String> { + let rt = Runtime::new().map_err(|e| e.to_string())?; + rt.block_on(async { + #[allow(clippy::expect_used)] + let stammbaum: Stammbaum = serde_json::from_reader(BufReader::new( + std::fs::File::open("test.json").expect("tbd file"), + )) + .expect("tbd struct"); + + let app = Router::new() + .route("/", get(list_stammbaum)) + .route("/{id}", get(get_person)) + .with_state(stammbaum); + let addr = tokio::net::TcpListener::bind("0.0.0.0:3000") + .await + .expect("faild to bind port"); + + serve(addr, app).await.map_err(|e| e.to_string()) + }) + .map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..75242da --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +use stammbaum::run; +fn main() { + #[allow(clippy::unwrap_used)] + run().unwrap(); +} diff --git a/src/person.rs b/src/person.rs index e69de29..5397726 100644 --- a/src/person.rs +++ b/src/person.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use askama::Template; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub type PersonID = Uuid; + +#[derive(Template, Clone, Debug, Serialize, Deserialize)] +#[template(path = "person.html")] +pub struct Person { + id: PersonID, + first_name: Arc, + last_name: Arc, + #[serde(skip_serializing_if = "Option::is_none")] + maiden_name: Option>, + sex: Sex, + date_of_birth: DateTime, + parents: Vec, + marriages: Vec, +} + +#[allow(dead_code)] +impl Person { + pub fn new>( + first_name: T, + last_name: T, + maiden_name: Option, + sex: Sex, + date_of_birth: DateTime, + parents: Vec, + ) -> Self { + let uuid = Uuid::new_v4(); + Self { + id: uuid, + first_name: Arc::from(first_name.as_ref()), + last_name: Arc::from(last_name.as_ref()), + maiden_name: maiden_name.map(|m| Arc::from(m.as_ref())), + date_of_birth, + sex, + parents, + marriages: Vec::with_capacity(1), + } + } + + pub fn add_parent(&mut self, parent: PersonID) { + self.parents.push(parent); + } + + pub fn add_marriage(&mut self, marriage: Marriage) { + self.marriages.push(marriage); + } + + pub fn id(&self) -> Uuid { + self.id + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Sex { + Male, + Female, + Other(Arc), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Marriage { + wife: PersonID, + husband: PersonID, + date: DateTime, + divorce_date: Option>, +} + +#[allow(dead_code)] +impl Marriage { + pub fn new( + wife: PersonID, + husband: PersonID, + date: DateTime, + divorce_date: Option>, + ) -> Self { + Self { + wife, + husband, + date, + divorce_date, + } + } + + pub fn divorce(&mut self, date: DateTime) { + self.divorce_date = Some(date); + } +} diff --git a/src/stammbaum.rs b/src/stammbaum.rs new file mode 100644 index 0000000..b0eac6f --- /dev/null +++ b/src/stammbaum.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; + +use crate::person::{Person, PersonID}; + +use askama::Template; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Stammbaum { + members: Vec, +} + +#[allow(dead_code)] +impl Stammbaum { + pub fn new(members: Vec) -> Self { + Self { members } + } + + pub fn get(&self, id: PersonID) -> Option<&Person> { + self.members.iter().find(|p| p.id() == id) + } + + pub fn render(&self) -> String { + self.members + .iter() + .flat_map(|p: &Person| p.render()) + .collect() + } +} + +mod test { + #![allow(unused_imports, clippy::unwrap_used)] + use std::io::Write; + + use chrono::{TimeZone, Utc}; + + use crate::person::{Marriage, Sex}; + + use super::*; + + #[test] + fn creation() { + let mut mother = Person::new( + "Jane", + "Doe", + Some("Musterfrau"), + Sex::Female, + Utc.with_ymd_and_hms(1990, 1, 2, 3, 4, 5).unwrap(), + vec![], + ); + let mut father = Person::new( + "John", + "Doe", + None, + Sex::Male, + Utc.with_ymd_and_hms(1991, 2, 3, 4, 5, 6).unwrap(), + vec![], + ); + + let marriage = Marriage::new( + mother.id(), + father.id(), + Utc.with_ymd_and_hms(2020, 3, 4, 5, 6, 7).unwrap(), + None, + ); + + mother.add_marriage(marriage.clone()); + father.add_marriage(marriage); + + let child = Person::new( + "Johnny", + "Doe", + None, + Sex::Male, + Utc.with_ymd_and_hms(2021, 4, 5, 6, 7, 8).unwrap(), + vec![mother.id(), father.id()], + ); + + let stammbaum = Stammbaum::new(vec![father, mother, child]); + + let mut file = std::fs::File::create("test.json").unwrap(); + file.write_all(serde_json::to_string_pretty(&stammbaum).unwrap().as_bytes()) + .unwrap(); + } +} diff --git a/templates/_layout.html b/templates/_layout.html new file mode 100644 index 0000000..54aeeb6 --- /dev/null +++ b/templates/_layout.html @@ -0,0 +1,60 @@ + + + + + + + Family Tree Node + + + + + {%~ block content%}{% endblock ~%} + + + diff --git a/templates/person.html b/templates/person.html new file mode 100644 index 0000000..a37a02f --- /dev/null +++ b/templates/person.html @@ -0,0 +1,38 @@ +{% extends "_layout.html" %} + +{%- block content -%} +
+
+ {{ first_name }} + {{ last_name }} + {% if maiden_name.is_some() %} + ({{ maiden_name.as_ref().unwrap() }}) + {% endif %} +
+
+
Sex: + {% match sex %} + {% when Sex::Male %}Male + {% when Sex::Female %}Female + {% when Sex::Other(desc) %}{{ desc }} + {% endmatch %} +
+
Birthday: {{ date_of_birth.format("%Y-%m-%d") }}
+ID: {{ id }} +
+ Parents: + {% if parents.is_empty() %} +
    + {% else %} +
      + {% for parent_id in &parents %} +
    • +{{ parent_id }} + +
    • + {% endfor %} +
    + {% endif %} +
    +
    + {%- endblock -%} diff --git a/test.json b/test.json new file mode 100644 index 0000000..e9321df --- /dev/null +++ b/test.json @@ -0,0 +1,49 @@ +{ + "members": [ + { + "id": "c7699942-7afa-47f1-b155-7357825a8aae", + "first_name": "John", + "last_name": "Doe", + "sex": "Male", + "date_of_birth": "1991-02-03T04:05:06Z", + "parents": [], + "marriages": [ + { + "wife": "41fb59f9-c572-4785-b0cc-90dfba3aef10", + "husband": "c7699942-7afa-47f1-b155-7357825a8aae", + "date": "2020-03-04T05:06:07Z", + "divorce_date": null + } + ] + }, + { + "id": "41fb59f9-c572-4785-b0cc-90dfba3aef10", + "first_name": "Jane", + "last_name": "Doe", + "maiden_name": "Musterfrau", + "sex": "Female", + "date_of_birth": "1990-01-02T03:04:05Z", + "parents": [], + "marriages": [ + { + "wife": "41fb59f9-c572-4785-b0cc-90dfba3aef10", + "husband": "c7699942-7afa-47f1-b155-7357825a8aae", + "date": "2020-03-04T05:06:07Z", + "divorce_date": null + } + ] + }, + { + "id": "1456bd02-c7d9-480d-922f-a2658bc4760c", + "first_name": "Johnny", + "last_name": "Doe", + "sex": "Male", + "date_of_birth": "2021-04-05T06:07:08Z", + "parents": [ + "41fb59f9-c572-4785-b0cc-90dfba3aef10", + "c7699942-7afa-47f1-b155-7357825a8aae" + ], + "marriages": [] + } + ] +} \ No newline at end of file