Switch to TOML for configuration

This commit is contained in:
Pantonshire committed 2022-06-04 20:53:54 +01:00
1 parent ad3a0ff956
commit d7a521e581
27 files changed
+276 -613

No files matched your search

+16 -84
View File
@@ -1,114 +1,46 @@
use std::{path::PathBuf, ops};
use std::{net::SocketAddr, path::PathBuf, str};
use knuffel::{
ast::{Literal, TypeName},
decode::{Context, Kind},
errors::{DecodeError, ExpectedType},
span::Spanned,
traits::ErrorSpan,
DecodeScalar,
};
use libshire::uuid::Uuid;
use serde::Deserialize;
//TODO: replace knuffel crate with kdl crate
#[derive(knuffel::Decode, Clone, Debug)]
pub struct Config {
#[knuffel(child, unwrap(argument))]
pub bind: String,
#[knuffel(child, unwrap(argument))]
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct Config {
pub bind: SocketAddr,
pub concurrency_limit: usize,
#[knuffel(child, unwrap(argument))]
pub static_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
pub favicon_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
pub robots_path: PathBuf,
#[knuffel(child, unwrap(argument))]
pub posts_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
pub post_media_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
pub namespace_uuid: Uuid,
#[knuffel(child)]
pub self_ref: SelfRefConfig,
#[knuffel(child)]
pub rss: RssConfig,
#[knuffel(child)]
pub atom: AtomConfig,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct SelfRefConfig {
#[knuffel(child, unwrap(argument))]
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct SelfRefConfig {
pub protocol: String,
#[knuffel(child, unwrap(argument))]
pub domain: String,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct RssConfig {
#[knuffel(child, unwrap(argument))]
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct RssConfig {
pub num_posts: usize,
#[knuffel(child, unwrap(argument))]
pub title: String,
#[knuffel(child, unwrap(argument))]
pub ttl: u32,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct AtomConfig {
#[knuffel(child, unwrap(argument))]
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct AtomConfig {
pub num_posts: usize,
#[knuffel(child, unwrap(argument))]
pub title: String,
}
#[derive(Clone, Copy, Default, Debug)]
#[repr(transparent)]
pub struct Uuid(pub libshire::uuid::Uuid);
impl str::FromStr for Config {
type Err = toml::de::Error;
impl Uuid {
pub fn as_inner(&self) -> &libshire::uuid::Uuid {
&self.0
}
}
impl ops::Deref for Uuid {
type Target = libshire::uuid::Uuid;
fn deref(&self) -> &Self::Target {
self.as_inner()
}
}
impl<S: ErrorSpan> DecodeScalar<S> for Uuid {
fn type_check(type_name: &Option<Spanned<TypeName, S>>, ctx: &mut Context<S>) {
if let Some(type_name) = type_name {
ctx.emit_error(DecodeError::TypeName {
span: type_name.span().clone(),
found: Some((&**type_name).clone()),
expected: ExpectedType::no_type(),
rust_type: "Uuid",
});
}
}
fn raw_decode(
value: &Spanned<Literal, S>,
ctx: &mut Context<S>,
) -> Result<Self, DecodeError<S>> {
match &**value {
Literal::String(s) => match s.parse() {
Ok(uuid) => Ok(Self(uuid)),
Err(err) => {
ctx.emit_error(DecodeError::conversion(value, err));
Ok(Default::default())
}
},
_ => {
ctx.emit_error(DecodeError::scalar_kind(Kind::String, value));
Ok(Default::default())
}
}
fn from_str(s: &str) -> Result<Self, Self::Err> {
toml::from_str(s)
}
}
+6 -7
View File
@@ -4,7 +4,6 @@ use std::{
time::Duration,
};
use miette::{IntoDiagnostic, WrapErr};
use notify::{
DebouncedEvent,
RecommendedWatcher,
@@ -14,20 +13,20 @@ use notify::{
};
use tracing::info;
pub fn start_watching(
use crate::Error;
pub(crate) fn start_watching(
tx: mpsc::Sender<DebouncedEvent>,
watch_path: &Path
) -> miette::Result<RecommendedWatcher>
) -> Result<RecommendedWatcher, Error>
{
let mut watcher = watcher(tx, Duration::from_secs(2))
.into_diagnostic()
.wrap_err("Failed to create filesystem watcher")?;
.map_err(Error::CreateWatcher)?;
// Watch the path in non-recursive mode, so events are not generated for nodes in
// sub-directories.
watcher.watch(watch_path, RecursiveMode::NonRecursive)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to watch directory {}", watch_path.to_string_lossy()))?;
.map_err(|err| Error::WatchDir(watch_path.to_owned(), err))?;
info!(path = %watch_path.to_string_lossy(), "Watching directory");
+81 -27
View File
@@ -4,10 +4,20 @@ mod render;
mod service;
mod template;
use std::{env, fs, sync::Arc, thread};
use std::{
env,
error,
fmt,
fs,
io,
net::SocketAddr,
path::PathBuf,
process,
sync::Arc,
thread,
};
use axum::Server;
use miette::{IntoDiagnostic, Context};
use hyper::Server;
use tracing::info;
use blog::{
@@ -18,23 +28,30 @@ use blog::{
use config::Config;
use render::Renderer;
fn main() -> miette::Result<()> {
fn main() {
if let Err(err) = run() {
eprintln!("***** Fatal error *****");
eprintln!("{}", err);
process::exit(1);
}
}
fn run() -> Result<(), Error> {
tracing_subscriber::fmt::init();
// Load the configuration from the KDL config file specified by the first command-line
// Load the configuration from the TOML config file specified by the first command-line
// argument.
let config = Arc::new({
let config_path = env::args().nth(1)
.ok_or_else(|| miette::Error::msg("No config file specified"))?;
.ok_or(Error::NoConfig)?;
info!(path = %config_path, "Loading config");
let contents = fs::read_to_string(&config_path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to read config file {}", config_path))?;
.map_err(Error::ReadConfig)?;
knuffel::parse::<Config>(&config_path, &contents)
.wrap_err_with(|| format!("Failed to parse config file {}", config_path))?
contents.parse::<Config>()
.map_err(Error::BadConfig)?
});
// Create the data structure used to store the rendered posts. This uses an `Arc` internally,
@@ -65,30 +82,67 @@ fn main() -> miette::Result<()> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.into_diagnostic()
.wrap_err("Failed to create async runtime")?
.block_on(run(config, posts_store))
.map_err(Error::TokioRuntime)?
.block_on(run_server(config, posts_store))
}
async fn run(
async fn run_server(
config: Arc<Config>,
posts_store: ConcurrentPostsStore,
) -> miette::Result<()>
) -> Result<(), Error>
{
let bind_address = &config.bind
.parse()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?;
let service = service::site_service(config.clone(), posts_store);
let service = service::site_service(config, posts_store);
info!(address = %config.bind, "Starting server");
info!(address = %bind_address, "Starting server");
Server::try_bind(bind_address)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to bind {}", bind_address))?
Server::try_bind(&config.bind)
.map_err(|err| Error::Bind(config.bind, err))?
.serve(service.into_make_service())
.await
.into_diagnostic()
.wrap_err("Fatal error while running the server")
.map_err(Error::Server)
}
#[derive(Debug)]
enum Error {
NoConfig,
ReadConfig(io::Error),
BadConfig(toml::de::Error),
CreateWatcher(notify::Error),
WatchDir(PathBuf, notify::Error),
TokioRuntime(io::Error),
Bind(SocketAddr, hyper::Error),
Server(hyper::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoConfig => {
write!(f, "no config file specified")
},
Self::ReadConfig(err) => {
write!(f, "failed to read config file: {}", err)
},
Self::BadConfig(err) => {
write!(f, "error in config: {}", err)
},
Self::CreateWatcher(err) => {
write!(f, "failed to create filesystem watcher: {}", err)
},
Self::WatchDir(path, err) => {
write!(f, "failed to watch directory {}: {}", path.to_string_lossy(), err)
},
Self::TokioRuntime(err) => {
write!(f, "failed to create async runtime: {}", err)
},
Self::Bind(addr, err) => {
write!(f, "failed to bind {}: {}", addr, err)
},
Self::Server(err) => {
write!(f, "error while running server: {}", err)
},
}
}
}
impl error::Error for Error {}
+8 -8
View File
@@ -18,7 +18,7 @@ use blog::{
use crate::Config;
pub struct Renderer {
pub(crate) struct Renderer {
config: Arc<Config>,
posts: ConcurrentPostsStore,
code_renderer: CodeBlockRenderer,
@@ -27,7 +27,7 @@ pub struct Renderer {
}
impl Renderer {
pub fn new(
pub(crate) fn new(
config: Arc<Config>,
posts: ConcurrentPostsStore,
code_renderer: CodeBlockRenderer,
@@ -51,7 +51,7 @@ impl Renderer {
}
#[tracing::instrument(skip(self))]
pub fn handle_events(self) {
pub(crate) fn handle_events(self) {
while let Ok(notify_event) = self.rx.recv() {
let fs_event = match notify_event {
// Convert create & write events for valid post file names to update events.
@@ -204,7 +204,7 @@ impl Renderer {
Post::new_from_str(
&self.code_renderer,
*self.config.namespace_uuid,
self.config.namespace_uuid,
target.id.clone(),
updated,
&contents
@@ -221,8 +221,8 @@ enum Event {
}
struct EventTarget {
pub path: PathBuf,
pub id: Id,
path: PathBuf,
id: Id,
}
impl fmt::Debug for EventTarget {
@@ -232,7 +232,7 @@ impl fmt::Debug for EventTarget {
}
impl EventTarget {
pub fn from_path(path: PathBuf) -> Option<Self> {
fn from_path(path: PathBuf) -> Option<Self> {
path.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(Id::from_file_name)
@@ -243,7 +243,7 @@ impl EventTarget {
}
}
pub enum Error {
pub(crate) enum Error {
Io(Box<io::Error>),
NotAFile,
Parsing(Box<ParseError>),
+2 -2
View File
@@ -9,7 +9,7 @@ use crate::Config;
use super::response::Atom;
pub async fn handle(
pub(super) async fn handle(
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,
) -> Atom<Bytes> {
@@ -50,7 +50,7 @@ pub async fn handle(
Atom(
atom::FeedBuilder::default()
.id(format!("urn:uuid:{}", *config.namespace_uuid))
.id(format!("urn:uuid:{}", config.namespace_uuid))
.title(config.atom.title.clone())
.updated(updated)
.links(vec![
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::template;
use super::response::Html;
pub async fn handle() -> Html {
pub(super) async fn handle() -> Html {
Html::new()
.with_title_static("Contact")
.with_crawler_permissive()
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::template;
use super::response::Html;
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
Html::new()
.with_title_static("Pantonshire")
.with_crawler_permissive()
+1 -1
View File
@@ -8,4 +8,4 @@ mod rss;
mod site;
mod static_content;
pub use site::service as site_service;
pub(crate) use site::service as site_service;
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::template;
use super::response::{Error, Html};
pub async fn handle(
pub(super) async fn handle(
Path(post_id): Path<String>,
Extension(posts): Extension<ConcurrentPostsStore>
) -> Result<Html, Error>
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::template;
use super::response::Html;
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
Html::new()
.with_title_static("Articles")
.with_crawler_permissive()
+22 -22
View File
@@ -11,7 +11,7 @@ use axum::{
use maud::{html, Markup, Render, Escaper, DOCTYPE};
#[derive(Debug)]
pub enum Error {
pub(super) enum Error {
Internal,
PostNotFound,
StaticResourceNotFound,
@@ -62,7 +62,7 @@ impl IntoResponse for Error {
}
}
pub struct Html {
pub(super) struct Html {
status: StatusCode,
title: Cow<'static, str>,
head: Option<Markup>,
@@ -71,7 +71,7 @@ pub struct Html {
}
impl Html {
pub fn new() -> Self {
pub(super) fn new() -> Self {
Self {
status: StatusCode::OK,
title: Cow::Borrowed("untitled"),
@@ -81,39 +81,39 @@ impl Html {
}
}
pub fn with_status(self, status: StatusCode) -> Self {
pub(super) fn with_status(self, status: StatusCode) -> Self {
Self { status, ..self }
}
pub fn with_title(self, title: Cow<'static, str>) -> Self {
pub(super) fn with_title(self, title: Cow<'static, str>) -> Self {
Self { title, ..self }
}
pub fn with_title_static(self, title: &'static str) -> Self {
pub(super) fn with_title_static(self, title: &'static str) -> Self {
self.with_title(Cow::Borrowed(title))
}
pub fn with_title_owned(self, title: String) -> Self {
pub(super) fn with_title_owned(self, title: String) -> Self {
self.with_title(Cow::Owned(title))
}
pub fn with_head(self, head: Markup) -> Self {
pub(super) fn with_head(self, head: Markup) -> Self {
Self { head: Some(head), ..self }
}
pub fn with_body(self, body: Markup) -> Self {
pub(super) fn with_body(self, body: Markup) -> Self {
Self { body: Some(body), ..self }
}
pub fn with_crawler_hints(self, crawler_hints: CrawlerHints) -> Self {
pub(super) fn with_crawler_hints(self, crawler_hints: CrawlerHints) -> Self {
Self { crawler_hints, ..self }
}
pub fn with_crawler_restrictive(self) -> Self {
pub(super) fn with_crawler_restrictive(self) -> Self {
self.with_crawler_hints(CrawlerHints::restrictive())
}
pub fn with_crawler_permissive(self) -> Self {
pub(super) fn with_crawler_permissive(self) -> Self {
self.with_crawler_hints(CrawlerHints::permissive())
}
}
@@ -160,7 +160,7 @@ impl IntoResponse for Html {
}
#[derive(Clone, Copy, Debug)]
pub struct CrawlerHints {
pub(super) struct CrawlerHints {
index: bool,
follow: bool,
archive: bool,
@@ -169,7 +169,7 @@ pub struct CrawlerHints {
}
impl CrawlerHints {
pub const fn restrictive() -> Self {
pub(super) const fn restrictive() -> Self {
Self {
index: false,
follow: false,
@@ -179,7 +179,7 @@ impl CrawlerHints {
}
}
pub const fn permissive() -> Self {
pub(super) const fn permissive() -> Self {
Self {
index: true,
follow: true,
@@ -189,23 +189,23 @@ impl CrawlerHints {
}
}
pub const fn with_index(self, index: bool) -> Self {
pub(super) const fn with_index(self, index: bool) -> Self {
Self { index, ..self }
}
pub const fn with_follow(self, follow: bool) -> Self {
pub(super) const fn with_follow(self, follow: bool) -> Self {
Self { follow, ..self }
}
pub const fn with_archive(self, archive: bool) -> Self {
pub(super) const fn with_archive(self, archive: bool) -> Self {
Self { archive, ..self }
}
pub const fn with_snippet(self, snippet: bool) -> Self {
pub(super) const fn with_snippet(self, snippet: bool) -> Self {
Self { snippet, ..self }
}
pub const fn with_image_index(self, image_index: bool) -> Self {
pub(super) const fn with_image_index(self, image_index: bool) -> Self {
Self { image_index, ..self }
}
@@ -271,7 +271,7 @@ impl Render for CrawlerHints {
}
}
pub struct Rss<T>(pub T);
pub(super) struct Rss<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Rss<T> {
fn into_response(self) -> Response {
@@ -284,7 +284,7 @@ impl<T: Into<Full<Bytes>>> IntoResponse for Rss<T> {
}
}
pub struct Atom<T>(pub T);
pub(super) struct Atom<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Atom<T> {
fn into_response(self) -> Response {
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::Config;
use super::response::Rss;
pub async fn handle(
pub(super) async fn handle(
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,
) -> Rss<Bytes> {
+2 -2
View File
@@ -26,7 +26,7 @@ use super::{
static_content,
};
pub fn service(
pub(crate) fn service(
config: Arc<Config>,
posts_store: ConcurrentPostsStore,
) -> Router
@@ -55,7 +55,7 @@ pub fn service(
.layer(Extension(posts_store))
}
pub async fn handle_fallback(uri: Uri) -> Error {
async fn handle_fallback(uri: Uri) -> Error {
info!(path = %uri.path(), "Requested resource not found");
Error::RouteNotFound
}
@@ -18,7 +18,7 @@ use tracing::{info, error};
use super::response::Error;
pub fn file_service(file_path: &Path, mime: Option<&Mime>) -> MethodRouter<Body, Infallible> {
pub(super) fn file_service(file_path: &Path, mime: Option<&Mime>) -> MethodRouter<Body, Infallible> {
let serve_file = match mime {
Some(mime) => ServeFile::new_with_mime(file_path, mime),
None => ServeFile::new(file_path),
@@ -28,7 +28,7 @@ pub fn file_service(file_path: &Path, mime: Option<&Mime>) -> MethodRouter<Body,
.handle_error(handle_error)
}
pub fn dir_service(dir_path: &Path) -> MethodRouter<Body, Infallible> {
pub(super) fn dir_service(dir_path: &Path) -> MethodRouter<Body, Infallible> {
let fallback_service = handle_fallback
.into_service()
.map_err(Empty::elim::<io::Error>);
@@ -40,12 +40,12 @@ pub fn dir_service(dir_path: &Path) -> MethodRouter<Body, Infallible> {
.handle_error(handle_error)
}
pub async fn handle_fallback(uri: Uri) -> Error {
pub(super) async fn handle_fallback(uri: Uri) -> Error {
info!(path = %uri.path(), "Requested static file not found");
Error::StaticResourceNotFound
}
pub async fn handle_error(uri: Uri, err: io::Error) -> Error {
pub(super) async fn handle_error(uri: Uri, err: io::Error) -> Error {
error!(path = %uri.path(), err = %err, "IO error");
Error::Internal
}
+1 -1
View File
@@ -1,6 +1,6 @@
use maud::{html, Markup};
pub fn main_page(content: Markup) -> Markup {
pub(crate) fn main_page(content: Markup) -> Markup {
html! {
header #page_header {
nav #page_nav {
+4
View File
@@ -0,0 +1,4 @@
fn main() {
}
+6 -21
View File
@@ -1,18 +1,9 @@
use std::{error, fmt};
use kdl::KdlError;
#[derive(Debug)]
pub enum Error {
NoDelim,
Syntax(Box<KdlError>),
FieldMissing {
field: &'static str,
},
BadType {
field: &'static str,
expected: &'static str,
},
Header(Box<toml::de::Error>),
IdTooLong(usize),
}
@@ -22,14 +13,8 @@ impl fmt::Display for Error {
Self::NoDelim => {
write!(f, "post has no header; no delimiter `\\n---\\n` found")
},
Self::Syntax(err) => {
write!(f, "syntax error in post header: {}", err)
},
Self::FieldMissing { field } => {
write!(f, "missing required post header field `{}`", field)
},
Self::BadType { field, expected } => {
write!(f, "expected post header field `{}` to be {}", field, expected)
Self::Header(err) => {
write!(f, "error decoding post header: {}", err)
},
Self::IdTooLong(len) => {
write!(f, "post id too long: {} bytes", len)
@@ -40,8 +25,8 @@ impl fmt::Display for Error {
impl error::Error for Error {}
impl From<KdlError> for Error {
fn from(err: KdlError) -> Self {
Self::Syntax(Box::new(err))
impl From<toml::de::Error> for Error {
fn from(err: toml::de::Error) -> Self {
Self::Header(Box::new(err))
}
}
+48 -68
View File
@@ -1,16 +1,19 @@
use chrono::{DateTime, Utc};
use kdl::KdlDocument;
use libshire::strings::ShString22;
use std::{fmt, str};
use crate::time::{datetime_unix_seconds, unix_epoch};
use chrono::{DateTime, Utc};
use libshire::strings::ShString22;
use serde::{Serialize, Deserialize};
use super::error::Error;
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Header {
pub(super) title: String,
pub(super) subtitle: Option<String>,
pub(super) author: ShString22,
#[serde(default)]
pub(super) tags: Vec<ShString22>,
#[serde(default = "crate::time::unix_epoch")]
pub(super) published: DateTime<Utc>,
}
@@ -21,97 +24,74 @@ impl Header {
&self.title
}
#[inline]
#[must_use]
pub fn title_mut(&mut self) -> &mut String {
&mut self.title
}
#[inline]
#[must_use]
pub fn subtitle(&self) -> Option<&str> {
self.subtitle.as_deref()
}
#[inline]
#[must_use]
pub fn subtitle_mut(&mut self) -> &mut Option<String> {
&mut self.subtitle
}
#[inline]
#[must_use]
pub fn author(&self) -> &str {
&self.author
}
#[inline]
#[must_use]
pub fn author_mut(&mut self) -> &mut ShString22 {
&mut self.author
}
#[inline]
#[must_use]
pub fn tags(&self) -> &[ShString22] {
&self.tags
}
#[inline]
#[must_use]
pub fn tags_mut(&mut self) -> &mut Vec<ShString22> {
&mut self.tags
}
#[inline]
#[must_use]
pub fn published(&self) -> DateTime<Utc> {
self.published
}
}
impl<'a> TryFrom<&'a KdlDocument> for Header {
type Error = Error;
fn try_from(doc: &'a KdlDocument) -> Result<Self, Self::Error> {
let title = doc
.get_arg("title")
.ok_or(Error::FieldMissing { field: "title" })
.and_then(|value| value
.as_string()
.ok_or(Error::BadType { field: "title", expected: "string" })
.map(|title| title.to_owned()))?;
let subtitle = doc
.get_arg("subtitle")
.map(|value| value
.as_string()
.ok_or(Error::BadType { field: "subtitle", expected: "string" })
.map(|subtitle| subtitle.to_owned()))
.transpose()?;
let author = doc
.get_arg("title")
.ok_or(Error::FieldMissing { field: "author" })
.and_then(|value| value
.as_string()
.ok_or(Error::BadType { field: "author", expected: "string" })
.map(ShString22::from))?;
let tags = doc
.get("tags")
.map(|node| node.entries())
.unwrap_or_default()
.iter()
.filter_map(|entry| match entry.name() {
Some(_) => None,
None => Some(entry.value()),
})
.map(|value| value
.as_string()
.ok_or(Error::BadType { field: "tag", expected: "string" })
.map(ShString22::from))
.collect::<Result<_, _>>()?;
let published = doc
.get_arg("published")
.map(|value| value
.as_i64()
.ok_or(Error::BadType { field: "published", expected: "integer unix timestamp (seconds)" })
.map(datetime_unix_seconds))
.transpose()?
.unwrap_or_else(unix_epoch);
Ok(Header {
title,
subtitle,
author,
tags,
published,
})
#[inline]
#[must_use]
pub fn published_mut(&mut self) -> &mut DateTime<Utc> {
&mut self.published
}
}
impl TryFrom<KdlDocument> for Header {
type Error = Error;
impl str::FromStr for Header {
type Err = Error;
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
Self::try_from(&value)
fn from_str(s: &str) -> Result<Self, Self::Err> {
toml::from_str(s)
.map_err(Error::from)
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
toml::to_string_pretty(self)
.map_err(|_| fmt::Error)
.and_then(|s| f.write_str(&s))
}
}
-31
View File
@@ -1,31 +0,0 @@
use super::{error::Error, header::Header, source::PostSource};
pub struct MarkdownPost {
pub(super) header: Header,
pub(super) markdown: String,
}
impl MarkdownPost {
#[inline]
#[must_use]
pub fn header(&self) -> &Header {
&self.header
}
#[inline]
#[must_use]
pub fn markdown(&self) -> &str {
&self.markdown
}
}
impl TryFrom<PostSource> for MarkdownPost {
type Error = Error;
fn try_from(source: PostSource) -> Result<Self, Self::Error> {
Ok(Self {
header: source.header.try_into()?,
markdown: source.markdown,
})
}
}
+1 -3
View File
@@ -1,7 +1,6 @@
mod error;
mod id;
mod header;
mod markdown_post;
mod render;
mod rendered_post;
mod source;
@@ -9,10 +8,9 @@ mod source;
pub use error::Error;
pub use header::Header;
pub use id::Id;
pub use markdown_post::MarkdownPost;
pub use rendered_post::RenderedPost;
pub use source::PostSource;
const POST_FILE_EXTENSION: &str = ".kdl.md";
const POST_FILE_EXTENSION: &str = ".toml.md";
pub type Post = RenderedPost;
+7 -11
View File
@@ -8,9 +8,8 @@ use super::{
error::Error,
header::Header,
id::Id,
markdown_post::MarkdownPost,
render::render_markdown,
source::PostSource,
render::render_markdown,
};
pub struct RenderedPost {
@@ -30,19 +29,16 @@ impl RenderedPost {
source: &str
) -> Result<Self, Error>
{
let markdown_post = source
.parse::<PostSource>()
.and_then(MarkdownPost::try_from)?;
Self::new_from_markdown_post(code_renderer, namespace, id, updated, markdown_post)
let source = source.parse::<PostSource>()?;
Self::new_from_source(code_renderer, namespace, id, updated, source)
}
pub fn new_from_markdown_post(
pub fn new_from_source(
code_renderer: &CodeBlockRenderer,
namespace: Uuid,
id: Id,
updated: Option<DateTime<Utc>>,
markdown_post: MarkdownPost
source: PostSource
) -> Result<Self, Error>
{
let uuid = Uuid::new_v5(namespace, &*id)
@@ -53,9 +49,9 @@ impl RenderedPost {
Ok(Self {
uuid,
id,
header: markdown_post.header,
header: source.header,
updated: updated.unwrap_or_else(unix_epoch),
html: render_markdown(code_renderer, &markdown_post.markdown),
html: render_markdown(code_renderer, &source.markdown),
})
}
+5 -7
View File
@@ -1,27 +1,25 @@
use std::{fmt, str};
use kdl::KdlDocument;
use super::error::Error;
use super::{error::Error, header::Header};
const DELIM: &str = "\n---\n";
#[derive(Clone, Debug)]
pub struct PostSource {
pub(super) header: KdlDocument,
pub(super) header: Header,
pub(super) markdown: String,
}
impl PostSource {
#[inline]
#[must_use]
pub fn header(&self) -> &KdlDocument {
pub fn header(&self) -> &Header {
&self.header
}
#[inline]
#[must_use]
pub fn header_mut(&mut self) -> &mut KdlDocument {
pub fn header_mut(&mut self) -> &mut Header {
&mut self.header
}
@@ -53,7 +51,7 @@ impl str::FromStr for PostSource {
}
impl fmt::Display for PostSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.header, DELIM, self.markdown)
}
}