Skip to main content

azul_layout/
fmt.rs

1//! C-compatible string formatting via `strfmt`.
2//!
3//! Provides [`FmtValue`], [`FmtArg`], and [`FmtArgVec`] for passing
4//! heterogeneous format arguments across FFI, and [`fmt_string`] as the
5//! main entry point. Used by `fluent.rs` and `icu.rs` for localization.
6
7use std::fmt;
8
9use azul_css::{AzString, StringVec, impl_option, impl_option_inner};
10
11/// A format argument value that can hold any primitive type or string.
12/// Used in [`FmtArg`] to pass typed values into `strfmt`-based formatting.
13#[derive(Debug, Clone, PartialEq, PartialOrd)]
14#[repr(C, u8)]
15pub enum FmtValue {
16    Bool(bool),
17    Uchar(u8),
18    Schar(i8),
19    Ushort(u16),
20    Sshort(i16),
21    Uint(u32),
22    Sint(i32),
23    Ulong(u64),
24    Slong(i64),
25    Isize(isize),
26    Usize(usize),
27    Float(f32),
28    Double(f64),
29    Str(AzString),
30    StrVec(StringVec),
31}
32
33impl strfmt::DisplayStr for FmtValue {
34    fn display_str(&self, f: &mut strfmt::Formatter<'_, '_>) -> strfmt::Result<()> {
35        use strfmt::DisplayStr;
36        match self {
37            Self::Bool(v) => format!("{v}").display_str(f),
38            Self::Uchar(v) => v.display_str(f),
39            Self::Schar(v) => v.display_str(f),
40            Self::Ushort(v) => v.display_str(f),
41            Self::Sshort(v) => v.display_str(f),
42            Self::Uint(v) => v.display_str(f),
43            Self::Sint(v) => v.display_str(f),
44            Self::Ulong(v) => v.display_str(f),
45            Self::Slong(v) => v.display_str(f),
46            Self::Isize(v) => v.display_str(f),
47            Self::Usize(v) => v.display_str(f),
48            Self::Float(v) => v.display_str(f),
49            Self::Double(v) => v.display_str(f),
50            Self::Str(v) => v.as_str().display_str(f),
51            Self::StrVec(sv) => {
52                "[".display_str(f)?;
53                for (i, s) in sv.as_ref().iter().enumerate() {
54                    if i != 0 {
55                        ", ".display_str(f)?;
56                    }
57                    s.as_str().display_str(f)?;
58                }
59                "]".display_str(f)?;
60                Ok(())
61            }
62        }
63    }
64}
65
66impl fmt::Display for FmtValue {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Bool(v) => v.fmt(f),
70            Self::Uchar(v) => v.fmt(f),
71            Self::Schar(v) => v.fmt(f),
72            Self::Ushort(v) => v.fmt(f),
73            Self::Sshort(v) => v.fmt(f),
74            Self::Uint(v) => v.fmt(f),
75            Self::Sint(v) => v.fmt(f),
76            Self::Ulong(v) => v.fmt(f),
77            Self::Slong(v) => v.fmt(f),
78            Self::Isize(v) => v.fmt(f),
79            Self::Usize(v) => v.fmt(f),
80            Self::Float(v) => v.fmt(f),
81            Self::Double(v) => v.fmt(f),
82            Self::Str(v) => v.as_str().fmt(f),
83            Self::StrVec(sv) => {
84                use std::fmt::Debug;
85                let vec: Vec<&str> = sv.as_ref().iter().map(AzString::as_str).collect();
86                vec.fmt(f)
87            }
88        }
89    }
90}
91
92/// A key-value pair mapping a format placeholder name to its value.
93#[derive(Debug, Clone, PartialEq, PartialOrd)]
94#[repr(C)]
95pub struct FmtArg {
96    pub key: AzString,
97    pub value: FmtValue,
98}
99
100azul_css::impl_option!(FmtArg, OptionFmtArg, copy = false, [Debug, Clone, PartialEq, PartialOrd]);
101azul_css::impl_vec!(FmtArg, FmtArgVec, FmtArgVecDestructor, FmtArgVecDestructorType, FmtArgVecSlice, OptionFmtArg);
102azul_css::impl_vec_clone!(FmtArg, FmtArgVec, FmtArgVecDestructor);
103azul_css::impl_vec_debug!(FmtArg, FmtArgVec);
104azul_css::impl_vec_partialeq!(FmtArg, FmtArgVec);
105azul_css::impl_vec_partialord!(FmtArg, FmtArgVec);
106
107/// Formats `format` by substituting placeholders with values from `args`.
108/// Returns the error message as a string on failure (for C FFI ergonomics).
109// FFI-exported formatter: owned AzString/FmtArgVec args are the api.json signature.
110#[allow(clippy::needless_pass_by_value)]
111#[must_use] pub fn fmt_string(format: AzString, args: FmtArgVec) -> String {
112    use strfmt::Format;
113    let format_map = args
114        .iter()
115        .map(|a| (a.key.clone().into_library_owned_string(), a.value.clone()))
116        .collect();
117    match format.as_str().format(&format_map) {
118        Ok(o) => o,
119        Err(e) => format!("{e}"),
120    }
121}
122
123#[cfg(test)]
124mod autotest_generated {
125    use super::*;
126
127    // ------------------------------------------------------------------
128    // Harness
129    // ------------------------------------------------------------------
130
131    fn az(s: &str) -> AzString {
132        AzString::from(s)
133    }
134
135    fn strvec(items: &[&str]) -> StringVec {
136        StringVec::from_vec(items.iter().map(|s| az(s)).collect())
137    }
138
139    /// Formats `format` with the given key/value pairs. Because `fmt_string`
140    /// swallows every `strfmt` failure into the returned `String`, the return
141    /// value here is EITHER the formatted output OR an error message of the
142    /// shape `Invalid(..)` / `KeyError(..)` / `TypeError(..)`.
143    fn run(format: &str, args: &[(&str, FmtValue)]) -> String {
144        let v: Vec<FmtArg> = args
145            .iter()
146            .map(|(k, value)| FmtArg {
147                key: az(k),
148                value: value.clone(),
149            })
150            .collect();
151        fmt_string(az(format), FmtArgVec::from_vec(v))
152    }
153
154    /// True if the output is one of the three `strfmt` error renderings rather
155    /// than a formatted string.
156    fn is_err_str(s: &str) -> bool {
157        s.starts_with("Invalid(") || s.starts_with("KeyError(") || s.starts_with("TypeError(")
158    }
159
160    /// Every scalar (non-`StrVec`) variant at an interesting boundary.
161    fn scalar_variants() -> Vec<FmtValue> {
162        vec![
163            FmtValue::Bool(true),
164            FmtValue::Bool(false),
165            FmtValue::Uchar(0),
166            FmtValue::Uchar(u8::MAX),
167            FmtValue::Schar(i8::MIN),
168            FmtValue::Schar(i8::MAX),
169            FmtValue::Ushort(0),
170            FmtValue::Ushort(u16::MAX),
171            FmtValue::Sshort(i16::MIN),
172            FmtValue::Sshort(i16::MAX),
173            FmtValue::Uint(0),
174            FmtValue::Uint(u32::MAX),
175            FmtValue::Sint(i32::MIN),
176            FmtValue::Sint(i32::MAX),
177            FmtValue::Ulong(0),
178            FmtValue::Ulong(u64::MAX),
179            FmtValue::Slong(i64::MIN),
180            FmtValue::Slong(i64::MAX),
181            FmtValue::Isize(isize::MIN),
182            FmtValue::Isize(isize::MAX),
183            FmtValue::Usize(0),
184            FmtValue::Usize(usize::MAX),
185            FmtValue::Float(0.0),
186            FmtValue::Float(f32::MIN),
187            FmtValue::Float(f32::MAX),
188            FmtValue::Float(f32::NAN),
189            FmtValue::Float(f32::INFINITY),
190            FmtValue::Float(f32::NEG_INFINITY),
191            FmtValue::Float(f32::EPSILON),
192            FmtValue::Double(0.0),
193            FmtValue::Double(f64::MIN),
194            FmtValue::Double(f64::MAX),
195            FmtValue::Double(f64::NAN),
196            FmtValue::Double(f64::INFINITY),
197            FmtValue::Double(f64::NEG_INFINITY),
198            FmtValue::Double(f64::MIN_POSITIVE),
199            FmtValue::Str(AzString::default()),
200            FmtValue::Str(az("plain")),
201        ]
202    }
203
204    // ------------------------------------------------------------------
205    // fmt_string: happy path + substitution invariants
206    // ------------------------------------------------------------------
207
208    #[test]
209    fn substitutes_a_single_placeholder() {
210        assert_eq!(
211            run("Hello {name}!", &[("name", FmtValue::Str(az("World")))]),
212            "Hello World!"
213        );
214    }
215
216    #[test]
217    fn empty_format_and_empty_args_yield_empty_output() {
218        assert_eq!(fmt_string(AzString::default(), FmtArgVec::new()), "");
219        assert_eq!(fmt_string(az("plain text"), FmtArgVec::new()), "plain text");
220    }
221
222    #[test]
223    fn args_without_a_matching_placeholder_are_ignored() {
224        assert_eq!(
225            run(
226                "{a}",
227                &[("a", FmtValue::Sint(1)), ("unused", FmtValue::Sint(2))]
228            ),
229            "1"
230        );
231    }
232
233    #[test]
234    fn the_same_key_can_be_substituted_repeatedly() {
235        assert_eq!(run("{k}-{k}-{k}", &[("k", FmtValue::Uint(7))]), "7-7-7");
236    }
237
238    #[test]
239    fn duplicate_keys_resolve_to_the_last_arg() {
240        // `fmt_string` collects into a `HashMap`, so a later duplicate overwrites
241        // the earlier one instead of erroring or panicking.
242        assert_eq!(
243            run("{k}", &[("k", FmtValue::Sint(1)), ("k", FmtValue::Sint(2))]),
244            "2"
245        );
246    }
247
248    #[test]
249    fn substituted_values_are_not_re_scanned_for_placeholders() {
250        // A value that itself looks like a format string must be emitted
251        // literally — no recursive expansion, no second-order injection.
252        assert_eq!(
253            run(
254                "{a}",
255                &[
256                    ("a", FmtValue::Str(az("{b}"))),
257                    ("b", FmtValue::Str(az("PWNED"))),
258                ]
259            ),
260            "{b}"
261        );
262    }
263
264    // ------------------------------------------------------------------
265    // fmt_string: malformed format strings must return an error, never panic
266    // ------------------------------------------------------------------
267
268    #[test]
269    fn missing_key_returns_a_key_error_string() {
270        let out = run("{missing}", &[]);
271        assert!(out.contains("Invalid key: missing"), "{out}");
272        assert!(is_err_str(&out), "{out}");
273    }
274
275    #[test]
276    fn unclosed_brace_returns_an_error_string() {
277        for bad in ["{", "{name", "abc {name"] {
278            let out = run(bad, &[("name", FmtValue::Sint(1))]);
279            assert!(out.contains("Expected '}'"), "{bad:?} -> {out}");
280        }
281    }
282
283    #[test]
284    fn lone_closing_brace_returns_an_error_string() {
285        for bad in ["}", "a}b", "trailing}"] {
286            let out = run(bad, &[]);
287            assert!(out.contains("Single '}'"), "{bad:?} -> {out}");
288        }
289    }
290
291    #[test]
292    fn nested_opening_brace_returns_an_error_string() {
293        let out = run("{a{b}", &[("a", FmtValue::Sint(1))]);
294        assert!(out.contains("extra {"), "{out}");
295    }
296
297    #[test]
298    fn empty_placeholder_is_rejected_even_when_an_empty_key_exists() {
299        // `{}` has no identifier; the empty-string key in `args` is unreachable.
300        let out = run("{}", &[("", FmtValue::Sint(1))]);
301        assert!(out.contains("must specify identifier"), "{out}");
302    }
303
304    #[test]
305    fn a_colon_in_a_key_makes_that_key_unaddressable() {
306        // Everything after the first `:` is parsed as a format spec, so the key
307        // "a:b" can never be looked up — this must be a KeyError, not a panic.
308        let out = run("{a:b}", &[("a:b", FmtValue::Sint(1))]);
309        assert!(out.contains("Invalid key: a"), "{out}");
310    }
311
312    #[test]
313    fn escaped_braces_are_emitted_literally() {
314        assert_eq!(run("{{literal}}", &[]), "{literal}");
315        assert_eq!(run("{{{{", &[]), "{{");
316        assert_eq!(
317            run("{{{k}}}", &[("k", FmtValue::Str(az("v")))]),
318            "{v}",
319            "escaped braces around a real placeholder"
320        );
321    }
322
323    // ------------------------------------------------------------------
324    // fmt_string: numeric limits, saturation and non-finite floats
325    // ------------------------------------------------------------------
326
327    #[test]
328    fn every_scalar_variant_round_trips_through_its_display_impl() {
329        // Invariant: for scalars, the strfmt path (`DisplayStr`) and the
330        // `fmt::Display` path must agree. This also pins NaN/inf/MIN/MAX.
331        for v in scalar_variants() {
332            let out = run("{v}", &[("v", v.clone())]);
333            assert_eq!(out, v.to_string(), "mismatch for {v:?}");
334            assert!(!is_err_str(&out), "unexpected error for {v:?}: {out}");
335        }
336    }
337
338    #[test]
339    fn integer_limits_match_the_native_rendering() {
340        assert_eq!(
341            run("{v}", &[("v", FmtValue::Slong(i64::MIN))]),
342            i64::MIN.to_string()
343        );
344        assert_eq!(
345            run("{v}", &[("v", FmtValue::Ulong(u64::MAX))]),
346            u64::MAX.to_string()
347        );
348        assert_eq!(
349            run("{v}", &[("v", FmtValue::Usize(usize::MAX))]),
350            usize::MAX.to_string()
351        );
352        assert_eq!(
353            run("{v}", &[("v", FmtValue::Isize(isize::MIN))]),
354            isize::MIN.to_string()
355        );
356        assert_eq!(run("{v}", &[("v", FmtValue::Schar(i8::MIN))]), "-128");
357    }
358
359    #[test]
360    fn non_finite_floats_render_without_panicking() {
361        assert_eq!(run("{v}", &[("v", FmtValue::Float(f32::NAN))]), "NaN");
362        assert_eq!(run("{v}", &[("v", FmtValue::Float(f32::INFINITY))]), "inf");
363        assert_eq!(
364            run("{v}", &[("v", FmtValue::Double(f64::NEG_INFINITY))]),
365            "-inf"
366        );
367        // Precision must not turn NaN/inf into a panic or a bogus number.
368        assert_eq!(run("{v:.2}", &[("v", FmtValue::Double(f64::NAN))]), "NaN");
369        assert_eq!(
370            run("{v:.5}", &[("v", FmtValue::Double(f64::INFINITY))]),
371            "inf"
372        );
373    }
374
375    #[test]
376    fn float_extremes_match_the_native_rendering() {
377        assert_eq!(
378            run("{v}", &[("v", FmtValue::Double(f64::MAX))]),
379            f64::MAX.to_string()
380        );
381        assert_eq!(
382            run("{v}", &[("v", FmtValue::Double(f64::MIN_POSITIVE))]),
383            f64::MIN_POSITIVE.to_string()
384        );
385        assert_eq!(
386            run("{v}", &[("v", FmtValue::Float(f32::MIN))]),
387            f32::MIN.to_string()
388        );
389    }
390
391    #[test]
392    fn radix_format_codes_match_the_native_rendering() {
393        assert_eq!(run("{v:x}", &[("v", FmtValue::Uint(255))]), "ff");
394        assert_eq!(run("{v:X}", &[("v", FmtValue::Uint(255))]), "FF");
395        assert_eq!(run("{v:#x}", &[("v", FmtValue::Uint(255))]), "0xff");
396        assert_eq!(run("{v:b}", &[("v", FmtValue::Uchar(5))]), "101");
397        assert_eq!(run("{v:o}", &[("v", FmtValue::Uint(8))]), "10");
398        // Two's-complement rendering of the most negative value must not panic.
399        assert_eq!(
400            run("{v:b}", &[("v", FmtValue::Sint(i32::MIN))]),
401            format!("{:b}", i32::MIN)
402        );
403        assert_eq!(
404            run("{v:x}", &[("v", FmtValue::Usize(usize::MAX))]),
405            format!("{:x}", usize::MAX)
406        );
407    }
408
409    #[test]
410    fn explicit_sign_is_honoured_for_numbers() {
411        assert_eq!(run("{v:+}", &[("v", FmtValue::Sint(5))]), "+5");
412        assert_eq!(run("{v:+}", &[("v", FmtValue::Sint(-5))]), "-5");
413        assert_eq!(run("{v:+}", &[("v", FmtValue::Sint(0))]), "+0");
414    }
415
416    #[test]
417    fn exponent_and_precision_codes_match_the_native_rendering() {
418        assert_eq!(
419            run("{v:e}", &[("v", FmtValue::Double(1234.0))]),
420            format!("{:e}", 1234.0_f64)
421        );
422        assert_eq!(
423            run("{v:.3}", &[("v", FmtValue::Double(1.234_56))]),
424            format!("{:.3}", 1.234_56_f64)
425        );
426    }
427
428    // ------------------------------------------------------------------
429    // fmt_string: format-spec type errors are reported, not panicked
430    // ------------------------------------------------------------------
431
432    #[test]
433    fn precision_on_an_integer_is_a_type_error() {
434        let out = run("{v:.2}", &[("v", FmtValue::Sint(5))]);
435        assert!(out.contains("precision not allowed for integers"), "{out}");
436    }
437
438    #[test]
439    fn a_string_valued_arg_rejects_numeric_format_codes() {
440        for spec in ["{v:x}", "{v:b}", "{v:e}", "{v:#}", "{v:+}"] {
441            let out = run(spec, &[("v", FmtValue::Str(az("hi")))]);
442            assert!(is_err_str(&out), "{spec} unexpectedly succeeded: {out}");
443        }
444    }
445
446    #[test]
447    fn a_float_valued_arg_rejects_integer_format_codes() {
448        let out = run("{v:x}", &[("v", FmtValue::Double(1.0))]);
449        assert!(out.contains("Unknown format code"), "{out}");
450        let out = run("{v:#}", &[("v", FmtValue::Double(1.0))]);
451        assert!(out.contains("Alternate form"), "{out}");
452    }
453
454    #[test]
455    fn an_unknown_type_specifier_is_reported() {
456        let out = run("{v:z}", &[("v", FmtValue::Sint(1))]);
457        assert!(out.contains("Invalid type specifier"), "{out}");
458    }
459
460    #[test]
461    fn unsupported_specs_degrade_to_an_error_string() {
462        // These are documented `strfmt` gaps; assert they surface as errors
463        // instead of producing wrong output or panicking.
464        let zero_pad = run("{v:08}", &[("v", FmtValue::Sint(42))]);
465        assert!(is_err_str(&zero_pad), "{zero_pad}");
466        let thousands = run("{v:,}", &[("v", FmtValue::Sint(1000))]);
467        assert!(thousands.contains("not yet supported"), "{thousands}");
468    }
469
470    #[test]
471    fn an_out_of_range_width_or_precision_is_rejected_not_overflowed() {
472        // 20 nines does not fit in the i64 the spec parser uses.
473        let w = run("{v:>99999999999999999999}", &[("v", FmtValue::Sint(1))]);
474        assert!(w.contains("overflow error when parsing width"), "{w}");
475        let p = run(
476            "{v:.99999999999999999999}",
477            &[("v", FmtValue::Str(az("abc")))],
478        );
479        assert!(p.contains("overflow error when parsing precision"), "{p}");
480        // A bare `.` with no digits is malformed.
481        let d = run("{v:.}", &[("v", FmtValue::Str(az("abc")))]);
482        assert!(d.contains("missing precision"), "{d}");
483    }
484
485    // ------------------------------------------------------------------
486    // fmt_string: unicode
487    // ------------------------------------------------------------------
488
489    #[test]
490    fn keys_values_and_literals_may_be_non_ascii() {
491        assert_eq!(
492            run("こんにちは、{名前}!🦀", &[("名前", FmtValue::Str(az("世界")))]),
493            "こんにちは、世界!🦀"
494        );
495    }
496
497    #[test]
498    fn width_and_precision_count_chars_not_bytes() {
499        // A byte-oriented implementation would slice mid-codepoint and panic.
500        assert_eq!(
501            run("{v:.2}", &[("v", FmtValue::Str(az("日本語")))]),
502            "日本",
503            "precision truncates on a char boundary"
504        );
505        assert_eq!(
506            run("{v:>5}", &[("v", FmtValue::Str(az("日本語")))]),
507            "  日本語",
508            "width pads by chars, not bytes"
509        );
510    }
511
512    #[test]
513    fn a_multibyte_fill_char_pads_correctly() {
514        assert_eq!(run("{v:🦀>4}", &[("v", FmtValue::Str(az("ab")))]), "🦀🦀ab");
515    }
516
517    #[test]
518    fn control_and_nul_characters_pass_through_untouched() {
519        let weird = "a\0b\tc\nd\u{7f}e";
520        assert_eq!(run(weird, &[]), weird);
521    }
522
523    // ------------------------------------------------------------------
524    // fmt_string: string alignment / truncation
525    // ------------------------------------------------------------------
526
527    #[test]
528    fn string_alignment_and_truncation_behave_as_specified() {
529        assert_eq!(run("{v:>8}", &[("v", FmtValue::Str(az("ab")))]), "      ab");
530        assert_eq!(run("{v:<4}|", &[("v", FmtValue::Str(az("ab")))]), "ab  |");
531        assert_eq!(run("{v:^6}", &[("v", FmtValue::Str(az("ab")))]), "  ab  ");
532        assert_eq!(run("{v:.3}", &[("v", FmtValue::Str(az("abcdefg")))]), "abc");
533        assert_eq!(
534            run("{v:.0}", &[("v", FmtValue::Str(az("abc")))]),
535            "",
536            "zero precision erases the value"
537        );
538        assert_eq!(
539            run("{v:.9}", &[("v", FmtValue::Str(az("abc")))]),
540            "abc",
541            "precision longer than the value does not overrun"
542        );
543        assert_eq!(
544            run("{v:>2}", &[("v", FmtValue::Str(az("abcdef")))]),
545            "abcdef",
546            "width smaller than the value never truncates"
547        );
548    }
549
550    // ------------------------------------------------------------------
551    // fmt_string: StrVec
552    // ------------------------------------------------------------------
553
554    #[test]
555    fn strvec_renders_as_a_bracketed_comma_list() {
556        assert_eq!(run("{v}", &[("v", FmtValue::StrVec(strvec(&[])))]), "[]");
557        assert_eq!(run("{v}", &[("v", FmtValue::StrVec(strvec(&["a"])))]), "[a]");
558        assert_eq!(
559            run("{v}", &[("v", FmtValue::StrVec(strvec(&["a", "b", "c"])))]),
560            "[a, b, c]"
561        );
562        assert_eq!(
563            run("{v}", &[("v", FmtValue::StrVec(strvec(&["", ""])))]),
564            "[, ]",
565            "empty members still get a separator"
566        );
567    }
568
569    #[test]
570    fn strvec_applies_the_width_spec_to_every_fragment() {
571        // KNOWN QUIRK, pinned deliberately: `DisplayStr for StrVec` reuses the
572        // same `Formatter` for "[", each item and "]", so a width spec pads each
573        // fragment rather than the list as a whole. 3 fragments * width 10 = 30.
574        let out = run("{v:>10}", &[("v", FmtValue::StrVec(strvec(&["a"])))]);
575        assert_eq!(out, "         [         a         ]");
576        assert_eq!(out.chars().count(), 30);
577    }
578
579    // ------------------------------------------------------------------
580    // fmt_string: size / stress
581    // ------------------------------------------------------------------
582
583    #[test]
584    fn many_placeholders_do_not_blow_up() {
585        let format: String = "{k}".repeat(500);
586        let out = run(&format, &[("k", FmtValue::Uchar(9))]);
587        assert_eq!(out, "9".repeat(500));
588    }
589
590    #[test]
591    fn a_very_long_value_is_substituted_whole() {
592        let big = "x".repeat(50_000);
593        let out = run("{v}", &[("v", FmtValue::Str(AzString::from(big.clone())))]);
594        assert_eq!(out.len(), 50_000);
595        assert_eq!(out, big);
596    }
597
598    #[test]
599    fn a_long_literal_format_string_is_returned_verbatim() {
600        let big = "y".repeat(100_000);
601        assert_eq!(fmt_string(AzString::from(big.clone()), FmtArgVec::new()), big);
602    }
603
604    // ------------------------------------------------------------------
605    // FmtValue::fmt (Display) — the serializer under test
606    // ------------------------------------------------------------------
607
608    #[test]
609    fn display_never_panics_on_any_variant_or_edge_value() {
610        for v in scalar_variants() {
611            let _ = v.to_string();
612        }
613        let _ = FmtValue::StrVec(strvec(&[])).to_string();
614        let _ = FmtValue::StrVec(strvec(&["a", "", "🦀"])).to_string();
615    }
616
617    #[test]
618    fn display_renders_each_variant_as_its_inner_value() {
619        assert_eq!(FmtValue::Bool(true).to_string(), "true");
620        assert_eq!(FmtValue::Bool(false).to_string(), "false");
621        assert_eq!(FmtValue::Uchar(u8::MAX).to_string(), "255");
622        assert_eq!(FmtValue::Schar(i8::MIN).to_string(), "-128");
623        assert_eq!(FmtValue::Sshort(i16::MIN).to_string(), "-32768");
624        assert_eq!(FmtValue::Ulong(u64::MAX).to_string(), u64::MAX.to_string());
625        assert_eq!(FmtValue::Slong(i64::MIN).to_string(), i64::MIN.to_string());
626        assert_eq!(FmtValue::Usize(usize::MAX).to_string(), usize::MAX.to_string());
627        assert_eq!(FmtValue::Isize(isize::MIN).to_string(), isize::MIN.to_string());
628        assert_eq!(FmtValue::Float(f32::NAN).to_string(), "NaN");
629        assert_eq!(FmtValue::Double(f64::INFINITY).to_string(), "inf");
630        assert_eq!(FmtValue::Double(f64::NEG_INFINITY).to_string(), "-inf");
631    }
632
633    #[test]
634    fn display_of_a_string_is_unquoted_but_a_strvec_is_debug_quoted() {
635        // The `Str` arm goes through `str::fmt`, the `StrVec` arm through
636        // `Vec<&str>::fmt` (Debug) — so quoting differs. Pin both.
637        assert_eq!(FmtValue::Str(az("a\"b")).to_string(), "a\"b");
638        assert_eq!(FmtValue::Str(AzString::default()).to_string(), "");
639        assert_eq!(FmtValue::StrVec(strvec(&[])).to_string(), "[]");
640        assert_eq!(
641            FmtValue::StrVec(strvec(&["a", "b"])).to_string(),
642            "[\"a\", \"b\"]"
643        );
644    }
645
646    #[test]
647    fn display_of_a_strvec_diverges_from_the_strfmt_rendering() {
648        // Documented divergence: `Display` is Debug-quoted, `DisplayStr` is not.
649        let v = FmtValue::StrVec(strvec(&["a", "b"]));
650        assert_ne!(run("{v}", &[("v", v.clone())]), v.to_string());
651        assert_eq!(run("{v}", &[("v", v)]), "[a, b]");
652    }
653
654    #[test]
655    fn display_forwards_width_precision_and_fill_flags() {
656        assert_eq!(format!("{:>6}", FmtValue::Uint(42)), "    42");
657        assert_eq!(format!("{:<6}|", FmtValue::Uint(42)), "42    |");
658        assert_eq!(format!("{:0>6}", FmtValue::Uint(42)), "000042");
659        assert_eq!(format!("{:>6}", FmtValue::Str(az("ab"))), "    ab");
660        assert_eq!(
661            format!("{:.2}", FmtValue::Double(2.0 / 3.0)),
662            format!("{:.2}", 2.0_f64 / 3.0)
663        );
664    }
665
666    #[test]
667    fn debug_is_variant_tagged() {
668        assert_eq!(format!("{:?}", FmtValue::Uint(1)), "Uint(1)");
669        assert_eq!(format!("{:?}", FmtValue::Bool(false)), "Bool(false)");
670    }
671
672    // ------------------------------------------------------------------
673    // Derived predicates: PartialEq / PartialOrd / Clone
674    // ------------------------------------------------------------------
675
676    #[test]
677    fn nan_breaks_reflexive_equality_and_has_no_ordering() {
678        let nan = FmtValue::Double(f64::NAN);
679        assert_ne!(nan, nan.clone(), "NaN must not compare equal to itself");
680        assert_eq!(nan.partial_cmp(&nan.clone()), None);
681        assert_eq!(
682            FmtValue::Float(f32::NAN).partial_cmp(&FmtValue::Float(1.0)),
683            None
684        );
685    }
686
687    #[test]
688    fn equality_and_ordering_discriminate_between_variants() {
689        assert_eq!(FmtValue::Uint(1), FmtValue::Uint(1));
690        assert_ne!(
691            FmtValue::Uint(1),
692            FmtValue::Sint(1),
693            "same numeric value, different variant"
694        );
695        // Derived PartialOrd compares the discriminant first.
696        assert!(FmtValue::Bool(true) < FmtValue::Uchar(0));
697        assert!(FmtValue::Uchar(1) > FmtValue::Uchar(0));
698        assert!(FmtValue::Double(f64::NEG_INFINITY) < FmtValue::Double(f64::MIN));
699    }
700
701    #[test]
702    fn cloning_an_arg_vec_preserves_equality_and_survives_a_double_drop() {
703        let args = FmtArgVec::from_vec(vec![
704            FmtArg {
705                key: az("a"),
706                value: FmtValue::Str(az("one")),
707            },
708            FmtArg {
709                key: az("b"),
710                value: FmtValue::StrVec(strvec(&["x", "y"])),
711            },
712        ]);
713        let copy = args.clone();
714        assert_eq!(args, copy);
715        assert_eq!(
716            fmt_string(az("{a}{b}"), args),
717            fmt_string(az("{a}{b}"), copy.clone()),
718            "a clone must format identically to its source"
719        );
720        drop(copy); // both the original and the clone are dropped: no double free
721    }
722}