use super::{
header::ConfigHeader, whitespace::Whitespace, NestedSetting, NestedSettingPath, Section,
SectionName, SectionPath, SectionType, Setting, SettingPath, Value,
};
use crate::lexer::{to_owned_input, Parsable};
use nom::combinator::map;
use nom::Parser;
use nom::{
combinator::{eof, opt},
error::VerboseError,
multi::many0,
sequence::tuple,
IResult,
};
use std::{fmt::Display, str::FromStr};
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
pub struct AwsConfigFile {
pub(crate) leading_whitespace: Whitespace,
pub(crate) sections: Vec<Section<ConfigHeader>>,
pub(crate) trailing_whitespace: Whitespace,
}
impl FromStr for AwsConfigFile {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
.map(|a| a.1)
.map_err(to_owned_input)
.map_err(crate::Error::from)
}
}
impl AwsConfigFile {
pub fn serialize(&self) -> String {
self.to_string()
}
pub fn get_section(&self, config_path: &SectionPath) -> Option<&Section<ConfigHeader>> {
let SectionPath {
section_type,
section_name,
} = config_path;
self.get_section_inner(section_type, section_name.as_ref())
}
pub fn get_setting(&self, setting_path: &SettingPath) -> Option<&Setting> {
let SettingPath {
section_path,
setting_name,
} = setting_path;
let section = self.get_section(section_path)?;
section.get_setting(setting_name)
}
pub fn get_nested_setting(&self, setting_path: &NestedSettingPath) -> Option<&NestedSetting> {
let NestedSettingPath {
section_path,
setting_name,
nested_setting_name,
} = setting_path;
let section = self.get_section(section_path)?;
section.get_nested_setting(setting_name, nested_setting_name)
}
pub fn set(&mut self, setting_path: SettingPath, value: Value) {
let section = match self.get_section_mut(
&setting_path.section_path.section_type,
&setting_path.section_path.section_name,
) {
Some(section) => section,
None => self.insert_section(&setting_path.section_path),
};
section.set(setting_path.setting_name, value);
}
fn get_section_inner(
&self,
section_type: &SectionType,
section_name: Option<&SectionName>,
) -> Option<&Section<ConfigHeader>> {
self.sections.iter().find(|section| {
section.header.section_type == *section_type
&& section.header.section_name.as_ref() == section_name
})
}
pub(crate) fn get_section_mut(
&mut self,
section_type: &SectionType,
section_name: &Option<SectionName>,
) -> Option<&mut Section<ConfigHeader>> {
self.sections.iter_mut().find_map(|section| {
if section.header.section_type == *section_type
&& section.header.section_name == *section_name
{
Some(section)
} else {
None
}
})
}
pub(crate) fn contains_section(&self, section_path: &SectionPath) -> bool {
self.sections.iter().any(|section| {
section.get_name() == section_path.section_name.as_ref()
&& *section.get_type() == section_path.section_type
})
}
pub(crate) fn insert_section(
&mut self,
section_path: &SectionPath,
) -> &mut Section<ConfigHeader> {
if !self.contains_section(section_path) {
let new_section: Section<ConfigHeader> =
Section::new(ConfigHeader::from(section_path.clone()));
self.sections.push(new_section);
}
#[allow(clippy::unwrap_used)]
self.get_section_mut(§ion_path.section_type, §ion_path.section_name)
.unwrap()
}
}
impl Display for AwsConfigFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}",
self.leading_whitespace,
self.sections
.iter()
.map(Section::to_string)
.collect::<String>(),
self.trailing_whitespace
)
}
}
impl<'a> Parsable<'a> for AwsConfigFile {
type Output = Self;
fn parse(input: &'a str) -> IResult<&'a str, Self::Output, VerboseError<&'a str>> {
map(
map(
tuple((
Whitespace::parse,
opt(many0(Section::<ConfigHeader>::parse)),
Whitespace::parse,
)),
Self::from,
)
.and(eof),
|(config, _)| config,
)
.parse(input)
}
}
impl From<(Whitespace, Option<Vec<Section<ConfigHeader>>>, Whitespace)> for AwsConfigFile {
fn from(
(leading_whitespace, sections, trailing_whitespace): (
Whitespace,
Option<Vec<Section<ConfigHeader>>>,
Whitespace,
),
) -> Self {
Self {
leading_whitespace,
sections: sections.unwrap_or_default(),
trailing_whitespace,
}
}
}
#[cfg(test)]
mod test {
use super::AwsConfigFile;
use crate::lexer::Parsable;
use nom::Finish;
const SAMPLE_CONFIG_FILE: &str = r#"
# I am a leading comment
[default] # This is my comment
region=us-west-2
output=json
[profile user1]
region=us-east-1
output=text
[services my-services]
dynamodb =
endpoint_url = http://localhost:8000
"#;
const SAMPLE_CONFIG_FILE_2: &str = r#"
[profile A]
credential_source = Ec2InstanceMetadata
endpoint_url = https://profile-a-endpoint.aws/
[profile B]
source_profile = A
role_arn = arn:aws:iam::123456789012:role/roleB
services = profileB
[services profileB]
ec2 =
endpoint_url = https://profile-b-ec2-endpoint.aws
dynamodb =
endpoint_url = http://localhost:8000
"#;
const EMPTY_CONFIG: &str = r#" "#;
#[test]
fn parses_sample_config() {
let (next, config) = AwsConfigFile::parse(SAMPLE_CONFIG_FILE).expect("Should be valid");
assert!(next.is_empty());
let mut sections = config.sections.iter();
let _ = sections.next().unwrap();
let _ = sections.next().unwrap();
let as_string = config.to_string();
assert_eq!(as_string, SAMPLE_CONFIG_FILE)
}
#[test]
fn parses_sample_config2() {
let (next, config) = AwsConfigFile::parse(SAMPLE_CONFIG_FILE_2).expect("Should be valid");
assert!(next.is_empty());
let mut sections = config.sections.iter();
let _ = sections.next().unwrap();
let _ = sections.next().unwrap();
let as_string = config.to_string();
assert_eq!(as_string, SAMPLE_CONFIG_FILE_2)
}
#[test]
fn empty_config_should_pass() {
let (next, _) = AwsConfigFile::parse(EMPTY_CONFIG).expect("Should be valid");
assert!(next.is_empty())
}
#[test]
fn empty_string_is_valid_config() {
AwsConfigFile::parse("").finish().ok().unwrap();
}
#[test]
fn multiline_whitespace_is_valid() {
let input = r#"
# some comment
"#;
AwsConfigFile::parse(input).finish().ok().unwrap();
}
}