ferroui_base/data/converters/
string_format_value_converter.rs1use super::composite_format::format_values_with;
2use super::IValueConverter;
3use crate::data::core::ValueType;
4use crate::data::BindingError;
5use crate::utilities::CultureInfo;
6use crate::BoxedValue;
7use std::rc::Rc;
8
9pub struct StringFormatValueConverter {
12 format: String,
13 inner: Option<Rc<dyn IValueConverter>>,
14}
15
16impl StringFormatValueConverter {
17 pub fn new(format: impl Into<String>, inner: Option<Rc<dyn IValueConverter>>) -> Self {
22 Self { format: format.into(), inner }
23 }
24
25 pub fn inner(&self) -> Option<&Rc<dyn IValueConverter>> {
28 self.inner.as_ref()
29 }
30
31 pub fn format(&self) -> &str {
33 &self.format
34 }
35}
36
37impl IValueConverter for StringFormatValueConverter {
38 fn convert(
39 &self,
40 value: Option<&BoxedValue>,
41 target_type: ValueType,
42 parameter: Option<&BoxedValue>,
43 culture: &CultureInfo,
44 ) -> Result<Option<BoxedValue>, BindingError> {
45 let converted = match &self.inner {
46 Some(inner) => inner.convert(value, target_type, parameter, culture)?,
47 None => None,
48 };
49 let value = converted.or_else(|| value.cloned());
50
51 let result = if self.format.contains('{') {
52 format_values_with(&self.format, &[value], culture)
53 } else {
54 format_values_with(&format!("{{0:{}}}", self.format), &[value], culture)
55 };
56 match result {
57 Ok(text) => Ok(Some(Rc::new(text))),
58 Err(error) => Err(BindingError::new(error)),
59 }
60 }
61
62 fn convert_back(
63 &self,
64 _value: Option<&BoxedValue>,
65 _target_type: ValueType,
66 _parameter: Option<&BoxedValue>,
67 _culture: &CultureInfo,
68 ) -> Result<Option<BoxedValue>, BindingError> {
69 Err(BindingError::message("Two way bindings are not supported with a string format"))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
78 use crate::data::converters::composite_format::FormatError;
79 use crate::data::converters::FuncValueConverter;
80
81 fn boxed<T: PartialEq + 'static>(value: T) -> BoxedValue {
82 Rc::new(value)
83 }
84
85 fn convert(target: &StringFormatValueConverter, value: Option<&BoxedValue>) -> String {
86 let result = target.convert(value, ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect("no error").expect("not null");
87 result.downcast_ref::<String>().cloned().expect("a string")
88 }
89
90 #[test]
91 fn exposes_format_and_inner() {
92 let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, i32>::new(|x| x));
93 let target = StringFormatValueConverter::new("{0}", Some(inner.clone()));
94
95 assert_eq!(target.format(), "{0}");
96 assert!(target.inner().is_some_and(|i| Rc::ptr_eq(i, &inner)));
97 assert!(StringFormatValueConverter::new("{0}", None).inner().is_none());
98 }
99
100 #[test]
101 fn formats_value_with_composite_format() {
102 let target = StringFormatValueConverter::new("Hello {0}!", None);
103
104 assert_eq!(convert(&target, Some(&boxed(String::from("world")))), "Hello world!");
105 assert_eq!(convert(&target, Some(&boxed(5i32))), "Hello 5!");
106 }
107
108 #[test]
109 fn format_without_braces_is_the_format_of_the_value() {
110 let target = StringFormatValueConverter::new("0.00", None);
111
112 assert_eq!(convert(&target, Some(&boxed(1.5f64))), "1.50");
113 assert_eq!(convert(&StringFormatValueConverter::new("F1", None), Some(&boxed(2.25f64))), "2.2");
114 assert_eq!(convert(&StringFormatValueConverter::new("X4", None), Some(&boxed(255i32))), "00FF");
115 }
116
117 #[test]
118 fn null_value_formats_as_empty_text() {
119 let target = StringFormatValueConverter::new("Converted: {0}", None);
120
121 assert_eq!(convert(&target, None), "Converted: ");
122 }
123
124 #[test]
125 fn inner_converter_is_called_before_formatting() {
126 let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, i32>::new(|x| x * 2));
127 let target = StringFormatValueConverter::new("{0:D3}", Some(inner));
128
129 assert_eq!(convert(&target, Some(&boxed(21i32))), "042");
130 }
131
132 #[test]
133 fn value_is_used_if_inner_converter_returns_null() {
134 let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, Option<i32>>::new(|_| None));
135 let target = StringFormatValueConverter::new("{0}", Some(inner));
136
137 assert_eq!(convert(&target, Some(&boxed(21i32))), "21");
138 }
139
140 #[test]
141 fn error_of_inner_converter_is_returned() {
142 struct Failing;
143 impl IValueConverter for Failing {
144 fn convert(
145 &self,
146 _value: Option<&BoxedValue>,
147 _target_type: ValueType,
148 _parameter: Option<&BoxedValue>,
149 _culture: &CultureInfo,
150 ) -> Result<Option<BoxedValue>, BindingError> {
151 Err(BindingError::message("inner failed"))
152 }
153
154 fn convert_back(
155 &self,
156 _value: Option<&BoxedValue>,
157 _target_type: ValueType,
158 _parameter: Option<&BoxedValue>,
159 _culture: &CultureInfo,
160 ) -> Result<Option<BoxedValue>, BindingError> {
161 Err(BindingError::message("inner failed"))
162 }
163 }
164 let target = StringFormatValueConverter::new("{0}", Some(Rc::new(Failing)));
165
166 let result = target.convert(Some(&boxed(1i32)), ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture());
167
168 assert_eq!(result.expect_err("an error").to_string(), "inner failed");
169 }
170
171 #[test]
172 fn invalid_format_is_an_error() {
173 let target = StringFormatValueConverter::new("{1}", None);
174
175 let error = target.convert(Some(&boxed(1i32)), ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect_err("an error");
176
177 assert_eq!(error.inner().downcast_ref::<FormatError>(), Some(&FormatError::IndexOutOfRange));
178 }
179
180 #[test]
181 fn convert_back_is_not_supported() {
182 let target = StringFormatValueConverter::new("{0}", None);
183
184 let result = target.convert_back(Some(&boxed(String::from("1"))), ValueType::of::<i32>(), None, &crate::utilities::CultureInfo::invariant_culture());
185
186 assert_eq!(
187 result.expect_err("an error").to_string(),
188 "Two way bindings are not supported with a string format"
189 );
190 }
191}