//! Copyright (C) 2022, Dmitriy Pleshevskiy //! //! vnetod is free software: you can redistribute it and/or modify //! it under the terms of the GNU General Public License as published by //! the Free Software Foundation, either version 3 of the License, or //! (at your option) any later version. //! //! vnetod is distributed in the hope that it will be useful, //! but WITHOUT ANY WARRANTY; without even the implied warranty of //! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //! GNU General Public License for more details. //! //! You should have received a copy of the GNU General Public License //! along with vnetod. If not, see . //! use std::io::{BufWriter, Write}; use super::Section; #[derive(Debug)] pub enum Error { WriteData, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::WriteData => f.write_str("Cannot write data"), } } } impl std::error::Error for Error {} pub struct Request<'args, W> where W: Write, { pub content: &'args str, pub writer: W, pub sections: &'args [String], } pub fn execute(req: Request) -> Result<(), Error> where W: Write, { let mut writer = BufWriter::new(req.writer); let choose_sections = req .sections .iter() .map(|s| Section::parse(s.as_str())) .collect::>(); let mut current_sections: Option> = None; for line in req.content.split_inclusive('\n') { let new_line = if is_section_end(line) { current_sections = None; line.to_string() } else if let Some(section_info) = line.strip_prefix("### ") { current_sections = section_info .split_whitespace() .next() .map(|r| r.split(',').map(Section::parse).collect()); line.to_string() } else if let Some(cur_sections) = current_sections.clone() { let trimmed_line = line.trim_start_matches(['#', ' ']); if !is_variables(trimmed_line) { line.to_string() } else if should_enable_variable(&choose_sections, &cur_sections) { String::from(trimmed_line) } else if should_disable_variable(&choose_sections, &cur_sections) { format!("# {}", trimmed_line) } else { line.to_string() } } else { line.to_string() }; writer .write_all(new_line.as_bytes()) .map_err(|_| Error::WriteData)?; } writer.flush().map_err(|_| Error::WriteData) } fn is_variables(trimmed_line: &str) -> bool { trimmed_line .chars() .filter(|ch| (*ch != '_' && ch.is_ascii_punctuation()) || ch.is_whitespace()) .enumerate() .find(|(_, ch)| *ch == '=') .map_or(false, |(i, _)| i == 0) } fn is_section_end(line: &str) -> bool { line.trim().is_empty() } fn should_enable_variable(choose_sections: &[Section], current_sections: &[Section]) -> bool { let cross_sections = choose_sections .iter() .filter(|s| { s.namespace.is_some() && current_sections .iter() .any(|s2| s.namespace == s2.namespace) }) .collect::>(); if cross_sections.is_empty() { choose_sections.iter().any(|s| { if s.namespace.is_none() { current_sections.iter().any(|s2| s.name == s2.name) } else { current_sections.contains(s) } }) } else { cross_sections.iter().any(|s| current_sections.contains(s)) } } fn should_disable_variable(choose_sections: &[Section], current_sections: &[Section]) -> bool { choose_sections.is_empty() || choose_sections.iter().any(|s| s.namespace.is_none()) || choose_sections .iter() .filter(|s| s.namespace.is_some()) .any(|s| { current_sections .iter() .any(|s2| s.namespace == s2.namespace && s.name != s2.name) }) } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; const BASE_ENV: &str = include_str!("../../test_data/base_env"); fn make_test(input: &str, expected_output: &str, sections: Vec<&str>) { let mut output_data = vec![]; let writer = Cursor::new(&mut output_data); match execute(Request { content: input, writer, sections: §ions.into_iter().map(String::from).collect::>(), }) { Ok(()) => { let output = String::from_utf8(output_data).unwrap(); assert_eq!( output.lines().collect::>(), expected_output.lines().collect::>() ); } _ => unreachable!(), } } #[test] fn should_disable_all_sections() { make_test(include_str!("../../test_data/all_env"), BASE_ENV, vec![]); } #[test] fn should_enable_local_sections() { make_test( BASE_ENV, include_str!("../../test_data/should_enable_local"), vec!["local"], ); } #[test] fn should_enable_staging_sections() { make_test( BASE_ENV, include_str!("../../test_data/should_enable_staging"), vec!["staging"], ); } #[test] fn should_use_debug_in_staging_section() { make_test( BASE_ENV, include_str!("../../test_data/should_use_debug_in_staging"), vec!["staging", "debug:on"], ); } #[test] fn should_use_staging_db_in_local_section() { make_test( BASE_ENV, include_str!("../../test_data/should_use_staging_db_in_local"), vec!["local", "db:staging"], ); } mod utils { use super::*; #[test] fn should_not_enable_variables() { assert!(!should_enable_variable( &[Section::new("local")], &[Section::new("staging")] )); assert!(!should_enable_variable( &[Section::with_namespace("db", "local")], &[Section::new("local")] )); assert!(!should_enable_variable( &[Section::with_namespace("db", "local")], &[Section::with_namespace("db", "staging")] )); assert!(!should_enable_variable( &[ Section::new("staging"), Section::with_namespace("debug", "on") ], &[ Section::with_namespace("debug", "off"), Section::new("staging") ] )); } #[test] fn should_enable_variables() { assert!(should_enable_variable( &[Section::new("local")], &[Section::new("local")] )); assert!(should_enable_variable( &[Section::new("local")], &[Section::with_namespace("db", "local")] )); assert!(should_enable_variable( &[Section::with_namespace("db", "local")], &[Section::with_namespace("db", "local")] )); assert!(should_enable_variable( &[ Section::new("local"), Section::with_namespace("debug", "on") ], &[ Section::with_namespace("debug", "on"), Section::new("staging") ] )); } #[test] fn should_not_disable_variables() { assert!(!should_disable_variable( &[Section::with_namespace("debug", "on")], &[Section::new("local")] )); } #[test] fn should_disable_variables() { assert!(should_disable_variable( &[Section::new("local")], &[Section::with_namespace("debug", "off")] )); assert!(should_disable_variable( &[], &[Section::with_namespace("debug", "off")] )); assert!(should_disable_variable( &[Section::with_namespace("debug", "on")], &[Section::with_namespace("debug", "off")] )); } #[test] fn should_check_line_on_variable() { let test_cases = [ ("VAR=10", true), ("THIS_IS_MY_VAR='hello world'", true), ("hello world", false), ("staging/production", false), ("staging/production=value", false), ]; for (input, expected) in test_cases { assert_eq!(is_variables(input), expected); } } } }