From 6169dc177158496359e8228222ffe75ba895d352 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:51:42 -0400 Subject: [PATCH 01/28] Creates lines structs and traits --- .gitignore | 1 + Cargo.lock | 75 +++++++++++++++++++++ Cargo.toml | 9 +++ src/main.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 1497e66..7be540d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ createnv.egg-info/ dist/ htmlcov/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..363237f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "createnv" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cd51bd1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "createnv" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.5" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5c667a8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,186 @@ +use rand::{thread_rng, Rng}; +use std::collections::HashMap; + +const DEFAULT_RANDOM_CHARS: &str = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; + +struct Title { + contents: String, +} + +impl Title { + fn as_string(&self) -> String { + format!("# {}", self.contents) + } +} + +trait VariableLine { + fn key(&self) -> String; + fn value(&self) -> String; + fn as_string(&self) -> String { + format!("{}={}", self.key(), self.value()) + } +} + +struct Variable { + name: String, + input: String, +} + +impl VariableLine for Variable { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + self.input.to_string() + } +} + +struct VariableWithDefaultValue { + name: String, + default: String, + input: Option, +} + +impl VariableLine for VariableWithDefaultValue { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + match &self.input { + Some(value) => value.to_string(), + None => self.default.to_string(), + } + } +} + +struct AutoGeneratedVariable { + name: String, + pattern: String, + settings: HashMap<&'static str, &'static str>, +} + +impl VariableLine for AutoGeneratedVariable { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + let mut value: String = self.pattern.to_string(); + for (k, v) in self.settings.iter() { + let key = format!("{{{}}}", *k); + value = value.replace(&key, *v); + } + value + } +} + +struct VariableWithRandomValue { + name: String, + length: Option, +} + +impl VariableLine for VariableWithRandomValue { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + let mut rng = thread_rng(); + let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); + let mut value: String = String::from(""); + let length = match self.length { + Some(n) => n, + None => rng.gen_range(64..=128), + }; + for _ in 0..length { + let pos = rng.gen_range(0..max_chars_idx); + value.push(DEFAULT_RANDOM_CHARS.chars().nth(pos).unwrap()) + } + value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_title() { + let line = Title { + contents: "Fourty-two".to_string(), + }; + assert_eq!(line.as_string(), "# Fourty-two") + } + + #[test] + fn test_variable() { + let line = Variable { + name: "ANSWER".to_string(), + input: "42".to_string(), + }; + assert_eq!(line.as_string(), "ANSWER=42") + } + + #[test] + fn test_empty_variable_with_default_value() { + let line = VariableWithDefaultValue { + name: "ANSWER".to_string(), + default: "42".to_string(), + input: None, + }; + assert_eq!(line.as_string(), "ANSWER=42") + } + + #[test] + fn test_variable_with_default_value() { + let line = VariableWithDefaultValue { + name: "ANSWER".to_string(), + default: "42".to_string(), + input: Some("Fourty-two".to_string()), + }; + assert_eq!(line.as_string(), "ANSWER=Fourty-two") + } + + #[test] + fn test_auto_generated_variable() { + let mut settings = HashMap::new(); + settings.insert("first", "Fourty"); + settings.insert("second", "two"); + let line = AutoGeneratedVariable { + name: "ANSWER".to_string(), + pattern: "{first}-{second}".to_string(), + settings, + }; + assert_eq!(line.as_string(), "ANSWER=Fourty-two") + } + + #[test] + fn test_variable_with_random_value_of_fixed_length() { + let line = VariableWithRandomValue { + name: "ANSWER".to_string(), + length: Some(42), + }; + let got = line.as_string(); + let suffix = got.strip_prefix("ANSWER=").unwrap(); + assert_eq!(suffix.chars().count(), 42); + let prefix = got.strip_suffix(suffix).unwrap(); + assert_eq!(prefix, "ANSWER=") + } + + #[test] + fn test_variable_with_random_value() { + let line = VariableWithRandomValue { + name: "ANSWER".to_string(), + length: None, + }; + let got = line.as_string(); + let suffix = got.strip_prefix("ANSWER=").unwrap(); + assert!(suffix.chars().count() >= 64); + assert!(suffix.chars().count() <= 128); + let prefix = got.strip_suffix(suffix).unwrap(); + assert_eq!(prefix, "ANSWER=") + } +} + +fn main() { + println!("Hello, world!") +} From 2826506886581ac5e5a50ea1604dcd08680724c9 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Tue, 18 Oct 2022 11:01:47 -0400 Subject: [PATCH 02/28] Adds Rust test to the CI --- .github/workflows/tests.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb56edc..0a0efd1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ name: Tests on: [push, pull_request] jobs: - build: + test-py: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -17,3 +17,10 @@ jobs: python_version: 3.9 poetry_version: 1.1.12 args: run pytest + test-rs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Run tests + uses: icepuma/rust-action@master + run: cargo test From d7c9d774244e7fed8166803a4c96c23d1c50b54d Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 10:48:29 -0400 Subject: [PATCH 03/28] Renames struct: Title -> Comment --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5c667a8..a802cf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,11 @@ use std::collections::HashMap; const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; -struct Title { +struct Comment { contents: String, } -impl Title { +impl Comment { fn as_string(&self) -> String { format!("# {}", self.contents) } @@ -105,7 +105,7 @@ mod tests { #[test] fn test_title() { - let line = Title { + let line = Comment { contents: "Fourty-two".to_string(), }; assert_eq!(line.as_string(), "# Fourty-two") From 1d33931157f24692d009792ca8219c3bc0d4aa95 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 10:50:19 -0400 Subject: [PATCH 04/28] Renames struct: Variable -> SimpleVariable --- src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index a802cf5..63b8041 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ impl Comment { } } -trait VariableLine { +trait Variable { fn key(&self) -> String; fn value(&self) -> String; fn as_string(&self) -> String { @@ -22,12 +22,12 @@ trait VariableLine { } } -struct Variable { +struct SimpleVariable { name: String, input: String, } -impl VariableLine for Variable { +impl Variable for SimpleVariable { fn key(&self) -> String { self.name.to_string() } @@ -42,7 +42,7 @@ struct VariableWithDefaultValue { input: Option, } -impl VariableLine for VariableWithDefaultValue { +impl Variable for VariableWithDefaultValue { fn key(&self) -> String { self.name.to_string() } @@ -60,7 +60,7 @@ struct AutoGeneratedVariable { settings: HashMap<&'static str, &'static str>, } -impl VariableLine for AutoGeneratedVariable { +impl Variable for AutoGeneratedVariable { fn key(&self) -> String { self.name.to_string() } @@ -79,7 +79,7 @@ struct VariableWithRandomValue { length: Option, } -impl VariableLine for VariableWithRandomValue { +impl Variable for VariableWithRandomValue { fn key(&self) -> String { self.name.to_string() } @@ -113,7 +113,7 @@ mod tests { #[test] fn test_variable() { - let line = Variable { + let line = SimpleVariable { name: "ANSWER".to_string(), input: "42".to_string(), }; From bd5f75d1ec5c1e959b1b87bd1f63708be81cec25 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 10:50:49 -0400 Subject: [PATCH 05/28] Creates block struct --- src/main.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/main.rs b/src/main.rs index 63b8041..3b1dd44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,6 +99,26 @@ impl Variable for VariableWithRandomValue { } } +struct Block { + title: Comment, + description: Option, + variables: Vec>, +} + +impl Block { + fn as_string(&self) -> String { + let mut lines: Vec = vec![self.title.as_string()]; + match &self.description { + Some(desc) => lines.push(desc.as_string()), + None => (), + } + for variable in &self.variables { + lines.push(variable.as_string()); + } + lines.join("\n") + } +} + #[cfg(test)] mod tests { use super::*; @@ -179,6 +199,32 @@ mod tests { let prefix = got.strip_suffix(suffix).unwrap(); assert_eq!(prefix, "ANSWER=") } + + #[test] + fn test_block_with_description() { + let title = Comment { + contents: "42".to_string(), + }; + let description = Some(Comment { + contents: "Fourty-two".to_string(), + }); + let variable1 = Box::new(SimpleVariable { + name: "ANSWER".to_string(), + input: "42".to_string(), + }) as Box; + let variable2 = Box::new(SimpleVariable { + name: "AS_TEXT".to_string(), + input: "fourty two".to_string(), + }) as Box; + let variables = vec![variable1, variable2]; + let block = Block { + title, + description, + variables, + }; + let got = block.as_string(); + assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") + } } fn main() { From 8790d477f14f6d88786aa4bdb6f12d0b36565f9d Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 10:51:46 -0400 Subject: [PATCH 06/28] Renames impl: as_string -> to_string --- src/main.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3b1dd44..339efb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ struct Comment { } impl Comment { - fn as_string(&self) -> String { + fn to_string(&self) -> String { format!("# {}", self.contents) } } @@ -17,7 +17,7 @@ impl Comment { trait Variable { fn key(&self) -> String; fn value(&self) -> String; - fn as_string(&self) -> String { + fn to_string(&self) -> String { format!("{}={}", self.key(), self.value()) } } @@ -106,14 +106,14 @@ struct Block { } impl Block { - fn as_string(&self) -> String { - let mut lines: Vec = vec![self.title.as_string()]; + fn to_string(&self) -> String { + let mut lines: Vec = vec![self.title.to_string()]; match &self.description { - Some(desc) => lines.push(desc.as_string()), + Some(desc) => lines.push(desc.to_string()), None => (), } for variable in &self.variables { - lines.push(variable.as_string()); + lines.push(variable.to_string()); } lines.join("\n") } @@ -128,7 +128,7 @@ mod tests { let line = Comment { contents: "Fourty-two".to_string(), }; - assert_eq!(line.as_string(), "# Fourty-two") + assert_eq!(line.to_string(), "# Fourty-two") } #[test] @@ -137,7 +137,7 @@ mod tests { name: "ANSWER".to_string(), input: "42".to_string(), }; - assert_eq!(line.as_string(), "ANSWER=42") + assert_eq!(line.to_string(), "ANSWER=42") } #[test] @@ -147,7 +147,7 @@ mod tests { default: "42".to_string(), input: None, }; - assert_eq!(line.as_string(), "ANSWER=42") + assert_eq!(line.to_string(), "ANSWER=42") } #[test] @@ -157,7 +157,7 @@ mod tests { default: "42".to_string(), input: Some("Fourty-two".to_string()), }; - assert_eq!(line.as_string(), "ANSWER=Fourty-two") + assert_eq!(line.to_string(), "ANSWER=Fourty-two") } #[test] @@ -170,7 +170,7 @@ mod tests { pattern: "{first}-{second}".to_string(), settings, }; - assert_eq!(line.as_string(), "ANSWER=Fourty-two") + assert_eq!(line.to_string(), "ANSWER=Fourty-two") } #[test] @@ -179,7 +179,7 @@ mod tests { name: "ANSWER".to_string(), length: Some(42), }; - let got = line.as_string(); + let got = line.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert_eq!(suffix.chars().count(), 42); let prefix = got.strip_suffix(suffix).unwrap(); @@ -192,7 +192,7 @@ mod tests { name: "ANSWER".to_string(), length: None, }; - let got = line.as_string(); + let got = line.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert!(suffix.chars().count() >= 64); assert!(suffix.chars().count() <= 128); @@ -222,7 +222,7 @@ mod tests { description, variables, }; - let got = block.as_string(); + let got = block.to_string(); assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") } } From 997a707eeb49798e02fdcf54baf98e39b8c26469 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 11:22:41 -0400 Subject: [PATCH 07/28] Creates model module --- src/main.rs | 251 +++++---------------------------------------------- src/model.rs | 229 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 228 deletions(-) create mode 100644 src/model.rs diff --git a/src/main.rs b/src/main.rs index 339efb4..8e0791d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,232 +1,27 @@ -use rand::{thread_rng, Rng}; -use std::collections::HashMap; +use crate::model::{Block, Comment, SimpleVariable, Variable}; -const DEFAULT_RANDOM_CHARS: &str = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; - -struct Comment { - contents: String, -} - -impl Comment { - fn to_string(&self) -> String { - format!("# {}", self.contents) - } -} - -trait Variable { - fn key(&self) -> String; - fn value(&self) -> String; - fn to_string(&self) -> String { - format!("{}={}", self.key(), self.value()) - } -} - -struct SimpleVariable { - name: String, - input: String, -} - -impl Variable for SimpleVariable { - fn key(&self) -> String { - self.name.to_string() - } - fn value(&self) -> String { - self.input.to_string() - } -} - -struct VariableWithDefaultValue { - name: String, - default: String, - input: Option, -} - -impl Variable for VariableWithDefaultValue { - fn key(&self) -> String { - self.name.to_string() - } - fn value(&self) -> String { - match &self.input { - Some(value) => value.to_string(), - None => self.default.to_string(), - } - } -} - -struct AutoGeneratedVariable { - name: String, - pattern: String, - settings: HashMap<&'static str, &'static str>, -} - -impl Variable for AutoGeneratedVariable { - fn key(&self) -> String { - self.name.to_string() - } - fn value(&self) -> String { - let mut value: String = self.pattern.to_string(); - for (k, v) in self.settings.iter() { - let key = format!("{{{}}}", *k); - value = value.replace(&key, *v); - } - value - } -} - -struct VariableWithRandomValue { - name: String, - length: Option, -} - -impl Variable for VariableWithRandomValue { - fn key(&self) -> String { - self.name.to_string() - } - fn value(&self) -> String { - let mut rng = thread_rng(); - let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); - let mut value: String = String::from(""); - let length = match self.length { - Some(n) => n, - None => rng.gen_range(64..=128), - }; - for _ in 0..length { - let pos = rng.gen_range(0..max_chars_idx); - value.push(DEFAULT_RANDOM_CHARS.chars().nth(pos).unwrap()) - } - value - } -} - -struct Block { - title: Comment, - description: Option, - variables: Vec>, -} - -impl Block { - fn to_string(&self) -> String { - let mut lines: Vec = vec![self.title.to_string()]; - match &self.description { - Some(desc) => lines.push(desc.to_string()), - None => (), - } - for variable in &self.variables { - lines.push(variable.to_string()); - } - lines.join("\n") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_title() { - let line = Comment { - contents: "Fourty-two".to_string(), - }; - assert_eq!(line.to_string(), "# Fourty-two") - } - - #[test] - fn test_variable() { - let line = SimpleVariable { - name: "ANSWER".to_string(), - input: "42".to_string(), - }; - assert_eq!(line.to_string(), "ANSWER=42") - } - - #[test] - fn test_empty_variable_with_default_value() { - let line = VariableWithDefaultValue { - name: "ANSWER".to_string(), - default: "42".to_string(), - input: None, - }; - assert_eq!(line.to_string(), "ANSWER=42") - } - - #[test] - fn test_variable_with_default_value() { - let line = VariableWithDefaultValue { - name: "ANSWER".to_string(), - default: "42".to_string(), - input: Some("Fourty-two".to_string()), - }; - assert_eq!(line.to_string(), "ANSWER=Fourty-two") - } - - #[test] - fn test_auto_generated_variable() { - let mut settings = HashMap::new(); - settings.insert("first", "Fourty"); - settings.insert("second", "two"); - let line = AutoGeneratedVariable { - name: "ANSWER".to_string(), - pattern: "{first}-{second}".to_string(), - settings, - }; - assert_eq!(line.to_string(), "ANSWER=Fourty-two") - } - - #[test] - fn test_variable_with_random_value_of_fixed_length() { - let line = VariableWithRandomValue { - name: "ANSWER".to_string(), - length: Some(42), - }; - let got = line.to_string(); - let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert_eq!(suffix.chars().count(), 42); - let prefix = got.strip_suffix(suffix).unwrap(); - assert_eq!(prefix, "ANSWER=") - } - - #[test] - fn test_variable_with_random_value() { - let line = VariableWithRandomValue { - name: "ANSWER".to_string(), - length: None, - }; - let got = line.to_string(); - let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert!(suffix.chars().count() >= 64); - assert!(suffix.chars().count() <= 128); - let prefix = got.strip_suffix(suffix).unwrap(); - assert_eq!(prefix, "ANSWER=") - } - - #[test] - fn test_block_with_description() { - let title = Comment { - contents: "42".to_string(), - }; - let description = Some(Comment { - contents: "Fourty-two".to_string(), - }); - let variable1 = Box::new(SimpleVariable { - name: "ANSWER".to_string(), - input: "42".to_string(), - }) as Box; - let variable2 = Box::new(SimpleVariable { - name: "AS_TEXT".to_string(), - input: "fourty two".to_string(), - }) as Box; - let variables = vec![variable1, variable2]; - let block = Block { - title, - description, - variables, - }; - let got = block.to_string(); - assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") - } -} +mod model; fn main() { - println!("Hello, world!") + let title = Comment { + contents: "42".to_string(), + }; + let description = Some(Comment { + contents: "Fourty-two".to_string(), + }); + let variable1 = Box::new(SimpleVariable { + name: "ANSWER".to_string(), + input: "42".to_string(), + }) as Box; + let variable2 = Box::new(SimpleVariable { + name: "AS_TEXT".to_string(), + input: "fourty two".to_string(), + }) as Box; + let variables = vec![variable1, variable2]; + let block = Block { + title, + description, + variables, + }; + println!("{}", block) } diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..be1f556 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,229 @@ +use rand::{thread_rng, Rng}; +use std::collections::HashMap; +use std::fmt; + +const DEFAULT_RANDOM_CHARS: &str = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; + +pub struct Comment { + pub contents: String, +} + +impl fmt::Display for Comment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "# {}", self.contents) + } +} + +pub trait Variable { + fn key(&self) -> String; + fn value(&self) -> String; + fn to_string(&self) -> String { + format!("{}={}", self.key(), self.value()) + } +} + +pub struct SimpleVariable { + pub name: String, + pub input: String, +} + +impl Variable for SimpleVariable { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + self.input.to_string() + } +} + +pub struct VariableWithDefaultValue { + pub name: String, + pub default: String, + pub input: Option, +} + +impl Variable for VariableWithDefaultValue { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + match &self.input { + Some(value) => value.to_string(), + None => self.default.to_string(), + } + } +} + +pub struct AutoGeneratedVariable { + pub name: String, + pub pattern: String, + pub settings: HashMap<&'static str, &'static str>, +} + +impl Variable for AutoGeneratedVariable { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + let mut value: String = self.pattern.to_string(); + for (k, v) in self.settings.iter() { + let key = format!("{{{}}}", *k); + value = value.replace(&key, *v); + } + value + } +} + +pub struct VariableWithRandomValue { + pub name: String, + pub length: Option, +} + +impl Variable for VariableWithRandomValue { + fn key(&self) -> String { + self.name.to_string() + } + fn value(&self) -> String { + let mut rng = thread_rng(); + let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); + let mut value: String = String::from(""); + let length = match self.length { + Some(n) => n, + None => rng.gen_range(64..=128), + }; + for _ in 0..length { + let pos = rng.gen_range(0..max_chars_idx); + value.push(DEFAULT_RANDOM_CHARS.chars().nth(pos).unwrap()) + } + value + } +} + +pub struct Block { + pub title: Comment, + pub description: Option, + pub variables: Vec>, +} + +impl fmt::Display for Block { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut lines: Vec = vec![self.title.to_string()]; + match &self.description { + Some(desc) => lines.push(desc.to_string()), + None => (), + } + for variable in &self.variables { + lines.push(variable.to_string()); + } + write!(f, "{}", lines.join("\n")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_title() { + let line = Comment { + contents: "Fourty-two".to_string(), + }; + assert_eq!(line.to_string(), "# Fourty-two") + } + + #[test] + fn test_variable() { + let line = SimpleVariable { + name: "ANSWER".to_string(), + input: "42".to_string(), + }; + assert_eq!(line.to_string(), "ANSWER=42") + } + + #[test] + fn test_empty_variable_with_default_value() { + let line = VariableWithDefaultValue { + name: "ANSWER".to_string(), + default: "42".to_string(), + input: None, + }; + assert_eq!(line.to_string(), "ANSWER=42") + } + + #[test] + fn test_variable_with_default_value() { + let line = VariableWithDefaultValue { + name: "ANSWER".to_string(), + default: "42".to_string(), + input: Some("Fourty-two".to_string()), + }; + assert_eq!(line.to_string(), "ANSWER=Fourty-two") + } + + #[test] + fn test_auto_generated_variable() { + let mut settings = HashMap::new(); + settings.insert("first", "Fourty"); + settings.insert("second", "two"); + let line = AutoGeneratedVariable { + name: "ANSWER".to_string(), + pattern: "{first}-{second}".to_string(), + settings, + }; + assert_eq!(line.to_string(), "ANSWER=Fourty-two") + } + + #[test] + fn test_variable_with_random_value_of_fixed_length() { + let line = VariableWithRandomValue { + name: "ANSWER".to_string(), + length: Some(42), + }; + let got = line.to_string(); + let suffix = got.strip_prefix("ANSWER=").unwrap(); + assert_eq!(suffix.chars().count(), 42); + let prefix = got.strip_suffix(suffix).unwrap(); + assert_eq!(prefix, "ANSWER=") + } + + #[test] + fn test_variable_with_random_value() { + let line = VariableWithRandomValue { + name: "ANSWER".to_string(), + length: None, + }; + let got = line.to_string(); + let suffix = got.strip_prefix("ANSWER=").unwrap(); + assert!(suffix.chars().count() >= 64); + assert!(suffix.chars().count() <= 128); + let prefix = got.strip_suffix(suffix).unwrap(); + assert_eq!(prefix, "ANSWER=") + } + + #[test] + fn test_block_with_description() { + let title = Comment { + contents: "42".to_string(), + }; + let description = Some(Comment { + contents: "Fourty-two".to_string(), + }); + let variable1 = Box::new(SimpleVariable { + name: "ANSWER".to_string(), + input: "42".to_string(), + }) as Box; + let variable2 = Box::new(SimpleVariable { + name: "AS_TEXT".to_string(), + input: "fourty two".to_string(), + }) as Box; + let variables = vec![variable1, variable2]; + let block = Block { + title, + description, + variables, + }; + let got = block.to_string(); + assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") + } +} From b73f22f42d2936203d9c4c556eba2fe400805e59 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 22 Oct 2022 11:27:44 -0400 Subject: [PATCH 08/28] Fixes Rust test flow on CI --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a0efd1..bacb00b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,8 @@ jobs: test-rs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Run tests - uses: icepuma/rust-action@master - run: cargo test + uses: actions-rs/cargo@v1 + with: + command: test From 244be9867626cea456ef6281212938367ac72655 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:25:44 -0400 Subject: [PATCH 09/28] Adds context to generate AutoGeneratedVariable values --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/main.rs | 60 ++++++++++++++++++------ src/model.rs | 125 +++++++++++++++++++++++++++++++++++++++----------- src/parser.rs | 53 +++++++++++++++++++++ 5 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index 363237f..38ee7c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "cfg-if" version = "1.0.0" @@ -12,6 +18,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "createnv" version = "0.1.0" dependencies = [ + "anyhow", "rand", ] diff --git a/Cargo.toml b/Cargo.toml index cd51bd1..c4ebb1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.71" rand = "0.8.5" diff --git a/src/main.rs b/src/main.rs index 8e0791d..2b1d592 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,61 @@ -use crate::model::{Block, Comment, SimpleVariable, Variable}; +use std::env::args; +use std::io::{stdout, BufWriter}; + +use anyhow::Result; + +use crate::model::{ + AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableWithDefaultValue, + VariableWithRandomValue, Variables, +}; +use crate::parser::Parser; mod model; +mod parser; + +fn main() -> Result<()> { + if let Some(path) = args().nth(1) { + let out = BufWriter::new(stdout()); + let mut parser = Parser::new(out); + parser.parse(path)?; + return Ok(()); + } -fn main() { let title = Comment { contents: "42".to_string(), }; let description = Some(Comment { contents: "Fourty-two".to_string(), }); - let variable1 = Box::new(SimpleVariable { + let variable1 = Variables::Simple(SimpleVariable { name: "ANSWER".to_string(), input: "42".to_string(), - }) as Box; - let variable2 = Box::new(SimpleVariable { + }); + let variable2 = Variables::Simple(SimpleVariable { name: "AS_TEXT".to_string(), input: "fourty two".to_string(), - }) as Box; - let variables = vec![variable1, variable2]; - let block = Block { - title, - description, - variables, - }; - println!("{}", block) + }); + let variable3 = Variables::DefaultValue(VariableWithDefaultValue { + name: "DEFAULT_VALUE_ONE".to_string(), + input: "".to_string(), + default: "default".to_string(), + }); + let variable4 = Variables::DefaultValue(VariableWithDefaultValue { + name: "DEFAULT_VALUE_ONE".to_string(), + input: "custom".to_string(), + default: "default".to_string(), + }); + let variable5 = Variables::Random(VariableWithRandomValue { + name: "SECRET_KEY".to_string(), + length: None, + }); + let variable6 = Variables::AutoGenerated(AutoGeneratedVariable::new( + "AUTO_GENERATED".to_string(), + "{ANSWER}-{DEFAULT_VALUE_ONE}".to_string(), + )); + let variables = vec![ + variable1, variable2, variable3, variable4, variable5, variable6, + ]; + let block = Block::new(title, description, variables); + println!("{block}"); + Ok(()) } diff --git a/src/model.rs b/src/model.rs index be1f556..3230ae9 100644 --- a/src/model.rs +++ b/src/model.rs @@ -40,7 +40,7 @@ impl Variable for SimpleVariable { pub struct VariableWithDefaultValue { pub name: String, pub default: String, - pub input: Option, + pub input: String, } impl Variable for VariableWithDefaultValue { @@ -48,9 +48,10 @@ impl Variable for VariableWithDefaultValue { self.name.to_string() } fn value(&self) -> String { - match &self.input { - Some(value) => value.to_string(), - None => self.default.to_string(), + if self.input.is_empty() { + self.default.to_string() + } else { + self.input.to_string() } } } @@ -58,7 +59,24 @@ impl Variable for VariableWithDefaultValue { pub struct AutoGeneratedVariable { pub name: String, pub pattern: String, - pub settings: HashMap<&'static str, &'static str>, + + context: HashMap, +} + +impl AutoGeneratedVariable { + pub fn new(name: String, pattern: String) -> Self { + Self { + name, + pattern, + context: HashMap::new(), + } + } + + pub fn load_context(&mut self, ctx: &HashMap) { + for (k, v) in ctx.iter() { + self.context.insert(k.to_string(), v.to_string()); + } + } } impl Variable for AutoGeneratedVariable { @@ -67,9 +85,9 @@ impl Variable for AutoGeneratedVariable { } fn value(&self) -> String { let mut value: String = self.pattern.to_string(); - for (k, v) in self.settings.iter() { + for (k, v) in self.context.iter() { let key = format!("{{{}}}", *k); - value = value.replace(&key, *v); + value = value.replace(&key, v); } value } @@ -100,10 +118,54 @@ impl Variable for VariableWithRandomValue { } } +pub enum Variables { + Simple(SimpleVariable), + DefaultValue(VariableWithDefaultValue), + AutoGenerated(AutoGeneratedVariable), + Random(VariableWithRandomValue), +} + pub struct Block { pub title: Comment, pub description: Option, - pub variables: Vec>, + pub variables: Vec, + + context: HashMap, +} + +impl Block { + pub fn new(title: Comment, description: Option, variables: Vec) -> Self { + let context: HashMap = HashMap::new(); + let has_auto_generated_variables = variables + .iter() + .any(|v| matches!(v, Variables::AutoGenerated(_))); + + let mut block = Self { + title, + description, + variables, + context, + }; + + if has_auto_generated_variables { + for variable in &block.variables { + match variable { + Variables::Simple(var) => block.context.insert(var.key(), var.value()), + Variables::DefaultValue(var) => block.context.insert(var.key(), var.value()), + Variables::AutoGenerated(_) => None, + Variables::Random(var) => block.context.insert(var.key(), var.value()), + }; + } + + for variable in &mut block.variables { + if let Variables::AutoGenerated(var) = variable { + var.load_context(&block.context); + } + } + } + + block + } } impl fmt::Display for Block { @@ -113,9 +175,16 @@ impl fmt::Display for Block { Some(desc) => lines.push(desc.to_string()), None => (), } + for variable in &self.variables { - lines.push(variable.to_string()); + match variable { + Variables::Simple(var) => lines.push(var.to_string()), + Variables::DefaultValue(var) => lines.push(var.to_string()), + Variables::AutoGenerated(var) => lines.push(var.to_string()), + Variables::Random(var) => lines.push(var.to_string()), + } } + write!(f, "{}", lines.join("\n")) } } @@ -146,7 +215,7 @@ mod tests { let line = VariableWithDefaultValue { name: "ANSWER".to_string(), default: "42".to_string(), - input: None, + input: "".to_string(), }; assert_eq!(line.to_string(), "ANSWER=42") } @@ -156,21 +225,27 @@ mod tests { let line = VariableWithDefaultValue { name: "ANSWER".to_string(), default: "42".to_string(), - input: Some("Fourty-two".to_string()), + input: "Fourty-two".to_string(), }; assert_eq!(line.to_string(), "ANSWER=Fourty-two") } #[test] fn test_auto_generated_variable() { - let mut settings = HashMap::new(); - settings.insert("first", "Fourty"); - settings.insert("second", "two"); - let line = AutoGeneratedVariable { - name: "ANSWER".to_string(), - pattern: "{first}-{second}".to_string(), - settings, + let mut line = + AutoGeneratedVariable::new("ANSWER".to_string(), "{first}-{second}".to_string()); + let first = SimpleVariable { + name: "first".to_string(), + input: "Fourty".to_string(), + }; + let second = SimpleVariable { + name: "second".to_string(), + input: "two".to_string(), }; + let mut ctx = HashMap::new(); + ctx.insert(first.key(), first.value()); + ctx.insert(second.key(), second.value()); + line.load_context(&ctx); assert_eq!(line.to_string(), "ANSWER=Fourty-two") } @@ -209,20 +284,16 @@ mod tests { let description = Some(Comment { contents: "Fourty-two".to_string(), }); - let variable1 = Box::new(SimpleVariable { + let variable1 = Variables::Simple(SimpleVariable { name: "ANSWER".to_string(), input: "42".to_string(), - }) as Box; - let variable2 = Box::new(SimpleVariable { + }); + let variable2 = Variables::Simple(SimpleVariable { name: "AS_TEXT".to_string(), input: "fourty two".to_string(), - }) as Box; + }); let variables = vec![variable1, variable2]; - let block = Block { - title, - description, - variables, - }; + let block = Block::new(title, description, variables); let got = block.to_string(); assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") } diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..b863772 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,53 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Stdout, Write}; + +use anyhow::Result; + +use crate::model::Block; + +pub struct Parser { + writer: BufWriter, + + buffer: Option, + block: Option, + + written: bool, +} + +impl Parser { + pub fn new(writer: BufWriter) -> Self { + Self { + writer, + buffer: None, + block: None, + written: false, + } + } + + fn parse_line(&mut self, line: String) -> Result<()> { + self.buffer = Some(line); + println!("{}", self.buffer.as_ref().unwrap_or(&"".to_string())); + Ok(()) + } + + pub fn parse(&mut self, path: String) -> Result<()> { + let file = File::open(path)?; + let reader = BufReader::new(file); + for line in reader.lines() { + self.parse_line(line?)?; + } + self.save_block()?; + Ok(()) + } + + fn save_block(&mut self) -> Result<()> { + if let Some(block) = &self.block { + if self.written { + write!(self.writer, "\n\n")?; + } + write!(self.writer, "{block}")?; + self.block = None; + } + Ok(()) + } +} From 4f74b4fb8cbb63a85c43d29d13cf831e8a393d7e Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:42:43 -0400 Subject: [PATCH 10/28] Removes Python code --- .github/workflows/tests.yml | 18 +- .gitignore | 8 - Makefile | 10 - createnv/__init__.py | 9 - createnv/cli.py | 32 - createnv/config.py | 84 --- createnv/echo.py | 0 createnv/generator.py | 50 -- createnv/parser.py | 194 ------ poetry.lock | 877 --------------------------- pyproject.toml | 36 -- setup.cfg | 19 - tests/.env.sample | 21 - tests/__init__.py | 0 tests/test_auto_config_class.py | 7 - tests/test_block_class.py | 32 - tests/test_cli_module.py | 28 - tests/test_config_class.py | 38 -- tests/test_echo_method.py | 0 tests/test_generator_class.py | 119 ---- tests/test_group_class.py | 69 --- tests/test_line_class.py | 21 - tests/test_parser_class.py | 162 ----- tests/test_parser_error_exception.py | 18 - tests/test_random_config_class.py | 26 - 25 files changed, 5 insertions(+), 1873 deletions(-) delete mode 100644 Makefile delete mode 100644 createnv/__init__.py delete mode 100644 createnv/cli.py delete mode 100644 createnv/config.py delete mode 100644 createnv/echo.py delete mode 100644 createnv/generator.py delete mode 100644 createnv/parser.py delete mode 100644 poetry.lock delete mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 tests/.env.sample delete mode 100644 tests/__init__.py delete mode 100644 tests/test_auto_config_class.py delete mode 100644 tests/test_block_class.py delete mode 100644 tests/test_cli_module.py delete mode 100644 tests/test_config_class.py delete mode 100644 tests/test_echo_method.py delete mode 100644 tests/test_generator_class.py delete mode 100644 tests/test_group_class.py delete mode 100644 tests/test_line_class.py delete mode 100644 tests/test_parser_class.py delete mode 100644 tests/test_parser_error_exception.py delete mode 100644 tests/test_random_config_class.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bacb00b..607097f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,23 +1,15 @@ name: Tests on: [push, pull_request] jobs: - test-py: + clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Install dependencies - uses: abatilo/actions-poetry@v1.5.0 - with: - python_version: 3.9 - poetry_version: 1.1.12 - args: install + - uses: actions/checkout@v2 - name: Run tests - uses: abatilo/actions-poetry@v1.5.0 + uses: actions-rs/cargo@v1 with: - python_version: 3.9 - poetry_version: 1.1.12 - args: run pytest - test-rs: + command: clippy + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 7be540d..b7f7b51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,2 @@ -*.pyc -.coverage .env -.mypy_cache/ -.tox/ -__pycache__/ -createnv.egg-info/ -dist/ -htmlcov/ target/ diff --git a/Makefile b/Makefile deleted file mode 100644 index 2bea9bd..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -clean: - @rm -rf .coverage - @rm -rf .mypy_cache - @rm -rf .pytest_cache - @rm -rf .ropeproject - @rm -rf createnv.egg-info - @rm -rf dist - @rm -rf htmlcov - @find . -iname "*.pyc" | xargs rm - @find . -iname "__pycache__" | xargs rm -rf diff --git a/createnv/__init__.py b/createnv/__init__.py deleted file mode 100644 index c5a0a0a..0000000 --- a/createnv/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from functools import partial - -import typer - - -echo = typer.echo -error = partial(typer.secho, fg="red") -success = partial(typer.secho, fg="green") -warning = partial(typer.secho, fg="yellow") diff --git a/createnv/cli.py b/createnv/cli.py deleted file mode 100644 index b6a7fdc..0000000 --- a/createnv/cli.py +++ /dev/null @@ -1,32 +0,0 @@ -from pathlib import Path -from string import ascii_letters, digits - -from typer import run - -from createnv.generator import Generator -from createnv.parser import Parser - - -RANDOM_CHARS = f"{ascii_letters}{digits}!@#$%^&*(-_=+)" - - -def main( - target: str = ".env", - source: str = ".env.sample", - overwrite: bool = False, - use_default: bool = False, - chars_for_random_string: str = RANDOM_CHARS, -): - """Creates a .env file with the environment variables following a sample - .env.sample file. These defaults and other options can be changed. Check - them with the --help option.""" - Generator( - Path(target), - Parser(Path(source), chars_for_random_string), - overwrite, - use_default, - )() - - -def cli(): - run(main) diff --git a/createnv/config.py b/createnv/config.py deleted file mode 100644 index c7e522c..0000000 --- a/createnv/config.py +++ /dev/null @@ -1,84 +0,0 @@ -from dataclasses import dataclass -from random import choice, randint -from typing import Iterable, Mapping, Optional, Union - -from typer import prompt - - -@dataclass -class Config: - name: str - human_name: Optional[str] = None - default: Optional[str] = None - - def __str__(self) -> str: - return self.human_name or self.name - - def __call__(self, use_default: bool = False) -> str: - if self.default and use_default: - return self.default - return prompt(str(self), default=self.default) - - -@dataclass -class RandomConfig: - name: str - allowed_chars: str - human_name: Optional[str] = None - length: Optional[int] = None - - def __str__(self) -> str: - return self.human_name or self.name - - @property - def default(self) -> str: - length = self.length or randint(64, 128) - chars = (choice(self.allowed_chars) for _ in range(length)) - return "".join(chars) - - def __call__(self, use_default: bool = False) -> str: - if use_default: - return self.default - - return prompt(str(self), default=self.default) - - -@dataclass -class AutoConfig: - """Config generated within a Group, using other Config values. The `value` - format method is called used `arguments`, so it expects the curly-braces - syntax with ordered arguments.""" - - name: str - value: str - - def __call__(self, settings: Mapping[str, str]) -> Mapping[str, str]: - value = self.value.format(**settings) - return {self.name: value} - - -@dataclass -class Group: - title: str - configs: Iterable[Union[Config, RandomConfig]] - description: Optional[str] = None - auto_config: Optional[AutoConfig] = None - - def __str__(self): - contents = ("", self.title, f"({self.description})") - return "\n".join(contents if self.description else contents[:-1]) - - def should_echo(self, use_default: bool = False) -> bool: - if not use_default: - return True - - return not all(c.default for c in self.configs) - - def __call__(self, use_default: bool = False) -> Mapping[str, str]: - settings = {c.name: c(use_default) for c in self.configs} - - if self.auto_config: - auto_settings = self.auto_config(settings) - settings.update(auto_settings) - - return settings diff --git a/createnv/echo.py b/createnv/echo.py deleted file mode 100644 index e69de29..0000000 diff --git a/createnv/generator.py b/createnv/generator.py deleted file mode 100644 index 677292e..0000000 --- a/createnv/generator.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, Iterator - -from typer import confirm - -from createnv import echo, error, success, warning -from createnv.config import Group -from createnv.parser import Parser, ParserError - - -@dataclass -class Generator: - path: Path - parser: Parser - overwrite: bool - use_default: bool - - def can_write_to_path(self) -> bool: - if self.overwrite or not self.path.exists(): - return True - - warning(f"There is an existing {self.path.name} file.") - return confirm("Do you want to overwrite it?") - - def contents(self, settings: Iterable[Group]) -> Iterator[str]: - for group in settings: - if group.should_echo(self.use_default): - echo(str(group)) - - values = group(self.use_default) - yield f"# {group.title}" - if group.description: - yield f"# {group.description}" - yield from (f"{key}={value}" for key, value in values.items()) - yield "" - - def __call__(self) -> None: - try: - settings = self.parser() - except ParserError as parser_error: - error(str(parser_error)) - return - - if not self.can_write_to_path(): - return - - contents = "\n".join(self.contents(settings)) - self.path.write_text(contents) - success(f"{self.path.name} created!") diff --git a/createnv/parser.py b/createnv/parser.py deleted file mode 100644 index b6dfdb0..0000000 --- a/createnv/parser.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from re import match, search -from typing import Iterable, Iterator, List, Optional, Union - -from createnv.config import AutoConfig, Config, Group, RandomConfig - - -@dataclass -class Line: - number: int - contents: str - - def cleaned(self) -> str: - return self.contents.strip() - - def is_empty(self) -> bool: - return not bool(self.cleaned()) - - def is_comment(self) -> bool: - return bool(self.cleaned().startswith("#")) - - -@dataclass -class Block: - lines: List[Line] = field(default_factory=list) - - def __add__(self, line: Line) -> Block: - self.lines.append(line) - return self - - def __iter__(self) -> Iterator[Line]: - yield from self.lines - - def is_empty(self) -> bool: - return not bool(self.lines) - - -class ParserError(Exception): - def __init__(self, *args, **kwargs): - if isinstance(args[0], Line): - line, text, *new_args = args - return super().__init__(self.message(line, text), *new_args, **kwargs) - return super().__init__(*args, **kwargs) - - @staticmethod - def message(line: Line, text: str) -> str: - message = ( - "", - f"==> Parsing error at line {line.number}:", - f" {text}", - "", - f" The content of the line {line.number} is:", - f" {line.contents}", - "", - ) - return "\n".join(message) - - -@dataclass -class Parser: - source: Path - chars_for_random_string: str - - TITLE: str = r"^# (?P.+)$" - CONFIG: str = r"^(?P<name>[A-Z_0-9]+)=(?P<value>.+)?" - AUTO_CONFIG_VALUE: str = r"{[A-Z_0-0]+}" - RANDOM_VALUE: str = r"<random(:(?P<length>\d+))?>" - INLINE_COMMENT: str = " # " - - def blocks(self) -> Iterator[Block]: - block = Block() - for values in enumerate(self.source.open(), 1): - line = Line(*values) - if line.is_empty(): - if block.is_empty(): - continue - - yield block - block = Block() - - else: - block += line - - if not block.is_empty(): - yield block - - def parse_title(self, line: Line) -> str: - matches = match(self.TITLE, line.cleaned()) - if not matches: - message = ( - f"This is the first line of a block in {self.source}. A block " - "is a group of lines separated from others by one (or more) " - "empty line(s). The first line of a block is expected to be a " - "title, that is to say, to start with `# `, the remaining " - "text is considered the title of this block. This lines " - "does not match this pattern." - ) - raise ParserError(line, message) - - return matches.group("title") - - def parse_config(self, line: Line) -> Union[Config, AutoConfig, RandomConfig]: - matches = match(self.CONFIG, line.cleaned()) - if not matches: - message = ( - "This line was expected to be a config variable. The format " - "should be a name using capital ASCII letters, digits or " - "underscore, followed by an equal sign. This line does not " - "match this expected pattern." - ) - raise ParserError(line, message) - - name, value, human = matches.group("name"), matches.group("value"), None - if value is not None and self.INLINE_COMMENT in value: - value, human = value.rsplit(self.INLINE_COMMENT, maxsplit=1) - - if search(self.AUTO_CONFIG_VALUE, value or ""): - return AutoConfig(name, value) - - random_match = match(self.RANDOM_VALUE, value or "") - if random_match: - length = random_match.group("length") - return RandomConfig( - name=name, - allowed_chars=self.chars_for_random_string, - human_name=human or None, - length=int(length) if length else None, - ) - - return Config(name, human or None, value or None) - - def parse_description_or_config( - self, line: Line - ) -> Union[str, Config, AutoConfig, RandomConfig]: - method = self.parse_title if line.is_comment() else self.parse_config - try: - result = method(line) - except ParserError: - message = ( - f"This is the second line of a block in {self.source}. A " - "block is a group of lines separated from others by one (or " - "more) empty line(s). The second line of a block is expected " - "to be a description of that block or a config variable. The " - "description line should start `# `, and the remaining text " - "is considered the description of this block. A config " - "variable line should start with a name in uppercase, no " - "spaces, followed by an equal sign. This lines does not match " - "this expected patterns." - ) - raise ParserError(line, message) - - return result - - def parse(self, block: Block) -> Group: - _title, _description_or_config, *_configs = block - - title: str = self.parse_title(_title) - configs: List[Union[Config, RandomConfig]] = [] - description: Optional[str] = None - auto_config: Optional[AutoConfig] = None - - parsed = self.parse_description_or_config(_description_or_config) - if isinstance(parsed, str): - description = parsed - elif isinstance(parsed, AutoConfig): - auto_config = parsed - else: - configs.append(parsed) - - for line in _configs: - parsed = self.parse_config(line) - if isinstance(parsed, (Config, RandomConfig)): - configs.append(parsed) - elif isinstance(parsed, AutoConfig) and not auto_config: - auto_config = parsed - - return Group( - title=title, - configs=configs, - description=description, - auto_config=auto_config, - ) - - def __call__(self) -> Iterable[Group]: - if not self.source.exists(): - raise ParserError(f"{self.source} does not exist.") - - if not self.source.is_file(): - raise ParserError(f"{self.source} is not a file.") - - return tuple(self.parse(block) for block in self.blocks()) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index bbadd01..0000000 --- a/poetry.lock +++ /dev/null @@ -1,877 +0,0 @@ -[[package]] -name = "appnope" -version = "0.1.2" -description = "Disable App Nap on macOS >= 10.9" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "21.4.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] - -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "black" -version = "22.1.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.0.4" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "coverage" -version = "6.3.2" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "filelock" -version = "3.6.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "ipdb" -version = "0.13.9" -description = "IPython-enabled pdb" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -decorator = {version = "*", markers = "python_version > \"3.6\""} -ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} -toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} - -[[package]] -name = "ipython" -version = "7.32.0" -description = "IPython: Productive Interactive Computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" -pygments = "*" -traitlets = ">=4.2" - -[package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] -doc = ["Sphinx (>=1.3)"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["notebook", "ipywidgets"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] - -[[package]] -name = "jedi" -version = "0.18.1" -description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -parso = ">=0.8.0,<0.9.0" - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.3" -description = "Inline Matplotlib backend for Jupyter" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "more-itertools" -version = "8.12.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "mypy" -version = "0.931" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "parso" -version = "0.8.3" -description = "A Python Parser" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "platformdirs" -version = "2.5.1" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] - -[[package]] -name = "pluggy" -version = "0.13.1" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.28" -description = "Library for building powerful interactive command lines in Python" -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pygments" -version = "2.11.2" -description = "Pygments is a syntax highlighting package written in Python." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "5.4.3" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" - -[package.extras] -checkqa-mypy = ["mypy (==v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -name = "pytest-black" -version = "0.3.12" -description = "A pytest plugin to enable format checking with black" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -black = {version = "*", markers = "python_version >= \"3.6\""} -pytest = ">=3.5.0" -toml = "*" - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-flake8" -version = "1.0.7" -description = "pytest plugin to check FLAKE8 requirements" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -flake8 = ">=3.5" -pytest = ">=3.5" - -[[package]] -name = "pytest-mock" -version = "2.0.0" -description = "Thin-wrapper around the mock package for easier use with py.test" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pytest = ">=2.7" - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "pytest-mypy" -version = "0.5.0" -description = "Mypy static type checker plugin for Pytest" -category = "dev" -optional = false -python-versions = "~=3.4" - -[package.dependencies] -filelock = ">=3.0" -mypy = [ - {version = ">=0.500", markers = "python_version >= \"3.5\" and python_version < \"3.8\""}, - {version = ">=0.700", markers = "python_version >= \"3.8\""}, -] -pytest = {version = ">=3.5", markers = "python_version >= \"3.5\""} - -[[package]] -name = "shellingham" -version = "1.4.0" -description = "Tool to Detect Surrounding Shell" -category = "main" -optional = false -python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "traitlets" -version = "5.1.1" -description = "Traitlets Python configuration system" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "typed-ast" -version = "1.5.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typer" -version = "0.4.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -click = ">=7.1.1,<9.0.0" -colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} -shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] -test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"] - -[[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "zipp" -version = "3.7.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "69434ae97f4f0df68c5b00cd11ea54eb775c4299036ddf30fa4e03a6fb255f3c" - -[metadata.files] -appnope = [ - {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, - {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -backcall = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] -black = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, -] -click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] -filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -ipdb = [ - {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, -] -ipython = [ - {file = "ipython-7.32.0-py3-none-any.whl", hash = "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7"}, - {file = "ipython-7.32.0.tar.gz", hash = "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909"}, -] -jedi = [ - {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, - {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, -] -matplotlib-inline = [ - {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, - {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, - {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, -] -mypy = [ - {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, - {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, - {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, - {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, - {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, - {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, - {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, - {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, - {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, - {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, - {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, - {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, - {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, - {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, - {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, - {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, - {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, - {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, - {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, - {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -parso = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pexpect = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] -pickleshare = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] -platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"}, - {file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"}, -] -ptyprocess = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygments = [ - {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, - {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -pytest-black = [ - {file = "pytest-black-0.3.12.tar.gz", hash = "sha256:1d339b004f764d6cd0f06e690f6dd748df3d62e6fe1a692d6a5500ac2c5b75a5"}, -] -pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, -] -pytest-flake8 = [ - {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, - {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, -] -pytest-mock = [ - {file = "pytest-mock-2.0.0.tar.gz", hash = "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f"}, - {file = "pytest_mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307"}, -] -pytest-mypy = [ - {file = "pytest-mypy-0.5.0.tar.gz", hash = "sha256:14c746bd0db5e36618f2fda0ba61ddeb5dc52129ab3923a70f592f934c8887db"}, - {file = "pytest_mypy-0.5.0-py3-none-any.whl", hash = "sha256:6d47b786e460c5101423fec8462e17ac1b6e9c497b1052e790b2e4850a8b3796"}, -] -shellingham = [ - {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"}, - {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -traitlets = [ - {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, - {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, -] -typed-ast = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, -] -typer = [ - {file = "typer-0.4.0-py3-none-any.whl", hash = "sha256:d81169725140423d072df464cad1ff25ee154ef381aaf5b8225352ea187ca338"}, - {file = "typer-0.4.0.tar.gz", hash = "sha256:63c3aeab0549750ffe40da79a1b524f60e08a2cbc3126c520ebf2eeaf507f5dd"}, -] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a3fedd7..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[tool.poetry] -name = "createnv" -version = "0.0.2" -description = "CLI to create .env files with environment variables." -authors = ["Eduardo Cuducos <cuducos@users.noreply.github.com>"] -license = "BSD-3-Clause" -readme = "README.md" -repository = "https://github.com/cuducos/createnv" -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Natural Language :: English", - "Topic :: Software Development", - "Topic :: Utilities", -] - -[tool.poetry.scripts] -createnv = "createnv.cli:cli" - -[tool.poetry.dependencies] -python = "^3.7" -typer = {extras = ["all"], version = "^0.4.0"} - -[tool.poetry.dev-dependencies] -ipdb = "^0.13.2" -ipython = "^7.13.0" -pytest = "^5.4.1" -pytest-black = "^0.3.8" -pytest-cov = "^2.8.1" -pytest-flake8 = "^1.0.4" -pytest-mock = "^2.0.0" -pytest-mypy = "^0.5.0" - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0dd8089..0000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[tool:pytest] -addopts = --black --mypy --flake8 --cov=createnv --cov-report term - -[flake8] -max-line-length = 88 - -[testenv] -deps = - pytest - pytest-black - pytest-cov - pytest-mock - pytest-mypy -commands = pytest - -[coverage:report] -exclude_lines = - pragma: no cover - if __name__ == .__main__.: diff --git a/tests/.env.sample b/tests/.env.sample deleted file mode 100644 index 8855cca..0000000 --- a/tests/.env.sample +++ /dev/null @@ -1,21 +0,0 @@ -# This is the title -# (Here comes details to make the interface more user-friendly) -MY_FIRST_VARIABLE= -MY_SECOND_VARIABLE=42 -MY_THIRD_VARIABLE=42 # My third variable - -# This block has no description -SHOULD_I_DO_THAT=False - -# This block uses the auto-config and the random features -NAME=Cuducos -PERIOD=morning -THIS_IS_NOT_USED_IN_AUTO_CONFIG=ok? -GREETINGS=Good {PERIOD}, {NAME}! -I_HAVE_A_SECRET=<random> -I_HAVE_A_PIN_SECRET=<random:4> - -# Test auto-config as first line -GREETINGS=Good {PERIOD}, {NAME}! -NAME=Cuducos -PERIOD=morning diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_auto_config_class.py b/tests/test_auto_config_class.py deleted file mode 100644 index 371172e..0000000 --- a/tests/test_auto_config_class.py +++ /dev/null @@ -1,7 +0,0 @@ -from createnv.config import AutoConfig - - -def test_call(): - settings = {"NAME": "Cuducos", "PERIOD": "morning"} - config = AutoConfig("GREETING", "Good {PERIOD}, {NAME}!") - assert config(settings) == {"GREETING": "Good morning, Cuducos!"} diff --git a/tests/test_block_class.py b/tests/test_block_class.py deleted file mode 100644 index d1e70d1..0000000 --- a/tests/test_block_class.py +++ /dev/null @@ -1,32 +0,0 @@ -from createnv.parser import Block, Line - - -def test_init(): - block = Block() - assert block.lines == [] - - -def test_add(): - line1, line2 = Line(1, "Hell yeah!"), Line(2, "This is awesome") - block = Block() - block + line1 - block += line2 - assert block.lines == [line1, line2] - - -def test_iter(): - lines = (Line(1, "Hell yeah!"), Line(2, "This is awesome")) - block = Block() - for line in lines: - block += line - assert tuple(block.lines) == lines - - -def test_is_empty(): - block = Block() - assert block.is_empty() - - lines = (Line(1, "Hell yeah!"), Line(2, "This is awesome")) - for line in lines: - block += line - assert not block.is_empty() diff --git a/tests/test_cli_module.py b/tests/test_cli_module.py deleted file mode 100644 index c72a618..0000000 --- a/tests/test_cli_module.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path - - -from createnv.cli import RANDOM_CHARS, cli, main - - -def test_main_with_default_values(mocker): - parser = mocker.patch("createnv.cli.Parser") - generator = mocker.patch("createnv.cli.Generator") - main() - parser.assert_called_once_with(Path(".env.sample"), RANDOM_CHARS) - generator.assert_called_once_with(Path(".env"), parser.return_value, False, False) - generator.return_value.assert_called_once_with() - - -def test_main_with_custom_values(mocker): - parser = mocker.patch("createnv.cli.Parser") - generator = mocker.patch("createnv.cli.Generator") - main("env", "sample", True, True, "ab") - parser.assert_called_once_with(Path("sample"), "ab") - generator.assert_called_once_with(Path("env"), parser.return_value, True, True) - generator.return_value.assert_called_once_with() - - -def test_cli(mocker): - run = mocker.patch("createnv.cli.run") - cli() - run.assert_called_once_with(main) diff --git a/tests/test_config_class.py b/tests/test_config_class.py deleted file mode 100644 index 501e5e3..0000000 --- a/tests/test_config_class.py +++ /dev/null @@ -1,38 +0,0 @@ -from createnv.config import Config - - -def test_str(): - human, bot = Config("DEBUG", "Debug mode"), Config("DEBUG") - assert str(human) == "Debug mode" - assert str(bot) == "DEBUG" - - -def test_call_with_default_using_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - config = Config("DEBUG", "Debug mode", "True") - assert config(True) == "True" - prompt.assert_not_called() - - -def test_call_with_default_but_not_using_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode", "True") - assert config() == "42" - prompt.assert_called_once_with(str(config), default="True") - - -def test_call_without_default_trying_to_use_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode") - assert config(True) == "42" - prompt.assert_called_once_with(str(config), default=None) - - -def test_call_without_default_without_trying_to_use_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode") - assert config() == "42" - prompt.assert_called_once_with(str(config), default=None) diff --git a/tests/test_echo_method.py b/tests/test_echo_method.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_generator_class.py b/tests/test_generator_class.py deleted file mode 100644 index e0526ff..0000000 --- a/tests/test_generator_class.py +++ /dev/null @@ -1,119 +0,0 @@ -from pathlib import Path - -from createnv.config import Config, Group -from createnv.generator import Generator -from createnv.parser import ParserError - - -def test_can_write_to_non_existent_path_without_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert generator.can_write_to_path() - - -def test_can_write_to_non_existent_path_with_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), True, False) - assert generator.can_write_to_path() - - -def test_can_write_to_existent_path_with_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), True, False) - assert generator.can_write_to_path() - - -def test_can_write_to_existent_path_manually_confirming_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = True - confirm = mocker.patch("createnv.generator.confirm") - confirm.return_value = True - warning = mocker.patch("createnv.generator.warning") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert generator.can_write_to_path() - warning.assert_called_once_with("There is an existing .env file.") - confirm.called_once_with("Do you want to overwrite it?") - - -def test_can_write_to_existent_path_without_manually_confirming_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = True - confirm = mocker.patch("createnv.generator.confirm") - confirm.return_value = False - warning = mocker.patch("createnv.generator.warning") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert not generator.can_write_to_path() - warning.assert_called_once_with("There is an existing .env file.") - confirm.assert_called_once_with("Do you want to overwrite it?") - - -def test_contents(mocker): - group = mocker.patch.object(Group, "__call__") - group.side_effect = ({"NAME": "Cuducos", "PERIOD": "morning"}, {"DEBUG": "True"}) - should_echo = mocker.patch.object(Group, "should_echo") - should_echo.side_effect = (True, False) - echo = mocker.patch("createnv.generator.echo") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - settings = ( - Group("Greeting", (Config("NAME"), Config("Period")), "Say hi!"), - Group("Environment", (Config("DEBUG"))), - ) - assert tuple(generator.contents(settings)) == ( - "# Greeting", - "# Say hi!", - "NAME=Cuducos", - "PERIOD=morning", - "", - "# Environment", - "DEBUG=True", - "", - ) - echo.assert_called_once_with("\nGreeting\n(Say hi!)") - - -def test_call_prints_errors_from_parser(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - parser = mocker.Mock() - parser.side_effect = ParserError("oops") - generator = Generator(Path(".env"), parser, False, False) - generator() - parser.assert_called_once_with() - error.assert_called_once_with("oops") - write_text.assert_not_called() - success.assert_not_called() - - -def test_call_stops_when_it_cannot_write_to_target(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - can_write_to_path = mocker.patch.object(Generator, "can_write_to_path") - parser = mocker.Mock() - can_write_to_path.return_value = False - generator = Generator(Path(".env"), parser, False, False) - generator() - parser.assert_called_once_with() - error.assert_not_called() - write_text.assert_not_called() - success.assert_not_called() - - -def test_call(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - can_write_to_path = mocker.patch.object(Generator, "can_write_to_path") - can_write_to_path.return_value = True - contents = mocker.patch.object(Generator, "contents") - contents.return_value = ("Hell yeah!", "This is awesome") - parser = mocker.Mock() - generator = Generator(Path(".env"), parser, False, False) - generator() - error.assert_not_called() - write_text.assert_called_once_with("Hell yeah!\nThis is awesome") - success.assert_called_once_with(".env created!") diff --git a/tests/test_group_class.py b/tests/test_group_class.py deleted file mode 100644 index 27b8d18..0000000 --- a/tests/test_group_class.py +++ /dev/null @@ -1,69 +0,0 @@ -from unittest.mock import call - -from createnv.config import AutoConfig, Config, Group - - -def test_complete_str(): - group = Group("Hell yeah!", (Config("DEBUG"),), "This is awesome!") - assert str(group) == "\nHell yeah!\n(This is awesome!)" - - -def test_str_without_description(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert str(group) == "\nHell yeah!" - - -def test_should_echo_without_default_but_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert group.should_echo(True) - - -def test_should_echo_without_default_and_not_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert group.should_echo() - - -def test_should_echo_with_default_and_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG", default="42"),)) - assert not group.should_echo(True) - - -def test_should_echo_with_default_and_not_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG", default="42"),)) - assert group.should_echo() - - -def test_call_without_auto_config_without_use_default(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("True", "localhost") - group = Group("Hell yeah!", (Config("DEBUG"), Config("ALLOWED_HOSTS"))) - assert group() == {"DEBUG": "True", "ALLOWED_HOSTS": "localhost"} - assert config.call_count == 2 - config.assert_has_calls((call(False), call(False))) - - -def test_call_without_auto_config_with_use_default(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("True", "localhost") - group = Group("Hell yeah!", (Config("DEBUG"), Config("ALLOWED_HOSTS"))) - assert group(True) == {"DEBUG": "True", "ALLOWED_HOSTS": "localhost"} - assert config.call_count == 2 - config.assert_has_calls((call(True), call(True))) - - -def test_call_with_auto_config(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("morning", "Cuducos") - auto_config = mocker.patch.object(AutoConfig, "__call__") - auto_config.return_value = {"GREETING": "Good morning, Cuducos!"} - group = Group( - "Hell yeah!", - (Config("PERIOD"), Config("NAME")), - "This is pretty cool.", - AutoConfig("GREETING", "Good {PERIOD}, {NAME}!"), - ) - assert group() == { - "NAME": "Cuducos", - "PERIOD": "morning", - "GREETING": "Good morning, Cuducos!", - } diff --git a/tests/test_line_class.py b/tests/test_line_class.py deleted file mode 100644 index e9d08d2..0000000 --- a/tests/test_line_class.py +++ /dev/null @@ -1,21 +0,0 @@ -from createnv.parser import Line - - -def test_cleaned(): - assert Line(42, " \tyay\n ").cleaned() == "yay" - - -def test_is_empty(): - assert Line(42, "").is_empty() - assert Line(42, " ").is_empty() - assert Line(42, "\t").is_empty() - assert Line(42, "\n\t").is_empty() - assert not Line(42, " \tyay\n ").is_empty() - - -def test_is_comment(): - assert Line(42, "# Hell yeah!").is_comment() - assert Line(42, "\t# Hell yeah!").is_comment() - assert Line(42, " # Hell yeah!").is_comment() - assert not Line(42, "").is_comment() - assert not Line(42, "NAME=CUDUCOS").is_comment() diff --git a/tests/test_parser_class.py b/tests/test_parser_class.py deleted file mode 100644 index a734e6b..0000000 --- a/tests/test_parser_class.py +++ /dev/null @@ -1,162 +0,0 @@ -from pathlib import Path - -import pytest # type: ignore - -from createnv.cli import RANDOM_CHARS -from createnv.config import AutoConfig, Config, Group, RandomConfig -from createnv.parser import Block, Line, Parser, ParserError - - -def test_block(mocker): - path_open = mocker.patch.object(Path, "open") - path_open.return_value = ( - "\t", - "# Title", - "# Description", - "VARIABLE=", - "", - "# Another title", - "ANOTHER_VARIABLE=", - "", - ) - expected = ( - Block([Line(2, "# Title"), Line(3, "# Description"), Line(4, "VARIABLE=")]), - Block([Line(6, "# Another title"), Line(7, "ANOTHER_VARIABLE=")]), - ) - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - assert tuple(parser.blocks()) == expected - - -def test_parse_title(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - assert parser.parse_title(Line(2, "# Title")) == "Title" - with pytest.raises(ParserError): - parser.parse_title(Line(2, "VARIABLE=")) - - -def test_parse_config_with_config_lines(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - values = ( - (Line(4, "VARIABLE="), Config("VARIABLE")), - (Line(4, "VARIABLE= # Variable"), Config("VARIABLE", "Variable")), - (Line(4, "VARIABLE=42"), Config("VARIABLE", default="42")), - (Line(4, "VARIABLE=42 # Variable"), Config("VARIABLE", "Variable", "42")), - ) - for line, expected in values: - assert parser.parse_config(line) == expected - - -def test_parse_config_with_auto_config_line(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - line = Line(4, "VARIABLE=Hello, {NAME}!") - assert parser.parse_config(line) == AutoConfig("VARIABLE", "Hello, {NAME}!") - - -def test_parse_config_with_random_config_lines(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - lines = (Line(4, "VARIABLE=<random>"), Line(4, "VARIABLE=<random:42> # Variable")) - expected = ( - RandomConfig("VARIABLE", parser.chars_for_random_string), - RandomConfig("VARIABLE", parser.chars_for_random_string, "Variable", length=42), - ) - for line, expected in zip(lines, expected): - assert parser.parse_config(line) == expected - - -def test_parse_config_with_invalid_line(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser.parse_config(Line(2, "# Title")) - - -def test_parse_description_or_config_with_description(mocker): - description = Line(3, "# Here comes a description") - parse_title = mocker.patch.object(Parser, "parse_title") - parse_config = mocker.patch.object(Parser, "parse_config") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - parser.parse_description_or_config(description) - parse_title.assert_called_once_with(description) - parse_config.asseert_not_called() - - -def test_parse_description_or_config_with_config(mocker): - config = Line(4, "VARIABLE=") - parse_title = mocker.patch.object(Parser, "parse_title") - parse_config = mocker.patch.object(Parser, "parse_config") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - parser.parse_description_or_config(config) - parse_title.assert_not_called() - parse_config.assert_called_once_with(config) - - -def test_parse_description_or_config_with_invalid_line(): - nothing = Line(1, "\t") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser.parse_description_or_config(nothing) - - -def test_call_raises_error_without_source(): - fixture = Path() / "tests" / ".env.sample.that.does.not.exist" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser() - - -def test_call_raises_error_if_source_is_a_directory(): - fixture = Path() / "tests" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser() - - -def test_parser(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - expected = ( - Group( - title="This is the title", - description="(Here comes details to make the interface more user-friendly)", - configs=[ - Config("MY_FIRST_VARIABLE"), - Config("MY_SECOND_VARIABLE", default="42"), - Config( - "MY_THIRD_VARIABLE", human_name="My third variable", default="42" - ), - ], - ), - Group( - title="This block has no description", - configs=[Config("SHOULD_I_DO_THAT", default="False")], - ), - Group( - title="This block uses the auto-config and the random features", - configs=[ - Config("NAME", default="Cuducos"), - Config("PERIOD", default="morning"), - Config("THIS_IS_NOT_USED_IN_AUTO_CONFIG", default="ok?"), - RandomConfig("I_HAVE_A_SECRET", RANDOM_CHARS), - RandomConfig("I_HAVE_A_PIN_SECRET", RANDOM_CHARS, length=4), - ], - auto_config=AutoConfig("GREETINGS", "Good {PERIOD}, {NAME}!"), - ), - Group( - title="Test auto-config as first line", - configs=[ - Config("NAME", default="Cuducos"), - Config("PERIOD", default="morning"), - ], - auto_config=AutoConfig("GREETINGS", "Good {PERIOD}, {NAME}!"), - ), - ) - result = parser() - assert result == expected diff --git a/tests/test_parser_error_exception.py b/tests/test_parser_error_exception.py deleted file mode 100644 index 21e04ac..0000000 --- a/tests/test_parser_error_exception.py +++ /dev/null @@ -1,18 +0,0 @@ -from createnv.parser import Line, ParserError - - -def test_regular_call(): - exception = ParserError("oops") - assert str(exception) == "oops" - - -def test_call_with_line(): - exception = ParserError(Line(42, "yay"), "oops") - assert str(exception) == ( - "\n" - "==> Parsing error at line 42:\n" - " oops\n" - "\n" - " The content of the line 42 is:\n" - " yay\n" - ) diff --git a/tests/test_random_config_class.py b/tests/test_random_config_class.py deleted file mode 100644 index cbaf4ac..0000000 --- a/tests/test_random_config_class.py +++ /dev/null @@ -1,26 +0,0 @@ -from createnv.config import RandomConfig - - -def test_str(): - assert str(RandomConfig("SECRET_KEY", "ab")) == "SECRET_KEY" - assert str(RandomConfig("SECRET_KEY", "ab", "Secret key")) == "Secret key" - - -def test_default(): - config = RandomConfig("SECRET_KEY", "ab", "Secret key", 2) - assert config.default in {"aa", "ab", "ba", "bb"} - - -def test_call_without_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = RandomConfig("SECRET_KEY", "a", "Secret key", 3) - assert config() == "42" - prompt.assert_called_once_with("Secret key", default="aaa") - - -def test_call_with_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - config = RandomConfig("SECRET_KEY", "a", "Secret key", 3) - assert config(True) == "aaa" - prompt.assert_not_called() From 7d6c77825bbf2343417978590c1264e90ea8ccb9 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:46:32 -0400 Subject: [PATCH 11/28] Updates GitHub Action checkout version --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 607097f..6058e0b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,15 +4,15 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests uses: actions-rs/cargo@v1 with: command: clippy - test: + tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests uses: actions-rs/cargo@v1 with: From 9dc420c72c92d76e149b9968dee8f711d5257d01 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:53:08 -0400 Subject: [PATCH 12/28] Replaces: actions-rs/cargo -> dtolnay/rust-toolchain --- .github/workflows/tests.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6058e0b..77bfe65 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,15 +5,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: clippy + - uses: dtolnay/rust-toolchain@stable + - run: cargo clippy tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test + - uses: dtolnay/rust-toolchain@stable + - run: cargo test From 8c4d8fa6418f366a4cad87fdef5f10b4848b445d Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:16:08 -0400 Subject: [PATCH 13/28] Adds parser to read input character by character --- src/main.rs | 62 +++++++----------- src/model.rs | 178 +++++++++++++++++++++++--------------------------- src/parser.rs | 113 ++++++++++++++++++++++---------- 3 files changed, 182 insertions(+), 171 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2b1d592..e68a72a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ use std::env::args; -use std::io::{stdout, BufWriter}; +use std::path::PathBuf; use anyhow::Result; use crate::model::{ - AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableWithDefaultValue, - VariableWithRandomValue, Variables, + AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableWithRandomValue, Variables, }; use crate::parser::Parser; @@ -14,46 +13,31 @@ mod parser; fn main() -> Result<()> { if let Some(path) = args().nth(1) { - let out = BufWriter::new(stdout()); - let mut parser = Parser::new(out); - parser.parse(path)?; + let mut parser = Parser::new(PathBuf::from(path))?; + parser.parse()?; return Ok(()); } - let title = Comment { - contents: "42".to_string(), - }; - let description = Some(Comment { - contents: "Fourty-two".to_string(), - }); - let variable1 = Variables::Simple(SimpleVariable { - name: "ANSWER".to_string(), - input: "42".to_string(), - }); - let variable2 = Variables::Simple(SimpleVariable { - name: "AS_TEXT".to_string(), - input: "fourty two".to_string(), - }); - let variable3 = Variables::DefaultValue(VariableWithDefaultValue { - name: "DEFAULT_VALUE_ONE".to_string(), - input: "".to_string(), - default: "default".to_string(), - }); - let variable4 = Variables::DefaultValue(VariableWithDefaultValue { - name: "DEFAULT_VALUE_ONE".to_string(), - input: "custom".to_string(), - default: "default".to_string(), - }); - let variable5 = Variables::Random(VariableWithRandomValue { - name: "SECRET_KEY".to_string(), - length: None, - }); - let variable6 = Variables::AutoGenerated(AutoGeneratedVariable::new( - "AUTO_GENERATED".to_string(), - "{ANSWER}-{DEFAULT_VALUE_ONE}".to_string(), - )); + let title = Comment::new("42"); + let description = Some(Comment::new("Fourty-two")); + + let mut variable1 = SimpleVariable::new("ANSWER", None, None); + variable1.user_input("42"); + let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); + variable2.user_input("fourty two"); + let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); + let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); + variable4.user_input("custom"); + let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); + let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); + let variables = vec![ - variable1, variable2, variable3, variable4, variable5, variable6, + Variables::Input(variable1), + Variables::Input(variable2), + Variables::Input(variable3), + Variables::Input(variable4), + Variables::Random(variable5), + Variables::AutoGenerated(variable6), ]; let block = Block::new(title, description, variables); println!("{block}"); diff --git a/src/model.rs b/src/model.rs index 3230ae9..5a58d44 100644 --- a/src/model.rs +++ b/src/model.rs @@ -9,6 +9,14 @@ pub struct Comment { pub contents: String, } +impl Comment { + pub fn new(contents: &str) -> Self { + Self { + contents: contents.to_string(), + } + } +} + impl fmt::Display for Comment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "# {}", self.contents) @@ -24,35 +32,42 @@ pub trait Variable { } pub struct SimpleVariable { - pub name: String, - pub input: String, + pub input: Option<String>, + + name: String, + default: Option<String>, + help: Option<String>, } -impl Variable for SimpleVariable { - fn key(&self) -> String { - self.name.to_string() +impl SimpleVariable { + pub fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { + Self { + name: name.to_string(), + default: default.map(|s| s.to_string()), + help: help.map(|s| s.to_string()), + input: None, + } } - fn value(&self) -> String { - self.input.to_string() + pub fn user_input(&mut self, input: &str) { + if let Some(help) = &self.help { + println!("{help}"); + } + self.input = Some(input.to_string()); } } -pub struct VariableWithDefaultValue { - pub name: String, - pub default: String, - pub input: String, -} - -impl Variable for VariableWithDefaultValue { +impl Variable for SimpleVariable { fn key(&self) -> String { - self.name.to_string() + self.name.clone() } fn value(&self) -> String { - if self.input.is_empty() { - self.default.to_string() - } else { - self.input.to_string() + if let Some(input) = &self.input { + return input.clone(); + } + if let Some(default) = &self.default { + return default.clone(); } + "".to_string() // TODO: error? } } @@ -64,10 +79,10 @@ pub struct AutoGeneratedVariable { } impl AutoGeneratedVariable { - pub fn new(name: String, pattern: String) -> Self { + pub fn new(name: &str, pattern: &str) -> Self { Self { - name, - pattern, + name: name.to_string(), + pattern: pattern.to_string(), context: HashMap::new(), } } @@ -81,10 +96,10 @@ impl AutoGeneratedVariable { impl Variable for AutoGeneratedVariable { fn key(&self) -> String { - self.name.to_string() + self.name.clone() } fn value(&self) -> String { - let mut value: String = self.pattern.to_string(); + let mut value: String = self.pattern.clone(); for (k, v) in self.context.iter() { let key = format!("{{{}}}", *k); value = value.replace(&key, v); @@ -98,9 +113,18 @@ pub struct VariableWithRandomValue { pub length: Option<i32>, } +impl VariableWithRandomValue { + pub fn new(name: &str, length: Option<i32>) -> Self { + Self { + name: name.to_string(), + length, + } + } +} + impl Variable for VariableWithRandomValue { fn key(&self) -> String { - self.name.to_string() + self.name.clone() } fn value(&self) -> String { let mut rng = thread_rng(); @@ -119,8 +143,7 @@ impl Variable for VariableWithRandomValue { } pub enum Variables { - Simple(SimpleVariable), - DefaultValue(VariableWithDefaultValue), + Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), Random(VariableWithRandomValue), } @@ -150,8 +173,7 @@ impl Block { if has_auto_generated_variables { for variable in &block.variables { match variable { - Variables::Simple(var) => block.context.insert(var.key(), var.value()), - Variables::DefaultValue(var) => block.context.insert(var.key(), var.value()), + Variables::Input(var) => block.context.insert(var.key(), var.value()), Variables::AutoGenerated(_) => None, Variables::Random(var) => block.context.insert(var.key(), var.value()), }; @@ -178,8 +200,7 @@ impl fmt::Display for Block { for variable in &self.variables { match variable { - Variables::Simple(var) => lines.push(var.to_string()), - Variables::DefaultValue(var) => lines.push(var.to_string()), + Variables::Input(var) => lines.push(var.to_string()), Variables::AutoGenerated(var) => lines.push(var.to_string()), Variables::Random(var) => lines.push(var.to_string()), } @@ -195,104 +216,69 @@ mod tests { #[test] fn test_title() { - let line = Comment { - contents: "Fourty-two".to_string(), - }; + let line = Comment::new("Fourty-two"); assert_eq!(line.to_string(), "# Fourty-two") } #[test] fn test_variable() { - let line = SimpleVariable { - name: "ANSWER".to_string(), - input: "42".to_string(), - }; - assert_eq!(line.to_string(), "ANSWER=42") + let mut var = SimpleVariable::new("ANSWER", None, None); + var.user_input("42"); + assert_eq!(var.to_string(), "ANSWER=42") } #[test] fn test_empty_variable_with_default_value() { - let line = VariableWithDefaultValue { - name: "ANSWER".to_string(), - default: "42".to_string(), - input: "".to_string(), - }; - assert_eq!(line.to_string(), "ANSWER=42") + let var = SimpleVariable::new("ANSWER", Some("42"), None); + assert_eq!(var.to_string(), "ANSWER=42") } #[test] - fn test_variable_with_default_value() { - let line = VariableWithDefaultValue { - name: "ANSWER".to_string(), - default: "42".to_string(), - input: "Fourty-two".to_string(), - }; - assert_eq!(line.to_string(), "ANSWER=Fourty-two") + fn test_variable_with_default_value_and_input() { + let mut var = SimpleVariable::new("ANSWER", Some("42"), None); + var.user_input("fourty two"); + assert_eq!(var.to_string(), "ANSWER=fourty two") } #[test] fn test_auto_generated_variable() { - let mut line = - AutoGeneratedVariable::new("ANSWER".to_string(), "{first}-{second}".to_string()); - let first = SimpleVariable { - name: "first".to_string(), - input: "Fourty".to_string(), - }; - let second = SimpleVariable { - name: "second".to_string(), - input: "two".to_string(), - }; + let mut var = AutoGeneratedVariable::new("ANSWER", "{FIRST} {SECOND}"); let mut ctx = HashMap::new(); - ctx.insert(first.key(), first.value()); - ctx.insert(second.key(), second.value()); - line.load_context(&ctx); - assert_eq!(line.to_string(), "ANSWER=Fourty-two") + ctx.insert("FIRST".to_string(), "Fourty".to_string()); + ctx.insert("SECOND".to_string(), "two".to_string()); + var.load_context(&ctx); + assert_eq!(var.to_string(), "ANSWER=Fourty two") } #[test] - fn test_variable_with_random_value_of_fixed_length() { - let line = VariableWithRandomValue { - name: "ANSWER".to_string(), - length: Some(42), - }; - let got = line.to_string(); + fn test_variable_with_random_value() { + let var = VariableWithRandomValue::new("ANSWER", None); + let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert_eq!(suffix.chars().count(), 42); + assert!(suffix.chars().count() >= 64); + assert!(suffix.chars().count() <= 128); let prefix = got.strip_suffix(suffix).unwrap(); assert_eq!(prefix, "ANSWER=") } #[test] - fn test_variable_with_random_value() { - let line = VariableWithRandomValue { - name: "ANSWER".to_string(), - length: None, - }; - let got = line.to_string(); + fn test_variable_with_random_value_of_fixed_length() { + let var = VariableWithRandomValue::new("ANSWER", Some(42)); + let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert!(suffix.chars().count() >= 64); - assert!(suffix.chars().count() <= 128); + assert_eq!(suffix.chars().count(), 42); let prefix = got.strip_suffix(suffix).unwrap(); assert_eq!(prefix, "ANSWER=") } #[test] fn test_block_with_description() { - let title = Comment { - contents: "42".to_string(), - }; - let description = Some(Comment { - contents: "Fourty-two".to_string(), - }); - let variable1 = Variables::Simple(SimpleVariable { - name: "ANSWER".to_string(), - input: "42".to_string(), - }); - let variable2 = Variables::Simple(SimpleVariable { - name: "AS_TEXT".to_string(), - input: "fourty two".to_string(), - }); - let variables = vec![variable1, variable2]; + let title = Comment::new("42"); + let description = Some(Comment::new("Fourty-two")); + let mut variable1 = SimpleVariable::new("ANSWER", None, None); + variable1.user_input("42"); + let variable2 = SimpleVariable::new("AS_TEXT", Some("fourty two"), None); + let variables = vec![Variables::Input(variable1), Variables::Input(variable2)]; let block = Block::new(title, description, variables); let got = block.to_string(); assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") diff --git a/src/parser.rs b/src/parser.rs index b863772..6882409 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,53 +1,94 @@ -use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Stdout, Write}; +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; use anyhow::Result; -use crate::model::Block; - -pub struct Parser { - writer: BufWriter<Stdout>, - - buffer: Option<String>, - block: Option<Block>, +enum CharType { + Char(char), + Eol, + Eof, +} - written: bool, +struct CharReader { + line: usize, + column: usize, + current_line: Option<String>, + reader: BufReader<File>, + done: bool, } -impl Parser { - pub fn new(writer: BufWriter<Stdout>) -> Self { - Self { - writer, - buffer: None, - block: None, - written: false, - } +impl CharReader { + fn new(path: PathBuf) -> Result<Self> { + Ok(Self { + line: 0, + column: 0, + current_line: None, + done: false, + reader: BufReader::new(File::open(path)?), + }) } - fn parse_line(&mut self, line: String) -> Result<()> { - self.buffer = Some(line); - println!("{}", self.buffer.as_ref().unwrap_or(&"".to_string())); - Ok(()) + fn next(&mut self) -> Result<CharType> { + if self.done { + return Ok(CharType::Eof); + } + match &self.current_line { + None => { + let mut buffer = "".to_string(); + let size = self.reader.read_line(&mut buffer)?; + if size == 0 { + self.done = true; + return Ok(CharType::Eof); + } + self.current_line = Some(buffer.clone()); + self.line += 1; + self.column = 0; + self.next() + } + Some(line) => match line.chars().nth(self.column) { + Some(char) => match char { + '\n' => { + self.current_line = None; + Ok(CharType::Eol) + } + _ => { + self.column += 1; + Ok(CharType::Char(char)) + } + }, + None => { + self.current_line = None; + Ok(CharType::Eol) + } + }, + } } +} - pub fn parse(&mut self, path: String) -> Result<()> { - let file = File::open(path)?; - let reader = BufReader::new(file); - for line in reader.lines() { - self.parse_line(line?)?; - } - self.save_block()?; - Ok(()) +pub struct Parser { + reader: CharReader, +} + +impl Parser { + pub fn new(path: PathBuf) -> Result<Self> { + Ok(Self { + reader: CharReader::new(path)?, + }) } - fn save_block(&mut self) -> Result<()> { - if let Some(block) = &self.block { - if self.written { - write!(self.writer, "\n\n")?; + pub fn parse(&mut self) -> Result<()> { + let mut copy = "".to_string(); + loop { + match self.reader.next()? { + CharType::Char(char) => copy.push(char), + CharType::Eol => copy += "\n", + CharType::Eof => break, } - write!(self.writer, "{block}")?; - self.block = None; } + println!("{}", copy); Ok(()) } } From 3712af53bf57fe37ecdcc40efdc03af587b4763a Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Wed, 20 Sep 2023 08:56:12 -0400 Subject: [PATCH 14/28] Implements a basic comment parser --- src/main.rs | 6 ++-- src/parser.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index e68a72a..66e0244 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,10 @@ mod parser; fn main() -> Result<()> { if let Some(path) = args().nth(1) { - let mut parser = Parser::new(PathBuf::from(path))?; - parser.parse()?; + let mut parser = Parser::new(PathBuf::from(&path))?; + for block in parser.parse()? { + println!("{block}"); + } return Ok(()); } diff --git a/src/parser.rs b/src/parser.rs index 6882409..66555a8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,17 +4,23 @@ use std::{ path::PathBuf, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; -enum CharType { +use crate::model::{Block, Comment}; + +#[derive(PartialEq, Eq)] +pub enum CharType { Char(char), Eol, Eof, } +const CAPITAL_ASCII_LETTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + struct CharReader { line: usize, column: usize, + path: String, current_line: Option<String>, reader: BufReader<File>, done: bool, @@ -25,12 +31,25 @@ impl CharReader { Ok(Self { line: 0, column: 0, + path: path.display().to_string(), current_line: None, done: false, reader: BufReader::new(File::open(path)?), }) } + fn error(&self, character: &CharType, details: Option<String>) -> anyhow::Error { + let prefix = format!("{}:{}:{}", self.path, self.line, self.column); + let extra = details.map_or("".to_string(), |msg| format!(": {msg}")); + let token = match &character { + CharType::Char(char) => format!("character `{char}`"), + CharType::Eol => "EOL (end of line)".to_string(), + CharType::Eof => "EOF (end of file)".to_string(), + }; + + anyhow!(format!("{prefix}: Unexpected {token}{extra}")) + } + fn next(&mut self) -> Result<CharType> { if self.done { return Ok(CharType::Eof); @@ -79,16 +98,75 @@ impl Parser { }) } - pub fn parse(&mut self) -> Result<()> { - let mut copy = "".to_string(); + fn check_line_start(&self, char: &CharType) -> Result<()> { + if self.reader.column != 1 { + return Ok(()); + } + + match char { + CharType::Eol => Ok(()), + CharType::Eof => Ok(()), + CharType::Char(c) => { + if *c == '#' || CAPITAL_ASCII_LETTERS.contains(*c) { + return Ok(()); + } + + let msg = "A line must start with a capital ASCII letter or `#`".to_string(); + Err(anyhow!(self.reader.error(char, Some(msg)))) + } + } + } + + pub fn parse_until( + &mut self, + target: Vec<CharType>, + avoid: Vec<CharType>, + column: Option<usize>, + ) -> Result<String> { + let mut buffer = "".to_string(); loop { - match self.reader.next()? { - CharType::Char(char) => copy.push(char), - CharType::Eol => copy += "\n", - CharType::Eof => break, + let char = self.reader.next()?; + self.check_line_start(&char)?; + if avoid.contains(&char) { + return Err(anyhow!(self.reader.error(&char, None))); + } + if target.contains(&char) { + if let Some(col) = column { + if self.reader.column != col { + let msg = format!("expected at column {col}"); + return Err(self.reader.error(&char, Some(msg))); + } + } + break; + } + if let CharType::Char(c) = char { + buffer.push(c); } } - println!("{}", copy); - Ok(()) + + Ok(buffer.to_string()) + } + + fn parse_comment(&mut self) -> Result<Comment> { + self.parse_until(vec![CharType::Char('#')], vec![CharType::Eol], Some(1))?; + self.parse_until(vec![CharType::Char(' ')], vec![CharType::Eol], Some(2))?; + + let name = self.parse_until(vec![CharType::Eol], vec![], None)?; + + Ok(Comment::new(name.as_str())) + } + + fn parse_block(&mut self) -> Result<Block> { + let title = self + .parse_comment() + .map_err(|e| anyhow!("A block must start with `# `: {e}"))?; + + Ok(Block::new(title, None, vec![])) + } + + pub fn parse(&mut self) -> Result<Vec<Block>> { + let block = self.parse_block()?; + let blocks = vec![block]; + Ok(blocks) } } From acd8a6641ee5c6195adcf79f4014eeb9ae5374a3 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Wed, 20 Sep 2023 08:56:30 -0400 Subject: [PATCH 15/28] Checks format on CI --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77bfe65..74e4d04 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,12 @@ name: Tests on: [push, pull_request] jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo fmt --check clippy: runs-on: ubuntu-latest steps: From 8c5381c1d8db3b5bcaa8a64f2c2c8a28fda1c299 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Wed, 20 Sep 2023 19:18:48 -0400 Subject: [PATCH 16/28] Adds basic tokenizer --- src/main.rs | 20 +++++++---- src/model.rs | 27 ++++++++------- src/parser.rs | 90 +++--------------------------------------------- src/reader.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++ src/tokenizer.rs | 72 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 104 deletions(-) create mode 100644 src/reader.rs create mode 100644 src/tokenizer.rs diff --git a/src/main.rs b/src/main.rs index 66e0244..5ee91bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,15 @@ use std::path::PathBuf; use anyhow::Result; use crate::model::{ - AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableWithRandomValue, Variables, + AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, VariableWithRandomValue, }; use crate::parser::Parser; +use crate::tokenizer::Tokenizer; mod model; mod parser; +mod reader; +mod tokenizer; fn main() -> Result<()> { if let Some(path) = args().nth(1) { @@ -17,6 +20,9 @@ fn main() -> Result<()> { for block in parser.parse()? { println!("{block}"); } + + let mut tokenizer = Tokenizer::new(PathBuf::from(&path))?; + println!("{:?}", tokenizer.tokenize()?); return Ok(()); } @@ -34,12 +40,12 @@ fn main() -> Result<()> { let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); let variables = vec![ - Variables::Input(variable1), - Variables::Input(variable2), - Variables::Input(variable3), - Variables::Input(variable4), - Variables::Random(variable5), - Variables::AutoGenerated(variable6), + VariableType::Input(variable1), + VariableType::Input(variable2), + VariableType::Input(variable3), + VariableType::Input(variable4), + VariableType::Random(variable5), + VariableType::AutoGenerated(variable6), ]; let block = Block::new(title, description, variables); println!("{block}"); diff --git a/src/model.rs b/src/model.rs index 5a58d44..0491a4d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -142,7 +142,7 @@ impl Variable for VariableWithRandomValue { } } -pub enum Variables { +pub enum VariableType { Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), Random(VariableWithRandomValue), @@ -151,17 +151,17 @@ pub enum Variables { pub struct Block { pub title: Comment, pub description: Option<Comment>, - pub variables: Vec<Variables>, + pub variables: Vec<VariableType>, context: HashMap<String, String>, } impl Block { - pub fn new(title: Comment, description: Option<Comment>, variables: Vec<Variables>) -> Self { + pub fn new(title: Comment, description: Option<Comment>, variables: Vec<VariableType>) -> Self { let context: HashMap<String, String> = HashMap::new(); let has_auto_generated_variables = variables .iter() - .any(|v| matches!(v, Variables::AutoGenerated(_))); + .any(|v| matches!(v, VariableType::AutoGenerated(_))); let mut block = Self { title, @@ -173,14 +173,14 @@ impl Block { if has_auto_generated_variables { for variable in &block.variables { match variable { - Variables::Input(var) => block.context.insert(var.key(), var.value()), - Variables::AutoGenerated(_) => None, - Variables::Random(var) => block.context.insert(var.key(), var.value()), + VariableType::Input(var) => block.context.insert(var.key(), var.value()), + VariableType::AutoGenerated(_) => None, + VariableType::Random(var) => block.context.insert(var.key(), var.value()), }; } for variable in &mut block.variables { - if let Variables::AutoGenerated(var) = variable { + if let VariableType::AutoGenerated(var) = variable { var.load_context(&block.context); } } @@ -200,9 +200,9 @@ impl fmt::Display for Block { for variable in &self.variables { match variable { - Variables::Input(var) => lines.push(var.to_string()), - Variables::AutoGenerated(var) => lines.push(var.to_string()), - Variables::Random(var) => lines.push(var.to_string()), + VariableType::Input(var) => lines.push(var.to_string()), + VariableType::AutoGenerated(var) => lines.push(var.to_string()), + VariableType::Random(var) => lines.push(var.to_string()), } } @@ -278,7 +278,10 @@ mod tests { let mut variable1 = SimpleVariable::new("ANSWER", None, None); variable1.user_input("42"); let variable2 = SimpleVariable::new("AS_TEXT", Some("fourty two"), None); - let variables = vec![Variables::Input(variable1), Variables::Input(variable2)]; + let variables = vec![ + VariableType::Input(variable1), + VariableType::Input(variable2), + ]; let block = Block::new(title, description, variables); let got = block.to_string(); assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") diff --git a/src/parser.rs b/src/parser.rs index 66555a8..b51e5ec 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,98 +1,18 @@ -use std::{ - fs::File, - io::{BufRead, BufReader}, - path::PathBuf, -}; - use anyhow::{anyhow, Result}; -use crate::model::{Block, Comment}; - -#[derive(PartialEq, Eq)] -pub enum CharType { - Char(char), - Eol, - Eof, -} +use crate::{ + model::{Block, Comment}, + reader::{CharReader, CharType}, +}; const CAPITAL_ASCII_LETTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -struct CharReader { - line: usize, - column: usize, - path: String, - current_line: Option<String>, - reader: BufReader<File>, - done: bool, -} - -impl CharReader { - fn new(path: PathBuf) -> Result<Self> { - Ok(Self { - line: 0, - column: 0, - path: path.display().to_string(), - current_line: None, - done: false, - reader: BufReader::new(File::open(path)?), - }) - } - - fn error(&self, character: &CharType, details: Option<String>) -> anyhow::Error { - let prefix = format!("{}:{}:{}", self.path, self.line, self.column); - let extra = details.map_or("".to_string(), |msg| format!(": {msg}")); - let token = match &character { - CharType::Char(char) => format!("character `{char}`"), - CharType::Eol => "EOL (end of line)".to_string(), - CharType::Eof => "EOF (end of file)".to_string(), - }; - - anyhow!(format!("{prefix}: Unexpected {token}{extra}")) - } - - fn next(&mut self) -> Result<CharType> { - if self.done { - return Ok(CharType::Eof); - } - match &self.current_line { - None => { - let mut buffer = "".to_string(); - let size = self.reader.read_line(&mut buffer)?; - if size == 0 { - self.done = true; - return Ok(CharType::Eof); - } - self.current_line = Some(buffer.clone()); - self.line += 1; - self.column = 0; - self.next() - } - Some(line) => match line.chars().nth(self.column) { - Some(char) => match char { - '\n' => { - self.current_line = None; - Ok(CharType::Eol) - } - _ => { - self.column += 1; - Ok(CharType::Char(char)) - } - }, - None => { - self.current_line = None; - Ok(CharType::Eol) - } - }, - } - } -} - pub struct Parser { reader: CharReader, } impl Parser { - pub fn new(path: PathBuf) -> Result<Self> { + pub fn new(path: std::path::PathBuf) -> Result<Self> { Ok(Self { reader: CharReader::new(path)?, }) diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 0000000..23fb99a --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,84 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; + +use anyhow::{anyhow, Result}; + +#[derive(PartialEq, Eq)] +pub enum CharType { + Char(char), + Eol, + Eof, +} + +pub struct CharReader { + pub line: usize, + pub column: usize, + path: String, + current_line: Option<String>, + reader: BufReader<File>, + done: bool, +} + +impl CharReader { + pub fn new(path: PathBuf) -> Result<Self> { + Ok(Self { + line: 0, + column: 0, + path: path.display().to_string(), + current_line: None, + done: false, + reader: BufReader::new(File::open(path)?), + }) + } + + pub fn error(&self, character: &CharType, details: Option<String>) -> anyhow::Error { + let prefix = format!("{}:{}:{}", self.path, self.line, self.column); + let extra = details.map_or("".to_string(), |msg| format!(": {msg}")); + let token = match &character { + CharType::Char(char) => format!("character `{char}`"), + CharType::Eol => "EOL (end of line)".to_string(), + CharType::Eof => "EOF (end of file)".to_string(), + }; + + anyhow!(format!("{prefix}: Unexpected {token}{extra}")) + } + + pub fn next(&mut self) -> Result<CharType> { + if self.done { + return Ok(CharType::Eof); + } + match &self.current_line { + None => { + let mut buffer = "".to_string(); + let size = self.reader.read_line(&mut buffer)?; + if size == 0 { + self.done = true; + return Ok(CharType::Eof); + } + self.current_line = Some(buffer.clone()); + self.line += 1; + self.column = 0; + self.next() + } + Some(line) => match line.chars().nth(self.column) { + Some(char) => match char { + '\n' => { + self.current_line = None; + Ok(CharType::Eol) + } + _ => { + self.column += 1; + Ok(CharType::Char(char)) + } + }, + None => { + self.current_line = None; + Ok(CharType::Eol) + } + }, + } + } +} diff --git a/src/tokenizer.rs b/src/tokenizer.rs new file mode 100644 index 0000000..12e189c --- /dev/null +++ b/src/tokenizer.rs @@ -0,0 +1,72 @@ +use std::path::PathBuf; + +use anyhow::Result; + +use crate::reader::{CharReader, CharType}; + +#[derive(Debug)] +pub enum Token { + Text(String), + CommentMark, + HelpMark, + EqualSign, +} + +pub struct Tokenizer { + reader: CharReader, +} + +impl Tokenizer { + pub fn new(path: PathBuf) -> Result<Self> { + Ok(Self { + reader: CharReader::new(path)?, + }) + } + + fn next_tokens(&mut self) -> Result<Vec<Token>> { + let mut buffer = "".to_string(); + loop { + let char = self.reader.next()?; + match char { + CharType::Eof => return Ok(vec![]), + CharType::Eol => { + if buffer.is_empty() { + continue; + } + return Ok(vec![Token::Text(buffer.trim().to_string())]); + } + CharType::Char(c) => { + let mut token: Option<Token> = None; + if c == '=' { + token = Some(Token::EqualSign); + } else if c == '#' && self.reader.column == 1 { + token = Some(Token::CommentMark); + } else if c == ' ' && buffer.ends_with(" #") { + buffer = buffer.strip_suffix(" #").unwrap_or("").to_string(); + token = Some(Token::HelpMark); + } + if let Some(t) = token { + if buffer.is_empty() { + return Ok(vec![t]); + } + return Ok(vec![Token::Text(buffer.trim().to_string()), t]); + } + buffer.push(c) + } + } + } + } + + // TODO: make iterator? + pub fn tokenize(&mut self) -> Result<Vec<Token>> { + let mut tokens: Vec<Token> = vec![]; + loop { + let new_tokens = self.next_tokens()?; + if new_tokens.is_empty() { + break; + } + tokens.extend(new_tokens); + } + Ok(tokens) + } +} From 284ca543e40d93231d2d3d592afdce7e8989dd10 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:57:01 -0400 Subject: [PATCH 17/28] Makes tokens aware of their line & column numbers --- src/main.rs | 5 +- src/tokenizer.rs | 128 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5ee91bd..7346551 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,10 @@ fn main() -> Result<()> { } let mut tokenizer = Tokenizer::new(PathBuf::from(&path))?; - println!("{:?}", tokenizer.tokenize()?); + let tokens = tokenizer.tokenize()?; + for token in &tokens { + println!("{}: {:?}", token.error_prefix(&path), token); + } return Ok(()); } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 12e189c..158f492 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -4,12 +4,25 @@ use anyhow::Result; use crate::reader::{CharReader, CharType}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Token { - Text(String), - CommentMark, - HelpMark, - EqualSign, + Text(usize, usize, String), + CommentMark(usize, usize), + HelpMark(usize, usize), + EqualSign(usize, usize), +} + +impl Token { + pub fn error_prefix(&self, path: &String) -> String { + let (line, column) = match self { + Token::Text(x, y, _) => (x, y), + Token::CommentMark(x, y) => (x, y), + Token::HelpMark(x, y) => (x, y), + Token::EqualSign(x, y) => (x, y), + }; + + format!("{path}:{line}:{column}") + } } pub struct Tokenizer { @@ -23,6 +36,30 @@ impl Tokenizer { }) } + fn text(&self, buffer: String, eol: bool, prepends_help: bool) -> Token { + let adjust = match (eol, prepends_help) { + (true, false) => -1, + (false, true) => 2, + _ => 0, + } + (buffer.len() as i8); + + Token::Text( + self.reader.line, + self.reader.column - (adjust as usize), + buffer.trim().to_string(), + ) + } + + fn equal_sign(&self) -> Token { + Token::EqualSign(self.reader.line, self.reader.column) + } + fn comment_mark(&self) -> Token { + Token::CommentMark(self.reader.line, self.reader.column) + } + fn help_mark(&self) -> Token { + Token::HelpMark(self.reader.line, self.reader.column - 2) + } + fn next_tokens(&mut self) -> Result<Vec<Token>> { let mut buffer = "".to_string(); loop { @@ -33,23 +70,27 @@ impl Tokenizer { if buffer.is_empty() { continue; } - return Ok(vec![Token::Text(buffer.trim().to_string())]); + return Ok(vec![self.text(buffer, true, false)]); } CharType::Char(c) => { let mut token: Option<Token> = None; + let mut prepends_help = false; if c == '=' { - token = Some(Token::EqualSign); - } else if c == '#' && self.reader.column == 1 { - token = Some(Token::CommentMark); - } else if c == ' ' && buffer.ends_with(" #") { - buffer = buffer.strip_suffix(" #").unwrap_or("").to_string(); - token = Some(Token::HelpMark); + token = Some(self.equal_sign()); + } else if c == '#' { + if self.reader.column == 1 { + token = Some(self.comment_mark()); + } else if buffer.ends_with(" ") { + buffer = buffer.strip_suffix(" ").unwrap_or("").to_string(); + prepends_help = true; + token = Some(self.help_mark()); + } } if let Some(t) = token { if buffer.is_empty() { return Ok(vec![t]); } - return Ok(vec![Token::Text(buffer.trim().to_string()), t]); + return Ok(vec![self.text(buffer, false, prepends_help), t]); } buffer.push(c) } @@ -70,3 +111,64 @@ impl Tokenizer { Ok(tokens) } } + +// TODO: move to tests/ as integration test? +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tokenizer() { + let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env.sample"); + let mut tokenizer = Tokenizer::new(sample).unwrap(); + let tokens = tokenizer.tokenize().unwrap(); + assert_eq!(tokens.len(), 19); + + // line 1 + assert_eq!(tokens[0], Token::CommentMark(1, 1)); + assert_eq!(tokens[1], Token::Text(1, 2, "Createnv".to_string())); + assert_eq!(tokens[2], Token::CommentMark(2, 1)); + + // line 2 + assert_eq!( + tokens[3], + Token::Text( + 2, + 2, + "This is a simple example of how Createnv works".to_string() + ) + ); + + // line 3 + assert_eq!(tokens[4], Token::Text(3, 1, "NAME".to_string())); + assert_eq!(tokens[5], Token::EqualSign(3, 5)); + assert_eq!(tokens[6], Token::HelpMark(3, 6)); + assert_eq!( + tokens[7], + Token::Text(3, 9, "What's your name?".to_string()) + ); + + // line 4 + assert_eq!(tokens[8], Token::Text(4, 1, "GREETING".to_string())); + assert_eq!(tokens[9], Token::EqualSign(4, 9)); + assert_eq!(tokens[10], Token::Text(4, 10, "Hello, {NAME}!".to_string())); + + // line 5 + assert_eq!( + tokens[11], + Token::Text(5, 1, "DO_YOU_LIKE_OPEN_SOURCE".to_string()) + ); + assert_eq!(tokens[12], Token::EqualSign(5, 24)); + assert_eq!(tokens[13], Token::Text(5, 25, "True".to_string())); + assert_eq!(tokens[14], Token::HelpMark(5, 29)); + assert_eq!( + tokens[15], + Token::Text(5, 32, "Do you like open-source?".to_string()) + ); + + // line 6 + assert_eq!(tokens[16], Token::Text(6, 1, "PASSWORD".to_string())); + assert_eq!(tokens[17], Token::EqualSign(6, 9)); + assert_eq!(tokens[18], Token::Text(6, 10, "<random:16>".to_string())); + } +} From b29ca328cdac0819188f6a4126c7f32728abe1a6 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:46:17 -0400 Subject: [PATCH 18/28] Re-organize the package using lib.rs --- .github/workflows/tests.yml | 4 +-- .rustfmt.toml | 2 ++ Cargo.lock | 2 +- Cargo.toml | 4 +-- src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++ src/main.rs | 49 +++-------------------------- src/model.rs | 3 +- src/tokenizer.rs | 1 - 8 files changed, 75 insertions(+), 53 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 src/lib.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 74e4d04..881370e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo fmt --check + - uses: dtolnay/rust-toolchain@nightly + - run: cargo +nightly fmt --check clippy: runs-on: ubuntu-latest steps: diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..38b9e24 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +reorder_imports = true +group_imports = "StdExternalCrate" diff --git a/Cargo.lock b/Cargo.lock index 38ee7c4..c760988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "createnv" -version = "0.1.0" +version = "0.0.3" dependencies = [ "anyhow", "rand", diff --git a/Cargo.toml b/Cargo.toml index c4ebb1c..f6db494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,8 @@ [package] name = "createnv" -version = "0.1.0" +version = "0.0.3" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow = "1.0.71" rand = "0.8.5" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7fae99a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,63 @@ +mod model; +mod parser; +mod reader; +mod tokenizer; + +use std::path::PathBuf; + +use anyhow::Result; + +use crate::parser::Parser; +use crate::{ + model::{ + AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, + VariableWithRandomValue, + }, + tokenizer::Tokenizer, +}; + +pub fn tokenize(path: &String) -> Result<()> { + let mut tokenizer = Tokenizer::new(PathBuf::from(path))?; + for token in tokenizer.tokenize()? { + println!("{}: {:?}", token.error_prefix(path), token); + } + + Ok(()) +} + +pub fn parse(path: &String) -> Result<()> { + let mut parser = Parser::new(PathBuf::from(path))?; + for block in parser.parse()? { + println!("{block}"); + } + + Ok(()) +} + +pub fn model_to_text() -> Result<()> { + let title = Comment::new("42"); + let description = Some(Comment::new("Fourty-two")); + + let mut variable1 = SimpleVariable::new("ANSWER", None, None); + variable1.user_input("42"); + let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); + variable2.user_input("fourty two"); + let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); + let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); + variable4.user_input("custom"); + let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); + let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); + + let variables = vec![ + VariableType::Input(variable1), + VariableType::Input(variable2), + VariableType::Input(variable3), + VariableType::Input(variable4), + VariableType::Random(variable5), + VariableType::AutoGenerated(variable6), + ]; + let block = Block::new(title, description, variables); + println!("{block}"); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 7346551..d9aea20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,15 @@ use std::env::args; -use std::path::PathBuf; use anyhow::Result; - -use crate::model::{ - AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, VariableWithRandomValue, -}; -use crate::parser::Parser; -use crate::tokenizer::Tokenizer; - -mod model; -mod parser; -mod reader; -mod tokenizer; +use createnv::{model_to_text, parse, tokenize}; fn main() -> Result<()> { if let Some(path) = args().nth(1) { - let mut parser = Parser::new(PathBuf::from(&path))?; - for block in parser.parse()? { - println!("{block}"); - } + parse(&path)?; + tokenize(&path)?; - let mut tokenizer = Tokenizer::new(PathBuf::from(&path))?; - let tokens = tokenizer.tokenize()?; - for token in &tokens { - println!("{}: {:?}", token.error_prefix(&path), token); - } return Ok(()); } - let title = Comment::new("42"); - let description = Some(Comment::new("Fourty-two")); - - let mut variable1 = SimpleVariable::new("ANSWER", None, None); - variable1.user_input("42"); - let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); - variable2.user_input("fourty two"); - let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); - let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); - variable4.user_input("custom"); - let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); - let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); - - let variables = vec![ - VariableType::Input(variable1), - VariableType::Input(variable2), - VariableType::Input(variable3), - VariableType::Input(variable4), - VariableType::Random(variable5), - VariableType::AutoGenerated(variable6), - ]; - let block = Block::new(title, description, variables); - println!("{block}"); - Ok(()) + model_to_text() } diff --git a/src/model.rs b/src/model.rs index 0491a4d..5cbc4b7 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,7 +1,8 @@ -use rand::{thread_rng, Rng}; use std::collections::HashMap; use std::fmt; +use rand::{thread_rng, Rng}; + const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 158f492..c3291cc 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -112,7 +112,6 @@ impl Tokenizer { } } -// TODO: move to tests/ as integration test? #[cfg(test)] mod tests { use super::*; From 8988bef3bc875421e2739c155803a279395346ce Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:59:32 -0400 Subject: [PATCH 19/28] Cleans up: removes unused code and unnecessary pub --- .github/workflows/tests.yml | 1 + src/lib.rs | 55 ++-------------------- src/main.rs | 3 +- src/model.rs | 76 ++++++++++++++++++++---------- src/parser.rs | 92 ------------------------------------- src/reader.rs | 16 +------ src/tokenizer.rs | 18 ++++++-- 7 files changed, 74 insertions(+), 187 deletions(-) delete mode 100644 src/parser.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 881370e..1a8141e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly + - run: rustup component add rustfmt - run: cargo +nightly fmt --check clippy: runs-on: ubuntu-latest diff --git a/src/lib.rs b/src/lib.rs index 7fae99a..1b4e9bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,63 +1,16 @@ mod model; -mod parser; mod reader; mod tokenizer; -use std::path::PathBuf; - use anyhow::Result; -use crate::parser::Parser; -use crate::{ - model::{ - AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, - VariableWithRandomValue, - }, - tokenizer::Tokenizer, -}; +use crate::model::model_to_text_cli; +use crate::tokenizer::tokenize_cli; pub fn tokenize(path: &String) -> Result<()> { - let mut tokenizer = Tokenizer::new(PathBuf::from(path))?; - for token in tokenizer.tokenize()? { - println!("{}: {:?}", token.error_prefix(path), token); - } - - Ok(()) -} - -pub fn parse(path: &String) -> Result<()> { - let mut parser = Parser::new(PathBuf::from(path))?; - for block in parser.parse()? { - println!("{block}"); - } - - Ok(()) + tokenize_cli(path) } pub fn model_to_text() -> Result<()> { - let title = Comment::new("42"); - let description = Some(Comment::new("Fourty-two")); - - let mut variable1 = SimpleVariable::new("ANSWER", None, None); - variable1.user_input("42"); - let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); - variable2.user_input("fourty two"); - let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); - let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); - variable4.user_input("custom"); - let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); - let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); - - let variables = vec![ - VariableType::Input(variable1), - VariableType::Input(variable2), - VariableType::Input(variable3), - VariableType::Input(variable4), - VariableType::Random(variable5), - VariableType::AutoGenerated(variable6), - ]; - let block = Block::new(title, description, variables); - println!("{block}"); - - Ok(()) + model_to_text_cli() } diff --git a/src/main.rs b/src/main.rs index d9aea20..ed18ed3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ use std::env::args; use anyhow::Result; -use createnv::{model_to_text, parse, tokenize}; +use createnv::{model_to_text, tokenize}; fn main() -> Result<()> { if let Some(path) = args().nth(1) { - parse(&path)?; tokenize(&path)?; return Ok(()); diff --git a/src/model.rs b/src/model.rs index 5cbc4b7..70e770c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,17 +1,18 @@ use std::collections::HashMap; use std::fmt; +use anyhow::Result; use rand::{thread_rng, Rng}; const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; -pub struct Comment { - pub contents: String, +struct Comment { + contents: String, } impl Comment { - pub fn new(contents: &str) -> Self { + fn new(contents: &str) -> Self { Self { contents: contents.to_string(), } @@ -24,7 +25,7 @@ impl fmt::Display for Comment { } } -pub trait Variable { +trait Variable { fn key(&self) -> String; fn value(&self) -> String; fn to_string(&self) -> String { @@ -32,8 +33,8 @@ pub trait Variable { } } -pub struct SimpleVariable { - pub input: Option<String>, +struct SimpleVariable { + input: Option<String>, name: String, default: Option<String>, @@ -41,7 +42,7 @@ pub struct SimpleVariable { } impl SimpleVariable { - pub fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { + fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { Self { name: name.to_string(), default: default.map(|s| s.to_string()), @@ -49,7 +50,7 @@ impl SimpleVariable { input: None, } } - pub fn user_input(&mut self, input: &str) { + fn user_input(&mut self, input: &str) { if let Some(help) = &self.help { println!("{help}"); } @@ -72,15 +73,15 @@ impl Variable for SimpleVariable { } } -pub struct AutoGeneratedVariable { - pub name: String, - pub pattern: String, +struct AutoGeneratedVariable { + name: String, + pattern: String, context: HashMap<String, String>, } impl AutoGeneratedVariable { - pub fn new(name: &str, pattern: &str) -> Self { + fn new(name: &str, pattern: &str) -> Self { Self { name: name.to_string(), pattern: pattern.to_string(), @@ -88,7 +89,7 @@ impl AutoGeneratedVariable { } } - pub fn load_context(&mut self, ctx: &HashMap<String, String>) { + fn load_context(&mut self, ctx: &HashMap<String, String>) { for (k, v) in ctx.iter() { self.context.insert(k.to_string(), v.to_string()); } @@ -109,13 +110,13 @@ impl Variable for AutoGeneratedVariable { } } -pub struct VariableWithRandomValue { - pub name: String, - pub length: Option<i32>, +struct VariableWithRandomValue { + name: String, + length: Option<i32>, } impl VariableWithRandomValue { - pub fn new(name: &str, length: Option<i32>) -> Self { + fn new(name: &str, length: Option<i32>) -> Self { Self { name: name.to_string(), length, @@ -143,22 +144,22 @@ impl Variable for VariableWithRandomValue { } } -pub enum VariableType { +enum VariableType { Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), Random(VariableWithRandomValue), } -pub struct Block { - pub title: Comment, - pub description: Option<Comment>, - pub variables: Vec<VariableType>, +struct Block { + title: Comment, + description: Option<Comment>, + variables: Vec<VariableType>, context: HashMap<String, String>, } impl Block { - pub fn new(title: Comment, description: Option<Comment>, variables: Vec<VariableType>) -> Self { + fn new(title: Comment, description: Option<Comment>, variables: Vec<VariableType>) -> Self { let context: HashMap<String, String> = HashMap::new(); let has_auto_generated_variables = variables .iter() @@ -288,3 +289,32 @@ mod tests { assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") } } + +// TODO: remove (only written for manual tests & debug) +pub fn model_to_text_cli() -> Result<()> { + let title = Comment::new("42"); + let description = Some(Comment::new("Fourty-two")); + + let mut variable1 = SimpleVariable::new("ANSWER", None, None); + variable1.user_input("42"); + let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); + variable2.user_input("fourty two"); + let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); + let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); + variable4.user_input("custom"); + let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); + let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); + + let variables = vec![ + VariableType::Input(variable1), + VariableType::Input(variable2), + VariableType::Input(variable3), + VariableType::Input(variable4), + VariableType::Random(variable5), + VariableType::AutoGenerated(variable6), + ]; + let block = Block::new(title, description, variables); + println!("{block}"); + + Ok(()) +} diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index b51e5ec..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,92 +0,0 @@ -use anyhow::{anyhow, Result}; - -use crate::{ - model::{Block, Comment}, - reader::{CharReader, CharType}, -}; - -const CAPITAL_ASCII_LETTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -pub struct Parser { - reader: CharReader, -} - -impl Parser { - pub fn new(path: std::path::PathBuf) -> Result<Self> { - Ok(Self { - reader: CharReader::new(path)?, - }) - } - - fn check_line_start(&self, char: &CharType) -> Result<()> { - if self.reader.column != 1 { - return Ok(()); - } - - match char { - CharType::Eol => Ok(()), - CharType::Eof => Ok(()), - CharType::Char(c) => { - if *c == '#' || CAPITAL_ASCII_LETTERS.contains(*c) { - return Ok(()); - } - - let msg = "A line must start with a capital ASCII letter or `#`".to_string(); - Err(anyhow!(self.reader.error(char, Some(msg)))) - } - } - } - - pub fn parse_until( - &mut self, - target: Vec<CharType>, - avoid: Vec<CharType>, - column: Option<usize>, - ) -> Result<String> { - let mut buffer = "".to_string(); - loop { - let char = self.reader.next()?; - self.check_line_start(&char)?; - if avoid.contains(&char) { - return Err(anyhow!(self.reader.error(&char, None))); - } - if target.contains(&char) { - if let Some(col) = column { - if self.reader.column != col { - let msg = format!("expected at column {col}"); - return Err(self.reader.error(&char, Some(msg))); - } - } - break; - } - if let CharType::Char(c) = char { - buffer.push(c); - } - } - - Ok(buffer.to_string()) - } - - fn parse_comment(&mut self) -> Result<Comment> { - self.parse_until(vec![CharType::Char('#')], vec![CharType::Eol], Some(1))?; - self.parse_until(vec![CharType::Char(' ')], vec![CharType::Eol], Some(2))?; - - let name = self.parse_until(vec![CharType::Eol], vec![], None)?; - - Ok(Comment::new(name.as_str())) - } - - fn parse_block(&mut self) -> Result<Block> { - let title = self - .parse_comment() - .map_err(|e| anyhow!("A block must start with `# `: {e}"))?; - - Ok(Block::new(title, None, vec![])) - } - - pub fn parse(&mut self) -> Result<Vec<Block>> { - let block = self.parse_block()?; - let blocks = vec![block]; - Ok(blocks) - } -} diff --git a/src/reader.rs b/src/reader.rs index 23fb99a..d8cebfd 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -4,7 +4,7 @@ use std::{ path::PathBuf, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; #[derive(PartialEq, Eq)] pub enum CharType { @@ -16,7 +16,6 @@ pub enum CharType { pub struct CharReader { pub line: usize, pub column: usize, - path: String, current_line: Option<String>, reader: BufReader<File>, done: bool, @@ -27,25 +26,12 @@ impl CharReader { Ok(Self { line: 0, column: 0, - path: path.display().to_string(), current_line: None, done: false, reader: BufReader::new(File::open(path)?), }) } - pub fn error(&self, character: &CharType, details: Option<String>) -> anyhow::Error { - let prefix = format!("{}:{}:{}", self.path, self.line, self.column); - let extra = details.map_or("".to_string(), |msg| format!(": {msg}")); - let token = match &character { - CharType::Char(char) => format!("character `{char}`"), - CharType::Eol => "EOL (end of line)".to_string(), - CharType::Eof => "EOF (end of file)".to_string(), - }; - - anyhow!(format!("{prefix}: Unexpected {token}{extra}")) - } - pub fn next(&mut self) -> Result<CharType> { if self.done { return Ok(CharType::Eof); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index c3291cc..5375ba9 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -5,7 +5,7 @@ use anyhow::Result; use crate::reader::{CharReader, CharType}; #[derive(Debug, PartialEq)] -pub enum Token { +enum Token { Text(usize, usize, String), CommentMark(usize, usize), HelpMark(usize, usize), @@ -13,7 +13,7 @@ pub enum Token { } impl Token { - pub fn error_prefix(&self, path: &String) -> String { + fn error_prefix(&self, path: &String) -> String { let (line, column) = match self { Token::Text(x, y, _) => (x, y), Token::CommentMark(x, y) => (x, y), @@ -25,7 +25,7 @@ impl Token { } } -pub struct Tokenizer { +struct Tokenizer { reader: CharReader, } @@ -99,7 +99,7 @@ impl Tokenizer { } // TODO: make iterator? - pub fn tokenize(&mut self) -> Result<Vec<Token>> { + fn tokenize(&mut self) -> Result<Vec<Token>> { let mut tokens: Vec<Token> = vec![]; loop { let new_tokens = self.next_tokens()?; @@ -171,3 +171,13 @@ mod tests { assert_eq!(tokens[18], Token::Text(6, 10, "<random:16>".to_string())); } } + +// TODO: remove (just written for manual tests * debug) +pub fn tokenize_cli(path: &String) -> Result<()> { + let mut tokenizer = Tokenizer::new(PathBuf::from(path))?; + for token in tokenizer.tokenize()? { + println!("{}: {:?}", token.error_prefix(path), token); + } + + Ok(()) +} From 6da2751c0684a8d39944f84831e2e0c6a2cec027 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:09:35 -0400 Subject: [PATCH 20/28] Makes tokenizer an interator --- src/tokenizer.rs | 55 ++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 5375ba9..7d54a8d 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -27,12 +27,14 @@ impl Token { struct Tokenizer { reader: CharReader, + buffer: Option<Token>, } impl Tokenizer { pub fn new(path: PathBuf) -> Result<Self> { Ok(Self { reader: CharReader::new(path)?, + buffer: None, }) } @@ -60,17 +62,17 @@ impl Tokenizer { Token::HelpMark(self.reader.line, self.reader.column - 2) } - fn next_tokens(&mut self) -> Result<Vec<Token>> { + fn next_tokens(&mut self) -> Result<(Option<Token>, Option<Token>)> { let mut buffer = "".to_string(); loop { let char = self.reader.next()?; match char { - CharType::Eof => return Ok(vec![]), + CharType::Eof => return Ok((None, None)), CharType::Eol => { if buffer.is_empty() { continue; } - return Ok(vec![self.text(buffer, true, false)]); + return Ok((Some(self.text(buffer, true, false)), None)); } CharType::Char(c) => { let mut token: Option<Token> = None; @@ -88,27 +90,35 @@ impl Tokenizer { } if let Some(t) = token { if buffer.is_empty() { - return Ok(vec![t]); + return Ok((Some(t), None)); } - return Ok(vec![self.text(buffer, false, prepends_help), t]); + return Ok((Some(self.text(buffer, false, prepends_help)), Some(t))); } buffer.push(c) } } } } +} - // TODO: make iterator? - fn tokenize(&mut self) -> Result<Vec<Token>> { - let mut tokens: Vec<Token> = vec![]; - loop { - let new_tokens = self.next_tokens()?; - if new_tokens.is_empty() { - break; +impl Iterator for Tokenizer { + type Item = Result<Token>; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(token) = self.buffer.take() { + return Some(Ok(token)); + } + + match self.next_tokens() { + Ok((Some(first), Some(second))) => { + self.buffer = Some(second); + Some(Ok(first)) } - tokens.extend(new_tokens); + Ok((Some(token), None)) => Some(Ok(token)), + Ok((None, Some(token))) => Some(Ok(token)), + Ok((None, None)) => None, + Err(e) => Some(Err(e)), } - Ok(tokens) } } @@ -119,8 +129,8 @@ mod tests { #[test] fn test_tokenizer() { let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env.sample"); - let mut tokenizer = Tokenizer::new(sample).unwrap(); - let tokens = tokenizer.tokenize().unwrap(); + let tokenizer = Tokenizer::new(sample).unwrap(); + let tokens: Vec<Token> = tokenizer.map(|t| t.unwrap()).collect(); assert_eq!(tokens.len(), 19); // line 1 @@ -172,11 +182,16 @@ mod tests { } } -// TODO: remove (just written for manual tests * debug) +// TODO: remove (just written for manual tests & debug) pub fn tokenize_cli(path: &String) -> Result<()> { - let mut tokenizer = Tokenizer::new(PathBuf::from(path))?; - for token in tokenizer.tokenize()? { - println!("{}: {:?}", token.error_prefix(path), token); + for token in Tokenizer::new(PathBuf::from(path))? { + println!("{:?}", token?); + } + if let Some(token) = Tokenizer::new(PathBuf::from(path))?.next() { + println!( + "\nThe error prefix looks like:\n{}", + token?.error_prefix(path) + ); } Ok(()) From 47519d3054df148fc84bdbec7931a835a6d10921 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:59:35 -0400 Subject: [PATCH 21/28] Starts prototype for the parser --- src/lib.rs | 10 ++-- src/main.rs | 3 +- src/model.rs | 94 +++++++++++++++++------------------ src/parser.rs | 126 +++++++++++++++++++++++++++++++++++++++++++++++ src/tokenizer.rs | 6 +-- 5 files changed, 185 insertions(+), 54 deletions(-) create mode 100644 src/parser.rs diff --git a/src/lib.rs b/src/lib.rs index 1b4e9bd..3fd31d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ mod model; +mod parser; mod reader; mod tokenizer; use anyhow::Result; - -use crate::model::model_to_text_cli; -use crate::tokenizer::tokenize_cli; +use model::model_to_text_cli; +use parser::parser_cli; +use tokenizer::tokenize_cli; pub fn tokenize(path: &String) -> Result<()> { tokenize_cli(path) @@ -14,3 +15,6 @@ pub fn tokenize(path: &String) -> Result<()> { pub fn model_to_text() -> Result<()> { model_to_text_cli() } +pub fn parser(path: &String) -> Result<()> { + parser_cli(path) +} diff --git a/src/main.rs b/src/main.rs index ed18ed3..e57f771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use std::env::args; use anyhow::Result; -use createnv::{model_to_text, tokenize}; +use createnv::{model_to_text, parser, tokenize}; fn main() -> Result<()> { if let Some(path) = args().nth(1) { tokenize(&path)?; + parser(&path)?; return Ok(()); } diff --git a/src/model.rs b/src/model.rs index 70e770c..0f7e29c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,12 +7,12 @@ use rand::{thread_rng, Rng}; const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; -struct Comment { +pub struct Comment { contents: String, } impl Comment { - fn new(contents: &str) -> Self { + pub fn new(contents: &str) -> Self { Self { contents: contents.to_string(), } @@ -33,7 +33,7 @@ trait Variable { } } -struct SimpleVariable { +pub struct SimpleVariable { input: Option<String>, name: String, @@ -73,7 +73,7 @@ impl Variable for SimpleVariable { } } -struct AutoGeneratedVariable { +pub struct AutoGeneratedVariable { name: String, pattern: String, @@ -110,7 +110,7 @@ impl Variable for AutoGeneratedVariable { } } -struct VariableWithRandomValue { +pub struct VariableWithRandomValue { name: String, length: Option<i32>, } @@ -144,51 +144,55 @@ impl Variable for VariableWithRandomValue { } } -enum VariableType { +pub enum VariableType { Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), Random(VariableWithRandomValue), } -struct Block { +pub struct Block { title: Comment, description: Option<Comment>, variables: Vec<VariableType>, - - context: HashMap<String, String>, } impl Block { - fn new(title: Comment, description: Option<Comment>, variables: Vec<VariableType>) -> Self { - let context: HashMap<String, String> = HashMap::new(); - let has_auto_generated_variables = variables - .iter() - .any(|v| matches!(v, VariableType::AutoGenerated(_))); - - let mut block = Self { + pub fn new(title: Comment, description: Option<Comment>) -> Self { + Self { title, description, - variables, - context, - }; + variables: vec![], + } + } - if has_auto_generated_variables { - for variable in &block.variables { - match variable { - VariableType::Input(var) => block.context.insert(var.key(), var.value()), - VariableType::AutoGenerated(_) => None, - VariableType::Random(var) => block.context.insert(var.key(), var.value()), - }; - } + fn has_auto_generated_variables(&self) -> bool { + self.variables + .iter() + .any(|v| matches!(v, VariableType::AutoGenerated(_))) + } - for variable in &mut block.variables { - if let VariableType::AutoGenerated(var) = variable { - var.load_context(&block.context); - } - } + pub fn push(&mut self, variable: VariableType) { + self.variables.push(variable); + if !self.has_auto_generated_variables() { + return; } - block + let mut context = HashMap::new(); + for var in &self.variables { + match var { + VariableType::AutoGenerated(_) => None, + VariableType::Input(v) => context.insert(v.key(), v.value()), + VariableType::Random(v) => context.insert(v.key(), v.value()), + }; + } + + let mut variables: Vec<VariableType> = vec![]; + for variable in &mut variables { + if let VariableType::AutoGenerated(var) = variable { + var.load_context(&context); + } + } + self.variables = variables; } } @@ -280,11 +284,9 @@ mod tests { let mut variable1 = SimpleVariable::new("ANSWER", None, None); variable1.user_input("42"); let variable2 = SimpleVariable::new("AS_TEXT", Some("fourty two"), None); - let variables = vec![ - VariableType::Input(variable1), - VariableType::Input(variable2), - ]; - let block = Block::new(title, description, variables); + let mut block = Block::new(title, description); + block.push(VariableType::Input(variable1)); + block.push(VariableType::Input(variable2)); let got = block.to_string(); assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") } @@ -305,15 +307,13 @@ pub fn model_to_text_cli() -> Result<()> { let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); - let variables = vec![ - VariableType::Input(variable1), - VariableType::Input(variable2), - VariableType::Input(variable3), - VariableType::Input(variable4), - VariableType::Random(variable5), - VariableType::AutoGenerated(variable6), - ]; - let block = Block::new(title, description, variables); + let mut block = Block::new(title, description); + block.push(VariableType::Input(variable1)); + block.push(VariableType::Input(variable2)); + block.push(VariableType::Input(variable3)); + block.push(VariableType::Input(variable4)); + block.push(VariableType::Random(variable5)); + block.push(VariableType::AutoGenerated(variable6)); println!("{block}"); Ok(()) diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..f56a104 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,126 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; + +use crate::{ + model::{Block, Comment}, + tokenizer::{Token, Tokenizer}, +}; + +struct Parser { + tokens: Tokenizer, + path: String, + previous_token: Option<Token>, + current_token: Option<Token>, +} + +impl Parser { + pub fn new(path: &String) -> Result<Self> { + Ok(Self { + tokens: Tokenizer::new(PathBuf::from(path))?, + path: path.clone(), + current_token: None, + previous_token: None, + }) + } + + fn load_next_token(&mut self) -> Result<()> { + self.previous_token = self.current_token.take(); + match self.tokens.next() { + Some(token) => self.current_token = Some(token?), + None => self.current_token = None, + } + + Ok(()) + } + + fn error(&self, msg: &str) -> anyhow::Error { + let prefix = if let Some(curr) = &self.current_token { + curr.error_prefix(&self.path) + } else if let Some(prev) = &self.previous_token { + prev.error_prefix(&self.path) + } else { + "EOF".to_string() + }; + + anyhow!("{}: {}", prefix, msg) + } + + fn parse_title(&mut self) -> Result<String> { + self.load_next_token()?; + match self.current_token { + Some(Token::CommentMark(_, _)) => (), + Some(_) => return Err(self.error("Expected a title line starting with `#`")), + None => { + return Err( + self.error("Expected a title line starting with `#`, got the end of the file") + ) + } + } + + self.load_next_token()?; + match &self.current_token { + Some(Token::Text(_, _, text)) => Ok(text.clone()), + Some(_) => Err(self.error("Expected the text of the title")), + None => Err(self.error("Expected the text of the title, got the end of the file")), + } + } + + fn parse_description(&mut self) -> Result<Option<String>> { + self.load_next_token()?; + match self.current_token { + Some(Token::CommentMark(_, _)) => (), + Some(_) => return Ok(None), + None => return Err(self.error("Expected a descrition line starting with `#` or a variable definition, got the end of the file")), + } + + self.load_next_token()?; + match &self.current_token { + Some(Token::Text(_, _, text)) => Ok(Some(text.clone())), + Some(_) => Err(self.error("Expected a descrition text")), + None => Err(self.error("Expected a descrition text, got the end of the file")), + } + } + + pub fn parse(&mut self) -> Result<Vec<Block>> { + let mut blocks: Vec<Block> = vec![]; + let title = Comment::new(self.parse_title()?.as_str()); + let descrition = self + .parse_description()? + .map(|desc| Comment::new(desc.as_str())); + blocks.push(Block::new(title, descrition)); + + Ok(blocks) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() { + let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(".env.sample") + .into_os_string() + .into_string(); + let parsed = Parser::new(&sample.unwrap()).unwrap().parse().unwrap(); + let got = parsed + .iter() + .map(|block| block.to_string()) + .collect::<Vec<String>>() + .join("\n"); + let expected = "# Createnv\n# This is a simple example of how Createnv works".to_string(); + assert_eq!(expected, got); + } +} +// +// TODO: remove (just written for manual tests & debug) +pub fn parser_cli(path: &String) -> Result<()> { + let mut parser = Parser::new(path)?; + for block in parser.parse()? { + println!("{block}"); + } + + Ok(()) +} diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 7d54a8d..d3cf4bd 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -5,7 +5,7 @@ use anyhow::Result; use crate::reader::{CharReader, CharType}; #[derive(Debug, PartialEq)] -enum Token { +pub enum Token { Text(usize, usize, String), CommentMark(usize, usize), HelpMark(usize, usize), @@ -13,7 +13,7 @@ enum Token { } impl Token { - fn error_prefix(&self, path: &String) -> String { + pub fn error_prefix(&self, path: &String) -> String { let (line, column) = match self { Token::Text(x, y, _) => (x, y), Token::CommentMark(x, y) => (x, y), @@ -25,7 +25,7 @@ impl Token { } } -struct Tokenizer { +pub struct Tokenizer { reader: CharReader, buffer: Option<Token>, } From 70fa3f17260bf26e35a095093d5c71996a42d96b Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Fri, 30 Aug 2024 21:01:20 -0400 Subject: [PATCH 22/28] Finishes CLI interaction --- .rustfmt.toml | 2 +- Cargo.lock | 126 +++++++++++++++++++++++-- Cargo.toml | 3 +- src/lib.rs | 11 +-- src/main.rs | 15 +-- src/model.rs | 238 +++++++++++++++++++++++++++-------------------- src/parser.rs | 162 ++++++++++++++------------------ src/reader.rs | 70 -------------- src/tokenizer.rs | 198 --------------------------------------- 9 files changed, 341 insertions(+), 484 deletions(-) delete mode 100644 src/reader.rs delete mode 100644 src/tokenizer.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index 38b9e24..6abf97b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,2 +1,2 @@ reorder_imports = true -group_imports = "StdExternalCrate" +group_imports = "StdExternalCrate" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c760988..7de3fe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,26 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" @@ -20,13 +35,14 @@ version = "0.0.3" dependencies = [ "anyhow", "rand", + "regex", ] [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -35,15 +51,42 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] [[package]] name = "rand" @@ -75,8 +118,75 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index f6db494..c5eb977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ version = "0.0.3" edition = "2021" [dependencies] -anyhow = "1.0.71" +anyhow = "1.0.86" rand = "0.8.5" +regex = "1.10.6" diff --git a/src/lib.rs b/src/lib.rs index 3fd31d5..468930d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,17 @@ mod model; mod parser; -mod reader; -mod tokenizer; use anyhow::Result; use model::model_to_text_cli; use parser::parser_cli; -use tokenizer::tokenize_cli; -pub fn tokenize(path: &String) -> Result<()> { - tokenize_cli(path) -} +pub const DEFAULT_ENV_SAMPLE: &str = ".env.sample"; +pub const DEFAULT_ENV: &str = ".env"; pub fn model_to_text() -> Result<()> { model_to_text_cli() } -pub fn parser(path: &String) -> Result<()> { + +pub fn parser(path: &str) -> Result<()> { parser_cli(path) } diff --git a/src/main.rs b/src/main.rs index e57f771..f8fb4f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ use std::env::args; use anyhow::Result; -use createnv::{model_to_text, parser, tokenize}; +use createnv::{model_to_text, parser, DEFAULT_ENV_SAMPLE}; fn main() -> Result<()> { - if let Some(path) = args().nth(1) { - tokenize(&path)?; - parser(&path)?; - + let path = args() + .nth(1) + .unwrap_or_else(|| DEFAULT_ENV_SAMPLE.to_string()); + if path == "--debug" { + model_to_text()?; return Ok(()); } - - model_to_text() + parser(&path)?; + Ok(()) } diff --git a/src/model.rs b/src/model.rs index 0f7e29c..ce5c61a 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,12 +1,18 @@ -use std::collections::HashMap; use std::fmt; +use std::{ + collections::HashMap, + io::{stdout, BufRead, Write}, +}; use anyhow::Result; use rand::{thread_rng, Rng}; +use crate::DEFAULT_ENV; + const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; +#[derive(Clone)] pub struct Comment { contents: String, } @@ -27,22 +33,22 @@ impl fmt::Display for Comment { trait Variable { fn key(&self) -> String; - fn value(&self) -> String; - fn to_string(&self) -> String { - format!("{}={}", self.key(), self.value()) + fn value(&self) -> Result<String>; + fn as_text(&self) -> Result<String> { + Ok(format!("{}={}", self.key(), self.value()?)) } } +#[derive(Clone, Debug)] pub struct SimpleVariable { - input: Option<String>, - name: String, default: Option<String>, help: Option<String>, + input: Option<String>, } impl SimpleVariable { - fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { + pub fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { Self { name: name.to_string(), default: default.map(|s| s.to_string()), @@ -50,11 +56,27 @@ impl SimpleVariable { input: None, } } - fn user_input(&mut self, input: &str) { - if let Some(help) = &self.help { - println!("{help}"); + + fn ask_for_input<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + match (&self.help, &self.default) { + (Some(h), Some(d)) => print!("{} [{}]: ", h, d), + (Some(h), None) => print!("{}: ", h), + (None, Some(d)) => print!("{} [{}]: ", self.name, d), + (None, None) => print!("{}: ", self.name), + }; + + stdout().flush()?; + let mut input = "".to_string(); + terminal.read_line(&mut input)?; + + let value = input.trim(); + if value.is_empty() && self.default.is_none() { + return self.ask_for_input(terminal); + } + if !value.is_empty() { + self.input = Some(value.to_string()); } - self.input = Some(input.to_string()); + Ok(()) } } @@ -62,21 +84,21 @@ impl Variable for SimpleVariable { fn key(&self) -> String { self.name.clone() } - fn value(&self) -> String { + fn value(&self) -> Result<String> { if let Some(input) = &self.input { - return input.clone(); + return Ok(input.clone()); } if let Some(default) = &self.default { - return default.clone(); + return Ok(default.clone()); } - "".to_string() // TODO: error? + Err(anyhow::anyhow!("Variable {} has no value", self.name)) } } +#[derive(Clone, Debug)] pub struct AutoGeneratedVariable { name: String, pattern: String, - context: HashMap<String, String>, } @@ -100,39 +122,29 @@ impl Variable for AutoGeneratedVariable { fn key(&self) -> String { self.name.clone() } - fn value(&self) -> String { + fn value(&self) -> Result<String> { let mut value: String = self.pattern.clone(); for (k, v) in self.context.iter() { let key = format!("{{{}}}", *k); value = value.replace(&key, v); } - value + Ok(value) } } +#[derive(Clone, Debug)] pub struct VariableWithRandomValue { name: String, - length: Option<i32>, + value: String, } impl VariableWithRandomValue { fn new(name: &str, length: Option<i32>) -> Self { - Self { - name: name.to_string(), - length, - } - } -} - -impl Variable for VariableWithRandomValue { - fn key(&self) -> String { - self.name.clone() - } - fn value(&self) -> String { + let name = name.to_string(); let mut rng = thread_rng(); let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); let mut value: String = String::from(""); - let length = match self.length { + let length = match length { Some(n) => n, None => rng.gen_range(64..=128), }; @@ -140,20 +152,31 @@ impl Variable for VariableWithRandomValue { let pos = rng.gen_range(0..max_chars_idx); value.push(DEFAULT_RANDOM_CHARS.chars().nth(pos).unwrap()) } - value + Self { name, value } + } +} + +impl Variable for VariableWithRandomValue { + fn key(&self) -> String { + self.name.clone() + } + fn value(&self) -> Result<String> { + Ok(self.value.clone()) } } +#[derive(Clone, Debug)] pub enum VariableType { Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), Random(VariableWithRandomValue), } +#[derive(Clone)] pub struct Block { - title: Comment, - description: Option<Comment>, - variables: Vec<VariableType>, + pub title: Comment, + pub description: Option<Comment>, + pub variables: Vec<VariableType>, } impl Block { @@ -171,95 +194,133 @@ impl Block { .any(|v| matches!(v, VariableType::AutoGenerated(_))) } - pub fn push(&mut self, variable: VariableType) { + pub fn push(&mut self, variable: VariableType) -> Result<()> { self.variables.push(variable); if !self.has_auto_generated_variables() { - return; + return Ok(()); } + Ok(()) + } + + pub fn resolve<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + for variable in &mut self.variables { + if let VariableType::Input(var) = variable { + if var.input.is_none() { + var.ask_for_input(terminal)?; + } + } + } + if !self.has_auto_generated_variables() { + return Ok(()); + } let mut context = HashMap::new(); for var in &self.variables { match var { VariableType::AutoGenerated(_) => None, - VariableType::Input(v) => context.insert(v.key(), v.value()), - VariableType::Random(v) => context.insert(v.key(), v.value()), + VariableType::Input(v) => context.insert(v.key(), v.value()?), + VariableType::Random(v) => context.insert(v.key(), v.value()?), }; } - - let mut variables: Vec<VariableType> = vec![]; - for variable in &mut variables { + for variable in &mut self.variables { if let VariableType::AutoGenerated(var) = variable { var.load_context(&context); } } - self.variables = variables; + Ok(()) } -} -impl fmt::Display for Block { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + pub fn as_text(&mut self) -> Result<String> { let mut lines: Vec<String> = vec![self.title.to_string()]; - match &self.description { - Some(desc) => lines.push(desc.to_string()), - None => (), + if let Some(desc) = &self.description { + lines.push(desc.to_string()); } - - for variable in &self.variables { + for variable in &mut self.variables { match variable { - VariableType::Input(var) => lines.push(var.to_string()), - VariableType::AutoGenerated(var) => lines.push(var.to_string()), - VariableType::Random(var) => lines.push(var.to_string()), + VariableType::Input(var) => lines.push(var.as_text()?), + VariableType::AutoGenerated(var) => lines.push(var.as_text()?), + VariableType::Random(var) => lines.push(var.as_text()?), } } - - write!(f, "{}", lines.join("\n")) + Ok(lines.join("\n")) } } +// TODO: remove (only written for manual tests & debug) +pub fn model_to_text_cli() -> Result<()> { + let variable1 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); + let variable2 = SimpleVariable::new("ANSWER", None, Some("If you read that book, you know!")); + let variable3 = SimpleVariable::new("AS_TEXT", None, None); + let variable4 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); + let variable5 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); + let variable6 = VariableWithRandomValue::new("SECRET_KEY", Some(16)); + + let mut block = Block::new( + Comment::new("Here comes a new block!"), + Some(Comment::new("And here comes a description about it.")), + ); + block.push(VariableType::AutoGenerated(variable1))?; + block.push(VariableType::Input(variable2))?; + block.push(VariableType::Input(variable3))?; + block.push(VariableType::Input(variable4))?; + block.push(VariableType::Input(variable5))?; + block.push(VariableType::Random(variable6))?; + block.resolve(&mut std::io::stdin().lock())?; + + println!( + "\nThis would be written to {}:\n\n{}", + DEFAULT_ENV, + block.as_text()? + ); + Ok(()) +} + #[cfg(test)] mod tests { + use std::io::Cursor; + use super::*; #[test] fn test_title() { - let line = Comment::new("Fourty-two"); - assert_eq!(line.to_string(), "# Fourty-two") + let line = Comment::new("Forty-two"); + assert_eq!(line.to_string(), "# Forty-two") } #[test] fn test_variable() { let mut var = SimpleVariable::new("ANSWER", None, None); - var.user_input("42"); - assert_eq!(var.to_string(), "ANSWER=42") + var.ask_for_input(&mut Cursor::new("42")).unwrap(); + assert_eq!(var.as_text().unwrap(), "ANSWER=42") } #[test] fn test_empty_variable_with_default_value() { let var = SimpleVariable::new("ANSWER", Some("42"), None); - assert_eq!(var.to_string(), "ANSWER=42") + assert_eq!(var.as_text().unwrap(), "ANSWER=42") } #[test] fn test_variable_with_default_value_and_input() { let mut var = SimpleVariable::new("ANSWER", Some("42"), None); - var.user_input("fourty two"); - assert_eq!(var.to_string(), "ANSWER=fourty two") + var.ask_for_input(&mut Cursor::new("forty two")).unwrap(); + assert_eq!(var.as_text().unwrap(), "ANSWER=forty two") } #[test] fn test_auto_generated_variable() { let mut var = AutoGeneratedVariable::new("ANSWER", "{FIRST} {SECOND}"); let mut ctx = HashMap::new(); - ctx.insert("FIRST".to_string(), "Fourty".to_string()); + ctx.insert("FIRST".to_string(), "Forty".to_string()); ctx.insert("SECOND".to_string(), "two".to_string()); var.load_context(&ctx); - assert_eq!(var.to_string(), "ANSWER=Fourty two") + assert_eq!(var.as_text().unwrap(), "ANSWER=Forty two") } #[test] fn test_variable_with_random_value() { let var = VariableWithRandomValue::new("ANSWER", None); - let got = var.to_string(); + let got = var.as_text().unwrap(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert!(suffix.chars().count() >= 64); assert!(suffix.chars().count() <= 128); @@ -270,7 +331,7 @@ mod tests { #[test] fn test_variable_with_random_value_of_fixed_length() { let var = VariableWithRandomValue::new("ANSWER", Some(42)); - let got = var.to_string(); + let got = var.as_text().unwrap(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert_eq!(suffix.chars().count(), 42); let prefix = got.strip_suffix(suffix).unwrap(); @@ -280,41 +341,14 @@ mod tests { #[test] fn test_block_with_description() { let title = Comment::new("42"); - let description = Some(Comment::new("Fourty-two")); + let description = Some(Comment::new("Forty-two")); let mut variable1 = SimpleVariable::new("ANSWER", None, None); - variable1.user_input("42"); - let variable2 = SimpleVariable::new("AS_TEXT", Some("fourty two"), None); + variable1.ask_for_input(&mut Cursor::new("42")).unwrap(); + let variable2 = SimpleVariable::new("AS_TEXT", Some("forty two"), None); let mut block = Block::new(title, description); - block.push(VariableType::Input(variable1)); - block.push(VariableType::Input(variable2)); - let got = block.to_string(); - assert_eq!(got, "# 42\n# Fourty-two\nANSWER=42\nAS_TEXT=fourty two") + block.push(VariableType::Input(variable1)).unwrap(); + block.push(VariableType::Input(variable2)).unwrap(); + let got = block.as_text().unwrap(); + assert_eq!(got, "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two") } } - -// TODO: remove (only written for manual tests & debug) -pub fn model_to_text_cli() -> Result<()> { - let title = Comment::new("42"); - let description = Some(Comment::new("Fourty-two")); - - let mut variable1 = SimpleVariable::new("ANSWER", None, None); - variable1.user_input("42"); - let mut variable2 = SimpleVariable::new("AS_TEXT", None, None); - variable2.user_input("fourty two"); - let variable3 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); - let mut variable4 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); - variable4.user_input("custom"); - let variable5 = VariableWithRandomValue::new("SECRET_KEY", None); - let variable6 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); - - let mut block = Block::new(title, description); - block.push(VariableType::Input(variable1)); - block.push(VariableType::Input(variable2)); - block.push(VariableType::Input(variable3)); - block.push(VariableType::Input(variable4)); - block.push(VariableType::Random(variable5)); - block.push(VariableType::AutoGenerated(variable6)); - println!("{block}"); - - Ok(()) -} diff --git a/src/parser.rs b/src/parser.rs index f56a104..8a6c1f2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,101 +1,85 @@ -use std::path::PathBuf; +use std::{ + fs::File, + io::{stdin, BufRead, BufReader}, +}; -use anyhow::{anyhow, Result}; +use anyhow::Result; -use crate::{ - model::{Block, Comment}, - tokenizer::{Token, Tokenizer}, -}; +use crate::model::{Block, Comment}; + +enum Expecting { + Title, + DescriptionOrVariables, + Variables, +} struct Parser { - tokens: Tokenizer, + blocks: Vec<Block>, + buffer: Option<Block>, path: String, - previous_token: Option<Token>, - current_token: Option<Token>, + state: Expecting, } impl Parser { - pub fn new(path: &String) -> Result<Self> { - Ok(Self { - tokens: Tokenizer::new(PathBuf::from(path))?, - path: path.clone(), - current_token: None, - previous_token: None, - }) - } - - fn load_next_token(&mut self) -> Result<()> { - self.previous_token = self.current_token.take(); - match self.tokens.next() { - Some(token) => self.current_token = Some(token?), - None => self.current_token = None, + pub fn new(path: &str) -> Self { + Self { + blocks: vec![], + buffer: None, + path: path.to_string(), + state: Expecting::Title, } - - Ok(()) } - fn error(&self, msg: &str) -> anyhow::Error { - let prefix = if let Some(curr) = &self.current_token { - curr.error_prefix(&self.path) - } else if let Some(prev) = &self.previous_token { - prev.error_prefix(&self.path) - } else { - "EOF".to_string() - }; - - anyhow!("{}: {}", prefix, msg) - } - - fn parse_title(&mut self) -> Result<String> { - self.load_next_token()?; - match self.current_token { - Some(Token::CommentMark(_, _)) => (), - Some(_) => return Err(self.error("Expected a title line starting with `#`")), - None => { - return Err( - self.error("Expected a title line starting with `#`, got the end of the file") - ) + pub fn parse(&mut self) -> Result<()> { + let reader = BufReader::new(File::open(&self.path)?); + for line in reader.lines() { + // TODO: empty line => start new block + match self.state { + Expecting::Title => { + if let Some(txt) = line?.strip_prefix('#') { + self.buffer = Some(Block::new(Comment::new(txt.trim()), None)); + } + self.state = Expecting::DescriptionOrVariables; + } + Expecting::DescriptionOrVariables => { + if let Some(txt) = line?.strip_prefix('#') { + if let Some(b) = self.buffer.as_mut() { + b.description = Some(Comment::new(txt.trim())); + } + self.state = Expecting::Variables; + } else { + // TODO: handle variable line + } + } + Expecting::Variables => { + // TODO: , + } } } - - self.load_next_token()?; - match &self.current_token { - Some(Token::Text(_, _, text)) => Ok(text.clone()), - Some(_) => Err(self.error("Expected the text of the title")), - None => Err(self.error("Expected the text of the title, got the end of the file")), + if let Some(block) = &self.buffer { + self.blocks.push(block.clone()); + self.buffer = None } + Ok(()) } +} - fn parse_description(&mut self) -> Result<Option<String>> { - self.load_next_token()?; - match self.current_token { - Some(Token::CommentMark(_, _)) => (), - Some(_) => return Ok(None), - None => return Err(self.error("Expected a descrition line starting with `#` or a variable definition, got the end of the file")), - } - - self.load_next_token()?; - match &self.current_token { - Some(Token::Text(_, _, text)) => Ok(Some(text.clone())), - Some(_) => Err(self.error("Expected a descrition text")), - None => Err(self.error("Expected a descrition text, got the end of the file")), - } +// TODO: remove (just written for manual tests & debug) +pub fn parser_cli(path: &str) -> Result<()> { + let mut parser = Parser::new(path); + parser.parse()?; + for block in &mut parser.blocks { + block.resolve(&mut stdin().lock())?; + println!("{}", block.as_text()?); } - pub fn parse(&mut self) -> Result<Vec<Block>> { - let mut blocks: Vec<Block> = vec![]; - let title = Comment::new(self.parse_title()?.as_str()); - let descrition = self - .parse_description()? - .map(|desc| Comment::new(desc.as_str())); - blocks.push(Block::new(title, descrition)); - - Ok(blocks) - } + Ok(()) } #[cfg(test)] mod tests { + use std::{io::Cursor, path::PathBuf}; + use super::*; #[test] @@ -104,23 +88,21 @@ mod tests { .join(".env.sample") .into_os_string() .into_string(); - let parsed = Parser::new(&sample.unwrap()).unwrap().parse().unwrap(); - let got = parsed - .iter() - .map(|block| block.to_string()) + let mut parser = Parser::new(&sample.unwrap()); + parser.parse().unwrap(); + let got = parser + .blocks + .iter_mut() + .map(|block| { + block.resolve(&mut Cursor::new("foobar")).unwrap(); + block.as_text().unwrap() + }) .collect::<Vec<String>>() .join("\n"); + + // TODO: include variables (currently it is just title and description) let expected = "# Createnv\n# This is a simple example of how Createnv works".to_string(); + assert_eq!(expected, got); } } -// -// TODO: remove (just written for manual tests & debug) -pub fn parser_cli(path: &String) -> Result<()> { - let mut parser = Parser::new(path)?; - for block in parser.parse()? { - println!("{block}"); - } - - Ok(()) -} diff --git a/src/reader.rs b/src/reader.rs deleted file mode 100644 index d8cebfd..0000000 --- a/src/reader.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::{ - fs::File, - io::{BufRead, BufReader}, - path::PathBuf, -}; - -use anyhow::Result; - -#[derive(PartialEq, Eq)] -pub enum CharType { - Char(char), - Eol, - Eof, -} - -pub struct CharReader { - pub line: usize, - pub column: usize, - current_line: Option<String>, - reader: BufReader<File>, - done: bool, -} - -impl CharReader { - pub fn new(path: PathBuf) -> Result<Self> { - Ok(Self { - line: 0, - column: 0, - current_line: None, - done: false, - reader: BufReader::new(File::open(path)?), - }) - } - - pub fn next(&mut self) -> Result<CharType> { - if self.done { - return Ok(CharType::Eof); - } - match &self.current_line { - None => { - let mut buffer = "".to_string(); - let size = self.reader.read_line(&mut buffer)?; - if size == 0 { - self.done = true; - return Ok(CharType::Eof); - } - self.current_line = Some(buffer.clone()); - self.line += 1; - self.column = 0; - self.next() - } - Some(line) => match line.chars().nth(self.column) { - Some(char) => match char { - '\n' => { - self.current_line = None; - Ok(CharType::Eol) - } - _ => { - self.column += 1; - Ok(CharType::Char(char)) - } - }, - None => { - self.current_line = None; - Ok(CharType::Eol) - } - }, - } - } -} diff --git a/src/tokenizer.rs b/src/tokenizer.rs deleted file mode 100644 index d3cf4bd..0000000 --- a/src/tokenizer.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; - -use crate::reader::{CharReader, CharType}; - -#[derive(Debug, PartialEq)] -pub enum Token { - Text(usize, usize, String), - CommentMark(usize, usize), - HelpMark(usize, usize), - EqualSign(usize, usize), -} - -impl Token { - pub fn error_prefix(&self, path: &String) -> String { - let (line, column) = match self { - Token::Text(x, y, _) => (x, y), - Token::CommentMark(x, y) => (x, y), - Token::HelpMark(x, y) => (x, y), - Token::EqualSign(x, y) => (x, y), - }; - - format!("{path}:{line}:{column}") - } -} - -pub struct Tokenizer { - reader: CharReader, - buffer: Option<Token>, -} - -impl Tokenizer { - pub fn new(path: PathBuf) -> Result<Self> { - Ok(Self { - reader: CharReader::new(path)?, - buffer: None, - }) - } - - fn text(&self, buffer: String, eol: bool, prepends_help: bool) -> Token { - let adjust = match (eol, prepends_help) { - (true, false) => -1, - (false, true) => 2, - _ => 0, - } + (buffer.len() as i8); - - Token::Text( - self.reader.line, - self.reader.column - (adjust as usize), - buffer.trim().to_string(), - ) - } - - fn equal_sign(&self) -> Token { - Token::EqualSign(self.reader.line, self.reader.column) - } - fn comment_mark(&self) -> Token { - Token::CommentMark(self.reader.line, self.reader.column) - } - fn help_mark(&self) -> Token { - Token::HelpMark(self.reader.line, self.reader.column - 2) - } - - fn next_tokens(&mut self) -> Result<(Option<Token>, Option<Token>)> { - let mut buffer = "".to_string(); - loop { - let char = self.reader.next()?; - match char { - CharType::Eof => return Ok((None, None)), - CharType::Eol => { - if buffer.is_empty() { - continue; - } - return Ok((Some(self.text(buffer, true, false)), None)); - } - CharType::Char(c) => { - let mut token: Option<Token> = None; - let mut prepends_help = false; - if c == '=' { - token = Some(self.equal_sign()); - } else if c == '#' { - if self.reader.column == 1 { - token = Some(self.comment_mark()); - } else if buffer.ends_with(" ") { - buffer = buffer.strip_suffix(" ").unwrap_or("").to_string(); - prepends_help = true; - token = Some(self.help_mark()); - } - } - if let Some(t) = token { - if buffer.is_empty() { - return Ok((Some(t), None)); - } - return Ok((Some(self.text(buffer, false, prepends_help)), Some(t))); - } - buffer.push(c) - } - } - } - } -} - -impl Iterator for Tokenizer { - type Item = Result<Token>; - - fn next(&mut self) -> Option<Self::Item> { - if let Some(token) = self.buffer.take() { - return Some(Ok(token)); - } - - match self.next_tokens() { - Ok((Some(first), Some(second))) => { - self.buffer = Some(second); - Some(Ok(first)) - } - Ok((Some(token), None)) => Some(Ok(token)), - Ok((None, Some(token))) => Some(Ok(token)), - Ok((None, None)) => None, - Err(e) => Some(Err(e)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tokenizer() { - let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env.sample"); - let tokenizer = Tokenizer::new(sample).unwrap(); - let tokens: Vec<Token> = tokenizer.map(|t| t.unwrap()).collect(); - assert_eq!(tokens.len(), 19); - - // line 1 - assert_eq!(tokens[0], Token::CommentMark(1, 1)); - assert_eq!(tokens[1], Token::Text(1, 2, "Createnv".to_string())); - assert_eq!(tokens[2], Token::CommentMark(2, 1)); - - // line 2 - assert_eq!( - tokens[3], - Token::Text( - 2, - 2, - "This is a simple example of how Createnv works".to_string() - ) - ); - - // line 3 - assert_eq!(tokens[4], Token::Text(3, 1, "NAME".to_string())); - assert_eq!(tokens[5], Token::EqualSign(3, 5)); - assert_eq!(tokens[6], Token::HelpMark(3, 6)); - assert_eq!( - tokens[7], - Token::Text(3, 9, "What's your name?".to_string()) - ); - - // line 4 - assert_eq!(tokens[8], Token::Text(4, 1, "GREETING".to_string())); - assert_eq!(tokens[9], Token::EqualSign(4, 9)); - assert_eq!(tokens[10], Token::Text(4, 10, "Hello, {NAME}!".to_string())); - - // line 5 - assert_eq!( - tokens[11], - Token::Text(5, 1, "DO_YOU_LIKE_OPEN_SOURCE".to_string()) - ); - assert_eq!(tokens[12], Token::EqualSign(5, 24)); - assert_eq!(tokens[13], Token::Text(5, 25, "True".to_string())); - assert_eq!(tokens[14], Token::HelpMark(5, 29)); - assert_eq!( - tokens[15], - Token::Text(5, 32, "Do you like open-source?".to_string()) - ); - - // line 6 - assert_eq!(tokens[16], Token::Text(6, 1, "PASSWORD".to_string())); - assert_eq!(tokens[17], Token::EqualSign(6, 9)); - assert_eq!(tokens[18], Token::Text(6, 10, "<random:16>".to_string())); - } -} - -// TODO: remove (just written for manual tests & debug) -pub fn tokenize_cli(path: &String) -> Result<()> { - for token in Tokenizer::new(PathBuf::from(path))? { - println!("{:?}", token?); - } - if let Some(token) = Tokenizer::new(PathBuf::from(path))?.next() { - println!( - "\nThe error prefix looks like:\n{}", - token?.error_prefix(path) - ); - } - - Ok(()) -} From 0fc0bc5875446d8d0b0c34c1f129bdf3e55aad38 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Fri, 30 Aug 2024 22:18:09 -0400 Subject: [PATCH 23/28] Finishes minimum viable parser --- src/model.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/model.rs b/src/model.rs index ce5c61a..b35598f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -41,9 +41,9 @@ trait Variable { #[derive(Clone, Debug)] pub struct SimpleVariable { - name: String, - default: Option<String>, - help: Option<String>, + pub name: String, + pub default: Option<String>, + pub help: Option<String>, input: Option<String>, } @@ -97,13 +97,13 @@ impl Variable for SimpleVariable { #[derive(Clone, Debug)] pub struct AutoGeneratedVariable { - name: String, + pub name: String, pattern: String, context: HashMap<String, String>, } impl AutoGeneratedVariable { - fn new(name: &str, pattern: &str) -> Self { + pub fn new(name: &str, pattern: &str) -> Self { Self { name: name.to_string(), pattern: pattern.to_string(), @@ -134,12 +134,12 @@ impl Variable for AutoGeneratedVariable { #[derive(Clone, Debug)] pub struct VariableWithRandomValue { - name: String, + pub name: String, value: String, } impl VariableWithRandomValue { - fn new(name: &str, length: Option<i32>) -> Self { + pub fn new(name: &str, length: Option<usize>) -> Self { let name = name.to_string(); let mut rng = thread_rng(); let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); From 2add4c3d7505e4fd02174315046d40f51e05bb0f Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Fri, 30 Aug 2024 23:32:46 -0400 Subject: [PATCH 24/28] CLI MVP in Rust --- src/lib.rs | 17 ----- src/main.rs | 23 +++++-- src/model.rs | 168 ++++++++++++++++++++++-------------------------- src/parser.rs | 175 ++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 228 insertions(+), 155 deletions(-) delete mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 468930d..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod model; -mod parser; - -use anyhow::Result; -use model::model_to_text_cli; -use parser::parser_cli; - -pub const DEFAULT_ENV_SAMPLE: &str = ".env.sample"; -pub const DEFAULT_ENV: &str = ".env"; - -pub fn model_to_text() -> Result<()> { - model_to_text_cli() -} - -pub fn parser(path: &str) -> Result<()> { - parser_cli(path) -} diff --git a/src/main.rs b/src/main.rs index f8fb4f2..85fe1a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,25 @@ -use std::env::args; +use std::{ + env::args, + fs::File, + io::{stdin, Write}, +}; use anyhow::Result; -use createnv::{model_to_text, parser, DEFAULT_ENV_SAMPLE}; +use parser::Parser; + +mod model; +mod parser; + +const DEFAULT_ENV_SAMPLE: &str = ".env.sample"; +const DEFAULT_ENV: &str = ".env"; fn main() -> Result<()> { let path = args() .nth(1) .unwrap_or_else(|| DEFAULT_ENV_SAMPLE.to_string()); - if path == "--debug" { - model_to_text()?; - return Ok(()); - } - parser(&path)?; + let mut parser = Parser::new(path.as_str())?; + parser.parse(&mut stdin().lock())?; + let mut output = File::create(DEFAULT_ENV)?; + output.write_all(parser.to_string().as_bytes())?; Ok(()) } diff --git a/src/model.rs b/src/model.rs index b35598f..3abca1b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,14 +1,13 @@ use std::fmt; use std::{ collections::HashMap, + fmt::Display, io::{stdout, BufRead, Write}, }; use anyhow::Result; use rand::{thread_rng, Rng}; -use crate::DEFAULT_ENV; - const DEFAULT_RANDOM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; @@ -32,11 +31,7 @@ impl fmt::Display for Comment { } trait Variable { - fn key(&self) -> String; - fn value(&self) -> Result<String>; - fn as_text(&self) -> Result<String> { - Ok(format!("{}={}", self.key(), self.value()?)) - } + fn value(&self) -> String; } #[derive(Clone, Debug)] @@ -57,7 +52,7 @@ impl SimpleVariable { } } - fn ask_for_input<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + fn resolve<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { match (&self.help, &self.default) { (Some(h), Some(d)) => print!("{} [{}]: ", h, d), (Some(h), None) => print!("{}: ", h), @@ -71,7 +66,7 @@ impl SimpleVariable { let value = input.trim(); if value.is_empty() && self.default.is_none() { - return self.ask_for_input(terminal); + return self.resolve(terminal); } if !value.is_empty() { self.input = Some(value.to_string()); @@ -80,18 +75,24 @@ impl SimpleVariable { } } -impl Variable for SimpleVariable { - fn key(&self) -> String { - self.name.clone() +impl Display for SimpleVariable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}={}", self.name, self.value()) } - fn value(&self) -> Result<String> { +} + +impl Variable for SimpleVariable { + fn value(&self) -> String { if let Some(input) = &self.input { - return Ok(input.clone()); + return input.clone(); } if let Some(default) = &self.default { - return Ok(default.clone()); + return default.clone(); } - Err(anyhow::anyhow!("Variable {} has no value", self.name)) + panic!( + "Tryinyg to read the value of a {} before resolving it", + self.name + ); } } @@ -117,18 +118,20 @@ impl AutoGeneratedVariable { } } } +impl Display for AutoGeneratedVariable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}={}", self.name, self.value()) + } +} impl Variable for AutoGeneratedVariable { - fn key(&self) -> String { - self.name.clone() - } - fn value(&self) -> Result<String> { + fn value(&self) -> String { let mut value: String = self.pattern.clone(); for (k, v) in self.context.iter() { let key = format!("{{{}}}", *k); value = value.replace(&key, v); } - Ok(value) + value } } @@ -156,12 +159,15 @@ impl VariableWithRandomValue { } } -impl Variable for VariableWithRandomValue { - fn key(&self) -> String { - self.name.clone() +impl Display for VariableWithRandomValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}={}", self.name, self.value()) } - fn value(&self) -> Result<String> { - Ok(self.value.clone()) +} + +impl Variable for VariableWithRandomValue { + fn value(&self) -> String { + self.value.clone() } } @@ -188,26 +194,32 @@ impl Block { } } - fn has_auto_generated_variables(&self) -> bool { + fn has_auto_input_variables(&self) -> bool { self.variables .iter() - .any(|v| matches!(v, VariableType::AutoGenerated(_))) + .any(|v| matches!(v, VariableType::Input(_))) } - pub fn push(&mut self, variable: VariableType) -> Result<()> { - self.variables.push(variable); - if !self.has_auto_generated_variables() { - return Ok(()); - } - - Ok(()) + fn has_auto_generated_variables(&self) -> bool { + self.variables + .iter() + .any(|v| matches!(v, VariableType::AutoGenerated(_))) } pub fn resolve<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + if self.has_auto_input_variables() { + println!( + "{}", + self.title.to_string().strip_prefix("# ").unwrap_or("") + ); + if let Some(desc) = &self.description { + println!("{}", desc.to_string().strip_prefix("# ").unwrap_or("")); + } + } for variable in &mut self.variables { if let VariableType::Input(var) = variable { if var.input.is_none() { - var.ask_for_input(terminal)?; + var.resolve(terminal)?; } } } @@ -218,8 +230,8 @@ impl Block { for var in &self.variables { match var { VariableType::AutoGenerated(_) => None, - VariableType::Input(v) => context.insert(v.key(), v.value()?), - VariableType::Random(v) => context.insert(v.key(), v.value()?), + VariableType::Input(v) => context.insert(v.name.clone(), v.value()), + VariableType::Random(v) => context.insert(v.name.clone(), v.value()), }; } for variable in &mut self.variables { @@ -229,52 +241,26 @@ impl Block { } Ok(()) } +} - pub fn as_text(&mut self) -> Result<String> { - let mut lines: Vec<String> = vec![self.title.to_string()]; +impl Display for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.title)?; if let Some(desc) = &self.description { - lines.push(desc.to_string()); + writeln!(f, "{}", desc)?; } - for variable in &mut self.variables { - match variable { - VariableType::Input(var) => lines.push(var.as_text()?), - VariableType::AutoGenerated(var) => lines.push(var.as_text()?), - VariableType::Random(var) => lines.push(var.as_text()?), - } + for variable in &self.variables { + let content = match variable { + VariableType::Input(var) => var.to_string(), + VariableType::AutoGenerated(var) => var.to_string(), + VariableType::Random(var) => var.to_string(), + }; + write!(f, "{}", content)?; } - Ok(lines.join("\n")) + Ok(()) } } -// TODO: remove (only written for manual tests & debug) -pub fn model_to_text_cli() -> Result<()> { - let variable1 = AutoGeneratedVariable::new("AUTO_GENERATED", "{ANSWER}-{DEFAULT_VALUE_ONE}"); - let variable2 = SimpleVariable::new("ANSWER", None, Some("If you read that book, you know!")); - let variable3 = SimpleVariable::new("AS_TEXT", None, None); - let variable4 = SimpleVariable::new("DEFAULT_VALUE_ONE", Some("default value"), None); - let variable5 = SimpleVariable::new("DEFAULT_VALUE_TWO", Some("default"), None); - let variable6 = VariableWithRandomValue::new("SECRET_KEY", Some(16)); - - let mut block = Block::new( - Comment::new("Here comes a new block!"), - Some(Comment::new("And here comes a description about it.")), - ); - block.push(VariableType::AutoGenerated(variable1))?; - block.push(VariableType::Input(variable2))?; - block.push(VariableType::Input(variable3))?; - block.push(VariableType::Input(variable4))?; - block.push(VariableType::Input(variable5))?; - block.push(VariableType::Random(variable6))?; - block.resolve(&mut std::io::stdin().lock())?; - - println!( - "\nThis would be written to {}:\n\n{}", - DEFAULT_ENV, - block.as_text()? - ); - Ok(()) -} - #[cfg(test)] mod tests { use std::io::Cursor; @@ -290,21 +276,21 @@ mod tests { #[test] fn test_variable() { let mut var = SimpleVariable::new("ANSWER", None, None); - var.ask_for_input(&mut Cursor::new("42")).unwrap(); - assert_eq!(var.as_text().unwrap(), "ANSWER=42") + var.resolve(&mut Cursor::new("42")).unwrap(); + assert_eq!(format!("{}", var), "ANSWER=42") } #[test] fn test_empty_variable_with_default_value() { let var = SimpleVariable::new("ANSWER", Some("42"), None); - assert_eq!(var.as_text().unwrap(), "ANSWER=42") + assert_eq!(format!("{}", var), "ANSWER=42"); } #[test] fn test_variable_with_default_value_and_input() { let mut var = SimpleVariable::new("ANSWER", Some("42"), None); - var.ask_for_input(&mut Cursor::new("forty two")).unwrap(); - assert_eq!(var.as_text().unwrap(), "ANSWER=forty two") + var.resolve(&mut Cursor::new("forty two")).unwrap(); + assert_eq!(format!("{}", var), "ANSWER=forty two"); } #[test] @@ -314,13 +300,13 @@ mod tests { ctx.insert("FIRST".to_string(), "Forty".to_string()); ctx.insert("SECOND".to_string(), "two".to_string()); var.load_context(&ctx); - assert_eq!(var.as_text().unwrap(), "ANSWER=Forty two") + assert_eq!(format!("{}", var), "ANSWER=Forty two"); } #[test] fn test_variable_with_random_value() { let var = VariableWithRandomValue::new("ANSWER", None); - let got = var.as_text().unwrap(); + let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert!(suffix.chars().count() >= 64); assert!(suffix.chars().count() <= 128); @@ -331,7 +317,7 @@ mod tests { #[test] fn test_variable_with_random_value_of_fixed_length() { let var = VariableWithRandomValue::new("ANSWER", Some(42)); - let got = var.as_text().unwrap(); + let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert_eq!(suffix.chars().count(), 42); let prefix = got.strip_suffix(suffix).unwrap(); @@ -343,12 +329,14 @@ mod tests { let title = Comment::new("42"); let description = Some(Comment::new("Forty-two")); let mut variable1 = SimpleVariable::new("ANSWER", None, None); - variable1.ask_for_input(&mut Cursor::new("42")).unwrap(); + variable1.resolve(&mut Cursor::new("42")).unwrap(); let variable2 = SimpleVariable::new("AS_TEXT", Some("forty two"), None); let mut block = Block::new(title, description); - block.push(VariableType::Input(variable1)).unwrap(); - block.push(VariableType::Input(variable2)).unwrap(); - let got = block.as_text().unwrap(); - assert_eq!(got, "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two") + block.variables.push(VariableType::Input(variable1)); + block.variables.push(VariableType::Input(variable2)); + assert_eq!( + block.to_string(), + "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two" + ) } } diff --git a/src/parser.rs b/src/parser.rs index 8a6c1f2..9e97cc0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,11 +1,18 @@ use std::{ + fmt::Display, fs::File, - io::{stdin, BufRead, BufReader}, + io::{BufRead, BufReader}, }; use anyhow::Result; +use regex::Regex; -use crate::model::{Block, Comment}; +const RANDOM_VARIABLE_PATTERN: &str = r"\<random(:(?P<size>\d*))?\>"; +const AUTO_GENERATED_PATTERN: &str = r"\{[A-Z0-9_]+\}"; + +use crate::model::{ + AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, VariableWithRandomValue, +}; enum Expecting { Title, @@ -13,67 +20,132 @@ enum Expecting { Variables, } -struct Parser { - blocks: Vec<Block>, +pub struct Parser { + pub blocks: Vec<Block>, buffer: Option<Block>, path: String, state: Expecting, + random_pattern: Regex, + auto_generated_pattern: Regex, } impl Parser { - pub fn new(path: &str) -> Self { - Self { + pub fn new(path: &str) -> Result<Self> { + Ok(Self { blocks: vec![], buffer: None, path: path.to_string(), state: Expecting::Title, + random_pattern: Regex::new(RANDOM_VARIABLE_PATTERN)?, + auto_generated_pattern: Regex::new(AUTO_GENERATED_PATTERN)?, + }) + } + + fn flush<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + if let Some(block) = self.buffer.as_mut() { + block.resolve(terminal)?; + self.blocks.push(block.clone()); + self.buffer = None + } + Ok(()) + } + + fn parse_random_variable(&self, name: &str, value: &str) -> Result<VariableWithRandomValue> { + if let Some(matches) = self.random_pattern.captures(value) { + let size = &matches["size"]; + let length = if size.is_empty() { + None + } else { + let number: usize = size.parse()?; + Some(number) + }; + return Ok(VariableWithRandomValue::new(name, length)); } + Err(anyhow::anyhow!("Invalid random variable: {}", value)) } - pub fn parse(&mut self) -> Result<()> { + fn parse_auto_generated_variable( + &self, + name: &str, + value: &str, + ) -> Result<AutoGeneratedVariable> { + if self.auto_generated_pattern.find(value).is_some() { + return Ok(AutoGeneratedVariable::new(name, value)); + } + Err(anyhow::anyhow!( + "Invalid auto-generated variable: {}", + value + )) + } + + fn parse_variable(&self, line: &str) -> Result<VariableType> { + let (name, rest) = line + .split_once('=') + .ok_or(anyhow::anyhow!("Invalid variable line: {}", line))?; + let (default, help) = match rest.split_once(" # ") { + Some((default, help)) => (Some(default), Some(help)), + None => (Some(rest), None), + }; + if let Some(val) = default { + if let Ok(v) = self.parse_random_variable(name, val) { + return Ok(VariableType::Random(v)); + } + if let Ok(v) = self.parse_auto_generated_variable(name, val) { + return Ok(VariableType::AutoGenerated(v)); + } + } + let variable = SimpleVariable::new(name, default, help); + Ok(VariableType::Input(variable)) + } + + pub fn parse<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { let reader = BufReader::new(File::open(&self.path)?); for line in reader.lines() { - // TODO: empty line => start new block + let cleaned = line?.trim().to_string(); + if cleaned.is_empty() { + self.flush(terminal)?; + continue; + } match self.state { Expecting::Title => { - if let Some(txt) = line?.strip_prefix('#') { + if let Some(txt) = cleaned.strip_prefix('#') { self.buffer = Some(Block::new(Comment::new(txt.trim()), None)); } self.state = Expecting::DescriptionOrVariables; } Expecting::DescriptionOrVariables => { - if let Some(txt) = line?.strip_prefix('#') { + if let Some(txt) = cleaned.strip_prefix('#') { if let Some(b) = self.buffer.as_mut() { b.description = Some(Comment::new(txt.trim())); } self.state = Expecting::Variables; } else { - // TODO: handle variable line + let variable = self.parse_variable(&cleaned)?; + if let Some(b) = self.buffer.as_mut() { + b.variables.push(variable); + } } } Expecting::Variables => { - // TODO: , + let variable = self.parse_variable(&cleaned)?; + if let Some(b) = self.buffer.as_mut() { + b.variables.push(variable); + } } } } - if let Some(block) = &self.buffer { - self.blocks.push(block.clone()); - self.buffer = None - } + self.flush(terminal)?; Ok(()) } } -// TODO: remove (just written for manual tests & debug) -pub fn parser_cli(path: &str) -> Result<()> { - let mut parser = Parser::new(path); - parser.parse()?; - for block in &mut parser.blocks { - block.resolve(&mut stdin().lock())?; - println!("{}", block.as_text()?); +impl Display for Parser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for block in &self.blocks { + write!(f, "{}", block)?; + } + Ok(()) } - - Ok(()) } #[cfg(test)] @@ -88,21 +160,42 @@ mod tests { .join(".env.sample") .into_os_string() .into_string(); - let mut parser = Parser::new(&sample.unwrap()); - parser.parse().unwrap(); - let got = parser - .blocks - .iter_mut() - .map(|block| { - block.resolve(&mut Cursor::new("foobar")).unwrap(); - block.as_text().unwrap() - }) - .collect::<Vec<String>>() - .join("\n"); - - // TODO: include variables (currently it is just title and description) - let expected = "# Createnv\n# This is a simple example of how Createnv works".to_string(); - - assert_eq!(expected, got); + let mut parser = Parser::new(&sample.unwrap()).unwrap(); + parser.parse(&mut Cursor::new("World")).unwrap(); + assert_eq!(parser.blocks.len(), 1); + assert_eq!(parser.blocks[0].variables.len(), 4); + let names: [&str; 4] = ["NAME", "GREETING", "DO_YOU_LIKE_OPEN_SOURCE", "PASSWORD"]; + for (variable, expected) in parser.blocks[0].variables.iter().zip(names) { + let got = match variable { + VariableType::Input(v) => &v.name, + VariableType::Random(v) => &v.name, + VariableType::AutoGenerated(v) => &v.name, + }; + assert_eq!(got, expected); + } + for (idx, variable) in parser.blocks[0].variables.iter().enumerate() { + if idx == 0 || idx == 2 { + match variable { + VariableType::Input(_) => (), + _ => panic!( + "Expected variable number {} to be Input, got {:?}", + idx + 1, + variable + ), + } + } + if idx == 1 { + match variable { + VariableType::AutoGenerated(_) => (), + _ => panic!("Expected variable to be AutoGenerated, got {:?}", variable), + } + } + if idx == 3 { + match variable { + VariableType::Random(_) => (), + _ => panic!("Expected variable to be Random, got {:?}", variable), + } + } + } } } From 687f562b801335dc000e2ebb7fd2e3e6fdc3bc79 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 31 Aug 2024 01:06:31 -0400 Subject: [PATCH 25/28] Adds options to the CLI --- Cargo.lock | 178 +++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 92 +++++++++++++++++++++++--- src/model.rs | 32 ++++----- src/parser.rs | 94 +++++++++++++++----------- 5 files changed, 333 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7de3fe4..080272d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -29,11 +78,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "createnv" version = "0.0.3" dependencies = [ "anyhow", + "clap", "rand", "regex", ] @@ -49,6 +132,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "libc" version = "0.2.158" @@ -147,11 +236,17 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -164,12 +259,91 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index c5eb977..3394497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" [dependencies] anyhow = "1.0.86" +clap = "4.5.16" rand = "0.8.5" regex = "1.10.6" diff --git a/src/main.rs b/src/main.rs index 85fe1a1..6c17abf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use std::{ - env::args, - fs::File, - io::{stdin, Write}, + fs::{metadata, File}, + io::{stdin, stdout, Write}, + process::exit, }; use anyhow::Result; +use clap::{Arg, ArgAction, Command}; use parser::Parser; mod model; @@ -12,14 +13,89 @@ mod parser; const DEFAULT_ENV_SAMPLE: &str = ".env.sample"; const DEFAULT_ENV: &str = ".env"; +const DEFAULT_RANDOM_CHARS: &str = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; + +fn should_write_to(path: &str) -> Result<bool> { + if metadata(path).is_ok() { + print!( + "{} already exists. Do you want to overwrite it? (y/n) ", + path + ); + stdout().flush()?; + let mut input = String::new(); + stdin().read_line(&mut input)?; + let input = input.trim(); + match input.to_lowercase().as_str() { + "y" | "yes" => { + return Ok(true); + // Perform the overwrite operation here + } + "n" | "no" => { + return Ok(false); + } + _ => return should_write_to(path), + } + } + Ok(true) +} fn main() -> Result<()> { - let path = args() - .nth(1) - .unwrap_or_else(|| DEFAULT_ENV_SAMPLE.to_string()); - let mut parser = Parser::new(path.as_str())?; + let matches = Command::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .arg( + clap::Arg::new("target") + .long("target") + .short('t') + .default_value(DEFAULT_ENV) + .help("File to write the result"), + ) + .arg( + Arg::new("source") + .long("source") + .short('s') + .default_value(DEFAULT_ENV_SAMPLE) + .help("File to use as a sample"), + ) + .arg( + Arg::new("overwrite") + .long("overwrite") + .short('o') + .action(ArgAction::SetTrue) + .help("Overwrites target file without asking for user input"), + ) + .arg( + Arg::new("use-default") + .long("use-default") + .short('u') + .action(ArgAction::SetTrue) + .help("Use default values without asking for user input"), + ) + .arg( + Arg::new("chars-for-random-string") + .long("chars-for-random-string") + .short('c') + .default_value(DEFAULT_RANDOM_CHARS) + .help("Characters used to create random strings"), + ) + .get_matches(); + + let target = matches.get_one::<String>("target").unwrap(); + let overwrite = matches.get_one::<bool>("overwrite").unwrap(); + if !overwrite && !should_write_to(target)? { + exit(0); + } + + let source = matches.get_one::<String>("source").unwrap(); + let use_default = matches.get_one::<bool>("use-default").unwrap(); + let chars = matches + .get_one::<String>("chars-for-random-string") + .unwrap(); + + let mut parser = Parser::new(source.as_str(), chars, use_default)?; parser.parse(&mut stdin().lock())?; - let mut output = File::create(DEFAULT_ENV)?; + + let mut output = File::create(target)?; output.write_all(parser.to_string().as_bytes())?; Ok(()) } diff --git a/src/model.rs b/src/model.rs index 3abca1b..9a94892 100644 --- a/src/model.rs +++ b/src/model.rs @@ -8,9 +8,6 @@ use std::{ use anyhow::Result; use rand::{thread_rng, Rng}; -const DEFAULT_RANDOM_CHARS: &str = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; - #[derive(Clone)] pub struct Comment { contents: String, @@ -77,7 +74,7 @@ impl SimpleVariable { impl Display for SimpleVariable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}={}", self.name, self.value()) + write!(f, "{}={}", self.name, self.value()) } } @@ -120,7 +117,7 @@ impl AutoGeneratedVariable { } impl Display for AutoGeneratedVariable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}={}", self.name, self.value()) + write!(f, "{}={}", self.name, self.value()) } } @@ -142,10 +139,10 @@ pub struct VariableWithRandomValue { } impl VariableWithRandomValue { - pub fn new(name: &str, length: Option<usize>) -> Self { + pub fn new(name: &str, length: Option<usize>, allowed: &str) -> Self { let name = name.to_string(); let mut rng = thread_rng(); - let max_chars_idx = DEFAULT_RANDOM_CHARS.chars().count(); + let max_chars_idx = allowed.chars().count(); let mut value: String = String::from(""); let length = match length { Some(n) => n, @@ -153,7 +150,7 @@ impl VariableWithRandomValue { }; for _ in 0..length { let pos = rng.gen_range(0..max_chars_idx); - value.push(DEFAULT_RANDOM_CHARS.chars().nth(pos).unwrap()) + value.push(allowed.chars().nth(pos).unwrap()) } Self { name, value } } @@ -161,7 +158,7 @@ impl VariableWithRandomValue { impl Display for VariableWithRandomValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}={}", self.name, self.value()) + write!(f, "{}={}", self.name, self.value()) } } @@ -206,7 +203,7 @@ impl Block { .any(|v| matches!(v, VariableType::AutoGenerated(_))) } - pub fn resolve<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + pub fn resolve<T: BufRead>(&mut self, terminal: &mut T, use_default: bool) -> Result<()> { if self.has_auto_input_variables() { println!( "{}", @@ -219,7 +216,11 @@ impl Block { for variable in &mut self.variables { if let VariableType::Input(var) = variable { if var.input.is_none() { - var.resolve(terminal)?; + if use_default && var.default.is_some() { + var.input = var.default.clone(); + } else { + var.resolve(terminal)?; + } } } } @@ -255,7 +256,7 @@ impl Display for Block { VariableType::AutoGenerated(var) => var.to_string(), VariableType::Random(var) => var.to_string(), }; - write!(f, "{}", content)?; + writeln!(f, "{}", content)?; } Ok(()) } @@ -266,6 +267,7 @@ mod tests { use std::io::Cursor; use super::*; + use crate::DEFAULT_RANDOM_CHARS; #[test] fn test_title() { @@ -305,7 +307,7 @@ mod tests { #[test] fn test_variable_with_random_value() { - let var = VariableWithRandomValue::new("ANSWER", None); + let var = VariableWithRandomValue::new("ANSWER", None, DEFAULT_RANDOM_CHARS); let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert!(suffix.chars().count() >= 64); @@ -316,7 +318,7 @@ mod tests { #[test] fn test_variable_with_random_value_of_fixed_length() { - let var = VariableWithRandomValue::new("ANSWER", Some(42)); + let var = VariableWithRandomValue::new("ANSWER", Some(42), DEFAULT_RANDOM_CHARS); let got = var.to_string(); let suffix = got.strip_prefix("ANSWER=").unwrap(); assert_eq!(suffix.chars().count(), 42); @@ -336,7 +338,7 @@ mod tests { block.variables.push(VariableType::Input(variable2)); assert_eq!( block.to_string(), - "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two" + "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two\n" ) } } diff --git a/src/parser.rs b/src/parser.rs index 9e97cc0..afb1e6a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -21,29 +21,33 @@ enum Expecting { } pub struct Parser { - pub blocks: Vec<Block>, - buffer: Option<Block>, path: String, - state: Expecting, + random_chars: String, + use_default: bool, random_pattern: Regex, auto_generated_pattern: Regex, + state: Expecting, + buffer: Option<Block>, + pub blocks: Vec<Block>, } impl Parser { - pub fn new(path: &str) -> Result<Self> { + pub fn new(path: &str, random_chars: &str, use_default: &bool) -> Result<Self> { Ok(Self { - blocks: vec![], - buffer: None, path: path.to_string(), - state: Expecting::Title, + random_chars: random_chars.to_string(), + use_default: *use_default, random_pattern: Regex::new(RANDOM_VARIABLE_PATTERN)?, auto_generated_pattern: Regex::new(AUTO_GENERATED_PATTERN)?, + state: Expecting::Title, + buffer: None, + blocks: vec![], }) } fn flush<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { if let Some(block) = self.buffer.as_mut() { - block.resolve(terminal)?; + block.resolve(terminal, self.use_default)?; self.blocks.push(block.clone()); self.buffer = None } @@ -52,14 +56,15 @@ impl Parser { fn parse_random_variable(&self, name: &str, value: &str) -> Result<VariableWithRandomValue> { if let Some(matches) = self.random_pattern.captures(value) { - let size = &matches["size"]; - let length = if size.is_empty() { - None - } else { - let number: usize = size.parse()?; - Some(number) - }; - return Ok(VariableWithRandomValue::new(name, length)); + let length = matches + .name("size") + .map(|m| m.as_str().parse::<usize>()) + .transpose()?; + return Ok(VariableWithRandomValue::new( + name, + length, + self.random_chars.as_str(), + )); } Err(anyhow::anyhow!("Invalid random variable: {}", value)) } @@ -82,16 +87,20 @@ impl Parser { let (name, rest) = line .split_once('=') .ok_or(anyhow::anyhow!("Invalid variable line: {}", line))?; - let (default, help) = match rest.split_once(" # ") { + let (mut default, help) = match rest.split_once(" # ") { Some((default, help)) => (Some(default), Some(help)), None => (Some(rest), None), }; if let Some(val) = default { - if let Ok(v) = self.parse_random_variable(name, val) { - return Ok(VariableType::Random(v)); - } - if let Ok(v) = self.parse_auto_generated_variable(name, val) { - return Ok(VariableType::AutoGenerated(v)); + if val.is_empty() { + default = None; + } else { + if let Ok(v) = self.parse_random_variable(name, val) { + return Ok(VariableType::Random(v)); + } + if let Ok(v) = self.parse_auto_generated_variable(name, val) { + return Ok(VariableType::AutoGenerated(v)); + } } } let variable = SimpleVariable::new(name, default, help); @@ -104,6 +113,7 @@ impl Parser { let cleaned = line?.trim().to_string(); if cleaned.is_empty() { self.flush(terminal)?; + self.state = Expecting::Title; continue; } match self.state { @@ -141,8 +151,13 @@ impl Parser { impl Display for Parser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; for block in &self.blocks { + if !first { + writeln!(f)?; + } write!(f, "{}", block)?; + first = false; } Ok(()) } @@ -153,6 +168,7 @@ mod tests { use std::{io::Cursor, path::PathBuf}; use super::*; + use crate::DEFAULT_RANDOM_CHARS; #[test] fn test_parser() { @@ -160,7 +176,7 @@ mod tests { .join(".env.sample") .into_os_string() .into_string(); - let mut parser = Parser::new(&sample.unwrap()).unwrap(); + let mut parser = Parser::new(&sample.unwrap(), DEFAULT_RANDOM_CHARS, &false).unwrap(); parser.parse(&mut Cursor::new("World")).unwrap(); assert_eq!(parser.blocks.len(), 1); assert_eq!(parser.blocks[0].variables.len(), 4); @@ -175,26 +191,26 @@ mod tests { } for (idx, variable) in parser.blocks[0].variables.iter().enumerate() { if idx == 0 || idx == 2 { - match variable { - VariableType::Input(_) => (), - _ => panic!( - "Expected variable number {} to be Input, got {:?}", - idx + 1, - variable - ), - } + assert!( + matches!(variable, VariableType::Input(_)), + "Expected variable number {} to be Input, got {:?}", + idx + 1, + variable + ); } if idx == 1 { - match variable { - VariableType::AutoGenerated(_) => (), - _ => panic!("Expected variable to be AutoGenerated, got {:?}", variable), - } + assert!( + matches!(variable, VariableType::AutoGenerated(_)), + "Expected variable 2 to be AutoGenerated, got {:?}", + variable + ); } if idx == 3 { - match variable { - VariableType::Random(_) => (), - _ => panic!("Expected variable to be Random, got {:?}", variable), - } + assert!( + matches!(variable, VariableType::Random(_)), + "Expected variable 4 to be Random, got {:?}", + variable + ); } } } From cd312c612d14806096584f84d52b2224637e244b Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 31 Aug 2024 01:28:23 -0400 Subject: [PATCH 26/28] Drops type for random variable --- src/model.rs | 69 +++------------------------------------------------ src/parser.rs | 49 ++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 90 deletions(-) diff --git a/src/model.rs b/src/model.rs index 9a94892..ed5e0c4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -6,7 +6,6 @@ use std::{ }; use anyhow::Result; -use rand::{thread_rng, Rng}; #[derive(Clone)] pub struct Comment { @@ -132,47 +131,10 @@ impl Variable for AutoGeneratedVariable { } } -#[derive(Clone, Debug)] -pub struct VariableWithRandomValue { - pub name: String, - value: String, -} - -impl VariableWithRandomValue { - pub fn new(name: &str, length: Option<usize>, allowed: &str) -> Self { - let name = name.to_string(); - let mut rng = thread_rng(); - let max_chars_idx = allowed.chars().count(); - let mut value: String = String::from(""); - let length = match length { - Some(n) => n, - None => rng.gen_range(64..=128), - }; - for _ in 0..length { - let pos = rng.gen_range(0..max_chars_idx); - value.push(allowed.chars().nth(pos).unwrap()) - } - Self { name, value } - } -} - -impl Display for VariableWithRandomValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}={}", self.name, self.value()) - } -} - -impl Variable for VariableWithRandomValue { - fn value(&self) -> String { - self.value.clone() - } -} - #[derive(Clone, Debug)] pub enum VariableType { Input(SimpleVariable), AutoGenerated(AutoGeneratedVariable), - Random(VariableWithRandomValue), } #[derive(Clone)] @@ -229,11 +191,9 @@ impl Block { } let mut context = HashMap::new(); for var in &self.variables { - match var { - VariableType::AutoGenerated(_) => None, - VariableType::Input(v) => context.insert(v.name.clone(), v.value()), - VariableType::Random(v) => context.insert(v.name.clone(), v.value()), - }; + if let VariableType::Input(v) = var { + context.insert(v.name.clone(), v.value()); + } } for variable in &mut self.variables { if let VariableType::AutoGenerated(var) = variable { @@ -254,7 +214,6 @@ impl Display for Block { let content = match variable { VariableType::Input(var) => var.to_string(), VariableType::AutoGenerated(var) => var.to_string(), - VariableType::Random(var) => var.to_string(), }; writeln!(f, "{}", content)?; } @@ -267,7 +226,6 @@ mod tests { use std::io::Cursor; use super::*; - use crate::DEFAULT_RANDOM_CHARS; #[test] fn test_title() { @@ -305,27 +263,6 @@ mod tests { assert_eq!(format!("{}", var), "ANSWER=Forty two"); } - #[test] - fn test_variable_with_random_value() { - let var = VariableWithRandomValue::new("ANSWER", None, DEFAULT_RANDOM_CHARS); - let got = var.to_string(); - let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert!(suffix.chars().count() >= 64); - assert!(suffix.chars().count() <= 128); - let prefix = got.strip_suffix(suffix).unwrap(); - assert_eq!(prefix, "ANSWER=") - } - - #[test] - fn test_variable_with_random_value_of_fixed_length() { - let var = VariableWithRandomValue::new("ANSWER", Some(42), DEFAULT_RANDOM_CHARS); - let got = var.to_string(); - let suffix = got.strip_prefix("ANSWER=").unwrap(); - assert_eq!(suffix.chars().count(), 42); - let prefix = got.strip_suffix(suffix).unwrap(); - assert_eq!(prefix, "ANSWER=") - } - #[test] fn test_block_with_description() { let title = Comment::new("42"); diff --git a/src/parser.rs b/src/parser.rs index afb1e6a..e91c57e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5,14 +5,13 @@ use std::{ }; use anyhow::Result; +use rand::{thread_rng, Rng}; use regex::Regex; const RANDOM_VARIABLE_PATTERN: &str = r"\<random(:(?P<size>\d*))?\>"; const AUTO_GENERATED_PATTERN: &str = r"\{[A-Z0-9_]+\}"; -use crate::model::{ - AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType, VariableWithRandomValue, -}; +use crate::model::{AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType}; enum Expecting { Title, @@ -54,19 +53,29 @@ impl Parser { Ok(()) } - fn parse_random_variable(&self, name: &str, value: &str) -> Result<VariableWithRandomValue> { + fn parse_random_variable( + &self, + name: &str, + description: Option<&str>, + value: &str, + ) -> Result<SimpleVariable> { if let Some(matches) = self.random_pattern.captures(value) { + let mut rng = thread_rng(); let length = matches .name("size") .map(|m| m.as_str().parse::<usize>()) - .transpose()?; - return Ok(VariableWithRandomValue::new( - name, - length, - self.random_chars.as_str(), - )); + .transpose()? + .unwrap_or(rng.gen_range(64..=128)); + let max_chars_idx = self.random_chars.chars().count(); + let mut value: String = String::from(""); + for _ in 0..length { + let pos = rng.gen_range(0..max_chars_idx); + value.push(self.random_chars.chars().nth(pos).unwrap()) + } + Ok(SimpleVariable::new(name, Some(value.as_str()), description)) + } else { + Err(anyhow::anyhow!("Invalid random variable: {}", value)) } - Err(anyhow::anyhow!("Invalid random variable: {}", value)) } fn parse_auto_generated_variable( @@ -87,7 +96,7 @@ impl Parser { let (name, rest) = line .split_once('=') .ok_or(anyhow::anyhow!("Invalid variable line: {}", line))?; - let (mut default, help) = match rest.split_once(" # ") { + let (mut default, description) = match rest.split_once(" # ") { Some((default, help)) => (Some(default), Some(help)), None => (Some(rest), None), }; @@ -95,15 +104,15 @@ impl Parser { if val.is_empty() { default = None; } else { - if let Ok(v) = self.parse_random_variable(name, val) { - return Ok(VariableType::Random(v)); + if let Ok(v) = self.parse_random_variable(name, description, val) { + return Ok(VariableType::Input(v)); } if let Ok(v) = self.parse_auto_generated_variable(name, val) { return Ok(VariableType::AutoGenerated(v)); } } } - let variable = SimpleVariable::new(name, default, help); + let variable = SimpleVariable::new(name, default, description); Ok(VariableType::Input(variable)) } @@ -184,13 +193,12 @@ mod tests { for (variable, expected) in parser.blocks[0].variables.iter().zip(names) { let got = match variable { VariableType::Input(v) => &v.name, - VariableType::Random(v) => &v.name, VariableType::AutoGenerated(v) => &v.name, }; assert_eq!(got, expected); } for (idx, variable) in parser.blocks[0].variables.iter().enumerate() { - if idx == 0 || idx == 2 { + if idx != 1 { assert!( matches!(variable, VariableType::Input(_)), "Expected variable number {} to be Input, got {:?}", @@ -205,13 +213,6 @@ mod tests { variable ); } - if idx == 3 { - assert!( - matches!(variable, VariableType::Random(_)), - "Expected variable 4 to be Random, got {:?}", - variable - ); - } } } } From 008339ae6ade12d522dd567b7662f318a985a6e7 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:47:41 -0400 Subject: [PATCH 27/28] Enhances error messages --- src/parser.rs | 107 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 18 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index e91c57e..8af5b25 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -8,10 +8,29 @@ use anyhow::Result; use rand::{thread_rng, Rng}; use regex::Regex; +use crate::model::{AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType}; + +const NAME_PATTERN: &str = r"^[A-Z0-9_]+$"; const RANDOM_VARIABLE_PATTERN: &str = r"\<random(:(?P<size>\d*))?\>"; const AUTO_GENERATED_PATTERN: &str = r"\{[A-Z0-9_]+\}"; -use crate::model::{AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType}; +const HELP_TITLE: &str = "This is the first line of a block. A block is a \ + group of lines separated from others by one (or more) empty line(s). \ + The first line of a block is expected to be a title, that is to say, to \ + start with `# `, the remaining text is considered the title of this block. \ + This line does not match this pattern."; +const HELP_DESCRIPTION: &str = "This is the second line of a block. A block is \ + a group of lines separated from others by one (or more) empty line(s). The \ + second line of a block is expected to be a description of that block or a \ + variable line. The description line should start `# `, and the remaining \ + text is considered the description of this block. A config variable line \ + should start with a name in uppercase, no spaces, followed by an equal \ + sign. No spaces before the equal sign. This lines does not match this \ + expected patterns."; +const HELP_VARIABLE: &str = "This line was expected to be a variable line. The \ + format should be a name using capital ASCII letters, digits or underscore, \ + followed by an equal sign. No spaces before the equal sign. This line does \ + not match this expected pattern."; enum Expecting { Title, @@ -19,10 +38,23 @@ enum Expecting { Variables, } +impl Display for Expecting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expecting::Title => write!(f, "expecting a block title"), + Expecting::DescriptionOrVariables => { + write!(f, "expecting a block description or a variable line") + } + Expecting::Variables => write!(f, "expecting a variable line"), + } + } +} + pub struct Parser { path: String, random_chars: String, use_default: bool, + name_pattern: Regex, random_pattern: Regex, auto_generated_pattern: Regex, state: Expecting, @@ -36,6 +68,7 @@ impl Parser { path: path.to_string(), random_chars: random_chars.to_string(), use_default: *use_default, + name_pattern: Regex::new(NAME_PATTERN)?, random_pattern: Regex::new(RANDOM_VARIABLE_PATTERN)?, auto_generated_pattern: Regex::new(AUTO_GENERATED_PATTERN)?, state: Expecting::Title, @@ -44,15 +77,6 @@ impl Parser { }) } - fn flush<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { - if let Some(block) = self.buffer.as_mut() { - block.resolve(terminal, self.use_default)?; - self.blocks.push(block.clone()); - self.buffer = None - } - Ok(()) - } - fn parse_random_variable( &self, name: &str, @@ -92,10 +116,21 @@ impl Parser { )) } - fn parse_variable(&self, line: &str) -> Result<VariableType> { - let (name, rest) = line - .split_once('=') - .ok_or(anyhow::anyhow!("Invalid variable line: {}", line))?; + fn parse_variable(&self, pos: usize, line: &str) -> Result<VariableType> { + let (name, rest) = line.split_once('=').ok_or(anyhow::anyhow!( + "Invalid variable line on line {}: {}\nHint: {}", + pos, + line, + HELP_VARIABLE + ))?; + if !self.name_pattern.is_match(name) { + return Err(anyhow::anyhow!( + "Invalid variable name on line {}: {}\nHint :{}", + pos, + name, + HELP_VARIABLE + )); + } let (mut default, description) = match rest.split_once(" # ") { Some((default, help)) => (Some(default), Some(help)), None => (Some(rest), None), @@ -116,9 +151,20 @@ impl Parser { Ok(VariableType::Input(variable)) } + fn flush<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + if let Some(block) = self.buffer.as_mut() { + block.resolve(terminal, self.use_default)?; + self.blocks.push(block.clone()); + self.buffer = None + } + Ok(()) + } + pub fn parse<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { let reader = BufReader::new(File::open(&self.path)?); - for line in reader.lines() { + let mut cursor: usize = 0; + for (idx, line) in reader.lines().enumerate() { + cursor = idx + 1; let cleaned = line?.trim().to_string(); if cleaned.is_empty() { self.flush(terminal)?; @@ -129,8 +175,15 @@ impl Parser { Expecting::Title => { if let Some(txt) = cleaned.strip_prefix('#') { self.buffer = Some(Block::new(Comment::new(txt.trim()), None)); + self.state = Expecting::DescriptionOrVariables; + } else { + return Err(anyhow::anyhow!( + "Unexpected title on line {}: {}\nHint: {}", + cursor, + cleaned, + HELP_TITLE + )); } - self.state = Expecting::DescriptionOrVariables; } Expecting::DescriptionOrVariables => { if let Some(txt) = cleaned.strip_prefix('#') { @@ -139,20 +192,38 @@ impl Parser { } self.state = Expecting::Variables; } else { - let variable = self.parse_variable(&cleaned)?; + let variable = self.parse_variable(cursor, &cleaned)?; if let Some(b) = self.buffer.as_mut() { b.variables.push(variable); } } } Expecting::Variables => { - let variable = self.parse_variable(&cleaned)?; + let variable = self.parse_variable(cursor, &cleaned)?; if let Some(b) = self.buffer.as_mut() { b.variables.push(variable); } } } } + let last_block_has_variables = self + .buffer + .as_ref() + .map(|block| !block.variables.is_empty()) + .unwrap_or(false); + if !last_block_has_variables { + let help = match self.state { + Expecting::Title => HELP_TITLE, + Expecting::DescriptionOrVariables => HELP_DESCRIPTION, + Expecting::Variables => HELP_VARIABLE, + }; + return Err(anyhow::anyhow!( + "Unexpected EOF while {} at line {}: the last block has no variables\nHint: {}", + self.state, + cursor, + help + )); + } self.flush(terminal)?; Ok(()) } From 1fd51cd03e541a3f6294e3345155d804e38506fc Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:58:35 -0400 Subject: [PATCH 28/28] Updates the docs --- README.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 10a41ee..ac9f662 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ -# Createnv - -[![GitHub Actions: Tests](https://github.com/cuducos/createnv/workflows/Tests/badge.svg)]() -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/createnv)](https://pypi.org/project/createnv/) -[![PyPI](https://img.shields.io/pypi/v/createnv)](https://pypi.org/project/createnv/) - +# Createnv [![Tests](https://github.com/cuducos/createnv/actions/workflows/tests.yml/badge.svg)](https://github.com/cuducos/createnv/actions/workflows/tests.yml) A simple CLI to create `.env` files. ## Motivation @@ -22,10 +17,10 @@ You can now experiment by yourself, or try more advanced `.env.sample` such as t ## Install -Createnv requires [Python](https://python.org) 3.7 or newer: +Createnv requires [Rust's `cargo`](https://www.rust-lang.org/tools/install): ```console -$ pip install createnv +$ cargo install --path . ``` ## Usage @@ -38,13 +33,13 @@ $ createnv ### Options -| Option | Default | Description | +| Option | Description | Default | |---|---|---| -| `--target` | `.env` | File to write the result | -| `--source` | `.env.sample` | File to use as a sample | -| `--overwrite` and `--no-overwrite` | `--no-overwrite` | Whether to ask before overwriting files -| `--use-default` or `--no-use-default` | `--no-use-default` | Whether to ask for input on fields that have a default value set | -| `--chars-for-random-string` | All ASCII letters, numbers and a few extra characters (`!@#$%^&*(-_=+)`) | Characters used to create random strings | +| `--target` | File to write the result | `.env` | +| `--source` | File to use as a sample | `.env.sample` | +| `--chars-for-random-string` | Characters used to create random strings | All ASCII letters, numbers and a few extra characters (`!@#$%^&*(-_=+)`) | +| `--overwrite` | Do not ask before overwriting files | | +| `--use-default` | Do not ask for input on fields that have a default value | | ## Format of sample files @@ -106,7 +101,7 @@ Now it's a complete variable with a name (_NAME_), a default value (_Cuducos_), If you want to have a variable with a random value, you can set its default value to `<random>` and Createnv will take care of it. Optionally you can specify how long this variable should be with `:int`. -You can use the [`--chars-for-random-string` option](#options) to specify which characters to be used in the random value. +You can use the `--chars-for-random-string` option to specify which characters to be used in the random value. ##### Example