Skip to main content

slint_interpreter/
json.rs

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