Skip to main content

pax_runtime_api/pax_value/
coercion_impls.rs

1// ------------------------------- Coercion rules ----------------------------------
2// default coercion only allows a single type: the type expected
3// custom coercion rules can be implemented by a type
4
5use std::ops::Range;
6
7use crate::{
8    impl_default_coercion_rule,
9    math::{Transform2, Vector2},
10    Color, ColorChannel, Depth, Duration, Fill, GradientStop, LayoutRole, LightShape,
11    LinearGradient, Material, MaterialParams, MeshGradient, MeshPoint, Numeric, Opacity, Paint,
12    PathElement, PathSmoothing, PaxValue, Percent, Property, RadialGradient, Rotation, Size,
13    Stroke, StrokeCap, StrokeJoin, Transform2D, UnitValue, Vector3,
14};
15
16// Default coercion rules:
17// call Into::<first param>::into() on contents of second enum variant
18impl_default_coercion_rule!(bool, PaxValue::Bool, true);
19
20impl_default_coercion_rule!(u8, PaxValue::Numeric, true);
21impl_default_coercion_rule!(u16, PaxValue::Numeric, true);
22impl_default_coercion_rule!(u32, PaxValue::Numeric, true);
23impl_default_coercion_rule!(u64, PaxValue::Numeric, true);
24
25impl_default_coercion_rule!(i8, PaxValue::Numeric, true);
26impl_default_coercion_rule!(i16, PaxValue::Numeric, true);
27impl_default_coercion_rule!(i32, PaxValue::Numeric, true);
28impl_default_coercion_rule!(i64, PaxValue::Numeric, true);
29
30impl_default_coercion_rule!(f32, PaxValue::Numeric, true);
31impl_default_coercion_rule!(f64, PaxValue::Numeric, true);
32
33impl_default_coercion_rule!(isize, PaxValue::Numeric, true);
34impl_default_coercion_rule!(usize, PaxValue::Numeric, true);
35
36impl CoercionRules for LayoutRole {
37    fn try_coerce(value: PaxValue) -> Result<Self, String> {
38        match value {
39            PaxValue::Enum(contents) => {
40                let (_, variant, args) = *contents;
41                if !args.is_empty() {
42                    return Err(format!(
43                        "failed to coerce LayoutRole: expected no enum args, got {:?}",
44                        args
45                    ));
46                }
47                match variant.as_str() {
48                    "Default" => Ok(LayoutRole::Default),
49                    "Breakout" => Ok(LayoutRole::Breakout),
50                    _ => Err(format!(
51                        "failed to coerce LayoutRole: unknown enum variant {:?}",
52                        variant
53                    )),
54                }
55            }
56            PaxValue::Option(o) => {
57                if let Some(o) = *o {
58                    LayoutRole::try_coerce(o)
59                } else {
60                    Err("failed to coerce LayoutRole".to_string())
61                }
62            }
63            _ => Err("failed to coerce LayoutRole".to_string()),
64        }
65    }
66}
67
68/// Attempts to coerce a dynamic `PaxValue` into a concrete Rust type.
69///
70/// Coercion is intentionally broader than exact `ToPaxValue` conversion; for
71/// example a `Color` can be coerced into a `Paint`.
72pub trait CoercionRules
73where
74    Self: Sized + 'static,
75{
76    /// Attempts to coerce `value` into `Self`.
77    fn try_coerce(value: PaxValue) -> Result<Self, String>;
78
79    /// Opts appearance layers into scalar-to-singleton stack conversion.
80    /// Ordinary vector values remain list-only.
81    fn allows_singleton_stack() -> bool {
82        false
83    }
84
85    /// Whether cloning `Self` preserves the semantics of converting that clone
86    /// through `ToPaxValue` and back with `try_coerce`.
87    ///
88    /// Opt in only for exact, side-effect-free value roundtrips. In particular,
89    /// shared mutable handles are not eligible when conversion snapshots their
90    /// contents. Custom conversions conservatively use the default fallback.
91    /// Composite implementations must check every field with
92    /// [`super::is_typed_binding_safe`] so recursive types terminate safely.
93    fn is_identity_roundtrip() -> bool {
94        false
95    }
96}
97
98/// Checks the opt-in value-roundtrip contract for typed one-way bindings.
99/// Recursive type graphs conservatively fall back to conversion. This tracks
100/// only the current capability check, never values or property lifetimes.
101pub fn is_typed_binding_safe<T: CoercionRules>() -> bool {
102    use std::{any::TypeId, cell::RefCell, collections::HashSet};
103    thread_local! {
104        static CHECKING: RefCell<HashSet<TypeId>> = RefCell::new(HashSet::new());
105    }
106    struct Check(TypeId);
107    impl Drop for Check {
108        fn drop(&mut self) {
109            CHECKING.with(|checking| checking.borrow_mut().remove(&self.0));
110        }
111    }
112    let id = TypeId::of::<T>();
113    if !CHECKING.with(|checking| checking.borrow_mut().insert(id)) {
114        return false;
115    }
116    let _check = Check(id);
117    T::is_identity_roundtrip()
118}
119
120// #[allow(non_camel_case_types)]
121// #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq)]
122// pub enum Color {
123//     /// Models a color in the RGB space, with an alpha channel of 100%
124//     rgb(ColorChannel, ColorChannel, ColorChannel),
125//     /// Models a color in the RGBA space
126//     rgba(ColorChannel, ColorChannel, ColorChannel, ColorChannel),
127
128//     /// Models a color in the HSL space.
129//     hsl(Rotation, ColorChannel, ColorChannel),
130//     /// Models a color in the HSLA space.
131//     hsla(Rotation, ColorChannel, ColorChannel, ColorChannel),
132
133//     #[default]
134//     SLATE,
135//     GRAY,
136//     ZINC,
137//     NEUTRAL,
138//     STONE,
139//     RED,
140//     ORANGE,
141//     AMBER,
142//     YELLOW,
143//     LIME,
144//     GREEN,
145//     EMERALD,
146//     TEAL,
147//     CYAN,
148//     SKY,
149//     BLUE,
150//     INDIGO,
151//     VIOLET,
152//     PURPLE,
153//     FUCHSIA,
154//     PINK,
155//     ROSE,
156//     BLACK,
157//     WHITE,
158//     TRANSPARENT,
159//     NONE,
160// }
161
162impl CoercionRules for Color {
163    fn is_identity_roundtrip() -> bool {
164        true
165    }
166
167    fn try_coerce(value: PaxValue) -> Result<Self, String> {
168        match value {
169            PaxValue::Color(color) => Ok(*color),
170            PaxValue::Enum(contents) => {
171                let (name, variant, mut args) = *contents;
172                if name != "Color" {
173                    return Err(format!(
174                        "{:?} can't be coerced into a Color",
175                        PaxValue::Enum(Box::new((name, variant, args)))
176                    ));
177                }
178                match variant.as_str() {
179                    "rgb" => Ok(Color::rgb(
180                        ColorChannel::try_coerce(args.remove(0))?,
181                        ColorChannel::try_coerce(args.remove(0))?,
182                        ColorChannel::try_coerce(args.remove(0))?,
183                    )),
184                    "rgba" => Ok(Color::rgba(
185                        ColorChannel::try_coerce(args.remove(0))?,
186                        ColorChannel::try_coerce(args.remove(0))?,
187                        ColorChannel::try_coerce(args.remove(0))?,
188                        ColorChannel::try_coerce(args.remove(0))?,
189                    )),
190                    "hsl" => Ok(Color::hsl(
191                        Rotation::try_coerce(args.remove(0))?,
192                        ColorChannel::try_coerce(args.remove(0))?,
193                        ColorChannel::try_coerce(args.remove(0))?,
194                    )),
195                    "hsla" => Ok(Color::hsla(
196                        Rotation::try_coerce(args.remove(0))?,
197                        ColorChannel::try_coerce(args.remove(0))?,
198                        ColorChannel::try_coerce(args.remove(0))?,
199                        ColorChannel::try_coerce(args.remove(0))?,
200                    )),
201                    "SLATE" => Ok(Color::SLATE),
202                    "GRAY" => Ok(Color::GRAY),
203                    "ZINC" => Ok(Color::ZINC),
204                    "NEUTRAL" => Ok(Color::NEUTRAL),
205                    "STONE" => Ok(Color::STONE),
206                    "RED" => Ok(Color::RED),
207                    "ORANGE" => Ok(Color::ORANGE),
208                    "AMBER" => Ok(Color::AMBER),
209                    "YELLOW" => Ok(Color::YELLOW),
210                    "LIME" => Ok(Color::LIME),
211                    "GREEN" => Ok(Color::GREEN),
212                    "EMERALD" => Ok(Color::EMERALD),
213                    "TEAL" => Ok(Color::TEAL),
214                    "CYAN" => Ok(Color::CYAN),
215                    "SKY" => Ok(Color::SKY),
216                    "BLUE" => Ok(Color::BLUE),
217                    "INDIGO" => Ok(Color::INDIGO),
218                    "VIOLET" => Ok(Color::VIOLET),
219                    "PURPLE" => Ok(Color::PURPLE),
220                    "FUCHSIA" => Ok(Color::FUCHSIA),
221                    "PINK" => Ok(Color::PINK),
222                    "ROSE" => Ok(Color::ROSE),
223                    "BLACK" => Ok(Color::BLACK),
224                    "WHITE" => Ok(Color::WHITE),
225                    "TRANSPARENT" => Ok(Color::TRANSPARENT),
226                    "NONE" => Ok(Color::NONE),
227                    _ => Err(format!(
228                        "failed to convert to Color: unknown variant {:?}",
229                        variant
230                    )),
231                }
232            }
233            PaxValue::Option(o) => {
234                if let Some(o) = *o {
235                    Color::try_coerce(o)
236                } else {
237                    Err("failed to convert to Color".to_string())
238                }
239            }
240            _ => return Err(format!("{:?} can't be coerced into a Color", value)),
241        }
242    }
243}
244
245impl CoercionRules for PathElement {
246    fn is_identity_roundtrip() -> bool {
247        true
248    }
249
250    fn try_coerce(value: PaxValue) -> Result<Self, String> {
251        match value {
252            PaxValue::PathElement(path_elem) => Ok(*path_elem),
253            // Why is this needed? should never deserialize a path into this
254            PaxValue::Enum(contents) => {
255                let (name, variant, values) = *contents;
256                if name == "PathElement" {
257                    let mut values_itr = values.into_iter();
258                    match variant.as_str() {
259                        "Line" => Ok(PathElement::Line),
260                        "Close" => Ok(PathElement::Close),
261                        "Empty" => Ok(PathElement::Empty),
262                        "Point" => Ok(PathElement::Point(
263                            Size::try_coerce(values_itr.next().unwrap())?,
264                            Size::try_coerce(values_itr.next().unwrap())?,
265                        )),
266                        "Quadratic" => Ok(PathElement::Quadratic(
267                            Size::try_coerce(values_itr.next().unwrap())?,
268                            Size::try_coerce(values_itr.next().unwrap())?,
269                        )),
270                        "Cubic" => Ok(PathElement::Cubic(
271                            Size::try_coerce(values_itr.next().unwrap())?,
272                            Size::try_coerce(values_itr.next().unwrap())?,
273                            Size::try_coerce(values_itr.next().unwrap())?,
274                            Size::try_coerce(values_itr.next().unwrap())?,
275                        )),
276                        _ => {
277                            return Err(format!(
278                                "failed to coerce PathElement: unknown enum variant: {:?}",
279                                variant
280                            ))
281                        }
282                    }
283                } else {
284                    return Err(format!(
285                        "failed to coerce PathElement: enum name doesn't match"
286                    ));
287                }
288            }
289            _ => return Err(format!("failed to coerce PathElement: PaxValue not a path")),
290        }
291    }
292}
293
294// Paint is a type that other types (Color) can be coerced into, thus the default
295// from to pax macro isn't enough (only translates directly back and forth, and returns
296// an error if it contains any other type)
297impl CoercionRules for Paint {
298    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
299        Ok(match pax_value {
300            PaxValue::Color(color) => Paint::Solid(*color),
301            PaxValue::Enum(contents) => {
302                let (scope, variant, args) = *contents;
303                if scope == "Color" {
304                    return Color::try_coerce(PaxValue::Enum(Box::new((scope, variant, args))))
305                        .map(Paint::Solid);
306                }
307                if scope == "Fill" {
308                    return Err("Fill paint constructors moved to Paint; Fill now describes an appearance layer".into());
309                }
310                if scope != "Paint" {
311                    return Err(format!("expected Paint, received {scope}::{variant}"));
312                }
313                if matches!(
314                    variant.as_str(),
315                    "Solid" | "LinearGradient" | "RadialGradient" | "MeshGradient"
316                ) && args.len() != 1
317                {
318                    return Err(format!("Paint::{variant} requires exactly one argument"));
319                }
320                match variant.as_str() {
321                    "Blend" => {
322                        if args.len() != 1 {
323                            return Err("Paint::Blend requires one list of weighted paints".into());
324                        }
325                        Paint::blend(Vec::<(Paint, f64)>::try_coerce(
326                            args.into_iter().next().unwrap(),
327                        )?)
328                    }
329                    "Solid" => {
330                        let color = Color::try_coerce(args.into_iter().next().unwrap())?;
331                        Paint::Solid(color)
332                    }
333                    "MeshGradient" => Paint::MeshGradient(MeshGradient::try_coerce(
334                        args.into_iter().next().unwrap(),
335                    )?),
336                    "LinearGradient" => {
337                        let gradient =
338                            LinearGradient::try_coerce(args.into_iter().next().unwrap())?;
339                        Paint::LinearGradient(gradient)
340                    }
341                    "linearGradient" => Paint::LinearGradient(parse_linear_gradient_args(args)?),
342                    "RadialGradient" => {
343                        let gradient =
344                            RadialGradient::try_coerce(args.into_iter().next().unwrap())?;
345                        Paint::RadialGradient(gradient)
346                    }
347                    "radialGradient" => Paint::RadialGradient(parse_radial_gradient_args(args)?),
348                    _ => {
349                        return Err(format!(
350                            "failed to coerce Paint: unknown enum variant {:?}",
351                            variant
352                        ))
353                    }
354                }
355            }
356            PaxValue::Option(o) => {
357                if let Some(o) = *o {
358                    Paint::try_coerce(o)?
359                } else {
360                    return Err(format!("failed to coerce Paint: can't coerce None"));
361                }
362            }
363            _ => return Err(format!("{:?} can't be coerced into a Paint", pax_value)),
364        })
365    }
366}
367
368impl CoercionRules for MeshPoint {
369    fn try_coerce(value: PaxValue) -> Result<Self, String> {
370        match value {
371            PaxValue::Object(fields) => {
372                if fields
373                    .iter()
374                    .any(|(key, _)| key != "position" && key != "color")
375                {
376                    return Err("mesh anchors accept only `position` and `color`".into());
377                }
378                let [position, color] = extract_options(["position", "color"], fields)?;
379                let coordinates = Vec::<Size>::try_coerce(position)?;
380                let [x, y]: [Size; 2] = coordinates
381                    .try_into()
382                    .map_err(|_| "mesh anchor position requires exactly two coordinates")?;
383                Ok(Self {
384                    position: (x, y),
385                    color: Color::try_coerce(color)?,
386                })
387            }
388            PaxValue::Option(value) => Self::try_coerce(value.ok_or("mesh anchor cannot be None")?),
389            _ => Err("mesh anchor requires {position: [x, y], color: ...}".into()),
390        }
391    }
392}
393
394impl CoercionRules for MeshGradient {
395    fn try_coerce(value: PaxValue) -> Result<Self, String> {
396        match value {
397            PaxValue::Object(fields) => {
398                if fields.iter().any(|(key, _)| key != "rows") {
399                    return Err("mesh gradients accept only `rows`".into());
400                }
401                let [rows] = extract_options(["rows"], fields)?;
402                Ok(Self {
403                    rows: Vec::<Vec<MeshPoint>>::try_coerce(rows)?,
404                })
405            }
406            PaxValue::Option(value) => {
407                Self::try_coerce(value.ok_or("mesh gradient cannot be None")?)
408            }
409            _ => Err("mesh gradient requires {rows: ...}".into()),
410        }
411    }
412}
413
414impl CoercionRules for LinearGradient {
415    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
416        Ok(match pax_value {
417            PaxValue::Object(map) => {
418                let [start, end, stops] = extract_options(["start", "end", "stops"], map)
419                    .map_err(|e| format!("failed to convert to LinearGradient: {e}"))?;
420                let (s1, s2) = match start {
421                    PaxValue::Vec(vec) => {
422                        let mut itr = vec.into_iter();
423                        let s1 = Size::try_coerce(itr.next().unwrap())?;
424                        let s2 = Size::try_coerce(itr.next().unwrap())?;
425                        (s1, s2)
426                    }
427                    _ => return Err(format!("failed to coerce LinearGradient")),
428                };
429
430                let (e1, e2) = match end {
431                    PaxValue::Vec(vec) => {
432                        let mut itr = vec.into_iter();
433                        let e1 = Size::try_coerce(itr.next().unwrap())?;
434                        let e2 = Size::try_coerce(itr.next().unwrap())?;
435                        (e1, e2)
436                    }
437                    _ => return Err(format!("failed to coerce LinearGradient")),
438                };
439                let stops = Vec::<GradientStop>::try_coerce(stops)?;
440                LinearGradient {
441                    start: (s1, s2),
442                    end: (e1, e2),
443                    stops,
444                }
445            }
446            PaxValue::Enum(contents) => {
447                let (_, variant, args) = *contents;
448                match variant.as_str() {
449                    "linearGradient" => parse_linear_gradient_args(args)?,
450                    _ => return Err(format!("failed to coerce LinearGradient")),
451                }
452            }
453            PaxValue::Option(o) => {
454                if let Some(o) = *o {
455                    LinearGradient::try_coerce(o)?
456                } else {
457                    return Err(format!("failed to coerce LinearGradient"));
458                }
459            }
460            _ => {
461                return Err(format!(
462                    "{:?} can't be coerced into a LinearGradient",
463                    pax_value
464                ))
465            }
466        })
467    }
468}
469
470impl CoercionRules for RadialGradient {
471    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
472        Ok(match pax_value {
473            PaxValue::Object(map) => {
474                let [start, end, stops, radius] =
475                    extract_options(["start", "end", "stops", "radius"], map)
476                        .map_err(|e| format!("failed to convert to RadialGradient: {e}"))?;
477                let (s1, s2) = match start {
478                    PaxValue::Vec(vec) => {
479                        let mut itr = vec.into_iter();
480                        let s1 = Size::try_coerce(itr.next().unwrap())?;
481                        let s2 = Size::try_coerce(itr.next().unwrap())?;
482                        (s1, s2)
483                    }
484                    _ => {
485                        return Err(format!("failed to coerce RadialGradient"));
486                    }
487                };
488
489                let (e1, e2) = match end {
490                    PaxValue::Vec(vec) => {
491                        let mut itr = vec.into_iter();
492                        let e1 = Size::try_coerce(itr.next().unwrap())?;
493                        let e2 = Size::try_coerce(itr.next().unwrap())?;
494                        (e1, e2)
495                    }
496                    _ => {
497                        return Err(format!("failed to coerce RadialGradient"));
498                    }
499                };
500                let radius = match radius {
501                    PaxValue::Numeric(n) => n.to_float(),
502                    _ => {
503                        return Err(format!("failed to coerce RadialGradient"));
504                    }
505                };
506                let stops = Vec::<GradientStop>::try_coerce(stops)?;
507                RadialGradient {
508                    start: (s1, s2),
509                    end: (e1, e2),
510                    radius,
511                    stops,
512                }
513            }
514            PaxValue::Enum(contents) => {
515                let (_, variant, args) = *contents;
516                match variant.as_str() {
517                    "radialGradient" => parse_radial_gradient_args(args)?,
518                    _ => return Err(format!("failed to coerce RadialGradient")),
519                }
520            }
521            PaxValue::Option(o) => {
522                if let Some(o) = *o {
523                    RadialGradient::try_coerce(o)?
524                } else {
525                    return Err(format!("failed to coerce RadialGradient"));
526                }
527            }
528            _ => {
529                return Err(format!("failed to coerce RadialGradient"));
530            }
531        })
532    }
533}
534
535impl CoercionRules for GradientStop {
536    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
537        Ok(match pax_value {
538            PaxValue::Object(map) => {
539                let [position, color] = extract_options(["position", "color"], map)
540                    .map_err(|e| format!("failed to convert to GradientStop: {e}"))?;
541                let position = Size::try_coerce(position)?;
542                let color = Color::try_coerce(color)?;
543                GradientStop { position, color }
544            }
545            PaxValue::Enum(contents) => {
546                let (_, variant, args) = *contents;
547                match variant.as_str() {
548                    "get" => {
549                        let mut args = args.into_iter();
550                        let color = Color::try_coerce(
551                            args.next()
552                                .ok_or_else(|| "failed to convert to GradientStop".to_string())?,
553                        )?;
554                        let position = Size::try_coerce(
555                            args.next()
556                                .ok_or_else(|| "failed to convert to GradientStop".to_string())?,
557                        )?;
558                        GradientStop { position, color }
559                    }
560                    _ => return Err(format!("failed to convert to GradientStop")),
561                }
562            }
563            PaxValue::Option(o) => {
564                if let Some(o) = *o {
565                    GradientStop::try_coerce(o)?
566                } else {
567                    return Err(format!("failed to convert to GradientStop"));
568                }
569            }
570            _ => {
571                return Err(format!("failed to convert to GradientStop"));
572            }
573        })
574    }
575}
576
577impl CoercionRules for Depth {
578    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
579        Ok(match pax_value {
580            PaxValue::Size(Size::Pixels(value)) => Depth(value),
581            PaxValue::Size(Size::Percent(_)) | PaxValue::Percent(_) => {
582                return Err("failed to coerce Depth: percent depth is not supported".to_string());
583            }
584            PaxValue::Size(Size::Combined(_, _)) => {
585                return Err("failed to coerce Depth: combined depth is not supported".to_string());
586            }
587            PaxValue::Numeric(value) => Depth(value),
588            PaxValue::Option(o) => {
589                if let Some(o) = *o {
590                    Depth::try_coerce(o)?
591                } else {
592                    return Err("failed to coerce Depth".to_string());
593                }
594            }
595            _ => return Err(format!("{:?} can't be coerced into a Depth", pax_value)),
596        })
597    }
598}
599
600impl CoercionRules for Vector3 {
601    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
602        Ok(match pax_value {
603            PaxValue::Object(map) => {
604                let [x, y, z] = extract_options(["x", "y", "z"], map)
605                    .map_err(|e| format!("failed to convert to Vector3: {e}"))?;
606                Vector3 {
607                    x: f64::try_coerce(x)?,
608                    y: f64::try_coerce(y)?,
609                    z: f64::try_coerce(z)?,
610                }
611            }
612            PaxValue::Vec(values) => {
613                if values.len() != 3 {
614                    return Err("failed to coerce Vector3: expected three values".to_string());
615                }
616                let mut values = values.into_iter();
617                Vector3 {
618                    x: f64::try_coerce(values.next().unwrap())?,
619                    y: f64::try_coerce(values.next().unwrap())?,
620                    z: f64::try_coerce(values.next().unwrap())?,
621                }
622            }
623            PaxValue::Enum(contents) => {
624                let (_, variant, values) = *contents;
625                match variant.as_str() {
626                    "new" => {
627                        if values.len() != 3 {
628                            return Err("failed to coerce Vector3::new".to_string());
629                        }
630                        let mut values = values.into_iter();
631                        Vector3 {
632                            x: f64::try_coerce(values.next().unwrap())?,
633                            y: f64::try_coerce(values.next().unwrap())?,
634                            z: f64::try_coerce(values.next().unwrap())?,
635                        }
636                    }
637                    _ => {
638                        return Err(format!(
639                            "failed to coerce Vector3: unknown enum variant {:?}",
640                            variant
641                        ))
642                    }
643                }
644            }
645            PaxValue::Option(o) => {
646                if let Some(o) = *o {
647                    Vector3::try_coerce(o)?
648                } else {
649                    return Err("failed to coerce Vector3".to_string());
650                }
651            }
652            _ => return Err(format!("{:?} can't be coerced into a Vector3", pax_value)),
653        })
654    }
655}
656
657impl CoercionRules for LightShape {
658    fn try_coerce(value: PaxValue) -> Result<Self, String> {
659        match value {
660            PaxValue::Enum(contents) => {
661                let (_, variant, args) = *contents;
662                if !args.is_empty() {
663                    return Err(format!(
664                        "failed to coerce LightShape: expected no enum args, got {:?}",
665                        args
666                    ));
667                }
668                match variant.as_str() {
669                    "Point" => Ok(LightShape::Point),
670                    "Directional" => Ok(LightShape::Directional),
671                    _ => Err(format!(
672                        "failed to coerce LightShape: unknown enum variant {:?}",
673                        variant
674                    )),
675                }
676            }
677            PaxValue::Option(o) => {
678                if let Some(o) = *o {
679                    LightShape::try_coerce(o)
680                } else {
681                    Err("failed to coerce LightShape".to_string())
682                }
683            }
684            _ => Err("failed to coerce LightShape".to_string()),
685        }
686    }
687}
688
689impl CoercionRules for MaterialParams {
690    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
691        Ok(match pax_value {
692            PaxValue::Object(map) => {
693                let mut params = MaterialParams::default();
694                for (key, value) in map {
695                    match key.as_str() {
696                        "ambient" => params.ambient = Property::new(f64::try_coerce(value)?),
697                        "diffuse" => params.diffuse = Property::new(f64::try_coerce(value)?),
698                        "specular" => params.specular = Property::new(f64::try_coerce(value)?),
699                        "roughness" => params.roughness = Property::new(f64::try_coerce(value)?),
700                        "metallic" => params.metallic = Property::new(f64::try_coerce(value)?),
701                        "emissive" => params.emissive = Property::new(Color::try_coerce(value)?),
702                        "emissive_intensity" | "emissiveIntensity" => {
703                            params.emissive_intensity = Property::new(f64::try_coerce(value)?)
704                        }
705                        _ => {}
706                    }
707                }
708                params
709            }
710            PaxValue::Option(o) => {
711                if let Some(o) = *o {
712                    MaterialParams::try_coerce(o)?
713                } else {
714                    return Err("failed to coerce MaterialParams".to_string());
715                }
716            }
717            _ => {
718                return Err(format!(
719                    "{:?} can't be coerced into MaterialParams",
720                    pax_value
721                ))
722            }
723        })
724    }
725}
726
727impl CoercionRules for Material {
728    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
729        Ok(match pax_value {
730            PaxValue::Object(map) => {
731                Material::Lit(MaterialParams::try_coerce(PaxValue::Object(map))?)
732            }
733            PaxValue::Enum(contents) => {
734                let (_, variant, args) = *contents;
735                match variant.as_str() {
736                    "Lit" | "custom" => {
737                        let params = args
738                            .into_iter()
739                            .next()
740                            .ok_or_else(|| "failed to coerce Material::custom".to_string())?;
741                        Material::Lit(MaterialParams::try_coerce(params)?)
742                    }
743                    "Unlit" | "unlit" => Material::Unlit,
744                    "Matte" | "matte" => Material::matte(),
745                    "Glossy" | "glossy" => {
746                        let specular = args
747                            .into_iter()
748                            .next()
749                            .map(f64::try_coerce)
750                            .transpose()?
751                            .unwrap_or(0.28);
752                        Material::glossy(specular)
753                    }
754                    "Metallic" | "metallic" => {
755                        let metallic = args
756                            .into_iter()
757                            .next()
758                            .map(f64::try_coerce)
759                            .transpose()?
760                            .unwrap_or(0.72);
761                        Material::metallic(metallic)
762                    }
763                    "Emissive" | "emissive" => {
764                        if args.len() != 2 {
765                            return Err("failed to coerce Material::emissive".to_string());
766                        }
767                        let mut args = args.into_iter();
768                        let color = Color::try_coerce(args.next().unwrap())?;
769                        let intensity = f64::try_coerce(args.next().unwrap())?;
770                        Material::emissive(color, intensity)
771                    }
772                    _ => {
773                        return Err(format!(
774                            "failed to coerce Material: unknown enum variant {:?}",
775                            variant
776                        ))
777                    }
778                }
779            }
780            PaxValue::Option(o) => {
781                if let Some(o) = *o {
782                    Material::try_coerce(o)?
783                } else {
784                    return Err("failed to coerce Material".to_string());
785                }
786            }
787            _ => return Err(format!("{:?} can't be coerced into Material", pax_value)),
788        })
789    }
790}
791
792fn parse_gradient_point(value: PaxValue) -> Result<(Size, Size), String> {
793    match value {
794        PaxValue::Vec(vec) => {
795            let mut itr = vec.into_iter();
796            let x = Size::try_coerce(
797                itr.next()
798                    .ok_or_else(|| "failed to coerce gradient point".to_string())?,
799            )?;
800            let y = Size::try_coerce(
801                itr.next()
802                    .ok_or_else(|| "failed to coerce gradient point".to_string())?,
803            )?;
804            Ok((x, y))
805        }
806        _ => Err("failed to coerce gradient point".to_string()),
807    }
808}
809
810fn parse_linear_gradient_args(args: Vec<PaxValue>) -> Result<LinearGradient, String> {
811    if args.len() != 3 {
812        return Err("failed to coerce Paint::linearGradient".to_string());
813    }
814    let mut args = args.into_iter();
815    let start = parse_gradient_point(args.next().unwrap())?;
816    let end = parse_gradient_point(args.next().unwrap())?;
817    let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
818    Ok(LinearGradient { start, end, stops })
819}
820
821fn parse_radial_gradient_args(args: Vec<PaxValue>) -> Result<RadialGradient, String> {
822    if args.len() != 4 {
823        return Err("failed to coerce Paint::radialGradient".to_string());
824    }
825    let mut args = args.into_iter();
826    let start = parse_gradient_point(args.next().unwrap())?;
827    let end = parse_gradient_point(args.next().unwrap())?;
828    let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
829    let radius = Numeric::try_coerce(args.next().unwrap())?.to_float();
830    Ok(RadialGradient {
831        start,
832        end,
833        radius,
834        stops,
835    })
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    fn pct(value: i64) -> PaxValue {
843        PaxValue::Percent(Percent(Numeric::I64(value)))
844    }
845
846    fn color(hex: &str) -> PaxValue {
847        PaxValue::Color(Box::new(Color::from_hex(hex)))
848    }
849
850    #[test]
851    fn coerces_color_helper_syntax() {
852        let pax_value = PaxValue::Enum(Box::new((
853            "Color".to_string(),
854            "rgba".to_string(),
855            vec![
856                PaxValue::Numeric(255.into()),
857                PaxValue::Numeric(224.into()),
858                PaxValue::Numeric(162.into()),
859                PaxValue::Numeric(255.into()),
860            ],
861        )));
862
863        let color = Color::try_coerce(pax_value).expect("rgba helper syntax should coerce");
864        assert_eq!(
865            color,
866            Color::rgba(
867                ColorChannel::Integer(255),
868                ColorChannel::Integer(224),
869                ColorChannel::Integer(162),
870                ColorChannel::Integer(255),
871            )
872        );
873    }
874
875    #[test]
876    fn coerces_fill_linear_gradient_helper_syntax() {
877        let pax_value = PaxValue::Enum(Box::new((
878            "Paint".to_string(),
879            "linearGradient".to_string(),
880            vec![
881                PaxValue::Vec(vec![pct(0), pct(50)]),
882                PaxValue::Vec(vec![pct(100), pct(50)]),
883                PaxValue::Vec(vec![
884                    PaxValue::Enum(Box::new((
885                        "GradientStop".to_string(),
886                        "get".to_string(),
887                        vec![color("ff00ffff"), pct(0)],
888                    ))),
889                    PaxValue::Enum(Box::new((
890                        "GradientStop".to_string(),
891                        "get".to_string(),
892                        vec![color("ffffffff"), pct(100)],
893                    ))),
894                ]),
895            ],
896        )));
897
898        let fill = Paint::try_coerce(pax_value).expect("helper syntax should coerce");
899        let Paint::LinearGradient(gradient) = fill else {
900            panic!("expected linear gradient");
901        };
902
903        assert_eq!(gradient.stops.len(), 2);
904        assert_eq!(gradient.start.0, Size::Percent(Numeric::I64(0)));
905        assert_eq!(gradient.end.0, Size::Percent(Numeric::I64(100)));
906    }
907
908    #[test]
909    fn coerces_gradient_stop_get_helper_syntax() {
910        let pax_value = PaxValue::Enum(Box::new((
911            "GradientStop".to_string(),
912            "get".to_string(),
913            vec![color("11223344"), pct(48)],
914        )));
915
916        let stop = GradientStop::try_coerce(pax_value).expect("helper syntax should coerce");
917        assert_eq!(stop.position, Size::Percent(Numeric::I64(48)));
918        assert_eq!(stop.color, Color::from_hex("11223344"));
919    }
920
921    #[test]
922    fn coerces_opacity_from_percent_literal() {
923        let opacity = Opacity::try_coerce(pct(50)).expect("percent opacity should coerce");
924        assert_eq!(opacity, Opacity::Percent(Numeric::I64(50)));
925        assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
926    }
927
928    #[test]
929    fn coerces_opacity_from_numeric_literal() {
930        let opacity = Opacity::try_coerce(PaxValue::Numeric(Numeric::F64(0.5)))
931            .expect("numeric opacity should coerce");
932        assert_eq!(opacity, Opacity::Alpha(Numeric::F64(0.5)));
933        assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
934    }
935
936    #[test]
937    fn coerces_depth_from_px_but_rejects_percent() {
938        let depth = Depth::try_coerce(PaxValue::Size(Size::Pixels(Numeric::F64(260.0))))
939            .expect("px depth should coerce");
940        assert_eq!(depth.to_float(), 260.0);
941
942        let err = Depth::try_coerce(pct(50)).expect_err("percent depth should fail");
943        assert!(err.contains("percent depth"));
944    }
945
946    #[test]
947    fn coerces_material_helper_syntax() {
948        let pax_value = PaxValue::Enum(Box::new((
949            "Material".to_string(),
950            "glossy".to_string(),
951            vec![PaxValue::Numeric(Numeric::F64(0.44))],
952        )));
953
954        let material = Material::try_coerce(pax_value).expect("material helper should coerce");
955        let Material::Lit(params) = material else {
956            panic!("expected lit material");
957        };
958        assert_eq!(params.specular.get(), 0.44);
959        assert_eq!(params.roughness.get(), 0.28);
960    }
961}
962
963fn layer_object(value: PaxValue, expected: &str) -> Result<PaxValue, String> {
964    match value {
965        PaxValue::Enum(contents) if contents.0 == "Fill" || contents.0 == "Stroke" => {
966            let (name, variant, mut args) = *contents;
967            if name != expected || variant != "__layer" || args.len() != 1 {
968                return Err(format!("expected {expected} layer, received {name}::{variant}; paint constructors now use Paint"));
969            }
970            Ok(args.remove(0))
971        }
972        PaxValue::Option(value) => match *value {
973            Some(value) => layer_object(value, expected),
974            None => Err(format!(
975                "expected {expected}, received None; use [] for no layers"
976            )),
977        },
978        value => Ok(value),
979    }
980}
981
982macro_rules! layer_coercion {
983    ($ty:ident, $($field:ident: $field_ty:ty),+) => {
984        impl CoercionRules for $ty {
985            fn allows_singleton_stack() -> bool { true }
986            fn try_coerce(value: PaxValue) -> Result<Self, String> {
987                match layer_object(value, stringify!($ty))? {
988                    PaxValue::Object(fields) => {
989                        let mut layer = Self::default();
990                        for (name, value) in fields {
991                            match name.as_str() {
992                                $(stringify!($field) => {
993                                    layer.$field = Property::new(<$field_ty>::try_coerce(value)
994                                        .map_err(|e| format!("{}.{}: expected {}: {e}", stringify!($ty), name, stringify!($field_ty)))?);
995                                },)+
996                                "color" => return Err(format!("{}.color was renamed to paint", stringify!($ty))),
997                                _ => return Err(format!("unknown {} field `{name}`", stringify!($ty))),
998                            }
999                        }
1000                        layer.validate_layer()?;
1001                        Ok(layer)
1002                    }
1003                    value => Paint::try_coerce(value).map(Self::from)
1004                        .map_err(|e| format!("expected {} layer or Paint: {e}", stringify!($ty))),
1005                }
1006            }
1007        }
1008    }
1009}
1010layer_coercion!(Fill, paint: Paint, material: Material, opacity: Opacity);
1011layer_coercion!(Stroke, paint: Paint, material: Material, opacity: Opacity, width: Size, cap: StrokeCap, join: StrokeJoin);
1012
1013impl CoercionRules for StrokeCap {
1014    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1015        match value {
1016            PaxValue::Enum(contents) => {
1017                let (_, variant, args) = *contents;
1018                if !args.is_empty() {
1019                    return Err(format!(
1020                        "failed to coerce StrokeCap: expected no enum args, got {:?}",
1021                        args
1022                    ));
1023                }
1024                match variant.as_str() {
1025                    "Butt" => Ok(StrokeCap::Butt),
1026                    "Round" => Ok(StrokeCap::Round),
1027                    "Square" => Ok(StrokeCap::Square),
1028                    _ => Err(format!(
1029                        "failed to coerce StrokeCap: unknown enum variant {:?}",
1030                        variant
1031                    )),
1032                }
1033            }
1034            PaxValue::Option(o) => {
1035                if let Some(o) = *o {
1036                    StrokeCap::try_coerce(o)
1037                } else {
1038                    Err("failed to coerce StrokeCap".to_string())
1039                }
1040            }
1041            _ => Err(format!("failed to coerce StrokeCap")),
1042        }
1043    }
1044}
1045
1046impl CoercionRules for PathSmoothing {
1047    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1048        match value {
1049            PaxValue::Enum(contents) => {
1050                let (_, variant, args) = *contents;
1051                if !args.is_empty() {
1052                    return Err(format!(
1053                        "failed to coerce PathSmoothing: expected no enum args, got {:?}",
1054                        args
1055                    ));
1056                }
1057                match variant.as_str() {
1058                    "None" => Ok(PathSmoothing::None),
1059                    "Light" => Ok(PathSmoothing::Light),
1060                    "Strong" => Ok(PathSmoothing::Strong),
1061                    _ => Err(format!(
1062                        "failed to coerce PathSmoothing: unknown enum variant {:?}",
1063                        variant
1064                    )),
1065                }
1066            }
1067            PaxValue::Option(o) => {
1068                if let Some(o) = *o {
1069                    PathSmoothing::try_coerce(o)
1070                } else {
1071                    Err("failed to coerce PathSmoothing".to_string())
1072                }
1073            }
1074            _ => Err(format!("failed to coerce PathSmoothing")),
1075        }
1076    }
1077}
1078
1079impl CoercionRules for StrokeJoin {
1080    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1081        match value {
1082            PaxValue::Enum(contents) => {
1083                let (_, variant, args) = *contents;
1084                if !args.is_empty() {
1085                    return Err(format!(
1086                        "failed to coerce StrokeJoin: expected no enum args, got {:?}",
1087                        args
1088                    ));
1089                }
1090                match variant.as_str() {
1091                    "Miter" => Ok(StrokeJoin::Miter),
1092                    "Round" => Ok(StrokeJoin::Round),
1093                    "Bevel" => Ok(StrokeJoin::Bevel),
1094                    _ => Err(format!(
1095                        "failed to coerce StrokeJoin: unknown enum variant {:?}",
1096                        variant
1097                    )),
1098                }
1099            }
1100            PaxValue::Option(o) => {
1101                if let Some(o) = *o {
1102                    StrokeJoin::try_coerce(o)
1103                } else {
1104                    Err("failed to coerce StrokeJoin".to_string())
1105                }
1106            }
1107            _ => Err(format!("failed to coerce StrokeJoin")),
1108        }
1109    }
1110}
1111
1112impl CoercionRules for ColorChannel {
1113    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1114        Ok(match value {
1115            PaxValue::Rotation(rot) => ColorChannel::Rotation(rot),
1116            PaxValue::Percent(perc) => ColorChannel::Percent(perc.0),
1117            PaxValue::Numeric(num) => ColorChannel::Integer(num.to_int().clamp(0, 255) as u8),
1118            PaxValue::Enum(contents) => {
1119                let (_, variant, args) = *contents;
1120                match variant.as_str() {
1121                    "Rotation" => {
1122                        let rot = Rotation::try_coerce(args.into_iter().next().unwrap())?;
1123                        ColorChannel::Rotation(rot)
1124                    }
1125                    "Integer" => {
1126                        let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1127                        ColorChannel::Integer(num.to_int().clamp(0, 255) as u8)
1128                    }
1129                    "Percent" => {
1130                        let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1131                        ColorChannel::Percent(num)
1132                    }
1133                    _ => {
1134                        return Err(format!(
1135                            "failed to convert to ColorChannel: unknown variant {:?}",
1136                            variant
1137                        ))
1138                    }
1139                }
1140            }
1141            PaxValue::Option(o) => {
1142                if let Some(o) = *o {
1143                    ColorChannel::try_coerce(o)?
1144                } else {
1145                    return Err(format!("failed to convert to ColorChannel"));
1146                }
1147            }
1148            _ => return Err(format!("failed to convert to ColorChannel")),
1149        })
1150    }
1151}
1152
1153impl CoercionRules for Opacity {
1154    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1155        Ok(match value {
1156            PaxValue::Percent(perc) => Opacity::Percent(perc.0),
1157            PaxValue::Numeric(num) => Opacity::Alpha(num),
1158            PaxValue::Enum(contents) => {
1159                let (_, variant, args) = *contents;
1160                match variant.as_str() {
1161                    "Alpha" | "Numeric" => {
1162                        let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1163                        Opacity::Alpha(num)
1164                    }
1165                    "Percent" => {
1166                        let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1167                        Opacity::Percent(num)
1168                    }
1169                    _ => {
1170                        return Err(format!(
1171                            "failed to convert to Opacity: unknown variant {:?}",
1172                            variant
1173                        ))
1174                    }
1175                }
1176            }
1177            PaxValue::Option(o) => {
1178                if let Some(o) = *o {
1179                    Opacity::try_coerce(o)?
1180                } else {
1181                    return Err("failed to convert to Opacity".to_string());
1182                }
1183            }
1184            _ => return Err("failed to convert to Opacity".to_string()),
1185        })
1186    }
1187}
1188
1189impl CoercionRules for UnitValue {
1190    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1191        Ok(match value {
1192            PaxValue::Percent(percent) => UnitValue::Percent(percent.0),
1193            PaxValue::Numeric(num) => UnitValue::Unitless(num),
1194            PaxValue::Enum(contents) => {
1195                let (_, variant, args) = *contents;
1196                let mut args = args.into_iter();
1197                match variant.as_str() {
1198                    "Unitless" | "Numeric" => {
1199                        UnitValue::Unitless(Numeric::try_coerce(args.next().unwrap())?)
1200                    }
1201                    "Percent" => UnitValue::Percent(Numeric::try_coerce(args.next().unwrap())?),
1202                    _ => {
1203                        return Err(format!(
1204                            "failed to convert to UnitValue: unknown variant {:?}",
1205                            variant
1206                        ))
1207                    }
1208                }
1209            }
1210            PaxValue::Option(mut opt) => {
1211                if let Some(value) = opt.take() {
1212                    UnitValue::try_coerce(value)?
1213                } else {
1214                    return Err("failed to convert to UnitValue".to_string());
1215                }
1216            }
1217            _ => return Err("failed to convert to UnitValue".to_string()),
1218        })
1219    }
1220}
1221
1222impl CoercionRules for Percent {
1223    fn is_identity_roundtrip() -> bool {
1224        true
1225    }
1226
1227    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1228        Ok(match pax_value {
1229            PaxValue::Percent(p) => p,
1230            PaxValue::Numeric(v) => Percent(v),
1231            PaxValue::Option(mut opt) => {
1232                if let Some(p) = opt.take() {
1233                    Percent::try_coerce(p)?
1234                } else {
1235                    return Err(format!("None can't be coerced into a Percent"));
1236                }
1237            }
1238            _ => return Err(format!("{:?} can't be coerced into a Percent", pax_value)),
1239        })
1240    }
1241}
1242
1243impl CoercionRules for Size {
1244    fn is_identity_roundtrip() -> bool {
1245        true
1246    }
1247
1248    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1249        Ok(match pax_value {
1250            PaxValue::Size(size) => size,
1251            PaxValue::Percent(p) => Size::Percent(p.0),
1252            PaxValue::Numeric(v) => Size::Pixels(v),
1253            PaxValue::Option(mut opt) => {
1254                if let Some(p) = opt.take() {
1255                    Size::try_coerce(p)?
1256                } else {
1257                    return Err(format!("None can't be coerced into a Size"));
1258                }
1259            }
1260            _ => return Err(format!("{:?} can't be coerced into a Size", pax_value)),
1261        })
1262    }
1263}
1264
1265impl CoercionRules for String {
1266    fn is_identity_roundtrip() -> bool {
1267        true
1268    }
1269
1270    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1271        Ok(match pax_value {
1272            PaxValue::String(s) => s,
1273            PaxValue::Numeric(n) => {
1274                if n.is_float() {
1275                    n.to_float().to_string()
1276                } else {
1277                    n.to_int().to_string()
1278                }
1279            }
1280            PaxValue::Option(mut opt) => {
1281                if let Some(p) = opt.take() {
1282                    String::try_coerce(p)?
1283                } else {
1284                    return Err(format!("None can't be coerced into a String"));
1285                }
1286            }
1287            _ => return Err(format!("{:?} can't be coerced into a String", pax_value)),
1288        })
1289    }
1290}
1291
1292impl CoercionRules for Rotation {
1293    fn is_identity_roundtrip() -> bool {
1294        true
1295    }
1296
1297    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1298        Ok(match pax_value {
1299            PaxValue::Rotation(r) => r,
1300            PaxValue::Numeric(n) => Rotation::Degrees(n),
1301            PaxValue::Percent(p) => Rotation::Percent(p.0),
1302            PaxValue::Option(mut opt) => {
1303                if let Some(p) = opt.take() {
1304                    Rotation::try_coerce(p)?
1305                } else {
1306                    return Err(format!("None can't be coerced into a Rotation"));
1307                }
1308            }
1309            _ => return Err(format!("{:?} can't be coerced into a Rotation", pax_value)),
1310        })
1311    }
1312}
1313
1314impl CoercionRules for Duration {
1315    fn is_identity_roundtrip() -> bool {
1316        true
1317    }
1318
1319    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1320        Ok(match pax_value {
1321            PaxValue::Duration(duration) => duration,
1322            PaxValue::Numeric(value) => Duration::Frames(value),
1323            PaxValue::Option(mut opt) => {
1324                if let Some(p) = opt.take() {
1325                    Duration::try_coerce(p)?
1326                } else {
1327                    return Err(format!("None can't be coerced into a Duration"));
1328                }
1329            }
1330            _ => return Err(format!("{:?} can't be coerced into a Duration", pax_value)),
1331        })
1332    }
1333}
1334
1335impl CoercionRules for Numeric {
1336    fn is_identity_roundtrip() -> bool {
1337        true
1338    }
1339
1340    fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1341        Ok(match pax_value {
1342            PaxValue::Bool(b) => (b as i32).into(),
1343            PaxValue::Numeric(n) => n.into(),
1344            PaxValue::Size(n) => n.into(),
1345            PaxValue::Option(mut opt) => {
1346                if let Some(p) = opt.take() {
1347                    Numeric::try_coerce(p)?
1348                } else {
1349                    return Err(format!("None can't be coerced into a Numeric"));
1350                }
1351            }
1352            _ => return Err(format!("{:?} can't be coerced into a Numeric", pax_value)),
1353        })
1354    }
1355}
1356
1357impl<T: CoercionRules> CoercionRules for Vec<T> {
1358    fn allows_singleton_stack() -> bool {
1359        T::allows_singleton_stack()
1360    }
1361    fn is_identity_roundtrip() -> bool {
1362        is_typed_binding_safe::<T>()
1363    }
1364
1365    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1366        match value {
1367            PaxValue::Vec(vec) => {
1368                let res: Result<Vec<T>, _> = vec
1369                    .into_iter()
1370                    .enumerate()
1371                    .map(|(i, v)| T::try_coerce(v).map_err(|e| format!("entry [{i}]: {e}")))
1372                    .collect();
1373                res.map_err(|e| format!("couldn't coerce vec, element {:?}", e))
1374            }
1375            PaxValue::Option(mut opt) => {
1376                if let Some(p) = opt.take() {
1377                    Vec::<T>::try_coerce(p)
1378                } else {
1379                    return Err(format!("None can't be coerced into a Vec"));
1380                }
1381            }
1382            v if T::allows_singleton_stack() => T::try_coerce(v).map(|layer| vec![layer]),
1383            v => Err(format!(
1384                "{:?} can't be coerced into {:?}",
1385                v,
1386                std::any::type_name::<Vec<T>>(),
1387            )),
1388        }
1389    }
1390}
1391
1392impl<T1: CoercionRules, T2: CoercionRules> CoercionRules for (T1, T2) {
1393    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1394        match value {
1395            PaxValue::Vec(vec) => {
1396                let mut itr = vec.into_iter();
1397                let res: Result<T1, _> = T1::try_coerce(itr.next().unwrap());
1398                let res2: Result<T2, _> = T2::try_coerce(itr.next().unwrap());
1399                res.and_then(|v1| res2.map(|v2| (v1, v2)))
1400            }
1401            PaxValue::Option(opt) => {
1402                if let Some(p) = *opt {
1403                    <(T1, T2)>::try_coerce(p)
1404                } else {
1405                    return Err(format!("None can't be coerced into a Vec"));
1406                }
1407            }
1408            v => Err(format!(
1409                "{:?} can't be coerced into {:?}",
1410                v,
1411                std::any::type_name::<(T1, T2)>(),
1412            )),
1413        }
1414    }
1415}
1416
1417impl<T: CoercionRules> CoercionRules for Option<T> {
1418    fn is_identity_roundtrip() -> bool {
1419        is_typed_binding_safe::<T>()
1420    }
1421
1422    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1423        match value {
1424            PaxValue::Option(opt) => {
1425                let res: Result<Option<T>, _> = opt.map(|v| T::try_coerce(v)).transpose();
1426                res.map_err(|e| format!("couldn't coerce option, element {:?}", e))
1427            }
1428            v => Some(T::try_coerce(v)).transpose(),
1429        }
1430    }
1431}
1432
1433impl<T: CoercionRules> CoercionRules for Range<T> {
1434    fn is_identity_roundtrip() -> bool {
1435        is_typed_binding_safe::<T>()
1436    }
1437
1438    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1439        match value {
1440            PaxValue::Range(start, end) => {
1441                let start = T::try_coerce(*start)?;
1442                let end = T::try_coerce(*end)?;
1443                Ok(start..end)
1444            }
1445            PaxValue::Option(mut opt) => {
1446                if let Some(p) = opt.take() {
1447                    Range::<T>::try_coerce(p)
1448                } else {
1449                    return Err(format!("None can't be coerced into a Range"));
1450                }
1451            }
1452            v => Err(format!(
1453                "{:?} can't be coerced into {:?}",
1454                v,
1455                std::any::type_name::<Range<T>>(),
1456            )),
1457        }
1458    }
1459}
1460
1461impl CoercionRules for PaxValue {
1462    fn is_identity_roundtrip() -> bool {
1463        true
1464    }
1465
1466    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1467        Ok(value)
1468    }
1469}
1470
1471impl<T: CoercionRules> CoercionRules for Box<T> {
1472    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1473        Ok(Box::new(T::try_coerce(value)?))
1474    }
1475}
1476
1477impl CoercionRules for Transform2D {
1478    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1479        Ok(match value {
1480            PaxValue::Option(opt) => {
1481                if let Some(t) = *opt {
1482                    Transform2D::try_coerce(t)?
1483                } else {
1484                    return Err(format!("None can't be coerced into a Transform2D"));
1485                }
1486            }
1487            PaxValue::Object(map) => {
1488                let [previous, rotate, translate, anchor, scale, skew] = extract_options(
1489                    ["previous", "rotate", "translate", "anchor", "scale", "skew"],
1490                    map,
1491                )
1492                .map_err(|e| format!("failed to convert to Transform2D: {e}"))?;
1493                let previous = Option::<Box<Transform2D>>::try_coerce(previous)?;
1494                let rotate = Option::<Rotation>::try_coerce(rotate)?;
1495                let translate = match translate {
1496                    PaxValue::Option(opt) => {
1497                        if let Some(t) = *opt {
1498                            let t = Vec::<Size>::try_coerce(t)?;
1499                            if t.len() != 2 {
1500                                return Err(format!(
1501                                    "expected 2 elements in translate, got {:?}",
1502                                    t.len()
1503                                ));
1504                            }
1505                            Some([t[0], t[1]])
1506                        } else {
1507                            None
1508                        }
1509                    }
1510                    _ => return Err(format!("translate can't be coerced into a Transform2D",)),
1511                };
1512                let anchor = match anchor {
1513                    PaxValue::Option(opt) => {
1514                        if let Some(a) = *opt {
1515                            let a = Vec::<Size>::try_coerce(a)?;
1516                            if a.len() != 2 {
1517                                return Err(format!(
1518                                    "expected 2 elements in anchor, got {:?}",
1519                                    a.len()
1520                                ));
1521                            }
1522                            Some([a[0], a[1]])
1523                        } else {
1524                            None
1525                        }
1526                    }
1527                    _ => return Err(format!("anchor can't be coerced into a Transform2D",)),
1528                };
1529                let scale = match scale {
1530                    PaxValue::Option(opt) => {
1531                        if let Some(s) = *opt {
1532                            let s = Vec::<Size>::try_coerce(s)?;
1533                            if s.len() != 2 {
1534                                return Err(format!(
1535                                    "expected 2 elements in scale, got {:?}",
1536                                    s.len()
1537                                ));
1538                            }
1539                            Some([s[0], s[1]])
1540                        } else {
1541                            None
1542                        }
1543                    }
1544                    _ => return Err(format!("scale can't be coerced into a Transform2D",)),
1545                };
1546                let skew = match skew {
1547                    PaxValue::Option(opt) => {
1548                        if let Some(s) = *opt {
1549                            let s = Vec::<Rotation>::try_coerce(s)?;
1550                            if s.len() != 2 {
1551                                return Err(format!(
1552                                    "expected 2 elements in skew, got {:?}",
1553                                    s.len()
1554                                ));
1555                            }
1556                            Some([s[0], s[1]])
1557                        } else {
1558                            None
1559                        }
1560                    }
1561                    _ => return Err(format!("skew can't be coerced into a Transform2D",)),
1562                };
1563                Transform2D {
1564                    previous,
1565                    rotate,
1566                    translate,
1567                    anchor,
1568                    scale,
1569                    skew,
1570                }
1571            }
1572            _ => return Err(format!("{:?} can't be coerced into a Transform2D", value)),
1573        })
1574    }
1575}
1576
1577impl CoercionRules for Transform2 {
1578    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1579        Ok(match value {
1580            PaxValue::Option(mut opt) => {
1581                if let Some(t) = opt.take() {
1582                    Transform2::try_coerce(t)?
1583                } else {
1584                    return Err(format!("None can't be coerced into a Transform2"));
1585                }
1586            }
1587            PaxValue::Object(map) => {
1588                let m = Vec::<f64>::try_coerce(
1589                    map.into_iter()
1590                        .find_map(|(n, v)| (n == "m").then_some(v))
1591                        .unwrap(),
1592                )?;
1593                if m.len() != 6 {
1594                    return Err(format!("expected 6 elements in coeffs, got {:?}", m.len()));
1595                }
1596                Transform2::new([m[0], m[1], m[2], m[3], m[4], m[5]])
1597            }
1598            _ => return Err(format!("{:?} can't be coerced into a Transform2", value)),
1599        })
1600    }
1601}
1602
1603impl CoercionRules for Vector2 {
1604    fn try_coerce(value: PaxValue) -> Result<Self, String> {
1605        Ok(match value {
1606            PaxValue::Option(mut opt) => {
1607                if let Some(t) = opt.take() {
1608                    Vector2::try_coerce(t)?
1609                } else {
1610                    return Err(format!("None can't be coerced into a Vector2"));
1611                }
1612            }
1613            PaxValue::Object(map) => {
1614                let [x, y] = extract_options(["x", "y"], map)
1615                    .map_err(|e| format!("failed to convert to Vector2: {e}"))?;
1616                let x = f64::try_coerce(x)?;
1617                let y = f64::try_coerce(y)?;
1618                Vector2::new(x, y)
1619            }
1620            _ => return Err(format!("{:?} can't be coerced into a Vector2", value)),
1621        })
1622    }
1623}
1624
1625// Pulls named object fields into a fixed-order array for coercion implementations.
1626pub fn extract_options<T, const N: usize>(
1627    keys: [&'static str; N],
1628    vec: Vec<(String, T)>,
1629) -> Result<[T; N], String> {
1630    // First create array of Options
1631    let mut intermediate: [Option<T>; N] = std::array::from_fn(|_| None);
1632
1633    // Paint in the values we find
1634    for (k, v) in vec {
1635        if let Some(pos) = keys.iter().position(|&key| k == key) {
1636            intermediate[pos] = Some(v);
1637        }
1638    }
1639
1640    // Convert to Result, ensuring all values were found
1641    let result: Result<[T; N], String> = intermediate
1642        .into_iter()
1643        .enumerate()
1644        .map(|(i, opt)| opt.ok_or_else(|| format!("missing field: {}", keys[i])))
1645        .collect::<Result<Vec<_>, _>>()?
1646        .try_into()
1647        .map_err(|_| "Internal error converting vec to array".to_string());
1648
1649    result
1650}