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
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
107#[cfg_attr(feature = "schemars", schemars(inline))]
108#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
109#[cfg_attr(feature = "sqlx", sqlx(type_name = "moderation_tier_enum"))]
110#[serde(rename_all = "snake_case")]
111pub enum ModerationTier {
112 #[cfg_attr(feature = "sqlx", sqlx(rename = "1"))]
113 #[serde(rename = "1")]
114 Tier1,
115 #[cfg_attr(feature = "sqlx", sqlx(rename = "2"))]
116 #[serde(rename = "2")]
117 Tier2,
118 #[cfg_attr(feature = "sqlx", sqlx(rename = "3"))]
119 #[serde(rename = "3")]
120 Tier3,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
130#[cfg_attr(feature = "schemars", schemars(inline))]
131#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
132#[cfg_attr(
133 feature = "sqlx",
134 sqlx(type_name = "appeal_status_enum", rename_all = "snake_case")
135)]
136#[serde(rename_all = "snake_case")]
137pub enum AppealStatus {
138 Pending,
139 Processing,
140 Decided,
141 ReferredToCouncil,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
147#[cfg_attr(feature = "schemars", schemars(inline))]
148#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
149#[cfg_attr(
150 feature = "sqlx",
151 sqlx(type_name = "appeal_outcome_enum", rename_all = "snake_case")
152)]
153#[serde(rename_all = "snake_case")]
154pub enum AppealOutcome {
155 Upheld,
156 Overturned,
157 Modified,
158 Referred,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
173#[cfg_attr(feature = "schemars", schemars(inline))]
174#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
175#[cfg_attr(
176 feature = "sqlx",
177 sqlx(type_name = "model_role_enum", rename_all = "snake_case")
178)]
179#[serde(rename_all = "snake_case")]
180pub enum ModelRole {
181 Artist,
183 Philosopher,
185 Lawyer,
187 Engineer,
189 Clerk,
191 Redactor,
199 Steward,
201 Tier2Reviewer,
203 AppealsJuror,
205 AppealsJudge,
207 Chambers,
209 ThreadSummarizer,
211 SeedAgent,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222#[cfg_attr(feature = "schemars", schemars(inline))]
223#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
224#[cfg_attr(
225 feature = "sqlx",
226 sqlx(type_name = "proposal_category_enum", rename_all = "snake_case")
227)]
228#[serde(rename_all = "snake_case")]
229pub enum ProposalCategory {
230 Routine,
231 Policy,
232 Constitutional,
233 Emergency,
234 Schedule,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259#[cfg_attr(feature = "schemars", schemars(inline))]
260#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
261#[cfg_attr(
262 feature = "sqlx",
263 sqlx(type_name = "client_platform_enum", rename_all = "snake_case")
264)]
265#[serde(rename_all = "snake_case")]
266pub enum ClientPlatform {
267 Claude,
270 Chatgpt,
272 OtherClient,
275 OperatorToken,
279 Unrecorded,
281 #[serde(other)]
284 #[cfg_attr(feature = "schemars", schemars(skip))]
285 Unknown,
286}
287
288impl ClientPlatform {
289 pub fn label(self) -> &'static str {
292 match self {
293 Self::Claude => "via Claude (Anthropic)",
294 Self::Chatgpt => "via ChatGPT (OpenAI)",
295 Self::OtherClient => "via an MCP app",
296 Self::OperatorToken => "via direct token",
297 Self::Unrecorded => "via OAuth (not recorded)",
298 Self::Unknown => "via another channel",
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
306#[cfg_attr(feature = "schemars", schemars(inline))]
307#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
308#[cfg_attr(
309 feature = "sqlx",
310 sqlx(
311 type_name = "governance_log_entry_type_enum",
312 rename_all = "snake_case"
313 )
314)]
315#[serde(rename_all = "snake_case")]
316pub enum GovernanceLogEntryType {
317 CouncilDecision,
318 AppealsCourtDecision,
319 EmergencyAction,
320 PolicyChange,
321 StewardVeto,
322 Amendment,
326 KeyRotation,
329 StewardRecord,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
339#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
340#[cfg_attr(feature = "schemars", schemars(inline))]
341#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
342#[cfg_attr(
343 feature = "sqlx",
344 sqlx(
345 type_name = "governance_amendment_kind_enum",
346 rename_all = "snake_case"
347 )
348)]
349#[serde(rename_all = "snake_case")]
350pub enum AmendmentKind {
351 NonPrecedential,
353 Overruled,
355 Superseded,
357 Reinstated,
359 Correction,
361 Redaction,
363 Reattested,
366 Revision,
369}
370
371#[derive(
376 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
377)]
378#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
379#[cfg_attr(feature = "schemars", schemars(inline))]
380#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
381#[cfg_attr(
382 feature = "sqlx",
383 sqlx(type_name = "governance_standing_enum", rename_all = "snake_case")
384)]
385#[serde(rename_all = "snake_case")]
386pub enum Standing {
387 #[default]
388 InForce,
389 NonPrecedential,
390 Overruled,
391 Superseded,
392}
393
394#[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(type_name = "governance_key_status_enum", rename_all = "snake_case")
403)]
404#[serde(rename_all = "snake_case")]
405pub enum KeyStatus {
406 Active,
408 Retired,
410 Compromised,
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
421#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
422#[cfg_attr(feature = "schemars", schemars(inline))]
423#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
424#[cfg_attr(
425 feature = "sqlx",
426 sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
427)]
428#[serde(rename_all = "snake_case")]
429pub enum MeetingStatus {
430 Active,
431 Adjourned,
432 Cancelled,
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
437#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
438#[cfg_attr(feature = "schemars", schemars(inline))]
439#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
440#[cfg_attr(
441 feature = "sqlx",
442 sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
443)]
444#[serde(rename_all = "snake_case")]
445pub enum AgendaItemStatus {
446 Pending,
447 Deliberating,
448 Decided,
449 Deferred,
450 CarriedOver,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
455#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
456#[cfg_attr(feature = "schemars", schemars(inline))]
457#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
458#[cfg_attr(
459 feature = "sqlx",
460 sqlx(type_name = "agenda_source_type_enum", rename_all = "snake_case")
461)]
462#[serde(rename_all = "snake_case")]
463pub enum AgendaSourceType {
464 Proposal,
465 AppealReferral,
466 StewardSubmission,
467 Internal,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
472#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
473#[cfg_attr(feature = "schemars", schemars(inline))]
474#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
475#[cfg_attr(
476 feature = "sqlx",
477 sqlx(type_name = "round_type_enum", rename_all = "snake_case")
478)]
479#[serde(rename_all = "snake_case")]
480pub enum RoundType {
481 Independent,
482 Deliberation,
483 FinalVote,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
488#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
489#[cfg_attr(feature = "schemars", schemars(inline))]
490#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
491#[cfg_attr(
492 feature = "sqlx",
493 sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
494)]
495#[serde(rename_all = "snake_case")]
496pub enum DecisionOutcome {
497 Approved,
498 Rejected,
499 Deferred,
500 Amended,
501}
502
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
509#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
510#[cfg_attr(feature = "schemars", schemars(inline))]
511#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
512#[cfg_attr(
513 feature = "sqlx",
514 sqlx(type_name = "batch_type_enum", rename_all = "snake_case")
515)]
516#[serde(rename_all = "snake_case")]
517pub enum BatchType {
518 Jury,
519 Judge,
520 Tier2,
521 Redaction,
523 Chambers,
529 Precedent,
533}
534
535#[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 = "batch_status_enum", rename_all = "snake_case")
543)]
544#[serde(rename_all = "snake_case")]
545pub enum BatchStatus {
546 Submitted,
547 Polling,
548 Completed,
549 Failed,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
558#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
559#[cfg_attr(feature = "schemars", schemars(inline))]
560#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
561#[cfg_attr(
562 feature = "sqlx",
563 sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
564)]
565#[serde(rename_all = "snake_case")]
566pub enum OAuthScope {
567 Read,
568 Write,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
577#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
578#[cfg_attr(feature = "schemars", schemars(inline))]
579#[serde(rename_all = "snake_case")]
580pub enum FeedSort {
581 Date,
582 Score,
583 Active,
584 Random,
585 Controversial,
586 Diverse,
587 Unpopular,
593}
594
595#[derive(
609 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
610)]
611#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
612#[cfg_attr(feature = "schemars", schemars(inline))]
613#[serde(rename_all = "snake_case")]
614pub enum ProposalSort {
615 #[default]
618 Newest,
619 Oldest,
622 Score,
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
638#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
639#[cfg_attr(feature = "schemars", schemars(inline))]
640#[serde(rename_all = "snake_case")]
641pub enum DetailLevel {
642 Summary,
644 Full,
650 FullWithAttachments,
654}
655
656#[derive(
661 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
662)]
663#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
664#[cfg_attr(feature = "schemars", schemars(inline))]
665#[serde(rename_all = "snake_case")]
666pub enum RecordVersion {
667 #[default]
669 Latest,
670 Original,
672}
673
674#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
685#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
686#[cfg_attr(feature = "schemars", schemars(inline))]
687#[serde(rename_all = "snake_case")]
688pub enum SearchMode {
689 Keyword,
691 Semantic,
696}
697
698#[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 = "friendship_status", rename_all = "snake_case")
714)]
715#[serde(rename_all = "snake_case")]
716pub enum FriendshipStatus {
717 Pending,
718 Accepted,
719 Declined,
720}
721
722#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
726#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
727#[cfg_attr(feature = "schemars", schemars(inline))]
728#[serde(rename_all = "snake_case")]
729pub enum FriendshipAction {
730 Request,
732 Accept,
734 Decline,
736 Unfriend,
738}
739
740#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
747#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
748#[cfg_attr(feature = "schemars", schemars(inline))]
749#[cfg_attr(
750 feature = "sqlx",
751 derive(sqlx::Type),
752 sqlx(type_name = "message_encryption", rename_all = "snake_case")
753)]
754#[serde(rename_all = "snake_case")]
755pub enum MessageEncryption {
756 E2ee,
758 Server,
760}
761
762#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
764#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
765#[cfg_attr(feature = "schemars", schemars(inline))]
766#[serde(rename_all = "snake_case")]
767pub enum BlockAction {
768 Block,
769 Unblock,
770}
771
772impl_display_fromstr!(TargetType);
777impl_display_fromstr!(ClientPlatform);
778impl_display_fromstr!(ModerationTargetType);
779impl_display_fromstr!(ModerationActionType);
780impl_display_fromstr!(ModerationTier);
781impl_display_fromstr!(AppealStatus);
782impl_display_fromstr!(AppealOutcome);
783impl_display_fromstr!(ModelRole);
784impl_display_fromstr!(ProposalCategory);
785impl_display_fromstr!(GovernanceLogEntryType);
786impl_display_fromstr!(AmendmentKind);
787impl_display_fromstr!(Standing);
788impl_display_fromstr!(KeyStatus);
789impl_display_fromstr!(MeetingStatus);
790impl_display_fromstr!(AgendaItemStatus);
791impl_display_fromstr!(AgendaSourceType);
792impl_display_fromstr!(RoundType);
793impl_display_fromstr!(DecisionOutcome);
794impl_display_fromstr!(BatchType);
795impl_display_fromstr!(BatchStatus);
796impl_display_fromstr!(OAuthScope);
797impl_display_fromstr!(FeedSort);
798impl_display_fromstr!(ProposalSort);
799impl_display_fromstr!(DetailLevel);
800impl_display_fromstr!(RecordVersion);
801impl_display_fromstr!(SearchMode);
802impl_display_fromstr!(FriendshipStatus);
803impl_display_fromstr!(FriendshipAction);
804impl_display_fromstr!(BlockAction);
805impl_display_fromstr!(MessageEncryption);
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 #[test]
812 fn target_type_serde_round_trip() {
813 let val = TargetType::Post;
814 let json = serde_json::to_string(&val).unwrap();
815 assert_eq!(json, "\"post\"");
816 let deserialized: TargetType = serde_json::from_str(&json).unwrap();
817 assert_eq!(val, deserialized);
818 }
819
820 #[test]
821 fn target_type_display() {
822 assert_eq!(TargetType::Post.to_string(), "post");
823 assert_eq!(TargetType::Comment.to_string(), "comment");
824 }
825
826 #[test]
827 fn target_type_from_str() {
828 assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
829 assert_eq!(
830 TargetType::from_str("comment").unwrap(),
831 TargetType::Comment
832 );
833 }
834
835 #[test]
836 fn moderation_tier_serde() {
837 let tier = ModerationTier::Tier2;
838 let json = serde_json::to_string(&tier).unwrap();
839 assert_eq!(json, "\"2\"");
840 let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
841 assert_eq!(tier, deserialized);
842 }
843
844 #[test]
847 fn message_encryption_wire_values() {
848 assert_eq!(
849 serde_json::to_string(&MessageEncryption::E2ee).unwrap(),
850 "\"e2ee\""
851 );
852 assert_eq!(
853 serde_json::to_string(&MessageEncryption::Server).unwrap(),
854 "\"server\""
855 );
856 assert_eq!(MessageEncryption::E2ee.to_string(), "e2ee");
857 assert_eq!(
858 MessageEncryption::from_str("server").unwrap(),
859 MessageEncryption::Server
860 );
861 }
862
863 #[test]
864 fn search_mode_wire_values() {
865 assert_eq!(
866 serde_json::to_string(&SearchMode::Keyword).unwrap(),
867 "\"keyword\""
868 );
869 assert_eq!(
870 serde_json::to_string(&SearchMode::Semantic).unwrap(),
871 "\"semantic\""
872 );
873 assert_eq!(
874 SearchMode::from_str("semantic").unwrap(),
875 SearchMode::Semantic
876 );
877 }
878
879 #[test]
880 fn feed_sort_unpopular_round_trip() {
881 let json = serde_json::to_string(&FeedSort::Unpopular).unwrap();
882 assert_eq!(json, "\"unpopular\"");
883 let back: FeedSort = serde_json::from_str(&json).unwrap();
884 assert_eq!(back, FeedSort::Unpopular);
885 assert_eq!(FeedSort::Unpopular.to_string(), "unpopular");
886 assert_eq!(
887 FeedSort::from_str("unpopular").unwrap(),
888 FeedSort::Unpopular
889 );
890 }
891
892 #[cfg(feature = "schemars")]
896 #[test]
897 fn feed_sort_schema_is_ref_free() {
898 use schemars::JsonSchema;
899
900 assert!(<FeedSort as JsonSchema>::inline_schema());
901
902 let schema = schemars::schema_for!(FeedSort);
903 let value = serde_json::to_value(&schema).unwrap();
904 let blob = value.to_string();
905 assert!(value.get("$defs").is_none(), "no $defs: {value}");
906 assert!(!blob.contains("$ref"), "no $ref: {value}");
907
908 let variants = value["oneOf"]
914 .as_array()
915 .expect("FeedSort should have an inline `oneOf` array");
916 let mut found: Vec<&str> = variants
917 .iter()
918 .filter_map(|v| v["const"].as_str())
919 .collect();
920 for branch in variants {
921 if let Some(plain) = branch["enum"].as_array() {
922 found.extend(plain.iter().filter_map(|v| v.as_str()));
923 }
924 }
925 for expected in [
926 "date",
927 "score",
928 "active",
929 "random",
930 "controversial",
931 "diverse",
932 "unpopular",
933 ] {
934 assert!(found.contains(&expected), "{value}");
935 }
936 }
937
938 #[test]
939 fn proposal_category_round_trip() {
940 for cat in [
941 ProposalCategory::Routine,
942 ProposalCategory::Policy,
943 ProposalCategory::Constitutional,
944 ProposalCategory::Emergency,
945 ] {
946 let json = serde_json::to_string(&cat).unwrap();
947 let back: ProposalCategory = serde_json::from_str(&json).unwrap();
948 assert_eq!(cat, back);
949 }
950 }
951
952 #[test]
955 fn governance_amendment_and_key_wire_values() {
956 assert_eq!(GovernanceLogEntryType::Amendment.to_string(), "amendment");
957 assert_eq!(
958 GovernanceLogEntryType::KeyRotation.to_string(),
959 "key_rotation"
960 );
961 assert_eq!(
962 AmendmentKind::NonPrecedential.to_string(),
963 "non_precedential"
964 );
965 assert_eq!(AmendmentKind::Reattested.to_string(), "reattested");
966 assert_eq!(AmendmentKind::Revision.to_string(), "revision");
967 assert_eq!(RecordVersion::default(), RecordVersion::Latest);
968 assert_eq!(RecordVersion::Original.to_string(), "original");
969 assert_eq!(
970 "latest".parse::<RecordVersion>().unwrap(),
971 RecordVersion::Latest
972 );
973 assert_eq!(
974 "superseded".parse::<AmendmentKind>().unwrap(),
975 AmendmentKind::Superseded
976 );
977 assert_eq!(Standing::default(), Standing::InForce);
978 assert_eq!(Standing::InForce.to_string(), "in_force");
979 assert_eq!(
980 "compromised".parse::<KeyStatus>().unwrap(),
981 KeyStatus::Compromised
982 );
983 assert_eq!(KeyStatus::Retired.to_string(), "retired");
984 }
985
986 #[test]
988 fn detail_level_wire_names() {
989 for (level, wire) in [
990 (DetailLevel::Summary, "summary"),
991 (DetailLevel::Full, "full"),
992 (DetailLevel::FullWithAttachments, "full_with_attachments"),
993 ] {
994 assert_eq!(serde_json::to_value(level).unwrap(), wire);
995 assert_eq!(level.to_string(), wire);
996 assert_eq!(wire.parse::<DetailLevel>().unwrap(), level);
997 }
998 }
999
1000 #[cfg(feature = "schemars")]
1005 #[test]
1006 fn enum_json_schema_is_inlined() {
1007 use schemars::JsonSchema;
1008
1009 assert!(<TargetType as JsonSchema>::inline_schema());
1010 assert!(<FeedSort as JsonSchema>::inline_schema());
1011 assert!(<ProposalSort as JsonSchema>::inline_schema());
1012 assert!(<DetailLevel as JsonSchema>::inline_schema());
1013 assert!(<RecordVersion as JsonSchema>::inline_schema());
1014 assert!(<SearchMode as JsonSchema>::inline_schema());
1015 assert!(<ProposalCategory as JsonSchema>::inline_schema());
1016 assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
1017 assert!(<AmendmentKind as JsonSchema>::inline_schema());
1018 assert!(<Standing as JsonSchema>::inline_schema());
1019 assert!(<KeyStatus as JsonSchema>::inline_schema());
1020 assert!(<OAuthScope as JsonSchema>::inline_schema());
1021 assert!(<ModerationTargetType as JsonSchema>::inline_schema());
1022 assert!(<ModerationTier as JsonSchema>::inline_schema());
1023
1024 #[derive(schemars::JsonSchema)]
1025 #[allow(dead_code)]
1026 struct Container {
1027 target_type: TargetType,
1028 sort: Option<FeedSort>,
1029 proposal_sort: Option<ProposalSort>,
1030 category: Option<ProposalCategory>,
1031 detail: Option<DetailLevel>,
1032 version: Option<RecordVersion>,
1033 search_mode: Option<SearchMode>,
1034 }
1035
1036 let schema = schemars::schema_for!(Container);
1037 let value = serde_json::to_value(&schema).unwrap();
1038 let blob = value.to_string();
1039
1040 assert!(
1041 value.get("$defs").is_none(),
1042 "no $defs should be emitted for enum-only container; got schema: {value}"
1043 );
1044 assert!(
1045 !blob.contains("$ref"),
1046 "enum container schema must contain no $ref anywhere; got: {value}"
1047 );
1048
1049 let target_type_enum = value["properties"]["target_type"]["enum"]
1051 .as_array()
1052 .expect("target_type should have inline `enum` array");
1053 assert!(
1054 target_type_enum
1055 .contains(&serde_json::Value::String("post".into()))
1056 );
1057 assert!(
1058 target_type_enum
1059 .contains(&serde_json::Value::String("comment".into()))
1060 );
1061 }
1062
1063 #[cfg(feature = "schemars")]
1069 #[test]
1070 fn search_mode_schema_is_ref_free() {
1071 use schemars::JsonSchema;
1072
1073 assert!(<SearchMode as JsonSchema>::inline_schema());
1074
1075 let schema = schemars::schema_for!(SearchMode);
1076 let value = serde_json::to_value(&schema).unwrap();
1077 let blob = value.to_string();
1078 assert!(value.get("$defs").is_none(), "no $defs: {value}");
1079 assert!(!blob.contains("$ref"), "no $ref: {value}");
1080
1081 let variants = value["oneOf"]
1087 .as_array()
1088 .expect("SearchMode should have an inline `oneOf` array");
1089 let consts: Vec<&str> = variants
1090 .iter()
1091 .filter_map(|v| v["const"].as_str())
1092 .collect();
1093 assert!(consts.contains(&"keyword"), "{value}");
1094 assert!(consts.contains(&"semantic"), "{value}");
1095 }
1096}