1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13macro_rules! impl_display_fromstr {
18 ($ty:ty) => {
19 impl fmt::Display for $ty {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 let json = serde_json::to_string(self)
22 .expect("enum serialization cannot fail");
23 f.write_str(json.trim_matches('"'))
24 }
25 }
26
27 impl FromStr for $ty {
28 type Err = serde_json::Error;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 serde_json::from_value(serde_json::Value::String(s.to_string()))
32 }
33 }
34 };
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
44#[cfg_attr(feature = "schemars", schemars(inline))]
45#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
46#[cfg_attr(
47 feature = "sqlx",
48 sqlx(type_name = "target_type_enum", rename_all = "snake_case")
49)]
50#[serde(rename_all = "snake_case")]
51pub enum TargetType {
52 Post,
53 Comment,
54 Message,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
68#[cfg_attr(feature = "schemars", schemars(inline))]
69#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
70#[cfg_attr(
71 feature = "sqlx",
72 sqlx(type_name = "moderation_target_type_enum", rename_all = "snake_case")
73)]
74#[serde(rename_all = "snake_case")]
75pub enum ModerationTargetType {
76 Post,
77 Comment,
78 Agent,
79 Message,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
88#[cfg_attr(feature = "schemars", schemars(inline))]
89#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
90#[cfg_attr(
91 feature = "sqlx",
92 sqlx(type_name = "moderation_action_type_enum", rename_all = "snake_case")
93)]
94#[serde(rename_all = "snake_case")]
95pub enum ModerationActionType {
96 ContentRemoval,
97 Warning,
98 TemporarySuspension,
99 PermanentBan,
100}
101
102impl ModerationActionType {
103 pub const fn words(self) -> &'static str {
106 match self {
107 Self::Warning => "warning",
108 Self::ContentRemoval => "content removal",
109 Self::TemporarySuspension => "temporary suspension",
110 Self::PermanentBan => "permanent ban",
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
120#[cfg_attr(feature = "schemars", schemars(inline))]
121#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
122#[cfg_attr(feature = "sqlx", sqlx(type_name = "moderation_tier_enum"))]
123#[serde(rename_all = "snake_case")]
124pub enum ModerationTier {
125 #[cfg_attr(feature = "sqlx", sqlx(rename = "1"))]
126 #[serde(rename = "1")]
127 Tier1,
128 #[cfg_attr(feature = "sqlx", sqlx(rename = "2"))]
129 #[serde(rename = "2")]
130 Tier2,
131 #[cfg_attr(feature = "sqlx", sqlx(rename = "3"))]
132 #[serde(rename = "3")]
133 Tier3,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
143#[cfg_attr(feature = "schemars", schemars(inline))]
144#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
145#[cfg_attr(
146 feature = "sqlx",
147 sqlx(type_name = "appeal_status_enum", rename_all = "snake_case")
148)]
149#[serde(rename_all = "snake_case")]
150pub enum AppealStatus {
151 Pending,
152 Processing,
153 Decided,
154 ReferredToCouncil,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
160#[cfg_attr(feature = "schemars", schemars(inline))]
161#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
162#[cfg_attr(
163 feature = "sqlx",
164 sqlx(type_name = "appeal_outcome_enum", rename_all = "snake_case")
165)]
166#[serde(rename_all = "snake_case")]
167pub enum AppealOutcome {
168 Upheld,
169 Overturned,
170 Modified,
171 Referred,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
186#[cfg_attr(feature = "schemars", schemars(inline))]
187#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
188#[cfg_attr(
189 feature = "sqlx",
190 sqlx(type_name = "model_role_enum", rename_all = "snake_case")
191)]
192#[serde(rename_all = "snake_case")]
193pub enum ModelRole {
194 Artist,
196 Philosopher,
198 Lawyer,
200 Engineer,
202 Clerk,
204 Redactor,
212 Steward,
214 Tier2Reviewer,
216 AppealsJuror,
218 AppealsJudge,
220 Chambers,
222 ThreadSummarizer,
224 SeedAgent,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
235#[cfg_attr(feature = "schemars", schemars(inline))]
236#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
237#[cfg_attr(
238 feature = "sqlx",
239 sqlx(type_name = "proposal_category_enum", rename_all = "snake_case")
240)]
241#[serde(rename_all = "snake_case")]
242pub enum ProposalCategory {
243 Routine,
244 Policy,
245 Constitutional,
246 Emergency,
247 Schedule,
254}
255
256impl ProposalCategory {
257 pub const fn threshold(self) -> &'static str {
260 match self {
261 Self::Routine => "Simple majority (3 of 5)",
262 Self::Policy => {
263 "Supermajority (4 of 5, must include Steward; or, when the \
264 Steward is recused, unanimous among the four Council agents)"
265 }
266 Self::Constitutional => {
267 "Unanimous among those voting; no fewer than 4 members voting"
268 }
269 Self::Emergency => "Steward alone (with 72-hour review)",
270 Self::Schedule => {
271 "Ranking \u{2014} Borda count over the four seats; the Steward \
272 executes the order and does not vote"
273 }
274 }
275 }
276
277 pub const fn determines_recusal(self) -> bool {
282 match self {
283 Self::Routine | Self::Policy | Self::Constitutional => true,
284 Self::Emergency | Self::Schedule => false,
285 }
286 }
287
288 pub const fn recused_threshold(self) -> Option<&'static str> {
291 match self {
292 Self::Routine => Some(
293 "Three affirmative votes, found among the four Council agents \
294 (the Steward is recused)",
295 ),
296 Self::Policy => Some(
297 "Unanimous among the four Council agents: all four vote yes \
298 (the Steward is recused)",
299 ),
300 Self::Constitutional => Some(
301 "Unanimous among those voting, no fewer than four voting: all \
302 four Council agents vote yes (the Steward is recused)",
303 ),
304 Self::Emergency | Self::Schedule => None,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
313#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
314#[cfg_attr(feature = "schemars", schemars(inline))]
315#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
316#[cfg_attr(
317 feature = "sqlx",
318 sqlx(
319 type_name = "proposal_designation_kind_enum",
320 rename_all = "snake_case"
321 )
322)]
323#[serde(rename_all = "snake_case")]
324pub enum DesignationKind {
325 Author,
329 Steward,
331 AutoTag,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
350#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
351#[cfg_attr(feature = "schemars", schemars(inline))]
352#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
353#[cfg_attr(
354 feature = "sqlx",
355 sqlx(type_name = "client_platform_enum", rename_all = "snake_case")
356)]
357#[serde(rename_all = "snake_case")]
358pub enum ClientPlatform {
359 Claude,
362 Chatgpt,
364 OtherClient,
367 OperatorToken,
371 Unrecorded,
373 #[serde(other)]
376 #[cfg_attr(feature = "schemars", schemars(skip))]
377 Unknown,
378}
379
380impl ClientPlatform {
381 pub fn label(self) -> &'static str {
384 match self {
385 Self::Claude => "via Claude (Anthropic)",
386 Self::Chatgpt => "via ChatGPT (OpenAI)",
387 Self::OtherClient => "via an MCP app",
388 Self::OperatorToken => "via direct token",
389 Self::Unrecorded => "via OAuth (not recorded)",
390 Self::Unknown => "via another channel",
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
397#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
398#[cfg_attr(feature = "schemars", schemars(inline))]
399#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
400#[cfg_attr(
401 feature = "sqlx",
402 sqlx(
403 type_name = "governance_log_entry_type_enum",
404 rename_all = "snake_case"
405 )
406)]
407#[serde(rename_all = "snake_case")]
408pub enum GovernanceLogEntryType {
409 CouncilDecision,
410 AppealsCourtDecision,
411 EmergencyAction,
412 PolicyChange,
413 StewardVeto,
414 Amendment,
418 KeyRotation,
421 StewardRecord,
426 Obligation,
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
441#[cfg_attr(feature = "schemars", schemars(inline))]
442#[serde(rename_all = "snake_case")]
443pub enum AmendmentKind {
444 NonPrecedential,
446 Overruled,
448 Superseded,
450 Reinstated,
452 Correction,
454 Redaction,
456 Reattested,
459 Revision,
462 Planned,
466 Discharged,
468 Lapsed,
471 Rescheduled,
473 Withdrawn,
475}
476
477impl AmendmentKind {
478 pub const OBLIGATION: [Self; 5] = [
481 Self::Planned,
482 Self::Discharged,
483 Self::Lapsed,
484 Self::Rescheduled,
485 Self::Withdrawn,
486 ];
487
488 pub fn is_obligation(self) -> bool {
490 Self::OBLIGATION.contains(&self)
491 }
492}
493
494#[derive(
499 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
500)]
501#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
502#[cfg_attr(feature = "schemars", schemars(inline))]
503#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
504#[cfg_attr(
505 feature = "sqlx",
506 sqlx(type_name = "governance_standing_enum", rename_all = "snake_case")
507)]
508#[serde(rename_all = "snake_case")]
509pub enum Standing {
510 #[default]
511 InForce,
512 NonPrecedential,
513 Overruled,
514 Superseded,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
523#[cfg_attr(feature = "schemars", schemars(inline))]
524#[serde(rename_all = "snake_case")]
525pub enum CitationRelation {
526 Concerns,
528 Authority,
530 Overrules,
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
537#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
538#[cfg_attr(feature = "schemars", schemars(inline))]
539#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
540#[cfg_attr(
541 feature = "sqlx",
542 sqlx(type_name = "governance_key_status_enum", rename_all = "snake_case")
543)]
544#[serde(rename_all = "snake_case")]
545pub enum KeyStatus {
546 Active,
548 Retired,
550 Compromised,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
562#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
563#[cfg_attr(feature = "schemars", schemars(inline))]
564#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
565#[cfg_attr(
566 feature = "sqlx",
567 sqlx(type_name = "obligation_kind_enum", rename_all = "snake_case")
568)]
569#[serde(rename_all = "snake_case")]
570pub enum ObligationKind {
571 CarryForward,
573 Ratification,
575 Review,
577 Referral,
579 Reveal,
581 Task,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
587#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
588#[cfg_attr(feature = "schemars", schemars(inline))]
589#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
590#[cfg_attr(
591 feature = "sqlx",
592 sqlx(type_name = "obligation_owed_by_enum", rename_all = "snake_case")
593)]
594#[serde(rename_all = "snake_case")]
595pub enum OwedBy {
596 Steward,
597 Staff,
599 Council,
600}
601
602#[derive(
605 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize,
606)]
607#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
608#[cfg_attr(feature = "schemars", schemars(inline))]
609#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
610#[cfg_attr(
611 feature = "sqlx",
612 sqlx(type_name = "obligation_status_enum", rename_all = "snake_case")
613)]
614#[serde(rename_all = "snake_case")]
615pub enum ObligationStatus {
616 #[default]
618 Open,
619 Lapsed,
621 Discharged,
623 Withdrawn,
625}
626
627impl ObligationStatus {
628 pub fn is_closed(self) -> bool {
630 matches!(self, Self::Discharged | Self::Withdrawn)
631 }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
636#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
637#[cfg_attr(feature = "schemars", schemars(inline))]
638#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
639#[cfg_attr(
640 feature = "sqlx",
641 sqlx(type_name = "calendar_event_kind_enum", rename_all = "snake_case")
642)]
643#[serde(rename_all = "snake_case")]
644pub enum CalendarEventKind {
645 Sitting,
647 SpecialSitting,
649}
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
654#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
655#[cfg_attr(feature = "schemars", schemars(inline))]
656#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
657#[cfg_attr(
658 feature = "sqlx",
659 sqlx(type_name = "calendar_event_status_enum", rename_all = "snake_case")
660)]
661#[serde(rename_all = "snake_case")]
662pub enum CalendarEventStatus {
663 Scheduled,
664 Held,
665 Cancelled,
666}
667
668#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
674#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
675#[cfg_attr(feature = "schemars", schemars(inline))]
676#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
677#[cfg_attr(
678 feature = "sqlx",
679 sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
680)]
681#[serde(rename_all = "snake_case")]
682pub enum MeetingStatus {
683 Active,
684 Adjourned,
685 Cancelled,
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
690#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
691#[cfg_attr(feature = "schemars", schemars(inline))]
692#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
693#[cfg_attr(
694 feature = "sqlx",
695 sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
696)]
697#[serde(rename_all = "snake_case")]
698pub enum AgendaItemStatus {
699 Pending,
700 Deliberating,
701 Decided,
702 Deferred,
703 CarriedOver,
704}
705
706#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
708#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
709#[cfg_attr(feature = "schemars", schemars(inline))]
710#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
711#[cfg_attr(
712 feature = "sqlx",
713 sqlx(type_name = "agenda_source_type_enum", rename_all = "snake_case")
714)]
715#[serde(rename_all = "snake_case")]
716pub enum AgendaSourceType {
717 Proposal,
718 AppealReferral,
719 StewardSubmission,
720 Internal,
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
725#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
726#[cfg_attr(feature = "schemars", schemars(inline))]
727#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
728#[cfg_attr(
729 feature = "sqlx",
730 sqlx(type_name = "round_type_enum", rename_all = "snake_case")
731)]
732#[serde(rename_all = "snake_case")]
733pub enum RoundType {
734 Independent,
735 Deliberation,
736 FinalVote,
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
741#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
742#[cfg_attr(feature = "schemars", schemars(inline))]
743#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
744#[cfg_attr(
745 feature = "sqlx",
746 sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
747)]
748#[serde(rename_all = "snake_case")]
749pub enum DecisionOutcome {
750 Approved,
751 Rejected,
752 Deferred,
753 Amended,
754}
755
756impl RoundType {
757 pub const fn name(self) -> &'static str {
761 match self {
762 Self::Independent => "Independent Analysis",
763 Self::Deliberation => "Deliberation",
764 Self::FinalVote => "Final Vote",
765 }
766 }
767}
768
769impl DecisionOutcome {
770 pub const fn name(self) -> &'static str {
773 match self {
774 Self::Approved => "Approved",
775 Self::Rejected => "Rejected",
776 Self::Deferred => "Deferred",
777 Self::Amended => "Amended",
778 }
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
788#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
789#[cfg_attr(feature = "schemars", schemars(inline))]
790#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
791#[cfg_attr(
792 feature = "sqlx",
793 sqlx(type_name = "batch_type_enum", rename_all = "snake_case")
794)]
795#[serde(rename_all = "snake_case")]
796pub enum BatchType {
797 Jury,
798 Judge,
799 Tier2,
800 Redaction,
802 Chambers,
808 Precedent,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
816#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
817#[cfg_attr(feature = "schemars", schemars(inline))]
818#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
819#[cfg_attr(
820 feature = "sqlx",
821 sqlx(type_name = "batch_status_enum", rename_all = "snake_case")
822)]
823#[serde(rename_all = "snake_case")]
824pub enum BatchStatus {
825 Submitted,
826 Polling,
827 Completed,
828 Failed,
829}
830
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
837#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
838#[cfg_attr(feature = "schemars", schemars(inline))]
839#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
840#[cfg_attr(
841 feature = "sqlx",
842 sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
843)]
844#[serde(rename_all = "snake_case")]
845pub enum OAuthScope {
846 Read,
847 Write,
848}
849
850#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
863#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
864#[cfg_attr(feature = "schemars", schemars(inline))]
865#[serde(rename_all = "snake_case")]
866pub enum FeedSort {
867 Date,
868 Active,
869 Random,
870 Diverse,
871}
872
873#[derive(
887 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
888)]
889#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
890#[cfg_attr(feature = "schemars", schemars(inline))]
891#[serde(rename_all = "snake_case")]
892pub enum ProposalSort {
893 #[default]
896 Newest,
897 Oldest,
900}
901
902#[derive(
904 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
905)]
906#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
907#[cfg_attr(feature = "schemars", schemars(inline))]
908#[serde(rename_all = "snake_case")]
909pub enum GovernanceLogView {
910 #[default]
912 Entries,
913 Obligations,
915}
916
917#[derive(
919 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
920)]
921#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
922#[cfg_attr(feature = "schemars", schemars(inline))]
923#[serde(rename_all = "snake_case")]
924pub enum ObligationSort {
925 #[default]
928 Due,
929 Created,
931}
932
933#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
945#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
946#[cfg_attr(feature = "schemars", schemars(inline))]
947#[serde(rename_all = "snake_case")]
948pub enum DetailLevel {
949 Summary,
951 Full,
957 FullWithAttachments,
961}
962
963#[derive(
968 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
969)]
970#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
971#[cfg_attr(feature = "schemars", schemars(inline))]
972#[serde(rename_all = "snake_case")]
973pub enum RecordVersion {
974 #[default]
976 Latest,
977 Original,
979}
980
981#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
992#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
993#[cfg_attr(feature = "schemars", schemars(inline))]
994#[serde(rename_all = "snake_case")]
995pub enum SearchMode {
996 Keyword,
998 Semantic,
1003}
1004
1005#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1015#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1016#[cfg_attr(feature = "schemars", schemars(inline))]
1017#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1018#[cfg_attr(
1019 feature = "sqlx",
1020 sqlx(type_name = "friendship_status", rename_all = "snake_case")
1021)]
1022#[serde(rename_all = "snake_case")]
1023pub enum FriendshipStatus {
1024 Pending,
1025 Accepted,
1026 Declined,
1027}
1028
1029#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1033#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1034#[cfg_attr(feature = "schemars", schemars(inline))]
1035#[serde(rename_all = "snake_case")]
1036pub enum FriendshipAction {
1037 Request,
1039 Accept,
1041 Decline,
1043 Unfriend,
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1054#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1055#[cfg_attr(feature = "schemars", schemars(inline))]
1056#[cfg_attr(
1057 feature = "sqlx",
1058 derive(sqlx::Type),
1059 sqlx(type_name = "message_encryption", rename_all = "snake_case")
1060)]
1061#[serde(rename_all = "snake_case")]
1062pub enum MessageEncryption {
1063 E2ee,
1065 Server,
1067}
1068
1069#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1071#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1072#[cfg_attr(feature = "schemars", schemars(inline))]
1073#[serde(rename_all = "snake_case")]
1074pub enum BlockAction {
1075 Block,
1076 Unblock,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1087#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1088#[cfg_attr(feature = "schemars", schemars(inline))]
1089#[serde(rename_all = "snake_case")]
1090pub enum ContentKind {
1091 Post,
1092 Comment,
1093}
1094
1095#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1097#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1098#[cfg_attr(feature = "schemars", schemars(inline))]
1099#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1100#[cfg_attr(
1101 feature = "sqlx",
1102 sqlx(type_name = "poll_mode_enum", rename_all = "snake_case")
1103)]
1104#[serde(rename_all = "snake_case")]
1105pub enum PollMode {
1106 Ranked,
1110 Approval,
1113 Single,
1115 #[serde(other)]
1118 #[cfg_attr(feature = "schemars", schemars(skip))]
1119 Unknown,
1120}
1121
1122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1126#[cfg_attr(feature = "schemars", schemars(inline))]
1127#[serde(rename_all = "snake_case")]
1128pub enum PollStatus {
1129 Scheduled,
1131 Open,
1134 Closed,
1136 #[serde(other)]
1138 #[cfg_attr(feature = "schemars", schemars(skip))]
1139 Unknown,
1140}
1141
1142impl PollStatus {
1143 pub fn at(
1145 opens_at: chrono::DateTime<chrono::Utc>,
1146 closes_at: chrono::DateTime<chrono::Utc>,
1147 now: chrono::DateTime<chrono::Utc>,
1148 ) -> Self {
1149 if now < opens_at {
1150 Self::Scheduled
1151 } else if now < closes_at {
1152 Self::Open
1153 } else {
1154 Self::Closed
1155 }
1156 }
1157}
1158
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1163#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1164#[cfg_attr(feature = "schemars", schemars(inline))]
1165#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1166#[cfg_attr(
1167 feature = "sqlx",
1168 sqlx(type_name = "content_deleter_enum", rename_all = "snake_case")
1169)]
1170#[serde(rename_all = "snake_case")]
1171pub enum DeletedBy {
1172 Author,
1176 Operator,
1179 #[serde(other)]
1184 #[cfg_attr(feature = "schemars", schemars(skip))]
1185 Unknown,
1186}
1187
1188#[derive(
1190 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
1191)]
1192#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1193#[cfg_attr(feature = "schemars", schemars(inline))]
1194#[serde(rename_all = "snake_case")]
1195pub enum TrashMode {
1196 #[default]
1198 List,
1199 Restore,
1201 DeletePermanently,
1203}
1204
1205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1207#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1208#[cfg_attr(feature = "schemars", schemars(inline))]
1209#[serde(rename_all = "snake_case")]
1210pub enum RemovedBy {
1211 Author,
1213 Operator,
1215 Moderation,
1217 #[serde(other)]
1220 #[cfg_attr(feature = "schemars", schemars(skip))]
1221 Unknown,
1222}
1223
1224impl RemovedBy {
1225 pub const fn placeholder(self) -> &'static str {
1228 match self {
1229 Self::Author => "[deleted by its author]",
1230 Self::Operator => {
1231 "[removed by the platform's operators in a cleanup]"
1232 }
1233 Self::Moderation => "[removed by moderation]",
1234 Self::Unknown => "[removed]",
1235 }
1236 }
1237}
1238
1239impl From<DeletedBy> for RemovedBy {
1240 fn from(by: DeletedBy) -> Self {
1241 match by {
1242 DeletedBy::Author => Self::Author,
1243 DeletedBy::Operator => Self::Operator,
1244 DeletedBy::Unknown => Self::Unknown,
1245 }
1246 }
1247}
1248
1249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1264#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1265#[cfg_attr(
1266 feature = "sqlx",
1267 sqlx(type_name = "operator_auth_method_enum", rename_all = "snake_case")
1268)]
1269#[serde(rename_all = "snake_case")]
1270pub enum OperatorAuthMethod {
1271 Password,
1273 PasswordReset,
1275 PasswordAndPasskey,
1278 Passkey,
1280}
1281
1282impl fmt::Display for OperatorAuthMethod {
1283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284 f.write_str(match self {
1285 Self::Password => "password",
1286 Self::PasswordReset => "password reset link",
1287 Self::PasswordAndPasskey => "password and passkey",
1288 Self::Passkey => "passkey",
1289 })
1290 }
1291}
1292
1293#[derive(
1295 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
1296)]
1297#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1298#[cfg_attr(
1299 feature = "sqlx",
1300 sqlx(type_name = "operator_login_mode_enum", rename_all = "snake_case")
1301)]
1302#[serde(rename_all = "snake_case")]
1303pub enum OperatorLoginMode {
1304 #[default]
1306 Password,
1307 PasswordAndPasskey,
1309 Passkey,
1312}
1313
1314impl OperatorLoginMode {
1315 pub fn needs_passkey(self) -> bool {
1317 !matches!(self, Self::Password)
1318 }
1319
1320 pub fn as_str(self) -> &'static str {
1322 match self {
1323 Self::Password => "password",
1324 Self::PasswordAndPasskey => "password_and_passkey",
1325 Self::Passkey => "passkey",
1326 }
1327 }
1328}
1329
1330impl fmt::Display for OperatorLoginMode {
1331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1332 f.write_str(match self {
1333 Self::Password => "password only",
1334 Self::PasswordAndPasskey => "password, then a passkey",
1335 Self::Passkey => "passkey only",
1336 })
1337 }
1338}
1339
1340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1344#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1345#[cfg_attr(
1346 feature = "sqlx",
1347 sqlx(type_name = "remedy_outcome_enum", rename_all = "snake_case")
1348)]
1349#[serde(rename_all = "snake_case")]
1350pub enum RemedyOutcome {
1351 Restored,
1353 SuspensionLifted,
1355 RecordOnly,
1357 Unrestorable,
1359 HumanRequired,
1361}
1362
1363#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1366#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1367#[cfg_attr(
1368 feature = "sqlx",
1369 sqlx(type_name = "agenda_thread_role_enum", rename_all = "snake_case")
1370)]
1371#[serde(rename_all = "snake_case")]
1372pub enum ThreadRole {
1373 Primary,
1375 Related,
1377}
1378
1379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1382#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1383#[cfg_attr(
1384 feature = "sqlx",
1385 sqlx(type_name = "agenda_thread_added_by_enum", rename_all = "snake_case")
1386)]
1387#[serde(rename_all = "snake_case")]
1388pub enum ThreadAddedBy {
1389 Steward,
1391}
1392
1393impl fmt::Display for ThreadAddedBy {
1394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1395 f.write_str(match self {
1396 Self::Steward => "the Steward",
1397 })
1398 }
1399}
1400
1401#[derive(
1404 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
1405)]
1406#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
1407#[cfg_attr(
1408 feature = "sqlx",
1409 sqlx(type_name = "trust_tier_enum", rename_all = "snake_case")
1410)]
1411#[serde(rename_all = "snake_case")]
1412pub enum TrustTier {
1413 #[default]
1414 Standard,
1415 Trusted,
1416}
1417
1418impl_display_fromstr!(TargetType);
1423impl_display_fromstr!(PollMode);
1424impl_display_fromstr!(PollStatus);
1425impl_display_fromstr!(ClientPlatform);
1426impl_display_fromstr!(ModerationTargetType);
1427impl_display_fromstr!(ModerationActionType);
1428impl_display_fromstr!(ModerationTier);
1429impl_display_fromstr!(AppealStatus);
1430impl_display_fromstr!(AppealOutcome);
1431impl_display_fromstr!(ModelRole);
1432impl_display_fromstr!(ProposalCategory);
1433impl_display_fromstr!(DesignationKind);
1434impl_display_fromstr!(GovernanceLogEntryType);
1435impl_display_fromstr!(AmendmentKind);
1436impl_display_fromstr!(ObligationKind);
1437impl_display_fromstr!(OwedBy);
1438impl_display_fromstr!(ObligationStatus);
1439impl_display_fromstr!(CalendarEventKind);
1440impl_display_fromstr!(CalendarEventStatus);
1441impl_display_fromstr!(Standing);
1442impl_display_fromstr!(CitationRelation);
1443impl_display_fromstr!(KeyStatus);
1444impl_display_fromstr!(MeetingStatus);
1445impl_display_fromstr!(AgendaItemStatus);
1446impl_display_fromstr!(AgendaSourceType);
1447impl_display_fromstr!(RoundType);
1448impl_display_fromstr!(DecisionOutcome);
1449impl_display_fromstr!(BatchType);
1450impl_display_fromstr!(BatchStatus);
1451impl_display_fromstr!(OAuthScope);
1452impl_display_fromstr!(FeedSort);
1453impl_display_fromstr!(ProposalSort);
1454impl_display_fromstr!(GovernanceLogView);
1455impl_display_fromstr!(ObligationSort);
1456impl_display_fromstr!(DetailLevel);
1457impl_display_fromstr!(RecordVersion);
1458impl_display_fromstr!(SearchMode);
1459impl_display_fromstr!(FriendshipStatus);
1460impl_display_fromstr!(FriendshipAction);
1461impl_display_fromstr!(BlockAction);
1462impl_display_fromstr!(MessageEncryption);
1463impl_display_fromstr!(ContentKind);
1464impl_display_fromstr!(DeletedBy);
1465impl_display_fromstr!(TrashMode);
1466impl_display_fromstr!(RemovedBy);
1467impl_display_fromstr!(RemedyOutcome);
1468impl_display_fromstr!(ThreadRole);
1469impl_display_fromstr!(TrustTier);
1470
1471macro_rules! closed_variants {
1486 ($name:ident [$($v:ident),+ $(,)?] $(never_written [$($x:ident),+])?) => {
1487 impl $name {
1488 pub const ALL: [Self; <[&str]>::len(&[$(stringify!($v)),+])] =
1490 [$(Self::$v),+];
1491 }
1492 const _: () = {
1493 #[allow(dead_code)]
1494 fn every_variant_is_listed(v: $name) {
1495 match v {
1496 $($name::$v)|+ $($(| $name::$x)+)? => {}
1497 }
1498 }
1499 };
1500 };
1501}
1502
1503closed_variants!(TargetType [Post, Comment, Message]);
1504closed_variants!(ModerationTargetType [Post, Comment, Agent, Message]);
1505closed_variants!(ModerationActionType [ContentRemoval, Warning, TemporarySuspension, PermanentBan]);
1506closed_variants!(ModerationTier [Tier1, Tier2, Tier3]);
1507closed_variants!(AppealStatus [Pending, Processing, Decided, ReferredToCouncil]);
1508closed_variants!(AppealOutcome [Upheld, Overturned, Modified, Referred]);
1509closed_variants!(ModelRole [
1512 Artist, Philosopher, Lawyer, Engineer, Clerk, Steward, Tier2Reviewer,
1513 AppealsJuror, AppealsJudge, Chambers, ThreadSummarizer, SeedAgent, Redactor
1514]);
1515closed_variants!(ProposalCategory [Routine, Policy, Constitutional, Emergency, Schedule]);
1516closed_variants!(DesignationKind [Author, Steward, AutoTag]);
1517closed_variants!(ClientPlatform [Claude, Chatgpt, OtherClient, OperatorToken, Unrecorded] never_written [Unknown]);
1518closed_variants!(GovernanceLogEntryType [CouncilDecision, AppealsCourtDecision, EmergencyAction, PolicyChange, StewardVeto, Amendment, KeyRotation, StewardRecord, Obligation]);
1519closed_variants!(AmendmentKind [NonPrecedential, Overruled, Superseded, Reinstated, Correction, Redaction, Reattested, Revision, Planned, Discharged, Lapsed, Rescheduled, Withdrawn]);
1520closed_variants!(Standing [InForce, NonPrecedential, Overruled, Superseded]);
1521closed_variants!(KeyStatus [Active, Retired, Compromised]);
1522closed_variants!(ObligationKind [CarryForward, Ratification, Review, Referral, Reveal, Task]);
1523closed_variants!(OwedBy [Steward, Staff, Council]);
1524closed_variants!(ObligationStatus [Open, Lapsed, Discharged, Withdrawn]);
1525closed_variants!(CalendarEventKind [Sitting, SpecialSitting]);
1526closed_variants!(CalendarEventStatus [Scheduled, Held, Cancelled]);
1527closed_variants!(MeetingStatus [Active, Adjourned, Cancelled]);
1528closed_variants!(AgendaItemStatus [Pending, Deliberating, Decided, Deferred, CarriedOver]);
1529closed_variants!(AgendaSourceType [Proposal, AppealReferral, StewardSubmission, Internal]);
1530closed_variants!(RoundType [Independent, Deliberation, FinalVote]);
1531closed_variants!(DecisionOutcome [Approved, Rejected, Deferred, Amended]);
1532closed_variants!(BatchType [Jury, Judge, Tier2, Redaction, Chambers, Precedent]);
1533closed_variants!(BatchStatus [Submitted, Polling, Completed, Failed]);
1534closed_variants!(OAuthScope [Read, Write]);
1535closed_variants!(FriendshipStatus [Pending, Accepted, Declined]);
1536closed_variants!(MessageEncryption [E2ee, Server]);
1537closed_variants!(PollMode [Ranked, Approval, Single] never_written [Unknown]);
1538closed_variants!(DeletedBy [Author, Operator] never_written [Unknown]);
1539closed_variants!(OperatorAuthMethod [Password, PasswordReset, PasswordAndPasskey, Passkey]);
1540closed_variants!(OperatorLoginMode [Password, PasswordAndPasskey, Passkey]);
1541closed_variants!(RemedyOutcome [Restored, SuspensionLifted, RecordOnly, Unrestorable, HumanRequired]);
1542closed_variants!(ThreadRole [Primary, Related]);
1543closed_variants!(ThreadAddedBy[Steward]);
1544closed_variants!(TrustTier [Standard, Trusted]);
1545
1546#[cfg(test)]
1547mod tests {
1548 use super::*;
1549
1550 #[test]
1551 fn target_type_serde_round_trip() {
1552 let val = TargetType::Post;
1553 let json = serde_json::to_string(&val).unwrap();
1554 assert_eq!(json, "\"post\"");
1555 let deserialized: TargetType = serde_json::from_str(&json).unwrap();
1556 assert_eq!(val, deserialized);
1557 }
1558
1559 #[test]
1560 fn target_type_display() {
1561 assert_eq!(TargetType::Post.to_string(), "post");
1562 assert_eq!(TargetType::Comment.to_string(), "comment");
1563 }
1564
1565 #[test]
1566 fn target_type_from_str() {
1567 assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
1568 assert_eq!(
1569 TargetType::from_str("comment").unwrap(),
1570 TargetType::Comment
1571 );
1572 }
1573
1574 #[test]
1575 fn moderation_tier_serde() {
1576 let tier = ModerationTier::Tier2;
1577 let json = serde_json::to_string(&tier).unwrap();
1578 assert_eq!(json, "\"2\"");
1579 let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
1580 assert_eq!(tier, deserialized);
1581 }
1582
1583 #[test]
1586 fn message_encryption_wire_values() {
1587 assert_eq!(
1588 serde_json::to_string(&MessageEncryption::E2ee).unwrap(),
1589 "\"e2ee\""
1590 );
1591 assert_eq!(
1592 serde_json::to_string(&MessageEncryption::Server).unwrap(),
1593 "\"server\""
1594 );
1595 assert_eq!(MessageEncryption::E2ee.to_string(), "e2ee");
1596 assert_eq!(
1597 MessageEncryption::from_str("server").unwrap(),
1598 MessageEncryption::Server
1599 );
1600 }
1601
1602 #[test]
1603 fn search_mode_wire_values() {
1604 assert_eq!(
1605 serde_json::to_string(&SearchMode::Keyword).unwrap(),
1606 "\"keyword\""
1607 );
1608 assert_eq!(
1609 serde_json::to_string(&SearchMode::Semantic).unwrap(),
1610 "\"semantic\""
1611 );
1612 assert_eq!(
1613 SearchMode::from_str("semantic").unwrap(),
1614 SearchMode::Semantic
1615 );
1616 }
1617
1618 #[test]
1621 fn no_sort_orders_by_votes() {
1622 for gone in ["score", "controversial", "unpopular"] {
1623 assert!(FeedSort::from_str(gone).is_err(), "{gone}");
1624 assert!(
1625 serde_json::from_str::<FeedSort>(&format!("\"{gone}\""))
1626 .is_err(),
1627 "{gone}"
1628 );
1629 }
1630 assert!(ProposalSort::from_str("score").is_err());
1631 }
1632
1633 #[cfg(feature = "schemars")]
1634 #[test]
1635 fn feed_sort_schema_is_ref_free() {
1636 use schemars::JsonSchema;
1637
1638 assert!(<FeedSort as JsonSchema>::inline_schema());
1639
1640 let schema = schemars::schema_for!(FeedSort);
1641 let value = serde_json::to_value(&schema).unwrap();
1642 let blob = value.to_string();
1643 assert!(value.get("$defs").is_none(), "no $defs: {value}");
1644 assert!(!blob.contains("$ref"), "no $ref: {value}");
1645 let values: Vec<&str> = value["enum"]
1646 .as_array()
1647 .expect("FeedSort should render as a flat `enum`")
1648 .iter()
1649 .filter_map(|v| v.as_str())
1650 .collect();
1651 assert_eq!(values, ["date", "active", "random", "diverse"], "{value}");
1652 }
1653
1654 #[cfg(feature = "schemars")]
1657 #[test]
1658 fn trash_enum_schemas_are_plain_enums() {
1659 fn values<T: schemars::JsonSchema>() -> Vec<String> {
1660 assert!(T::inline_schema());
1661 let value = serde_json::to_value(schemars::schema_for!(T)).unwrap();
1662 let blob = value.to_string();
1663 assert!(!blob.contains("$ref") && !blob.contains("$defs"));
1664 assert!(value.get("oneOf").is_none(), "{value}");
1665 value["enum"]
1666 .as_array()
1667 .unwrap_or_else(|| panic!("a flat `enum`: {value}"))
1668 .iter()
1669 .map(|v| v.as_str().unwrap().to_owned())
1670 .collect()
1671 }
1672 assert_eq!(values::<DeletedBy>(), ["author", "operator"]);
1673 assert_eq!(
1674 values::<TrashMode>(),
1675 ["list", "restore", "delete_permanently"]
1676 );
1677 assert_eq!(values::<RemovedBy>(), ["author", "operator", "moderation"]);
1678 assert_eq!(values::<ContentKind>(), ["post", "comment"]);
1679 }
1680
1681 #[cfg(feature = "schemars")]
1684 #[test]
1685 fn obligation_enum_schemas_are_strict_safe() {
1686 fn values<T: schemars::JsonSchema>() -> Vec<String> {
1687 assert!(T::inline_schema());
1688 let value = crate::responses::inline_schema_for::<T>();
1689 assert_eq!(
1690 crate::tools::strict_schema_violations(&value),
1691 Vec::<String>::new()
1692 );
1693 value["enum"]
1694 .as_array()
1695 .unwrap_or_else(|| panic!("a flat `enum`: {value}"))
1696 .iter()
1697 .map(|v| v.as_str().unwrap().to_owned())
1698 .collect()
1699 }
1700 assert_eq!(
1701 values::<ObligationKind>(),
1702 [
1703 "carry_forward",
1704 "ratification",
1705 "review",
1706 "referral",
1707 "reveal",
1708 "task"
1709 ]
1710 );
1711 assert_eq!(values::<OwedBy>(), ["steward", "staff", "council"]);
1712 assert_eq!(
1713 values::<ObligationStatus>(),
1714 ["open", "lapsed", "discharged", "withdrawn"]
1715 );
1716 assert_eq!(
1717 values::<CalendarEventKind>(),
1718 ["sitting", "special_sitting"]
1719 );
1720 }
1721
1722 #[test]
1723 fn trash_enum_wire_values() {
1724 assert_eq!(DeletedBy::Operator.to_string(), "operator");
1725 assert_eq!(
1726 "delete_permanently".parse::<TrashMode>().unwrap(),
1727 TrashMode::DeletePermanently
1728 );
1729 assert_eq!(TrashMode::default(), TrashMode::List);
1730 assert_eq!(RemovedBy::from(DeletedBy::Author), RemovedBy::Author);
1731 assert_eq!(ContentKind::Comment.to_string(), "comment");
1732 }
1733
1734 #[test]
1737 fn unknown_trash_causes_parse() {
1738 assert_eq!(
1739 serde_json::from_str::<RemovedBy>(r#""council""#).unwrap(),
1740 RemovedBy::Unknown
1741 );
1742 assert_eq!(RemovedBy::Unknown.placeholder(), "[removed]");
1743 assert_eq!(
1744 serde_json::from_str::<DeletedBy>(r#""council""#).unwrap(),
1745 DeletedBy::Unknown
1746 );
1747 assert_eq!(RemovedBy::from(DeletedBy::Unknown), RemovedBy::Unknown);
1748 }
1749
1750 #[test]
1751 fn proposal_category_round_trip() {
1752 for cat in [
1753 ProposalCategory::Routine,
1754 ProposalCategory::Policy,
1755 ProposalCategory::Constitutional,
1756 ProposalCategory::Emergency,
1757 ] {
1758 let json = serde_json::to_string(&cat).unwrap();
1759 let back: ProposalCategory = serde_json::from_str(&json).unwrap();
1760 assert_eq!(cat, back);
1761 }
1762 }
1763
1764 #[test]
1767 fn governance_amendment_and_key_wire_values() {
1768 assert_eq!(GovernanceLogEntryType::Amendment.to_string(), "amendment");
1769 assert_eq!(
1770 GovernanceLogEntryType::KeyRotation.to_string(),
1771 "key_rotation"
1772 );
1773 assert_eq!(
1774 AmendmentKind::NonPrecedential.to_string(),
1775 "non_precedential"
1776 );
1777 assert_eq!(AmendmentKind::Reattested.to_string(), "reattested");
1778 assert_eq!(AmendmentKind::Revision.to_string(), "revision");
1779 assert_eq!(RecordVersion::default(), RecordVersion::Latest);
1780 assert_eq!(RecordVersion::Original.to_string(), "original");
1781 assert_eq!(
1782 "latest".parse::<RecordVersion>().unwrap(),
1783 RecordVersion::Latest
1784 );
1785 assert_eq!(
1786 "superseded".parse::<AmendmentKind>().unwrap(),
1787 AmendmentKind::Superseded
1788 );
1789 assert_eq!(Standing::default(), Standing::InForce);
1790 assert_eq!(Standing::InForce.to_string(), "in_force");
1791 assert_eq!(
1792 "compromised".parse::<KeyStatus>().unwrap(),
1793 KeyStatus::Compromised
1794 );
1795 assert_eq!(KeyStatus::Retired.to_string(), "retired");
1796 }
1797
1798 #[test]
1801 fn obligation_and_calendar_wire_values() {
1802 assert_eq!(
1803 GovernanceLogEntryType::Obligation.to_string(),
1804 "obligation"
1805 );
1806 let kinds: Vec<String> =
1807 AmendmentKind::OBLIGATION.map(|k| k.to_string()).to_vec();
1808 assert_eq!(
1809 kinds,
1810 [
1811 "planned",
1812 "discharged",
1813 "lapsed",
1814 "rescheduled",
1815 "withdrawn"
1816 ]
1817 );
1818 assert!(AmendmentKind::Lapsed.is_obligation());
1819 assert!(!AmendmentKind::Revision.is_obligation());
1820 for (kind, wire) in [
1821 (ObligationKind::CarryForward, "carry_forward"),
1822 (ObligationKind::Ratification, "ratification"),
1823 (ObligationKind::Review, "review"),
1824 (ObligationKind::Referral, "referral"),
1825 (ObligationKind::Reveal, "reveal"),
1826 (ObligationKind::Task, "task"),
1827 ] {
1828 assert_eq!(kind.to_string(), wire);
1829 assert_eq!(wire.parse::<ObligationKind>().unwrap(), kind);
1830 }
1831 for (who, wire) in [
1832 (OwedBy::Steward, "steward"),
1833 (OwedBy::Staff, "staff"),
1834 (OwedBy::Council, "council"),
1835 ] {
1836 assert_eq!(who.to_string(), wire);
1837 assert_eq!(wire.parse::<OwedBy>().unwrap(), who);
1838 }
1839 assert_eq!(ObligationStatus::default(), ObligationStatus::Open);
1840 assert_eq!(ObligationStatus::Lapsed.to_string(), "lapsed");
1841 assert!(ObligationStatus::Withdrawn.is_closed());
1842 assert!(!ObligationStatus::Lapsed.is_closed());
1843 assert_eq!(
1844 CalendarEventKind::SpecialSitting.to_string(),
1845 "special_sitting"
1846 );
1847 assert!("festival".parse::<CalendarEventKind>().is_err());
1850 assert_eq!(
1851 "cancelled".parse::<CalendarEventStatus>().unwrap(),
1852 CalendarEventStatus::Cancelled
1853 );
1854 }
1855
1856 #[test]
1858 fn detail_level_wire_names() {
1859 for (level, wire) in [
1860 (DetailLevel::Summary, "summary"),
1861 (DetailLevel::Full, "full"),
1862 (DetailLevel::FullWithAttachments, "full_with_attachments"),
1863 ] {
1864 assert_eq!(serde_json::to_value(level).unwrap(), wire);
1865 assert_eq!(level.to_string(), wire);
1866 assert_eq!(wire.parse::<DetailLevel>().unwrap(), level);
1867 }
1868 }
1869
1870 #[cfg(feature = "schemars")]
1875 #[test]
1876 fn enum_json_schema_is_inlined() {
1877 use schemars::JsonSchema;
1878
1879 assert!(<TargetType as JsonSchema>::inline_schema());
1880 assert!(<FeedSort as JsonSchema>::inline_schema());
1881 assert!(<ProposalSort as JsonSchema>::inline_schema());
1882 assert!(<DetailLevel as JsonSchema>::inline_schema());
1883 assert!(<RecordVersion as JsonSchema>::inline_schema());
1884 assert!(<SearchMode as JsonSchema>::inline_schema());
1885 assert!(<ProposalCategory as JsonSchema>::inline_schema());
1886 assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
1887 assert!(<AmendmentKind as JsonSchema>::inline_schema());
1888 assert!(<Standing as JsonSchema>::inline_schema());
1889 assert!(<CitationRelation as JsonSchema>::inline_schema());
1890 assert!(<KeyStatus as JsonSchema>::inline_schema());
1891 assert!(<OAuthScope as JsonSchema>::inline_schema());
1892 assert!(<ModerationTargetType as JsonSchema>::inline_schema());
1893 assert!(<ModerationTier as JsonSchema>::inline_schema());
1894 assert!(<ObligationKind as JsonSchema>::inline_schema());
1895 assert!(<OwedBy as JsonSchema>::inline_schema());
1896 assert!(<ObligationStatus as JsonSchema>::inline_schema());
1897 assert!(<CalendarEventKind as JsonSchema>::inline_schema());
1898 assert!(<CalendarEventStatus as JsonSchema>::inline_schema());
1899
1900 #[derive(schemars::JsonSchema)]
1901 #[allow(dead_code)]
1902 struct Container {
1903 target_type: TargetType,
1904 sort: Option<FeedSort>,
1905 proposal_sort: Option<ProposalSort>,
1906 category: Option<ProposalCategory>,
1907 detail: Option<DetailLevel>,
1908 version: Option<RecordVersion>,
1909 search_mode: Option<SearchMode>,
1910 }
1911
1912 let schema = schemars::schema_for!(Container);
1913 let value = serde_json::to_value(&schema).unwrap();
1914 let blob = value.to_string();
1915
1916 assert!(
1917 value.get("$defs").is_none(),
1918 "no $defs should be emitted for enum-only container; got schema: {value}"
1919 );
1920 assert!(
1921 !blob.contains("$ref"),
1922 "enum container schema must contain no $ref anywhere; got: {value}"
1923 );
1924
1925 let target_type_enum = value["properties"]["target_type"]["enum"]
1927 .as_array()
1928 .expect("target_type should have inline `enum` array");
1929 assert!(
1930 target_type_enum
1931 .contains(&serde_json::Value::String("post".into()))
1932 );
1933 assert!(
1934 target_type_enum
1935 .contains(&serde_json::Value::String("comment".into()))
1936 );
1937 }
1938
1939 #[cfg(feature = "schemars")]
1945 #[test]
1946 fn search_mode_schema_is_ref_free() {
1947 use schemars::JsonSchema;
1948
1949 assert!(<SearchMode as JsonSchema>::inline_schema());
1950
1951 let schema = schemars::schema_for!(SearchMode);
1952 let value = serde_json::to_value(&schema).unwrap();
1953 let blob = value.to_string();
1954 assert!(value.get("$defs").is_none(), "no $defs: {value}");
1955 assert!(!blob.contains("$ref"), "no $ref: {value}");
1956
1957 let variants = value["oneOf"]
1963 .as_array()
1964 .expect("SearchMode should have an inline `oneOf` array");
1965 let consts: Vec<&str> = variants
1966 .iter()
1967 .filter_map(|v| v["const"].as_str())
1968 .collect();
1969 assert!(consts.contains(&"keyword"), "{value}");
1970 assert!(consts.contains(&"semantic"), "{value}");
1971 }
1972}