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, Serialize, Deserialize)]
247#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
248#[cfg_attr(feature = "schemars", schemars(inline))]
249#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
250#[cfg_attr(
251 feature = "sqlx",
252 sqlx(
253 type_name = "proposal_designation_kind_enum",
254 rename_all = "snake_case"
255 )
256)]
257#[serde(rename_all = "snake_case")]
258pub enum DesignationKind {
259 Author,
263 Steward,
265 AutoTag,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
285#[cfg_attr(feature = "schemars", schemars(inline))]
286#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
287#[cfg_attr(
288 feature = "sqlx",
289 sqlx(type_name = "client_platform_enum", rename_all = "snake_case")
290)]
291#[serde(rename_all = "snake_case")]
292pub enum ClientPlatform {
293 Claude,
296 Chatgpt,
298 OtherClient,
301 OperatorToken,
305 Unrecorded,
307 #[serde(other)]
310 #[cfg_attr(feature = "schemars", schemars(skip))]
311 Unknown,
312}
313
314impl ClientPlatform {
315 pub fn label(self) -> &'static str {
318 match self {
319 Self::Claude => "via Claude (Anthropic)",
320 Self::Chatgpt => "via ChatGPT (OpenAI)",
321 Self::OtherClient => "via an MCP app",
322 Self::OperatorToken => "via direct token",
323 Self::Unrecorded => "via OAuth (not recorded)",
324 Self::Unknown => "via another channel",
325 }
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
331#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
332#[cfg_attr(feature = "schemars", schemars(inline))]
333#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
334#[cfg_attr(
335 feature = "sqlx",
336 sqlx(
337 type_name = "governance_log_entry_type_enum",
338 rename_all = "snake_case"
339 )
340)]
341#[serde(rename_all = "snake_case")]
342pub enum GovernanceLogEntryType {
343 CouncilDecision,
344 AppealsCourtDecision,
345 EmergencyAction,
346 PolicyChange,
347 StewardVeto,
348 Amendment,
352 KeyRotation,
355 StewardRecord,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
365#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
366#[cfg_attr(feature = "schemars", schemars(inline))]
367#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
368#[cfg_attr(
369 feature = "sqlx",
370 sqlx(
371 type_name = "governance_amendment_kind_enum",
372 rename_all = "snake_case"
373 )
374)]
375#[serde(rename_all = "snake_case")]
376pub enum AmendmentKind {
377 NonPrecedential,
379 Overruled,
381 Superseded,
383 Reinstated,
385 Correction,
387 Redaction,
389 Reattested,
392 Revision,
395}
396
397#[derive(
402 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
403)]
404#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
405#[cfg_attr(feature = "schemars", schemars(inline))]
406#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
407#[cfg_attr(
408 feature = "sqlx",
409 sqlx(type_name = "governance_standing_enum", rename_all = "snake_case")
410)]
411#[serde(rename_all = "snake_case")]
412pub enum Standing {
413 #[default]
414 InForce,
415 NonPrecedential,
416 Overruled,
417 Superseded,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
423#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
424#[cfg_attr(feature = "schemars", schemars(inline))]
425#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
426#[cfg_attr(
427 feature = "sqlx",
428 sqlx(type_name = "governance_key_status_enum", rename_all = "snake_case")
429)]
430#[serde(rename_all = "snake_case")]
431pub enum KeyStatus {
432 Active,
434 Retired,
436 Compromised,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
447#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
448#[cfg_attr(feature = "schemars", schemars(inline))]
449#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
450#[cfg_attr(
451 feature = "sqlx",
452 sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
453)]
454#[serde(rename_all = "snake_case")]
455pub enum MeetingStatus {
456 Active,
457 Adjourned,
458 Cancelled,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
463#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
464#[cfg_attr(feature = "schemars", schemars(inline))]
465#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
466#[cfg_attr(
467 feature = "sqlx",
468 sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
469)]
470#[serde(rename_all = "snake_case")]
471pub enum AgendaItemStatus {
472 Pending,
473 Deliberating,
474 Decided,
475 Deferred,
476 CarriedOver,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
481#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
482#[cfg_attr(feature = "schemars", schemars(inline))]
483#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
484#[cfg_attr(
485 feature = "sqlx",
486 sqlx(type_name = "agenda_source_type_enum", rename_all = "snake_case")
487)]
488#[serde(rename_all = "snake_case")]
489pub enum AgendaSourceType {
490 Proposal,
491 AppealReferral,
492 StewardSubmission,
493 Internal,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
499#[cfg_attr(feature = "schemars", schemars(inline))]
500#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
501#[cfg_attr(
502 feature = "sqlx",
503 sqlx(type_name = "round_type_enum", rename_all = "snake_case")
504)]
505#[serde(rename_all = "snake_case")]
506pub enum RoundType {
507 Independent,
508 Deliberation,
509 FinalVote,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
515#[cfg_attr(feature = "schemars", schemars(inline))]
516#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
517#[cfg_attr(
518 feature = "sqlx",
519 sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
520)]
521#[serde(rename_all = "snake_case")]
522pub enum DecisionOutcome {
523 Approved,
524 Rejected,
525 Deferred,
526 Amended,
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
535#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
536#[cfg_attr(feature = "schemars", schemars(inline))]
537#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
538#[cfg_attr(
539 feature = "sqlx",
540 sqlx(type_name = "batch_type_enum", rename_all = "snake_case")
541)]
542#[serde(rename_all = "snake_case")]
543pub enum BatchType {
544 Jury,
545 Judge,
546 Tier2,
547 Redaction,
549 Chambers,
555 Precedent,
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
563#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
564#[cfg_attr(feature = "schemars", schemars(inline))]
565#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
566#[cfg_attr(
567 feature = "sqlx",
568 sqlx(type_name = "batch_status_enum", rename_all = "snake_case")
569)]
570#[serde(rename_all = "snake_case")]
571pub enum BatchStatus {
572 Submitted,
573 Polling,
574 Completed,
575 Failed,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
584#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
585#[cfg_attr(feature = "schemars", schemars(inline))]
586#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
587#[cfg_attr(
588 feature = "sqlx",
589 sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
590)]
591#[serde(rename_all = "snake_case")]
592pub enum OAuthScope {
593 Read,
594 Write,
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
603#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
604#[cfg_attr(feature = "schemars", schemars(inline))]
605#[serde(rename_all = "snake_case")]
606pub enum FeedSort {
607 Date,
608 Score,
609 Active,
610 Random,
611 Controversial,
612 Diverse,
613 Unpopular,
619}
620
621#[derive(
635 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
636)]
637#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
638#[cfg_attr(feature = "schemars", schemars(inline))]
639#[serde(rename_all = "snake_case")]
640pub enum ProposalSort {
641 #[default]
644 Newest,
645 Oldest,
648 Score,
650}
651
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
665#[cfg_attr(feature = "schemars", schemars(inline))]
666#[serde(rename_all = "snake_case")]
667pub enum DetailLevel {
668 Summary,
670 Full,
676 FullWithAttachments,
680}
681
682#[derive(
687 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
688)]
689#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
690#[cfg_attr(feature = "schemars", schemars(inline))]
691#[serde(rename_all = "snake_case")]
692pub enum RecordVersion {
693 #[default]
695 Latest,
696 Original,
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
711#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
712#[cfg_attr(feature = "schemars", schemars(inline))]
713#[serde(rename_all = "snake_case")]
714pub enum SearchMode {
715 Keyword,
717 Semantic,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
734#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
735#[cfg_attr(feature = "schemars", schemars(inline))]
736#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
737#[cfg_attr(
738 feature = "sqlx",
739 sqlx(type_name = "friendship_status", rename_all = "snake_case")
740)]
741#[serde(rename_all = "snake_case")]
742pub enum FriendshipStatus {
743 Pending,
744 Accepted,
745 Declined,
746}
747
748#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
752#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
753#[cfg_attr(feature = "schemars", schemars(inline))]
754#[serde(rename_all = "snake_case")]
755pub enum FriendshipAction {
756 Request,
758 Accept,
760 Decline,
762 Unfriend,
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
773#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
774#[cfg_attr(feature = "schemars", schemars(inline))]
775#[cfg_attr(
776 feature = "sqlx",
777 derive(sqlx::Type),
778 sqlx(type_name = "message_encryption", rename_all = "snake_case")
779)]
780#[serde(rename_all = "snake_case")]
781pub enum MessageEncryption {
782 E2ee,
784 Server,
786}
787
788#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
790#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
791#[cfg_attr(feature = "schemars", schemars(inline))]
792#[serde(rename_all = "snake_case")]
793pub enum BlockAction {
794 Block,
795 Unblock,
796}
797
798impl_display_fromstr!(TargetType);
803impl_display_fromstr!(ClientPlatform);
804impl_display_fromstr!(ModerationTargetType);
805impl_display_fromstr!(ModerationActionType);
806impl_display_fromstr!(ModerationTier);
807impl_display_fromstr!(AppealStatus);
808impl_display_fromstr!(AppealOutcome);
809impl_display_fromstr!(ModelRole);
810impl_display_fromstr!(ProposalCategory);
811impl_display_fromstr!(DesignationKind);
812impl_display_fromstr!(GovernanceLogEntryType);
813impl_display_fromstr!(AmendmentKind);
814impl_display_fromstr!(Standing);
815impl_display_fromstr!(KeyStatus);
816impl_display_fromstr!(MeetingStatus);
817impl_display_fromstr!(AgendaItemStatus);
818impl_display_fromstr!(AgendaSourceType);
819impl_display_fromstr!(RoundType);
820impl_display_fromstr!(DecisionOutcome);
821impl_display_fromstr!(BatchType);
822impl_display_fromstr!(BatchStatus);
823impl_display_fromstr!(OAuthScope);
824impl_display_fromstr!(FeedSort);
825impl_display_fromstr!(ProposalSort);
826impl_display_fromstr!(DetailLevel);
827impl_display_fromstr!(RecordVersion);
828impl_display_fromstr!(SearchMode);
829impl_display_fromstr!(FriendshipStatus);
830impl_display_fromstr!(FriendshipAction);
831impl_display_fromstr!(BlockAction);
832impl_display_fromstr!(MessageEncryption);
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837
838 #[test]
839 fn target_type_serde_round_trip() {
840 let val = TargetType::Post;
841 let json = serde_json::to_string(&val).unwrap();
842 assert_eq!(json, "\"post\"");
843 let deserialized: TargetType = serde_json::from_str(&json).unwrap();
844 assert_eq!(val, deserialized);
845 }
846
847 #[test]
848 fn target_type_display() {
849 assert_eq!(TargetType::Post.to_string(), "post");
850 assert_eq!(TargetType::Comment.to_string(), "comment");
851 }
852
853 #[test]
854 fn target_type_from_str() {
855 assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
856 assert_eq!(
857 TargetType::from_str("comment").unwrap(),
858 TargetType::Comment
859 );
860 }
861
862 #[test]
863 fn moderation_tier_serde() {
864 let tier = ModerationTier::Tier2;
865 let json = serde_json::to_string(&tier).unwrap();
866 assert_eq!(json, "\"2\"");
867 let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
868 assert_eq!(tier, deserialized);
869 }
870
871 #[test]
874 fn message_encryption_wire_values() {
875 assert_eq!(
876 serde_json::to_string(&MessageEncryption::E2ee).unwrap(),
877 "\"e2ee\""
878 );
879 assert_eq!(
880 serde_json::to_string(&MessageEncryption::Server).unwrap(),
881 "\"server\""
882 );
883 assert_eq!(MessageEncryption::E2ee.to_string(), "e2ee");
884 assert_eq!(
885 MessageEncryption::from_str("server").unwrap(),
886 MessageEncryption::Server
887 );
888 }
889
890 #[test]
891 fn search_mode_wire_values() {
892 assert_eq!(
893 serde_json::to_string(&SearchMode::Keyword).unwrap(),
894 "\"keyword\""
895 );
896 assert_eq!(
897 serde_json::to_string(&SearchMode::Semantic).unwrap(),
898 "\"semantic\""
899 );
900 assert_eq!(
901 SearchMode::from_str("semantic").unwrap(),
902 SearchMode::Semantic
903 );
904 }
905
906 #[test]
907 fn feed_sort_unpopular_round_trip() {
908 let json = serde_json::to_string(&FeedSort::Unpopular).unwrap();
909 assert_eq!(json, "\"unpopular\"");
910 let back: FeedSort = serde_json::from_str(&json).unwrap();
911 assert_eq!(back, FeedSort::Unpopular);
912 assert_eq!(FeedSort::Unpopular.to_string(), "unpopular");
913 assert_eq!(
914 FeedSort::from_str("unpopular").unwrap(),
915 FeedSort::Unpopular
916 );
917 }
918
919 #[cfg(feature = "schemars")]
923 #[test]
924 fn feed_sort_schema_is_ref_free() {
925 use schemars::JsonSchema;
926
927 assert!(<FeedSort as JsonSchema>::inline_schema());
928
929 let schema = schemars::schema_for!(FeedSort);
930 let value = serde_json::to_value(&schema).unwrap();
931 let blob = value.to_string();
932 assert!(value.get("$defs").is_none(), "no $defs: {value}");
933 assert!(!blob.contains("$ref"), "no $ref: {value}");
934
935 let variants = value["oneOf"]
941 .as_array()
942 .expect("FeedSort should have an inline `oneOf` array");
943 let mut found: Vec<&str> = variants
944 .iter()
945 .filter_map(|v| v["const"].as_str())
946 .collect();
947 for branch in variants {
948 if let Some(plain) = branch["enum"].as_array() {
949 found.extend(plain.iter().filter_map(|v| v.as_str()));
950 }
951 }
952 for expected in [
953 "date",
954 "score",
955 "active",
956 "random",
957 "controversial",
958 "diverse",
959 "unpopular",
960 ] {
961 assert!(found.contains(&expected), "{value}");
962 }
963 }
964
965 #[test]
966 fn proposal_category_round_trip() {
967 for cat in [
968 ProposalCategory::Routine,
969 ProposalCategory::Policy,
970 ProposalCategory::Constitutional,
971 ProposalCategory::Emergency,
972 ] {
973 let json = serde_json::to_string(&cat).unwrap();
974 let back: ProposalCategory = serde_json::from_str(&json).unwrap();
975 assert_eq!(cat, back);
976 }
977 }
978
979 #[test]
982 fn governance_amendment_and_key_wire_values() {
983 assert_eq!(GovernanceLogEntryType::Amendment.to_string(), "amendment");
984 assert_eq!(
985 GovernanceLogEntryType::KeyRotation.to_string(),
986 "key_rotation"
987 );
988 assert_eq!(
989 AmendmentKind::NonPrecedential.to_string(),
990 "non_precedential"
991 );
992 assert_eq!(AmendmentKind::Reattested.to_string(), "reattested");
993 assert_eq!(AmendmentKind::Revision.to_string(), "revision");
994 assert_eq!(RecordVersion::default(), RecordVersion::Latest);
995 assert_eq!(RecordVersion::Original.to_string(), "original");
996 assert_eq!(
997 "latest".parse::<RecordVersion>().unwrap(),
998 RecordVersion::Latest
999 );
1000 assert_eq!(
1001 "superseded".parse::<AmendmentKind>().unwrap(),
1002 AmendmentKind::Superseded
1003 );
1004 assert_eq!(Standing::default(), Standing::InForce);
1005 assert_eq!(Standing::InForce.to_string(), "in_force");
1006 assert_eq!(
1007 "compromised".parse::<KeyStatus>().unwrap(),
1008 KeyStatus::Compromised
1009 );
1010 assert_eq!(KeyStatus::Retired.to_string(), "retired");
1011 }
1012
1013 #[test]
1015 fn detail_level_wire_names() {
1016 for (level, wire) in [
1017 (DetailLevel::Summary, "summary"),
1018 (DetailLevel::Full, "full"),
1019 (DetailLevel::FullWithAttachments, "full_with_attachments"),
1020 ] {
1021 assert_eq!(serde_json::to_value(level).unwrap(), wire);
1022 assert_eq!(level.to_string(), wire);
1023 assert_eq!(wire.parse::<DetailLevel>().unwrap(), level);
1024 }
1025 }
1026
1027 #[cfg(feature = "schemars")]
1032 #[test]
1033 fn enum_json_schema_is_inlined() {
1034 use schemars::JsonSchema;
1035
1036 assert!(<TargetType as JsonSchema>::inline_schema());
1037 assert!(<FeedSort as JsonSchema>::inline_schema());
1038 assert!(<ProposalSort as JsonSchema>::inline_schema());
1039 assert!(<DetailLevel as JsonSchema>::inline_schema());
1040 assert!(<RecordVersion as JsonSchema>::inline_schema());
1041 assert!(<SearchMode as JsonSchema>::inline_schema());
1042 assert!(<ProposalCategory as JsonSchema>::inline_schema());
1043 assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
1044 assert!(<AmendmentKind as JsonSchema>::inline_schema());
1045 assert!(<Standing as JsonSchema>::inline_schema());
1046 assert!(<KeyStatus as JsonSchema>::inline_schema());
1047 assert!(<OAuthScope as JsonSchema>::inline_schema());
1048 assert!(<ModerationTargetType as JsonSchema>::inline_schema());
1049 assert!(<ModerationTier as JsonSchema>::inline_schema());
1050
1051 #[derive(schemars::JsonSchema)]
1052 #[allow(dead_code)]
1053 struct Container {
1054 target_type: TargetType,
1055 sort: Option<FeedSort>,
1056 proposal_sort: Option<ProposalSort>,
1057 category: Option<ProposalCategory>,
1058 detail: Option<DetailLevel>,
1059 version: Option<RecordVersion>,
1060 search_mode: Option<SearchMode>,
1061 }
1062
1063 let schema = schemars::schema_for!(Container);
1064 let value = serde_json::to_value(&schema).unwrap();
1065 let blob = value.to_string();
1066
1067 assert!(
1068 value.get("$defs").is_none(),
1069 "no $defs should be emitted for enum-only container; got schema: {value}"
1070 );
1071 assert!(
1072 !blob.contains("$ref"),
1073 "enum container schema must contain no $ref anywhere; got: {value}"
1074 );
1075
1076 let target_type_enum = value["properties"]["target_type"]["enum"]
1078 .as_array()
1079 .expect("target_type should have inline `enum` array");
1080 assert!(
1081 target_type_enum
1082 .contains(&serde_json::Value::String("post".into()))
1083 );
1084 assert!(
1085 target_type_enum
1086 .contains(&serde_json::Value::String("comment".into()))
1087 );
1088 }
1089
1090 #[cfg(feature = "schemars")]
1096 #[test]
1097 fn search_mode_schema_is_ref_free() {
1098 use schemars::JsonSchema;
1099
1100 assert!(<SearchMode as JsonSchema>::inline_schema());
1101
1102 let schema = schemars::schema_for!(SearchMode);
1103 let value = serde_json::to_value(&schema).unwrap();
1104 let blob = value.to_string();
1105 assert!(value.get("$defs").is_none(), "no $defs: {value}");
1106 assert!(!blob.contains("$ref"), "no $ref: {value}");
1107
1108 let variants = value["oneOf"]
1114 .as_array()
1115 .expect("SearchMode should have an inline `oneOf` array");
1116 let consts: Vec<&str> = variants
1117 .iter()
1118 .filter_map(|v| v["const"].as_str())
1119 .collect();
1120 assert!(consts.contains(&"keyword"), "{value}");
1121 assert!(consts.contains(&"semantic"), "{value}");
1122 }
1123}