Remove chrono as direct dependency, moving to time

- Clean up lints
- Update deps
pull/52/head v0.13.0
Anthony J. Martinez 1 year ago
parent 24ae0ee23e
commit e5b8265658
Signed by: ajmartinez
GPG Key ID: A2206FDD769DBCFC
  1. 189
      Cargo.lock
  2. 8
      Cargo.toml
  3. 75
      src/common.rs
  4. 364
      src/config.rs
  5. 9
      src/main.rs
  6. 512
      src/render.rs
  7. 449
      src/routes.rs

189
Cargo.lock generated

@ -13,25 +13,24 @@ dependencies = [
[[package]]
name = "ansi_term"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.44"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "arse"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"glob",
"hyper",
@ -44,6 +43,7 @@ dependencies = [
"simplelog",
"tempfile",
"tera",
"time 0.3.5",
"tokio",
"toml",
]
@ -146,25 +146,37 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"time 0.1.43",
"winapi",
]
[[package]]
name = "chrono-tz"
version = "0.5.3"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]]
name = "clap"
version = "2.33.3"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
@ -271,9 +283,9 @@ dependencies = [
[[package]]
name = "encoding_rs"
version = "0.8.29"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
@ -292,30 +304,30 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures-channel"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-sink"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
[[package]]
name = "futures-task"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
@ -382,9 +394,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.7"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
dependencies = [
"bytes",
"fnv",
@ -422,7 +434,7 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 0.4.8",
]
[[package]]
@ -444,9 +456,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]]
name = "httpdate"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humansize"
@ -456,9 +468,9 @@ checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "hyper"
version = "0.14.14"
version = "0.14.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
dependencies = [
"bytes",
"futures-channel",
@ -469,7 +481,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa",
"itoa 0.4.8",
"pin-project-lite",
"socket2",
"tokio",
@ -527,6 +539,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -535,9 +553,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.106"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
[[package]]
name = "lock_api"
@ -631,9 +649,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "opaque-debug"
@ -724,6 +742,45 @@ dependencies = [
"sha-1",
]
[[package]]
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher",
"uncased",
]
[[package]]
name = "pin-project-lite"
version = "0.2.7"
@ -744,9 +801,9 @@ checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "proc-macro2"
version = "1.0.32"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
dependencies = [
"unicode-xid",
]
@ -882,9 +939,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.5"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "same-file"
@ -903,18 +960,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.130"
version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.130"
version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
dependencies = [
"proc-macro2",
"quote",
@ -923,11 +980,11 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.68"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [
"itoa",
"itoa 1.0.1",
"ryu",
"serde",
]
@ -955,15 +1012,21 @@ dependencies = [
[[package]]
name = "simplelog"
version = "0.10.2"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959"
checksum = "ecabc0118918611790b8615670ab79296272cbe09496b6884b02b1e929c20886"
dependencies = [
"chrono",
"log",
"termcolor",
]
[[package]]
name = "siphasher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]]
name = "slab"
version = "0.4.5"
@ -1009,9 +1072,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "syn"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
dependencies = [
"proc-macro2",
"quote",
@ -1034,9 +1097,9 @@ dependencies = [
[[package]]
name = "tera"
version = "1.12.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95b0d8a46da5fe3ea119394a6c7f1e745f9de359081641c99946e2bf55d4f2"
checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
dependencies = [
"chrono",
"chrono-tz",
@ -1091,11 +1154,20 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad"
dependencies = [
"libc",
]
[[package]]
name = "tokio"
version = "1.13.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg",
"bytes",
@ -1113,9 +1185,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.5.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
dependencies = [
"proc-macro2",
"quote",
@ -1189,6 +1261,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "uncased"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
dependencies = [
"version_check",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"

@ -1,6 +1,6 @@
[package]
name = "arse"
version = "0.12.0"
version = "0.13.0"
authors = ["Anthony Martinez"]
edition = "2021"
license = "MIT OR Apache-2.0"
@ -15,8 +15,8 @@ categories = ["command-line-utilities", "web-programming::http-server"]
[dependencies]
anyhow = "1.0"
chrono = "0.4"
clap = "2.33.3"
time = "0.3"
clap = "2"
glob = "0.3.0"
hyper = "0.14"
log = "0.4"
@ -25,7 +25,7 @@ rand = "0.8"
routerify = "2"
rss = "1"
serde = { version = "1", features = ["derive"] }
simplelog = "0.10"
simplelog = "0.11"
tera = "1"
tokio = { version = "1", features = ["full"] }
toml = "0.5"

@ -25,7 +25,7 @@ use super::{Context, Result};
/// On UNIX-like systems this creates the file with mode `0o600`.
///
/// On Windows systems this sets the file to readonly.
pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<()> {
pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<()> {
debug!("Writing protected file: {}", &dest.as_ref().display());
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
@ -33,15 +33,18 @@ pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(
options.create(true);
options.write(true);
options.mode(0o600);
trace!("Opening '{}' to write", &dest.as_ref().display());
let mut ro_file = options.open(&dest)
.with_context(|| format!("failed to open '{}' for writing", &dest.as_ref().display()))?;
ro_file.write_all(content.as_bytes())
let mut ro_file = options
.open(&dest)
.with_context(|| format!("failed to open '{}' for writing", &dest.as_ref().display()))?;
ro_file
.write_all(content.as_bytes())
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
if !content.ends_with('\n') {
ro_file.write_all(b"\n")
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
ro_file
.write_all(b"\n")
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
}
trace!("Content written to destination");
Ok(())
@ -53,14 +56,20 @@ pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(
trace!("Opening '{}' to write", &dest.as_ref().display());
let mut ro_file = std::fs::File::create(&dest)
.with_context(|| format!("failed to open '{}' for writing", &dest.as_ref().display()))?;
ro_file.write_all(content.as_bytes()).
with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
let metadata = ro_file.metadata()
.with_context(|| format!("failure retrieving metadata on '{}'", &dest.as_ref().display()))?;
ro_file
.write_all(content.as_bytes())
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
let metadata = ro_file.metadata().with_context(|| {
format!(
"failure retrieving metadata on '{}'",
&dest.as_ref().display()
)
})?;
let mut perms = metadata.permissions();
if !content.ends_with('\n') {
ro_file.write_all(b"\n")
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
ro_file
.write_all(b"\n")
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
}
trace!("Content written to destination");
trace!("Setting read-only on destination file");
@ -68,7 +77,6 @@ pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(
Ok(())
}
/// Returns a `Result<Vec<Pathbuf>>` for a given pattern.
///
/// The returned items are reverse-lexically sorted.
@ -78,23 +86,22 @@ pub fn path_matches(pat: &str) -> Result<Vec<PathBuf>> {
path.pop();
if path.exists() {
debug!("Building topic content vector from {}", &pat);
let mut path_vec: Vec<PathBuf> = Vec::new();
trace!("Globbing {}", &pat);
let entries = glob(pat)
.context("failure globbing paths")?;
for entry in entries.filter_map(Result::ok) {
trace!("Adding '{}' to topic content vector", &entry.display());
path_vec.push(entry);
}
trace!("Reversing topic content vector for LIFO site rendering");
path_vec.reverse();
Ok(path_vec)
debug!("Building topic content vector from {}", &pat);
let mut path_vec: Vec<PathBuf> = Vec::new();
trace!("Globbing {}", &pat);
let entries = glob(pat).context("failure globbing paths")?;
for entry in entries.filter_map(Result::ok) {
trace!("Adding '{}' to topic content vector", &entry.display());
path_vec.push(entry);
}
trace!("Reversing topic content vector for LIFO site rendering");
path_vec.reverse();
Ok(path_vec)
} else {
Err(anyhow!("No valid parent path for '{}'", &pat))
Err(anyhow!("No valid parent path for '{}'", &pat))
}
}
@ -110,9 +117,9 @@ mod tests {
#[test]
fn invalid_parent_path() {
let dir = tempfile::tempdir().unwrap();
let pat = format!("{}/nope/*.md", dir.path().display());
let paths = path_matches(&pat);
assert!(paths.is_err());
let dir = tempfile::tempdir().unwrap();
let pat = format!("{}/nope/*.md", dir.path().display());
let paths = path_matches(&pat);
assert!(paths.is_err());
}
}

@ -17,37 +17,47 @@ copied, modified, or distributed except according to those terms.
//! - Generating a new application configuration and directory structure (when `arse new` is called)
use std::fs::create_dir_all;
use std::{io::BufRead, usize};
use std::path::Path;
use std::{io::BufRead, usize};
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand, crate_authors, crate_description, crate_version};
use clap::{
crate_authors, crate_description, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand,
};
use log::{debug, error, info, trace};
use simplelog::{SimpleLogger, ConfigBuilder};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use simplelog::{ConfigBuilder, SimpleLogger};
use super::common;
use super::{anyhow, Context, Result};
fn args() -> App<'static, 'static> {
App::new("A Rust Site Engine")
.version(crate_version!())
.author(crate_authors!())
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.setting(AppSettings::ArgRequiredElseHelp)
.arg(Arg::with_name("verbosity")
.short("v")
.multiple(true)
.help("Sets the log level. Default: INFO. -v = DEBUG, -vv = TRACE"))
.subcommand(SubCommand::with_name("run")
.about("Run the site server")
.arg(Arg::with_name("config")
.help("Provides the path to the server configuration file.")
.required(true)
.takes_value(true)
.index(1)))
.subcommand(SubCommand::with_name("new")
.about("Generates a base directory structure and configuration file for a new site")
)
.arg(
Arg::with_name("verbosity")
.short("v")
.multiple(true)
.help("Sets the log level. Default: INFO. -v = DEBUG, -vv = TRACE"),
)
.subcommand(
SubCommand::with_name("run")
.about("Run the site server")
.arg(
Arg::with_name("config")
.help("Provides the path to the server configuration file.")
.required(true)
.takes_value(true)
.index(1),
),
)
.subcommand(
SubCommand::with_name("new").about(
"Generates a base directory structure and configuration file for a new site",
),
)
}
/// Processes command-line arguments and configures logging.
@ -58,34 +68,36 @@ pub(crate) fn load() -> Result<AppConfig> {
let matches = args().get_matches();
// Create a Config with ISO timestamps
let log_config = ConfigBuilder::new()
.set_time_format_str("%+")
.build();
let log_config = ConfigBuilder::new().set_time_format_str("%+").build();
// After this block logging is configured at the specified level
match matches.occurrences_of("verbosity") {
0 => SimpleLogger::init(log::LevelFilter::Info, log_config).context("failed to initialize logger at level - INFO")?,
1 => SimpleLogger::init(log::LevelFilter::Debug, log_config).context("failed to initialize logger at level - DEBUG")?,
_ => SimpleLogger::init(log::LevelFilter::Trace, log_config).context("failed to initialize logger at level - TRACE")?,
0 => SimpleLogger::init(log::LevelFilter::Info, log_config)
.context("failed to initialize logger at level - INFO")?,
1 => SimpleLogger::init(log::LevelFilter::Debug, log_config)
.context("failed to initialize logger at level - DEBUG")?,
_ => SimpleLogger::init(log::LevelFilter::Trace, log_config)
.context("failed to initialize logger at level - TRACE")?,
}
info!("Logging started");
debug!("Processing subcommands");
if matches.is_present("run") {
trace!("Application called with `run` subcommand - loading config from disk");
config = runner_config(matches);
trace!("Application called with `run` subcommand - loading config from disk");
config = runner_config(matches);
} else if matches.is_present("new") {
trace!("Application called with `new` subcommand - creating config from user input");
let reader = std::io::stdin();
let mut reader = reader.lock();
let current_path = std::env::current_dir().context("failed to get current working directory")?;
let _ = AppConfig::generate(current_path, &mut reader);
std::process::exit(0);
trace!("Application called with `new` subcommand - creating config from user input");
let reader = std::io::stdin();
let mut reader = reader.lock();
let current_path =
std::env::current_dir().context("failed to get current working directory")?;
let _ = AppConfig::generate(current_path, &mut reader);
std::process::exit(0);
} else {
let msg = "Unable to load configuration".to_owned();
error!("{}", &msg);
config = Err(anyhow!("{}", msg));
let msg = "Unable to load configuration".to_owned();
error!("{}", &msg);
config = Err(anyhow!("{}", msg));
}
config
@ -93,34 +105,38 @@ pub(crate) fn load() -> Result<AppConfig> {
fn runner_config(m: ArgMatches) -> Result<AppConfig> {
if let Some(run) = m.subcommand_matches("run") {
let value = run.value_of("config").unwrap();
let config = AppConfig::from_path(value)?;
Ok(config)
let value = run.value_of("config").unwrap();
let config = AppConfig::from_path(value)?;
Ok(config)
} else {
let msg = "Failed to read arguments for 'run' subcommand".to_owned();
error!("{}", &msg);
Err(anyhow!("{}", msg))
let msg = "Failed to read arguments for 'run' subcommand".to_owned();
error!("{}", &msg);
Err(anyhow!("{}", msg))
}
}
fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String> {
let mut buf = String::new();
println!("{}", prompt);
reader.read_line(&mut buf)
reader
.read_line(&mut buf)
.context("failed reading input from user")?;
let buf = String::from(buf
.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace));
let buf = String::from(
buf.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace),
);
Ok(buf)
}
fn csv_to_vec(csv: &str) -> Vec<String> {
debug!("Creating Vec<String> from csv topics: {}", &csv);
let val_vec: Vec<String> = csv.split(',')
.map(|s| s
.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace)
.to_string())
let val_vec: Vec<String> = csv
.split(',')
.map(|s| {
s.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace)
.to_string()
})
.collect();
val_vec
@ -139,16 +155,22 @@ pub(crate) struct Site {
impl Site {
/// Creates a new [`Site`] from user input.
pub(crate) fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Site> {
let name = get_input("Please enter a name for the site: ", reader)?;
let author = get_input("Please enter the site author's name: ", reader)?;
let url = get_input("Please enter the base URL for your site: ", reader)?;
let topics = get_input("Please enter comma-separated site topics: ", reader)?;
let topics = csv_to_vec(&topics);
let template = "default.tmpl".to_owned();
let site = Site { name, author, url, template, topics };
trace!("Site: {:?}", site);
Ok(site)
let name = get_input("Please enter a name for the site: ", reader)?;
let author = get_input("Please enter the site author's name: ", reader)?;
let url = get_input("Please enter the base URL for your site: ", reader)?;
let topics = get_input("Please enter comma-separated site topics: ", reader)?;
let topics = csv_to_vec(&topics);
let template = "default.tmpl".to_owned();
let site = Site {
name,
author,
url,
template,
topics,
};
trace!("Site: {:?}", site);
Ok(site)
}
}
@ -162,10 +184,10 @@ pub(crate) struct Server {
impl Server {
/// Creates a new [`Server`] instance with defaults: `0.0.0.0:9090`.
pub(crate) fn new() -> Server {
Server {
bind: "0.0.0.0".to_owned(),
port: 9090,
}
Server {
bind: "0.0.0.0".to_owned(),
port: 9090,
}
}
}
@ -181,14 +203,14 @@ impl DocPaths {
///
/// Relative paths are supported, and can be changed in the resulting `config.toml`
pub(crate) fn new<P: AsRef<Path>>(dir: P) -> DocPaths {
debug!("Creating site DocPaths");
let dir = dir.as_ref().display();
let templates = format!("{}/site/templates", dir);
let webroot = format!("{}/site/webroot", dir);
let docpaths = DocPaths { templates, webroot };
trace!("Site DocPaths: {:?}", docpaths);
docpaths
debug!("Creating site DocPaths");
let dir = dir.as_ref().display();
let templates = format!("{}/site/templates", dir);
let webroot = format!("{}/site/webroot", dir);
let docpaths = DocPaths { templates, webroot };
trace!("Site DocPaths: {:?}", docpaths);
docpaths
}
}
@ -203,61 +225,70 @@ pub(crate) struct AppConfig {
impl AppConfig {
/// Loads an existing [`AppConfig`] from disk.
pub(crate) fn from_path<T: AsRef<Path>>(config: T) -> Result<AppConfig> {
debug!("Loading site configuration from {}", &config.as_ref().display());
let config_string = std::fs::read_to_string(&config)
.with_context(|| format!("failed reading '{}' to string", &config.as_ref().display()))?;
trace!("Parsing configuration TOML");
let app_config: AppConfig = toml::from_str(&config_string)
.context("failed to parse TOML")?;
Ok(app_config)
debug!(
"Loading site configuration from {}",
&config.as_ref().display()
);
let config_string = std::fs::read_to_string(&config).with_context(|| {
format!("failed reading '{}' to string", &config.as_ref().display())
})?;
trace!("Parsing configuration TOML");
let app_config: AppConfig =
toml::from_str(&config_string).context("failed to parse TOML")?;
Ok(app_config)
}
/// Generates a new [`AppConfig`] from user input, and creates necessary [`DocPaths`] paths on disk.
pub(crate) fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig> {
info!("Generating new site configuration");
let docpaths = DocPaths::new(&dir);
let site = Site::new_from_input(reader)?;
let server = Server::new();
let config = AppConfig {
site,
server,
docpaths,
};
config.create_paths()
.context("failed while creating site paths")?;
config.write(&dir)
.context("failed to write site config to disk")?;
Ok(config)
pub(crate) fn generate<P: AsRef<Path>, R: BufRead>(
dir: P,
reader: &mut R,
) -> Result<AppConfig> {
info!("Generating new site configuration");
let docpaths = DocPaths::new(&dir);
let site = Site::new_from_input(reader)?;
let server = Server::new();
let config = AppConfig {
site,
server,
docpaths,
};
config
.create_paths()
.context("failed while creating site paths")?;
config
.write(&dir)
.context("failed to write site config to disk")?;
Ok(config)
}
fn create_paths(&self) -> Result<()> {
info!("Creating site filesystem tree");
create_dir_all(&self.docpaths.templates)?;
create_dir_all(format!("{}/static", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/ext", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;
for topic in &self.site.topics {
let topic = common::slugify(&topic);
create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
create_dir_all(format!("{}/{}/posts", &self.docpaths.webroot, &topic))?;
}
Ok(())
info!("Creating site filesystem tree");
create_dir_all(&self.docpaths.templates)?;
create_dir_all(format!("{}/static", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/ext", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;
for topic in &self.site.topics {
let topic = common::slugify(topic);
create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
create_dir_all(format!("{}/{}/posts", &self.docpaths.webroot, &topic))?;
}
Ok(())
}
/// Writes an [`AppConfig`] to disk in the current working directory as `config.toml`.
fn write<P: AsRef<Path>>(&self, dir: P) -> Result<()> {
info!("Writing site configuration to disk");
let config = toml::to_string_pretty(&self).context("failure creating TOML")?;
let conf_path = &dir.as_ref().join("config.toml");
common::str_to_ro_file(&config, &conf_path)?;
Ok(())
info!("Writing site configuration to disk");
let config = toml::to_string_pretty(&self).context("failure creating TOML")?;
let conf_path = &dir.as_ref().join("config.toml");
common::str_to_ro_file(&config, &conf_path)?;
Ok(())
}
}
@ -267,53 +298,68 @@ mod tests {
#[test]
fn build_run_config() {
let arg_vec = vec!["arse", "run", "./test_files/test-config.toml"];
let matches = args().get_matches_from(arg_vec);
let config = runner_config(matches);
assert!(config.is_ok());
let arg_vec = vec!["arse", "run", "./test_files/test-config.toml"];
let matches = args().get_matches_from(arg_vec);
let config = runner_config(matches);
assert!(config.is_ok());
}
#[test]
fn build_config_from_input() {
let dir = tempfile::tempdir().unwrap();
// Setup all target fields
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://my.example.site\nOne, Two, Three, And More\n";
let config = AppConfig::generate(&dir, &mut src);
assert!(config.is_ok());
let tmp_dir = &dir.path();
let config_path = &tmp_dir.join("config.toml");
let site = &tmp_dir.join("site");
let templates = &tmp_dir.join("site/templates");
let webroot = &tmp_dir.join("site/webroot");
let static_data = &tmp_dir.join("site/webroot/static");
let main_ext = &tmp_dir.join("site/webroot/main/ext");
let main_posts = &tmp_dir.join("site/webroot/main/posts");
let one_ext = &tmp_dir.join("site/webroot/one/ext");
let one_posts = &tmp_dir.join("site/webroot/one/posts");
let two_ext = &tmp_dir.join("site/webroot/two/ext");
let two_posts = &tmp_dir.join("site/webroot/two/posts");
let three_ext = &tmp_dir.join("site/webroot/three/ext");
let three_posts = &tmp_dir.join("site/webroot/three/posts");
let and_more_ext = &tmp_dir.join("site/webroot/and-more/ext");
let and_more_posts = &tmp_dir.join("site/webroot/and-more/posts");
let core = vec![config_path, site, templates,
webroot, static_data, main_ext, main_posts,
one_ext, one_posts, two_ext, two_posts,
three_ext, three_posts, and_more_ext, and_more_posts];
for p in core {
assert!(Path::new(p).exists())
}
let dir = tempfile::tempdir().unwrap();
// Setup all target fields
let mut src: &[u8] =
b"Site Name\nAuthor Name\nhttps://my.example.site\nOne, Two, Three, And More\n";
let config = AppConfig::generate(&dir, &mut src);
assert!(config.is_ok());
let tmp_dir = &dir.path();
let config_path = &tmp_dir.join("config.toml");
let site = &tmp_dir.join("site");
let templates = &tmp_dir.join("site/templates");
let webroot = &tmp_dir.join("site/webroot");
let static_data = &tmp_dir.join("site/webroot/static");
let main_ext = &tmp_dir.join("site/webroot/main/ext");
let main_posts = &tmp_dir.join("site/webroot/main/posts");
let one_ext = &tmp_dir.join("site/webroot/one/ext");
let one_posts = &tmp_dir.join("site/webroot/one/posts");
let two_ext = &tmp_dir.join("site/webroot/two/ext");
let two_posts = &tmp_dir.join("site/webroot/two/posts");
let three_ext = &tmp_dir.join("site/webroot/three/ext");
let three_posts = &tmp_dir.join("site/webroot/three/posts");
let and_more_ext = &tmp_dir.join("site/webroot/and-more/ext");
let and_more_posts = &tmp_dir.join("site/webroot/and-more/posts");
let core = vec![
config_path,
site,
templates,
webroot,
static_data,
main_ext,
main_posts,
one_ext,
one_posts,
two_ext,
two_posts,
three_ext,
three_posts,
and_more_ext,
and_more_posts,
];
for p in core {
assert!(Path::new(p).exists())
}
}
#[test]
fn handle_csv_topics() {
let reference_topics: Vec<String> = vec!["One".to_owned(),
"Two".to_owned(),
"Three".to_owned(),
"And More".to_owned()];
let topics = "One, Two, Three, And More".to_owned();
assert_eq!(reference_topics, csv_to_vec(&topics))
let reference_topics: Vec<String> = vec![
"One".to_owned(),
"Two".to_owned(),
"Three".to_owned(),
"And More".to_owned(),
];
let topics = "One, Two, Three, And More".to_owned();
assert_eq!(reference_topics, csv_to_vec(&topics))
}
}

@ -15,17 +15,16 @@ copied, modified, or distributed except according to those terms.
//! - `new`: Creates a new `[config]` TOML from user input, and creates
//! the site's directory structure.
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{anyhow, Context, Error, Result};
use log::{info, error};
use hyper::Server;
use log::{error, info};
use routerify::RouterService;
mod config;
mod common;
mod config;
mod render;
mod routes;
@ -37,7 +36,7 @@ async fn main() -> Result<()> {
let engine = Arc::new(render::Engine::new(app));
info!("Rendering Engine loaded");
let router = routes::router(engine.clone());
info!("Route handlers loaded");
@ -52,7 +51,7 @@ async fn main() -> Result<()> {
info!("Running server on: {}", &addr);
if let Err(err) = server.await {
error!("Server error: {}", err)
error!("Server error: {}", err)
}
Ok(())

@ -13,21 +13,21 @@ copied, modified, or distributed except according to those terms.
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use super::config::AppConfig;
use super::common;
use super::config::AppConfig;
use super::{Context, Result};
use chrono::{DateTime, Utc};
use log::{debug, trace};
use pulldown_cmark::{Parser, html};
use pulldown_cmark::{html, Parser};
use rss::{Channel, Item};
use tera::{Tera, Context as TemplateContext};
use tera::{Context as TemplateContext, Tera};
use time::OffsetDateTime;
/// Static defaults for the rendering engine.
/// Static defaults for the rendering engine.g
mod default;
/// Rendering engine for topics and posts.
///
/// [`Engine`] stores an [`Arc<AppConfig>`] and a [`Tera`] instance from which
@ -41,183 +41,212 @@ pub(crate) struct Engine {
impl Engine {
/// Creates a new [`Engine`] from a given [`AppConfig`].
pub(crate) fn new(app: Arc<AppConfig>) -> Engine {
trace!("Loading rendering engine");
let instance = Self::load_template(app.clone()).unwrap();
Engine {
app,
instance,
}
trace!("Loading rendering engine");
let instance = Self::load_template(app.clone()).unwrap();
Engine { app, instance }
}
fn load_template(app: Arc<AppConfig>) -> Result<Tera> {
trace!("Loading Tera rendering template");
let mut tera = Tera::default();
let template = app.site.template.as_str();
let template_dir = PathBuf::from(&app.docpaths.templates);
if let "default.tmpl" = template {
tera.add_raw_template("default.tmpl", default::TEMPLATE)
.context("failure adding default template")?;
} else {
let template_path = template_dir.join(template);
tera.add_template_file(template_path, Some(template))
.context("failure loading template from file")?;
}
trace!("Tera template loaded: {:?}", tera);
Ok(tera)
trace!("Loading Tera rendering template");
let mut tera = Tera::default();
let template = app.site.template.as_str();
let template_dir = PathBuf::from(&app.docpaths.templates);
if let "default.tmpl" = template {
tera.add_raw_template("default.tmpl", default::TEMPLATE)
.context("failure adding default template")?;
} else {
let template_path = template_dir.join(template);
tera.add_template_file(template_path, Some(template))
.context("failure loading template from file")?;
}
trace!("Tera template loaded: {:?}", tera);
Ok(tera)
}
/// Renders `/:topic` content as HTML
pub(crate) fn render_topic(&self, topic_slug: &str) -> Result<String> {
let site = &self.app.site;
let mut context = TemplateContext::new();
context.insert("site", site);
if topic_slug == "gallery" {
debug!("Rendering image gallery");
let gallery = self.load_gallery()?;
context.insert("gallery", &gallery);
} else {
debug!("Rendering topic: '{}'", topic_slug);
let topic_data = self.load_topic(topic_slug)?;
context.insert("posts", &topic_data);
}
let output = self.instance.render(&site.template, &context)
.with_context(|| format!("failed rendering topic: {}, with Tera instance: {:?}", topic_slug, self.instance))?;
trace!("Rendered content for topic: {}\n{}", topic_slug, output);
Ok(output)
let site = &self.app.site;
let mut context = TemplateContext::new();
context.insert("site", site);
if topic_slug == "gallery" {
debug!("Rendering image gallery");
let gallery = self.load_gallery()?;
context.insert("gallery", &gallery);
} else {
debug!("Rendering topic: '{}'", topic_slug);
let topic_data = self.load_topic(topic_slug)?;
context.insert("posts", &topic_data);
}
let output = self
.instance
.render(&site.template, &context)
.with_context(|| {
format!(
"failed rendering topic: {}, with Tera instance: {:?}",
topic_slug, self.instance
)
})?;
trace!("Rendered content for topic: {}\n{}", topic_slug, output);
Ok(output)
}
fn load_topic(&self, topic_slug: &str) -> Result<Vec<String>> {
trace!("Loading topic content for '{}'", topic_slug);
let topic_path = Path::new(&self.app.docpaths.webroot).join(topic_slug).join("posts");
let pat = format!("{}/*.md", topic_path.display());
let paths = common::path_matches(&pat)?;
Self::read_all_to_html(paths)
trace!("Loading topic content for '{}'", topic_slug);
let topic_path = Path::new(&self.app.docpaths.webroot)
.join(topic_slug)
.join("posts");
let pat = format!("{}/*.md", topic_path.display());
let paths = common::path_matches(&pat)?;
Self::read_all_to_html(paths)
}
fn read_all_to_html(paths: Vec<PathBuf>) -> Result<Vec<String>> {
debug!("Rendering Topic Markdown to HTML");
let mut contents: Vec<String> = Vec::new();
for path in paths {
trace!("Rendering {} to HTML", &path.display());
let buf = std::fs::read_to_string(&path)
.with_context(|| format!("failure reading '{}' to string", &path.display()))?;
let parser = Parser::new(&buf);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
contents.push(html_output);
}
Ok(contents)
debug!("Rendering Topic Markdown to HTML");
let mut contents: Vec<String> = Vec::new();
for path in paths {
trace!("Rendering {} to HTML", &path.display());
let buf = std::fs::read_to_string(&path)
.with_context(|| format!("failure reading '{}' to string", &path.display()))?;
let parser = Parser::new(&buf);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
contents.push(html_output);
}
Ok(contents)
}
fn load_gallery(&self) -> Result<Vec<PathBuf>> {
debug!("Loading gallery content");
let gallery_path = Path::new(&self.app.docpaths.webroot).join("gallery").join("ext");
let pat = format!("{}/*.jpg", gallery_path.display());
let mut paths = common::path_matches(&pat)?;