8000 I went a little bit ahead of myself... · Pull Request #22 · iboss-ptk/ficon · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

I went a little bit ahead of myself... #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ exitcode = "1.1.2"
toml = "0.4"
serde = "1.0"
serde_derive = "1.0"
human-panic = "1.0.1"
human-panic = "1.0.1"
lazy_static = "1.3.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can use OnceCell instead of lazy_static I guess

207 changes: 140 additions & 67 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@
extern crate serde_derive;
extern crate regex;
extern crate structopt;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;

lazy_static! {
static ref REGEX_PATTERN: Regex = Regex::new(r"/(.*)/").unwrap();
}

use exitfailure::ExitFailure;
use failure::{Context, ResultExt};
use failure::{Context, Error, ResultExt};
use glob::Pattern;
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use std::convert::{TryFrom, TryInto};
use std::{
fs,
path::{Path, PathBuf},
};
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
Expand Down Expand Up @@ -38,20 +48,38 @@ struct SubConfigByPattern {

pub struct Ficon {
option: CliOption,
config: Config,
validated_config: ValidatedConfig,
}

struct ValidatedSubConfig {
pattern: Pattern,
convention: String,
convention_regex: Option<Regex>,
}

struct ValidatedConfig {
default_convention: Regex,
patterns: Vec<ValidatedSubConfig>,
}

pub fn filename_of(path: &Path) -> &str {
path.file_stem()
.expect("file stem is missing")
.to_str()
.expect("can't cast file stem to string")
}

impl Ficon {
const DEFAULT_CONFIG_FILE: &'static str = "Ficon.toml";

pub fn new() -> Result<Ficon, ExitFailure> {
pub fn new() -> Result<Ficon, Error> {
let option: CliOption = CliOption::from_args();

let config_path = if option.path.is_dir() {
Ok(format!(
"{}/{}",
option.path.display(),
Ficon::DEFAULT_CONFIG_FILE
Self::DEFAULT_CONFIG_FILE
))
} else {
Err(Context::new(format!(
Expand All @@ -61,82 +89,127 @@ impl Ficon {
}?;

let config = fs::read_to_string(&config_path)
.with_context(|_| format!("Config file is missing: {}", &config_path))?;
.with_context(|_| format!("Config file is missing: '{}'", &config_path))?;

let config: Config = toml::from_str(&config).with_context(|_| {
format!(
"Error while parsing configuration file at '{}'",
config_path
)
})?;

Ok(Ficon {
option,
validated_config: <_ as TryInto<ValidatedConfig>>::try_into(config).with_context(
|_| format!("Validation of configuration at '{}' failed", config_path),
)?,
})
}

pub fn target_dir(&self) -> &Path {
&self.option.path
}

let config: Config = toml::from_str(&config)
.with_context(|_| "Error while parsing configuration file")?;
pub fn check(&mut self, path: &Path) -> Result<bool, Error> {
let convention_regex = self.validated_config.convention_for(path)?;
let file_name = filename_of(path)
// ignore multiple extension by default
// TODO: make this configurable
.split('.')
.next()
.unwrap_or("");

Ok(Ficon { option, config })
Ok(convention_regex.is_match(file_name))
}
}

pub fn target_dir(&self) -> &Path {
return self.option.path.as_ref();
impl TryFrom<Config> for ValidatedConfig {
type Error = Error;

fn try_from(value: Config) -> Result<ValidatedConfig, Error> {
Ok(ValidatedConfig {
default_convention: Self::new_regex_for_convention(&value.default.convention)?,
patterns: match value.for_patterns {
Some(mut pattern_configs) => pattern_configs
.drain(..)
.map(|conf| {
Pattern::new(&conf.pattern)
.with_context(|_| format!("Failed to parse pattern '{}'", conf.pattern))
.map(|pattern| ValidatedSubConfig {
convention: conf.convention,
convention_regex: None,
pattern,
})
})
.collect::<Result<_, _>>()?,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the best tricks in the book. Collect into a Result<_, _>, which is useful if your iterators produces Result<T, _>. That way, if there is an error, you will get it into the top level. You get a pristine Vec<T>.

None => Vec::default(),
},
})
}
}

pub fn check(&self, path: &Path) -> Result<bool, ExitFailure> {
let convention_str = self.config.convention_for(path);
let reg_pattern = Regex::new(r"/(.*)/").unwrap();

let convention_regex = match convention_str.as_str() {
"any" => Ficon::convention_from_regex(r".*"),
"kebab" => Ficon::convention_from_regex(r"^[a-z][a-z\-\d]*[a-z\d]$"),
"snake" => Ficon::convention_from_regex(r"^[a-z][a-z_\d]*[a-z\d]$"),
"upper_snake" => Ficon::convention_from_regex(r"^[A-Z][A-Z_\d]*$"),
"camel" => Ficon::convention_from_regex(r"^[a-z][A-Za-z\d]*$"),
"pascal" => Ficon::convention_from_regex(r"^[A-Z][A-Za-z\d]*$"),
impl ValidatedConfig {
fn new_regex_for_convention(convention: &str) -> Result<Regex, Error> {
fn new_regex(pattern: &str) -> Result<Regex, Error> {
Regex::new(pattern)
.with_context(|_| format!("Invalid convention definition: {}", pattern))
.map_err(Into::into)
}

match convention {
"any" => new_regex(r".*"),
"kebab" => new_regex(r"^[a-z][a-z\-\d]*[a-z\d]$"),
"snake" => new_regex(r"^[a-z][a-z_\d]*[a-z\d]$"),
"upper_snake" => new_regex(r"^[A-Z][A-Z_\d]*$"),
"camel" => new_regex(r"^[a-z][A-Za-z\d]*$"),
"pascal" => new_regex(r"^[A-Z][A-Za-z\d]*$"),
convention => {
if reg_pattern.is_match(convention_str.as_str()) {
let convention = reg_pattern.replace(convention, "$1").to_string();
Regex::new(convention.as_str())
if REGEX_PATTERN.is_match(convention) {
let convention = REGEX_PATTERN.replace(convention, "$1");
Regex::new(&convention)
.with_context(|_| format!("{} is not a valid regexp", convention))
.map_err(Into::into)
} else {
Err(Context::new(format!(
bail!(
"convention is not predefined or defined as regexp: {}",
convention
)))
)
}
}
};

let file_name = path
.file_stem()
.expect("file stem is missing")
.to_str()
.expect("can't cast file stem to string");

// ignore multiple extension by default
// TODO: make this configurable
let file_name = file_name.split(".").next().unwrap_or("");

let convention = convention_regex.with_context(|_| "fail to parse convention")?;

Ok(convention.is_match(file_name))
}
}

fn convention_from_regex(pattern: &str) -> Result<Regex, Context<String>> {
Regex::new(pattern).with_context(|_| format!("Invalid convention definition: {}", pattern))
fn convention_for(&mut self, path: &Path) -> Result<&Regex, Error> {
match self
.patterns
.iter_mut()
.find(|p| p.pattern.matches_path(path))
{
Some(pattern) => {
let convention = &pattern.convention;
Ok(get_or_insert_with_error(
&mut pattern.convention_regex,
|| Self::new_regex_for_convention(convention),
)?)
}
None => Ok(&self.default_convention),
}
}
}

impl Config {
fn convention_for(&self, path: &Path) -> String {
let pattern_configs = &self.for_patterns;

let empty_vec = vec![];
let pattern_configs = pattern_configs.as_ref().map_or(&empty_vec, |e| e);

let matched_formats: Vec<&SubConfigByPattern> = pattern_configs
.iter()
.filter(|conf| {
let pattern = Pattern::new(conf.pattern.as_str()).expect("invalid glob pattern");

pattern.matches_path(path)
})
.collect();

return matched_formats
.first()
.map(|e| e.convention.clone())
.unwrap_or(self.default.convention.clone());
}
/// Like Option::get_or_insert_with(), but supports errors
fn get_or_insert_with_error<T, E>(
input: &mut Option<T>,
f: impl FnOnce() -> Result<T, E>,
) -> Result<&T, E> {
Ok(match input {
Some(ref v) => v,
None => {
*input = Some(f()?);
match input {
Some(ref v) => v,
None => unreachable!(),
}
}
})
}
47 changes: 25 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,64 @@ extern crate toml;

use exitfailure::ExitFailure;
use failure::ResultExt;
use ficon::Ficon;
use ficon::{filename_of, Ficon};
use human_panic::setup_panic;
use ignore::Walk;
use std::path::Path;
use std::{io, path::Path};
use termion::{color, style};

fn main() -> Result<(), ExitFailure> {
setup_panic!();

let ficon = Ficon::new()?;
let mut ok = true;
let mut ficon = Ficon::new()?;
let mut all_files_passed = true;
let stdout = io::stdout();
let mut locked_stdout = stdout.lock();

// skip first entry since it's the root dir and we only care about content inside
for result in Walk::new(ficon.target_dir()).skip(1) {
let entry = result.with_context(|_| format!("can't retrieve directory entry"))?;

let entry = result.with_context(|_| "can't retrieve directory entry")?;
let path = entry.path();

let is_passed = ficon.check(path)?;
if !is_passed {
ok = false;
}
let file_passed = ficon.check(path)?;
print_check_result(&mut locked_stdout, path, entry.depth(), file_passed)?;

print_check_result(path, entry.depth(), is_passed);
all_files_passed = all_files_passed && file_passed;
}

if !ok {
if !all_files_passed {
std::process::exit(exitcode::DATAERR)
}

Ok(())
}

fn print_check_result(path: &Path, depth: usize, is_passed: bool) {
fn print_check_result(
mut out: impl io::Write,
path: &Path,
depth: usize,
is_passed: bool,
) -> Result<(), io::Error> {
let depth_space = " ".repeat(depth);
let file_name = path
.file_name()
.expect("filename doesn't exist")
.to_str()
.expect("filename can't be casted to string");
let file_name = filename_of(path);

if is_passed {
println!(
writeln!(
out,
"{green}{path}{reset}",
path = format!("{}✓ {}", depth_space, file_name),
green = color::Fg(color::LightGreen),
reset = style::Reset
);
)?;
} else {
println!(
writeln!(
out,
"{bold}{red}{path}{reset}",
path = format!("{}✘ {}", depth_space, file_name),
red = color::Fg(color::LightRed),
bold = style::Bold,
reset = style::Reset
);
)?;
};
Ok(())
}
0