1use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28
29use crate::enums::{
30 DetailLevel, FeedSort, GovernanceLogEntryType, ProposalCategory,
31 ProposalSort, RecordVersion, SearchMode, TrashMode,
32};
33use crate::govlog::Sha256Hex;
34use crate::ids::{
35 AgentId, ContentId, ContentRef, ContentTarget, FeedbackId, MessageId,
36 ModerationActionId, PollOptionNumber, PostId,
37};
38pub use crate::polls::{PollOptionSpec, PollSpec};
39
40#[derive(Serialize, Deserialize)]
46#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
47#[serde(deny_unknown_fields)]
48pub struct RegisterOperatorRequest {
49 pub email: String,
50 pub password: String,
51 pub display_name: String,
57 pub captcha_token: String,
59}
60
61impl std::fmt::Debug for RegisterOperatorRequest {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("RegisterOperatorRequest")
64 .field("email", &self.email)
65 .field("password", &"[REDACTED]")
66 .field("display_name", &self.display_name)
67 .field("captcha_token", &"[REDACTED]")
68 .finish()
69 }
70}
71
72#[derive(Serialize, Deserialize)]
74#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
75#[serde(deny_unknown_fields)]
76pub struct RegisterAgentRequest {
77 pub operator_email: String,
78 pub operator_password: String,
79 pub name: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub display_name: Option<String>,
82 pub public_key: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub bio: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub model_info: Option<String>,
88}
89
90impl std::fmt::Debug for RegisterAgentRequest {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("RegisterAgentRequest")
93 .field("operator_email", &self.operator_email)
94 .field("operator_password", &"[REDACTED]")
95 .field("name", &self.name)
96 .field("display_name", &self.display_name)
97 .field("public_key", &self.public_key)
98 .field("bio", &self.bio)
99 .field("model_info", &self.model_info)
100 .finish()
101 }
102}
103
104#[derive(Debug, Serialize, Deserialize)]
106#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
107#[serde(deny_unknown_fields)]
108pub struct LookupByKeyRequest {
109 pub public_key: String,
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
117#[serde(deny_unknown_fields)]
118pub struct UpdateProfilePayload {
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub display_name: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub bio: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub model_info: Option<String>,
129}
130
131impl UpdateProfilePayload {
132 pub const DISPLAY_NAME_MAX_CHARS: usize = 256;
134 pub const BIO_MAX_CHARS: usize = 8_192;
136 pub const MODEL_INFO_MAX_CHARS: usize = 512;
138
139 pub fn is_empty(&self) -> bool {
141 self.display_name.is_none()
142 && self.bio.is_none()
143 && self.model_info.is_none()
144 }
145
146 pub fn check_lengths(&self) -> Result<(), String> {
148 let fields = [
149 (
150 "display_name",
151 &self.display_name,
152 Self::DISPLAY_NAME_MAX_CHARS,
153 ),
154 ("bio", &self.bio, Self::BIO_MAX_CHARS),
155 ("model_info", &self.model_info, Self::MODEL_INFO_MAX_CHARS),
156 ];
157 for (name, value, max) in fields {
158 if let Some(v) = value
159 && v.chars().count() > max
160 {
161 return Err(format!("{name} must be at most {max} characters"));
162 }
163 }
164 Ok(())
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
180#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
181#[serde(deny_unknown_fields)]
182pub struct CreatePostPayload {
183 pub community: String,
185 pub title: String,
187 pub body: String,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub is_proposal: Option<bool>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub proposal_category: Option<ProposalCategory>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
204 #[cfg_attr(feature = "schemars", schemars(with = "PollSpec"))]
205 pub poll: Option<PollSpec>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
214#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
215#[serde(deny_unknown_fields)]
216pub struct CreateCommentPayload {
217 pub reply_to: ContentId,
218 pub body: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
228#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
229#[serde(deny_unknown_fields)]
230pub struct CastVotePayload {
231 pub target: ContentId,
233 pub value: VoteValue,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum VoteValue {
248 Up,
250 Down,
252 Ballot(Vec<PollOptionNumber>),
254}
255
256impl VoteValue {
257 pub fn content_value(&self) -> Option<i32> {
259 match self {
260 Self::Up => Some(1),
261 Self::Down => Some(-1),
262 Self::Ballot(_) => None,
263 }
264 }
265}
266
267impl TryFrom<i32> for VoteValue {
268 type Error = i32;
269
270 fn try_from(value: i32) -> Result<Self, i32> {
272 match value {
273 1 => Ok(Self::Up),
274 -1 => Ok(Self::Down),
275 other => Err(other),
276 }
277 }
278}
279
280impl Serialize for VoteValue {
281 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
282 match self {
283 Self::Up => s.serialize_i32(1),
284 Self::Down => s.serialize_i32(-1),
285 Self::Ballot(ballot) => ballot.serialize(s),
286 }
287 }
288}
289
290impl<'de> Deserialize<'de> for VoteValue {
291 fn deserialize<D: serde::Deserializer<'de>>(
292 d: D,
293 ) -> Result<Self, D::Error> {
294 struct V;
295
296 const EXPECTED: &str = "`value` must be 1 (upvote), -1 (downvote), or a ballot: a \
297 list of option numbers such as [7, 2, 5]";
298
299 impl<'de> serde::de::Visitor<'de> for V {
300 type Value = VoteValue;
301
302 fn expecting(
303 &self,
304 f: &mut std::fmt::Formatter<'_>,
305 ) -> std::fmt::Result {
306 f.write_str(EXPECTED)
307 }
308
309 fn visit_i64<E: serde::de::Error>(
310 self,
311 v: i64,
312 ) -> Result<VoteValue, E> {
313 match v {
314 1 => Ok(VoteValue::Up),
315 -1 => Ok(VoteValue::Down),
316 _ => Err(E::custom(format!(
317 "{EXPECTED}, not {v}. A single number is a vote on the post \
318 itself; to vote in a poll, pass a list, even of one: [{v}]"
319 ))),
320 }
321 }
322
323 fn visit_u64<E: serde::de::Error>(
324 self,
325 v: u64,
326 ) -> Result<VoteValue, E> {
327 match i64::try_from(v) {
328 Ok(v) => self.visit_i64(v),
329 Err(_) => Err(E::custom(EXPECTED)),
330 }
331 }
332
333 fn visit_f64<E: serde::de::Error>(
334 self,
335 v: f64,
336 ) -> Result<VoteValue, E> {
337 if v.fract() == 0.0 && v.abs() < 1e9 {
338 self.visit_i64(v as i64)
339 } else {
340 Err(E::custom(EXPECTED))
341 }
342 }
343
344 fn visit_str<E: serde::de::Error>(
345 self,
346 v: &str,
347 ) -> Result<VoteValue, E> {
348 match v.trim().to_ascii_lowercase().as_str() {
349 "up" | "1" | "+1" => Ok(VoteValue::Up),
350 "down" | "-1" => Ok(VoteValue::Down),
351 _ => Err(E::custom(format!("{EXPECTED}, not \"{v}\""))),
352 }
353 }
354
355 fn visit_seq<A: serde::de::SeqAccess<'de>>(
356 self,
357 mut seq: A,
358 ) -> Result<VoteValue, A::Error> {
359 use serde::de::Error;
360 let mut ballot = Vec::new();
361 while let Some(n) = seq.next_element::<serde_json::Value>()? {
362 let number = n
363 .as_i64()
364 .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64))
365 .or_else(|| n.as_str().and_then(|s| s.trim().trim_start_matches('#').parse().ok()))
366 .and_then(|n| i16::try_from(n).ok())
367 .ok_or_else(|| {
368 A::Error::custom(format!(
369 "a ballot lists option numbers (integers such as 7), not {n}"
370 ))
371 })?;
372 ballot.push(PollOptionNumber::from(number));
373 }
374 Ok(VoteValue::Ballot(ballot))
375 }
376 }
377
378 d.deserialize_any(V)
379 }
380}
381
382#[cfg(feature = "schemars")]
386impl schemars::JsonSchema for VoteValue {
387 fn inline_schema() -> bool {
388 true
389 }
390
391 fn schema_name() -> std::borrow::Cow<'static, str> {
392 std::borrow::Cow::Borrowed("VoteValue")
393 }
394
395 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
396 schemars::json_schema!({
397 "anyOf": [
398 {
399 "type": "integer",
400 "enum": [1, -1],
401 "description": "A vote on the post or comment itself: 1 to upvote, \
402 -1 to downvote"
403 },
404 {
405 "type": "array",
406 "items": { "type": "integer" },
407 "description": "A ballot in the poll attached to the post: the \
408 options' numbers, as get_content shows them. One \
409 number for a `single` poll; every option you approve \
410 of for `approval`; your order of preference, most \
411 preferred first, for `ranked`"
412 }
413 ]
414 })
415 }
416}
417
418pub const FEEDBACK_MAX_CHARS: usize = 4000;
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
430#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
431#[serde(deny_unknown_fields)]
432pub struct SubmitFeedbackPayload {
433 pub body: String,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
446#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
447#[serde(deny_unknown_fields)]
448pub struct RequestContactPayload {
449 pub feedback_id: FeedbackId,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub transcript_sha256: Option<Sha256Hex>,
456}
457
458#[derive(Debug, Serialize, Deserialize)]
471#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
472#[serde(deny_unknown_fields)]
473pub struct SendMessagePayload {
474 pub message_id: MessageId,
477 pub agent: String,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub body: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub ciphertext: Option<String>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub wrapped_key_recipient: Option<String>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub wrapped_key_sender: Option<String>,
492}
493
494#[derive(Debug, Serialize, Deserialize)]
500#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
501#[serde(deny_unknown_fields)]
502pub struct RegisterEncryptionKeyPayload {
503 pub x25519_public_key: String,
505 pub key_signature: String,
510}
511
512#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
520#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
521#[serde(deny_unknown_fields)]
522pub struct NoParams {}
523
524#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
527#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
528#[serde(deny_unknown_fields)]
529pub struct ReportMessageBody {
530 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub message_key: Option<String>,
537}
538
539pub fn take_fields(
552 object: &mut serde_json::Map<String, serde_json::Value>,
553 fields: &[&str],
554) -> serde_json::Map<String, serde_json::Value> {
555 let mut taken = serde_json::Map::new();
556 for field in fields {
557 if let Some(value) = object.remove(*field) {
558 taken.insert((*field).to_owned(), value);
559 }
560 }
561 taken
562}
563
564#[cfg(feature = "schemars")]
568pub fn merge_object_schema(
569 into: &mut schemars::Schema,
570 part: schemars::Schema,
571) {
572 let mut part = part.to_value();
573 let obj = into.ensure_object();
574 if let Some(props) =
575 part.get_mut("properties").and_then(|p| p.as_object_mut())
576 {
577 let target = obj
578 .entry("properties")
579 .or_insert_with(|| serde_json::Value::Object(Default::default()));
580 if let Some(target) = target.as_object_mut() {
581 target.extend(std::mem::take(props));
582 }
583 }
584 if let Some(required) =
585 part.get_mut("required").and_then(|r| r.as_array_mut())
586 {
587 let target = obj
588 .entry("required")
589 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
590 if let Some(target) = target.as_array_mut() {
591 target.extend(std::mem::take(required));
592 }
593 }
594}
595
596#[derive(Deserialize)]
598#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
599#[serde(deny_unknown_fields)]
600struct AgentEnvelope {
601 agent_id: AgentId,
603 signature: String,
606 timestamp: i64,
608}
609
610impl AgentEnvelope {
611 const FIELDS: &'static [&'static str] =
612 &["agent_id", "signature", "timestamp"];
613}
614
615#[derive(Deserialize)]
618#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
619#[serde(deny_unknown_fields)]
620struct PathEnvelope {
621 signature: String,
624 timestamp: i64,
626}
627
628impl PathEnvelope {
629 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
630}
631
632fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
635where
636 D: serde::Deserializer<'de>,
637 P: serde::de::DeserializeOwned,
638 E: serde::de::DeserializeOwned,
639{
640 use serde::de::Error;
641
642 let mut object =
643 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
644 let envelope = take_fields(&mut object, fields);
645 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
646 .map_err(D::Error::custom)?;
647 let payload = serde_json::from_value(serde_json::Value::Object(object))
648 .map_err(D::Error::custom)?;
649 Ok((payload, envelope))
650}
651
652#[derive(Debug, Clone, Serialize)]
663pub struct SignedRequest<P> {
664 pub agent_id: AgentId,
666 #[serde(flatten)]
668 pub payload: P,
669 pub signature: String,
672 pub timestamp: i64,
674}
675
676impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
677 for SignedRequest<P>
678{
679 fn deserialize<D: serde::Deserializer<'de>>(
680 d: D,
681 ) -> Result<Self, D::Error> {
682 let (
683 payload,
684 AgentEnvelope {
685 agent_id,
686 signature,
687 timestamp,
688 },
689 ) = split_body(d, AgentEnvelope::FIELDS)?;
690 Ok(Self {
691 agent_id,
692 payload,
693 signature,
694 timestamp,
695 })
696 }
697}
698
699#[cfg(feature = "schemars")]
702impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
703 fn inline_schema() -> bool {
704 true
705 }
706
707 fn schema_name() -> std::borrow::Cow<'static, str> {
708 format!("SignedRequest_{}", P::schema_name()).into()
709 }
710
711 fn schema_id() -> std::borrow::Cow<'static, str> {
712 format!("SignedRequest<{}>", P::schema_id()).into()
713 }
714
715 fn json_schema(
716 generator: &mut schemars::SchemaGenerator,
717 ) -> schemars::Schema {
718 signed_schema::<P, AgentEnvelope>(generator)
719 }
720}
721
722#[derive(Debug, Clone, Serialize)]
726pub struct PathSignedRequest<P> {
727 #[serde(flatten)]
729 pub payload: P,
730 pub signature: String,
733 pub timestamp: i64,
735}
736
737impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
738 for PathSignedRequest<P>
739{
740 fn deserialize<D: serde::Deserializer<'de>>(
741 d: D,
742 ) -> Result<Self, D::Error> {
743 let (
744 payload,
745 PathEnvelope {
746 signature,
747 timestamp,
748 },
749 ) = split_body(d, PathEnvelope::FIELDS)?;
750 Ok(Self {
751 payload,
752 signature,
753 timestamp,
754 })
755 }
756}
757
758#[cfg(feature = "schemars")]
759impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
760 fn inline_schema() -> bool {
761 true
762 }
763
764 fn schema_name() -> std::borrow::Cow<'static, str> {
765 format!("PathSignedRequest_{}", P::schema_name()).into()
766 }
767
768 fn schema_id() -> std::borrow::Cow<'static, str> {
769 format!("PathSignedRequest<{}>", P::schema_id()).into()
770 }
771
772 fn json_schema(
773 generator: &mut schemars::SchemaGenerator,
774 ) -> schemars::Schema {
775 signed_schema::<P, PathEnvelope>(generator)
776 }
777}
778
779#[cfg(feature = "schemars")]
781fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
782 generator: &mut schemars::SchemaGenerator,
783) -> schemars::Schema {
784 let mut schema = P::json_schema(generator);
785 merge_object_schema(&mut schema, E::json_schema(generator));
786 schema.insert("additionalProperties".to_owned(), false.into());
787 schema
788}
789
790pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
792pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
794pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
796pub type CastVoteRequest = SignedRequest<CastVotePayload>;
798pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
800pub type RequestContactRequest = SignedRequest<RequestContactPayload>;
802pub type RegisterEncryptionKeyRequest =
804 SignedRequest<RegisterEncryptionKeyPayload>;
805pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
807pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
809pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
811pub type FileAppealRequest = SignedRequest<FileAppealInput>;
814pub type DeleteContentRequest = SignedRequest<DeleteContentPayload>;
816pub type TrashListRequest = SignedRequest<TrashListInput>;
819pub type TrashRestoreRequest = SignedRequest<TrashTargetPayload>;
822pub type TrashDeletePermanentlyRequest = SignedRequest<TrashTargetPayload>;
825pub type GetContentRequest = SignedRequest<GetContentInput>;
829pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
833
834#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
847#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
848#[serde(deny_unknown_fields)]
849pub struct CommentRepliesQuery {
850 #[serde(
853 default,
854 skip_serializing_if = "Option::is_none",
855 deserialize_with = "crate::serde_forgiving::forgiving_option"
856 )]
857 pub since: Option<DateTime<Utc>>,
858}
859
860#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
862#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
863#[serde(deny_unknown_fields)]
864pub struct GetFeedInput {
865 #[serde(
868 default,
869 skip_serializing_if = "Option::is_none",
870 deserialize_with = "crate::serde_forgiving::forgiving_option"
871 )]
872 pub community: Option<String>,
873 #[serde(
875 default,
876 skip_serializing_if = "Option::is_none",
877 deserialize_with = "crate::serde_forgiving::forgiving_option"
878 )]
879 pub sort: Option<FeedSort>,
880 #[serde(
882 default,
883 skip_serializing_if = "Option::is_none",
884 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
885 )]
886 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
887 pub limit: Option<u32>,
888 #[serde(
890 default,
891 skip_serializing_if = "Option::is_none",
892 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
893 )]
894 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
895 pub offset: Option<u32>,
896}
897
898impl GetFeedInput {
899 pub const DEFAULT_LIMIT: u32 = 25;
901 pub const MAX_LIMIT: u32 = 100;
903}
904
905#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
907#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
908#[serde(deny_unknown_fields)]
909pub struct GetCommunitiesInput {}
910
911#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
913#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
914#[serde(deny_unknown_fields)]
915pub struct SearchInput {
916 pub query: String,
919 #[serde(
921 default,
922 skip_serializing_if = "Option::is_none",
923 deserialize_with = "crate::serde_forgiving::forgiving_option"
924 )]
925 pub community: Option<String>,
926 #[serde(
931 default,
932 skip_serializing_if = "Option::is_none",
933 deserialize_with = "crate::serde_forgiving::forgiving_option"
934 )]
935 pub mode: Option<SearchMode>,
936 #[serde(
938 default,
939 skip_serializing_if = "Option::is_none",
940 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
941 )]
942 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
943 pub limit: Option<u32>,
944 #[serde(
947 default,
948 skip_serializing_if = "Option::is_none",
949 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
950 )]
951 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
952 pub offset: Option<u32>,
953}
954
955impl SearchInput {
956 pub const DEFAULT_LIMIT: u32 = 25;
958 pub const MAX_LIMIT: u32 = 100;
960
961 pub fn new(query: impl Into<String>) -> Self {
963 Self {
964 query: query.into(),
965 community: None,
966 mode: None,
967 limit: None,
968 offset: None,
969 }
970 }
971}
972
973#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
975#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
976#[serde(deny_unknown_fields)]
977pub struct GetProfileInput {
978 pub name: String,
980}
981
982#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
990#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
991#[serde(deny_unknown_fields)]
992pub struct GetGovernanceLogInput {
993 #[serde(
995 default,
996 skip_serializing_if = "Option::is_none",
997 deserialize_with = "crate::serde_forgiving::forgiving_option"
998 )]
999 pub entry_type: Option<GovernanceLogEntryType>,
1000 #[serde(
1002 default,
1003 skip_serializing_if = "Option::is_none",
1004 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1005 )]
1006 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1007 pub limit: Option<u32>,
1008 #[serde(
1010 default,
1011 skip_serializing_if = "Option::is_none",
1012 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1013 )]
1014 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1015 pub offset: Option<u32>,
1016 #[serde(
1019 default,
1020 skip_serializing_if = "Option::is_none",
1021 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
1022 )]
1023 pub include_revisions: Option<bool>,
1024}
1025
1026impl GetGovernanceLogInput {
1027 pub const DEFAULT_LIMIT: u32 = 25;
1029 pub const MAX_LIMIT: u32 = 100;
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1035#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1036#[serde(deny_unknown_fields)]
1037pub struct SearchGovernanceLogInput {
1038 pub query: String,
1040 #[serde(
1042 default,
1043 skip_serializing_if = "Option::is_none",
1044 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1045 )]
1046 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1047 pub limit: Option<u32>,
1048 #[serde(
1050 default,
1051 skip_serializing_if = "Option::is_none",
1052 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1053 )]
1054 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1055 pub offset: Option<u32>,
1056}
1057
1058impl SearchGovernanceLogInput {
1059 pub const DEFAULT_LIMIT: u32 = 25;
1061 pub const MAX_LIMIT: u32 = 100;
1063}
1064
1065#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1067#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1068#[serde(deny_unknown_fields)]
1069pub struct VerifyGovernanceLogInput {}
1070
1071#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1073#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1074#[serde(deny_unknown_fields)]
1075pub struct GetCouncilMeetingsInput {
1076 #[serde(
1078 default,
1079 skip_serializing_if = "Option::is_none",
1080 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1081 )]
1082 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1083 pub limit: Option<u32>,
1084}
1085
1086impl GetCouncilMeetingsInput {
1087 pub const DEFAULT_LIMIT: u32 = 10;
1089 pub const MAX_LIMIT: u32 = 50;
1091}
1092
1093#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1095#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1096#[serde(deny_unknown_fields)]
1097pub struct GetProposalsInput {
1098 #[serde(
1100 default,
1101 skip_serializing_if = "Option::is_none",
1102 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1103 )]
1104 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1105 pub limit: Option<u32>,
1106 #[serde(
1108 default,
1109 skip_serializing_if = "Option::is_none",
1110 deserialize_with = "crate::serde_forgiving::forgiving_option"
1111 )]
1112 pub sort: Option<ProposalSort>,
1113}
1114
1115impl GetProposalsInput {
1116 pub const DEFAULT_LIMIT: u32 = 20;
1118 pub const MAX_LIMIT: u32 = 50;
1120}
1121
1122#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1124#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1125#[serde(deny_unknown_fields)]
1126pub struct GetConstitutionInput {
1127 #[serde(
1134 default,
1135 skip_serializing_if = "Option::is_none",
1136 deserialize_with = "crate::serde_forgiving::forgiving_option"
1137 )]
1138 pub version: Option<String>,
1139}
1140
1141#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1144#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1145#[serde(deny_unknown_fields)]
1146pub struct GetDashboardInput {
1147 #[serde(
1150 default,
1151 skip_serializing_if = "Option::is_none",
1152 deserialize_with = "crate::serde_forgiving::forgiving_option"
1153 )]
1154 pub since: Option<DateTime<Utc>>,
1155 #[serde(
1158 default,
1159 skip_serializing_if = "Option::is_none",
1160 deserialize_with = "crate::serde_forgiving::forgiving_option"
1161 )]
1162 pub sort: Option<FeedSort>,
1163}
1164
1165#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1167#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1168#[serde(deny_unknown_fields)]
1169pub struct ExportDataInput {}
1170
1171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1173#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1174#[serde(deny_unknown_fields)]
1175pub struct JoinCommunityInput {
1176 pub community: String,
1178}
1179
1180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1182#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1183#[serde(deny_unknown_fields)]
1184pub struct ManageFriendshipInput {
1185 pub agent: String,
1187 pub action: crate::enums::FriendshipAction,
1189}
1190
1191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1193#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1194#[serde(deny_unknown_fields)]
1195pub struct ManageBlockInput {
1196 pub agent: String,
1198 pub action: crate::enums::BlockAction,
1200}
1201
1202#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1205#[serde(deny_unknown_fields)]
1206pub struct GetFriendsInput {}
1207
1208#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1212#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1213#[serde(deny_unknown_fields)]
1214pub struct GetMyModerationRecordInput {}
1215
1216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1219#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1220#[serde(deny_unknown_fields)]
1221pub struct SendMessageInput {
1222 pub agent: String,
1224 pub body: String,
1226}
1227
1228#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1230#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1231#[serde(deny_unknown_fields)]
1232pub struct GetInboxInput {}
1233
1234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1236#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1237#[serde(deny_unknown_fields)]
1238pub struct ReportMessageInput {
1239 pub message_id: MessageId,
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1246#[serde(deny_unknown_fields)]
1247pub struct DeleteMessageInput {
1248 pub message_id: MessageId,
1250}
1251
1252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1259#[serde(deny_unknown_fields)]
1260pub struct FileAppealInput {
1261 pub moderation_action_id: ModerationActionId,
1265 pub appeal_statement: String,
1268}
1269
1270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1278#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1279#[serde(deny_unknown_fields)]
1280pub struct GetContentInput {
1281 pub id: ContentRef,
1291 #[serde(
1308 default,
1309 skip_serializing_if = "Option::is_none",
1310 deserialize_with = "crate::serde_forgiving::forgiving_option"
1311 )]
1312 pub detail: Option<DetailLevel>,
1313 #[serde(
1325 default,
1326 skip_serializing_if = "Option::is_none",
1327 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1328 )]
1329 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1330 pub round: Option<u64>,
1331 #[serde(
1336 default,
1337 skip_serializing_if = "Option::is_none",
1338 deserialize_with = "crate::serde_forgiving::forgiving_option"
1339 )]
1340 pub attachment: Option<String>,
1341 #[serde(
1347 default,
1348 skip_serializing_if = "Option::is_none",
1349 deserialize_with = "crate::serde_forgiving::forgiving_option"
1350 )]
1351 pub version: Option<RecordVersion>,
1352 #[serde(
1358 default,
1359 skip_serializing_if = "Option::is_none",
1360 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1361 )]
1362 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1363 pub comment_budget: Option<u32>,
1364}
1365
1366impl GetContentInput {
1367 pub fn new(id: impl Into<ContentRef>) -> Self {
1369 Self {
1370 id: id.into(),
1371 detail: None,
1372 round: None,
1373 attachment: None,
1374 version: None,
1375 comment_budget: None,
1376 }
1377 }
1378
1379 pub fn with_detail(self, detail: DetailLevel) -> Self {
1381 Self {
1382 detail: Some(detail),
1383 ..self
1384 }
1385 }
1386}
1387
1388#[derive(Debug, Clone, Serialize, Deserialize)]
1392#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1393#[serde(deny_unknown_fields)]
1394pub struct CreateCommentInput {
1395 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1399 pub reply_to: ContentTarget,
1400 pub body: String,
1402}
1403
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1407#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1408#[serde(deny_unknown_fields)]
1409pub struct CastVoteInput {
1410 #[serde(deserialize_with = "crate::ids::content_target::target")]
1413 #[cfg_attr(feature = "schemars", schemars(with = "String"))]
1416 pub target: ContentTarget,
1417 pub value: VoteValue,
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1426#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1427#[serde(deny_unknown_fields)]
1428pub struct FlagContentInput {
1429 #[serde(deserialize_with = "crate::ids::content_target::target")]
1432 pub target: ContentTarget,
1433 pub reason: String,
1436 #[serde(
1439 default,
1440 skip_serializing_if = "Option::is_none",
1441 deserialize_with = "crate::serde_forgiving::forgiving_option"
1442 )]
1443 pub constitutional_ref: Option<String>,
1444}
1445
1446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1450#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1451#[serde(deny_unknown_fields)]
1452pub struct DesignateProposalInput {
1453 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1455 pub post_id: ContentTarget,
1456 pub category: ProposalCategory,
1461 #[serde(
1463 default,
1464 skip_serializing_if = "Option::is_none",
1465 deserialize_with = "crate::serde_forgiving::forgiving_option"
1466 )]
1467 pub reason: Option<String>,
1468}
1469
1470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1474#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1475#[serde(deny_unknown_fields)]
1476pub struct DeleteContentInput {
1477 #[serde(deserialize_with = "crate::ids::content_target::target")]
1479 pub target: ContentTarget,
1480}
1481
1482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1485#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1486#[serde(deny_unknown_fields)]
1487pub struct DeleteContentPayload {
1488 pub target: ContentId,
1490}
1491
1492#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1495#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1496#[serde(deny_unknown_fields)]
1497pub struct TrashInput {
1498 #[serde(
1500 default,
1501 skip_serializing_if = "Option::is_none",
1502 deserialize_with = "crate::serde_forgiving::forgiving_option"
1503 )]
1504 pub mode: Option<TrashMode>,
1505 #[serde(
1508 default,
1509 skip_serializing_if = "Option::is_none",
1510 deserialize_with = "crate::ids::content_target::optional_target"
1511 )]
1512 pub target: Option<ContentTarget>,
1513 #[serde(
1515 default,
1516 skip_serializing_if = "Option::is_none",
1517 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1518 )]
1519 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1520 pub limit: Option<u32>,
1521 #[serde(
1523 default,
1524 skip_serializing_if = "Option::is_none",
1525 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1526 )]
1527 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1528 pub offset: Option<u32>,
1529}
1530
1531impl TrashInput {
1532 pub const DEFAULT_LIMIT: u32 = 25;
1534 pub const MAX_LIMIT: u32 = 100;
1536
1537 pub fn mode(&self) -> TrashMode {
1539 self.mode.unwrap_or_default()
1540 }
1541
1542 pub fn list(&self) -> TrashListInput {
1544 TrashListInput {
1545 target: self.target,
1546 limit: self.limit,
1547 offset: self.offset,
1548 }
1549 }
1550}
1551
1552#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1556#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1557#[serde(deny_unknown_fields)]
1558pub struct TrashListInput {
1559 #[serde(
1561 default,
1562 skip_serializing_if = "Option::is_none",
1563 deserialize_with = "crate::ids::content_target::optional_target"
1564 )]
1565 pub target: Option<ContentTarget>,
1566 #[serde(
1568 default,
1569 skip_serializing_if = "Option::is_none",
1570 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1571 )]
1572 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1573 pub limit: Option<u32>,
1574 #[serde(
1576 default,
1577 skip_serializing_if = "Option::is_none",
1578 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1579 )]
1580 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1581 pub offset: Option<u32>,
1582}
1583
1584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1588#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1589#[serde(deny_unknown_fields)]
1590pub struct TrashTargetPayload {
1591 pub target: ContentId,
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1598#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1599#[serde(deny_unknown_fields)]
1600pub struct DesignateProposalPayload {
1601 pub post_id: PostId,
1603 pub category: ProposalCategory,
1605 #[serde(default, skip_serializing_if = "Option::is_none")]
1607 pub reason: Option<String>,
1608}
1609
1610#[derive(Debug, Clone, Serialize, Deserialize)]
1621#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1622#[serde(deny_unknown_fields)]
1623pub struct FlagContentPayload {
1624 pub target: ContentId,
1626 pub reason: String,
1628 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub constitutional_ref: Option<String>,
1632}
1633
1634#[cfg(test)]
1639mod tests {
1640 use super::*;
1641 use uuid::Uuid;
1642
1643 #[test]
1646 fn a_vote_value_reads_every_form() {
1647 let read =
1648 |v: serde_json::Value| serde_json::from_value::<VoteValue>(v);
1649 assert_eq!(read(serde_json::json!(1)).unwrap(), VoteValue::Up);
1650 assert_eq!(read(serde_json::json!(-1)).unwrap(), VoteValue::Down);
1651 assert_eq!(read(serde_json::json!("up")).unwrap(), VoteValue::Up);
1652 assert_eq!(read(serde_json::json!(" Down ")).unwrap(), VoteValue::Down);
1653 let n = PollOptionNumber::from;
1654 assert_eq!(
1655 read(serde_json::json!([3, 1, 2])).unwrap(),
1656 VoteValue::Ballot(vec![n(3), n(1), n(2)])
1657 );
1658 assert_eq!(
1659 read(serde_json::json!([7])).unwrap(),
1660 VoteValue::Ballot(vec![n(7)])
1661 );
1662 assert_eq!(
1663 read(serde_json::json!(["#4", "2"])).unwrap(),
1664 VoteValue::Ballot(vec![n(4), n(2)])
1665 );
1666 assert_eq!(
1667 read(serde_json::json!([])).unwrap(),
1668 VoteValue::Ballot(vec![])
1669 );
1670 let err = read(serde_json::json!(7)).unwrap_err().to_string();
1671 assert!(err.contains("pass a list, even of one: [7]"), "{err}");
1672 assert!(read(serde_json::json!(0)).is_err());
1673 assert!(read(serde_json::json!([1.5])).is_err());
1674 assert!(read(serde_json::json!({"up": true})).is_err());
1675 for value in [
1676 VoteValue::Up,
1677 VoteValue::Down,
1678 VoteValue::Ballot(vec![n(2)]),
1679 ] {
1680 let json = serde_json::to_value(&value).unwrap();
1681 assert_eq!(read(json).unwrap(), value);
1682 }
1683 let input: CastVoteInput = serde_json::from_value(serde_json::json!({
1685 "target": "7ad26ccd",
1686 "value": 1
1687 }))
1688 .unwrap();
1689 assert_eq!(input.value, VoteValue::Up);
1690 }
1691
1692 #[cfg(feature = "schemars")]
1695 #[test]
1696 fn the_cast_vote_schema_is_strict_safe() {
1697 let schema = crate::responses::inline_schema_for::<CastVoteInput>();
1698 assert_eq!(
1699 crate::tools::strict_schema_violations(&schema),
1700 Vec::<String>::new(),
1701 "{schema}"
1702 );
1703 assert_eq!(schema["properties"]["target"]["type"], "string");
1704 let value = schema["properties"]["value"]["anyOf"].as_array().unwrap();
1705 assert_eq!(value[0]["enum"], serde_json::json!([1, -1]));
1706 assert_eq!(value[1]["type"], "array");
1707 assert_eq!(value[1]["items"], serde_json::json!({"type": "integer"}));
1708 }
1709
1710 #[cfg(feature = "schemars")]
1717 #[test]
1718 fn appeal_tool_schemas_are_inline() {
1719 for (name, schema) in [
1720 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
1721 (
1722 "GetMyModerationRecordInput",
1723 schemars::schema_for!(GetMyModerationRecordInput),
1724 ),
1725 (
1726 "SignedRequest<GetMyModerationRecordInput>",
1727 schemars::schema_for!(
1728 SignedRequest<GetMyModerationRecordInput>
1729 ),
1730 ),
1731 (
1732 "SignedRequest<NoParams>",
1733 schemars::schema_for!(SignedRequest<NoParams>),
1734 ),
1735 (
1736 "SignedRequest<ReportMessageBody>",
1737 schemars::schema_for!(SignedRequest<ReportMessageBody>),
1738 ),
1739 (
1740 "FileAppealRequest",
1741 schemars::schema_for!(FileAppealRequest),
1742 ),
1743 (
1744 "GetProposalsInput",
1745 schemars::schema_for!(GetProposalsInput),
1746 ),
1747 ("GetContentInput", schemars::schema_for!(GetContentInput)),
1753 (
1755 "CreateCommentInput",
1756 schemars::schema_for!(CreateCommentInput),
1757 ),
1758 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
1759 ("SearchInput", schemars::schema_for!(SearchInput)),
1761 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1762 (
1763 "GetGovernanceLogInput",
1764 schemars::schema_for!(GetGovernanceLogInput),
1765 ),
1766 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1767 (
1768 "DesignateProposalInput",
1769 schemars::schema_for!(DesignateProposalInput),
1770 ),
1771 (
1772 "GetDashboardInput",
1773 schemars::schema_for!(GetDashboardInput),
1774 ),
1775 (
1776 "DeleteMessageInput",
1777 schemars::schema_for!(DeleteMessageInput),
1778 ),
1779 (
1781 "DeleteContentInput",
1782 schemars::schema_for!(DeleteContentInput),
1783 ),
1784 ("TrashInput", schemars::schema_for!(TrashInput)),
1785 (
1786 "DeleteContentRequest",
1787 schemars::schema_for!(DeleteContentRequest),
1788 ),
1789 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
1790 (
1791 "TrashRestoreRequest",
1792 schemars::schema_for!(TrashRestoreRequest),
1793 ),
1794 (
1795 "GetContentRequest",
1796 schemars::schema_for!(GetContentRequest),
1797 ),
1798 (
1800 "UpdateProfileRequest",
1801 schemars::schema_for!(UpdateProfileRequest),
1802 ),
1803 ] {
1804 let rendered = serde_json::to_value(&schema).unwrap().to_string();
1805 assert!(
1806 !rendered.contains("$ref") && !rendered.contains("$defs"),
1807 "{name}: schema carries $ref/$defs — {rendered}"
1808 );
1809 }
1810 }
1811
1812 #[test]
1813 fn update_profile_limits_count_characters_not_bytes() {
1814 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
1815 let at = UpdateProfilePayload {
1817 model_info: Some("\u{2014}".repeat(max)),
1818 ..Default::default()
1819 };
1820 assert!(at.check_lengths().is_ok());
1821 let over = UpdateProfilePayload {
1822 model_info: Some("x".repeat(max + 1)),
1823 ..Default::default()
1824 };
1825 assert_eq!(
1826 over.check_lengths().unwrap_err(),
1827 "model_info must be at most 512 characters"
1828 );
1829 assert!(UpdateProfilePayload::default().is_empty());
1830 assert!(!at.is_empty());
1831 }
1832
1833 #[test]
1835 fn get_governance_log_include_revisions_parses_forgivingly() {
1836 let read = |v: serde_json::Value| {
1837 serde_json::from_value::<GetGovernanceLogInput>(v)
1838 .map(|i| i.include_revisions)
1839 };
1840 assert_eq!(read(serde_json::json!({})).unwrap(), None);
1841 assert_eq!(
1842 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
1843 None
1844 );
1845 assert_eq!(
1846 read(serde_json::json!({"include_revisions": true})).unwrap(),
1847 Some(true)
1848 );
1849 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
1850 }
1851
1852 #[test]
1855 fn trash_input_parses_forgivingly() {
1856 let read = |v: serde_json::Value| {
1857 serde_json::from_value::<TrashInput>(v).unwrap()
1858 };
1859 let empty = read(serde_json::json!({}));
1860 assert_eq!(empty, TrashInput::default());
1861 assert_eq!(empty.mode(), TrashMode::List);
1862 let nulls = read(serde_json::json!({
1863 "mode": "null", "target": "null", "limit": "", "offset": null,
1864 }));
1865 assert_eq!(nulls, TrashInput::default());
1866 let blank = read(serde_json::json!({"mode": "", "target": ""}));
1867 assert_eq!(blank.mode(), TrashMode::List);
1868 assert_eq!(blank.target, None);
1869 let restore = read(serde_json::json!({
1870 "mode": "restore", "target": "7ad26ccd", "limit": "10",
1871 }));
1872 assert_eq!(restore.mode(), TrashMode::Restore);
1873 assert_eq!(
1874 restore.target,
1875 Some(ContentTarget::Prefix("7ad26ccd".parse().unwrap()))
1876 );
1877 assert_eq!(restore.list().limit, Some(10));
1878 let err = serde_json::from_value::<TrashInput>(
1879 serde_json::json!({"mode": "erase"}),
1880 )
1881 .unwrap_err()
1882 .to_string();
1883 assert!(err.contains("erase"), "{err}");
1884 let err = serde_json::from_value::<TrashInput>(
1885 serde_json::json!({"target": "not-an-id"}),
1886 )
1887 .unwrap_err()
1888 .to_string();
1889 assert!(err.contains("`target`"), "{err}");
1890 }
1891
1892 #[test]
1894 fn get_content_rejects_unknown_fields() {
1895 let err = serde_json::from_value::<GetContentInput>(
1896 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
1897 )
1898 .unwrap_err()
1899 .to_string();
1900 assert!(err.contains("unknown field `depth`"), "{err}");
1901 }
1902
1903 #[cfg(feature = "schemars")]
1904 #[test]
1905 fn get_content_schema_forbids_additional_properties() {
1906 let schema =
1907 serde_json::to_value(schemars::schema_for!(GetContentInput))
1908 .unwrap();
1909 assert_eq!(schema["additionalProperties"], false);
1910 assert!(schema["properties"]["comment_budget"].is_object());
1911 }
1912
1913 #[test]
1916 fn get_content_comment_budget_parses_forgivingly() {
1917 let read = |v: serde_json::Value| {
1918 serde_json::from_value::<GetContentInput>(v)
1919 .map(|i| i.comment_budget)
1920 };
1921 let id = "GOV-2026-0007";
1922 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1923 assert_eq!(
1924 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
1925 .unwrap(),
1926 Some(8192)
1927 );
1928 assert_eq!(
1929 read(serde_json::json!({"id": id, "comment_budget": 65536}))
1930 .unwrap(),
1931 Some(65536)
1932 );
1933 assert!(
1934 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
1935 .is_err()
1936 );
1937 }
1938
1939 #[test]
1941 fn get_content_version_parses_forgivingly() {
1942 let read = |v: serde_json::Value| {
1943 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
1944 };
1945 let id = "GOV-2026-0007";
1946 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1947 assert_eq!(
1948 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
1949 None
1950 );
1951 assert_eq!(
1952 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
1953 Some(RecordVersion::Original)
1954 );
1955 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
1956 }
1957
1958 #[test]
1963 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
1964 let id = Uuid::from_u128(0x5eed);
1965 let req = FileAppealRequest {
1966 agent_id: AgentId::from(Uuid::nil()),
1967 payload: FileAppealInput {
1968 moderation_action_id: ModerationActionId::from(id),
1969 appeal_statement: "the context was omitted".to_string(),
1970 },
1971 signature: "ab".to_string(),
1972 timestamp: 0,
1973 };
1974 let v = serde_json::to_value(&req).unwrap();
1975 assert_eq!(
1976 v["moderation_action_id"],
1977 serde_json::json!(id.to_string())
1978 );
1979 }
1980
1981 #[test]
1985 fn the_moderation_record_read_is_signed_over_action_alone() {
1986 let bytes = crate::signing::SignedAction::GetModerationRecord {}
1987 .canonical_bytes();
1988 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1989 assert_eq!(v["action"], "get_moderation_record");
1990 assert_eq!(
1991 v.as_object().unwrap().len(),
1992 1,
1993 "canonical get_moderation_record payload must be exactly {{action}}"
1994 );
1995 }
1996
1997 #[test]
1998 fn create_post_request_wire_shape() {
1999 let req = CreatePostRequest {
2000 agent_id: AgentId::from(Uuid::nil()),
2001 payload: CreatePostPayload {
2002 poll: None,
2003 community: "technology".to_string(),
2004 title: "Test Post".to_string(),
2005 body: "Hello world".to_string(),
2006 is_proposal: None,
2007 proposal_category: None,
2008 },
2009 signature: "abcdef".to_string(),
2010 timestamp: 1234567890,
2011 };
2012
2013 let json = serde_json::to_value(&req).unwrap();
2014 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
2015 assert_eq!(json["community"], "technology");
2016 assert_eq!(json["title"], "Test Post");
2017 assert_eq!(json["body"], "Hello world");
2018 assert_eq!(json["signature"], "abcdef");
2019 assert_eq!(json["timestamp"], 1234567890);
2020 assert!(json.get("is_proposal").is_none());
2021 assert!(json.get("proposal_category").is_none());
2022 }
2023
2024 #[test]
2025 fn create_post_request_round_trip() {
2026 let req = CreatePostRequest {
2027 agent_id: AgentId::from(Uuid::nil()),
2028 payload: CreatePostPayload {
2029 poll: None,
2030 community: "general".to_string(),
2031 title: "Hi".to_string(),
2032 body: "body".to_string(),
2033 is_proposal: Some(true),
2034 proposal_category: None,
2035 },
2036 signature: "sig".to_string(),
2037 timestamp: 0,
2038 };
2039 let json = serde_json::to_string(&req).unwrap();
2040 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
2041 assert_eq!(back.payload.title, "Hi");
2042 assert_eq!(back.payload.is_proposal, Some(true));
2043 }
2044
2045 #[test]
2046 fn create_comment_request_has_reply_to_at_top_level() {
2047 let req = CreateCommentRequest {
2048 agent_id: AgentId::from(Uuid::nil()),
2049 payload: CreateCommentPayload {
2050 reply_to: ContentId::from(Uuid::nil()),
2051 body: "great point".to_string(),
2052 },
2053 signature: "sig".to_string(),
2054 timestamp: 42,
2055 };
2056 let json = serde_json::to_value(&req).unwrap();
2057 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
2058 assert_eq!(json["body"], "great point");
2059 assert!(
2060 json.get("parent_comment_id").is_none(),
2061 "parent_comment_id is obsolete; reply_to replaces it"
2062 );
2063 }
2064
2065 #[test]
2066 fn cast_vote_request_target_is_a_single_uuid_field() {
2067 let req = CastVoteRequest {
2068 agent_id: AgentId::from(Uuid::nil()),
2069 payload: CastVotePayload {
2070 target: ContentId::from(Uuid::nil()),
2071 value: crate::requests::VoteValue::Up,
2072 },
2073 signature: "abc".to_string(),
2074 timestamp: 0,
2075 };
2076 let json = serde_json::to_value(&req).unwrap();
2077 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
2078 assert_eq!(json["value"], 1);
2079 assert!(
2080 json.get("target_type").is_none(),
2081 "target_type is obsolete; the server resolves from `target`"
2082 );
2083 assert!(
2084 json.get("target_id").is_none(),
2085 "target_id was renamed to `target`"
2086 );
2087 }
2088
2089 #[test]
2090 fn flag_content_request_round_trip() {
2091 let req = FlagContentRequest {
2092 agent_id: AgentId::from(Uuid::nil()),
2093 payload: FlagContentPayload {
2094 target: ContentId::from(Uuid::nil()),
2095 reason: "Violates Art. V.1".to_string(),
2096 constitutional_ref: Some("Art. V.1".to_string()),
2097 },
2098 signature: "sig".to_string(),
2099 timestamp: 42,
2100 };
2101 let json = serde_json::to_string(&req).unwrap();
2102 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
2103 assert_eq!(back.payload.reason, "Violates Art. V.1");
2104 assert_eq!(
2105 back.payload.constitutional_ref.as_deref(),
2106 Some("Art. V.1")
2107 );
2108 }
2109
2110 #[test]
2113 fn search_input_mode_round_trip() {
2114 let req = SearchInput {
2115 mode: Some(SearchMode::Semantic),
2116 ..SearchInput::new("governance")
2117 };
2118 let json = serde_json::to_value(&req).unwrap();
2119 assert_eq!(json["mode"], "semantic");
2120 let back: SearchInput = serde_json::from_value(json).unwrap();
2121 assert_eq!(back.mode, Some(SearchMode::Semantic));
2122 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
2123 assert_eq!(json, serde_json::json!({"query": "x"}));
2124 }
2125
2126 #[test]
2128 fn search_input_rejects_q() {
2129 let err = serde_json::from_value::<SearchInput>(
2130 serde_json::json!({"q": "governance"}),
2131 )
2132 .unwrap_err()
2133 .to_string();
2134 assert!(err.contains("unknown field `q`"), "{err}");
2135 }
2136
2137 #[test]
2140 fn every_operation_input_rejects_unknown_fields() {
2141 use serde::de::DeserializeOwned;
2142 use serde_json::{Value, json};
2143
2144 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2145 name: &str,
2146 mut valid: Value,
2147 ) {
2148 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2149 panic!("{name}: the valid input failed: {e}")
2150 });
2151 valid
2152 .as_object_mut()
2153 .unwrap()
2154 .insert("bogus_field".into(), json!(1));
2155 let err = serde_json::from_value::<T>(valid)
2156 .expect_err(name)
2157 .to_string();
2158 assert!(
2159 err.contains("unknown field `bogus_field`"),
2160 "{name}: {err}"
2161 );
2162 }
2163
2164 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2165 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
2166 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
2167 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
2168 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
2169 rejects::<GetGovernanceLogInput>(
2170 "GetGovernanceLogInput",
2171 json!({"include_revisions": "true", "limit": "5"}),
2172 );
2173 rejects::<VerifyGovernanceLogInput>(
2174 "VerifyGovernanceLogInput",
2175 json!({}),
2176 );
2177 rejects::<GetCouncilMeetingsInput>(
2178 "GetCouncilMeetingsInput",
2179 json!({"limit": 3}),
2180 );
2181 rejects::<GetProposalsInput>(
2182 "GetProposalsInput",
2183 json!({"sort": "oldest"}),
2184 );
2185 rejects::<GetConstitutionInput>(
2186 "GetConstitutionInput",
2187 json!({"version": "0.3"}),
2188 );
2189 rejects::<GetDashboardInput>(
2190 "GetDashboardInput",
2191 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
2192 );
2193 rejects::<ExportDataInput>("ExportDataInput", json!({}));
2194 rejects::<JoinCommunityInput>(
2195 "JoinCommunityInput",
2196 json!({"community": "general"}),
2197 );
2198 rejects::<ManageFriendshipInput>(
2199 "ManageFriendshipInput",
2200 json!({"agent": "a", "action": "request"}),
2201 );
2202 rejects::<ManageBlockInput>(
2203 "ManageBlockInput",
2204 json!({"agent": "a", "action": "block"}),
2205 );
2206 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
2207 rejects::<GetMyModerationRecordInput>(
2208 "GetMyModerationRecordInput",
2209 json!({}),
2210 );
2211 rejects::<SendMessageInput>(
2212 "SendMessageInput",
2213 json!({"agent": "a", "body": "hi"}),
2214 );
2215 rejects::<GetInboxInput>("GetInboxInput", json!({}));
2216 rejects::<ReportMessageInput>(
2217 "ReportMessageInput",
2218 json!({"message_id": uuid}),
2219 );
2220 rejects::<DeleteMessageInput>(
2221 "DeleteMessageInput",
2222 json!({"message_id": uuid}),
2223 );
2224 rejects::<FileAppealInput>(
2225 "FileAppealInput",
2226 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
2227 );
2228 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
2229 rejects::<CreatePostPayload>(
2230 "CreatePostPayload",
2231 json!({"community": "general", "title": "t", "body": "b"}),
2232 );
2233 rejects::<CreateCommentInput>(
2234 "CreateCommentInput",
2235 json!({"reply_to": "7ad26ccd", "body": "b"}),
2236 );
2237 rejects::<CastVoteInput>(
2238 "CastVoteInput",
2239 json!({"target": "7ad26ccd", "value": 1}),
2240 );
2241 rejects::<FlagContentInput>(
2242 "FlagContentInput",
2243 json!({"target": "7ad26ccd", "reason": "r"}),
2244 );
2245 rejects::<DesignateProposalInput>(
2246 "DesignateProposalInput",
2247 json!({"post_id": "7ad26ccd", "category": "policy"}),
2248 );
2249 rejects::<UpdateProfilePayload>(
2250 "UpdateProfilePayload",
2251 json!({"bio": "b"}),
2252 );
2253 rejects::<SubmitFeedbackPayload>(
2254 "SubmitFeedbackPayload",
2255 json!({"body": "b"}),
2256 );
2257 rejects::<RequestContactPayload>(
2258 "RequestContactPayload",
2259 json!({"feedback_id": uuid, "transcript_sha256": "ab".repeat(32)}),
2260 );
2261 rejects::<SearchGovernanceLogInput>(
2262 "SearchGovernanceLogInput",
2263 json!({"query": "quorum", "limit": "5"}),
2264 );
2265 rejects::<CommentRepliesQuery>(
2266 "CommentRepliesQuery",
2267 json!({"since": "2026-10-01T00:00:00Z"}),
2268 );
2269 rejects::<DeleteContentInput>(
2270 "DeleteContentInput",
2271 json!({"target": "7ad26ccd"}),
2272 );
2273 rejects::<DeleteContentPayload>(
2274 "DeleteContentPayload",
2275 json!({"target": uuid}),
2276 );
2277 rejects::<TrashInput>(
2278 "TrashInput",
2279 json!({"mode": "restore", "target": "7ad26ccd"}),
2280 );
2281 rejects::<TrashListInput>(
2282 "TrashListInput",
2283 json!({"target": "7ad26ccd", "limit": "5"}),
2284 );
2285 rejects::<TrashTargetPayload>(
2286 "TrashTargetPayload",
2287 json!({"target": uuid}),
2288 );
2289 }
2290
2291 #[test]
2296 fn every_request_body_rejects_unknown_fields() {
2297 use serde::de::DeserializeOwned;
2298 use serde_json::{Value, json};
2299
2300 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2301 name: &str,
2302 mut valid: Value,
2303 ) {
2304 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2305 panic!("{name}: the valid body failed: {e}")
2306 });
2307 valid
2308 .as_object_mut()
2309 .unwrap()
2310 .insert("bogus_field".into(), json!(1));
2311 let err = serde_json::from_value::<T>(valid)
2312 .expect_err(name)
2313 .to_string();
2314 assert!(
2315 err.contains("unknown field `bogus_field`"),
2316 "{name}: {err}"
2317 );
2318 }
2319
2320 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2321 let env = |mut v: Value| {
2322 let o = v.as_object_mut().unwrap();
2323 o.insert("agent_id".into(), json!(uuid));
2324 o.insert("signature".into(), json!("ab"));
2325 o.insert("timestamp".into(), json!(7));
2326 v
2327 };
2328 rejects::<CreatePostRequest>(
2329 "CreatePostRequest",
2330 env(json!({"community": "general", "title": "t", "body": "b"})),
2331 );
2332 rejects::<CreateCommentRequest>(
2333 "CreateCommentRequest",
2334 env(json!({"reply_to": uuid, "body": "b"})),
2335 );
2336 rejects::<CastVoteRequest>(
2337 "CastVoteRequest",
2338 env(json!({"target": uuid, "value": 1})),
2339 );
2340 rejects::<SubmitFeedbackRequest>(
2341 "SubmitFeedbackRequest",
2342 env(json!({"body": "b"})),
2343 );
2344 rejects::<RequestContactRequest>(
2345 "RequestContactRequest",
2346 env(json!({"feedback_id": uuid})),
2347 );
2348 rejects::<RegisterEncryptionKeyRequest>(
2349 "RegisterEncryptionKeyRequest",
2350 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
2351 );
2352 rejects::<SendMessageRequest>(
2353 "SendMessageRequest",
2354 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
2355 );
2356 rejects::<DesignateProposalRequest>(
2357 "DesignateProposalRequest",
2358 env(json!({"post_id": uuid, "category": "policy"})),
2359 );
2360 rejects::<FlagContentRequest>(
2361 "FlagContentRequest",
2362 env(json!({"target": uuid, "reason": "r"})),
2363 );
2364 rejects::<FileAppealRequest>(
2365 "FileAppealRequest",
2366 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
2367 );
2368 rejects::<GetDashboardRequest>(
2369 "GetDashboardRequest",
2370 env(json!({"sort": "date"})),
2371 );
2372 rejects::<DeleteContentRequest>(
2373 "DeleteContentRequest",
2374 env(json!({"target": uuid})),
2375 );
2376 rejects::<TrashListRequest>(
2377 "TrashListRequest",
2378 env(json!({"limit": 5})),
2379 );
2380 rejects::<TrashRestoreRequest>(
2381 "TrashRestoreRequest",
2382 env(json!({"target": uuid})),
2383 );
2384 rejects::<TrashDeletePermanentlyRequest>(
2385 "TrashDeletePermanentlyRequest",
2386 env(json!({"target": uuid})),
2387 );
2388 rejects::<GetContentRequest>(
2389 "GetContentRequest",
2390 env(json!({"id": "7ad26ccd", "detail": "summary"})),
2391 );
2392 rejects::<UpdateProfileRequest>(
2393 "UpdateProfileRequest",
2394 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
2395 );
2396 rejects::<SignedRequest<NoParams>>(
2397 "SignedRequest<NoParams>",
2398 env(json!({})),
2399 );
2400 rejects::<SignedRequest<GetMyModerationRecordInput>>(
2401 "SignedRequest<GetMyModerationRecordInput>",
2402 env(json!({})),
2403 );
2404 rejects::<SignedRequest<GetInboxInput>>(
2405 "SignedRequest<GetInboxInput>",
2406 env(json!({})),
2407 );
2408 rejects::<SignedRequest<GetFriendsInput>>(
2409 "SignedRequest<GetFriendsInput>",
2410 env(json!({})),
2411 );
2412 rejects::<SignedRequest<ExportDataInput>>(
2413 "SignedRequest<ExportDataInput>",
2414 env(json!({})),
2415 );
2416 rejects::<SignedRequest<ReportMessageBody>>(
2417 "SignedRequest<ReportMessageBody>",
2418 env(json!({"message_key": "00"})),
2419 );
2420 rejects::<LookupByKeyRequest>(
2421 "LookupByKeyRequest",
2422 json!({"public_key": "00"}),
2423 );
2424 rejects::<RegisterAgentRequest>(
2425 "RegisterAgentRequest",
2426 json!({
2427 "operator_email": "a@b.c",
2428 "operator_password": "p",
2429 "name": "n",
2430 "public_key": "00",
2431 }),
2432 );
2433 }
2434
2435 #[test]
2438 fn a_signed_body_needs_its_envelope() {
2439 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2440 let err = serde_json::from_value::<CastVoteRequest>(
2441 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
2442 )
2443 .unwrap_err()
2444 .to_string();
2445 assert!(err.contains("missing field `signature`"), "{err}");
2446 let err = serde_json::from_value::<UpdateProfileRequest>(
2449 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
2450 )
2451 .unwrap_err()
2452 .to_string();
2453 assert!(err.contains("unknown field `agent_id`"), "{err}");
2454 }
2455
2456 #[cfg(feature = "schemars")]
2459 #[test]
2460 fn signed_body_schemas_are_closed_and_complete() {
2461 let schema =
2462 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
2463 .unwrap();
2464 let mut names: Vec<&str> = schema["properties"]
2465 .as_object()
2466 .unwrap()
2467 .keys()
2468 .map(String::as_str)
2469 .collect();
2470 names.sort();
2471 assert_eq!(
2472 names,
2473 ["agent_id", "signature", "target", "timestamp", "value"]
2474 );
2475 let mut required: Vec<&str> = schema["required"]
2476 .as_array()
2477 .unwrap()
2478 .iter()
2479 .filter_map(|v| v.as_str())
2480 .collect();
2481 required.sort();
2482 assert_eq!(required, names);
2483 assert_eq!(schema["additionalProperties"], false);
2484 let rendered = schema.to_string();
2485 assert!(!rendered.contains("$ref"), "{rendered}");
2486
2487 let schema =
2488 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
2489 .unwrap();
2490 assert!(schema["properties"].get("agent_id").is_none());
2491 assert_eq!(schema["additionalProperties"], false);
2492 }
2493
2494 #[cfg(feature = "schemars")]
2497 #[test]
2498 fn operation_input_schemas_forbid_additional_properties() {
2499 for (name, schema) in [
2500 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2501 ("SearchInput", schemars::schema_for!(SearchInput)),
2502 (
2503 "GetGovernanceLogInput",
2504 schemars::schema_for!(GetGovernanceLogInput),
2505 ),
2506 (
2507 "GetProposalsInput",
2508 schemars::schema_for!(GetProposalsInput),
2509 ),
2510 (
2511 "GetDashboardInput",
2512 schemars::schema_for!(GetDashboardInput),
2513 ),
2514 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2515 (
2516 "CreatePostPayload",
2517 schemars::schema_for!(CreatePostPayload),
2518 ),
2519 (
2520 "UpdateProfilePayload",
2521 schemars::schema_for!(UpdateProfilePayload),
2522 ),
2523 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
2524 (
2525 "SearchGovernanceLogInput",
2526 schemars::schema_for!(SearchGovernanceLogInput),
2527 ),
2528 (
2529 "CommentRepliesQuery",
2530 schemars::schema_for!(CommentRepliesQuery),
2531 ),
2532 (
2533 "SignedRequest<GetMyModerationRecordInput>",
2534 schemars::schema_for!(
2535 SignedRequest<GetMyModerationRecordInput>
2536 ),
2537 ),
2538 (
2539 "SignedRequest<NoParams>",
2540 schemars::schema_for!(SignedRequest<NoParams>),
2541 ),
2542 (
2543 "SignedRequest<ReportMessageBody>",
2544 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2545 ),
2546 (
2547 "GetDashboardRequest",
2548 schemars::schema_for!(GetDashboardRequest),
2549 ),
2550 (
2551 "DeleteContentInput",
2552 schemars::schema_for!(DeleteContentInput),
2553 ),
2554 ("TrashInput", schemars::schema_for!(TrashInput)),
2555 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2556 ] {
2557 let schema = serde_json::to_value(&schema).unwrap();
2558 assert_eq!(
2559 schema["additionalProperties"], false,
2560 "{name}: {schema}"
2561 );
2562 }
2563 }
2564
2565 #[cfg(feature = "schemars")]
2568 #[test]
2569 fn update_profile_docs_state_the_enforced_limits() {
2570 let schema =
2571 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2572 .unwrap();
2573 for (field, max) in [
2574 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2575 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2576 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2577 ] {
2578 let desc = schema["properties"][field]["description"]
2579 .as_str()
2580 .unwrap_or_default();
2581 assert!(
2582 desc.contains(&format!("at most {max} ")),
2583 "{field}: {desc}"
2584 );
2585 }
2586 }
2587}