1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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)] // requires `derive` feature
#[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)
    }
}