meerkat-machine-schema 0.8.13

Formal machine schemas and transition definitions for Meerkat
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
//! Slug-validated identifier newtypes for the machine schema layer.
//!
//! Every identity in the machine kernel vocabulary — machine names, phase names,
//! variant names, field names, transitions, routes, protocols, actors, enum type
//! names and variants, and composition names — is represented here as a distinct
//! newtype wrapping a validated ASCII slug. This closes the first dogma gap in
//! wave (b): kernel identities stop being bare `String` and become typed, so that
//! the compiler rejects field/phase/variant cross-contamination at the boundary
//! instead of the runtime.
//!
//! Validation rules (identical for every identity type):
//! - non-empty
//! - first character: ASCII alphabetic or `_`
//! - subsequent characters: ASCII alphanumeric, `_`, or `-`
//!
//! Anything else — spaces, dots, slashes, control characters, non-ASCII — is
//! rejected at construction time with a structured [`IdentityError`].

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use thiserror::Error;

/// Why an identity string failed validation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdentityErrorKind {
    /// The input string was empty.
    Empty,
    /// The first character was not ASCII alpha or underscore.
    InvalidStartChar(char),
    /// A later character was not ASCII alphanumeric, underscore, or hyphen.
    InvalidChar { ch: char, position: usize },
}

/// Structured error returned by every identity `parse` constructor.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub struct IdentityError {
    pub kind: IdentityErrorKind,
    pub raw: String,
}

impl fmt::Display for IdentityError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.kind {
            IdentityErrorKind::Empty => {
                write!(f, "identity must not be empty")
            }
            IdentityErrorKind::InvalidStartChar(ch) => {
                write!(
                    f,
                    "identity {:?} must start with ASCII letter or underscore, found {:?}",
                    self.raw, ch
                )
            }
            IdentityErrorKind::InvalidChar { ch, position } => {
                write!(
                    f,
                    "identity {:?} contains invalid character {:?} at position {}",
                    self.raw, ch, position
                )
            }
        }
    }
}

fn validate_slug(raw: &str) -> Result<(), IdentityErrorKind> {
    let mut chars = raw.chars().enumerate();
    let (_, first) = chars.next().ok_or(IdentityErrorKind::Empty)?;
    if !(first.is_ascii_alphabetic() || first == '_') {
        return Err(IdentityErrorKind::InvalidStartChar(first));
    }
    for (pos, ch) in chars {
        if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
            return Err(IdentityErrorKind::InvalidChar { ch, position: pos });
        }
    }
    Ok(())
}

macro_rules! define_identity {
    ($(#[$attr:meta])* $name:ident) => {
        $(#[$attr])*
        #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
        pub struct $name(String);

        impl $name {
            /// Parse a slug, returning a structured error on violation.
            pub fn parse(value: impl Into<String>) -> Result<Self, IdentityError> {
                let raw = value.into();
                match validate_slug(&raw) {
                    Ok(()) => Ok(Self(raw)),
                    Err(kind) => Err(IdentityError { kind, raw }),
                }
            }

            /// Borrow the underlying validated slug.
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str(&self.0)
            }
        }

        impl Serialize for $name {
            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                serializer.serialize_str(&self.0)
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
                let raw = String::deserialize(deserializer)?;
                Self::parse(raw).map_err(serde::de::Error::custom)
            }
        }
    };
}

define_identity!(
    /// Name of a declared machine (e.g. `"MobMachine"`).
    MachineId
);
define_identity!(
    /// Instance id of a machine within a composition (e.g. `"mob"`).
    MachineInstanceId
);
define_identity!(
    /// Phase name within a machine.
    PhaseId
);
define_identity!(
    /// Input-variant name.
    InputVariantId
);

impl InputVariantId {
    /// Construct from a crate-owned catalog literal.
    ///
    /// This is intentionally crate-private: schema catalog metadata can use
    /// typed identities without fallible runtime parsing, while external
    /// callers still go through [`Self::parse`].
    pub(crate) fn from_trusted_catalog_literal(value: &'static str) -> Self {
        Self(value.to_owned())
    }
}
define_identity!(
    /// Signal-variant name.
    SignalVariantId
);

impl SignalVariantId {
    /// Construct from a crate-owned catalog literal.
    pub(crate) fn from_trusted_catalog_literal(value: &'static str) -> Self {
        Self(value.to_owned())
    }
}
define_identity!(
    /// Effect-variant name.
    EffectVariantId
);

impl EffectVariantId {
    /// Construct from a crate-owned catalog literal.
    pub(crate) fn from_trusted_catalog_literal(value: &'static str) -> Self {
        Self(value.to_owned())
    }
}
define_identity!(
    /// Field name within a kernel state, input, signal, or effect.
    FieldId
);
define_identity!(
    /// Transition name.
    TransitionId
);

impl TransitionId {
    /// Construct from a crate-owned catalog literal.
    pub(crate) fn from_trusted_catalog_literal(value: &'static str) -> Self {
        Self(value.to_owned())
    }

    /// Construct from a crate-owned catalog string assembled from trusted
    /// literals.
    pub(crate) fn from_trusted_catalog_string(value: String) -> Self {
        Self(value)
    }
}
define_identity!(
    /// Route name within a composition.
    RouteId
);
define_identity!(
    /// Protocol name (e.g. for effect handoff).
    ProtocolId
);
define_identity!(
    /// Actor name within a composition.
    ActorId
);
define_identity!(
    /// Named type alias declared in the DSL.
    NamedTypeId
);
define_identity!(
    /// Enum type declared in the DSL.
    EnumTypeId
);
define_identity!(
    /// Variant name inside an enum type.
    EnumVariantId
);
define_identity!(
    /// Composition name.
    CompositionId
);
define_identity!(
    /// Driver name within a composition.
    CompositionDriverId
);
define_identity!(
    /// Transaction plan name within a composition.
    TransactionPlanId
);
define_identity!(
    /// Transaction trigger name within a composition.
    TransactionTriggerId
);
define_identity!(
    /// Witness name within a composition.
    CompositionWitnessId
);
define_identity!(
    /// Entry input name within a composition.
    EntryInputId
);

/// Store primitive referenced by a composition transaction plan.
///
/// Unlike kernel slugs, store primitives name existing Rust-side atomic
/// operations and may use qualified path syntax such as
/// `ScheduleStore::claim_due_occurrences`. The type still owns validation at
/// the schema boundary instead of letting transaction plans carry raw strings.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StorePrimitiveId(String);

impl StorePrimitiveId {
    pub fn parse(value: impl Into<String>) -> Result<Self, IdentityError> {
        let raw = value.into();
        if raw.is_empty() {
            return Err(IdentityError {
                kind: IdentityErrorKind::Empty,
                raw,
            });
        }
        for (position, ch) in raw.chars().enumerate() {
            if ch.is_control() || ch.is_whitespace() {
                return Err(IdentityError {
                    kind: IdentityErrorKind::InvalidChar { ch, position },
                    raw,
                });
            }
        }
        Ok(Self(raw))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for StorePrimitiveId {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for StorePrimitiveId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl Serialize for StorePrimitiveId {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.0)
    }
}

impl<'de> Deserialize<'de> for StorePrimitiveId {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let raw = String::deserialize(deserializer)?;
        Self::parse(raw).map_err(serde::de::Error::custom)
    }
}

/// Payload field shapes for structural variants in a [`RustTypeAtom::TypePathEnum`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TypePathEnumPayloadAtom {
    /// Finite set of plain strings.
    StringSet,
    /// Finite set of values drawn from a named value domain.
    NamedSet(NamedTypeId),
    /// Single plain string value.
    String,
    /// Optional plain string value.
    OptionalString,
    /// Single value drawn from a named value domain.
    Named(NamedTypeId),
}

/// One field in a structural enum-variant sample carried by the typed owner.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TypePathEnumPayloadField {
    pub name: FieldId,
    pub atom: TypePathEnumPayloadAtom,
}

impl TypePathEnumPayloadField {
    /// Construct a payload field whose value is a finite set of strings.
    pub fn string_set(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural enum field slug"),
            atom: TypePathEnumPayloadAtom::StringSet,
        }
    }

    /// Construct a payload field whose value is a finite set of values from a
    /// named value domain.
    pub fn named_set(name: &str, type_name: &str) -> Self {
        #[allow(clippy::expect_used)]
        let type_name = NamedTypeId::parse(type_name).expect("valid nested named-type slug");
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural enum field slug"),
            atom: TypePathEnumPayloadAtom::NamedSet(type_name),
        }
    }

    /// Construct a payload field whose value is a single string.
    pub fn string(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural enum field slug"),
            atom: TypePathEnumPayloadAtom::String,
        }
    }

    /// Construct a payload field whose value is an optional string.
    pub fn optional_string(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural enum field slug"),
            atom: TypePathEnumPayloadAtom::OptionalString,
        }
    }

    /// Construct a payload field whose value is a single value from a named
    /// value domain.
    pub fn named(name: &str, type_name: &str) -> Self {
        #[allow(clippy::expect_used)]
        let type_name = NamedTypeId::parse(type_name).expect("valid nested named-type slug");
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural enum field slug"),
            atom: TypePathEnumPayloadAtom::Named(type_name),
        }
    }
}

/// Structural enum variants whose sample values are represented as tagged maps.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TypePathEnumStructuralVariant {
    pub variant: EnumVariantId,
    pub fields: Vec<TypePathEnumPayloadField>,
}

impl TypePathEnumStructuralVariant {
    /// Construct a one-field structural variant with a string-set payload.
    pub fn string_set(variant: &str, field: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            variant: EnumVariantId::parse(variant).expect("valid enum variant slug"),
            fields: vec![TypePathEnumPayloadField::string_set(field)],
        }
    }

    /// Construct a one-field structural variant whose payload is a finite set
    /// of values from a named value domain.
    pub fn named_set(variant: &str, field: &str, type_name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            variant: EnumVariantId::parse(variant).expect("valid enum variant slug"),
            fields: vec![TypePathEnumPayloadField::named_set(field, type_name)],
        }
    }

    /// Construct a structural variant from explicit payload fields.
    pub fn with_fields(variant: &str, fields: Vec<TypePathEnumPayloadField>) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            variant: EnumVariantId::parse(variant).expect("valid enum variant slug"),
            fields,
        }
    }
}

/// Field value shapes for structural type-path records.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TypePathStructFieldAtom {
    String,
    Named(NamedTypeId),
    OptionalNamed(NamedTypeId),
}

/// One field in a structural record carried by the typed owner.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TypePathStructField {
    pub name: FieldId,
    pub atom: TypePathStructFieldAtom,
}

impl TypePathStructField {
    /// Construct a structural field whose value is a string.
    pub fn string(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural record field slug"),
            atom: TypePathStructFieldAtom::String,
        }
    }

    /// Construct a structural field whose value is another named type.
    pub fn named(name: &str, type_name: &str) -> Self {
        #[allow(clippy::expect_used)]
        let type_name = NamedTypeId::parse(type_name).expect("valid nested named-type slug");
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural record field slug"),
            atom: TypePathStructFieldAtom::Named(type_name),
        }
    }

    /// Construct a structural field whose value is an optional named type.
    pub fn optional_named(name: &str, type_name: &str) -> Self {
        #[allow(clippy::expect_used)]
        let type_name = NamedTypeId::parse(type_name).expect("valid nested named-type slug");
        Self {
            #[allow(clippy::expect_used)]
            name: FieldId::parse(name).expect("valid structural record field slug"),
            atom: TypePathStructFieldAtom::OptionalNamed(type_name),
        }
    }
}

/// Atomic Rust-level representation used by [`NamedTypeBinding`] to anchor a
/// DSL-declared named type to the concrete Rust type codegen must emit.
///
/// Grown as needed by wave-b codegen. Intentionally small and explicit — avoids
/// the old `render_named_type_alias_target` allow-list which silently defaulted
/// unknown aliases to `String`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum RustTypeAtom {
    U64,
    U32,
    U16,
    U8,
    Bool,
    String,
    /// String-backed closed semantic domain.
    ///
    /// This keeps the schema-level representation compatible with DSL enum
    /// literals while giving codegen and the runtime oracle an authoritative
    /// finite value set for named string types.
    StringEnum {
        variants: Vec<EnumVariantId>,
    },
    /// Fully-qualified Rust type path, e.g. `"crate::domain::MySpecialType"`.
    TypePath(String),
    /// Fully-qualified Rust struct type path whose model domain is represented
    /// as finite sets of present field names.
    TypePathFieldPresenceSet {
        path: String,
        fields: Vec<FieldId>,
    },
    /// Fully-qualified Rust struct type path whose model and runtime domains are
    /// represented as structural records with typed fields.
    TypePathStruct {
        path: String,
        fields: Vec<TypePathStructField>,
    },
    /// Fully-qualified Rust enum type path with explicit unit variants that
    /// can appear as DSL named-variant literals.
    TypePathEnum {
        path: String,
        unit_variants: Vec<EnumVariantId>,
        #[serde(default)]
        structural_variants: Vec<TypePathEnumStructuralVariant>,
    },
}

impl RustTypeAtom {
    /// Returns whether two named-type bindings project to the same composition
    /// model domain shape.
    ///
    /// Machine-local `TypePath` owners can differ by Rust module path while
    /// still sharing a composition-level TLA domain through the named slug.
    /// `TypePathEnum` owners are likewise path-agnostic here, but their unit
    /// and structural variant payload shapes must agree because those variants
    /// define the generated finite domain.
    pub fn has_same_composition_domain_shape(&self, other: &Self) -> bool {
        if self == other {
            return true;
        }

        match (self, other) {
            (Self::TypePath(_), Self::TypePath(_)) => true,
            (
                Self::TypePathFieldPresenceSet {
                    fields: left_fields,
                    ..
                },
                Self::TypePathFieldPresenceSet {
                    fields: right_fields,
                    ..
                },
            ) => left_fields == right_fields,
            (
                Self::TypePathStruct {
                    fields: left_fields,
                    ..
                },
                Self::TypePathStruct {
                    fields: right_fields,
                    ..
                },
            ) => left_fields == right_fields,
            (
                Self::TypePathEnum {
                    unit_variants: left_units,
                    structural_variants: left_structural,
                    ..
                },
                Self::TypePathEnum {
                    unit_variants: right_units,
                    structural_variants: right_structural,
                    ..
                },
            ) => left_units == right_units && left_structural == right_structural,
            _ => false,
        }
    }
}

/// Authoritative binding from a DSL-declared named type to its Rust atom.
///
/// Consumed by codegen (B-2) to replace the hard-coded allow-list. The mapping
/// is carried on the DSL declaration itself so the schema layer is the single
/// source of truth.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NamedTypeBinding {
    pub name: NamedTypeId,
    pub rust: RustTypeAtom,
}

impl NamedTypeBinding {
    /// Construct a binding whose Rust representation is `u64`.
    ///
    /// Panics if `name` is not a valid [`NamedTypeId`] slug. Intended for
    /// catalog construction sites; callers that want fallible
    /// construction should build [`NamedTypeId`] directly and assemble
    /// the struct by hand.
    pub fn u64(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::U64,
        }
    }

    /// Construct a binding whose Rust representation is `String`.
    pub fn string(name: &str) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::String,
        }
    }

    /// Construct a binding whose Rust representation is a closed string
    /// domain rendered as a Rust enum.
    ///
    /// Panics if `name` or any variant is not a valid slug, or if the variant
    /// set is empty. Intended for catalog construction sites.
    pub fn string_enum(name: &str, variants: &[&str]) -> Self {
        assert!(
            !variants.is_empty(),
            "string enum named-type bindings require at least one variant"
        );
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::StringEnum {
                variants: variants
                    .iter()
                    .map(|variant| {
                        #[allow(clippy::expect_used)]
                        EnumVariantId::parse(*variant).expect("valid enum variant slug")
                    })
                    .collect(),
            },
        }
    }

    /// Construct a binding whose Rust representation is a fully-qualified
    /// type path.
    pub fn type_path(name: &str, rust_path: impl Into<String>) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::TypePath(rust_path.into()),
        }
    }

    /// Construct a binding whose Rust representation is a fully-qualified type
    /// path and whose generated model domain is finite field-presence sets.
    pub fn type_path_field_presence_set(
        name: &str,
        rust_path: impl Into<String>,
        fields: &[&str],
    ) -> Self {
        assert!(
            !fields.is_empty(),
            "field-presence named-type bindings require at least one field"
        );
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::TypePathFieldPresenceSet {
                path: rust_path.into(),
                fields: fields
                    .iter()
                    .map(|field| {
                        #[allow(clippy::expect_used)]
                        FieldId::parse(*field).expect("valid field-presence slug")
                    })
                    .collect(),
            },
        }
    }

    /// Construct a binding whose Rust representation is a fully-qualified type
    /// path and whose generated model/runtime domain is a typed structural
    /// record.
    pub fn type_path_struct(
        name: &str,
        rust_path: impl Into<String>,
        fields: Vec<TypePathStructField>,
    ) -> Self {
        assert!(
            !fields.is_empty(),
            "struct named-type bindings require at least one field"
        );
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::TypePathStruct {
                path: rust_path.into(),
                fields,
            },
        }
    }

    /// Construct a binding whose Rust representation is a fully-qualified
    /// structural enum type path with a closed variant domain.
    pub fn type_path_enum(
        name: &str,
        rust_path: impl Into<String>,
        unit_variants: &[&str],
    ) -> Self {
        assert!(
            !unit_variants.is_empty(),
            "type-path enum named-type bindings require at least one unit variant"
        );
        Self {
            #[allow(clippy::expect_used)]
            name: NamedTypeId::parse(name).expect("valid named-type slug"),
            rust: RustTypeAtom::TypePathEnum {
                path: rust_path.into(),
                unit_variants: unit_variants
                    .iter()
                    .map(|variant| {
                        #[allow(clippy::expect_used)]
                        EnumVariantId::parse(*variant).expect("valid enum variant slug")
                    })
                    .collect(),
                structural_variants: Vec::new(),
            },
        }
    }

    /// Construct a binding whose Rust representation is a fully-qualified
    /// structural enum type path with unit and payload-carrying variants.
    pub fn type_path_enum_with_structural_variants(
        name: &str,
        rust_path: impl Into<String>,
        unit_variants: &[&str],
        structural_variants: Vec<TypePathEnumStructuralVariant>,
    ) -> Self {
        let mut binding = Self::type_path_enum(name, rust_path, unit_variants);
        if let RustTypeAtom::TypePathEnum {
            structural_variants: variants,
            ..
        } = &mut binding.rust
        {
            *variants = structural_variants;
        }
        binding
    }
}