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, 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, } @@ -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, variables: Vec, - - context: HashMap, } impl Block { - 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, VariableType::AutoGenerated(_))); - - let mut block = Self { + pub fn new(title: Comment, description: Option) -> 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 = 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, + current_token: Option, +} + +impl Parser { + pub fn new(path: &String) -> Result { + 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 { + 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> { + 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> { + let mut blocks: Vec = 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::>() + .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, }