feat: initial data model + server

This commit is contained in:
itsscb 2025-06-21 21:09:56 +02:00
parent a6246ffe66
commit 76d27dfcec
9 changed files with 475 additions and 0 deletions

View File

@ -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"] }

78
index.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Family Tree Node</title>
<style>
.family-node {
border: 1px solid #aaa;
border-radius: 6px;
padding: 12px 16px;
width: 220px;
font-family: Arial, sans-serif;
background: #f9f9f9;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.1);
}
.family-node .name {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 6px;
}
.family-node .maiden-name {
font-weight: normal;
font-style: italic;
margin-left: 6px;
color: #555;
}
.family-node .details div {
margin: 3px 0;
font-size: 0.9em;
color: #333;
}
.family-node .parents {
margin-top: 8px;
font-size: 0.85em;
color: #666;
}
.family-node .parents ul {
margin: 4px 0 0 16px;
padding: 0;
list-style-type: disc;
}
.family-node .parents ul li {
word-break: break-word;
}
</style>
</head>
<body>
<div class="family-node">
<div class="name">
<span class="first-name">Jane</span>
<span class="last-name">Doe</span>
<span class="maiden-name">(Musterfrau)</span>
</div>
<div class="details">
<div>Sex: Female</div>
<div>DOB: 1990-01-02</div>
<div class="parents">
Parents:
<ul>
<!-- No parents in this example, so empty -->
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@ -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<String>,
State(stammbaum): State<Stammbaum>,
) -> Result<impl IntoResponse, impl IntoResponse> {
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<Stammbaum>,
) -> Result<impl IntoResponse, impl IntoResponse> {
Ok::<axum::response::Html<std::string::String>, 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(())
}

5
src/main.rs Normal file
View File

@ -0,0 +1,5 @@
use stammbaum::run;
fn main() {
#[allow(clippy::unwrap_used)]
run().unwrap();
}

View File

@ -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<str>,
last_name: Arc<str>,
#[serde(skip_serializing_if = "Option::is_none")]
maiden_name: Option<Arc<str>>,
sex: Sex,
date_of_birth: DateTime<Utc>,
parents: Vec<PersonID>,
marriages: Vec<Marriage>,
}
#[allow(dead_code)]
impl Person {
pub fn new<T: AsRef<str>>(
first_name: T,
last_name: T,
maiden_name: Option<T>,
sex: Sex,
date_of_birth: DateTime<Utc>,
parents: Vec<PersonID>,
) -> 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<str>),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Marriage {
wife: PersonID,
husband: PersonID,
date: DateTime<Utc>,
divorce_date: Option<DateTime<Utc>>,
}
#[allow(dead_code)]
impl Marriage {
pub fn new(
wife: PersonID,
husband: PersonID,
date: DateTime<Utc>,
divorce_date: Option<DateTime<Utc>>,
) -> Self {
Self {
wife,
husband,
date,
divorce_date,
}
}
pub fn divorce(&mut self, date: DateTime<Utc>) {
self.divorce_date = Some(date);
}
}

84
src/stammbaum.rs Normal file
View File

@ -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<Person>,
}
#[allow(dead_code)]
impl Stammbaum {
pub fn new(members: Vec<Person>) -> 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();
}
}

60
templates/_layout.html Normal file
View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Family Tree Node</title>
<style>
.family-node {
border: 1px solid #aaa;
border-radius: 6px;
padding: 12px 16px;
width: 220px;
font-family: Arial, sans-serif;
background: #f9f9f9;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.1);
}
.family-node .name {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 6px;
}
.family-node .maiden-name {
font-weight: normal;
font-style: italic;
margin-left: 6px;
color: #555;
}
.family-node .details div {
margin: 3px 0;
font-size: 0.9em;
color: #333;
}
.family-node .parents {
margin-top: 8px;
font-size: 0.85em;
color: #666;
}
.family-node .parents ul {
margin: 4px 0 0 16px;
padding: 0;
list-style-type: disc;
}
.family-node .parents ul li {
word-break: break-word;
}
</style>
</head>
<body>
{%~ block content%}{% endblock ~%}
</body>
</html>

38
templates/person.html Normal file
View File

@ -0,0 +1,38 @@
{% extends "_layout.html" %}
{%- block content -%}
<div class="family-node">
<div class="name">
<span class="first-name">{{ first_name }}</span>
<span class="last-name">{{ last_name }}</span>
{% if maiden_name.is_some() %}
<span class="maiden-name">({{ maiden_name.as_ref().unwrap() }})</span>
{% endif %}
</div>
<div class="details">
<div>Sex:
{% match sex %}
{% when Sex::Male %}Male
{% when Sex::Female %}Female
{% when Sex::Other(desc) %}{{ desc }}
{% endmatch %}
</div>
<div>Birthday: {{ date_of_birth.format("%Y-%m-%d") }}</div>
<a href="/{{ id }}">ID: {{ id }}</a>
<div class="parents">
Parents:
{% if parents.is_empty() %}
<ul></ul>
{% else %}
<ul>
{% for parent_id in &parents %}
<li>
<a href="/{{ parent_id }}">{{ parent_id }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{%- endblock -%}

49
test.json Normal file
View File

@ -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": []
}
]
}