wellington 0.0.1

A lightweight blogging engine using markdown and supporting sidenotes
Documentation
use std::fmt;
use std::fs;
use std::time::SystemTime;

use url::Url;
use serde_json;
use url_serde;

use toc::IndexedBlogPost;


const CORE_DATA_PATH: &str = ".meta.json";


#[derive(Serialize)]
struct RssPost {
    title: Option<String>,
    first_published: SystemTime,
    author: String,
    #[serde(with = "url_serde")]
    link: Url
}


impl RssPost {
    fn example() -> Self {
        RssPost{
            title: None,
            first_published: SystemTime::now(),
            author: "Me".to_string(),
            link: Url::parse("https://example.com").unwrap()
        }
    }
}


#[derive(Serialize)]
pub struct RssData {
    core_data: CoreData,
    posts: Vec<RssPost>
}


impl RssData {
    pub fn example() -> Self {
        RssData{
            core_data: CoreData::new("bla", "https://bla.com", "2", "3", "5").unwrap(),
            posts: vec![RssPost::example()]
        }
    }

    pub fn new(core_data: CoreData) -> Self {
        RssData{core_data, posts: vec![]}
    }

    pub fn push_posts(&mut self, posts: &[IndexedBlogPost]) {
        for (i, post) in posts.iter().rev().enumerate() {
            let mut link = self.core_data.home.clone();
            link.set_path(&post.post_url);
            self.posts.push(RssPost{
                link, author: self.core_data.author.clone(),
                first_published: post.first_published,
                title: post.title.clone()
            });
            if i == 9 {
                break;
            }
        }
    }
}


#[derive(Serialize, Deserialize)]
pub struct CoreData {
    title: String,
    #[serde(with = "url_serde")]
    home: Url,
    description: String,
    author: String,
    #[serde(with = "url_serde")]
    index_url: Url
}


#[derive(Debug, Copy, Clone)]
pub enum ErrorKind {
    CantRead,
    BadSyntax,
    WriteError
}


#[derive(Debug)]
pub struct RSSError {
    msg: String,
    kind: ErrorKind
}


impl fmt::Display for RSSError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Bad template: {}", &self.msg)
    }
}


impl RSSError {
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }
}


impl CoreData {

    fn strip_path(mut u: Url) -> Url {
        u.set_path("");
        u
    }
    
    pub fn new(title: &str, home_s: &str, 
           description: &str, author: &str,
           blog_path: &str) -> Result<Self, RSSError> {
        match Url::parse(home_s) {
            Ok(home) => if home.path().len() <= 1 {
                let mut index_url = home.clone();
                index_url.set_path(blog_path);
                Ok(CoreData{
                    title: title.to_string(),
                    description: description.to_string(),
                    author: author.to_string(),
                    home, index_url
                })
            } else {
                Err(RSSError{
                    msg: format!("Please provide a URL *without path*, for example {}", 
                                 CoreData::strip_path(home)),
                    kind: ErrorKind::BadSyntax
                })
            },
            _ => Err(RSSError{
                msg: "Provided an invalid home address".to_string(),
                kind: ErrorKind::BadSyntax
            })
        }
    }

    pub fn load() -> Result<Self, RSSError> {
        let data_json = match fs::read_to_string(CORE_DATA_PATH) {
            Ok(j) => j,
            _ => { return Err(RSSError{
                msg: "Couldn't read core data file in this directory. Are you in the right place?".to_string(),
                kind: ErrorKind::CantRead,
            });}
        };
        match serde_json::from_str(&data_json) {
            Ok(c) => Ok(c),
            _ => Err(RSSError{
                msg: "Couldn't parse core data file. Run `init` again to fix and create a new one.".to_string(),
                kind: ErrorKind::BadSyntax
            })
        }
    }

    pub fn save(&self) -> Result<(), RSSError> {
        let data_json = match serde_json::to_string(&self) {
            Ok(s) => s,
            Err(e) => { return Err(RSSError{
                msg: format!("Couldn't serialize core data: {}", e),
                kind: ErrorKind::WriteError
            })}
        };
        match fs::write(CORE_DATA_PATH, data_json) {
            Ok(_) => Ok(()),
            Err(e) => { return Err(RSSError{
                msg: format!("Couldn't write to file: {}", e),
                kind: ErrorKind::WriteError
            })}
        }

    }
}


#[cfg(test)]
mod test {
    use super::CoreData;
    use url::Url;

    #[test]
    fn can_set() {
        assert!(CoreData::new("a", "b", "c", "d", "e").is_err());
        assert!(CoreData::new("a", "https://example.com/some-path", "c", "d", "e").is_err());
        assert_eq!(CoreData::new("a", "https://example.com/", "c", "d", "e")
                   .expect("Can't create new coredata").home,
                   Url::parse("https://example.com/").unwrap());
    }
}