Engine refactor (#48)

* refactor: move src/formatters/tool.rs to src/engine.rs

Start to put the utilities in the right place. More follow-up
refactoring is required.

* clean run_cli

* engine annotations

* docs: started a list of formatters

* refactor types, function, and argument naming

Co-authored-by: zimbatm <zimbatm@zimbatm.com>
This commit is contained in:
Andika Demas Riyandi 2021-02-10 20:37:34 +07:00 committed by GitHub
parent 22ae114512
commit c8e525f78c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 194 additions and 190 deletions

8
docs/formatters.md Normal file
View File

@ -0,0 +1,8 @@
# A list of known formatters
| name | files as argument | write-in-place | update-mtime |
|-----------|-------------------|----------------|----------------------|
| gofmt | yes | yes | keeps the same mtime |
| cargo fmt | no | yes | yes |

View File

@ -1,5 +1,5 @@
use crate::command::Cli;
use crate::formatters::tool::run_prjfmt;
use super::Cli;
use crate::engine::run_prjfmt;
use crate::CLOG;
use anyhow::anyhow;
use std::env;

View File

@ -6,7 +6,7 @@ mod init;
use self::format::format_cmd;
use self::init::init_cmd;
use crate::customlog::LogLevel;
use super::customlog::LogLevel;
use std::path::PathBuf;
use structopt::StructOpt;
@ -46,13 +46,9 @@ pub struct Cli {
/// Run a command with the given logger
pub fn run_cli(cli: Cli) -> anyhow::Result<()> {
if let Some(command) = cli.cmd {
match command {
Command::Init { path } => init_cmd(path)?,
}
} else {
format_cmd(cli)?;
return Ok(());
match cli.cmd {
Some(Command::Init { path }) => init_cmd(path)?,
None => format_cmd(cli)?,
}
return Ok(());

View File

@ -1,10 +1,17 @@
//! Fancy custom log functionality.
#![allow(missing_docs)]
use crate::emoji;
use anyhow;
use console::style;
use console::Emoji;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
pub static FOLDER: Emoji = Emoji("📂", "");
pub static WARN: Emoji = Emoji("⚠️", ":-)");
pub static ERROR: Emoji = Emoji("", "");
pub static INFO: Emoji = Emoji("", "");
pub static DEBUG: Emoji = Emoji("🐛", "");
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
/// The log level for prjfmt
@ -77,12 +84,7 @@ impl CustomLogOutput {
/// Add debug message.
pub fn debug(&self, message: &str) {
if !self.quiet() && self.is_log_enabled(LogLevel::Debug) {
let debug = format!(
"{} {}: {}",
emoji::DEBUG,
style("[DEBUG]").bold().dim(),
message,
);
let debug = format!("{} {}: {}", DEBUG, style("[DEBUG]").bold().dim(), message,);
self.message(&debug);
}
}
@ -90,12 +92,7 @@ impl CustomLogOutput {
/// Add an informational message.
pub fn info(&self, message: &str) {
if !self.quiet() && self.is_log_enabled(LogLevel::Info) {
let info = format!(
"{} {}: {}",
emoji::INFO,
style("[INFO]").bold().dim(),
message,
);
let info = format!("{} {}: {}", INFO, style("[INFO]").bold().dim(), message,);
self.message(&info);
}
}
@ -103,12 +100,7 @@ impl CustomLogOutput {
/// Add a warning message.
pub fn warn(&self, message: &str) {
if !self.quiet() && self.is_log_enabled(LogLevel::Warn) {
let warn = format!(
"{} {}: {}",
emoji::WARN,
style("[WARN]").bold().dim(),
message
);
let warn = format!("{} {}: {}", WARN, style("[WARN]").bold().dim(), message);
self.message(&warn);
}
}
@ -116,12 +108,7 @@ impl CustomLogOutput {
/// Add an error message.
pub fn error(&self, message: &str) {
if self.is_log_enabled(LogLevel::Error) {
let err = format!(
"{} {}: {}",
emoji::ERROR,
style("[ERR]").bold().dim(),
message
);
let err = format!("{} {}: {}", ERROR, style("[ERR]").bold().dim(), message);
self.message(&err);
}
}

View File

@ -1,11 +0,0 @@
//! Emoji contants used by `prjfmt`.
#![allow(missing_docs)]
use console::Emoji;
pub static FOLDER: Emoji = Emoji("📂", "");
pub static WARN: Emoji = Emoji("⚠️", ":-)");
pub static ERROR: Emoji = Emoji("", "");
pub static INFO: Emoji = Emoji("", "");
pub static DEBUG: Emoji = Emoji("🐛", "");

View File

@ -1,18 +1,20 @@
use super::manifest::create_prjfmt_manifest;
use crate::formatters::check::check_prjfmt;
use crate::formatters::manifest::{read_prjfmt_manifest, RootManifest};
use crate::{emoji, CLOG};
//! The main formatting engine logic should be in this module.
use crate::formatters::{
check::check_prjfmt,
manifest::{create_manifest, read_manifest},
RootManifest,
};
use crate::{customlog, CmdContext, FileExtensions, FileMeta, Root, CLOG};
use anyhow::{anyhow, Error, Result};
use filetime::FileTime;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::fs::{metadata, read_to_string};
use std::iter::{IntoIterator, Iterator};
use std::path::PathBuf;
use xshell::cmd;
use which::which;
use xshell::cmd;
/// Make sure that formatter binary exists. This also for other formatter
pub fn check_bin(command: &str) -> Result<()> {
@ -29,12 +31,25 @@ pub fn check_bin(command: &str) -> Result<()> {
}
/// Run the prjfmt
//
// 1. Find and load prjfmt.toml
// 1b. Resolve all of the formatters paths. If missing, print an error, remove the formatters from the list and continue.
// 2. Load the manifest if it exists, otherwise start with empty manifest
// 3. Get the list of files, use the ones passed as argument if not empty, other default to all.
// Errorr if a file belongs to two formatters
// => HashMap<formatter>(files, mtimes) // map A
// 4. Compare the list of files with the manifest, keep the ones that are not in the manifest. // map B
// 5. Iterate over each formatter (in parallel)
// a. Run the formatter with the list of files
// b. Collect the new list of (files, mtimes) and return that // map C
// 6. Merge map C into map B. Write this as the new manifest.
pub fn run_prjfmt(cwd: PathBuf, cache_dir: PathBuf) -> anyhow::Result<()> {
let prjfmt_toml = cwd.as_path().join("prjfmt.toml");
// Once the prjfmt found the $XDG_CACHE_DIR/prjfmt/eval-cache/ folder,
// it will try to scan the manifest and passed it into check_prjfmt function
let manifest: RootManifest = read_prjfmt_manifest(&prjfmt_toml, &cache_dir)?;
let manifest: RootManifest = read_manifest(&prjfmt_toml, &cache_dir)?;
let old_ctx = create_command_context(&prjfmt_toml)?;
let ctxs = check_prjfmt(&prjfmt_toml, &old_ctx, &manifest)?;
@ -47,7 +62,7 @@ pub fn run_prjfmt(cwd: PathBuf, cache_dir: PathBuf) -> anyhow::Result<()> {
if !prjfmt_toml.as_path().exists() {
return Err(anyhow!(
"{}prjfmt.toml not found, please run --init command",
emoji::ERROR
customlog::ERROR
));
}
@ -76,6 +91,8 @@ pub fn run_prjfmt(cwd: PathBuf, cache_dir: PathBuf) -> anyhow::Result<()> {
let cmd_arg = &c.command;
let paths = c.metadata.iter().map(|f| &f.path);
cmd!("{cmd_arg} {arg...} {paths...}").output()
// TODO: go over all the paths, and collect the ones that have a new mtime.
// => list (file, mtime)
})
.collect();
@ -97,11 +114,11 @@ pub fn run_prjfmt(cwd: PathBuf, cache_dir: PathBuf) -> anyhow::Result<()> {
.collect();
if manifest.manifest.is_empty() || ctxs.is_empty() {
create_prjfmt_manifest(prjfmt_toml, cache_dir, old_ctx)?;
create_manifest(prjfmt_toml, cache_dir, old_ctx)?;
} else {
println!("Format successful");
println!("capturing formatted file's state...");
create_prjfmt_manifest(prjfmt_toml, cache_dir, new_ctx)?;
create_manifest(prjfmt_toml, cache_dir, new_ctx)?;
}
Ok(())
@ -195,7 +212,7 @@ pub fn create_command_context(prjfmt_toml: &PathBuf) -> Result<Vec<CmdContext>>
None => {
return Err(anyhow!(
"{}prjfmt.toml not found, please run --init command",
emoji::ERROR
customlog::ERROR
))
}
};
@ -221,107 +238,6 @@ pub fn create_command_context(prjfmt_toml: &PathBuf) -> Result<Vec<CmdContext>>
Ok(cmd_context)
}
/// prjfmt.toml structure
#[derive(Debug, Deserialize)]
pub struct Root {
/// Map of formatters into the config
pub formatters: BTreeMap<String, FmtConfig>,
}
/// Config for each formatters
#[derive(Debug, Deserialize)]
pub struct FmtConfig {
/// File extensions that want to be formatted
pub files: FileExtensions,
/// File or Folder that is included to be formatted
pub includes: Option<Vec<String>>,
/// File or Folder that is excluded to be formatted
pub excludes: Option<Vec<String>>,
/// Command formatter to run
pub command: Option<String>,
/// Argument for formatter
pub options: Option<Vec<String>>,
}
/// File extensions can be single string (e.g. "*.hs") or
/// list of string (e.g. [ "*.hs", "*.rs" ])
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum FileExtensions {
/// Single file type
SingleFile(String),
/// List of file type
MultipleFile(Vec<String>),
}
impl<'a> IntoIterator for &'a FileExtensions {
type Item = &'a String;
type IntoIter = either::Either<std::iter::Once<&'a String>, std::slice::Iter<'a, String>>;
fn into_iter(self) -> Self::IntoIter {
match self {
FileExtensions::SingleFile(glob) => either::Either::Left(std::iter::once(glob)),
FileExtensions::MultipleFile(globs) => either::Either::Right(globs.iter()),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
/// Each context of the formatter config
pub struct CmdContext {
/// formatter command to run
pub command: String,
/// formatter arguments or flags
pub options: Vec<String>,
/// formatter target path
pub metadata: BTreeSet<FileMeta>,
}
impl PartialEq for CmdContext {
fn eq(&self, other: &Self) -> bool {
self.command == other.command
&& self.options == other.options
&& self.metadata == other.metadata
}
}
impl Eq for CmdContext {}
#[derive(Debug, Deserialize, Serialize, Clone)]
/// File metadata created after the first prjfmt run
pub struct FileMeta {
/// Last modification time listed in the file's metadata
pub mtimes: i64,
/// Path to the formatted file
pub path: PathBuf,
}
impl Ord for FileMeta {
fn cmp(&self, other: &Self) -> Ordering {
if self.eq(other) {
return Ordering::Equal;
}
if self.mtimes.eq(&other.mtimes) {
return self.path.cmp(&other.path);
}
self.mtimes.cmp(&other.mtimes)
}
}
impl PartialOrd for FileMeta {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for FileMeta {
fn eq(&self, other: &Self) -> bool {
self.mtimes == other.mtimes && self.path == other.path
}
}
impl Eq for FileMeta {}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,6 +1,5 @@
use crate::formatters::manifest::RootManifest;
use crate::formatters::tool::CmdContext;
use crate::CLOG;
use super::RootManifest;
use crate::{CmdContext, CLOG};
use anyhow::{Error, Result};
use std::path::PathBuf;
use std::vec::Vec;

View File

@ -1,23 +1,21 @@
use crate::{emoji, CLOG};
use super::RootManifest;
use crate::{customlog, CmdContext, CLOG};
use super::tool::CmdContext;
use anyhow::{anyhow, Result};
use hex;
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use std::collections::BTreeMap;
use std::fs::{read_to_string, File};
use std::io::Write;
use std::path::PathBuf;
use std::str;
/// Create <hex(hash(path-to-prjfmt))>.toml and put it in $XDG_CACHE_DIR/prjfmt/eval-cache/
pub fn create_prjfmt_manifest(
pub fn create_manifest(
prjfmt_toml: PathBuf,
cache_dir: PathBuf,
cmd_ctx: Vec<CmdContext>,
) -> Result<()> {
let hash_toml = create_prjfmt_hash(&prjfmt_toml)?;
let hash_toml = create_hash(&prjfmt_toml)?;
let mut f = File::create(cache_dir.as_path().join(hash_toml))?;
let map_manifest: BTreeMap<String, CmdContext> = cmd_ctx
@ -38,8 +36,8 @@ pub fn create_prjfmt_manifest(
f.write_all(
format!(
"# {} DO NOT HAND EDIT THIS FILE {}\n\n{}",
emoji::WARN,
emoji::WARN,
customlog::WARN,
customlog::WARN,
toml::to_string_pretty(&manifest_toml)?
)
.as_bytes(),
@ -48,12 +46,12 @@ pub fn create_prjfmt_manifest(
}
/// Read the <hex(hash(path-to-prjfmt))>.toml and return list of config's cache evaluation
pub fn read_prjfmt_manifest(prjfmt_toml: &PathBuf, path: &PathBuf) -> Result<RootManifest> {
let hash_toml = create_prjfmt_hash(&prjfmt_toml)?;
let manifest_toml = path.as_path().join(&hash_toml);
pub fn read_manifest(prjfmt_toml: &PathBuf, cache_dir: &PathBuf) -> Result<RootManifest> {
let hash_toml = create_hash(&prjfmt_toml)?;
let manifest_toml = cache_dir.as_path().join(&hash_toml);
if manifest_toml.as_path().exists() {
CLOG.debug(&format!("Found {} in: {}", hash_toml, path.display()));
CLOG.debug(&format!("Found {} in: {}", hash_toml, cache_dir.display()));
let open_file = match read_to_string(manifest_toml.as_path()) {
Ok(file) => file,
Err(err) => {
@ -75,10 +73,15 @@ pub fn read_prjfmt_manifest(prjfmt_toml: &PathBuf, path: &PathBuf) -> Result<Roo
}
}
fn create_prjfmt_hash(prjfmt_toml: &PathBuf) -> Result<String> {
fn create_hash(prjfmt_toml: &PathBuf) -> Result<String> {
let prjfmt_str = match prjfmt_toml.to_str() {
Some(str) => str.as_bytes(),
None => return Err(anyhow!("{}cannot convert to string slice", emoji::ERROR)),
None => {
return Err(anyhow!(
"{}cannot convert to string slice",
customlog::ERROR
))
}
};
let prjfmt_hash = Sha1::digest(prjfmt_str);
let result = hex::encode(prjfmt_hash);
@ -86,23 +89,16 @@ fn create_prjfmt_hash(prjfmt_toml: &PathBuf) -> Result<String> {
Ok(manifest_toml)
}
#[derive(Debug, Deserialize, Serialize)]
/// RootManifest
pub struct RootManifest {
/// Map of manifests config based on its formatter
pub manifest: BTreeMap<String, CmdContext>,
}
#[cfg(test)]
mod tests {
use super::*;
/// Every same path produce same hash
#[test]
fn test_create_prjfmt_hash() -> Result<()> {
fn test_create_hash() -> Result<()> {
let file_path = PathBuf::from(r"examples/monorepo/prjfmt.toml");
let prjfmt_hash = "02e97bc0a67b5d61f3152c184690216085ef0c03.toml";
assert_eq!(create_prjfmt_hash(&file_path)?, prjfmt_hash);
assert_eq!(create_hash(&file_path)?, prjfmt_hash);
Ok(())
}
}

View File

@ -5,5 +5,14 @@
pub mod check;
/// Manifest configuration
pub mod manifest;
/// Formatter utility
pub mod tool;
use crate::CmdContext;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Deserialize, Serialize)]
/// RootManifest
pub struct RootManifest {
/// Map of manifests config based on its formatter
pub manifest: BTreeMap<String, CmdContext>,
}

View File

@ -1,13 +1,117 @@
//! Your favorite all-in-one formatter tool!
#![deny(missing_docs)]
pub mod command;
pub mod customlog;
pub mod emoji;
pub mod engine;
pub mod formatters;
use customlog::CustomLogOutput;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
/// The global custom log and user-facing message output.
pub static CLOG: CustomLogOutput = CustomLogOutput::new();
/// prjfmt.toml structure
#[derive(Debug, Deserialize)]
pub struct Root {
/// Map of formatters into the config
pub formatters: BTreeMap<String, FmtConfig>,
}
/// Config for each formatters
#[derive(Debug, Deserialize)]
pub struct FmtConfig {
/// File extensions that want to be formatted
pub files: FileExtensions,
/// File or Folder that is included to be formatted
pub includes: Option<Vec<String>>,
/// File or Folder that is excluded to be formatted
pub excludes: Option<Vec<String>>,
/// Command formatter to run
pub command: Option<String>,
/// Argument for formatter
pub options: Option<Vec<String>>,
}
/// File extensions can be single string (e.g. "*.hs") or
/// list of string (e.g. [ "*.hs", "*.rs" ])
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum FileExtensions {
/// Single file type
SingleFile(String),
/// List of file type
MultipleFile(Vec<String>),
}
impl<'a> IntoIterator for &'a FileExtensions {
type Item = &'a String;
type IntoIter = either::Either<std::iter::Once<&'a String>, std::slice::Iter<'a, String>>;
fn into_iter(self) -> Self::IntoIter {
match self {
FileExtensions::SingleFile(glob) => either::Either::Left(std::iter::once(glob)),
FileExtensions::MultipleFile(globs) => either::Either::Right(globs.iter()),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
/// Each context of the formatter config
pub struct CmdContext {
/// formatter command to run
pub command: String,
/// formatter arguments or flags
pub options: Vec<String>,
/// formatter target path
pub metadata: BTreeSet<FileMeta>,
}
impl PartialEq for CmdContext {
fn eq(&self, other: &Self) -> bool {
self.command == other.command
&& self.options == other.options
&& self.metadata == other.metadata
}
}
impl Eq for CmdContext {}
#[derive(Debug, Deserialize, Serialize, Clone)]
/// File metadata created after the first prjfmt run
pub struct FileMeta {
/// Last modification time listed in the file's metadata
pub mtimes: i64,
/// Path to the formatted file
pub path: PathBuf,
}
impl Ord for FileMeta {
fn cmp(&self, other: &Self) -> Ordering {
if self.eq(other) {
return Ordering::Equal;
}
if self.mtimes.eq(&other.mtimes) {
return self.path.cmp(&other.path);
}
self.mtimes.cmp(&other.mtimes)
}
}
impl PartialOrd for FileMeta {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for FileMeta {
fn eq(&self, other: &Self) -> bool {
self.mtimes == other.mtimes && self.path == other.path
}
}
impl Eq for FileMeta {}