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) => Ok(crate::Struct(
186                obj.iter()
187                    .map(|(k, v)| {
188                        let k = crate::api::normalize_identifier(k);
189                        match s.fields.get(&k) {
190                            Some(t) => value_from_json(t, v).map(|v| (k, v)),
191                            None => Err(format!("Found unknown field in struct: {k}")),
192                        }
193                    })
194                    .collect::<Result<HashMap<smol_str::SmolStr, Value>, _>>()?,
195            )
196            .into()),
197            _ => Err("Got a struct where none was expected".into()),
198        },
199    }
200}
201
202/// Create a `Value` from a JSON string
203pub fn value_from_json_str(t: &langtype::Type, v: &str) -> Result<Value, String> {
204    let value = serde_json::from_str(v).map_err(|e| format!("Failed to parse JSON: {e}"))?;
205    Value::from_json(t, &value)
206}
207
208/// Write the `Value` out into a JSON value
209pub fn value_to_json(value: &Value) -> Result<serde_json::Value, String> {
210    fn color_to_string(color: &Color) -> String {
211        let a = color.alpha();
212        let r = color.red();
213        let g = color.green();
214        let b = color.blue();
215
216        if a == 255 {
217            format!("#{r:02x}{g:02x}{b:02x}")
218        } else {
219            format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
220        }
221    }
222
223    fn gradient_to_string_helper<'a>(
224        prefix: String,
225        stops: impl Iterator<Item = &'a i_slint_core::graphics::GradientStop>,
226    ) -> serde_json::Value {
227        let mut gradient = prefix;
228
229        for stop in stops {
230            gradient += &format!(", {} {}%", color_to_string(&stop.color), stop.position * 100.0);
231        }
232
233        gradient += ")";
234
235        serde_json::Value::String(gradient)
236    }
237
238    match value {
239        Value::Void => Ok(serde_json::Value::Null),
240        Value::Bool(b) => Ok((*b).into()),
241        Value::Number(n) => {
242            let r = if *n == n.round() {
243                if *n >= 0.0 {
244                    serde_json::Number::from_u128(*n as u128)
245                } else {
246                    serde_json::Number::from_i128(*n as i128)
247                }
248            } else {
249                serde_json::Number::from_f64(*n)
250            };
251            if let Some(r) = r {
252                Ok(serde_json::Value::Number(r))
253            } else {
254                Err(format!("Could not convert {n} into a number"))
255            }
256        }
257        Value::EnumerationValue(e, v) => Ok(serde_json::Value::String(format!("{e}.{v}"))),
258        Value::String(shared_string) => Ok(serde_json::Value::String(shared_string.to_string())),
259        Value::Image(image) => {
260            if let Some(p) = image.path() {
261                Ok(serde_json::Value::String(format!("{}", p.to_string_lossy())))
262            } else {
263                Err("Cannot serialize an image without a path".into())
264            }
265        }
266        Value::Model(model_rc) => Ok(serde_json::Value::Array(
267            model_rc.iter().map(|v| v.to_json()).collect::<Result<Vec<_>, _>>()?,
268        )),
269        Value::Struct(s) => Ok(serde_json::Value::Object(
270            s.iter()
271                .map(|(k, v)| v.to_json().map(|v| (k.to_string(), v)))
272                .collect::<Result<serde_json::Map<_, _>, _>>()?,
273        )),
274        Value::Brush(brush) => match brush {
275            Brush::SolidColor(color) => Ok(serde_json::Value::String(color_to_string(color))),
276            Brush::LinearGradient(lg) => Ok(gradient_to_string_helper(
277                format!("@linear-gradient({}deg", lg.angle()),
278                lg.stops(),
279            )),
280            Brush::RadialGradient(rg) => {
281                Ok(gradient_to_string_helper("@radial-gradient(circle".into(), rg.stops()))
282            }
283            _ => Err("Cannot serialize an unknown brush type".into()),
284        },
285        Value::PathData(_) => Err("Cannot serialize path data".into()),
286        Value::EasingCurve(_) => Err("Cannot serialize a easing curve".into()),
287        _ => Err("Cannot serialize an unknown value type".into()),
288    }
289}
290
291/// Write the `Value` out into a JSON string
292pub fn value_to_json_string(value: &Value) -> Result<String, String> {
293    Ok(value_to_json(value)?.to_string())
294}
295
296#[test]
297fn test_from_json() {
298    let v = value_from_json_str(&langtype::Type::Void, "null").unwrap();
299    assert_eq!(v, Value::Void);
300    let v = Value::from_json_str(&langtype::Type::Void, "null").unwrap();
301    assert_eq!(v, Value::Void);
302
303    let v = value_from_json_str(&langtype::Type::Float32, "42.0").unwrap();
304    assert_eq!(v, Value::Number(42.0));
305
306    let v = value_from_json_str(&langtype::Type::Int32, "23").unwrap();
307    assert_eq!(v, Value::Number(23.0));
308
309    let v = value_from_json_str(&langtype::Type::String, "\"a string with \\\\ escape\"").unwrap();
310    assert_eq!(v, Value::String("a string with \\ escape".into()));
311
312    let v = value_from_json_str(&langtype::Type::Color, "\"#0ab0cdff\"").unwrap();
313    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
314    let v = value_from_json_str(&langtype::Type::Brush, "\"#0ab0cdff\"").unwrap();
315    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
316    assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
317    let v = value_from_json_str(
318        &langtype::Type::Brush,
319        "\"@linear-gradient(42deg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
320    )
321    .unwrap();
322    assert_eq!(
323        v,
324        Value::Brush(Brush::LinearGradient(i_slint_core::graphics::LinearGradientBrush::new(
325            42.0,
326            vec![
327                i_slint_core::graphics::GradientStop {
328                    position: 0.0,
329                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
330                },
331                i_slint_core::graphics::GradientStop {
332                    position: 0.5,
333                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
334                },
335                i_slint_core::graphics::GradientStop {
336                    position: 1.0,
337                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
338                }
339            ]
340            .drain(..)
341        )))
342    );
343    assert!(
344        value_from_json_str(
345            &langtype::Type::Brush,
346            "\"@linear-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
347        )
348        .is_err()
349    );
350    assert!(
351        value_from_json_str(
352            &langtype::Type::Brush,
353            "\"@linear-gradient(#ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
354        )
355        .is_err()
356    );
357    assert!(
358        value_from_json_str(
359            &langtype::Type::Brush,
360            "\"@linear-gradient(90turns, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
361        )
362        .is_err()
363    );
364    assert!(
365        value_from_json_str(
366            &langtype::Type::Brush,
367            "\"@linear-gradient(xfdeg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
368        )
369        .is_err()
370    );
371    assert!(
372        value_from_json_str(
373            &langtype::Type::Brush,
374            "\"@linear-gradient(90deg, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
375        )
376        .is_err()
377    );
378    assert!(
379        value_from_json_str(
380            &langtype::Type::Brush,
381            "\"@linear-gradient(90deg, #ff0000ff 0, #00ff00ff 50%, #0000ffff 100%)\""
382        )
383        .is_err()
384    );
385
386    let v = value_from_json_str(
387        &langtype::Type::Brush,
388        "\"@radial-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
389    )
390    .unwrap();
391    assert_eq!(
392        v,
393        Value::Brush(Brush::RadialGradient(
394            i_slint_core::graphics::RadialGradientBrush::new_circle(
395                vec![
396                    i_slint_core::graphics::GradientStop {
397                        position: 0.0,
398                        color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
399                    },
400                    i_slint_core::graphics::GradientStop {
401                        position: 0.5,
402                        color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
403                    },
404                    i_slint_core::graphics::GradientStop {
405                        position: 1.0,
406                        color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
407                    }
408                ]
409                .drain(..)
410            )
411        ))
412    );
413    assert!(
414        value_from_json_str(
415            &langtype::Type::Brush,
416            "\"@radial-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
417        )
418        .is_err()
419    );
420    assert!(
421        value_from_json_str(
422            &langtype::Type::Brush,
423            "\"@radial-gradient(circle, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
424        )
425        .is_err()
426    );
427    assert!(
428        value_from_json_str(
429            &langtype::Type::Brush,
430            "\"@radial-gradient(circle, #ff0000ff 1000px, #00ff00ff 50%, #0000ffff 100%)\""
431        )
432        .is_err()
433    );
434    assert!(
435        value_from_json_str(
436            &langtype::Type::Brush,
437            "\"@radial-gradient(circle, #ff0000ff 0% #00ff00ff 50%, #0000ffff 100%)\""
438        )
439        .is_err()
440    );
441    assert!(
442        value_from_json_str(
443            &langtype::Type::Brush,
444            "\"@radial-gradient(circle, #ff0000ff, #0000ffff)\""
445        )
446        .is_err()
447    );
448
449    assert!(
450        value_from_json_str(
451            &langtype::Type::Brush,
452            "\"@radial-gradient(conical, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
453        )
454        .is_err()
455    );
456
457    assert!(
458        value_from_json_str(
459            &langtype::Type::Brush,
460            "\"@other-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
461        )
462        .is_err()
463    );
464}
465
466#[test]
467fn test_to_json() {
468    let v = value_to_json_string(&Value::Void).unwrap();
469    assert_eq!(&v, "null");
470    let v = Value::Void.to_json_string().unwrap();
471    assert_eq!(&v, "null");
472
473    let v = value_to_json_string(&Value::Number(23.0)).unwrap();
474    assert_eq!(&v, "23");
475
476    let v = value_to_json_string(&Value::Number(4.2)).unwrap();
477    assert_eq!(&v, "4.2");
478
479    let v = value_to_json_string(&Value::EnumerationValue("Foo".to_string(), "bar".to_string()))
480        .unwrap();
481    assert_eq!(&v, "\"Foo.bar\"");
482
483    let v = value_to_json_string(&Value::String("Hello World with \\ escaped".into())).unwrap();
484    assert_eq!(&v, "\"Hello World with \\\\ escaped\"");
485
486    // Image without path:
487    let buffer = i_slint_core::graphics::SharedPixelBuffer::new(2, 2);
488    assert!(value_to_json_string(&Value::Image(Image::from_rgb8(buffer))).is_err());
489
490    // Image with path
491    let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
492        .join("../../logo/MadeWithSlint-logo-dark.png")
493        .canonicalize()
494        .unwrap();
495    let v = value_to_json_string(&Value::Image(Image::load_from_path(&path).unwrap())).unwrap();
496    // We are looking at the JSON string which needs to be escaped!
497    let path = path.to_string_lossy().replace("\\", "\\\\");
498    assert_eq!(v, format!("\"{path}\""));
499
500    let v = value_to_json_string(&Value::Bool(true)).unwrap();
501    assert_eq!(&v, "true");
502
503    let v = value_to_json_string(&Value::Bool(false)).unwrap();
504    assert_eq!(&v, "false");
505
506    let model: ModelRc<Value> = std::rc::Rc::new(i_slint_core::model::VecModel::from(vec![
507        Value::Bool(true),
508        Value::Bool(false),
509    ]))
510    .into();
511    let v = value_to_json_string(&Value::Model(model)).unwrap();
512    assert_eq!(&v, "[true,false]");
513
514    let v = value_to_json_string(&Value::Struct(crate::Struct::from_iter([
515        ("kind".to_string(), Value::EnumerationValue("test".to_string(), "foo".to_string())),
516        ("is_bool".to_string(), Value::Bool(false)),
517        ("string-value".to_string(), Value::String("some string".into())),
518    ])))
519    .unwrap();
520    assert_eq!(&v, "{\"is-bool\":false,\"kind\":\"test.foo\",\"string-value\":\"some string\"}");
521
522    let v = value_to_json_string(&Value::Brush(Brush::SolidColor(Color::from_argb_u8(
523        0xff, 0x0a, 0xb0, 0xcd,
524    ))))
525    .unwrap();
526    assert_eq!(v, "\"#0ab0cd\"".to_string());
527
528    let v = value_to_json_string(&Value::Brush(Brush::LinearGradient(
529        i_slint_core::graphics::LinearGradientBrush::new(
530            42.0,
531            vec![
532                i_slint_core::graphics::GradientStop {
533                    position: 0.0,
534                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
535                },
536                i_slint_core::graphics::GradientStop {
537                    position: 0.5,
538                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
539                },
540                i_slint_core::graphics::GradientStop {
541                    position: 1.0,
542                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
543                },
544            ]
545            .drain(..),
546        ),
547    )))
548    .unwrap();
549    assert_eq!(&v, "\"@linear-gradient(42deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
550
551    let v = value_to_json_string(&Value::Brush(Brush::RadialGradient(
552        i_slint_core::graphics::RadialGradientBrush::new_circle(
553            vec![
554                i_slint_core::graphics::GradientStop {
555                    position: 0.0,
556                    color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
557                },
558                i_slint_core::graphics::GradientStop {
559                    position: 0.5,
560                    color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
561                },
562                i_slint_core::graphics::GradientStop {
563                    position: 1.0,
564                    color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
565                },
566            ]
567            .drain(..),
568        ),
569    )))
570    .unwrap();
571    assert_eq!(&v, "\"@radial-gradient(circle, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
572}