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