pub mod commands;
use std::{fs, path::PathBuf};
use clap::{Parser, Subcommand};
use commands::{compile::Compile, init::Init, new_post::NewPost};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BlogError {
#[error("IO Error")]
Io {
error: std::io::Error,
message: String,
},
#[error("Invalid Front Matter: {0}")]
InvalidFrontMatter(String),
#[error("Invalid File Name: {0}")]
InvalidFileName(String),
#[error("Invalid File Extension: {0}")]
SimpleIoError(#[from] std::io::Error),
#[error("Invalid State")]
InvalidState {
state: String,
help: String,
error: String,
},
}
impl BlogError {
pub fn io(error: std::io::Error, message: String) -> Self {
BlogError::Io { error, message }
}
}
type BlogResult<T> = Result<T, BlogError>;
pub trait Run {
fn run(&self) -> BlogResult<()>;
}
#[derive(Debug, Parser)] #[command(name = "blog-rs", version, author, about = "A blog CLI")]
#[command(about = "Blogging Tool", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
impl Cli {
pub fn run(&self) -> BlogResult<()> {
self.command.run()
}
}
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(about = "Compile the blog")]
Compile,
#[command(about = "Initialize a new blog")]
Init { path: String },
#[command(about = "Create a new post")]
Post { title: String },
}
impl Commands {
pub fn run(&self) -> BlogResult<()> {
match self {
Commands::Compile => Compile.run(),
Commands::Init { path } => Init.run(path),
Commands::Post { title } => NewPost.run(title),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default = "default_output")]
pub output: PathBuf,
#[serde(default = "default_template")]
pub template: String,
#[serde(default)]
pub title: String,
}
fn default_output() -> PathBuf {
PathBuf::from("static")
}
fn default_template() -> String {
"default".to_string()
}
impl Config {
pub(crate) fn template_dir(&self) -> PathBuf {
PathBuf::from("templates").join(&self.template)
}
fn load_toml() -> BlogResult<Self> {
let config = fs::read_to_string("config.toml").map_err(|e| BlogError::InvalidState {
state: "config.toml".to_string(),
help: "Run `blog-rs init` to create a new config".to_string(),
error: e.to_string(),
})?;
toml::from_str(&config).map_err(|e| BlogError::InvalidState {
state: "config.toml".to_string(),
help: "Run `blog-rs init` to create a new config".to_string(),
error: e.to_string(),
})
}
pub fn load_posts() -> BlogResult<Vec<PathBuf>> {
let dir = fs::read_dir("./posts")
.map_err(|e| BlogError::io(e, "Unable to read posts directory".to_string()))?;
let legible_posts: Vec<_> = dir
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() {
Some(path)
} else {
None
}
})
.collect();
Ok(legible_posts)
}
}