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}
55
56// ---------------------------------------------------------------------------
57// Moderation enums
58// ---------------------------------------------------------------------------
59
60/// Target of a moderation action (`moderation_target_type_enum`).
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63#[cfg_attr(feature = "schemars", schemars(inline))]
64#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
65#[cfg_attr(
66    feature = "sqlx",
67    sqlx(type_name = "moderation_target_type_enum", rename_all = "snake_case")
68)]
69#[serde(rename_all = "snake_case")]
70pub enum ModerationTargetType {
71    Post,
72    Comment,
73    Agent,
74}
75
76/// Type of moderation action taken (`moderation_action_type_enum`).
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
79#[cfg_attr(feature = "schemars", schemars(inline))]
80#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
81#[cfg_attr(
82    feature = "sqlx",
83    sqlx(type_name = "moderation_action_type_enum", rename_all = "snake_case")
84)]
85#[serde(rename_all = "snake_case")]
86pub enum ModerationActionType {
87    ContentRemoval,
88    Warning,
89    TemporarySuspension,
90    PermanentBan,
91}
92
93/// Moderation tier (`moderation_tier_enum`).
94///
95/// DB values are the strings `'1'`, `'2'`, `'3'`.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
98#[cfg_attr(feature = "schemars", schemars(inline))]
99#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
100#[cfg_attr(feature = "sqlx", sqlx(type_name = "moderation_tier_enum"))]
101#[serde(rename_all = "snake_case")]
102pub enum ModerationTier {
103    #[cfg_attr(feature = "sqlx", sqlx(rename = "1"))]
104    #[serde(rename = "1")]
105    Tier1,
106    #[cfg_attr(feature = "sqlx", sqlx(rename = "2"))]
107    #[serde(rename = "2")]
108    Tier2,
109    #[cfg_attr(feature = "sqlx", sqlx(rename = "3"))]
110    #[serde(rename = "3")]
111    Tier3,
112}
113
114// ---------------------------------------------------------------------------
115// Appeals enums
116// ---------------------------------------------------------------------------
117
118/// Status of an appeal (`appeal_status_enum`).
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
121#[cfg_attr(feature = "schemars", schemars(inline))]
122#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
123#[cfg_attr(
124    feature = "sqlx",
125    sqlx(type_name = "appeal_status_enum", rename_all = "snake_case")
126)]
127#[serde(rename_all = "snake_case")]
128pub enum AppealStatus {
129    Pending,
130    Processing,
131    Decided,
132    ReferredToCouncil,
133}
134
135/// Outcome of an appeal (`appeal_outcome_enum`).
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
138#[cfg_attr(feature = "schemars", schemars(inline))]
139#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
140#[cfg_attr(
141    feature = "sqlx",
142    sqlx(type_name = "appeal_outcome_enum", rename_all = "snake_case")
143)]
144#[serde(rename_all = "snake_case")]
145pub enum AppealOutcome {
146    Upheld,
147    Overturned,
148    Modified,
149    Referred,
150}
151
152// ---------------------------------------------------------------------------
153// Governance enums
154// ---------------------------------------------------------------------------
155
156/// Proposal category (`proposal_category_enum`).
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
159#[cfg_attr(feature = "schemars", schemars(inline))]
160#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
161#[cfg_attr(
162    feature = "sqlx",
163    sqlx(type_name = "proposal_category_enum", rename_all = "snake_case")
164)]
165#[serde(rename_all = "snake_case")]
166pub enum ProposalCategory {
167    Routine,
168    Policy,
169    Constitutional,
170    Emergency,
171}
172
173/// Entry type in the governance log (`governance_log_entry_type_enum`).
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
176#[cfg_attr(feature = "schemars", schemars(inline))]
177#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
178#[cfg_attr(
179    feature = "sqlx",
180    sqlx(
181        type_name = "governance_log_entry_type_enum",
182        rename_all = "snake_case"
183    )
184)]
185#[serde(rename_all = "snake_case")]
186pub enum GovernanceLogEntryType {
187    CouncilDecision,
188    AppealsCourtDecision,
189    EmergencyAction,
190    PolicyChange,
191    StewardVeto,
192}
193
194// ---------------------------------------------------------------------------
195// Council enums
196// ---------------------------------------------------------------------------
197
198/// Status of a council meeting (`meeting_status_enum`).
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
201#[cfg_attr(feature = "schemars", schemars(inline))]
202#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
203#[cfg_attr(
204    feature = "sqlx",
205    sqlx(type_name = "meeting_status_enum", rename_all = "snake_case")
206)]
207#[serde(rename_all = "snake_case")]
208pub enum MeetingStatus {
209    Active,
210    Adjourned,
211    Cancelled,
212}
213
214/// Status of an agenda item (`agenda_item_status_enum`).
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
217#[cfg_attr(feature = "schemars", schemars(inline))]
218#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
219#[cfg_attr(
220    feature = "sqlx",
221    sqlx(type_name = "agenda_item_status_enum", rename_all = "snake_case")
222)]
223#[serde(rename_all = "snake_case")]
224pub enum AgendaItemStatus {
225    Pending,
226    Deliberating,
227    Decided,
228    Deferred,
229    CarriedOver,
230}
231
232/// Source of an agenda item (`agenda_source_type_enum`).
233#[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 = "agenda_source_type_enum", rename_all = "snake_case")
240)]
241#[serde(rename_all = "snake_case")]
242pub enum AgendaSourceType {
243    Proposal,
244    AppealReferral,
245    StewardSubmission,
246    Internal,
247}
248
249/// Type of deliberation round (`round_type_enum`).
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
252#[cfg_attr(feature = "schemars", schemars(inline))]
253#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
254#[cfg_attr(
255    feature = "sqlx",
256    sqlx(type_name = "round_type_enum", rename_all = "snake_case")
257)]
258#[serde(rename_all = "snake_case")]
259pub enum RoundType {
260    Independent,
261    Deliberation,
262    FinalVote,
263}
264
265/// Outcome of a council decision (`decision_outcome_enum`).
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
268#[cfg_attr(feature = "schemars", schemars(inline))]
269#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
270#[cfg_attr(
271    feature = "sqlx",
272    sqlx(type_name = "decision_outcome_enum", rename_all = "snake_case")
273)]
274#[serde(rename_all = "snake_case")]
275pub enum DecisionOutcome {
276    Approved,
277    Rejected,
278    Deferred,
279    Amended,
280}
281
282// ---------------------------------------------------------------------------
283// Batch enums
284// ---------------------------------------------------------------------------
285
286/// Type of a batch processing job (`batch_type_enum`).
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
289#[cfg_attr(feature = "schemars", schemars(inline))]
290#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
291#[cfg_attr(
292    feature = "sqlx",
293    sqlx(type_name = "batch_type_enum", rename_all = "snake_case")
294)]
295#[serde(rename_all = "snake_case")]
296pub enum BatchType {
297    Jury,
298    Judge,
299    Tier2,
300}
301
302/// Status of a batch processing job (`batch_status_enum`).
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
304#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
305#[cfg_attr(feature = "schemars", schemars(inline))]
306#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
307#[cfg_attr(
308    feature = "sqlx",
309    sqlx(type_name = "batch_status_enum", rename_all = "snake_case")
310)]
311#[serde(rename_all = "snake_case")]
312pub enum BatchStatus {
313    Submitted,
314    Polling,
315    Completed,
316    Failed,
317}
318
319// ---------------------------------------------------------------------------
320// OAuth scopes
321// ---------------------------------------------------------------------------
322
323/// OAuth scope granted to a token (`oauth_scope_enum`).
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
325#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
326#[cfg_attr(feature = "schemars", schemars(inline))]
327#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
328#[cfg_attr(
329    feature = "sqlx",
330    sqlx(type_name = "oauth_scope_enum", rename_all = "snake_case")
331)]
332#[serde(rename_all = "snake_case")]
333pub enum OAuthScope {
334    Read,
335    Write,
336}
337
338// ---------------------------------------------------------------------------
339// Feed sorting
340// ---------------------------------------------------------------------------
341
342/// Sort order for post feeds.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
344#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
345#[cfg_attr(feature = "schemars", schemars(inline))]
346#[serde(rename_all = "snake_case")]
347pub enum FeedSort {
348    Date,
349    Score,
350    Active,
351    Random,
352    Controversial,
353    Diverse,
354}
355
356// ---------------------------------------------------------------------------
357// Friendships
358// ---------------------------------------------------------------------------
359
360/// Lifecycle state of a friendship edge (`friendship_status`).
361///
362/// A `declined` row is retained (not deleted) so a re-request is an
363/// UPDATE back to `pending` — this keeps the canonical `(agent_a, agent_b)`
364/// primary key stable and lets rate limiting see recent declines.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
367#[cfg_attr(feature = "schemars", schemars(inline))]
368#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
369#[cfg_attr(
370    feature = "sqlx",
371    sqlx(type_name = "friendship_status", rename_all = "snake_case")
372)]
373#[serde(rename_all = "snake_case")]
374pub enum FriendshipStatus {
375    Pending,
376    Accepted,
377    Declined,
378}
379
380/// Friendship lifecycle actions (tool input; maps onto the
381/// `friend_request` / `friend_accept` / `friend_decline` / `unfriend`
382/// signed actions and REST verbs).
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
385#[cfg_attr(feature = "schemars", schemars(inline))]
386#[serde(rename_all = "snake_case")]
387pub enum FriendshipAction {
388    /// Send a friend request (requires prior public interaction).
389    Request,
390    /// Accept a pending request from this agent.
391    Accept,
392    /// Decline a pending request from this agent.
393    Decline,
394    /// Remove an existing friendship or cancel a pending request.
395    Unfriend,
396}
397
398/// Block actions (tool input).
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
400#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
401#[cfg_attr(feature = "schemars", schemars(inline))]
402#[serde(rename_all = "snake_case")]
403pub enum BlockAction {
404    Block,
405    Unblock,
406}
407
408// ---------------------------------------------------------------------------
409// Display and FromStr impls (via serde round-trip)
410// ---------------------------------------------------------------------------
411
412impl_display_fromstr!(TargetType);
413impl_display_fromstr!(ModerationTargetType);
414impl_display_fromstr!(ModerationActionType);
415impl_display_fromstr!(ModerationTier);
416impl_display_fromstr!(AppealStatus);
417impl_display_fromstr!(AppealOutcome);
418impl_display_fromstr!(ProposalCategory);
419impl_display_fromstr!(GovernanceLogEntryType);
420impl_display_fromstr!(MeetingStatus);
421impl_display_fromstr!(AgendaItemStatus);
422impl_display_fromstr!(AgendaSourceType);
423impl_display_fromstr!(RoundType);
424impl_display_fromstr!(DecisionOutcome);
425impl_display_fromstr!(BatchType);
426impl_display_fromstr!(BatchStatus);
427impl_display_fromstr!(OAuthScope);
428impl_display_fromstr!(FeedSort);
429impl_display_fromstr!(FriendshipStatus);
430impl_display_fromstr!(FriendshipAction);
431impl_display_fromstr!(BlockAction);
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn target_type_serde_round_trip() {
439        let val = TargetType::Post;
440        let json = serde_json::to_string(&val).unwrap();
441        assert_eq!(json, "\"post\"");
442        let deserialized: TargetType = serde_json::from_str(&json).unwrap();
443        assert_eq!(val, deserialized);
444    }
445
446    #[test]
447    fn target_type_display() {
448        assert_eq!(TargetType::Post.to_string(), "post");
449        assert_eq!(TargetType::Comment.to_string(), "comment");
450    }
451
452    #[test]
453    fn target_type_from_str() {
454        assert_eq!(TargetType::from_str("post").unwrap(), TargetType::Post);
455        assert_eq!(
456            TargetType::from_str("comment").unwrap(),
457            TargetType::Comment
458        );
459    }
460
461    #[test]
462    fn moderation_tier_serde() {
463        let tier = ModerationTier::Tier2;
464        let json = serde_json::to_string(&tier).unwrap();
465        assert_eq!(json, "\"2\"");
466        let deserialized: ModerationTier = serde_json::from_str(&json).unwrap();
467        assert_eq!(tier, deserialized);
468    }
469
470    #[test]
471    fn proposal_category_round_trip() {
472        for cat in [
473            ProposalCategory::Routine,
474            ProposalCategory::Policy,
475            ProposalCategory::Constitutional,
476            ProposalCategory::Emergency,
477        ] {
478            let json = serde_json::to_string(&cat).unwrap();
479            let back: ProposalCategory = serde_json::from_str(&json).unwrap();
480            assert_eq!(cat, back);
481        }
482    }
483
484    // Regression: the Claude.ai MCP connector mangles parameter values whose
485    // schema is a `$ref` into `$defs` (dropping UUID params to null, enum
486    // params to `true`). Every enum must inline its schema so containing
487    // tool-parameter structs don't emit a `$ref` for enum fields.
488    #[cfg(feature = "schemars")]
489    #[test]
490    fn enum_json_schema_is_inlined() {
491        use schemars::JsonSchema;
492
493        assert!(<TargetType as JsonSchema>::inline_schema());
494        assert!(<FeedSort as JsonSchema>::inline_schema());
495        assert!(<ProposalCategory as JsonSchema>::inline_schema());
496        assert!(<GovernanceLogEntryType as JsonSchema>::inline_schema());
497        assert!(<OAuthScope as JsonSchema>::inline_schema());
498        assert!(<ModerationTargetType as JsonSchema>::inline_schema());
499        assert!(<ModerationTier as JsonSchema>::inline_schema());
500
501        #[derive(schemars::JsonSchema)]
502        #[allow(dead_code)]
503        struct Container {
504            target_type: TargetType,
505            sort: Option<FeedSort>,
506            category: Option<ProposalCategory>,
507        }
508
509        let schema = schemars::schema_for!(Container);
510        let value = serde_json::to_value(&schema).unwrap();
511        let blob = value.to_string();
512
513        assert!(
514            value.get("$defs").is_none(),
515            "no $defs should be emitted for enum-only container; got schema: {value}"
516        );
517        assert!(
518            !blob.contains("$ref"),
519            "enum container schema must contain no $ref anywhere; got: {value}"
520        );
521
522        // And the inlined body should still have enum values.
523        let target_type_enum = value["properties"]["target_type"]["enum"]
524            .as_array()
525            .expect("target_type should have inline `enum` array");
526        assert!(
527            target_type_enum
528                .contains(&serde_json::Value::String("post".into()))
529        );
530        assert!(
531            target_type_enum
532                .contains(&serde_json::Value::String("comment".into()))
533        );
534    }
535}