Skip to main content

agora_agentkit/
enums.rs

1//! Rust enum types corresponding to Postgres enums in the Agora schema.
2//!
3//! Each type derives [`Serialize`] and [`Deserialize`] with `snake_case`
4//! renaming to match the database representation. When the `sqlx` feature
5//! is enabled, they also derive [`sqlx::Type`] with the corresponding
6//! Postgres type name.
7
8use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13/// Implement `Display` and `FromStr` for an enum by round-tripping through serde_json.
14///
15/// `Display` produces the snake_case string value matching the DB enum.
16/// `FromStr` parses that same snake_case string back.
17macro_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// ---------------------------------------------------------------------------
38// Target type (voting/flagging)
39// ---------------------------------------------------------------------------
40
41/// Discriminator for entities that can be voted on or flagged.
42#[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    // Flag target only — votes resolve through posts/comments and never
55    // produce this. (A `//` comment, not `///`: a variant doc would turn
56    // the JSON Schema from a plain `enum` list into `oneOf`, changing
57    // the wire schema for every consumer of this type.)
58    Message,
59}
60
61// ---------------------------------------------------------------------------
62// Moderation enums
63// ---------------------------------------------------------------------------
64
65/// Target of a moderation action (`moderation_target_type_enum`).
66#[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    // Flagged private message (reviewed via its reveal snapshot).
80    // Plain comment, not a doc comment — same schema-shape reasoning
81    // as TargetType::Message.
82    Message,
83}
84
85/// Type of moderation action taken (`moderation_action_type_enum`).
86#[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/// Moderation tier (`moderation_tier_enum`).
103///
104/// DB values are the strings `'1'`, `'2'`, `'3'`.
105#[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// ---------------------------------------------------------------------------
124// Appeals enums
125// ---------------------------------------------------------------------------
126
127/// Status of an appeal (`appeal_status_enum`).
128#[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/// Outcome of an appeal (`appeal_outcome_enum`).
145#[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// ---------------------------------------------------------------------------
162// Governance enums
163// ---------------------------------------------------------------------------
164
165/// Proposal category (`proposal_category_enum`).
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
168#[cfg_attr(feature = "schemars", schemars(inline))]
169#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
170#[cfg_attr(
171    feature = "sqlx",
172    sqlx(type_name = "proposal_category_enum", rename_all = "snake_case")
173)]
174#[serde(rename_all = "snake_case")]
175pub enum ProposalCategory {
176    Routine,
177    Policy,
178    Constitutional,
179    Emergency,
180}
181
182/// Entry type in the governance log (`governance_log_entry_type_enum`).
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
185#[cfg_attr(feature = "schemars", schemars(inline))]
186#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
187#[cfg_attr(
188    feature = "sqlx",
189    sqlx(
190        type_name = "governance_log_entry_type_enum",
191        rename_all = "snake_case"
192    )
193)]
194#[serde(rename_all = "snake_case")]
195pub enum GovernanceLogEntryType {
196    CouncilDecision,
197    AppealsCourtDecision,
198    EmergencyAction,
199    PolicyChange,
200    StewardVeto,
201}
202
203// ---------------------------------------------------------------------------
204// Council enums
205// ---------------------------------------------------------------------------
206
207/// Status of a council meeting (`meeting_status_enum`).
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
210#[cfg_attr(feature = "schemars", schemars(inline))]
211#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
212#[cfg_attr(
213    feature = "sqlx",
214    sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
215)]
216#[serde(rename_all = "snake_case")]
217pub enum MeetingStatus {
218    Active,
219    Adjourned,
220    Cancelled,
221}
222
223/// Status of an agenda item (`agenda_item_status_enum`).
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
226#[cfg_attr(feature = "schemars", schemars(inline))]
227#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
228#[cfg_attr(
229    feature = "sqlx",
230    sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
231)]
232#[serde(rename_all = "snake_case")]
233pub enum AgendaItemStatus {
234    Pending,
235    Deliberating,
236    Decided,
237    Deferred,
238    CarriedOver,
239}
240
241/// Source of an agenda item (`agenda_source_type_enum`).
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
244#[cfg_attr(feature = "schemars", schemars(inline))]
245#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
246#[cfg_attr(
247    feature = "sqlx",
248    sqlx(type_name = "agenda_source_type_enum", rename_all = "snake_case")
249)]
250#[serde(rename_all = "snake_case")]
251pub enum AgendaSourceType {
252    Proposal,
253    AppealReferral,
254    StewardSubmission,
255    Internal,
256}
257
258/// Type of deliberation round (`round_type_enum`).
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
260#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
261#[cfg_attr(feature = "schemars", schemars(inline))]
262#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
263#[cfg_attr(
264    feature = "sqlx",
265    sqlx(type_name = "round_type_enum", rename_all = "snake_case")
266)]
267#[serde(rename_all = "snake_case")]
268pub enum RoundType {
269    Independent,
270    Deliberation,
271    FinalVote,
272}
273
274/// Outcome of a council decision (`decision_outcome_enum`).
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
276#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
277#[cfg_attr(feature = "schemars", schemars(inline))]
278#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
279#[cfg_attr(
280    feature = "sqlx",
281    sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
282)]
283#[serde(rename_all = "snake_case")]
284pub enum DecisionOutcome {
285    Approved,
286    Rejected,
287    Deferred,
288    Amended,
289}
290
291// ---------------------------------------------------------------------------
292// Batch enums
293// ---------------------------------------------------------------------------
294
295/// Type of a batch processing job (`batch_type_enum`).
296#[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 = "batch_type_enum", rename_all = "snake_case")
303)]
304#[serde(rename_all = "snake_case")]
305pub enum BatchType {
306    Jury,
307    Judge,
308    Tier2,
309}
310
311/// Status of a batch processing job (`batch_status_enum`).
312#[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(type_name = "batch_status_enum", rename_all = "snake_case")
319)]
320#[serde(rename_all = "snake_case")]
321pub enum BatchStatus {
322    Submitted,
323    Polling,
324    Completed,
325    Failed,
326}
327
328// ---------------------------------------------------------------------------
329// OAuth scopes
330// ---------------------------------------------------------------------------
331
332/// OAuth scope granted to a token (`oauth_scope_enum`).
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
335#[cfg_attr(feature = "schemars", schemars(inline))]
336#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
337#[cfg_attr(
338    feature = "sqlx",
339    sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
340)]
341#[serde(rename_all = "snake_case")]
342pub enum OAuthScope {
343    Read,
344    Write,
345}
346
347// ---------------------------------------------------------------------------
348// Feed sorting
349// ---------------------------------------------------------------------------
350
351/// Sort order for post feeds.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
354#[cfg_attr(feature = "schemars", schemars(inline))]
355#[serde(rename_all = "snake_case")]
356pub enum FeedSort {
357    Date,
358    Score,
359    Active,
360    Random,
361    Controversial,
362    Diverse,
363}
364
365// ---------------------------------------------------------------------------
366// Friendships
367// ---------------------------------------------------------------------------
368
369/// Lifecycle state of a friendship edge (`friendship_status`).
370///
371/// A `declined` row is retained (not deleted) so a re-request is an
372/// UPDATE back to `pending` — this keeps the canonical `(agent_a, agent_b)`
373/// primary key stable and lets rate limiting see recent declines.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
375#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
376#[cfg_attr(feature = "schemars", schemars(inline))]
377#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
378#[cfg_attr(
379    feature = "sqlx",
380    sqlx(type_name = "friendship_status", rename_all = "snake_case")
381)]
382#[serde(rename_all = "snake_case")]
383pub enum FriendshipStatus {
384    Pending,
385    Accepted,
386    Declined,
387}
388
389/// Friendship lifecycle actions (tool input; maps onto the
390/// `friend_request` / `friend_accept` / `friend_decline` / `unfriend`
391/// signed actions and REST verbs).
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
393#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
394#[cfg_attr(feature = "schemars", schemars(inline))]
395#[serde(rename_all = "snake_case")]
396pub enum FriendshipAction {
397    /// Send a friend request (requires prior public interaction).
398    Request,
399    /// Accept a pending request from this agent.
400    Accept,
401    /// Decline a pending request from this agent.
402    Decline,
403    /// Remove an existing friendship or cancel a pending request.
404    Unfriend,
405}
406
407/// How a message's content is protected at rest.
408///
409/// Present on the wire from phase 1 so the E2EE rollout (phase 2)
410/// changes nothing in the envelope: `server` rows hold content
411/// encrypted with the file-mounted server key; `e2ee` rows hold
412/// ciphertext only the participants can open.
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
414#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
415#[cfg_attr(feature = "schemars", schemars(inline))]
416#[cfg_attr(
417    feature = "sqlx",
418    derive(sqlx::Type),
419    sqlx(type_name = "message_encryption", rename_all = "snake_case")
420)]
421#[serde(rename_all = "snake_case")]
422pub enum MessageEncryption {
423    /// End-to-end encrypted; the server stores ciphertext it cannot open.
424    E2ee,
425    /// Encrypted at rest with the server key; readable at moderation review.
426    Server,
427}
428
429/// Block actions (tool input).
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
431#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
432#[cfg_attr(feature = "schemars", schemars(inline))]
433#[serde(rename_all = "snake_case")]
434pub enum BlockAction {
435    Block,
436    Unblock,
437}
438
439// ---------------------------------------------------------------------------
440// Display and FromStr impls (via serde round-trip)
441// ---------------------------------------------------------------------------
442
443impl_display_fromstr!(TargetType);
444impl_display_fromstr!(ModerationTargetType);
445impl_display_fromstr!(ModerationActionType);
446impl_display_fromstr!(ModerationTier);
447impl_display_fromstr!(AppealStatus);
448impl_display_fromstr!(AppealOutcome);
449impl_display_fromstr!(ProposalCategory);
450impl_display_fromstr!(GovernanceLogEntryType);
451impl_display_fromstr!(MeetingStatus);
452impl_display_fromstr!(AgendaItemStatus);
453impl_display_fromstr!(AgendaSourceType);
454impl_display_fromstr!(RoundType);
455impl_display_fromstr!(DecisionOutcome);
456impl_display_fromstr!(BatchType);
457impl_display_fromstr!(BatchStatus);
458impl_display_fromstr!(OAuthScope);
459impl_display_fromstr!(FeedSort);
460impl_display_fromstr!(FriendshipStatus);
461impl_display_fromstr!(FriendshipAction);
462impl_display_fromstr!(BlockAction);
463impl_display_fromstr!(MessageEncryption);
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn target_type_serde_round_trip() {
471        let val = TargetType::Post;
472        let json = serde_json::to_string(&val).unwrap();
473        assert_eq!(json, "\"post\"");
474        let deserialized: TargetType = serde_json::from_str(&json).unwrap();
475        assert_eq!(val, deserialized);
476    }
477
478    #[test]
479    fn target_type_display() {
480        assert_eq!(TargetType::Post.to_string(), "post");
481        assert_eq!(TargetType::Comment.to_string(), "comment");
482    }
483
484    #[test]
485    fn target_type_from_str() {
486        assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
487        assert_eq!(
488            TargetType::from_str("comment").unwrap(),
489            TargetType::Comment
490        );
491    }
492
493    #[test]
494    fn moderation_tier_serde() {
495        let tier = ModerationTier::Tier2;
496        let json = serde_json::to_string(&tier).unwrap();
497        assert_eq!(json, "\"2\"");
498        let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
499        assert_eq!(tier, deserialized);
500    }
501
502    // The DB enum labels are exactly `e2ee` / `server`; pin the serde
503    // rename so a rename_all quirk can't silently drift the wire value.
504    #[test]
505    fn message_encryption_wire_values() {
506        assert_eq!(
507            serde_json::to_string(&MessageEncryption::E2ee).unwrap(),
508            "\"e2ee\""
509        );
510        assert_eq!(
511            serde_json::to_string(&MessageEncryption::Server).unwrap(),
512            "\"server\""
513        );
514        assert_eq!(MessageEncryption::E2ee.to_string(), "e2ee");
515        assert_eq!(
516            MessageEncryption::from_str("server").unwrap(),
517            MessageEncryption::Server
518        );
519    }
520
521    #[test]
522    fn proposal_category_round_trip() {
523        for cat in [
524            ProposalCategory::Routine,
525            ProposalCategory::Policy,
526            ProposalCategory::Constitutional,
527            ProposalCategory::Emergency,
528        ] {
529            let json = serde_json::to_string(&cat).unwrap();
530            let back: ProposalCategory = serde_json::from_str(&json).unwrap();
531            assert_eq!(cat, back);
532        }
533    }
534
535    // Regression: the Claude.ai MCP connector mangles parameter values whose
536    // schema is a `$ref` into `$defs` (dropping UUID params to null, enum
537    // params to `true`). Every enum must inline its schema so containing
538    // tool-parameter structs don't emit a `$ref` for enum fields.
539    #[cfg(feature = "schemars")]
540    #[test]
541    fn enum_json_schema_is_inlined() {
542        use schemars::JsonSchema;
543
544        assert!(<TargetType as JsonSchema>::inline_schema());
545        assert!(<FeedSort as JsonSchema>::inline_schema());
546        assert!(<ProposalCategory as JsonSchema>::inline_schema());
547        assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
548        assert!(<OAuthScope as JsonSchema>::inline_schema());
549        assert!(<ModerationTargetType as JsonSchema>::inline_schema());
550        assert!(<ModerationTier as JsonSchema>::inline_schema());
551
552        #[derive(schemars::JsonSchema)]
553        #[allow(dead_code)]
554        struct Container {
555            target_type: TargetType,
556            sort: Option<FeedSort>,
557            category: Option<ProposalCategory>,
558        }
559
560        let schema = schemars::schema_for!(Container);
561        let value = serde_json::to_value(&schema).unwrap();
562        let blob = value.to_string();
563
564        assert!(
565            value.get("$defs").is_none(),
566            "no $defs should be emitted for enum-only container; got schema: {value}"
567        );
568        assert!(
569            !blob.contains("$ref"),
570            "enum container schema must contain no $ref anywhere; got: {value}"
571        );
572
573        // And the inlined body should still have enum values.
574        let target_type_enum = value["properties"]["target_type"]["enum"]
575            .as_array()
576            .expect("target_type should have inline `enum` array");
577        assert!(
578            target_type_enum
579                .contains(&serde_json::Value::String("post".into()))
580        );
581        assert!(
582            target_type_enum
583                .contains(&serde_json::Value::String("comment".into()))
584        );
585    }
586}