make code typesafe

This commit is contained in:
Matthias 2024-02-05 23:36:59 +01:00
parent e3c051ab97
commit e7447c8789
3 changed files with 181 additions and 15 deletions

62
render/Cargo.lock generated
View file

@ -73,6 +73,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "hashbrown"
version = "0.14.3"
@ -88,6 +97,16 @@ dependencies = [
"libm",
]
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.2.1"
@ -199,6 +218,7 @@ dependencies = [
"itertools",
"serde",
"serde_json",
"url",
]
[[package]]
@ -249,6 +269,21 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicase"
version = "2.7.0"
@ -258,12 +293,39 @@ dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "version_check"
version = "0.9.4"

View file

@ -11,3 +11,4 @@ indexmap = "2.2.1"
itertools = "0.12.1"
serde = { version = "1.0.196", features = ["serde_derive"] }
serde_json = "1.0.113"
url = { version = "2.5.0", features = ["serde"] }

View file

@ -3,7 +3,8 @@ use std::fs;
use askama::Template;
use indexmap::IndexMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use url::Url;
#[derive(Template)]
#[template(path = "README.md")]
@ -16,28 +17,130 @@ struct ReadmeTemplate {
forum: YearMap,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
/// A tag is a special kind of string that
/// - is lowercase
/// - has no whitespace
/// - has no special characters except for `-`
/// - has no leading or trailing `-`
/// - has no consecutive `-`
/// - has no more than 50 characters
/// - is not empty
/// - only contains ASCII characters
/// - does not contain numbers
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
struct Tag(String);
impl TryFrom<String> for Tag {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
println!("value: {:?}", value);
if value.is_empty() {
return Err("Tag cannot be empty");
}
if value.len() > 50 {
return Err("Tag cannot be longer than 50 characters");
}
if value.contains(|c: char| !c.is_ascii_lowercase() && c != '-') {
return Err("Tag can only contain lowercase ASCII characters");
}
if value.contains(|c: char| c.is_ascii_digit()) {
return Err("Tag cannot contain numbers");
}
if value.contains(|c: char| !c.is_ascii() && c != '-') {
return Err("Tag can only contain ASCII characters and hyphens");
}
if value.starts_with('-') || value.ends_with('-') {
return Err("Tag cannot start or end with a hyphen");
}
if value.contains("--") {
return Err("Tag cannot contain consecutive hyphens");
}
if value.contains(char::is_whitespace) {
return Err("Tag cannot contain whitespace");
}
Ok(Tag(value))
}
}
impl<'de> Deserialize<'de> for Tag {
fn deserialize<D>(deserializer: D) -> Result<Tag, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Tag::try_from(s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Deserialize, Clone, Serialize)]
enum Difficulty {
#[serde(rename = "all")]
All,
#[serde(rename = "beginner")]
Beginner,
#[serde(rename = "intermediate")]
Intermediate,
#[serde(rename = "advanced")]
Advanced,
}
#[derive(Debug, Deserialize, Clone, Serialize)]
enum InteractivityLevel {
#[serde(rename = "low")]
Low,
#[serde(rename = "medium")]
Medium,
#[serde(rename = "high")]
High,
}
#[derive(Debug, Deserialize, Clone, Serialize, Eq, PartialEq, Ord, PartialOrd)]
enum Category {
#[serde(rename = "project")]
Project,
#[serde(rename = "workshop")]
Workshop,
#[serde(rename = "book")]
Book,
#[serde(rename = "article")]
Article,
#[serde(rename = "talk")]
Talk,
#[serde(rename = "forum")]
Forum,
}
#[derive(Debug, Deserialize, Clone)]
struct Resource {
title: String,
url: String,
url: Url,
description: String,
tags: Vec<String>,
tags: Vec<Tag>,
official: bool,
year: usize,
#[serde(rename = "difficultyLevel")]
difficulty_level: String,
difficulty_level: Difficulty,
duration: Option<String>,
#[serde(rename = "interactivityLevel")]
interactivity_level: String,
interactivity_level: InteractivityLevel,
free: bool,
category: String,
category: Category,
}
type Resources = Vec<Resource>;
type YearMap = IndexMap<usize, Resources>;
fn group_by_year(resources: &Resources, category: &str) -> YearMap {
fn group_by_year(resources: &Resources, category: Category) -> YearMap {
resources
.iter()
.filter(|r| r.category == category)
@ -49,7 +152,7 @@ fn group_by_year(resources: &Resources, category: &str) -> YearMap {
})
}
fn sort_by_title(resources: &Resources, category: &str) -> Resources {
fn sort_by_title(resources: &Resources, category: Category) -> Resources {
resources
.iter()
.filter(|r| r.category == category)
@ -63,12 +166,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let resources: Resources = serde_json::from_reader(file)?;
let readme = ReadmeTemplate {
projects: sort_by_title(&resources, "project"),
workshops: sort_by_title(&resources, "workshop"),
books: sort_by_title(&resources, "book"),
articles: group_by_year(&resources, "article"),
talks: group_by_year(&resources, "talk"),
forum: group_by_year(&resources, "forum"),
projects: sort_by_title(&resources, Category::Project),
workshops: sort_by_title(&resources, Category::Workshop),
books: sort_by_title(&resources, Category::Book),
articles: group_by_year(&resources, Category::Article),
talks: group_by_year(&resources, Category::Talk),
forum: group_by_year(&resources, Category::Forum),
};
fs::write("README.md", readme.render()?)?;