use std::{collections::HashMap, convert::TryFrom};
use thiserror::Error;
use crate::parser::{Lines, ParserError};
#[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, strum::Display)]
#[strum(serialize_all = "shouty_snake_case")]
pub enum Property {
AddStyleName,
AverageWidth,
AvgCapitalWidth,
AvgLowercaseWidth,
AxisLimits,
AxisNames,
AxisTypes,
CapHeight,
CharsetEncoding,
CharsetRegistry,
Copyright,
DefaultChar,
Destination,
EndSpace,
FaceName,
FamilyName,
FigureWidth,
Font,
FontAscent,
FontDescent,
FontType,
FontVersion,
Foundry,
FullName,
ItalicAngle,
MaxSpace,
MinSpace,
NormSpace,
Notice,
PixelSize,
PointSize,
QuadWidth,
RasterizerName,
RasterizerVersion,
RawAscent,
RawDescent,
RelativeSetwidth,
RelativeWeight,
Resolution,
ResolutionX,
ResolutionY,
SetwidthName,
Slant,
SmallCapSize,
Spacing,
StrikeoutAscent,
StrikeoutDescent,
SubscriptSize,
SubscriptX,
SubscriptY,
SuperscriptSize,
SuperscriptX,
SuperscriptY,
UnderlinePosition,
UnderlineThickness,
Weight,
WeightName,
XHeight,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Properties {
properties: HashMap<String, PropertyValue>,
}
impl Properties {
#[cfg(test)]
pub(crate) fn new(properties: HashMap<String, PropertyValue>) -> Self {
Self { properties }
}
pub(crate) fn parse(lines: &mut Lines<'_>) -> Result<Self, ParserError> {
let start = lines.next().unwrap();
assert_eq!(start.keyword, "STARTPROPERTIES");
let _n_properties: usize = start
.parameters
.parse()
.map_err(|_| ParserError::with_line("invalid \"STARTPROPERTIES\"", &start))?;
let mut properties = HashMap::new();
for line in lines {
if line.keyword == "ENDPROPERTIES" {
break;
}
let value = if let Ok(int) = line.parameters.parse::<i32>() {
PropertyValue::Int(int)
} else if let Some(text) = line
.parameters
.strip_prefix('"')
.and_then(|p| p.strip_suffix('"'))
{
PropertyValue::Text(text.replace("\"\"", "\""))
} else {
return Err(ParserError::with_line("invalid property", &line));
};
properties.insert(line.keyword.to_string(), value);
}
Ok(Self { properties })
}
pub fn try_get<T: PropertyType>(
&self,
property: Property,
) -> Result<Option<T>, PropertyTypeError> {
self.try_get_by_name(&property.to_string())
}
pub fn try_get_by_name<T: PropertyType>(
&self,
name: &str,
) -> Result<Option<T>, PropertyTypeError> {
self.properties
.get(name)
.map(|value| value.try_into())
.transpose()
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
}
pub trait PropertyType
where
Self: for<'a> TryFrom<&'a PropertyValue, Error = PropertyTypeError>,
{
}
impl PropertyType for String {}
impl PropertyType for i32 {}
impl PropertyType for u32 {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropertyValue {
Text(String),
Int(i32),
}
impl TryFrom<&PropertyValue> for String {
type Error = PropertyTypeError;
fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Text(text) => Ok(text.clone()),
_ => Err(PropertyTypeError),
}
}
}
impl TryFrom<&PropertyValue> for i32 {
type Error = PropertyTypeError;
fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Int(int) => Ok(*int),
_ => Err(PropertyTypeError),
}
}
}
impl TryFrom<&PropertyValue> for u32 {
type Error = PropertyTypeError;
fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Int(int) if *int >= 0 => Ok(*int as u32),
_ => Err(PropertyTypeError),
}
}
}
#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
#[error("invalid property type")]
pub struct PropertyTypeError;
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn string_properties() {
const INPUT: &str = indoc! {r#"
STARTPROPERTIES 3
KEY1 "VALUE"
KEY2 "RANDOM WORDS AND STUFF"
WITH_QUOTE "1""23"""
ENDPROPERTIES
"#};
let mut lines = Lines::new(INPUT);
let properties = Properties::parse(&mut lines).unwrap();
for (key, expected) in [
("KEY1", "VALUE"),
("KEY2", "RANDOM WORDS AND STUFF"),
("WITH_QUOTE", "1\"23\""),
] {
assert_eq!(
properties.try_get_by_name::<String>(key).unwrap(),
Some(expected.to_string()),
"key=\"{key}\""
);
}
}
#[test]
fn integer_properties() {
const INPUT: &str = indoc! {r#"
STARTPROPERTIES 2
POS_INT 10
NEG_INT -20
ENDPROPERTIES
"#};
let mut lines = Lines::new(INPUT);
let properties = Properties::parse(&mut lines).unwrap();
assert_eq!(properties.try_get_by_name::<i32>("POS_INT"), Ok(Some(10)));
assert_eq!(properties.try_get_by_name::<i32>("NEG_INT"), Ok(Some(-20)));
assert_eq!(properties.try_get_by_name::<u32>("POS_INT"), Ok(Some(10)));
assert_eq!(
properties.try_get_by_name::<u32>("NEG_INT"),
Err(PropertyTypeError)
);
assert_eq!(
properties.try_get_by_name::<String>("POS_INT"),
Err(PropertyTypeError)
);
}
#[test]
fn empty_properties() {
const INPUT: &str = indoc! {r#"
STARTPROPERTIES 0
ENDPROPERTIES
"#};
let mut lines = Lines::new(INPUT);
let properties = Properties::parse(&mut lines).unwrap();
assert_eq!(properties.properties, HashMap::new());
}
#[test]
fn property_to_string() {
assert_eq!(&Property::Font.to_string(), "FONT");
assert_eq!(&Property::SuperscriptX.to_string(), "SUPERSCRIPT_X");
assert_eq!(
&Property::AvgLowercaseWidth.to_string(),
"AVG_LOWERCASE_WIDTH"
);
}
}