1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Metadata {
9 pub name: String,
11 #[serde(skip_serializing_if = "Option::is_none")]
13 pub description: Option<String>,
14}
15
16pub fn role_type(id: &str) -> &str {
20 id.split_once("__").map_or(id, |(prefix, _)| prefix)
21}
22
23pub fn derive_step_name(id: &str) -> String {
32 id.split(['_', '-'])
33 .map(|word| {
34 let mut chars = word.chars();
35 match chars.next() {
36 None => String::new(),
37 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
38 }
39 })
40 .collect::<Vec<_>>()
41 .join(" ")
42}
43
44pub fn derive_role_name(id: &str) -> String {
49 id.replace("__", " ")
50 .split(['_', '-', ' '])
51 .filter(|s| !s.is_empty())
52 .map(|word| {
53 let mut chars = word.chars();
54 match chars.next() {
55 None => String::new(),
56 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
57 }
58 })
59 .collect::<Vec<_>>()
60 .join(" ")
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66#[non_exhaustive]
67pub enum ActionType {
68 ClockCheck,
73 Confirm,
78 CheckValue,
83 OralReadback,
88 MachineInfo,
93
94 GenerateKey,
97 WrapKey,
106 UnwrapKey,
108 ExportPublic,
110 SignData,
115 VerifySignature,
122 Attest,
124 GatherEntropy,
131 TpmAttest,
135
136 PivReadCertificate,
141 PivSign,
146 YubikeyAttestSlot,
151
152 IssueCertificate,
158 GenerateCsr,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174#[non_exhaustive]
175pub enum BackendUsage {
176 Required,
178 SoftwareUnlessNamed,
182 Unused,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194#[non_exhaustive]
195pub enum CertProfile {
196 RootCa,
198 SubCa,
200 TlsServer,
202 CodeSigning,
204 EndEntity,
206}
207
208impl fmt::Display for CertProfile {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 CertProfile::RootCa => write!(f, "root_ca"),
212 CertProfile::SubCa => write!(f, "sub_ca"),
213 CertProfile::TlsServer => write!(f, "tls_server"),
214 CertProfile::CodeSigning => write!(f, "code_signing"),
215 CertProfile::EndEntity => write!(f, "end_entity"),
216 }
217 }
218}
219
220impl std::str::FromStr for CertProfile {
221 type Err = UnknownCertProfile;
222
223 fn from_str(s: &str) -> Result<Self, Self::Err> {
224 match s {
225 "root_ca" => Ok(Self::RootCa),
226 "sub_ca" | "intermediate_ca" => Ok(Self::SubCa),
229 "tls_server" => Ok(Self::TlsServer),
230 "code_signing" => Ok(Self::CodeSigning),
231 "end_entity" => Ok(Self::EndEntity),
232 _ => Err(UnknownCertProfile),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct UnknownCertProfile;
240
241impl fmt::Display for UnknownCertProfile {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(
244 f,
245 "supported profiles: root_ca, sub_ca, tls_server, code_signing, end_entity"
246 )
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254#[non_exhaustive]
255pub struct ReadsContract {
256 pub required: &'static [&'static str],
258 pub exactly_one_of: &'static [&'static [&'static str]],
261 pub with_field_requires: &'static [(&'static str, &'static str)],
269}
270
271impl ReadsContract {
272 pub const NONE: Self = Self::required(&[]);
274
275 #[must_use]
278 pub const fn required(fields: &'static [&'static str]) -> Self {
279 Self {
280 required: fields,
281 exactly_one_of: &[],
282 with_field_requires: &[],
283 }
284 }
285
286 pub fn is_empty(&self) -> bool {
290 self.required.is_empty() && self.exactly_one_of.is_empty()
291 }
292}
293
294impl ActionType {
295 pub const ALL: &'static [ActionType] = &[
302 ActionType::ClockCheck,
303 ActionType::Confirm,
304 ActionType::CheckValue,
305 ActionType::OralReadback,
306 ActionType::MachineInfo,
307 ActionType::GenerateKey,
308 ActionType::WrapKey,
309 ActionType::UnwrapKey,
310 ActionType::ExportPublic,
311 ActionType::SignData,
312 ActionType::VerifySignature,
313 ActionType::Attest,
314 ActionType::GatherEntropy,
315 ActionType::TpmAttest,
316 ActionType::PivReadCertificate,
317 ActionType::PivSign,
318 ActionType::YubikeyAttestSlot,
319 ActionType::IssueCertificate,
320 ActionType::GenerateCsr,
321 ];
322
323 pub fn backend_usage(self) -> BackendUsage {
329 match self {
330 ActionType::GenerateKey
331 | ActionType::SignData
332 | ActionType::WrapKey
333 | ActionType::UnwrapKey
334 | ActionType::ExportPublic
335 | ActionType::GenerateCsr
336 | ActionType::IssueCertificate
337 | ActionType::PivReadCertificate
338 | ActionType::PivSign
339 | ActionType::YubikeyAttestSlot
340 | ActionType::TpmAttest => BackendUsage::Required,
341
342 ActionType::VerifySignature => BackendUsage::SoftwareUnlessNamed,
343
344 ActionType::ClockCheck
345 | ActionType::Confirm
346 | ActionType::CheckValue
347 | ActionType::OralReadback
348 | ActionType::MachineInfo
349 | ActionType::Attest
350 | ActionType::GatherEntropy => BackendUsage::Unused,
351 }
352 }
353
354 pub fn required_with_fields(self) -> &'static [&'static str] {
363 match self {
364 ActionType::CheckValue => &["actual", "expected"],
365 ActionType::GenerateCsr => &["subject"],
366
367 ActionType::ClockCheck
368 | ActionType::Confirm
369 | ActionType::OralReadback
370 | ActionType::MachineInfo
371 | ActionType::GenerateKey
372 | ActionType::WrapKey
373 | ActionType::UnwrapKey
374 | ActionType::ExportPublic
375 | ActionType::SignData
376 | ActionType::VerifySignature
377 | ActionType::Attest
378 | ActionType::GatherEntropy
379 | ActionType::TpmAttest
380 | ActionType::PivReadCertificate
381 | ActionType::PivSign
382 | ActionType::YubikeyAttestSlot
383 | ActionType::IssueCertificate => &[],
384 }
385 }
386
387 pub fn known_with_fields(self) -> &'static [&'static str] {
401 match self {
402 ActionType::ClockCheck | ActionType::Confirm => &["message"],
403 ActionType::CheckValue => &["actual", "expected", "message", "sensitive"],
404 ActionType::OralReadback => &["value", "format", "characters", "sensitive", "message"],
405 ActionType::MachineInfo => &[
406 "include_machine_id",
407 "include_cpu",
408 "include_os",
409 "include_security_features",
410 "message",
411 ],
412 ActionType::GenerateKey => &["algorithm", "policy", "slot"],
413 ActionType::WrapKey => &["scheme", "expect_recipient"],
414 ActionType::UnwrapKey => &["algorithm", "expect_key", "label", "policy"],
415 ActionType::SignData | ActionType::VerifySignature => &["algorithm", "message"],
416 ActionType::Attest => &["statement"],
417 ActionType::GatherEntropy => &["instruction"],
418 ActionType::IssueCertificate => &["profile", "validity_days", "issuer_cn", "path_len"],
419 ActionType::GenerateCsr => &["subject", "san"],
420 ActionType::PivReadCertificate | ActionType::YubikeyAttestSlot => &["slot", "message"],
421 ActionType::PivSign => &["slot", "algorithm", "message"],
422 ActionType::ExportPublic | ActionType::TpmAttest => &[],
427 }
428 }
429
430 pub fn reads_contract(self) -> ReadsContract {
438 match self {
439 ActionType::WrapKey => ReadsContract {
440 required: &["key_to_wrap"],
441 exactly_one_of: &[&["wrapping_key", "recipient"]],
442 with_field_requires: &[("expect_recipient", "recipient")],
445 },
446 ActionType::UnwrapKey => ReadsContract::required(&["unwrapping_key", "wrapped_data"]),
447 ActionType::SignData => ReadsContract::required(&["key", "data"]),
448 ActionType::VerifySignature => ReadsContract::required(&["key", "data", "signature"]),
449 ActionType::IssueCertificate => ReadsContract::required(&["signing_key", "csr"]),
450 ActionType::GenerateCsr => ReadsContract::required(&["signing_key"]),
451
452 ActionType::ClockCheck
453 | ActionType::Confirm
454 | ActionType::CheckValue
455 | ActionType::OralReadback
456 | ActionType::MachineInfo
457 | ActionType::GenerateKey
458 | ActionType::ExportPublic
459 | ActionType::Attest
460 | ActionType::GatherEntropy
461 | ActionType::TpmAttest
462 | ActionType::PivReadCertificate
463 | ActionType::PivSign
464 | ActionType::YubikeyAttestSlot => ReadsContract::NONE,
465 }
466 }
467
468 pub fn describe(&self) -> &'static str {
473 match self {
474 ActionType::ClockCheck => "Verify system clock against a reference time.",
475 ActionType::Confirm => "Confirm readiness or completion of a manual step.",
476 ActionType::CheckValue => "Verify a value matches an expected result.",
477 ActionType::OralReadback => "Read back a value aloud for verification.",
478 ActionType::MachineInfo => "Record system and environment information.",
479 ActionType::Attest => "Record a signed attestation from a participant.",
480 ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
481 ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
482 ActionType::GenerateKey => "Generate a cryptographic key.",
483 ActionType::ExportPublic => "Export the public component of a keypair.",
484 ActionType::SignData => "Sign data with a ceremony key.",
485 ActionType::VerifySignature => "Verify a signature against a public key.",
486 ActionType::WrapKey => "Encrypt a key under another key for transport.",
487 ActionType::UnwrapKey => "Decrypt a wrapped key and import it into a backend.",
488 ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
489 ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
490 ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
491 ActionType::PivSign => "Sign data using a PIV smart card key.",
492 ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
493 }
494 }
495}
496
497impl std::fmt::Display for ActionType {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 match self {
500 ActionType::ClockCheck => write!(f, "clock_check"),
501 ActionType::Confirm => write!(f, "confirm"),
502 ActionType::CheckValue => write!(f, "check_value"),
503 ActionType::OralReadback => write!(f, "oral_readback"),
504 ActionType::MachineInfo => write!(f, "machine_info"),
505 ActionType::GenerateKey => write!(f, "generate_key"),
506 ActionType::WrapKey => write!(f, "wrap_key"),
507 ActionType::UnwrapKey => write!(f, "unwrap_key"),
508 ActionType::ExportPublic => write!(f, "export_public"),
509 ActionType::SignData => write!(f, "sign_data"),
510 ActionType::VerifySignature => write!(f, "verify_signature"),
511 ActionType::Attest => write!(f, "attest"),
512 ActionType::GatherEntropy => write!(f, "gather_entropy"),
513 ActionType::TpmAttest => write!(f, "tpm_attest"),
514 ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
515 ActionType::PivSign => write!(f, "piv_sign"),
516 ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
517 ActionType::IssueCertificate => write!(f, "issue_certificate"),
518 ActionType::GenerateCsr => write!(f, "generate_csr"),
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
525#[serde(rename_all = "snake_case")]
526#[non_exhaustive]
527pub enum OutputType {
528 PublicKey,
530 WrappedKey,
532 Certificate,
534 SignedRrset,
536 Sct,
538
539 Document,
542 CeremonyLog,
544}
545
546impl std::fmt::Display for OutputType {
547 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548 match self {
549 OutputType::PublicKey => write!(f, "public_key"),
550 OutputType::WrappedKey => write!(f, "wrapped_key"),
551 OutputType::Certificate => write!(f, "certificate"),
552 OutputType::SignedRrset => write!(f, "signed_rrset"),
553 OutputType::Sct => write!(f, "sct"),
554 OutputType::Document => write!(f, "document"),
555 OutputType::CeremonyLog => write!(f, "ceremony_log"),
556 }
557 }
558}
559
560impl OutputType {
561 pub fn default_extension(&self) -> &'static str {
563 match self {
564 OutputType::PublicKey | OutputType::Certificate => "pem",
565 OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
566 OutputType::Document => "txt",
567 OutputType::CeremonyLog => "json",
568 }
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574#[serde(rename_all = "snake_case")]
575#[non_exhaustive]
576pub enum ParameterType {
577 String,
579 Date,
581 Integer,
583 Boolean,
585}
586
587impl std::fmt::Display for ParameterType {
588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 match self {
590 ParameterType::String => write!(f, "string"),
591 ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
592 ParameterType::Integer => write!(f, "integer"),
593 ParameterType::Boolean => write!(f, "boolean (true/false)"),
594 }
595 }
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606#[serde(rename_all = "snake_case")]
607#[non_exhaustive]
608pub enum DutyType {
609 ReturnToVault,
611 DistributeShares,
613 DistributeMedia,
615 ArchiveMaterials,
617 PublishRecord,
619 NotifyStakeholders,
621 ImportKeys,
623 Custom,
625}
626
627impl DutyType {
628 pub fn display_name(&self) -> &'static str {
630 match self {
631 DutyType::ReturnToVault => "Return to Vault",
632 DutyType::DistributeShares => "Distribute Key Shares",
633 DutyType::DistributeMedia => "Distribute Backup Media",
634 DutyType::ArchiveMaterials => "Archive Materials",
635 DutyType::PublishRecord => "Publish Record",
636 DutyType::NotifyStakeholders => "Notify Stakeholders",
637 DutyType::ImportKeys => "Import Keys",
638 DutyType::Custom => "Custom Duty",
639 }
640 }
641
642 pub fn built_in_prose(&self) -> Option<&'static str> {
646 match self {
647 DutyType::ReturnToVault => Some("Return materials to secure storage"),
648 DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
649 DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
650 DutyType::ArchiveMaterials => {
651 Some("Archive ceremony materials at designated storage locations")
652 }
653 DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
654 DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
655 DutyType::ImportKeys => Some("Import generated keys into operational system"),
656 DutyType::Custom => None,
657 }
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn role_type_extracts_prefix() {
667 assert_eq!(role_type("witness__1"), "witness");
668 assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
669 assert_eq!(role_type("operator"), "operator");
670 assert_eq!(role_type("witness__"), "witness");
671 }
672
673 #[test]
674 fn derive_step_name_title_cases() {
675 assert_eq!(derive_step_name("verify_time"), "Verify Time");
676 assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
677 assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
678 assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
679 assert_eq!(derive_step_name("clock-check"), "Clock Check");
680 assert_eq!(derive_step_name("opening"), "Opening");
681 }
682
683 #[test]
684 fn derive_role_name_title_cases() {
685 assert_eq!(derive_role_name("witness__1"), "Witness 1");
686 assert_eq!(
687 derive_role_name("hsm_operator__primary"),
688 "Hsm Operator Primary"
689 );
690 assert_eq!(derive_role_name("operator"), "Operator");
691 assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
692 }
693
694 #[test]
695 fn action_type_serde_roundtrip() {
696 let cases: &[(ActionType, &str)] = &[
699 (ActionType::ClockCheck, "\"clock_check\""),
700 (ActionType::Confirm, "\"confirm\""),
701 (ActionType::CheckValue, "\"check_value\""),
702 (ActionType::OralReadback, "\"oral_readback\""),
703 (ActionType::MachineInfo, "\"machine_info\""),
704 (ActionType::GenerateKey, "\"generate_key\""),
705 (ActionType::WrapKey, "\"wrap_key\""),
706 (ActionType::UnwrapKey, "\"unwrap_key\""),
707 (ActionType::ExportPublic, "\"export_public\""),
708 (ActionType::Attest, "\"attest\""),
709 (ActionType::TpmAttest, "\"tpm_attest\""),
710 (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
711 (ActionType::PivSign, "\"piv_sign\""),
712 (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
713 (ActionType::IssueCertificate, "\"issue_certificate\""),
714 (ActionType::GenerateCsr, "\"generate_csr\""),
715 ];
716 for &(variant, expected) in cases {
717 let serialized = serde_json::to_string(&variant).unwrap();
718 assert_eq!(serialized, expected, "serialize {variant:?}");
719 let deserialized: ActionType = serde_json::from_str(expected).unwrap();
720 assert_eq!(deserialized, variant, "deserialize {expected}");
721 }
722 }
723
724 #[test]
725 fn action_type_display_matches_serde() {
726 let actions = [
729 ActionType::ClockCheck,
730 ActionType::Confirm,
731 ActionType::CheckValue,
732 ActionType::OralReadback,
733 ActionType::MachineInfo,
734 ActionType::GenerateKey,
735 ActionType::WrapKey,
736 ActionType::UnwrapKey,
737 ActionType::ExportPublic,
738 ActionType::Attest,
739 ActionType::TpmAttest,
740 ActionType::PivReadCertificate,
741 ActionType::PivSign,
742 ActionType::YubikeyAttestSlot,
743 ActionType::IssueCertificate,
744 ActionType::GenerateCsr,
745 ];
746 for action in actions {
747 let display = action.to_string();
748 let serde_json = serde_json::to_string(&action).unwrap();
749 assert_eq!(
750 display,
751 serde_json.trim_matches('"'),
752 "Display and serde disagree for {action:?}"
753 );
754 }
755 }
756
757 #[test]
758 fn output_type_serde_roundtrip() {
759 let cases: &[(OutputType, &str)] = &[
762 (OutputType::PublicKey, "\"public_key\""),
763 (OutputType::WrappedKey, "\"wrapped_key\""),
764 (OutputType::Certificate, "\"certificate\""),
765 (OutputType::Document, "\"document\""),
766 (OutputType::CeremonyLog, "\"ceremony_log\""),
767 ];
768 for &(variant, expected) in cases {
769 let serialized = serde_json::to_string(&variant).unwrap();
770 assert_eq!(serialized, expected, "serialize {variant:?}");
771 let deserialized: OutputType = serde_json::from_str(expected).unwrap();
772 assert_eq!(deserialized, variant, "deserialize {expected}");
773 }
774 }
775
776 #[test]
777 fn parameter_type_serde_roundtrip() {
778 let cases: &[(ParameterType, &str)] = &[
780 (ParameterType::String, "\"string\""),
781 (ParameterType::Date, "\"date\""),
782 (ParameterType::Integer, "\"integer\""),
783 (ParameterType::Boolean, "\"boolean\""),
784 ];
785 for (variant, expected) in cases {
786 let serialized = serde_json::to_string(variant).unwrap();
787 assert_eq!(serialized, *expected, "serialize {variant:?}");
788 let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
789 assert_eq!(
791 serde_json::to_string(&deserialized).unwrap(),
792 *expected,
793 "deserialize {expected}"
794 );
795 }
796 }
797
798 #[test]
799 fn duty_type_serde_roundtrip() {
800 let cases: &[(DutyType, &str)] = &[
802 (DutyType::ReturnToVault, "\"return_to_vault\""),
803 (DutyType::DistributeShares, "\"distribute_shares\""),
804 (DutyType::DistributeMedia, "\"distribute_media\""),
805 (DutyType::ArchiveMaterials, "\"archive_materials\""),
806 (DutyType::PublishRecord, "\"publish_record\""),
807 (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
808 (DutyType::ImportKeys, "\"import_keys\""),
809 (DutyType::Custom, "\"custom\""),
810 ];
811 for (variant, expected) in cases {
812 let serialized = serde_json::to_string(variant).unwrap();
813 assert_eq!(serialized, *expected, "serialize {variant:?}");
814 let deserialized: DutyType = serde_json::from_str(expected).unwrap();
815 assert_eq!(deserialized, *variant, "deserialize {expected}");
816 }
817 }
818
819 #[test]
820 fn duty_type_custom_has_no_built_in_prose() {
821 assert!(
824 DutyType::Custom.built_in_prose().is_none(),
825 "Custom duty must have no built-in prose"
826 );
827 for duty in [
829 DutyType::ReturnToVault,
830 DutyType::DistributeShares,
831 DutyType::DistributeMedia,
832 DutyType::ArchiveMaterials,
833 DutyType::PublishRecord,
834 DutyType::NotifyStakeholders,
835 DutyType::ImportKeys,
836 ] {
837 assert!(
838 duty.built_in_prose().is_some(),
839 "{duty:?} must have built-in prose"
840 );
841 }
842 }
843
844 #[test]
855 fn every_required_field_is_also_a_known_field() {
856 for action in ActionType::ALL {
857 let known = action.known_with_fields();
858 for field in action.required_with_fields() {
859 assert!(
860 known.contains(field),
861 "{action} requires '{field}' and does not accept it"
862 );
863 }
864 }
865 }
866
867 #[test]
868 fn all_lists_every_action_type() {
869 for action in ActionType::ALL {
870 match action {
871 ActionType::ClockCheck
872 | ActionType::Confirm
873 | ActionType::CheckValue
874 | ActionType::OralReadback
875 | ActionType::MachineInfo
876 | ActionType::GenerateKey
877 | ActionType::WrapKey
878 | ActionType::UnwrapKey
879 | ActionType::ExportPublic
880 | ActionType::SignData
881 | ActionType::VerifySignature
882 | ActionType::Attest
883 | ActionType::GatherEntropy
884 | ActionType::TpmAttest
885 | ActionType::PivReadCertificate
886 | ActionType::PivSign
887 | ActionType::YubikeyAttestSlot
888 | ActionType::IssueCertificate
889 | ActionType::GenerateCsr => {}
890 }
891 }
892
893 let names: std::collections::BTreeSet<String> =
894 ActionType::ALL.iter().map(ToString::to_string).collect();
895 assert_eq!(
896 names.len(),
897 ActionType::ALL.len(),
898 "two actions share a DSL name"
899 );
900 }
901}