Switch to TOML for configuration
This commit is contained in:
1 parent
ad3a0ff956
commit
d7a521e581
27 files changed
+276
-613
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {}
|
||||
@@ -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>),
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
||||
+6
-21
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user