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)] #[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}