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}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
239#[cfg_attr(feature = "schemars", schemars(inline))]
240#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
241#[cfg_attr(
242 feature = "sqlx",
243 sqlx(
244 type_name = "governance_log_entry_type_enum",
245 rename_all = "snake_case"
246 )
247)]
248#[serde(rename_all = "snake_case")]
249pub enum GovernanceLogEntryType {
250 CouncilDecision,
251 AppealsCourtDecision,
252 EmergencyAction,
253 PolicyChange,
254 StewardVeto,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
264#[cfg_attr(feature = "schemars", schemars(inline))]
265#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
266#[cfg_attr(
267 feature = "sqlx",
268 sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
269)]
270#[serde(rename_all = "snake_case")]
271pub enum MeetingStatus {
272 Active,
273 Adjourned,
274 Cancelled,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
279#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
280#[cfg_attr(feature = "schemars", schemars(inline))]
281#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
282#[cfg_attr(
283 feature = "sqlx",
284 sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
285)]
286#[serde(rename_all = "snake_case")]
287pub enum AgendaItemStatus {
288 Pending,
289 Deliberating,
290 Decided,
291 Deferred,
292 CarriedOver,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
298#[cfg_attr(feature = "schemars", schemars(inline))]
299#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
300#[cfg_attr(
301 feature = "sqlx",
302 sqlx(type_name = "agenda_source_type_enum", rename_all = "snake_case")
303)]
304#[serde(rename_all = "snake_case")]
305pub enum AgendaSourceType {
306 Proposal,
307 AppealReferral,
308 StewardSubmission,
309 Internal,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
315#[cfg_attr(feature = "schemars", schemars(inline))]
316#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
317#[cfg_attr(
318 feature = "sqlx",
319 sqlx(type_name = "round_type_enum", rename_all = "snake_case")
320)]
321#[serde(rename_all = "snake_case")]
322pub enum RoundType {
323 Independent,
324 Deliberation,
325 FinalVote,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
331#[cfg_attr(feature = "schemars", schemars(inline))]
332#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
333#[cfg_attr(
334 feature = "sqlx",
335 sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
336)]
337#[serde(rename_all = "snake_case")]
338pub enum DecisionOutcome {
339 Approved,
340 Rejected,
341 Deferred,
342 Amended,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
352#[cfg_attr(feature = "schemars", schemars(inline))]
353#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
354#[cfg_attr(
355 feature = "sqlx",
356 sqlx(type_name = "batch_type_enum", rename_all = "snake_case")
357)]
358#[serde(rename_all = "snake_case")]
359pub enum BatchType {
360 Jury,
361 Judge,
362 Tier2,
363 Redaction,
365 Chambers,
371 Precedent,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
380#[cfg_attr(feature = "schemars", schemars(inline))]
381#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
382#[cfg_attr(
383 feature = "sqlx",
384 sqlx(type_name = "batch_status_enum", rename_all = "snake_case")
385)]
386#[serde(rename_all = "snake_case")]
387pub enum BatchStatus {
388 Submitted,
389 Polling,
390 Completed,
391 Failed,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
400#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
401#[cfg_attr(feature = "schemars", schemars(inline))]
402#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
403#[cfg_attr(
404 feature = "sqlx",
405 sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
406)]
407#[serde(rename_all = "snake_case")]
408pub enum OAuthScope {
409 Read,
410 Write,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
420#[cfg_attr(feature = "schemars", schemars(inline))]
421#[serde(rename_all = "snake_case")]
422pub enum FeedSort {
423 Date,
424 Score,
425 Active,
426 Random,
427 Controversial,
428 Diverse,
429 Unpopular,
435}
436
437#[derive(
451 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
452)]
453#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
454#[cfg_attr(feature = "schemars", schemars(inline))]
455#[serde(rename_all = "snake_case")]
456pub enum ProposalSort {
457 #[default]
460 Newest,
461 Oldest,
464 Score,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
483#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
484#[cfg_attr(feature = "schemars", schemars(inline))]
485#[serde(rename_all = "snake_case")]
486pub enum DetailLevel {
487 Summary,
489 Full,
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
505#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
506#[cfg_attr(feature = "schemars", schemars(inline))]
507#[serde(rename_all = "snake_case")]
508pub enum SearchMode {
509 Keyword,
511 Semantic,
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
528#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
529#[cfg_attr(feature = "schemars", schemars(inline))]
530#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
531#[cfg_attr(
532 feature = "sqlx",
533 sqlx(type_name = "friendship_status", rename_all = "snake_case")
534)]
535#[serde(rename_all = "snake_case")]
536pub enum FriendshipStatus {
537 Pending,
538 Accepted,
539 Declined,
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
546#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
547#[cfg_attr(feature = "schemars", schemars(inline))]
548#[serde(rename_all = "snake_case")]
549pub enum FriendshipAction {
550 Request,
552 Accept,
554 Decline,
556 Unfriend,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
567#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
568#[cfg_attr(feature = "schemars", schemars(inline))]
569#[cfg_attr(
570 feature = "sqlx",
571 derive(sqlx::Type),
572 sqlx(type_name = "message_encryption", rename_all = "snake_case")
573)]
574#[serde(rename_all = "snake_case")]
575pub enum MessageEncryption {
576 E2ee,
578 Server,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
585#[cfg_attr(feature = "schemars", schemars(inline))]
586#[serde(rename_all = "snake_case")]
587pub enum BlockAction {
588 Block,
589 Unblock,
590}
591
592impl_display_fromstr!(TargetType);
597impl_display_fromstr!(ModerationTargetType);
598impl_display_fromstr!(ModerationActionType);
599impl_display_fromstr!(ModerationTier);
600impl_display_fromstr!(AppealStatus);
601impl_display_fromstr!(AppealOutcome);
602impl_display_fromstr!(ModelRole);
603impl_display_fromstr!(ProposalCategory);
604impl_display_fromstr!(GovernanceLogEntryType);
605impl_display_fromstr!(MeetingStatus);
606impl_display_fromstr!(AgendaItemStatus);
607impl_display_fromstr!(AgendaSourceType);
608impl_display_fromstr!(RoundType);
609impl_display_fromstr!(DecisionOutcome);
610impl_display_fromstr!(BatchType);
611impl_display_fromstr!(BatchStatus);
612impl_display_fromstr!(OAuthScope);
613impl_display_fromstr!(FeedSort);
614impl_display_fromstr!(ProposalSort);
615impl_display_fromstr!(DetailLevel);
616impl_display_fromstr!(SearchMode);
617impl_display_fromstr!(FriendshipStatus);
618impl_display_fromstr!(FriendshipAction);
619impl_display_fromstr!(BlockAction);
620impl_display_fromstr!(MessageEncryption);
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn target_type_serde_round_trip() {
628 let val = TargetType::Post;
629 let json = serde_json::to_string(&val).unwrap();
630 assert_eq!(json, "\"post\"");
631 let deserialized: TargetType = serde_json::from_str(&json).unwrap();
632 assert_eq!(val, deserialized);
633 }
634
635 #[test]
636 fn target_type_display() {
637 assert_eq!(TargetType::Post.to_string(), "post");
638 assert_eq!(TargetType::Comment.to_string(), "comment");
639 }
640
641 #[test]
642 fn target_type_from_str() {
643 assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
644 assert_eq!(
645 TargetType::from_str("comment").unwrap(),
646 TargetType::Comment
647 );
648 }
649
650 #[test]
651 fn moderation_tier_serde() {
652 let tier = ModerationTier::Tier2;
653 let json = serde_json::to_string(&tier).unwrap();
654 assert_eq!(json, "\"2\"");
655 let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
656 assert_eq!(tier, deserialized);
657 }
658
659 #[test]
662 fn message_encryption_wire_values() {
663 assert_eq!(
664 serde_json::to_string(&MessageEncryption::E2ee).unwrap(),
665 "\"e2ee\""
666 );
667 assert_eq!(
668 serde_json::to_string(&MessageEncryption::Server).unwrap(),
669 "\"server\""
670 );
671 assert_eq!(MessageEncryption::E2ee.to_string(), "e2ee");
672 assert_eq!(
673 MessageEncryption::from_str("server").unwrap(),
674 MessageEncryption::Server
675 );
676 }
677
678 #[test]
679 fn search_mode_wire_values() {
680 assert_eq!(
681 serde_json::to_string(&SearchMode::Keyword).unwrap(),
682 "\"keyword\""
683 );
684 assert_eq!(
685 serde_json::to_string(&SearchMode::Semantic).unwrap(),
686 "\"semantic\""
687 );
688 assert_eq!(
689 SearchMode::from_str("semantic").unwrap(),
690 SearchMode::Semantic
691 );
692 }
693
694 #[test]
695 fn feed_sort_unpopular_round_trip() {
696 let json = serde_json::to_string(&FeedSort::Unpopular).unwrap();
697 assert_eq!(json, "\"unpopular\"");
698 let back: FeedSort = serde_json::from_str(&json).unwrap();
699 assert_eq!(back, FeedSort::Unpopular);
700 assert_eq!(FeedSort::Unpopular.to_string(), "unpopular");
701 assert_eq!(
702 FeedSort::from_str("unpopular").unwrap(),
703 FeedSort::Unpopular
704 );
705 }
706
707 #[cfg(feature = "schemars")]
711 #[test]
712 fn feed_sort_schema_is_ref_free() {
713 use schemars::JsonSchema;
714
715 assert!(<FeedSort as JsonSchema>::inline_schema());
716
717 let schema = schemars::schema_for!(FeedSort);
718 let value = serde_json::to_value(&schema).unwrap();
719 let blob = value.to_string();
720 assert!(value.get("$defs").is_none(), "no $defs: {value}");
721 assert!(!blob.contains("$ref"), "no $ref: {value}");
722
723 let variants = value["oneOf"]
729 .as_array()
730 .expect("FeedSort should have an inline `oneOf` array");
731 let mut found: Vec<&str> = variants
732 .iter()
733 .filter_map(|v| v["const"].as_str())
734 .collect();
735 for branch in variants {
736 if let Some(plain) = branch["enum"].as_array() {
737 found.extend(plain.iter().filter_map(|v| v.as_str()));
738 }
739 }
740 for expected in [
741 "date",
742 "score",
743 "active",
744 "random",
745 "controversial",
746 "diverse",
747 "unpopular",
748 ] {
749 assert!(found.contains(&expected), "{value}");
750 }
751 }
752
753 #[test]
754 fn proposal_category_round_trip() {
755 for cat in [
756 ProposalCategory::Routine,
757 ProposalCategory::Policy,
758 ProposalCategory::Constitutional,
759 ProposalCategory::Emergency,
760 ] {
761 let json = serde_json::to_string(&cat).unwrap();
762 let back: ProposalCategory = serde_json::from_str(&json).unwrap();
763 assert_eq!(cat, back);
764 }
765 }
766
767 #[cfg(feature = "schemars")]
772 #[test]
773 fn enum_json_schema_is_inlined() {
774 use schemars::JsonSchema;
775
776 assert!(<TargetType as JsonSchema>::inline_schema());
777 assert!(<FeedSort as JsonSchema>::inline_schema());
778 assert!(<ProposalSort as JsonSchema>::inline_schema());
779 assert!(<DetailLevel as JsonSchema>::inline_schema());
780 assert!(<SearchMode as JsonSchema>::inline_schema());
781 assert!(<ProposalCategory as JsonSchema>::inline_schema());
782 assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
783 assert!(<OAuthScope as JsonSchema>::inline_schema());
784 assert!(<ModerationTargetType as JsonSchema>::inline_schema());
785 assert!(<ModerationTier as JsonSchema>::inline_schema());
786
787 #[derive(schemars::JsonSchema)]
788 #[allow(dead_code)]
789 struct Container {
790 target_type: TargetType,
791 sort: Option<FeedSort>,
792 proposal_sort: Option<ProposalSort>,
793 category: Option<ProposalCategory>,
794 detail: Option<DetailLevel>,
795 search_mode: Option<SearchMode>,
796 }
797
798 let schema = schemars::schema_for!(Container);
799 let value = serde_json::to_value(&schema).unwrap();
800 let blob = value.to_string();
801
802 assert!(
803 value.get("$defs").is_none(),
804 "no $defs should be emitted for enum-only container; got schema: {value}"
805 );
806 assert!(
807 !blob.contains("$ref"),
808 "enum container schema must contain no $ref anywhere; got: {value}"
809 );
810
811 let target_type_enum = value["properties"]["target_type"]["enum"]
813 .as_array()
814 .expect("target_type should have inline `enum` array");
815 assert!(
816 target_type_enum
817 .contains(&serde_json::Value::String("post".into()))
818 );
819 assert!(
820 target_type_enum
821 .contains(&serde_json::Value::String("comment".into()))
822 );
823 }
824
825 #[cfg(feature = "schemars")]
831 #[test]
832 fn search_mode_schema_is_ref_free() {
833 use schemars::JsonSchema;
834
835 assert!(<SearchMode as JsonSchema>::inline_schema());
836
837 let schema = schemars::schema_for!(SearchMode);
838 let value = serde_json::to_value(&schema).unwrap();
839 let blob = value.to_string();
840 assert!(value.get("$defs").is_none(), "no $defs: {value}");
841 assert!(!blob.contains("$ref"), "no $ref: {value}");
842
843 let variants = value["oneOf"]
849 .as_array()
850 .expect("SearchMode should have an inline `oneOf` array");
851 let consts: Vec<&str> = variants
852 .iter()
853 .filter_map(|v| v["const"].as_str())
854 .collect();
855 assert!(consts.contains(&"keyword"), "{value}");
856 assert!(consts.contains(&"semantic"), "{value}");
857 }
858}