Skip to main content

slint_interpreter/
json.rs

1// Copyright © SixtyFPS GmbH <[email protected]>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This module contains the code serialize and desrialize `Value`s to JSON
5
6use std::collections::HashMap;
7
8use i_slint_compiler::langtype;
9use i_slint_core::{
10    Brush, Color, SharedString, SharedVector,
11    graphics::Image,
12    model::{Model, ModelRc},
13};
14
15use crate::Value;
16
17/// Extension trait, adding JSON serialization methods
18pub trait JsonExt
19where
20    Self: Sized,
21{
22    /// Convert to a JSON object
23    fn to_json(&self) -> Result<serde_json::Value, String>;
24    /// Convert to a JSON-encoded string
25    fn to_json_string(&self) -> Result<String, String>;
26    /// Convert to JSON object to `Self`
27    fn from_json(t: &langtype::Type, value: &serde_json::Value) -> Result<Self, String>;
28    /// Convert to JSON encoded string to `Self`
29    fn from_json_str(t: &langtype::Type, value: &str) -> Result<Self, String>;
30}
31
32impl JsonExt for crate::Value {
33    fn to_json(&self) -> Result<serde_json::Value, String> {
34        value_to_json(self)
35    }
36
37    fn to_json_string(&self) -> Result<String, String> {
38        value_to_json_string(self)
39    }
40
41    fn from_json(t: &langtype::Type, value: &serde_json::Value) -> Result<Self, String> {
42        value_from_json(t, value)
43    }
44
45    fn from_json_str(t: &langtype::Type, value: &str) -> Result<Self, String> {
46        value_from_json_str(t, value)
47    }
48}
49
50/// Create a `Value` from a JSON Value
51pub fn value_from_json(t: &langtype::Type, v: &serde_json::Value) -> Result<Value, String> {
52    use smol_str::ToSmolStr;
53
54    fn string_to_color(s: &str) -> Option<i_slint_core::Color> {
55        i_slint_common::color_parsing::parse_color_literal(s).map(Color::from_argb_encoded)
56    }
57
58    match v {
59        serde_json::Value::Null => Ok(Value::Void),
60        serde_json::Value::Bool(b) => Ok((*b).into()),
61        serde_json::Value::Number(n) => Ok(Value::Number(n.as_f64().unwrap_or(f64::NAN))),
62        serde_json::Value::String(s) => match t {
63            langtype::Type::Enumeration(e) => {
64                let s = if let Some(suffix) = s.strip_prefix(&format!("{}.", e.name)) {
65                    suffix.to_smolstr()
66                } else {
67                    s.to_smolstr()
68                };
69
70                if e.values.contains(&s) {
71                    Ok(Value::EnumerationValue(e.name.to_string(), s.into()))
72                } else {
73                    Err(format!("Unexpected value for enum '{}': {}", e.name, s))
74                }
75            }
76            langtype::Type::Color => {
77                if let Some(c) = string_to_color(s) {
78                    Ok(Value::Brush(i_slint_core::Brush::SolidColor(c)))
79                } else {
80                    Err(format!("Failed to parse color: {s}"))
81                }
82            }
83            langtype::Type::String => Ok(SharedString::from(s.as_str()).into()),
84            langtype::Type::Image => match Image::load_from_path(std::path::Path::new(s)) {
85                Ok(image) => Ok(image.into()),
86                Err(e) => Err(format!("Failed to load image from path: {s}: {e}")),
87            },
88            langtype::Type::Brush => {
89                fn string_to_brush(input: &str) -> Result<i_slint_core::graphics::Brush, String> {
90                    fn parse_stops<'a>(
91                        it: impl Iterator<Item = &'a str>,
92                    ) -> Result<Vec<i_slint_core::graphics::GradientStop>, String>
93                    {
94                        it.filter(|part| !part.is_empty()).map(|part| {
95                            let sub_parts = part.split_whitespace().collect::<Vec<_>>();
96                            if sub_parts.len() != 2 {
97                                Err("A gradient stop must consist of a color and a position in '%' separated by whitespace".into())
98                            } else {
99                                let color = string_to_color(sub_parts[0]);
100                                let position = {
101                                    if let Some(percent_value) = sub_parts[1].strip_suffix("%") {
102                                        percent_value.parse::<f32>().map_err(|_| format!("Could not parse position '{}' as number", sub_parts[1]))
103                                    } else {
104                                        Err(format!("The position '{}' does not end in '%'", sub_parts[1]))
105                                    }
106                                };
107
108                                match (color, position) {
109                                    (Some(c), Ok(p)) => Ok(i_slint_core::graphics::GradientStop { color: c, position: p / 100.0}),
110                                    (_, Err(e)) => Err(e),
111                                    (None, _) => Err(format!("'{}' is not a color", sub_parts[0])),
112                                }
113                            }
114                        }).collect()
115                    }
116
117                    let Some(input) = input.strip_suffix(')') else {
118                        return Err(format!("No closing ')' in '{input}'"));
119                    };
120
121                    if let Some(linear) = input.strip_prefix("@linear-gradient(") {
122                        let mut split = linear.split(',').map(|p| p.trim());
123
124                        let angle = {
125                            let Some(angle_part) = split.next() else {
126                                return Err(
127                                    "A linear gradient must start with an angle in 'deg'".into()
128                                );
129                            };
130
131                            angle_part
132                                .strip_suffix("deg")
133                                .ok_or_else(|| {
134                                    "A linear brush needs to start with an angle in 'deg'"
135                                        .to_string()
136                                })
137                                .and_then(|no| {
138                                    no.parse::<f32>()
139                                        .map_err(|_| "Failed to parse angle value".into())
140                                })
141                        }?;
142
143                        Ok(i_slint_core::graphics::LinearGradientBrush::new(
144                            angle,
145                            parse_stops(split)?.drain(..),
146                        )
147                        .into())
148                    } else if let Some(radial) = input.strip_prefix("@radial-gradient(circle") {
149                        let split = radial.split(',').map(|p| p.trim());
150
151                        Ok(i_slint_core::graphics::RadialGradientBrush::new_circle(
152                            parse_stops(split)?.drain(..),
153                        )
154                        .into())
155                    } else {
156                        Err(format!("Could not parse gradient from '{input}'"))
157                    }
158                }
159
160                if s.starts_with('#') {
161                    if let Some(c) = string_to_color(s) {
162                        Ok(Value::Brush(i_slint_core::Brush::SolidColor(c)))
163                    } else {
164                        Err(format!("Failed to parse color value {s}"))
165                    }
166                } else {
167                    Ok(Value::Brush(string_to_brush(s)?))
168                }
169            }
170            _ => Err("Value type not supported".into()),
171        },
172        serde_json::Value::Array(array) => match t {
173            langtype::Type::Array(it) => {
174                Ok(Value::Model(ModelRc::new(i_slint_core::model::SharedVectorModel::from(
175                    array
176                        .iter()
177                        .map(|v| value_from_json(it, v))
178                        .collect::<Result<SharedVector<Value>, String>>()?,
179                ))))
180            }
181            _ => Err("Got an array where none was expected".into()),
182        },
183        serde_json::Value::Object(obj) => match t {
184            langtype::Type::Struct(s) => Ok(crate::Struct(
185                obj.iter()
186                    .map(|(k, v)| {
187                        let k = crate::api::normalize_identifier(k);
188                        match s.fields.get(&k) {
189                            Some(t) => value_from_json(t, v).map(|v| (k, v)),
190                            None => Err(format!("Found unknown field in struct: {k}")),
191                        }
192                    })
193                    .collect::<Result<HashMap<smol_str::SmolStr, Value>, _>>()?,
194            )
195            .into()),
196            _ => Err("Got a struct where none was expected".into()),
197        },
198    }
199}
200
201/// Create a `Value` from a JSON string
202pub fn value_from_json_str(t: &langtype::Type, v: &str) -> Result<Value, String> {
203    let value = serde_json::from_str(v).map_err(|e| format!("Failed to parse JSON: {e}"))?;
204    Value::from_json(t, &value)
205}
206
207/// Write the `Value` out into a JSON value
208pub fn value_to_json(value: &Value) -> Result<serde_json::Value, String> {
209    fn color_to_string(color: &Color) -> String {
210        let a = color.alpha();
211        let r = color.red();
212        let g = color.green();
213        let b = color.blue();
214
215        if a == 255 {
216            format!("#{r:02x}{g:02x}{b:02x}")
217        } else {
218            format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
219        }
220    }
221
222    fn gradient_to_string_helper<'a>(
223        prefix: String,
224        stops: impl Iterator<Item = &'a i_slint_core::graphics::GradientStop>,
225    ) -> serde_json::Value {
226        let mut gradient = prefix;
227
228        for stop in stops {
229            gradient += &format!(", {} {}%", color_to_string(&stop.color), stop.position * 100.0);
230        }
231
232        gradient += ")";
233
234        serde_json::Value::String(gradient)
235    }
236
237    match value {
238        Value::Void => Ok(serde_json::Value::Null),
239        Value::Bool(b) => Ok((*b).into()),
240        Value::Number(n) => {
241            let r = if *n == n.round() {
242                if *n >= 0.0 {
243                    serde_json::Number::from_u128(*n as u128)
244                } else {
245                    serde_json::Number::from_i128(*n as i128)
246                }
247            } else {
248                serde_json::Number::from_f64(*n)
249            };
250            if let Some(r) = r {
251                Ok(serde_json::Value::Number(r))
252            } else {
253                Err(format!("Could not convert {n} into a number"))
254            }
255        }
256        Value::EnumerationValue(e, v) => Ok(serde_json::Value::String(format!("{e}.{v}"))),
257        Value::String(shared_string) => Ok(serde_json::Value::String(shared_string.to_string())),
258        Value::Image(image) => {
259            if let Some(p) = image.path() {
260                Ok(serde_json::Value::String(format!("{}", p.to_string_lossy())))
261            } else {
262                Err("Cannot serialize an image without a path".into())
263            }
264        }
265        Value::Model(model_rc) => Ok(serde_json::Value::Array(
266            model_rc.iter().map(|v| v.to_json()).collect::<Result<Vec<_>, _>>()?,
267        )),
268        Value::Struct(s) => Ok(serde_json::Value::Object(
269            s.iter()
270                .map(|(k, v)| v.to_json().map(|v| (k.to_string(), v)))
271                .collect::<Result<serde_json::Map<_, _>, _>>()?,
272        )),
273        Value::Brush(brush) => match brush {
274            Brush::SolidColor(color) => Ok(serde_json::Value::String(color_to_string(color))),
275            Brush::LinearGradient(lg) => Ok(gradient_to_string_helper(
276                format!("@linear-gradient({}deg", lg.angle()),
277                lg.stops(),
278            )),
279            Brush::RadialGradient(rg) => {
280                Ok(gradient_to_string_helper("@radial-gradient(circle".into(), rg.stops()))
281            }
282            _ => Err("Cannot serialize an unknown brush type".into()),
283        },
284        Value::PathData(_) => Err("Cannot serialize path data".into()),
285        Value::EasingCurve(_) => Err("Cannot serialize a easing curve".into()),
286        _ => Err("Cannot serialize an unknown value type".into()),
287    }
288}
289
290/// Write the `Value` out into a JSON string
291pub fn value_to_json_string(value: &Value) -> Result<String, String> {
292    Ok(value_to_json(value)?.to_string())
293}
294
295#[test]
296fn test_from_json() {
297    let v = value_from_json_str(&langtype::Type::Void, "null").unwrap();
298    assert_eq!(v, Value::Void);
299    let v = Value::from_json_str(&langtype::Type::Void, "null").unwrap();
300    assert_eq!(v, Value::Void);
301
302    let v = value_from_json_str(&langtype::Type::Float32, "42.0").unwrap();
303    assert_eq!(v, Value::Number(42.0));
304
305    let v = value_from_json_str(&langtype::Type::Int32, "23").unwrap();
306    assert_eq!(v, Value::Number(23.0));
307
308    let v = value_from_json_str(&langtype::Type::String, "\"a string with \\\\ escape\"").unwrap();
309    assert_eq!(v, Value::String("a string with \\ escape".into()));
310
311    let v = value_from_json_str(&langtype::Type::Color, "\"#0ab0cdff\"").unwrap();
312    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
313    let v = value_from_json_str(&langtype::Type::Brush, "\"#0ab0cdff\"").unwrap();
314    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
315    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
316    let v = value_from_json_str(
317        &langtype::Type::Brush,
318        "\"@linear-gradient(42deg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
319    )
320    .unwrap();
321    assert_eq!(
322        v,
323        Value::Brush(Brush::LinearGradient(i_slint_core::graphics::LinearGradientBrush::new(
324            42.0,
325            vec![
326                i_slint_core::graphics::GradientStop {
327                    position: 0.0,
328                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
329                },
330                i_slint_core::graphics::GradientStop {
331                    position: 0.5,
332                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
333                },
334                i_slint_core::graphics::GradientStop {
335                    position: 1.0,
336                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
337                }
338            ]
339            .drain(..)
340        )))
341    );
342    assert!(
343        value_from_json_str(
344            &langtype::Type::Brush,
345            "\"@linear-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
346        )
347        .is_err()
348    );
349    assert!(
350        value_from_json_str(
351            &langtype::Type::Brush,
352            "\"@linear-gradient(#ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
353        )
354        .is_err()
355    );
356    assert!(
357        value_from_json_str(
358            &langtype::Type::Brush,
359            "\"@linear-gradient(90turns, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
360        )
361        .is_err()
362    );
363    assert!(
364        value_from_json_str(
365            &langtype::Type::Brush,
366            "\"@linear-gradient(xfdeg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
367        )
368        .is_err()
369    );
370    assert!(
371        value_from_json_str(
372            &langtype::Type::Brush,
373            "\"@linear-gradient(90deg, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
374        )
375        .is_err()
376    );
377    assert!(
378        value_from_json_str(
379            &langtype::Type::Brush,
380            "\"@linear-gradient(90deg, #ff0000ff 0, #00ff00ff 50%, #0000ffff 100%)\""
381        )
382        .is_err()
383    );
384
385    let v = value_from_json_str(
386        &langtype::Type::Brush,
387        "\"@radial-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
388    )
389    .unwrap();
390    assert_eq!(
391        v,
392        Value::Brush(Brush::RadialGradient(
393            i_slint_core::graphics::RadialGradientBrush::new_circle(
394                vec![
395                    i_slint_core::graphics::GradientStop {
396                        position: 0.0,
397                        color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
398                    },
399                    i_slint_core::graphics::GradientStop {
400                        position: 0.5,
401                        color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
402                    },
403                    i_slint_core::graphics::GradientStop {
404                        position: 1.0,
405                        color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
406                    }
407                ]
408                .drain(..)
409            )
410        ))
411    );
412    assert!(
413        value_from_json_str(
414            &langtype::Type::Brush,
415            "\"@radial-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
416        )
417        .is_err()
418    );
419    assert!(
420        value_from_json_str(
421            &langtype::Type::Brush,
422            "\"@radial-gradient(circle, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
423        )
424        .is_err()
425    );
426    assert!(
427        value_from_json_str(
428            &langtype::Type::Brush,
429            "\"@radial-gradient(circle, #ff0000ff 1000px, #00ff00ff 50%, #0000ffff 100%)\""
430        )
431        .is_err()
432    );
433    assert!(
434        value_from_json_str(
435            &langtype::Type::Brush,
436            "\"@radial-gradient(circle, #ff0000ff 0% #00ff00ff 50%, #0000ffff 100%)\""
437        )
438        .is_err()
439    );
440    assert!(
441        value_from_json_str(
442            &langtype::Type::Brush,
443            "\"@radial-gradient(circle, #ff0000ff, #0000ffff)\""
444        )
445        .is_err()
446    );
447
448    assert!(
449        value_from_json_str(
450            &langtype::Type::Brush,
451            "\"@radial-gradient(conical, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
452        )
453        .is_err()
454    );
455
456    assert!(
457        value_from_json_str(
458            &langtype::Type::Brush,
459            "\"@other-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
460        )
461        .is_err()
462    );
463}
464
465#[test]
466fn test_to_json() {
467    let v = value_to_json_string(&Value::Void).unwrap();
468    assert_eq!(&v, "null");
469    let v = Value::Void.to_json_string().unwrap();
470    assert_eq!(&v, "null");
471
472    let v = value_to_json_string(&Value::Number(23.0)).unwrap();
473    assert_eq!(&v, "23");
474
475    let v = value_to_json_string(&Value::Number(4.2)).unwrap();
476    assert_eq!(&v, "4.2");
477
478    let v = value_to_json_string(&Value::EnumerationValue("Foo".to_string(), "bar".to_string()))
479        .unwrap();
480    assert_eq!(&v, "\"Foo.bar\"");
481
482    let v = value_to_json_string(&Value::String("Hello World with \\ escaped".into())).unwrap();
483    assert_eq!(&v, "\"Hello World with \\\\ escaped\"");
484
485    // Image without path:
486    let buffer = i_slint_core::graphics::SharedPixelBuffer::new(2, 2);
487    assert!(value_to_json_string(&Value::Image(Image::from_rgb8(buffer))).is_err());
488
489    // Image with path
490    let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
491        .join("../../logo/MadeWithSlint-logo-dark.png")
492        .canonicalize()
493        .unwrap();
494    let v = value_to_json_string(&Value::Image(Image::load_from_path(&path).unwrap())).unwrap();
495    // We are looking at the JSON string which needs to be escaped!
496    let path = path.to_string_lossy().replace("\\", "\\\\");
497    assert_eq!(v, format!("\"{path}\""));
498
499    let v = value_to_json_string(&Value::Bool(true)).unwrap();
500    assert_eq!(&v, "true");
501
502    let v = value_to_json_string(&Value::Bool(false)).unwrap();
503    assert_eq!(&v, "false");
504
505    let model: ModelRc<Value> = std::rc::Rc::new(i_slint_core::model::VecModel::from(vec![
506        Value::Bool(true),
507        Value::Bool(false),
508    ]))
509    .into();
510    let v = value_to_json_string(&Value::Model(model)).unwrap();
511    assert_eq!(&v, "[true,false]");
512
513    let v = value_to_json_string(&Value::Struct(crate::Struct::from_iter([
514        ("kind".to_string(), Value::EnumerationValue("test".to_string(), "foo".to_string())),
515        ("is_bool".to_string(), Value::Bool(false)),
516        ("string-value".to_string(), Value::String("some string".into())),
517    ])))
518    .unwrap();
519    assert_eq!(&v, "{\"is-bool\":false,\"kind\":\"test.foo\",\"string-value\":\"some string\"}");
520
521    let v = value_to_json_string(&Value::Brush(Brush::SolidColor(Color::from_argb_u8(
522        0xff, 0x0a, 0xb0, 0xcd,
523    ))))
524    .unwrap();
525    assert_eq!(v, "\"#0ab0cd\"".to_string());
526
527    let v = value_to_json_string(&Value::Brush(Brush::LinearGradient(
528        i_slint_core::graphics::LinearGradientBrush::new(
529            42.0,
530            vec![
531                i_slint_core::graphics::GradientStop {
532                    position: 0.0,
533                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
534                },
535                i_slint_core::graphics::GradientStop {
536                    position: 0.5,
537                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
538                },
539                i_slint_core::graphics::GradientStop {
540                    position: 1.0,
541                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
542                },
543            ]
544            .drain(..),
545        ),
546    )))
547    .unwrap();
548    assert_eq!(&v, "\"@linear-gradient(42deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
549
550    let v = value_to_json_string(&Value::Brush(Brush::RadialGradient(
551        i_slint_core::graphics::RadialGradientBrush::new_circle(
552            vec![
553                i_slint_core::graphics::GradientStop {
554                    position: 0.0,
555                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
556                },
557                i_slint_core::graphics::GradientStop {
558                    position: 0.5,
559                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
560                },
561                i_slint_core::graphics::GradientStop {
562                    position: 1.0,
563                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
564                },
565            ]
566            .drain(..),
567        ),
568    )))
569    .unwrap();
570    assert_eq!(&v, "\"@radial-gradient(circle, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
571}