use std::collections::BTreeMap;
use std::str::FromStr;
use serde::Serialize;
use serde_json::ser::PrettyFormatter;
use crate::sysconfig::cursor::Cursor;
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)]
#[serde(untagged)]
pub(super) enum Value {
String(String),
Int(i32),
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)]
pub(super) struct SysconfigData(BTreeMap<String, Value>);
impl SysconfigData {
pub(super) fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, Value> {
self.0.iter_mut()
}
pub(super) fn insert(&mut self, key: String, value: Value) -> Option<Value> {
self.0.insert(key, value)
}
pub(super) fn to_string_pretty(&self) -> Result<String, serde_json::Error> {
let output = {
let mut buf = Vec::new();
let mut serializer = serde_json::Serializer::with_formatter(
&mut buf,
PrettyFormatter::with_indent(b" "),
);
self.0.serialize(&mut serializer)?;
String::from_utf8(buf).unwrap()
};
Ok(format!(
"# system configuration generated and used by the sysconfig module\nbuild_time_vars = {output}\n",
))
}
}
impl std::fmt::Display for SysconfigData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = {
let mut buf = Vec::new();
let mut serializer = serde_json::Serializer::new(&mut buf);
self.0.serialize(&mut serializer).unwrap();
String::from_utf8(buf).unwrap()
};
write!(f, "{output}",)
}
}
impl FromIterator<(String, Value)> for SysconfigData {
fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl FromStr for SysconfigData {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some(s) =
s.strip_prefix("# system configuration generated and used by the sysconfig module\n")
else {
return Err(Error::MissingHeader);
};
let Some(s) = s.strip_prefix("build_time_vars") else {
return Err(Error::MissingAssignment);
};
let mut cursor = Cursor::new(s);
cursor.eat_while(is_python_whitespace);
if !cursor.eat_char('=') {
return Err(Error::MissingAssignment);
}
cursor.eat_while(is_python_whitespace);
if !cursor.eat_char('{') {
return Err(Error::MissingOpenBrace);
}
let mut map = BTreeMap::new();
loop {
let Some(next) = cursor.bump() else {
return Err(Error::UnexpectedEof);
};
match next {
'\'' | '"' => {
let key = parse_string(&mut cursor, next)?;
cursor.eat_while(is_python_whitespace);
cursor.eat_char(':');
cursor.eat_while(is_python_whitespace);
let value = match cursor.first() {
'\'' | '"' => Value::String(parse_concatenated_string(&mut cursor)?),
'-' => {
cursor.bump();
Value::Int(-parse_int(&mut cursor)?)
}
c if c.is_ascii_digit() => Value::Int(parse_int(&mut cursor)?),
c => return Err(Error::UnexpectedCharacter(c)),
};
map.insert(key, value);
cursor.eat_while(is_python_whitespace);
cursor.eat_char(',');
cursor.eat_while(is_python_whitespace);
}
' ' | '\n' | '\r' | '\t' => {}
'}' => {
break;
}
c => return Err(Error::UnexpectedCharacter(c)),
}
}
Ok(Self(map))
}
}
fn parse_string(cursor: &mut Cursor, quote: char) -> Result<String, Error> {
let mut result = String::new();
loop {
let Some(c) = cursor.bump() else {
return Err(Error::UnexpectedEof);
};
match c {
'\\' => {
if cursor.first() == quote {
cursor.bump();
result.push(quote);
continue;
}
result.push('\\');
result.push(cursor.first());
cursor.bump();
}
c if c == quote => {
break;
}
c => {
result.push(c);
}
}
}
Ok(result)
}
fn parse_concatenated_string(cursor: &mut Cursor) -> Result<String, Error> {
let mut result = String::new();
loop {
let Some(c) = cursor.bump() else {
return Err(Error::UnexpectedEof);
};
match c {
'\'' | '"' => {
result.push_str(&parse_string(cursor, c)?);
}
c if is_python_whitespace(c) => {
}
c => return Err(Error::UnexpectedCharacter(c)),
}
if matches!(cursor.first(), ',' | '}') {
break;
}
}
Ok(result)
}
fn parse_int(cursor: &mut Cursor) -> Result<i32, std::num::ParseIntError> {
let mut result = String::new();
loop {
let c = cursor.first();
if !c.is_ascii_digit() {
break;
}
result.push(c);
cursor.bump();
}
result.parse()
}
const fn is_python_whitespace(c: char) -> bool {
matches!(
c,
' ' | '\t' | '\x0C' | '\n' | '\r'
)
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Missing opening brace")]
MissingOpenBrace,
#[error("Unexpected character: {0}")]
UnexpectedCharacter(char),
#[error("Unexpected end of file")]
UnexpectedEof,
#[error("Failed to parse integer")]
ParseInt(#[from] std::num::ParseIntError),
#[error("`_sysconfigdata_` is missing a header comment")]
MissingHeader,
#[error("`_sysconfigdata_` is missing an assignment to `build_time_vars`")]
MissingAssignment,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_string() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value1",
"key2": 42,
"key3": "multi-part" " string"
}
"#
);
let result = input.parse::<SysconfigData>().expect("Parsing failed");
let snapshot = result.to_string_pretty().unwrap();
insta::assert_snapshot!(snapshot, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value1",
"key2": 42,
"key3": "multi-part string"
}
"###);
}
#[test]
fn test_parse_trailing_comma() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value1",
"key2": 42,
"key3": "multi-part" " string",
}
"#
);
let result = input.parse::<SysconfigData>().expect("Parsing failed");
let snapshot = result.to_string_pretty().unwrap();
insta::assert_snapshot!(snapshot, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value1",
"key2": 42,
"key3": "multi-part string"
}
"###);
}
#[test]
fn test_parse_integer_values() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": 12345,
"key2": -15
}
"#
);
let result = input.parse::<SysconfigData>().expect("Parsing failed");
let snapshot = result.to_string_pretty().unwrap();
insta::assert_snapshot!(snapshot, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": 12345,
"key2": -15
}
"###);
}
#[test]
fn test_parse_escaped_quotes() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value with \"escaped quotes\"",
"key2": 'single-quoted \'escaped\''
}
"#
);
let result = input.parse::<SysconfigData>().expect("Parsing failed");
let snapshot = result.to_string_pretty().unwrap();
insta::assert_snapshot!(snapshot, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "value with \"escaped quotes\"",
"key2": "single-quoted 'escaped'"
}
"###);
}
#[test]
fn test_parse_concatenated_strings() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "multi-"
"line "
"string"
}
"#
);
let result = input.parse::<SysconfigData>().expect("Parsing failed");
let snapshot = result.to_string_pretty().unwrap();
insta::assert_snapshot!(snapshot, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": "multi-line string"
}
"###);
}
#[test]
fn test_missing_header_error() {
let input = indoc::indoc!(
r#"
build_time_vars = {
"key1": "value1"
}
"#
);
let result = input.parse::<SysconfigData>();
assert!(matches!(result, Err(Error::MissingHeader)));
}
#[test]
fn test_missing_assignment_error() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
{
"key1": "value1"
}
"#
);
let result = input.parse::<SysconfigData>();
assert!(matches!(result, Err(Error::MissingAssignment)));
}
#[test]
fn test_unexpected_character_error() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": &123
}
"#
);
let result = input.parse::<SysconfigData>();
assert!(
result.is_err(),
"Expected parsing to fail due to unexpected character"
);
}
#[test]
fn test_unexpected_eof() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": 123
"#
);
let result = input.parse::<SysconfigData>();
assert!(
result.is_err(),
"Expected parsing to fail due to unexpected character"
);
}
#[test]
fn test_unexpected_comma() {
let input = indoc::indoc!(
r#"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"key1": 123,,
}
"#
);
let result = input.parse::<SysconfigData>();
assert!(
result.is_err(),
"Expected parsing to fail due to unexpected character"
);
}
}