feat: initial data model + server
This commit is contained in:
parent
a6246ffe66
commit
76d27dfcec
@ -6,5 +6,10 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
askama = "0.14.0"
|
askama = "0.14.0"
|
||||||
axum = "0.8.4"
|
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 = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
uuid = { version = "1.17.0", features = ["serde", "v4"] }
|
||||||
|
78
index.html
Normal file
78
index.html
Normal 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>
|
62
src/lib.rs
62
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 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
5
src/main.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use stammbaum::run;
|
||||||
|
fn main() {
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
run().unwrap();
|
||||||
|
}
|
@ -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
84
src/stammbaum.rs
Normal 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
60
templates/_layout.html
Normal 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
38
templates/person.html
Normal 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
49
test.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user