Migrate what bit of the yapper.py script there was to rust.

This commit is contained in:
Olivia Brooks
2025-10-12 22:34:54 -04:00
parent bda1b138df
commit d9dcf43be8
6 changed files with 719 additions and 0 deletions

163
src/blog.rs Normal file
View File

@@ -0,0 +1,163 @@
use crate::error;
use chrono::NaiveDate;
use ron::error::SpannedResult;
use serde::{Deserialize, Deserializer, de::DeserializeOwned};
use std::{fs::File, path::PathBuf};
const SANE_DATE_FORMAT: &str = "%Y-%m-%d";
pub static FALLBACK_META_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static FALLBACK_META: std::sync::OnceLock<MetaFallback> = std::sync::OnceLock::new();
/// Make sure to populate `FALLBACK_META_PATH` before constructing a `Meta`.
///
/// `MetaFallback` is the same as `Meta`, but without a `Default` impl.
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct Meta {
title: String,
description: String,
tags: Vec<String>,
site_name: String,
locale: String,
#[serde(rename = "type")]
type_: String, // `type` is a keyword, this gets around that limitation.
authors: Vec<crate::og::Author>,
date: Date,
image: Option<Image>,
}
impl Default for Meta {
fn default() -> Self {
// Get FALLBACK_META, loading it from FALLBACK_META_PATH if OnceLock not initialized.
FALLBACK_META
.get_or_init(|| {
MetaFallback::try_from(FALLBACK_META_PATH.get().unwrap().to_owned())
.expect("Failed to deserialize fallback_meta file")
})
.to_owned()
.as_meta()
}
}
/// Same as `Meta`, but acts as the fallback for Meta's default values.
///
/// Not intended to be directly constructed; populate `FALLBACK_META_PATH`
/// before constructing a `Meta`.
#[derive(Debug, Deserialize)]
struct MetaFallback {
title: String,
description: String,
tags: Vec<String>,
site_name: String,
locale: String,
#[serde(rename = "type")]
type_: String, // `type` is a keyword, this gets around that limitation.
authors: Vec<crate::og::Author>,
date: Date,
image: Option<Image>,
}
impl MetaFallback {
fn as_meta(&self) -> Meta {
// This is just to skip some `self.` spam.
// There doesn't seem to be a better way to do this.
let MetaFallback {
title,
description,
tags,
site_name,
locale,
type_,
authors,
date,
image,
} = self;
Meta {
title: title.to_string(),
description: description.to_string(),
tags: tags.to_owned(),
site_name: site_name.to_string(),
locale: locale.to_string(),
type_: type_.to_string(),
authors: authors.to_owned(),
date: date.to_owned(),
image: image.to_owned(),
}
}
}
fn meta_try_from_file<T: DeserializeOwned>(file: File) -> SpannedResult<T> {
use ron::extensions::Extensions;
ron::Options::default()
//.with_default_extension(Extensions::EXPLICIT_STRUCT_NAMES)
.with_default_extension(Extensions::IMPLICIT_SOME)
.with_default_extension(Extensions::UNWRAP_NEWTYPES)
.with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
.from_reader(file)
}
impl TryFrom<File> for Meta {
type Error = error::MetaError;
fn try_from(file: File) -> Result<Self, Self::Error> {
Ok(meta_try_from_file(file)?)
}
}
impl TryFrom<File> for MetaFallback {
type Error = error::MetaError;
fn try_from(file: File) -> Result<Self, Self::Error> {
Ok(meta_try_from_file(file)?)
}
}
impl TryFrom<PathBuf> for Meta {
type Error = error::MetaError;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
Self::try_from(
std::fs::OpenOptions::new()
.read(true) // Just ensure that file opens read-only.
.open(path.as_path())?,
)
}
}
impl TryFrom<PathBuf> for MetaFallback {
type Error = error::MetaError;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
Self::try_from(
std::fs::OpenOptions::new()
.read(true) // Just ensure that file opens read-only.
.open(path.as_path())?,
)
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Image(String);
#[derive(Clone, Debug, Deserialize, Default)]
#[serde(default)]
pub struct Date {
#[serde(deserialize_with = "naive_date_from_str")]
posted: NaiveDate,
#[serde(deserialize_with = "naive_date_from_str")]
modified: NaiveDate,
}
fn naive_date_from_str<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
NaiveDate::parse_from_str(Deserialize::deserialize(deserializer)?, SANE_DATE_FORMAT)
.map_err(serde::de::Error::custom)
}

28
src/error.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::{fmt, io};
#[derive(Debug)]
pub enum MetaError {
IOError(io::Error),
RonSpanned(ron::error::SpannedError),
}
impl fmt::Display for MetaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IOError(err) => write!(f, "{}", err),
Self::RonSpanned(err) => write!(f, "{}", err),
}
}
}
impl From<ron::error::SpannedError> for MetaError {
fn from(error: ron::error::SpannedError) -> Self {
Self::RonSpanned(error)
}
}
impl From<io::Error> for MetaError {
fn from(error: io::Error) -> Self {
Self::IOError(error)
}
}

37
src/main.rs Normal file
View File

@@ -0,0 +1,37 @@
mod blog;
mod error;
mod og;
use clap::Parser;
use std::path::PathBuf;
use crate::blog::Meta;
#[derive(Debug, Parser)]
#[clap(author, version, about)]
struct Args {
/// Path to blog root.
#[arg(
short = 'i',
long,
value_hint = clap::ValueHint::DirPath,
default_value = format!("../test_tree/")
)]
blog_path: PathBuf,
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
fallback_meta: PathBuf,
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
single_meta: PathBuf,
}
fn main() {
let config = Args::parse();
blog::FALLBACK_META_PATH.get_or_init(|| config.fallback_meta);
let _meta = Meta::try_from(config.single_meta).expect("Failed to deserialize single_meta file");
println!("Loaded FallbackMeta");
}

46
src/og.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::fmt;
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub enum Gender {
Male,
Female,
}
impl fmt::Display for Gender {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl Into<String> for Gender {
fn into(self) -> String {
self.to_string().to_lowercase()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Author {
name: Name,
gender: Gender,
}
#[derive(Clone, Debug, Deserialize)]
pub enum Name {
FirstOnly {
first: String,
},
Full {
first: String,
last: String,
},
UserOnly {
user: String,
},
All {
first: String,
last: String,
user: String,
},
}