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

            
7
use std::fmt;
8

            
9
use 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)]
15
pub 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

            
33
impl strfmt::DisplayStr for FmtValue {
34
604
    fn display_str(&self, f: &mut strfmt::Formatter<'_, '_>) -> strfmt::Result<()> {
35
        use strfmt::DisplayStr;
36
604
        match self {
37
2
            Self::Bool(v) => format!("{v}").display_str(f),
38
503
            Self::Uchar(v) => v.display_str(f),
39
3
            Self::Schar(v) => v.display_str(f),
40
2
            Self::Ushort(v) => v.display_str(f),
41
2
            Self::Sshort(v) => v.display_str(f),
42
9
            Self::Uint(v) => v.display_str(f),
43
11
            Self::Sint(v) => v.display_str(f),
44
3
            Self::Ulong(v) => v.display_str(f),
45
3
            Self::Slong(v) => v.display_str(f),
46
3
            Self::Isize(v) => v.display_str(f),
47
4
            Self::Usize(v) => v.display_str(f),
48
10
            Self::Float(v) => v.display_str(f),
49
16
            Self::Double(v) => v.display_str(f),
50
24
            Self::Str(v) => v.as_str().display_str(f),
51
9
            Self::StrVec(sv) => {
52
9
                "[".display_str(f)?;
53
15
                for (i, s) in sv.as_ref().iter().enumerate() {
54
15
                    if i != 0 {
55
7
                        ", ".display_str(f)?;
56
8
                    }
57
15
                    s.as_str().display_str(f)?;
58
                }
59
9
                "]".display_str(f)?;
60
9
                Ok(())
61
            }
62
        }
63
604
    }
64
}
65

            
66
impl fmt::Display for FmtValue {
67
100
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68
100
        match self {
69
6
            Self::Bool(v) => v.fmt(f),
70
5
            Self::Uchar(v) => v.fmt(f),
71
5
            Self::Schar(v) => v.fmt(f),
72
4
            Self::Ushort(v) => v.fmt(f),
73
5
            Self::Sshort(v) => v.fmt(f),
74
7
            Self::Uint(v) => v.fmt(f),
75
4
            Self::Sint(v) => v.fmt(f),
76
5
            Self::Ulong(v) => v.fmt(f),
77
5
            Self::Slong(v) => v.fmt(f),
78
5
            Self::Isize(v) => v.fmt(f),
79
5
            Self::Usize(v) => v.fmt(f),
80
15
            Self::Float(v) => v.fmt(f),
81
17
            Self::Double(v) => v.fmt(f),
82
7
            Self::Str(v) => v.as_str().fmt(f),
83
5
            Self::StrVec(sv) => {
84
                use std::fmt::Debug;
85
5
                let vec: Vec<&str> = sv.as_ref().iter().map(AzString::as_str).collect();
86
5
                vec.fmt(f)
87
            }
88
        }
89
100
    }
90
}
91

            
92
/// A key-value pair mapping a format placeholder name to its value.
93
#[derive(Debug, Clone, PartialEq, PartialOrd)]
94
#[repr(C)]
95
pub struct FmtArg {
96
    pub key: AzString,
97
    pub value: FmtValue,
98
}
99

            
100
azul_css::impl_option!(FmtArg, OptionFmtArg, copy = false, [Debug, Clone, PartialEq, PartialOrd]);
101
azul_css::impl_vec!(FmtArg, FmtArgVec, FmtArgVecDestructor, FmtArgVecDestructorType, FmtArgVecSlice, OptionFmtArg);
102
azul_css::impl_vec_clone!(FmtArg, FmtArgVec, FmtArgVecDestructor);
103
azul_css::impl_vec_debug!(FmtArg, FmtArgVec);
104
azul_css::impl_vec_partialeq!(FmtArg, FmtArgVec);
105
azul_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
121
#[must_use] pub fn fmt_string(format: AzString, args: FmtArgVec) -> String {
112
    use strfmt::Format;
113
121
    let format_map = args
114
121
        .iter()
115
121
        .map(|a| (a.key.clone().into_library_owned_string(), a.value.clone()))
116
121
        .collect();
117
121
    match format.as_str().format(&format_map) {
118
97
        Ok(o) => o,
119
24
        Err(e) => format!("{e}"),
120
    }
121
121
}
122

            
123
#[cfg(test)]
124
mod 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
}