blog_rs/
lib.rs

1pub mod commands;
2use std::{fs, path::PathBuf};
3
4use clap::{Parser, Subcommand};
5use commands::{compile::Compile, init::Init, new_post::NewPost};
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Error, Debug)]
11pub enum BlogError {
12    #[error("IO Error")]
13    Io {
14        error: std::io::Error,
15        message: String,
16    },
17    #[error("Invalid Front Matter: {0}")]
18    InvalidFrontMatter(String),
19    #[error("Invalid File Name: {0}")]
20    InvalidFileName(String),
21    #[error("Invalid File Extension: {0}")]
22    SimpleIoError(#[from] std::io::Error),
23    #[error("Invalid State")]
24    InvalidState {
25        state: String,
26        help: String,
27        error: String,
28    },
29}
30
31impl BlogError {
32    pub fn io(error: std::io::Error, message: String) -> Self {
33        BlogError::Io { error, message }
34    }
35}
36
37type BlogResult<T> = Result<T, BlogError>;
38
39pub trait Run {
40    fn run(&self) -> BlogResult<()>;
41}
42
43#[derive(Debug, Parser)] // requires `derive` feature
44#[command(name = "blog-rs", version, author, about = "A blog CLI")]
45#[command(about = "Blogging Tool", long_about = None)]
46pub struct Cli {
47    #[command(subcommand)]
48    pub command: Commands,
49}
50
51impl Cli {
52    pub fn run(&self) -> BlogResult<()> {
53        self.command.run()
54    }
55}
56
57#[derive(Debug, Subcommand)]
58pub enum Commands {
59    #[command(about = "Compile the blog")]
60    Compile,
61    #[command(about = "Initialize a new blog")]
62    Init { path: String },
63    #[command(about = "Create a new post")]
64    Post { title: String },
65}
66
67impl Commands {
68    pub fn run(&self) -> BlogResult<()> {
69        match self {
70            Commands::Compile => Compile.run(),
71            Commands::Init { path } => Init.run(path),
72            Commands::Post { title } => NewPost.run(title),
73        }
74    }
75}
76
77#[derive(Debug, Serialize, Deserialize, Default)]
78pub struct Config {
79    #[serde(default = "default_output")]
80    pub output: PathBuf,
81    #[serde(default = "default_template")]
82    pub template: String,
83    #[serde(default)]
84    pub title: String,
85}
86
87fn default_output() -> PathBuf {
88    PathBuf::from("static")
89}
90
91fn default_template() -> String {
92    "default".to_string()
93}
94
95impl Config {
96    pub(crate) fn template_dir(&self) -> PathBuf {
97        PathBuf::from("templates").join(&self.template)
98    }
99
100    fn load_toml() -> BlogResult<Self> {
101        let config = fs::read_to_string("config.toml").map_err(|e| BlogError::InvalidState {
102            state: "config.toml".to_string(),
103            help: "Run `blog-rs init` to create a new config".to_string(),
104            error: e.to_string(),
105        })?;
106        toml::from_str(&config).map_err(|e| BlogError::InvalidState {
107            state: "config.toml".to_string(),
108            help: "Run `blog-rs init` to create a new config".to_string(),
109            error: e.to_string(),
110        })
111    }
112
113    pub fn load_posts() -> BlogResult<Vec<PathBuf>> {
114        let dir = fs::read_dir("./posts")
115            .map_err(|e| BlogError::io(e, "Unable to read posts directory".to_string()))?;
116
117        let legible_posts: Vec<_> = dir
118            .filter_map(|entry| {
119                let entry = entry.ok()?;
120                let path = entry.path();
121                if path.is_file() {
122                    Some(path)
123                } else {
124                    None
125                }
126            })
127            .collect();
128        Ok(legible_posts)
129    }
130}