feat: initial data model + server
This commit is contained in:
parent
a6246ffe66
commit
76d27dfcec
@ -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
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 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