From 36f721bbefb97dbc51b3964b038dcf1051e574fd Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:25:56 -0300 Subject: [PATCH 01/40] started working on next version --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8a4c77c..353bd00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] +# TODO: Consider changing package name to `fc4r`. name = "fileclass" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 4a79528e0e38654dc2df4d882fd342f404f39156 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:26:02 -0300 Subject: [PATCH 02/40] dev notes --- docs/pendings.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/pendings.md diff --git a/docs/pendings.md b/docs/pendings.md new file mode 100644 index 0000000..da1aa31 --- /dev/null +++ b/docs/pendings.md @@ -0,0 +1,2 @@ +- [ ] `system:unknown` +- [ ] `system:image` From 1427c3385162dc19fe5c72bbf02db18b5de13dfb Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:35:08 -0300 Subject: [PATCH 03/40] almost complete right side labels support - 1 test still not passing - may need more testing? --- src/core/document.rs | 72 ++++++++++++++++++++++++++++++++++++++------ src/extra/input.rs | 4 +-- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/core/document.rs b/src/core/document.rs index e03dda7..36fca7b 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -1,7 +1,19 @@ +use crate::utils::fs::get_prefix; + use super::label::LabelSet; use std::path::Path; -const FILENAME_LABELS_DELIMITER: &str = " fn "; +const DEFAULT_LABELS_DELIMITER: &str = " fn "; +const RIGHT_LABELS_DELIMITER: &str = " fr "; +const RIGHT_LABELS_TOGGLE: &str = "fr "; + +enum LabelPlacement { + Left, + /// Labels on the right, BUT still using `fn` as delimiter. + RightByFn, + /// Labels on the right using `fr` as delimiter. + RightByFr, +} #[derive(Debug, PartialEq, Eq, Clone)] pub struct Document { @@ -14,13 +26,36 @@ pub struct Document { impl Document { pub fn from_filename(path: &str) -> Self { let path = Path::new(path); - let filename = match path.file_name() { - Some(filename) => filename.to_string_lossy(), - None => "".into(), + let filename_prefix = get_prefix(path); + + let labels_placement = if filename_prefix.contains(RIGHT_LABELS_TOGGLE) { + if filename_prefix.starts_with(RIGHT_LABELS_TOGGLE) { + LabelPlacement::RightByFn + } else { + LabelPlacement::RightByFr + } + } else { + LabelPlacement::Left }; - match filename.split_once(FILENAME_LABELS_DELIMITER) { - Some((labels, name)) => { + let delimiter = match labels_placement { + LabelPlacement::Left => DEFAULT_LABELS_DELIMITER, + LabelPlacement::RightByFn => DEFAULT_LABELS_DELIMITER, + LabelPlacement::RightByFr => RIGHT_LABELS_DELIMITER, + }; + + match filename_prefix.split_once(delimiter) { + Some(parts) => { + let labels = match labels_placement { + LabelPlacement::Left => parts.0, + _ => parts.1, + }; + + let name = match labels_placement { + LabelPlacement::Left => parts.1, + _ => parts.0, + }; + // Does not require trim. let labels = labels.split_whitespace().map(|s| s.to_string()).collect(); Self { @@ -33,7 +68,7 @@ impl Document { None => Self { labels: LabelSet::empty(), path: path.to_str().unwrap().to_string(), - name: filename.to_string(), + name: filename_prefix.to_string(), }, } } @@ -47,21 +82,21 @@ mod tests { fn from_filename_works() { let doc = Document::from_filename("path/to/ l1 l2 fn name.ext "); - assert_eq!(doc.name, "name.ext"); + assert_eq!(doc.name, "name"); assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); } #[test] fn from_filename_works_without_labels() { let doc = Document::from_filename("path/to/name.ext"); - assert_eq!(doc.name, "name.ext"); + assert_eq!(doc.name, "name"); assert!(doc.labels.is_empty()); } #[test] fn from_filename_works_with_empty_labels() { let doc = Document::from_filename("path/to/ fn name.ext "); - assert_eq!(doc.name, "name.ext"); + assert_eq!(doc.name, "name"); assert!(doc.labels.is_empty()); } @@ -79,4 +114,21 @@ mod tests { assert_eq!(doc.name, ""); assert!(doc.labels.is_empty()); } + + #[test] + fn from_filename_works_with_right_fr_labels() { + let doc = Document::from_filename("path/to/ name fr l1 l2 .ext "); + + assert_eq!(doc.name, "name"); + assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); + } + + #[test] + fn from_filename_works_with_right_fn_labels() { + // Should handle empty space before `fr`? + let doc = Document::from_filename("path/to/fr name fn l1 l2 .ext "); + + assert_eq!(doc.name, "name"); + assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); + } } diff --git a/src/extra/input.rs b/src/extra/input.rs index 093e295..7a55ef8 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -65,13 +65,13 @@ mod tests { vec![ Document { path: "a b c fn file1.ext".to_string(), - name: "file1.ext".to_string(), + name: "file1".to_string(), labels: LabelSet::from(["a", "b", "c"]), }, Document { // TODO: Should this be trimmed by Document? path: " the path/to/la_la-la fn file2.ext".to_string(), - name: "file2.ext".to_string(), + name: "file2".to_string(), labels: LabelSet::from(["la_la-la"]), }, ] From 62efd70507a23b3dcf00fa8ea12eec8f53546c18 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:26:43 -0300 Subject: [PATCH 04/40] right side labels with tests passing --- src/core/document.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/document.rs b/src/core/document.rs index 36fca7b..8c3ecc9 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -23,6 +23,15 @@ pub struct Document { pub name: String, } +/// Util to remove the `fr` prefix mostly when present and trim. +fn clean_name(s: &str) -> &str { + if s.starts_with(RIGHT_LABELS_TOGGLE) { + &s[RIGHT_LABELS_TOGGLE.len()..].trim() + } else { + s.trim() + } +} + impl Document { pub fn from_filename(path: &str) -> Self { let path = Path::new(path); @@ -62,7 +71,7 @@ impl Document { labels, // Shall I use lossy here? path: path.to_str().unwrap().to_string(), - name: name.trim().to_string(), + name: clean_name(name).to_string(), } } None => Self { From 62d2804693198b788e00108e873cd051ffd1c173 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:34:48 -0300 Subject: [PATCH 05/40] added more tests to right side labels --- src/core/document.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/core/document.rs b/src/core/document.rs index 8c3ecc9..f733c3d 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -132,6 +132,31 @@ mod tests { assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); } + #[test] + fn from_filename_works_with_right_fr_labels_and_empty_name() { + // Warning: If `fr` is at the beginning, it will be considered a "right fn" case. + let doc = Document::from_filename("path/to/ fr l1 l2 .ext "); + + assert_eq!(doc.name, ""); + assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); + } + + #[test] + fn from_filename_works_with_right_fr_labels_and_empty_labels() { + let doc = Document::from_filename("path/to/ name fr .ext "); + + assert_eq!(doc.name, "name"); + assert!(doc.labels.is_empty()); + } + + #[test] + fn from_filename_works_with_right_fr_labels_and_empty_name_and_empty_labels() { + let doc = Document::from_filename("path/to/ fr .ext "); + + assert_eq!(doc.name, ""); + assert!(doc.labels.is_empty()); + } + #[test] fn from_filename_works_with_right_fn_labels() { // Should handle empty space before `fr`? @@ -140,4 +165,28 @@ mod tests { assert_eq!(doc.name, "name"); assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); } + + #[test] + fn from_filename_works_with_right_fn_labels_and_empty_name() { + let doc = Document::from_filename("path/to/fr fn l1 l2 .ext "); + + assert_eq!(doc.name, ""); + assert_eq!(doc.labels, LabelSet::from(["l1", "l2"])); + } + + #[test] + fn from_filename_works_with_right_fn_labels_and_empty_labels() { + let doc = Document::from_filename("path/to/fr name fn .ext "); + + assert_eq!(doc.name, "name"); + assert!(doc.labels.is_empty()); + } + + #[test] + fn from_filename_works_with_right_fn_labels_and_empty_name_and_empty_labels() { + let doc = Document::from_filename("path/to/fr fn .ext "); + + assert_eq!(doc.name, ""); + assert!(doc.labels.is_empty()); + } } From 94a5aaad7314130c4ceab678e8bc87282d21c6ca Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Mon, 11 Sep 2023 01:12:12 -0300 Subject: [PATCH 06/40] fcwalk now sends the config broken until fcq and fclabels understand this --- Cargo.toml | 1 + src/bin/fcwalk.rs | 10 +++++++++- src/core/config.rs | 5 +++-- src/core/label.rs | 4 +++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 353bd00..0dfa2de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ edition = "2021" [dependencies] serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.106" tabled = "0.12.1" toml = "0.7.4" diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index d5fb995..c748801 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -3,12 +3,14 @@ use std::env; use std::fs; -use fileclass::core::config::STD_CONFIG_DIR; +use fileclass::core::config::{Config, STD_CONFIG_DIR}; fn main() { // Get the current directory let current_dir = env::current_dir().unwrap(); + load_config(); + // Recursively traverse the directory tree traverse_directory(¤t_dir, ¤t_dir); } @@ -36,3 +38,9 @@ fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { } } } + +fn load_config() { + let config = Config::std_load().expect("Can't load config"); + // Can possibly use `serde_json::to_writer(std::io::stdout(), &config)`. + println!("{}", serde_json::to_string(&config).unwrap()); +} diff --git a/src/core/config.rs b/src/core/config.rs index 7b05280..06892c8 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fs; use std::path::Path; @@ -9,12 +9,13 @@ pub const STD_CONFIG_DIR: &str = "fileclass"; pub const LABELS_FILENAME: &str = "labels.toml"; pub const SETTINGS_FILENAME: &str = "settings.toml"; +#[derive(Deserialize, Serialize)] pub struct Config { pub labels: LabelLibrary, pub settings: Settings, } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct Settings { // TODO: Use a default if missing and use that default in fcinit. pub link_dir: String, diff --git a/src/core/label.rs b/src/core/label.rs index 55be68f..c6d1cc5 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::collections::HashSet; use std::error::Error; @@ -76,6 +76,7 @@ impl From<[&str; N]> for LabelSet { } } +#[derive(Deserialize, Serialize)] struct LabelDef { name: String, aliases: Vec, @@ -93,6 +94,7 @@ struct RawLabelDef { description: String, } +#[derive(Deserialize, Serialize)] pub struct LabelLibrary { label_defs: Vec, } From ac26313671117dc8a0963d082f8e76e4e9f71af6 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Mon, 11 Sep 2023 02:17:45 -0300 Subject: [PATCH 07/40] basic ipc helper --- src/bin/fcwalk.rs | 6 ++-- src/core/config.rs | 4 +-- src/core/label.rs | 4 +-- src/extra.rs | 1 + src/extra/ipc.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/extra/ipc.rs diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index c748801..59b58e8 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -4,6 +4,7 @@ use std::env; use std::fs; use fileclass::core::config::{Config, STD_CONFIG_DIR}; +use fileclass::extra::ipc::Message; fn main() { // Get the current directory @@ -41,6 +42,7 @@ fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { fn load_config() { let config = Config::std_load().expect("Can't load config"); - // Can possibly use `serde_json::to_writer(std::io::stdout(), &config)`. - println!("{}", serde_json::to_string(&config).unwrap()); + let msg = Message::Config(config); + + println!("{}", msg.serialize()); } diff --git a/src/core/config.rs b/src/core/config.rs index 06892c8..900996f 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -9,13 +9,13 @@ pub const STD_CONFIG_DIR: &str = "fileclass"; pub const LABELS_FILENAME: &str = "labels.toml"; pub const SETTINGS_FILENAME: &str = "settings.toml"; -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Config { pub labels: LabelLibrary, pub settings: Settings, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Settings { // TODO: Use a default if missing and use that default in fcinit. pub link_dir: String, diff --git a/src/core/label.rs b/src/core/label.rs index c6d1cc5..cb29a68 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -76,7 +76,7 @@ impl From<[&str; N]> for LabelSet { } } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] struct LabelDef { name: String, aliases: Vec, @@ -94,7 +94,7 @@ struct RawLabelDef { description: String, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct LabelLibrary { label_defs: Vec, } diff --git a/src/extra.rs b/src/extra.rs index 7839bc5..ed1335f 100644 --- a/src/extra.rs +++ b/src/extra.rs @@ -1 +1,2 @@ pub mod input; +pub mod ipc; diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs new file mode 100644 index 0000000..1d53d53 --- /dev/null +++ b/src/extra/ipc.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use serde_json::value::Value; + +use crate::core::config::Config; + +#[derive(Debug, Serialize, Deserialize)] +struct RawMessage { + #[serde(rename = "type")] + kind: String, + payload: Value, +} + +impl RawMessage { + fn deserialize(input: &str) -> Self { + if input.starts_with("{") { + serde_json::from_str(input).unwrap() + } else { + RawMessage { + kind: "path".to_string(), + payload: serde_json::to_value(input).unwrap(), + } + } + } + + fn serialize(&self) -> String { + if self.kind == "path" { + serde_json::from_value(self.payload.clone()).unwrap() + } else { + serde_json::to_string(&self).unwrap() + } + } +} + +#[derive(Debug)] +pub enum Message { + Config(Config), + // TODO: Consider renaming this to "Line", "String", "TextLine", etc. + Path(String), +} + +impl Message { + pub fn deserialize(input: &str) -> Self { + let raw_message = RawMessage::deserialize(input); + let payload = raw_message.payload; + + match raw_message.kind.as_str() { + "config" => Message::Config(serde_json::from_value(payload).unwrap()), + "path" => Message::Path(serde_json::from_value(payload).unwrap()), + kind => panic!("Unknown message type '{}'", kind), + } + } + + pub fn serialize(&self) -> String { + let raw_message = match self { + Message::Config(config) => RawMessage { + kind: "config".to_string(), + payload: serde_json::to_value(config).unwrap(), + }, + Message::Path(path) => RawMessage { + kind: "path".to_string(), + payload: serde_json::to_value(path).unwrap(), + }, + }; + + raw_message.serialize() + } + + // TODO: Check if these are safe to use. + pub fn send(writer: &mut impl std::io::Write, message: &Self) { + let serialized = message.serialize(); + writeln!(writer, "{}", serialized).unwrap(); + } + + // TODO: Check if these are safe to use. + pub fn recv(reader: &mut impl std::io::BufRead) -> Self { + let mut input = String::new(); + reader.read_line(&mut input).unwrap(); + Self::deserialize(&input) + } +} From c35e0799095355cdb83b8928afb209e39599431d Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Mon, 11 Sep 2023 02:31:05 -0300 Subject: [PATCH 08/40] read_messages input helper --- src/core/document.rs | 4 +++- src/core/label.rs | 2 +- src/extra/input.rs | 14 ++++++++++---- src/extra/ipc.rs | 8 +++++++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/core/document.rs b/src/core/document.rs index f733c3d..498856c 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use crate::utils::fs::get_prefix; use super::label::LabelSet; @@ -15,7 +17,7 @@ enum LabelPlacement { RightByFr, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Document { // Provisory name matching the `from_filename` function. pub path: String, diff --git a/src/core/label.rs b/src/core/label.rs index cb29a68..4219088 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -5,7 +5,7 @@ use std::error::Error; use std::iter::FromIterator; use std::iter::IntoIterator; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct LabelSet(HashSet); impl LabelSet { diff --git a/src/extra/input.rs b/src/extra/input.rs index 7a55ef8..30c40ef 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -4,15 +4,21 @@ use std::path::PathBuf; use crate::core::document::Document; use crate::utils::fs::get_unique_target; -pub fn read_documents(reader: impl io::BufRead) -> impl Iterator { +use super::ipc::Message; + +pub fn read_messages(reader: impl io::BufRead) -> impl Iterator { reader .lines() .map(|l| l.expect("Can't read line from input")) - .map(|l| Document::from_filename(&l)) + .map(|l| Message::deserialize(&l)) + .map(|m| match m { + Message::Path(p) => Message::Document(Document::from_filename(&p)), + _ => m, + }) } -pub fn read_stdin_documents() -> impl Iterator { - read_documents(io::stdin().lock()) +pub fn read_stdin_messages() -> impl Iterator { + read_messages(io::stdin().lock()) } #[derive(Debug, PartialEq, Eq, Clone)] diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index 1d53d53..f64e852 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::value::Value; -use crate::core::config::Config; +use crate::core::{config::Config, document::Document}; #[derive(Debug, Serialize, Deserialize)] struct RawMessage { @@ -36,6 +36,7 @@ pub enum Message { Config(Config), // TODO: Consider renaming this to "Line", "String", "TextLine", etc. Path(String), + Document(Document), } impl Message { @@ -45,6 +46,7 @@ impl Message { match raw_message.kind.as_str() { "config" => Message::Config(serde_json::from_value(payload).unwrap()), + "document" => Message::Document(serde_json::from_value(payload).unwrap()), "path" => Message::Path(serde_json::from_value(payload).unwrap()), kind => panic!("Unknown message type '{}'", kind), } @@ -56,6 +58,10 @@ impl Message { kind: "config".to_string(), payload: serde_json::to_value(config).unwrap(), }, + Message::Document(document) => RawMessage { + kind: "document".to_string(), + payload: serde_json::to_value(document).unwrap(), + }, Message::Path(path) => RawMessage { kind: "path".to_string(), payload: serde_json::to_value(path).unwrap(), From e9af81096932d9da9d01edbb2ab0cbe1a65baa3b Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Mon, 11 Sep 2023 02:34:10 -0300 Subject: [PATCH 09/40] renamed path message to line --- src/extra/input.rs | 2 +- src/extra/ipc.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/extra/input.rs b/src/extra/input.rs index 30c40ef..8eeed92 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -12,7 +12,7 @@ pub fn read_messages(reader: impl io::BufRead) -> impl Iterator .map(|l| l.expect("Can't read line from input")) .map(|l| Message::deserialize(&l)) .map(|m| match m { - Message::Path(p) => Message::Document(Document::from_filename(&p)), + Message::Line(l) => Message::Document(Document::from_filename(&l)), _ => m, }) } diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index f64e852..b0f041e 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -16,14 +16,14 @@ impl RawMessage { serde_json::from_str(input).unwrap() } else { RawMessage { - kind: "path".to_string(), + kind: "line".to_string(), payload: serde_json::to_value(input).unwrap(), } } } fn serialize(&self) -> String { - if self.kind == "path" { + if self.kind == "line" { serde_json::from_value(self.payload.clone()).unwrap() } else { serde_json::to_string(&self).unwrap() @@ -35,7 +35,7 @@ impl RawMessage { pub enum Message { Config(Config), // TODO: Consider renaming this to "Line", "String", "TextLine", etc. - Path(String), + Line(String), Document(Document), } @@ -47,7 +47,7 @@ impl Message { match raw_message.kind.as_str() { "config" => Message::Config(serde_json::from_value(payload).unwrap()), "document" => Message::Document(serde_json::from_value(payload).unwrap()), - "path" => Message::Path(serde_json::from_value(payload).unwrap()), + "line" => Message::Line(serde_json::from_value(payload).unwrap()), kind => panic!("Unknown message type '{}'", kind), } } @@ -62,9 +62,9 @@ impl Message { kind: "document".to_string(), payload: serde_json::to_value(document).unwrap(), }, - Message::Path(path) => RawMessage { - kind: "path".to_string(), - payload: serde_json::to_value(path).unwrap(), + Message::Line(line) => RawMessage { + kind: "line".to_string(), + payload: serde_json::to_value(line).unwrap(), }, }; From ec151b748df82e3a270afea5e2ffdf0bf5286dad Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Mon, 11 Sep 2023 02:36:00 -0300 Subject: [PATCH 10/40] panic at pending important tests --- src/extra/ipc.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index b0f041e..fd85cf7 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -84,3 +84,13 @@ impl Message { Self::deserialize(&input) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn todo() { + panic!("TODO"); + } +} From db1ced96dcf922195aa5e9bd324770d90a3b35ff Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Tue, 12 Sep 2023 23:24:09 -0300 Subject: [PATCH 11/40] replaced read_documents test with read_messages --- src/core/config.rs | 4 ++-- src/core/label.rs | 4 ++-- src/extra/input.rs | 16 ++++++++-------- src/extra/ipc.rs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/core/config.rs b/src/core/config.rs index 900996f..0de514d 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -9,13 +9,13 @@ pub const STD_CONFIG_DIR: &str = "fileclass"; pub const LABELS_FILENAME: &str = "labels.toml"; pub const SETTINGS_FILENAME: &str = "settings.toml"; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Config { pub labels: LabelLibrary, pub settings: Settings, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Settings { // TODO: Use a default if missing and use that default in fcinit. pub link_dir: String, diff --git a/src/core/label.rs b/src/core/label.rs index 4219088..dc65183 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -76,7 +76,7 @@ impl From<[&str; N]> for LabelSet { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] struct LabelDef { name: String, aliases: Vec, @@ -94,7 +94,7 @@ struct RawLabelDef { description: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct LabelLibrary { label_defs: Vec, } diff --git a/src/extra/input.rs b/src/extra/input.rs index 8eeed92..2272d90 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -62,26 +62,26 @@ mod tests { use std::path::Path; #[test] - fn read_documents_works() { + fn read_messages_works() { let input = "a b c fn file1.ext the path/to/la_la-la fn file2.ext"; - let documents: Vec<_> = read_documents(input.as_bytes()).collect(); + let messages: Vec<_> = read_messages(input.as_bytes()).collect(); assert_eq!( - documents, + messages, vec![ - Document { + Message::Document(Document { path: "a b c fn file1.ext".to_string(), name: "file1".to_string(), labels: LabelSet::from(["a", "b", "c"]), - }, - Document { + }), + Message::Document(Document { // TODO: Should this be trimmed by Document? path: " the path/to/la_la-la fn file2.ext".to_string(), name: "file2".to_string(), labels: LabelSet::from(["la_la-la"]), - }, + }), ] - ) + ); } #[test] diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index fd85cf7..896fd11 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -31,7 +31,7 @@ impl RawMessage { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Message { Config(Config), // TODO: Consider renaming this to "Line", "String", "TextLine", etc. From abc453b9a9321b6f7f5252fb69f4e38a49a56cb0 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Tue, 12 Sep 2023 23:37:29 -0300 Subject: [PATCH 12/40] ignore blank lines on read_messages --- src/extra/input.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/extra/input.rs b/src/extra/input.rs index 2272d90..72aac15 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -1,7 +1,7 @@ use std::io; use std::path::PathBuf; -use crate::core::document::Document; +use crate::core::{config::Config, document::Document}; use crate::utils::fs::get_unique_target; use super::ipc::Message; @@ -10,6 +10,7 @@ pub fn read_messages(reader: impl io::BufRead) -> impl Iterator reader .lines() .map(|l| l.expect("Can't read line from input")) + .filter(|l| !l.trim().is_empty()) .map(|l| Message::deserialize(&l)) .map(|m| match m { Message::Line(l) => Message::Document(Document::from_filename(&l)), @@ -63,19 +64,21 @@ mod tests { #[test] fn read_messages_works() { - let input = "a b c fn file1.ext - the path/to/la_la-la fn file2.ext"; + let input = r#" + a b c fn file1.ext + the path/to/la_la-la fn file2.ext"#; let messages: Vec<_> = read_messages(input.as_bytes()).collect(); assert_eq!( messages, vec![ Message::Document(Document { - path: "a b c fn file1.ext".to_string(), + // TODO: Should this be trimmed by Document? + path: " a b c fn file1.ext".to_string(), name: "file1".to_string(), labels: LabelSet::from(["a", "b", "c"]), }), Message::Document(Document { - // TODO: Should this be trimmed by Document? + // Should this be trimmed by Document? path: " the path/to/la_la-la fn file2.ext".to_string(), name: "file2".to_string(), labels: LabelSet::from(["la_la-la"]), From 6ed7fbeb6f4bf68bc1479d052a4003b0f7a435c4 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Tue, 12 Sep 2023 23:47:52 -0300 Subject: [PATCH 13/40] added line and document tests to read_messages --- src/extra/input.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/extra/input.rs b/src/extra/input.rs index 72aac15..e85020a 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -64,13 +64,26 @@ mod tests { #[test] fn read_messages_works() { - let input = r#" + // TODO: If the string doesn't start with `{`, it will be interpreted as a line. + // Shall that change or be trimmed? + let input = r#"{"type": "line", "payload": "bla"} +{"type": "document", "payload": {"path": "thepath", "name": "thename", "labels": ["thelabel"]}} a b c fn file1.ext the path/to/la_la-la fn file2.ext"#; let messages: Vec<_> = read_messages(input.as_bytes()).collect(); assert_eq!( messages, vec![ + Message::Document(Document { + path: "bla".to_string(), + name: "bla".to_string(), + labels: LabelSet::empty(), + }), + Message::Document(Document { + path: "thepath".to_string(), + name: "thename".to_string(), + labels: LabelSet::from(["thelabel"]), + }), Message::Document(Document { // TODO: Should this be trimmed by Document? path: " a b c fn file1.ext".to_string(), From bd8dd4ac4b6884ced9ba035ec0fec4b58e717d6a Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:14:36 -0300 Subject: [PATCH 14/40] full read_messages test --- src/extra/input.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/extra/input.rs b/src/extra/input.rs index e85020a..5a2c61d 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -1,7 +1,7 @@ use std::io; use std::path::PathBuf; -use crate::core::{config::Config, document::Document}; +use crate::core::document::Document; use crate::utils::fs::get_unique_target; use super::ipc::Message; @@ -59,7 +59,8 @@ pub fn map_stdin_sources_to_target_folder( #[cfg(test)] mod tests { use super::*; - use crate::core::label::LabelSet; + use crate::core::config::{Config, Settings}; + use crate::core::label::{LabelLibrary, LabelSet}; use std::path::Path; #[test] @@ -68,7 +69,8 @@ mod tests { // Shall that change or be trimmed? let input = r#"{"type": "line", "payload": "bla"} {"type": "document", "payload": {"path": "thepath", "name": "thename", "labels": ["thelabel"]}} - a b c fn file1.ext +{"type":"config","payload":{"labels":{"label_defs":[{"name":"lx","description":"dx","aliases":["ax"],"implies": []}]},"settings":{"link_dir":"ld"}}} + a b c fn file1.ext the path/to/la_la-la fn file2.ext"#; let messages: Vec<_> = read_messages(input.as_bytes()).collect(); assert_eq!( @@ -84,9 +86,21 @@ mod tests { name: "thename".to_string(), labels: LabelSet::from(["thelabel"]), }), + Message::Config(Config { + labels: LabelLibrary::from_toml( + r#"[lx] + description = "dx" + aliases = ["ax"] + "#, + ) + .unwrap(), + settings: Settings { + link_dir: "ld".to_string(), + }, + }), Message::Document(Document { // TODO: Should this be trimmed by Document? - path: " a b c fn file1.ext".to_string(), + path: " a b c fn file1.ext".to_string(), name: "file1".to_string(), labels: LabelSet::from(["a", "b", "c"]), }), From 080518a930dddb84ff69f4c61b8693a88ccc22b6 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:15:34 -0300 Subject: [PATCH 15/40] remove method that will not be used --- src/extra/ipc.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index 896fd11..1d76fc1 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -70,19 +70,6 @@ impl Message { raw_message.serialize() } - - // TODO: Check if these are safe to use. - pub fn send(writer: &mut impl std::io::Write, message: &Self) { - let serialized = message.serialize(); - writeln!(writer, "{}", serialized).unwrap(); - } - - // TODO: Check if these are safe to use. - pub fn recv(reader: &mut impl std::io::BufRead) -> Self { - let mut input = String::new(); - reader.read_line(&mut input).unwrap(); - Self::deserialize(&input) - } } #[cfg(test)] From 4182eba867d26737eb0f3a102b5ada42e239e06a Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:31:14 -0300 Subject: [PATCH 16/40] lazy but enough ipc tests --- src/extra/ipc.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index 1d76fc1..4c9b211 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -74,10 +74,69 @@ impl Message { #[cfg(test)] mod tests { + use crate::core::{ + config::Settings, + label::{LabelLibrary, LabelSet}, + }; + use super::*; #[test] - fn todo() { - panic!("TODO"); + fn serialize_line() { + let msg = Message::Line("test".to_string()); + let serialized = msg.serialize(); + + assert_eq!(serialized, "test"); + } + + #[test] + fn deserialize_implicit_line() { + let msg = Message::deserialize("test"); + let expected = Message::Line("test".to_string()); + + assert_eq!(msg, expected); + } + + #[test] + fn deserialize_explicit_line() { + let msg = Message::deserialize(r#"{"type": "line", "payload": "test"}"#); + let expected = Message::Line("test".to_string()); + + assert_eq!(msg, expected); + } + + #[test] + fn serialize_and_deserialize_document() { + let msg = Message::Document(Document { + path: "path".to_string(), + name: "name".to_string(), + labels: LabelSet::from(["label"]), + }); + let serialized = msg.serialize(); + let deserialized = Message::deserialize(&serialized); + + assert_eq!(msg, deserialized); + } + + #[test] + fn serialize_and_deserialize_config() { + let msg = Message::Config(Config { + labels: LabelLibrary::from_toml( + r#"[label] + aliases = ["alias"] + implies = ["implied"] + description = "a label" + + [implied]"#, + ) + .unwrap(), + settings: Settings { + link_dir: "link_dir".to_string(), + }, + }); + let serialized = msg.serialize(); + let deserialized = Message::deserialize(&serialized); + + assert_eq!(msg, deserialized); } } From 4f3bc0517803ae152d2bacd9e7a602f2afeff303 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:39:57 -0300 Subject: [PATCH 17/40] improved ipc testing --- src/extra/ipc.rs | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index 4c9b211..7b99015 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -107,36 +107,46 @@ mod tests { #[test] fn serialize_and_deserialize_document() { - let msg = Message::Document(Document { + let document = Document { path: "path".to_string(), name: "name".to_string(), - labels: LabelSet::from(["label"]), - }); + labels: LabelSet::from(["l1", "l2"]), + }; + let msg = Message::Document(document.clone()); let serialized = msg.serialize(); let deserialized = Message::deserialize(&serialized); assert_eq!(msg, deserialized); + assert_eq!(document.name, "name"); + assert_eq!(document.path, "path"); + assert_eq!(document.labels, LabelSet::from(["l1", "l2"])); } #[test] fn serialize_and_deserialize_config() { + let library = LabelLibrary::from_toml( + r#"[label] + aliases = ["alias"] + implies = ["implied"] + description = "a label" + + [implied]"#, + ) + .unwrap(); + let settings = Settings { + link_dir: "link_dir".to_string(), + }; + let msg = Message::Config(Config { - labels: LabelLibrary::from_toml( - r#"[label] - aliases = ["alias"] - implies = ["implied"] - description = "a label" - - [implied]"#, - ) - .unwrap(), - settings: Settings { - link_dir: "link_dir".to_string(), - }, + labels: library.clone(), + settings: settings.clone(), }); let serialized = msg.serialize(); let deserialized = Message::deserialize(&serialized); assert_eq!(msg, deserialized); + assert_eq!(library.resolve("alias"), "label"); + assert_eq!(library.resolve("implied"), "implied"); + assert_eq!(settings.link_dir, "link_dir"); } } From 97e8066fbce4ca0b7b973b832cbac224ee657a3f Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:57:04 -0300 Subject: [PATCH 18/40] re-added read_stdin_documents temporarily this is to keep fcq and fclabels working until propertly fixed --- src/extra/input.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/extra/input.rs b/src/extra/input.rs index 5a2c61d..ff59f46 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -22,6 +22,14 @@ pub fn read_stdin_messages() -> impl Iterator { read_messages(io::stdin().lock()) } +// TODO: Remove this compat. +pub fn read_stdin_documents() -> impl Iterator { + read_stdin_messages().filter_map(|m| match m { + Message::Document(d) => Some(d), + _ => None, + }) +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SourceTargetPair { pub source: PathBuf, From 252d0adbcf4ab0100b9b612b237b3a7da5b506ee Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 02:53:35 -0300 Subject: [PATCH 19/40] fcq now uses labels provided by fcwalk --- src/bin/fcq.rs | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/bin/fcq.rs b/src/bin/fcq.rs index 364a0c0..9f1a7ef 100644 --- a/src/bin/fcq.rs +++ b/src/bin/fcq.rs @@ -1,15 +1,16 @@ -use fileclass::core::{ - config::Config, - query::{check, CheckParams}, +use fileclass::{ + core::{ + label::LabelLibrary, + query::{check, CheckParams}, + }, + extra::{input::read_stdin_messages, ipc::Message}, }; -use fileclass::extra::input::read_stdin_documents; - use std::env; // TODO: Handle errors here. fn main() { - let config = Config::std_load().expect("Can't load config"); + let mut library = LabelLibrary::empty(); let args: Vec = env::args().collect(); if args.len() < 2 { @@ -18,15 +19,23 @@ fn main() { let prompt = &args[1..].join(" "); - let result = read_stdin_documents().filter(|d| { - let params = CheckParams { - prompt: &prompt, - document: &d, - library: &config.labels, - }; - - check(¶ms) - }); - - result.for_each(|d| println!("{}", d.path)); + for msg in read_stdin_messages() { + match msg { + Message::Config(c) => { + library = c.labels; + } + Message::Document(d) => { + let params = CheckParams { + prompt: &prompt, + document: &d, + library: &library, + }; + + if check(¶ms) { + println!("{}", d.path); + } + } + _ => panic!("Unexpected message"), + } + } } From 823d391a134a73e70abc9574e0b3bf936359378b Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 02:54:11 -0300 Subject: [PATCH 20/40] fcwalk will not crash when no config folder --- src/bin/fcwalk.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index 59b58e8..2d98b30 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -41,8 +41,15 @@ fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { } fn load_config() { - let config = Config::std_load().expect("Can't load config"); - let msg = Message::Config(config); - - println!("{}", msg.serialize()); + match Config::std_load() { + Ok(config) => { + let msg = Message::Config(config); + println!("{}", msg.serialize()); + } + Err(e) => { + // TODO: Only warn if missing. + // TODO: Fail if the config is invalid. + eprintln!("Error loading config: {}", e); + } + } } From 4820aee8977c691a05222515eb7b708db250e5f4 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 03:10:11 -0300 Subject: [PATCH 21/40] fclabels now uses labels provided by fcwalk --- src/bin/fclabels.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/bin/fclabels.rs b/src/bin/fclabels.rs index 3053243..ed7b455 100644 --- a/src/bin/fclabels.rs +++ b/src/bin/fclabels.rs @@ -3,8 +3,8 @@ use tabled::{ Table, Tabled, }; -use fileclass::core::config::Config; -use fileclass::extra::input::read_stdin_documents; +use fileclass::extra::ipc::Message; +use fileclass::{core::label::LabelLibrary, extra::input::read_stdin_messages}; #[derive(Tabled)] struct Row { @@ -14,10 +14,30 @@ struct Row { } fn main() { - let config = Config::std_load().unwrap(); - let library = config.labels; - + let mut library = LabelLibrary::empty(); let mut rows: Vec = Vec::new(); + + let mut unknown_labels: Vec = Vec::new(); + + for msg in read_stdin_messages() { + match msg { + Message::Config(c) => { + library = c.labels; + } + Message::Document(d) => { + for label in d.labels { + if !library.is_known(&label) { + unknown_labels.push(label); + } + } + } + _ => panic!("Unexpected message"), + } + } + + unknown_labels.sort(); + unknown_labels.dedup(); // Works thanks to the sort. + let mut names = library.label_names(); names.sort(); @@ -34,14 +54,6 @@ fn main() { }); } - let mut unknown_labels: Vec = read_stdin_documents() - .flat_map(|document| document.labels) - .filter(|label| !library.is_known(label)) - .collect(); - - unknown_labels.sort(); - unknown_labels.dedup(); // Works thanks to the sort. - for label in unknown_labels.iter() { rows.push(Row { name: label.to_string(), From f484ac17396a8da0bd3f61bd95a084a85660c6ab Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 03:22:36 -0300 Subject: [PATCH 22/40] allow explicit dir for fclink --- src/bin/fclink.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/bin/fclink.rs b/src/bin/fclink.rs index 8ab61a8..53c16e8 100644 --- a/src/bin/fclink.rs +++ b/src/bin/fclink.rs @@ -3,16 +3,31 @@ // TODO: Improve. // TODO: Support dirs. -use std::fs; use std::path::Path; use std::process; +use std::{env, fs}; use fileclass::core::config::Config; use fileclass::extra::input::{map_stdin_sources_to_target_folder, SourceTargetPair}; +fn get_link_dir(args: &Vec) -> String { + match args.len() { + 1 => { + // TODO: Handle. + let config = Config::std_load().unwrap(); + config.settings.link_dir + } + 2 => args[1].clone(), + _ => { + eprintln!("Usage: fclink "); + process::exit(1); + } + } +} + fn main() { - let config = Config::std_load().unwrap(); - let link_dir = config.settings.link_dir; + let args: Vec = env::args().collect(); + let link_dir = get_link_dir(&args); // Get the target folder path let target_folder = Path::new(&link_dir); From 0e1e240884253d753b83604d420410af1a110d48 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:18:10 -0300 Subject: [PATCH 23/40] support check by having known labels --- src/core/query.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/core/query.rs b/src/core/query.rs index 8ef352e..6a426f8 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -25,7 +25,7 @@ pub fn check(params: &CheckParams) -> bool { let mut extended_labels = document.labels.clone(); extended_labels.expand_with(library); - if !check_table(document, &extended_labels, label) { + if !check_table(document, &extended_labels, label, &library) { matches = false; break; } @@ -34,19 +34,34 @@ pub fn check(params: &CheckParams) -> bool { matches } -fn check_table(document: &Document, labels: &LabelSet, current_label: &str) -> bool { +fn check_table( + document: &Document, + labels: &LabelSet, + current_label: &str, + library: &LabelLibrary, +) -> bool { match current_label.split_once(PSEUDO_DELIMITER) { - Some((prefix, suffix)) => check_pseudo(document, labels, prefix, suffix), + Some((prefix, suffix)) => check_pseudo(document, labels, prefix, suffix, library), None => check_presence(labels, current_label), } } -fn check_pseudo(document: &Document, labels: &LabelSet, prefix: &str, suffix: &str) -> bool { +fn check_pseudo( + document: &Document, + labels: &LabelSet, + prefix: &str, + suffix: &str, + library: &LabelLibrary, +) -> bool { // TODO: Refator each type of pseudo matcher into it's own matcher module. match (prefix, suffix) { + // TODO: If this is the negation of `labeled` consider removing it. ("system", "unlabeled") => labels.is_empty(), ("system", "labeled") => !labels.is_empty(), - ("not", _) => !check_table(document, labels, suffix), + // TODO: If this is the negation of `known` consider removing it. + ("system", "unknown") => labels.iter().any(|l| !library.is_known(l)), + ("system", "known") => labels.iter().any(|l| library.is_known(l)), + ("not", _) => !check_table(document, labels, suffix, library), ("explicit", _) => check_presence(&document.labels, suffix), _ => false, } @@ -136,5 +151,7 @@ mod tests { params.prompt = "explicit:implied"; assert!(!check(¶ms)); + + // TODO: Add tests for `known` and `unknown`. } } From 98f00cccf8214d7b68c1aba61fc3f66f863c787c Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:18:51 -0300 Subject: [PATCH 24/40] refactored check tests --- src/core/query.rs | 112 +++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/src/core/query.rs b/src/core/query.rs index 6a426f8..720d4f5 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -76,82 +76,62 @@ mod tests { use super::*; use crate::core::label::LabelSet; - #[test] - fn check_works() { - let toml = r#" + fn make_document() -> Document { + Document { + path: "".into(), + name: "".into(), + labels: LabelSet::from(["l1", "l2", "label"]), + } + } + + fn make_library() -> LabelLibrary { + let toml = format!( + r#" [label] aliases = ["alias"] implies = ["implied"] description = "a label" [implied] - "#; - - let library = LabelLibrary::from_toml(toml).unwrap(); + "#, + ); - let document = Document { - path: "".into(), - name: "name".into(), - labels: LabelSet::from(["l1", "l2", "label"]), - }; + LabelLibrary::from_toml(&toml).unwrap() + } - let mut params = CheckParams { - prompt: "", - document: &document, - library: &library, + fn assert_check(prompt: &str, expected: bool, document: &Document, library: &LabelLibrary) { + let params = CheckParams { + prompt, + document, + library, }; - params.prompt = "l1"; - assert!(check(¶ms)); - - params.prompt = "l2"; - assert!(check(¶ms)); - - params.prompt = "l1 l2"; - assert!(check(¶ms)); - - params.prompt = "l3"; - assert!(!check(¶ms)); - - params.prompt = "l1 l3"; - assert!(!check(¶ms)); - - params.prompt = "l2 l3"; - assert!(!check(¶ms)); - - params.prompt = "l1 l2 l3"; - assert!(!check(¶ms)); - - params.prompt = "system:labeled"; - assert!(check(¶ms)); - - params.prompt = "system:unlabeled"; - assert!(!check(¶ms)); - - params.prompt = "not:system:labeled"; - assert!(!check(¶ms)); - - params.prompt = "not:system:unlabeled"; - assert!(check(¶ms)); - - params.prompt = "not:l1"; - assert!(!check(¶ms)); - - params.prompt = "not:l3"; - assert!(check(¶ms)); - - params.prompt = "explicit:l1"; - assert!(check(¶ms)); - - params.prompt = "explicit:not:l3"; - assert!(!check(¶ms)); - - params.prompt = "explicit:label"; - assert!(check(¶ms)); - - params.prompt = "explicit:implied"; - assert!(!check(¶ms)); + assert_eq!(check(¶ms), expected); + } - // TODO: Add tests for `known` and `unknown`. + #[test] + fn check_works() { + let library = make_library(); + let document = make_document(); + + let ac = |prompt: &str, expected: bool| assert_check(prompt, expected, &document, &library); + + ac("l1", true); + ac("l2", true); + ac("l1 l2", true); + ac("l3", false); + ac("l1 l3", false); + ac("l2 l3", false); + ac("l1 l2 l3", false); + ac("system:labeled", true); + ac("system:unlabeled", false); + ac("not:system:labeled", false); + ac("not:system:unlabeled", true); + ac("not:l1", false); + ac("not:l3", true); + ac("explicit:l1", true); + ac("explicit:not:l3", false); + ac("explicit:label", true); + ac("explicit:implied", false); } } From f6a69254c7e17d0660b1c9fa0aa41b11c199c755 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:27:47 -0300 Subject: [PATCH 25/40] refactored check tests further --- src/core/query.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/core/query.rs b/src/core/query.rs index 720d4f5..08e334d 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -110,10 +110,9 @@ mod tests { } #[test] - fn check_works() { + fn check_with_unknown_labels() { let library = make_library(); let document = make_document(); - let ac = |prompt: &str, expected: bool| assert_check(prompt, expected, &document, &library); ac("l1", true); @@ -123,12 +122,36 @@ mod tests { ac("l1 l3", false); ac("l2 l3", false); ac("l1 l2 l3", false); + } + + #[test] + fn check_with_system_labeled() { + let library = make_library(); + let document = make_document(); + let ac = |prompt: &str, expected: bool| assert_check(prompt, expected, &document, &library); + ac("system:labeled", true); ac("system:unlabeled", false); ac("not:system:labeled", false); ac("not:system:unlabeled", true); + } + + #[test] + fn check_with_unknown_not() { + let library = make_library(); + let document = make_document(); + let ac = |prompt: &str, expected: bool| assert_check(prompt, expected, &document, &library); + ac("not:l1", false); ac("not:l3", true); + } + + #[test] + fn check_with_explicit() { + let library = make_library(); + let document = make_document(); + let ac = |prompt: &str, expected: bool| assert_check(prompt, expected, &document, &library); + ac("explicit:l1", true); ac("explicit:not:l3", false); ac("explicit:label", true); From 8ec3cb1699f577d7b487725f66a2f22980f1c5e5 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:38:23 -0300 Subject: [PATCH 26/40] check system (un)known test --- src/core/query.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/core/query.rs b/src/core/query.rs index 08e334d..091726a 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -55,10 +55,8 @@ fn check_pseudo( ) -> bool { // TODO: Refator each type of pseudo matcher into it's own matcher module. match (prefix, suffix) { - // TODO: If this is the negation of `labeled` consider removing it. ("system", "unlabeled") => labels.is_empty(), ("system", "labeled") => !labels.is_empty(), - // TODO: If this is the negation of `known` consider removing it. ("system", "unknown") => labels.iter().any(|l| !library.is_known(l)), ("system", "known") => labels.iter().any(|l| library.is_known(l)), ("not", _) => !check_table(document, labels, suffix, library), @@ -157,4 +155,29 @@ mod tests { ac("explicit:label", true); ac("explicit:implied", false); } + + #[test] + fn check_with_system_known() { + let library = make_library(); + let mut document = make_document(); + let ac = |prompt: &str, expected: bool, document: &Document| { + assert_check(prompt, expected, &document, &library) + }; + + document.labels = LabelSet::from(["l1", "label"]); + ac("system:known", true, &document); + ac("system:unknown", true, &document); + // Proof of `not:system:known != system:unknown`. + ac("not:system:known", false, &document); + + document.labels = LabelSet::from(["l1", "l2"]); + ac("system:known", false, &document); + ac("system:unknown", true, &document); + ac("not:system:known", true, &document); + + document.labels = LabelSet::from(["label", "implied"]); + ac("system:known", true, &document); + ac("system:unknown", false, &document); + ac("not:system:known", false, &document); + } } From 52de9302ca69947c22f294e8d52a58517e37e7da Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:46:48 -0300 Subject: [PATCH 27/40] fcwalk --no-config and clap dependency added --- Cargo.toml | 1 + src/bin/fcwalk.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0dfa2de..2d1ab8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.4.5", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.106" tabled = "0.12.1" diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index 2d98b30..3c42db0 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -1,16 +1,30 @@ // TODO: Add a way to ignore certain directories. Specially the `fileclass` dir. +use clap::Parser; use std::env; use std::fs; use fileclass::core::config::{Config, STD_CONFIG_DIR}; use fileclass::extra::ipc::Message; +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Do not load any config file + #[arg(long)] + no_config: bool, +} + fn main() { + let args = Args::parse(); + let try_load_config = !args.no_config; + // Get the current directory let current_dir = env::current_dir().unwrap(); - load_config(); + if try_load_config { + load_config(); + } // Recursively traverse the directory tree traverse_directory(¤t_dir, ¤t_dir); From 4ccb70c5e61edbd6b05d4990b58b0bd3b0a6520a Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 00:34:29 -0300 Subject: [PATCH 28/40] fclabels toml format and fcwalk flexibility --- src/bin/fclabels.rs | 87 +++++++++++++++++++++++++++++++++++---------- src/bin/fcwalk.rs | 26 +++++++++++--- src/core/label.rs | 72 +++++++++++++++++++++++++++++++++---- 3 files changed, 154 insertions(+), 31 deletions(-) diff --git a/src/bin/fclabels.rs b/src/bin/fclabels.rs index ed7b455..55bb71a 100644 --- a/src/bin/fclabels.rs +++ b/src/bin/fclabels.rs @@ -3,9 +3,25 @@ use tabled::{ Table, Tabled, }; -use fileclass::extra::ipc::Message; +use clap::{Parser, ValueEnum}; + +use fileclass::{core::label::LabelDef, extra::ipc::Message}; use fileclass::{core::label::LabelLibrary, extra::input::read_stdin_messages}; +#[derive(Debug, Clone, ValueEnum)] +enum Format { + Table, + Toml, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Use a specific output format + #[arg(value_enum, short, long, default_value_t=Format::Table)] + format: Format, +} + #[derive(Tabled)] struct Row { name: String, @@ -14,10 +30,11 @@ struct Row { } fn main() { - let mut library = LabelLibrary::empty(); - let mut rows: Vec = Vec::new(); + let args = Args::parse(); - let mut unknown_labels: Vec = Vec::new(); + let mut library = LabelLibrary::empty(); + let mut known_library = LabelLibrary::empty(); + let mut unknown_library = LabelLibrary::empty(); for msg in read_stdin_messages() { match msg { @@ -26,8 +43,19 @@ fn main() { } Message::Document(d) => { for label in d.labels { - if !library.is_known(&label) { - unknown_labels.push(label); + if library.is_known(&label) { + if !known_library.is_known(&label) { + known_library.define(library.get_label_def(&label).unwrap().clone()); + } + } else { + if !unknown_library.is_known(&label) { + unknown_library.define(LabelDef { + name: label, + implies: Vec::new(), + aliases: Vec::new(), + description: "Unknown label".to_string(), + }); + } } } } @@ -35,33 +63,54 @@ fn main() { } } - unknown_labels.sort(); - unknown_labels.dedup(); // Works thanks to the sort. + match args.format { + Format::Table => print_table(known_library, unknown_library), + Format::Toml => print_toml(known_library, unknown_library), + } +} - let mut names = library.label_names(); - names.sort(); +fn print_toml(known_library: LabelLibrary, unknown_library: LabelLibrary) { + let known_toml = known_library.to_toml(); + let unknown_toml = unknown_library.to_toml(); - for name in names.iter() { - let mut aliases = Vec::from(library.get_aliases(name)); + println!("{}", known_toml); + println!("{}", unknown_toml); +} + +fn print_table(known_library: LabelLibrary, unknown_library: LabelLibrary) { + let mut known_rows: Vec = Vec::new(); + let mut unknown_rows: Vec = Vec::new(); + + for name in known_library.label_names() { + let mut aliases = Vec::from(known_library.get_aliases(name)); aliases.sort(); let aliases = aliases.join("\n"); - rows.push(Row { + let row = Row { name: name.to_string(), aliases, - description: library.get_description(name).to_string(), - }); + description: known_library.get_description(name).to_string(), + }; + + known_rows.push(row); } - for label in unknown_labels.iter() { - rows.push(Row { - name: label.to_string(), + for name in unknown_library.label_names() { + let row = Row { + name: name.to_string(), aliases: "".to_string(), description: "Unknown label".to_string(), - }); + }; + + unknown_rows.push(row); } + known_rows.sort_by(|a, b| a.name.cmp(&b.name)); + unknown_rows.sort_by(|a, b| a.name.cmp(&b.name)); + + let rows = known_rows.into_iter().chain(unknown_rows.into_iter()); + let mut table = Table::new(rows); // TODO: Use percents per col instead of fixed values. diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index 3c42db0..9d81a47 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -3,6 +3,7 @@ use clap::Parser; use std::env; use std::fs; +use std::io; use fileclass::core::config::{Config, STD_CONFIG_DIR}; use fileclass::extra::ipc::Message; @@ -13,21 +14,36 @@ struct Args { /// Do not load any config file #[arg(long)] no_config: bool, + + /// Do not output files/documents + #[arg(long)] + no_walk: bool, + + /// Forward stdin to stdout after processing + #[arg(short, long)] + forward: bool, } fn main() { let args = Args::parse(); - let try_load_config = !args.no_config; - // Get the current directory + let config_flag = !args.no_config; + let walk_flag = !args.no_walk; + let forward_flag = args.forward; + let current_dir = env::current_dir().unwrap(); - if try_load_config { + if config_flag { load_config(); } - // Recursively traverse the directory tree - traverse_directory(¤t_dir, ¤t_dir); + if walk_flag { + traverse_directory(¤t_dir, ¤t_dir); + } + + if forward_flag { + io::copy(&mut io::stdin(), &mut io::stdout()).unwrap(); + } } fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { diff --git a/src/core/label.rs b/src/core/label.rs index dc65183..413d751 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -77,14 +77,14 @@ impl From<[&str; N]> for LabelSet { } #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] -struct LabelDef { - name: String, - aliases: Vec, - implies: Vec, - description: String, +pub struct LabelDef { + pub name: String, + pub aliases: Vec, + pub implies: Vec, + pub description: String, } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize, Debug, Clone)] struct RawLabelDef { #[serde(default)] aliases: Vec, @@ -94,6 +94,7 @@ struct RawLabelDef { description: String, } +// TODO: Doesn't make sense to use equality since that would depend on the order. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct LabelLibrary { label_defs: Vec, @@ -116,11 +117,20 @@ impl LabelLibrary { Ok(()) } + pub fn define(&mut self, def: LabelDef) -> () { + if self.is_known(&def.name) { + panic!("Label '{}' already defined", def.name); + } + + self.label_defs.push(def); + } + + // TODO: Should this be a LabelSet? An iterator? pub fn label_names(&self) -> Vec<&str> { self.label_defs.iter().map(|l| l.name.as_str()).collect() } - fn get_label_def(&self, name: &str) -> Option<&LabelDef> { + pub fn get_label_def(&self, name: &str) -> Option<&LabelDef> { let def = self .label_defs .iter() @@ -196,6 +206,22 @@ impl LabelLibrary { Self::build(labels) } + + pub fn to_toml(&self) -> String { + let mut raw_labels: HashMap = HashMap::new(); + + for def in self.label_defs.iter() { + let raw = RawLabelDef { + aliases: def.aliases.clone(), + implies: def.implies.clone(), + description: def.description.clone(), + }; + + raw_labels.insert(def.name.clone(), raw); + } + + toml::to_string(&raw_labels).unwrap() + } } #[cfg(test)] @@ -389,4 +415,36 @@ pub mod tests { assert_eq!(library.is_known("unknown_label_name"), false); } + + #[test] + fn to_toml_works() { + let mut in_lib = setup_library(); + let toml = in_lib.to_toml(); + + let mut out_lib = LabelLibrary::from_toml(&toml).unwrap(); + + in_lib.label_defs = in_lib + .label_defs + .into_iter() + .map(|mut l| { + l.aliases.sort(); + l.implies.sort(); + l + }) + .collect(); + in_lib.label_defs.sort_by(|a, b| a.name.cmp(&b.name)); + + out_lib.label_defs = out_lib + .label_defs + .into_iter() + .map(|mut l| { + l.aliases.sort(); + l.implies.sort(); + l + }) + .collect(); + out_lib.label_defs.sort_by(|a, b| a.name.cmp(&b.name)); + + assert_eq!(in_lib, out_lib); + } } From 0e0f7eaa7b73f075eec4cf2b781ec4ae02bdde2d Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 00:36:18 -0300 Subject: [PATCH 29/40] removed version cli options --- src/bin/fclabels.rs | 2 +- src/bin/fcwalk.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/fclabels.rs b/src/bin/fclabels.rs index 55bb71a..25a0ee5 100644 --- a/src/bin/fclabels.rs +++ b/src/bin/fclabels.rs @@ -15,7 +15,7 @@ enum Format { } #[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] +#[command(author, about, long_about = None)] struct Args { /// Use a specific output format #[arg(value_enum, short, long, default_value_t=Format::Table)] diff --git a/src/bin/fcwalk.rs b/src/bin/fcwalk.rs index 9d81a47..f9c4477 100644 --- a/src/bin/fcwalk.rs +++ b/src/bin/fcwalk.rs @@ -9,7 +9,7 @@ use fileclass::core::config::{Config, STD_CONFIG_DIR}; use fileclass::extra::ipc::Message; #[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] +#[command(author, about, long_about = None)] struct Args { /// Do not load any config file #[arg(long)] From 141ddecae3754b90e2ccf2e6fe7961441eab57ff Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 00:36:56 -0300 Subject: [PATCH 30/40] fcwalk -> fcload --- src/bin/{fcwalk.rs => fcload.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/bin/{fcwalk.rs => fcload.rs} (100%) diff --git a/src/bin/fcwalk.rs b/src/bin/fcload.rs similarity index 100% rename from src/bin/fcwalk.rs rename to src/bin/fcload.rs From 6c9d2b2282cfbe7dfbaf35ca076e52c6d0f82b02 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:08:10 -0300 Subject: [PATCH 31/40] fclabels query opt --- src/bin/fclabels.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/bin/fclabels.rs b/src/bin/fclabels.rs index 25a0ee5..713e2d0 100644 --- a/src/bin/fclabels.rs +++ b/src/bin/fclabels.rs @@ -5,8 +5,18 @@ use tabled::{ use clap::{Parser, ValueEnum}; -use fileclass::{core::label::LabelDef, extra::ipc::Message}; -use fileclass::{core::label::LabelLibrary, extra::input::read_stdin_messages}; +use fileclass::{ + core::label::{LabelLibrary, LabelSet}, + extra::input::read_stdin_messages, +}; +use fileclass::{ + core::{ + document::Document, + label::LabelDef, + query::{check, CheckParams}, + }, + extra::ipc::Message, +}; #[derive(Debug, Clone, ValueEnum)] enum Format { @@ -20,6 +30,11 @@ struct Args { /// Use a specific output format #[arg(value_enum, short, long, default_value_t=Format::Table)] format: Format, + + // TODO: Consider turning this into positional arguments. + /// Only show labels that can match the provided query + #[arg(short, long)] + query: Option, } #[derive(Tabled)] @@ -43,6 +58,15 @@ fn main() { } Message::Document(d) => { for label in d.labels { + let passes_query = match &args.query { + Some(query) => test_query(&library, &label, &query), + None => true, + }; + + if !passes_query { + continue; + } + if library.is_known(&label) { if !known_library.is_known(&label) { known_library.define(library.get_label_def(&label).unwrap().clone()); @@ -69,6 +93,22 @@ fn main() { } } +fn test_query(library: &LabelLibrary, label: &str, query: &str) -> bool { + let document = Document { + path: "".to_string(), + name: "".to_string(), + labels: LabelSet::from([label]), + }; + + let params = CheckParams { + prompt: query, + document: &document, + library, + }; + + check(¶ms) +} + fn print_toml(known_library: LabelLibrary, unknown_library: LabelLibrary) { let known_toml = known_library.to_toml(); let unknown_toml = unknown_library.to_toml(); From 3838777a09f8717b6c159f55d89053c88a917ed3 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:21:49 -0300 Subject: [PATCH 32/40] validations and better error handling --- src/bin/fcload.rs | 15 ++++++---- src/core.rs | 1 + src/core/config.rs | 24 ++++++++++++---- src/core/error.rs | 46 ++++++++++++++++++++++++++++++ src/core/label.rs | 70 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 src/core/error.rs diff --git a/src/bin/fcload.rs b/src/bin/fcload.rs index f9c4477..01ee6da 100644 --- a/src/bin/fcload.rs +++ b/src/bin/fcload.rs @@ -6,6 +6,7 @@ use std::fs; use std::io; use fileclass::core::config::{Config, STD_CONFIG_DIR}; +use fileclass::core::error::ErrorKind; use fileclass::extra::ipc::Message; #[derive(Parser, Debug)] @@ -76,10 +77,14 @@ fn load_config() { let msg = Message::Config(config); println!("{}", msg.serialize()); } - Err(e) => { - // TODO: Only warn if missing. - // TODO: Fail if the config is invalid. - eprintln!("Error loading config: {}", e); - } + Err(e) => match e.kind { + ErrorKind::MissingConfig => { + eprintln!("Warning: {}", e); + } + _ => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + }, } } diff --git a/src/core.rs b/src/core.rs index b9f650e..b808165 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,5 @@ pub mod config; pub mod document; +pub mod error; pub mod label; pub mod query; diff --git a/src/core/config.rs b/src/core/config.rs index 0de514d..5aabdfe 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; -use std::error::Error; use std::fs; use std::path::Path; +use super::error::Error; use super::label::LabelLibrary; pub const STD_CONFIG_DIR: &str = "fileclass"; @@ -23,21 +23,33 @@ pub struct Settings { impl Config { // TODO: Remove file system dependency from core. - pub fn load(dir_path: &str) -> Result> { + pub fn load(dir_path: &str) -> Result { let labels_path = Path::new(dir_path).join(LABELS_FILENAME); - let labels_content = fs::read_to_string(labels_path)?; + let labels_content = match fs::read_to_string(labels_path) { + Ok(content) => content, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + return Err(Error::missing_config(format!( + "labels file not found in {}", + dir_path + ))) + } + _ => return Err(Error::invalid_config(e.to_string())), + }, + }; let labels = LabelLibrary::from_toml(&labels_content)?; + // These settings will be removed in the near future. So let's just unwrap. let settings_path = Path::new(dir_path).join(SETTINGS_FILENAME); - let settings_content = fs::read_to_string(settings_path)?; - let settings: Settings = toml::from_str(&settings_content)?; + let settings_content = fs::read_to_string(settings_path).unwrap(); + let settings: Settings = toml::from_str(&settings_content).unwrap(); let config = Config { labels, settings }; Ok(config) } - pub fn std_load() -> Result> { + pub fn std_load() -> Result { Config::load(STD_CONFIG_DIR) } } diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..7d6c8b9 --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,46 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + /// The loaded configuration is invalid + InvalidConfig, + /// Configurations are missing + MissingConfig, +} + +#[derive(Debug)] +pub struct Error { + pub kind: ErrorKind, + pub main_msg: String, + pub detail_msg: Option, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.main_msg)?; + + if let Some(detail_msg) = &self.detail_msg { + write!(f, ". {}", detail_msg)?; + } + + Ok(()) + } +} + +impl Error { + pub fn invalid_config(detail_msg: String) -> Self { + Error { + kind: ErrorKind::InvalidConfig, + main_msg: "the loaded configuration is invalid ".to_string(), + detail_msg: Some(detail_msg), + } + } + + pub fn missing_config(detail_msg: String) -> Self { + Error { + kind: ErrorKind::MissingConfig, + main_msg: "configurations are missing".to_string(), + detail_msg: Some(detail_msg), + } + } +} diff --git a/src/core/label.rs b/src/core/label.rs index 413d751..8d5ffbe 100644 --- a/src/core/label.rs +++ b/src/core/label.rs @@ -1,7 +1,7 @@ +use crate::core::error::Error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::collections::HashSet; -use std::error::Error; use std::iter::FromIterator; use std::iter::IntoIterator; @@ -101,7 +101,7 @@ pub struct LabelLibrary { } impl LabelLibrary { - fn build(defs: Vec) -> Result> { + fn build(defs: Vec) -> Result { Self::validate(&defs)?; Ok(Self { label_defs: defs }) } @@ -113,7 +113,60 @@ impl LabelLibrary { /// Validates that the label definitions are valid. /// /// This is a placeholder for now. - fn validate(_defs: &Vec) -> Result<(), Box> { + fn validate(defs: &Vec) -> Result<(), Error> { + let duplicate_id_error = |name: &str| { + let detail = format!("duplicate label identifier '{}'", name); + Error::invalid_config(detail) + }; + + let missing_implied_error = |name: &str| { + let detail = format!("implied label '{}' is not defined", name); + Error::invalid_config(detail) + }; + + let duplicate_implied_error = |name: &str| { + let detail = format!("label '{}' implied multiple times", name); + Error::invalid_config(detail) + }; + + let mut already_defined = HashSet::new(); + + for def in defs.iter() { + let name = &def.name; + + if already_defined.contains(name) { + return Err(duplicate_id_error(name)); + } + + already_defined.insert(name); + + for alias in def.aliases.iter() { + if already_defined.contains(alias) { + return Err(duplicate_id_error(alias)); + } + + already_defined.insert(alias); + } + + let mut already_implied = HashSet::new(); + + for implied in def.implies.iter() { + if already_implied.contains(implied) { + return Err(duplicate_implied_error(implied)); + } + + already_implied.insert(implied); + } + } + + for def in defs.iter() { + for implied in def.implies.iter() { + if !already_defined.contains(implied) { + return Err(missing_implied_error(implied)); + } + } + } + Ok(()) } @@ -192,8 +245,12 @@ impl LabelLibrary { } } - pub fn from_toml(toml: &str) -> Result> { - let raw_labels: HashMap = toml::from_str(toml)?; + pub fn from_toml(toml: &str) -> Result { + let raw_labels: HashMap = match toml::from_str(toml) { + Ok(raw) => raw, + Err(_) => return Err(Error::invalid_config("can't parse toml".to_string())), + }; + let labels = raw_labels .into_iter() .map(|(name, raw)| LabelDef { @@ -204,7 +261,8 @@ impl LabelLibrary { }) .collect(); - Self::build(labels) + let built = Self::build(labels)?; + Ok(built) } pub fn to_toml(&self) -> String { From 115c47109019c49a542bbeac7c1966ce2e00b0ad Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:30:21 -0300 Subject: [PATCH 33/40] fclink takes explicit target folder --- src/bin/fcinit.rs | 10 +--------- src/bin/fclink.rs | 6 ------ src/core/config.rs | 19 +------------------ src/extra/input.rs | 5 +---- src/extra/ipc.rs | 10 +--------- 5 files changed, 4 insertions(+), 46 deletions(-) diff --git a/src/bin/fcinit.rs b/src/bin/fcinit.rs index 030ec59..d0270cf 100644 --- a/src/bin/fcinit.rs +++ b/src/bin/fcinit.rs @@ -1,7 +1,7 @@ use std::fs; use std::path::Path; -use fileclass::core::config::{LABELS_FILENAME, SETTINGS_FILENAME, STD_CONFIG_DIR}; +use fileclass::core::config::{LABELS_FILENAME, STD_CONFIG_DIR}; const LABELS_CONTENT: &str = r#"[label] description = "a label" @@ -10,12 +10,9 @@ implies = ["implied"] [implied]"#; -const SETTINGS_CONTENT: &str = r#"link_dir="fileclass/temp/links""#; - fn main() { let folder_path = Path::new(STD_CONFIG_DIR); let labels_path = folder_path.join(LABELS_FILENAME); - let settings_path = folder_path.join(SETTINGS_FILENAME); // Create the folder if it doesn't exist if !folder_path.exists() { @@ -26,9 +23,4 @@ fn main() { if !labels_path.exists() { fs::write(labels_path, LABELS_CONTENT).expect("Failed to generate labels.toml"); } - - // Generate settings.toml if it doesn't exist - if !settings_path.exists() { - fs::write(settings_path, SETTINGS_CONTENT).expect("Failed to generate settings.toml"); - } } diff --git a/src/bin/fclink.rs b/src/bin/fclink.rs index 53c16e8..5fa26a6 100644 --- a/src/bin/fclink.rs +++ b/src/bin/fclink.rs @@ -7,16 +7,10 @@ use std::path::Path; use std::process; use std::{env, fs}; -use fileclass::core::config::Config; use fileclass::extra::input::{map_stdin_sources_to_target_folder, SourceTargetPair}; fn get_link_dir(args: &Vec) -> String { match args.len() { - 1 => { - // TODO: Handle. - let config = Config::std_load().unwrap(); - config.settings.link_dir - } 2 => args[1].clone(), _ => { eprintln!("Usage: fclink "); diff --git a/src/core/config.rs b/src/core/config.rs index 5aabdfe..a2acb22 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -7,18 +7,10 @@ use super::label::LabelLibrary; pub const STD_CONFIG_DIR: &str = "fileclass"; pub const LABELS_FILENAME: &str = "labels.toml"; -pub const SETTINGS_FILENAME: &str = "settings.toml"; #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Config { pub labels: LabelLibrary, - pub settings: Settings, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] -pub struct Settings { - // TODO: Use a default if missing and use that default in fcinit. - pub link_dir: String, } impl Config { @@ -39,12 +31,7 @@ impl Config { }; let labels = LabelLibrary::from_toml(&labels_content)?; - // These settings will be removed in the near future. So let's just unwrap. - let settings_path = Path::new(dir_path).join(SETTINGS_FILENAME); - let settings_content = fs::read_to_string(settings_path).unwrap(); - let settings: Settings = toml::from_str(&settings_content).unwrap(); - - let config = Config { labels, settings }; + let config = Config { labels }; Ok(config) } @@ -62,14 +49,10 @@ mod tests { fn load_works() { let config = Config::load("test_dir/fileclass").unwrap(); let labels = config.labels; - let settings = config.settings; let label_name = labels.resolve("alias"); let label_description = labels.get_description("label"); assert_eq!(label_name, "label"); assert_eq!(label_description, "a label"); - - let link_dir = settings.link_dir; - assert_eq!(link_dir, "test_dir/fileclass/temp/links"); } } diff --git a/src/extra/input.rs b/src/extra/input.rs index ff59f46..5609f73 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -67,7 +67,7 @@ pub fn map_stdin_sources_to_target_folder( #[cfg(test)] mod tests { use super::*; - use crate::core::config::{Config, Settings}; + use crate::core::config::Config; use crate::core::label::{LabelLibrary, LabelSet}; use std::path::Path; @@ -102,9 +102,6 @@ mod tests { "#, ) .unwrap(), - settings: Settings { - link_dir: "ld".to_string(), - }, }), Message::Document(Document { // TODO: Should this be trimmed by Document? diff --git a/src/extra/ipc.rs b/src/extra/ipc.rs index 7b99015..38965ca 100644 --- a/src/extra/ipc.rs +++ b/src/extra/ipc.rs @@ -74,10 +74,7 @@ impl Message { #[cfg(test)] mod tests { - use crate::core::{ - config::Settings, - label::{LabelLibrary, LabelSet}, - }; + use crate::core::label::{LabelLibrary, LabelSet}; use super::*; @@ -133,13 +130,9 @@ mod tests { [implied]"#, ) .unwrap(); - let settings = Settings { - link_dir: "link_dir".to_string(), - }; let msg = Message::Config(Config { labels: library.clone(), - settings: settings.clone(), }); let serialized = msg.serialize(); let deserialized = Message::deserialize(&serialized); @@ -147,6 +140,5 @@ mod tests { assert_eq!(msg, deserialized); assert_eq!(library.resolve("alias"), "label"); assert_eq!(library.resolve("implied"), "implied"); - assert_eq!(settings.link_dir, "link_dir"); } } From bbe5e7bbb918876bae51297e55156ad9b16a3a50 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sat, 30 Sep 2023 16:28:11 -0300 Subject: [PATCH 34/40] fcload custom directory --- src/bin/fcload.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/bin/fcload.rs b/src/bin/fcload.rs index 01ee6da..3896ff9 100644 --- a/src/bin/fcload.rs +++ b/src/bin/fcload.rs @@ -1,14 +1,16 @@ // TODO: Add a way to ignore certain directories. Specially the `fileclass` dir. use clap::Parser; -use std::env; use std::fs; use std::io; +use std::path::Path; use fileclass::core::config::{Config, STD_CONFIG_DIR}; use fileclass::core::error::ErrorKind; use fileclass::extra::ipc::Message; +const DEFAULT_WORKDIR: &str = "."; + #[derive(Parser, Debug)] #[command(author, about, long_about = None)] struct Args { @@ -23,6 +25,10 @@ struct Args { /// Forward stdin to stdout after processing #[arg(short, long)] forward: bool, + + /// Load the specified directory instead + #[arg(short, long, default_value = DEFAULT_WORKDIR)] + workdir: String, } fn main() { @@ -31,15 +37,14 @@ fn main() { let config_flag = !args.no_config; let walk_flag = !args.no_walk; let forward_flag = args.forward; - - let current_dir = env::current_dir().unwrap(); + let workdir = args.workdir; if config_flag { - load_config(); + load_config(&workdir); } if walk_flag { - traverse_directory(¤t_dir, ¤t_dir); + traverse_directory(&workdir, &workdir); } if forward_flag { @@ -47,7 +52,10 @@ fn main() { } } -fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { +fn traverse_directory, B: AsRef>(path: P, base_path: B) { + let path = path.as_ref(); + let base_path = base_path.as_ref(); + // Read the directory entries if let Ok(entries) = fs::read_dir(path) { for entry in entries { @@ -56,14 +64,15 @@ fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { // Omit the fileclass directory. if !entry_path.ends_with(STD_CONFIG_DIR) { - let relative_path = entry_path.strip_prefix(base_path).unwrap(); - + // let relative_path = entry_path.strip_prefix(base_path).unwrap(); // Print the relative entry path - println!("{}", relative_path.display()); + // println!("{}", relative_path.display()); + + println!("{}", entry_path.display()); // Recursively traverse subdirectories if entry_path.is_dir() { - traverse_directory(&entry_path, base_path); + traverse_directory(entry_path, base_path); } } } @@ -71,8 +80,10 @@ fn traverse_directory(path: &std::path::Path, base_path: &std::path::Path) { } } -fn load_config() { - match Config::std_load() { +fn load_config(workdir: &str) { + let config_dir = format!("{}/{}", workdir, STD_CONFIG_DIR); + + match Config::load(&config_dir) { Ok(config) => { let msg = Message::Config(config); println!("{}", msg.serialize()); From 45c7eea9b77166bbc4ae0284eed320516db50b55 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 1 Oct 2023 01:00:35 -0300 Subject: [PATCH 35/40] multiple label files support --- src/bin/fcinit.rs | 18 +++++--- src/core/config.rs | 47 ++++++++++++++------- src/core/error.rs | 10 +++++ test_dir/fileclass/{ => labels}/labels.toml | 0 test_dir/fileclass/settings.toml | 1 - 5 files changed, 55 insertions(+), 21 deletions(-) rename test_dir/fileclass/{ => labels}/labels.toml (100%) delete mode 100644 test_dir/fileclass/settings.toml diff --git a/src/bin/fcinit.rs b/src/bin/fcinit.rs index d0270cf..cfd4128 100644 --- a/src/bin/fcinit.rs +++ b/src/bin/fcinit.rs @@ -1,7 +1,9 @@ use std::fs; use std::path::Path; -use fileclass::core::config::{LABELS_FILENAME, STD_CONFIG_DIR}; +use fileclass::core::config::{LABELS_DIRNAME, STD_CONFIG_DIR}; + +const MAIN_LABELS_FILENAME: &str = "main.toml"; const LABELS_CONTENT: &str = r#"[label] description = "a label" @@ -12,15 +14,21 @@ implies = ["implied"] fn main() { let folder_path = Path::new(STD_CONFIG_DIR); - let labels_path = folder_path.join(LABELS_FILENAME); + let labels_path = folder_path.join(LABELS_DIRNAME); + let main_labels_path = labels_path.join(MAIN_LABELS_FILENAME); // Create the folder if it doesn't exist if !folder_path.exists() { - fs::create_dir(folder_path).expect("Failed to create folder"); + fs::create_dir_all(folder_path).expect("Failed to create folder"); } - // Generate labels.toml if it doesn't exist + // Create the labels directory if it doesn't exist if !labels_path.exists() { - fs::write(labels_path, LABELS_CONTENT).expect("Failed to generate labels.toml"); + fs::create_dir(labels_path).expect("Failed to create labels directory"); + } + + // Generate labels.toml if it doesn't exist + if !main_labels_path.exists() { + fs::write(main_labels_path, LABELS_CONTENT).expect("Failed to generate labels.toml"); } } diff --git a/src/core/config.rs b/src/core/config.rs index a2acb22..159c5bc 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -6,7 +6,7 @@ use super::error::Error; use super::label::LabelLibrary; pub const STD_CONFIG_DIR: &str = "fileclass"; -pub const LABELS_FILENAME: &str = "labels.toml"; +pub const LABELS_DIRNAME: &str = "labels"; #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Config { @@ -16,23 +16,40 @@ pub struct Config { impl Config { // TODO: Remove file system dependency from core. pub fn load(dir_path: &str) -> Result { - let labels_path = Path::new(dir_path).join(LABELS_FILENAME); - let labels_content = match fs::read_to_string(labels_path) { - Ok(content) => content, - Err(e) => match e.kind() { - std::io::ErrorKind::NotFound => { - return Err(Error::missing_config(format!( - "labels file not found in {}", - dir_path - ))) + let labels_path = Path::new(dir_path).join(LABELS_DIRNAME); + let rd = labels_path.read_dir().map_err(|e| { + if let std::io::ErrorKind::NotFound = e.kind() { + Error::missing_config(format!("labels directory not found in {}", dir_path)) + } else { + Error::invalid_config(e.to_string()) + } + })?; + + let mut labels_content = String::new(); + for entry in rd { + let entry = entry.map_err(|_| { + Error::unexpected(format!( + "could not read the labels directory in {}", + dir_path + )) + })?; + let path = entry.path(); + let content = fs::read_to_string(&path).map_err(|e| { + if let std::io::ErrorKind::NotFound = e.kind() { + Error::missing_config(format!( + "label file {} disappeared before reading it", + path.file_name().unwrap().to_string_lossy(), + )) + } else { + Error::invalid_config(e.to_string()) } - _ => return Err(Error::invalid_config(e.to_string())), - }, - }; - let labels = LabelLibrary::from_toml(&labels_content)?; + })?; + labels_content.push_str(&content); + labels_content.push('\n'); + } + let labels = LabelLibrary::from_toml(&labels_content)?; let config = Config { labels }; - Ok(config) } diff --git a/src/core/error.rs b/src/core/error.rs index 7d6c8b9..9cf7c9d 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -6,6 +6,8 @@ pub enum ErrorKind { InvalidConfig, /// Configurations are missing MissingConfig, + /// An unexpected/unknown error occurred + Unexpected, } #[derive(Debug)] @@ -43,4 +45,12 @@ impl Error { detail_msg: Some(detail_msg), } } + + pub fn unexpected(detail_msg: String) -> Self { + Error { + kind: ErrorKind::Unexpected, + main_msg: "an unexpected error occurred".to_string(), + detail_msg: Some(detail_msg), + } + } } diff --git a/test_dir/fileclass/labels.toml b/test_dir/fileclass/labels/labels.toml similarity index 100% rename from test_dir/fileclass/labels.toml rename to test_dir/fileclass/labels/labels.toml diff --git a/test_dir/fileclass/settings.toml b/test_dir/fileclass/settings.toml deleted file mode 100644 index b672a77..0000000 --- a/test_dir/fileclass/settings.toml +++ /dev/null @@ -1 +0,0 @@ -link_dir="test_dir/fileclass/temp/links" \ No newline at end of file From d978bea8883c99b6622006f5c4df50090b5337ae Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 1 Oct 2023 01:16:03 -0300 Subject: [PATCH 36/40] stricter config errors having only the root of the config folder but missing parts (or all of it) is considered invalid instead of missing, which will cause `fcload` to terminate immediately --- src/core/config.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/core/config.rs b/src/core/config.rs index 159c5bc..5c0372a 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -15,11 +15,22 @@ pub struct Config { impl Config { // TODO: Remove file system dependency from core. - pub fn load(dir_path: &str) -> Result { - let labels_path = Path::new(dir_path).join(LABELS_DIRNAME); + pub fn load>(dir_path: P) -> Result { + let dir_path = dir_path.as_ref(); + if !dir_path.exists() { + return Err(Error::missing_config(format!( + "config directory {} does not exist", + dir_path.to_string_lossy() + ))); + } + + let labels_path = dir_path.join(LABELS_DIRNAME); let rd = labels_path.read_dir().map_err(|e| { if let std::io::ErrorKind::NotFound = e.kind() { - Error::missing_config(format!("labels directory not found in {}", dir_path)) + Error::invalid_config(format!( + "labels directory not found in {}", + dir_path.to_string_lossy() + )) } else { Error::invalid_config(e.to_string()) } @@ -30,13 +41,13 @@ impl Config { let entry = entry.map_err(|_| { Error::unexpected(format!( "could not read the labels directory in {}", - dir_path + dir_path.to_string_lossy() )) })?; let path = entry.path(); let content = fs::read_to_string(&path).map_err(|e| { if let std::io::ErrorKind::NotFound = e.kind() { - Error::missing_config(format!( + Error::invalid_config(format!( "label file {} disappeared before reading it", path.file_name().unwrap().to_string_lossy(), )) From cd2fe8be04442b543b17fff330416e8a1455a50f Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 1 Oct 2023 01:24:45 -0300 Subject: [PATCH 37/40] added some tests to config load --- src/core/config.rs | 19 +++++++++++++++++++ .../labels/{labels.toml => main.toml} | 0 test_dir/fileclass_empty/.gitkeep | 0 test_dir/fileclass_invalid_structure/labels | 0 .../labels/invalid.toml | 1 + 5 files changed, 20 insertions(+) rename test_dir/fileclass/labels/{labels.toml => main.toml} (100%) create mode 100644 test_dir/fileclass_empty/.gitkeep create mode 100644 test_dir/fileclass_invalid_structure/labels create mode 100644 test_dir/fileclass_invalid_toml/labels/invalid.toml diff --git a/src/core/config.rs b/src/core/config.rs index 5c0372a..30ab837 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -72,6 +72,7 @@ impl Config { #[cfg(test)] mod tests { use super::*; + use crate::core::error::ErrorKind; #[test] fn load_works() { @@ -83,4 +84,22 @@ mod tests { assert_eq!(label_name, "label"); assert_eq!(label_description, "a label"); } + + #[test] + fn load_with_missing_config() { + let config = Config::load("test_dir/fileclass_missing"); + assert_eq!(config.err().unwrap().kind, ErrorKind::MissingConfig); + } + + #[test] + fn load_with_invalid_config_structure() { + let config = Config::load("test_dir/fileclass_invalid_structure"); + assert_eq!(config.err().unwrap().kind, ErrorKind::InvalidConfig); + } + + #[test] + fn load_with_invalid_toml() { + let config = Config::load("test_dir/fileclass_invalid_toml"); + assert_eq!(config.err().unwrap().kind, ErrorKind::InvalidConfig); + } } diff --git a/test_dir/fileclass/labels/labels.toml b/test_dir/fileclass/labels/main.toml similarity index 100% rename from test_dir/fileclass/labels/labels.toml rename to test_dir/fileclass/labels/main.toml diff --git a/test_dir/fileclass_empty/.gitkeep b/test_dir/fileclass_empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test_dir/fileclass_invalid_structure/labels b/test_dir/fileclass_invalid_structure/labels new file mode 100644 index 0000000..e69de29 diff --git a/test_dir/fileclass_invalid_toml/labels/invalid.toml b/test_dir/fileclass_invalid_toml/labels/invalid.toml new file mode 100644 index 0000000..05f6263 --- /dev/null +++ b/test_dir/fileclass_invalid_toml/labels/invalid.toml @@ -0,0 +1 @@ +[[[[[][]////]]]] From 266d06180d591d350d22254b19e677d75f0a2a7f Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 1 Oct 2023 01:55:22 -0300 Subject: [PATCH 38/40] experimental soft links on fclink --- src/bin/fclink.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/bin/fclink.rs b/src/bin/fclink.rs index 5fa26a6..557eac5 100644 --- a/src/bin/fclink.rs +++ b/src/bin/fclink.rs @@ -9,6 +9,22 @@ use std::{env, fs}; use fileclass::extra::input::{map_stdin_sources_to_target_folder, SourceTargetPair}; +#[cfg(windows)] +fn symlink, L: AsRef>(source: S, link: L) -> std::io::Result<()> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + if source.as_ref().is_dir() { + symlink_dir(source, link) + } else { + symlink_file(source, link) + } +} + +#[cfg(not(windows))] +fn symlink, L: AsRef>(source: S, link: L) -> std::io::Result<()> { + use std::os::unix::fs::symlink; + symlink(source, link) +} + fn get_link_dir(args: &Vec) -> String { match args.len() { 2 => args[1].clone(), @@ -51,9 +67,10 @@ fn main() { } // Hard link - if let Err(err) = fs::hard_link(&source, &target) { + // fs::hard_link(&source, &target) + if let Err(err) = symlink(fs::canonicalize(&source).unwrap(), &target) { eprintln!( - "Failed to create hard link for \"{}\": {}", + "Failed to create link for \"{}\": {}", source.to_str().unwrap(), err ); From e34da3c288f10720abeb278e14c75aeb8a0e00e9 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 1 Oct 2023 02:15:36 -0300 Subject: [PATCH 39/40] fclink supports directories now it uses smart logic to decide between symlink and hard link dirs will be symlinked files will be hard linked if possible, if not will be symlinked as well --- src/bin/fclink.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/bin/fclink.rs b/src/bin/fclink.rs index 557eac5..95f8d44 100644 --- a/src/bin/fclink.rs +++ b/src/bin/fclink.rs @@ -35,6 +35,17 @@ fn get_link_dir(args: &Vec) -> String { } } +fn smart_link, L: AsRef>(source: S, link: L) -> std::io::Result<()> { + let source = source.as_ref(); + let link = link.as_ref(); + + if source.is_dir() { + symlink(source, link) + } else { + fs::hard_link(source, link).or_else(|_| symlink(source, link)) + } +} + fn main() { let args: Vec = env::args().collect(); let link_dir = get_link_dir(&args); @@ -57,18 +68,17 @@ fn main() { // Temporal safe guard for directories and other entities. // TODO: Support directories at least. - if !Path::new(&source).is_file() { + /*if !Path::new(&source).is_file() { eprintln!( "Warning: \"{}\" is not a regular file, ignoring.", source.to_str().unwrap() ); return; - } + }*/ // Hard link - // fs::hard_link(&source, &target) - if let Err(err) = symlink(fs::canonicalize(&source).unwrap(), &target) { + if let Err(err) = smart_link(fs::canonicalize(&source).unwrap(), &target) { eprintln!( "Failed to create link for \"{}\": {}", source.to_str().unwrap(), From e4da32e1bb2838a2789d072afb64ca3a36ea2316 Mon Sep 17 00:00:00 2001 From: Noxware <7684329+noxware@users.noreply.github.com> Date: Sun, 8 Oct 2023 22:28:52 -0300 Subject: [PATCH 40/40] wip --- .gitignore | 3 ++- Cargo.toml | 1 + src/bin/fclink.rs | 25 +++++++++++++++---------- src/bin/fcmv.rs | 10 +++++++--- src/extra/input.rs | 2 +- src/utils/fs.rs | 43 +++++++++++++++++++++++++++++++++++++------ 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index d602214..89b2e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ Cargo.lock *.pdb # Custom -/ignored \ No newline at end of file +/ignored +playground.rs \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2d1ab8b..c55bf71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] clap = { version = "4.4.5", features = ["derive"] } +dunce = "1.0.4" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.106" tabled = "0.12.1" diff --git a/src/bin/fclink.rs b/src/bin/fclink.rs index 95f8d44..144765a 100644 --- a/src/bin/fclink.rs +++ b/src/bin/fclink.rs @@ -8,6 +8,7 @@ use std::process; use std::{env, fs}; use fileclass::extra::input::{map_stdin_sources_to_target_folder, SourceTargetPair}; +use fileclass::utils::fs::PathExt; #[cfg(windows)] fn symlink, L: AsRef>(source: S, link: L) -> std::io::Result<()> { @@ -64,8 +65,6 @@ fn main() { map_stdin_sources_to_target_folder(target_folder.to_path_buf()).for_each(|p| { let SourceTargetPair { source, target } = p; - println!("{}", target.to_str().unwrap()); - // Temporal safe guard for directories and other entities. // TODO: Support directories at least. /*if !Path::new(&source).is_file() { @@ -77,14 +76,20 @@ fn main() { return; }*/ - // Hard link - if let Err(err) = smart_link(fs::canonicalize(&source).unwrap(), &target) { - eprintln!( - "Failed to create link for \"{}\": {}", - source.to_str().unwrap(), - err - ); - process::exit(1); + if let Some(target) = target { + println!("{}", target.to_str().unwrap()); + // Canonicalize the source path to avoid problems with symlinks. However, the link + // will point to the original file if source is a symlink. + if let Err(err) = smart_link(&source.unc_safe_canonicalize().unwrap(), &target) { + eprintln!( + "Failed to create link for \"{}\": {}", + source.to_str().unwrap(), + err + ); + process::exit(1); + } + } else { + eprintln!("Ignoring file: {}", source.display()); } }); } diff --git a/src/bin/fcmv.rs b/src/bin/fcmv.rs index 8e774ce..9deee46 100644 --- a/src/bin/fcmv.rs +++ b/src/bin/fcmv.rs @@ -14,9 +14,13 @@ fn main() { map_stdin_sources_to_target_folder(target_folder.to_path_buf()).for_each(|p| { let SourceTargetPair { source, target } = p; - if let Err(err) = fs::rename(&source, &target) { - eprintln!("Failed to move file: {}", err); - process::exit(1); + if let Some(target) = target { + if let Err(err) = fs::rename(&source, &target) { + eprintln!("Failed to move file: {}", err); + process::exit(1); + } + } else { + eprintln!("Ignoring file: {}", source.display()); } }); } diff --git a/src/extra/input.rs b/src/extra/input.rs index 5609f73..ad89b4e 100644 --- a/src/extra/input.rs +++ b/src/extra/input.rs @@ -33,7 +33,7 @@ pub fn read_stdin_documents() -> impl Iterator { #[derive(Debug, PartialEq, Eq, Clone)] pub struct SourceTargetPair { pub source: PathBuf, - pub target: PathBuf, + pub target: Option, } fn map_sources_to_target_folder( diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 7e481dd..f57f552 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -12,7 +12,22 @@ pub fn get_suffix(path: &Path) -> &str { &file_name[first_dot_index..] } -pub fn get_unique_target(source_path: &Path, target_dir: &Path) -> PathBuf { +pub fn get_unique_target(source_path: &Path, target_dir: &Path) -> Option { + // Don't canonicalize `source_path`. Because of symlink resolution that's not equivalent. + let source_parent = source_path + .parent() + .unwrap() + .unc_safe_canonicalize() + .unwrap(); + let target_dir = target_dir.unc_safe_canonicalize().unwrap(); + + // TODO: Test symlinks in the middle, at the end. + // TODO: Test that this if works. + // The file is already in the target directory. + if source_parent == target_dir { + return None; + } + let mut target_file = target_dir.join(source_path.file_name().unwrap()); let mut index = 1; @@ -24,7 +39,23 @@ pub fn get_unique_target(source_path: &Path, target_dir: &Path) -> PathBuf { index += 1; } - target_file + Some(target_file) +} + +// On Windows, `canonicalize` will give you a UNC path. This tries to not do so. +pub fn unc_safe_canonicalize>(path: P) -> std::io::Result { + let path = path.as_ref(); + dunce::canonicalize(path) +} + +pub trait PathExt { + fn unc_safe_canonicalize(&self) -> std::io::Result; +} + +impl> PathExt for P { + fn unc_safe_canonicalize(&self) -> std::io::Result { + unc_safe_canonicalize(self) + } } #[cfg(test)] @@ -57,28 +88,28 @@ mod tests { let target_dir = Path::new("test_dir"); assert_eq!( get_unique_target(source_path, target_dir), - Path::new("test_dir/a.txt") + Some(PathBuf::from("test_dir/a.txt")) ); let source_path = Path::new("test_dir/files/a.txt"); let target_dir = Path::new("test_dir/files"); assert_eq!( get_unique_target(source_path, target_dir), - Path::new("test_dir/files/a (1).txt") + Some(PathBuf::from("test_dir/files/a (1).txt")) ); let source_path = Path::new("test_dir/files/b.ext.txt"); let target_dir = Path::new("test_dir/files"); assert_eq!( get_unique_target(source_path, target_dir), - Path::new("test_dir/files/b (2).ext.txt") + Some(PathBuf::from("test_dir/files/b (2).ext.txt")) ); let source_path = Path::new("test_dir/files/"); let target_dir = Path::new("test_dir/"); assert_eq!( get_unique_target(source_path, target_dir), - Path::new("test_dir/files (1)") + Some(PathBuf::from("test_dir/files (1)")) ); } }