use super::{
header::{ConfigHeader, CredentialHeader},
nested_setting::NestedSetting,
whitespace::Whitespace,
SectionName, SectionType, Setting, SettingName, Value, ValueType,
};
use crate::lexer::{Parsable, ParserOutput};
use nom::multi::many0;
use std::{
fmt::{Debug, Display},
hash::Hash,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Section<T> {
pub(crate) leading_whitespace: Whitespace,
pub(crate) header: T,
pub(crate) settings: Vec<Setting>,
pub(crate) trailing_whitespace: Whitespace,
}
impl<T> Section<T>
where
T: Default,
{
pub fn new(header: T) -> Self {
Self {
header,
..Default::default()
}
}
pub fn get_type(&self) -> &SectionType {
todo!()
}
pub fn settings(&self) -> &[Setting] {
&self.settings
}
pub fn get_setting(&self, setting_name: &SettingName) -> Option<&Setting> {
self.settings
.iter()
.find(|setting| setting.name() == setting_name)
}
pub fn get_value(&self, setting_name: &SettingName) -> Option<&ValueType> {
self.get_setting(setting_name)
.map(|setting| setting.value())
}
pub fn set_value(&mut self, setting_name: &SettingName, value: ValueType) {
if let Some(setting) = self.get_setting_mut(setting_name) {
setting.value = value
}
}
pub fn get_setting_mut(&mut self, setting_name: &SettingName) -> Option<&mut Setting> {
self.settings
.iter_mut()
.find(|setting| setting.name() == setting_name)
}
pub fn get_nested_setting(
&self,
setting_name: &SettingName,
nested_setting_name: &SettingName,
) -> Option<&NestedSetting> {
let setting = self.get_setting(setting_name)?;
match setting.value() {
super::ValueType::Single(_) => None,
super::ValueType::Nested(nested) => nested
.iter()
.find(|setting| setting.name() == nested_setting_name),
}
}
pub fn set(&mut self, setting_name: SettingName, value: Value) {
if let Some(setting) = self.get_setting_mut(&setting_name) {
setting.value = ValueType::Single(value);
} else {
let value = ValueType::Single(value);
let setting = Setting::new(setting_name, value);
self.settings.push(setting)
}
}
}
impl Section<ConfigHeader> {
pub fn get_name(&self) -> Option<&SectionName> {
self.header.section_name.as_ref()
}
}
impl Section<CredentialHeader> {
pub fn get_name(&self) -> &SectionName {
&self.header.profile_name
}
}
impl<T> Display for Section<T>
where
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}",
self.leading_whitespace,
self.header,
self.settings
.iter()
.map(Setting::to_string)
.collect::<String>(),
self.trailing_whitespace,
)
}
}
impl<'a, T> Parsable<'a> for Section<T>
where
T: Parsable<'a, Output = T>,
{
type Output = Self;
fn parse(input: &'a str) -> ParserOutput<'a, Self::Output> {
let (next, leading_whitespace) = Whitespace::parse(input)?;
let (next, header) = T::parse(next)?;
let (next, settings) = many0(Setting::parse)(next)?;
let (next, trailing_whitespace) = Whitespace::parse(next)?;
let section = Self {
header,
settings,
leading_whitespace,
trailing_whitespace,
};
Ok((next, section))
}
}
#[cfg(test)]
mod test {
use super::Section;
use crate::{lexer::Parsable, model::header::ConfigHeader};
const SAMPLE_SECTION: &str = r#"[default] # This is my comment
region=us-west-2
output=json"#;
const LEADING_COMMENT: &str = r#"# This is my comment
[default]
region=us-west-2
output=json"#;
const MULTIPLE_LEADING_COMMENT: &str = r#"# This is my comment
# This is my comment
[default]
region=us-west-2
output=json"#;
const MULTIPLE_TRAILING_COMMENT: &str = r#"[default]
region=us-west-2
output=json
# This is my comment
# This is my comment
"#;
const MULTIPLE_NESTED: &str = r#"[services profileB]
ec2 =
endpoint_url = https://profile-b-ec2-endpoint.aws
dynamodb =
endpoint_url = http://localhost:8000
"#;
#[test]
fn parses_section_with_two_entries_not_nested() {
let (rest, section) =
Section::<ConfigHeader>::parse(SAMPLE_SECTION).expect("Should be valid");
assert!(rest.is_empty());
let settings = section.settings;
let first = &settings[0];
assert_eq!(**first.name(), *"region");
match first.value() {
crate::ValueType::Single(_) => (),
crate::ValueType::Nested(_) => panic!("Should not be nested"),
}
let second = &settings[1];
match second.value() {
crate::ValueType::Single(_) => (),
crate::ValueType::Nested(_) => panic!("Should not be nested"),
}
}
#[test]
fn multiple_nested() {
let (rest, section) =
Section::<ConfigHeader>::parse(MULTIPLE_NESTED).expect("Should be valid");
assert!(rest.is_empty());
let settings = §ion.settings;
let first = &settings[0];
assert_eq!(**first.name(), *"ec2");
match first.value() {
crate::ValueType::Single(_) => panic!("Should not be single"),
crate::ValueType::Nested(nested) => nested,
};
let second = &settings[1];
assert_eq!(**second.name(), *"dynamodb");
match second.value() {
crate::ValueType::Single(_) => panic!("Should not be single"),
crate::ValueType::Nested(nested) => nested,
};
assert_eq!(§ion.to_string(), MULTIPLE_NESTED)
}
#[test]
fn leading_comment_on_section() {
let (_, section) =
Section::<ConfigHeader>::parse(LEADING_COMMENT).expect("Should be valid");
assert_eq!(§ion.to_string(), LEADING_COMMENT)
}
#[test]
fn multiple_leading_comment_on_section() {
let (_, section) =
Section::<ConfigHeader>::parse(MULTIPLE_LEADING_COMMENT).expect("Should be valid");
assert_eq!(§ion.to_string(), MULTIPLE_LEADING_COMMENT)
}
#[test]
fn multiple_trailing_comment_on_section() {
let (_, section) =
Section::<ConfigHeader>::parse(MULTIPLE_TRAILING_COMMENT).expect("Should be valid");
assert_eq!(§ion.to_string(), MULTIPLE_TRAILING_COMMENT)
}
}