From fb58080059510de9293fddaad7c543a3089b3824 Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 24 Feb 2025 22:31:58 +0100 Subject: feat: implement `ValueFromStr` for Rotation --- src/values/rotation.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/values/rotation.rs (limited to 'src/values/rotation.rs') diff --git a/src/values/rotation.rs b/src/values/rotation.rs new file mode 100644 index 0000000..2b7a223 --- /dev/null +++ b/src/values/rotation.rs @@ -0,0 +1,37 @@ +use std::num::ParseFloatError; +use std::str::FromStr; + +use iced::{Radians, Rotation}; + +use super::ValueFromStr; + +#[derive(Debug, thiserror::Error)] +pub enum ParseRotationError { + #[error("float parsing error: {0}")] + ParseFloatError(ParseFloatError), + #[error("invalid prefix")] + InvalidPrefix, +} + +impl From for ParseRotationError { + fn from(value: ParseFloatError) -> Self { + Self::ParseFloatError(value) + } +} + +impl ValueFromStr for Rotation { + type Err = ParseRotationError; + + fn value_from_str(s: &str) -> Result { + if s.starts_with(|c: char| !c.is_digit(10)) { + let (prefix, value) = s.split_at(1); + match prefix { + "s" => Ok(Rotation::Solid(Radians(f32::from_str(value)?))), + "f" => Ok(Rotation::Floating(Radians(f32::from_str(value)?))), + _ => Err(ParseRotationError::InvalidPrefix), + } + } else { + Ok(Rotation::Floating(Radians(f32::from_str(s)?))) + } + } +} -- cgit v1.2.3 From 21941c6de6e0843147ccab7b4045943b7a878442 Mon Sep 17 00:00:00 2001 From: pml68 Date: Wed, 26 Feb 2025 23:24:42 +0100 Subject: feat: rework `Value` trait, create unit tests for parser implementations --- src/options.rs | 14 ++-- src/values.rs | 8 +- src/values/padding.rs | 214 +++++++++++++++++++++++++++++++++++++++---------- src/values/rotation.rs | 85 ++++++++++++++++++-- 4 files changed, 264 insertions(+), 57 deletions(-) (limited to 'src/values/rotation.rs') diff --git a/src/options.rs b/src/options.rs index c381294..90fc63f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -4,9 +4,9 @@ use std::collections::BTreeMap; use iced::widget::{Button, Column, Container, Image, Row, Svg, Text}; use iced::{Padding, Rotation}; -use crate::values::ValueFromStr; +use crate::values::Value; -pub trait ApplyOptions: Sized { +pub trait ApplyOptions { fn apply_options(self, options: BTreeMap>) -> Self; } @@ -15,7 +15,7 @@ impl ApplyOptions for Button<'_, Message> { let mut button = self; if let Some(padding) = options.get("padding").expect("padding key") { - let padding = Padding::value_from_str(padding).unwrap(); + let padding = Padding::from_str(padding).unwrap(); button = button.padding(padding); } @@ -28,7 +28,7 @@ impl ApplyOptions for Column<'_, Message> { let mut column = self; if let Some(padding) = options.get("padding").expect("padding key") { - let padding = Padding::value_from_str(padding).unwrap(); + let padding = Padding::from_str(padding).unwrap(); column = column.padding(padding); } @@ -41,7 +41,7 @@ impl ApplyOptions for Row<'_, Message> { let mut row = self; if let Some(padding) = options.get("padding").expect("padding key") { - let padding = Padding::value_from_str(padding).unwrap(); + let padding = Padding::from_str(padding).unwrap(); row = row.padding(padding); } @@ -54,7 +54,7 @@ impl ApplyOptions for Image { let mut image = self; if let Some(rotation) = options.get("rotation").expect("rotation key") { - let rotation = Rotation::value_from_str(rotation).unwrap(); + let rotation = Rotation::from_str(rotation).unwrap(); image = image.rotation(rotation); } @@ -67,7 +67,7 @@ impl ApplyOptions for Svg<'_> { let mut svg = self; if let Some(rotation) = options.get("rotation").expect("rotation key") { - let rotation = Rotation::value_from_str(rotation).unwrap(); + let rotation = Rotation::from_str(rotation).unwrap(); svg = svg.rotation(rotation); } diff --git a/src/values.rs b/src/values.rs index 23e47d7..e71a8c4 100644 --- a/src/values.rs +++ b/src/values.rs @@ -1,8 +1,12 @@ mod padding; mod rotation; -pub trait ValueFromStr: Sized { +pub trait Value: Sized { type Err; - fn value_from_str(s: &str) -> Result; + fn from_str(s: &str) -> Result; + + // TODO remove this once RenderedElement's options field is redone + #[allow(dead_code)] + fn to_string(&self) -> String; } diff --git a/src/values/padding.rs b/src/values/padding.rs index 01e333b..12880a3 100644 --- a/src/values/padding.rs +++ b/src/values/padding.rs @@ -3,67 +3,197 @@ use std::str::FromStr; use iced::Padding; -use super::ValueFromStr; +use super::Value; -#[derive(Debug, thiserror::Error)] -pub enum PaddingError { - #[error("wrong number of values")] - WrongNumberOfValues, +#[derive(Debug, thiserror::Error, Clone, PartialEq)] +pub enum ParsePaddingError { + #[error("wrong number of values: {0}, expected 1-4")] + WrongNumberOfValues(usize), #[error("float parsing error: {0}")] ParseFloatError(ParseFloatError), #[error("missing bracket")] MissingBracket, - #[error("empty string given")] + #[error("cannot parse padding from empty string")] Empty, } -impl From for PaddingError { +impl From for ParsePaddingError { fn from(value: ParseFloatError) -> Self { Self::ParseFloatError(value) } } -impl ValueFromStr for Padding { - type Err = PaddingError; +impl Value for Padding { + type Err = ParsePaddingError; - fn value_from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { if s.is_empty() { - return Err(PaddingError::Empty); + return Err(ParsePaddingError::Empty); } - let values = s - .strip_prefix('[') - .ok_or(PaddingError::MissingBracket)? - .strip_suffix(']') - .ok_or(PaddingError::MissingBracket)? - .split(',') - .map(|n| f32::from_str(n)) - .collect::, _>>()?; - match values.len() { - 1 => Ok(Padding { - top: values[0], - right: values[0], - bottom: values[0], - left: values[0], + + if !s.contains(['[', ',', ']']) { + let value = f32::from_str(s)?; + Ok(Padding { + top: value, + right: value, + bottom: value, + left: value, + }) + } else { + let values = s + .strip_prefix('[') + .and_then(|s| s.strip_suffix(']')) + .ok_or(ParsePaddingError::MissingBracket)? + .split(',') + .map(str::trim) + .map(f32::from_str) + .collect::, _>>()?; + + match values.len() { + 1 => Ok(Padding { + top: values[0], + right: values[0], + bottom: values[0], + left: values[0], + }), + 2 => Ok(Padding { + top: values[0], + right: values[1], + bottom: values[0], + left: values[1], + }), + 3 => Ok(Padding { + top: values[0], + right: values[1], + bottom: values[2], + left: values[1], + }), + 4 => Ok(Padding { + top: values[0], + right: values[1], + bottom: values[2], + left: values[3], + }), + other => Err(ParsePaddingError::WrongNumberOfValues(other)), + } + } + } + + fn to_string(&self) -> String { + format!( + "[{}, {}, {}, {}]", + self.top, self.right, self.bottom, self.left + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_single_value() { + assert_eq!( + Padding::from_str("[1.5]"), + Ok(Padding { + top: 1.5, + right: 1.5, + bottom: 1.5, + left: 1.5, }), - 2 => Ok(Padding { - top: values[0], - right: values[1], - bottom: values[0], - left: values[1], + ) + } + + #[test] + fn can_parse_single_value_without_brackets() { + assert_eq!( + Padding::from_str("1.5"), + Ok(Padding { + top: 1.5, + right: 1.5, + bottom: 1.5, + left: 1.5, }), - 3 => Ok(Padding { - top: values[0], - right: values[1], - bottom: values[2], - left: values[1], + ) + } + + #[test] + fn can_parse_two_values() { + assert_eq!( + Padding::from_str("[3.2, 6.7]"), + Ok(Padding { + top: 3.2, + right: 6.7, + bottom: 3.2, + left: 6.7, }), - 4 => Ok(Padding { - top: values[0], - right: values[1], - bottom: values[2], - left: values[3], + ) + } + + #[test] + fn can_parse_three_values() { + assert_eq!( + Padding::from_str("[4.8, 8.1,5.9]"), + Ok(Padding { + top: 4.8, + right: 8.1, + bottom: 5.9, + left: 8.1, }), - _ => Err(PaddingError::WrongNumberOfValues), - } + ) + } + + #[test] + fn can_parse_four_values() { + assert_eq!( + Padding::from_str("[35.4,74.6 ,53.1, 25.0]"), + Ok(Padding { + top: 35.4, + right: 74.6, + bottom: 53.1, + left: 25.0, + }), + ) + } + + #[test] + fn cant_parse_five_values() { + assert_eq!( + Padding::from_str("[1,2,3,4,5]"), + Err(ParsePaddingError::WrongNumberOfValues(5)), + ) + } + + #[test] + fn cant_parse_invalid_floats() { + assert_eq!( + Padding::from_str("[1f,2,3,4]"), + Err(ParsePaddingError::ParseFloatError( + f32::from_str("1f").expect_err("") + )) + ) + } + + #[test] + fn cant_parse_with_missing_bracket() { + assert_eq!( + Padding::from_str("1,2,3,4,5]"), + Err(ParsePaddingError::MissingBracket) + ); + + assert_eq!( + Padding::from_str("[1,2,3,4,5"), + Err(ParsePaddingError::MissingBracket) + ); + + assert_eq!( + Padding::from_str("1,2,3,4,5"), + Err(ParsePaddingError::MissingBracket) + ) + } + + #[test] + fn cant_parse_empty_string() { + assert_eq!(Padding::from_str(""), Err(ParsePaddingError::Empty)) } } diff --git a/src/values/rotation.rs b/src/values/rotation.rs index 2b7a223..da291b2 100644 --- a/src/values/rotation.rs +++ b/src/values/rotation.rs @@ -3,14 +3,16 @@ use std::str::FromStr; use iced::{Radians, Rotation}; -use super::ValueFromStr; +use super::Value; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, PartialEq)] pub enum ParseRotationError { #[error("float parsing error: {0}")] ParseFloatError(ParseFloatError), #[error("invalid prefix")] InvalidPrefix, + #[error("cannot parse rotation from empty string")] + Empty, } impl From for ParseRotationError { @@ -19,13 +21,17 @@ impl From for ParseRotationError { } } -impl ValueFromStr for Rotation { +impl Value for Rotation { type Err = ParseRotationError; - fn value_from_str(s: &str) -> Result { - if s.starts_with(|c: char| !c.is_digit(10)) { + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(ParseRotationError::Empty); + } + + if s.starts_with(|c: char| !c.is_ascii_digit()) { let (prefix, value) = s.split_at(1); - match prefix { + match prefix.to_lowercase().as_str() { "s" => Ok(Rotation::Solid(Radians(f32::from_str(value)?))), "f" => Ok(Rotation::Floating(Radians(f32::from_str(value)?))), _ => Err(ParseRotationError::InvalidPrefix), @@ -34,4 +40,71 @@ impl ValueFromStr for Rotation { Ok(Rotation::Floating(Radians(f32::from_str(s)?))) } } + + fn to_string(&self) -> String { + match self { + Self::Floating(value) => format!("f{}", value), + Self::Solid(value) => format!("s{}", value), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_without_prefix() { + assert_eq!( + Rotation::from_str("10.5"), + Ok(Rotation::Floating(Radians(10.5))) + ) + } + + #[test] + fn can_parse_with_s_prefix() { + assert_eq!( + Rotation::from_str("s12.3"), + Ok(Rotation::Solid(Radians(12.3))) + ) + } + + #[test] + fn can_parse_with_f_prefix() { + assert_eq!( + Rotation::from_str("f16.9"), + Ok(Rotation::Floating(Radians(16.9))) + ) + } + + #[test] + fn can_parse_with_uppercase_prefix() { + assert_eq!( + Rotation::from_str("S9.4"), + Ok(Rotation::Solid(Radians(9.4))) + ) + } + + #[test] + fn cant_parse_invalid_prefix() { + assert_eq!( + Rotation::from_str("a6.0"), + Err(ParseRotationError::InvalidPrefix) + ) + } + + #[test] + fn cant_parse_invalid_float() { + assert_eq!( + Rotation::from_str("3.a"), + Err(ParseRotationError::ParseFloatError( + f32::from_str("3.a").expect_err("") + )) + ) + } + + #[test] + fn cant_parse_empty_string() { + assert_eq!(Rotation::from_str(""), Err(ParseRotationError::Empty)) + } } -- cgit v1.2.3 From 2fe98e55188dc2dda4170d5a25551f6c72170e8c Mon Sep 17 00:00:00 2001 From: pml68 Date: Thu, 27 Feb 2025 09:21:13 +0100 Subject: feat: implement `Value` for `Length` --- src/values.rs | 1 + src/values/length.rs | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ src/values/padding.rs | 6 +- src/values/rotation.rs | 6 +- 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/values/length.rs (limited to 'src/values/rotation.rs') diff --git a/src/values.rs b/src/values.rs index e71a8c4..31ee7bb 100644 --- a/src/values.rs +++ b/src/values.rs @@ -1,3 +1,4 @@ +mod length; mod padding; mod rotation; diff --git a/src/values/length.rs b/src/values/length.rs new file mode 100644 index 0000000..28a7156 --- /dev/null +++ b/src/values/length.rs @@ -0,0 +1,145 @@ +use std::num::{ParseFloatError, ParseIntError}; +use std::str::FromStr; + +use iced::Length; + +use super::Value; + +#[derive(Debug, thiserror::Error, Clone, PartialEq)] +pub enum ParseLengthError { + #[error("float parsing error: {0}")] + ParseFloatError(ParseFloatError), + #[error("int parsing error: {0}")] + ParseIntError(ParseIntError), + #[error("invalid type")] + InvalidType, + #[error("invalid prefix")] + InvalidPrefix, + #[error("missing prefix")] + MissingPrefix, + #[error("cannot parse length from empty string")] + Empty, +} + +impl From for ParseLengthError { + fn from(value: ParseFloatError) -> Self { + Self::ParseFloatError(value) + } +} + +impl From for ParseLengthError { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Value for Length { + type Err = ParseLengthError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + if s.is_empty() { + return Err(ParseLengthError::Empty); + } + + if !s.contains(|c: char| c.is_ascii_digit()) { + match s { + "fill" => Ok(Self::Fill), + "shrink" => Ok(Self::Shrink), + _ => Err(ParseLengthError::InvalidType), + } + } else { + if s.starts_with(|c: char| !c.is_alphabetic()) { + return Err(ParseLengthError::MissingPrefix); + } + + let (prefix, value) = s.split_at(2); + match prefix.to_lowercase().as_str() { + "fx" => Ok(Self::Fixed(f32::from_str(value)?)), + "fp" => Ok(Self::FillPortion(u16::from_str(value)?)), + _ => Err(ParseLengthError::InvalidPrefix), + } + } + } + + fn to_string(&self) -> String { + match self { + Self::Fill => String::from("fill"), + Self::Shrink => String::from("shrink"), + Self::Fixed(value) => format!("fx{}", value), + Self::FillPortion(value) => format!("fp{}", value), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_fill() { + assert_eq!(Length::from_str("fill"), Ok(Length::Fill)) + } + + #[test] + fn can_parse_shrink_with_space() { + assert_eq!(Length::from_str("shrink "), Ok(Length::Shrink)) + } + + #[test] + fn can_parse_fill_portion() { + assert_eq!(Length::from_str("fp15"), Ok(Length::FillPortion(15))) + } + + #[test] + fn can_parse_fixed_with_spaces() { + assert_eq!(Length::from_str(" fx3.1 "), Ok(Length::Fixed(3.1))) + } + + #[test] + fn cant_parse_invalid_type() { + assert_eq!( + Length::from_str("fillportion"), + Err(ParseLengthError::InvalidType) + ) + } + + #[test] + fn cant_parse_invalid_prefix() { + assert_eq!( + Length::from_str("f2.0"), + Err(ParseLengthError::InvalidPrefix), + ) + } + + #[test] + fn cant_parse_invalid_float() { + assert_eq!( + Length::from_str(" fx2.a"), + Err(ParseLengthError::ParseFloatError( + f32::from_str("2.a").expect_err("float parse should fail") + )) + ) + } + + #[test] + fn cant_parse_invalid_integer() { + assert_eq!( + Length::from_str("fp1a "), + Err(ParseLengthError::ParseIntError( + u16::from_str("1a").expect_err("integer parse should fail") + )) + ) + } + + #[test] + fn cant_parse_with_missing_prefix() { + assert_eq!(Length::from_str("24"), Err(ParseLengthError::MissingPrefix)) + } + + #[test] + fn cant_parse_empty_string() { + assert_eq!(Length::from_str(" "), Err(ParseLengthError::Empty)) + } +} diff --git a/src/values/padding.rs b/src/values/padding.rs index 12880a3..b6d3947 100644 --- a/src/values/padding.rs +++ b/src/values/padding.rs @@ -27,6 +27,8 @@ impl Value for Padding { type Err = ParsePaddingError; fn from_str(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { return Err(ParsePaddingError::Empty); } @@ -169,7 +171,7 @@ mod tests { assert_eq!( Padding::from_str("[1f,2,3,4]"), Err(ParsePaddingError::ParseFloatError( - f32::from_str("1f").expect_err("") + f32::from_str("1f").expect_err("float parse should fail") )) ) } @@ -194,6 +196,6 @@ mod tests { #[test] fn cant_parse_empty_string() { - assert_eq!(Padding::from_str(""), Err(ParsePaddingError::Empty)) + assert_eq!(Padding::from_str(" "), Err(ParsePaddingError::Empty)) } } diff --git a/src/values/rotation.rs b/src/values/rotation.rs index da291b2..2b609d3 100644 --- a/src/values/rotation.rs +++ b/src/values/rotation.rs @@ -25,6 +25,8 @@ impl Value for Rotation { type Err = ParseRotationError; fn from_str(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { return Err(ParseRotationError::Empty); } @@ -98,13 +100,13 @@ mod tests { assert_eq!( Rotation::from_str("3.a"), Err(ParseRotationError::ParseFloatError( - f32::from_str("3.a").expect_err("") + f32::from_str("3.a").expect_err("float parse should fail") )) ) } #[test] fn cant_parse_empty_string() { - assert_eq!(Rotation::from_str(""), Err(ParseRotationError::Empty)) + assert_eq!(Rotation::from_str(" "), Err(ParseRotationError::Empty)) } } -- cgit v1.2.3 From 1c1f8a6bccd680d5c9e540035a7ebff3e84c5001 Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 3 Mar 2025 23:49:25 +0100 Subject: refactor: move some `assert_eq`s --- src/values/rotation.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) mode change 100644 => 100755 src/values/rotation.rs (limited to 'src/values/rotation.rs') diff --git a/src/values/rotation.rs b/src/values/rotation.rs old mode 100644 new mode 100755 index 2b609d3..90e3f84 --- a/src/values/rotation.rs +++ b/src/values/rotation.rs @@ -66,8 +66,13 @@ mod tests { #[test] fn can_parse_with_s_prefix() { assert_eq!( - Rotation::from_str("s12.3"), + Rotation::from_str(" s12.3"), Ok(Rotation::Solid(Radians(12.3))) + ); + + assert_eq!( + Rotation::from_str("S9.4"), + Ok(Rotation::Solid(Radians(9.4))) ) } @@ -76,14 +81,11 @@ mod tests { assert_eq!( Rotation::from_str("f16.9"), Ok(Rotation::Floating(Radians(16.9))) - ) - } + ); - #[test] - fn can_parse_with_uppercase_prefix() { assert_eq!( - Rotation::from_str("S9.4"), - Ok(Rotation::Solid(Radians(9.4))) + Rotation::from_str("F21.45 "), + Ok(Rotation::Floating(Radians(21.45))) ) } -- cgit v1.2.3