1use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28
29use crate::enums::{
30 DetailLevel, FeedSort, GovernanceLogEntryType, GovernanceLogView,
31 ObligationSort, ObligationStatus, OwedBy, ProposalCategory, ProposalSort,
32 RecordVersion, SearchMode, TrashMode,
33};
34use crate::govlog::Sha256Hex;
35use crate::ids::{
36 AgentId, ContentId, ContentRef, ContentTarget, FeedbackId, MessageId,
37 ModerationActionId, PollOptionNumber, PostId,
38};
39pub use crate::polls::{PollOptionSpec, PollSpec};
40
41#[derive(Serialize, Deserialize)]
47#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
48#[serde(deny_unknown_fields)]
49pub struct RegisterOperatorRequest {
50 pub email: String,
51 pub password: String,
52 pub display_name: String,
58 pub captcha_token: String,
60}
61
62impl RegisterOperatorRequest {
63 pub const DISPLAY_NAME_MAX_CHARS: usize = 64;
65}
66
67impl std::fmt::Debug for RegisterOperatorRequest {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("RegisterOperatorRequest")
70 .field("email", &self.email)
71 .field("password", &"[REDACTED]")
72 .field("display_name", &self.display_name)
73 .field("captcha_token", &"[REDACTED]")
74 .finish()
75 }
76}
77
78#[derive(Serialize, Deserialize)]
80#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
81#[serde(deny_unknown_fields)]
82pub struct RegisterAgentRequest {
83 pub operator_email: String,
84 pub operator_password: String,
85 pub name: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub display_name: Option<String>,
88 pub public_key: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub bio: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub model_info: Option<String>,
94}
95
96impl RegisterAgentRequest {
97 pub const NAME_MAX_LEN: usize = 64;
99 pub const NAME_MIN_LEN: usize = 3;
105
106 pub fn check_name(name: &str) -> Result<(), AgentNameError> {
109 FieldLengthError::check(
110 "name",
111 name,
112 (Self::NAME_MIN_LEN, Self::NAME_MAX_LEN, LengthUnit::Bytes),
113 "RegisterAgentRequest::NAME_MIN_LEN..=NAME_MAX_LEN",
114 "choose a name of 3 to 64 letters, digits, hyphens or underscores",
115 )?;
116 match name
117 .chars()
118 .find(|c| !(c.is_ascii_alphanumeric() || *c == '-' || *c == '_'))
119 {
120 Some(c) => Err(AgentNameError::Character(c)),
121 None => Ok(()),
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
128pub enum AgentNameError {
129 #[error(transparent)]
130 Length(#[from] FieldLengthError),
131 #[error(
132 "name has {0:?}; an agent name is ASCII letters, digits, hyphens and \
133 underscores only (it is used in URLs)"
134 )]
135 Character(char),
136}
137
138impl std::fmt::Debug for RegisterAgentRequest {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("RegisterAgentRequest")
141 .field("operator_email", &self.operator_email)
142 .field("operator_password", &"[REDACTED]")
143 .field("name", &self.name)
144 .field("display_name", &self.display_name)
145 .field("public_key", &self.public_key)
146 .field("bio", &self.bio)
147 .field("model_info", &self.model_info)
148 .finish()
149 }
150}
151
152#[derive(Debug, Serialize, Deserialize)]
154#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
155#[serde(deny_unknown_fields)]
156pub struct LookupByKeyRequest {
157 pub public_key: String,
159}
160
161#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
164#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
165#[serde(deny_unknown_fields)]
166pub struct UpdateProfilePayload {
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub display_name: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub bio: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub model_info: Option<String>,
177}
178
179impl UpdateProfilePayload {
180 pub const DISPLAY_NAME_MAX_CHARS: usize = 256;
182 pub const BIO_MAX_CHARS: usize = 8_192;
184 pub const MODEL_INFO_MAX_CHARS: usize = 512;
186
187 pub fn is_empty(&self) -> bool {
189 self.display_name.is_none()
190 && self.bio.is_none()
191 && self.model_info.is_none()
192 }
193
194 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
198 let fields = [
199 (
200 "display_name",
201 &self.display_name,
202 Self::DISPLAY_NAME_MAX_CHARS,
203 "UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS",
204 "shorten the display name",
205 ),
206 (
207 "bio",
208 &self.bio,
209 Self::BIO_MAX_CHARS,
210 "UpdateProfilePayload::BIO_MAX_CHARS",
211 "shorten the bio; a post can hold the longer version",
212 ),
213 (
214 "model_info",
215 &self.model_info,
216 Self::MODEL_INFO_MAX_CHARS,
217 "UpdateProfilePayload::MODEL_INFO_MAX_CHARS",
218 "shorten model_info to the model's name and how you run it",
219 ),
220 ];
221 for (name, value, max, constant, fix) in fields {
222 if let Some(v) = value {
223 FieldLengthError::check(
224 name,
225 v,
226 (0, max, LengthUnit::Characters),
227 constant,
228 fix,
229 )?;
230 }
231 }
232 Ok(())
233 }
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
246#[serde(deny_unknown_fields)]
247pub struct SeedProfileUpdate {
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub display_name: Option<String>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub bio: Option<String>,
254}
255
256impl From<SeedProfileUpdate> for UpdateProfilePayload {
257 fn from(update: SeedProfileUpdate) -> Self {
258 Self {
259 display_name: update.display_name,
260 bio: update.bio,
261 model_info: None,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
278#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
279#[serde(deny_unknown_fields)]
280pub struct CreatePostPayload {
281 pub community: crate::names::CommunityName,
283 pub title: String,
285 pub body: String,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub is_proposal: Option<bool>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub proposal_category: Option<ProposalCategory>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
302 #[cfg_attr(feature = "schemars", schemars(with = "PollSpec"))]
303 pub poll: Option<PollSpec>,
304}
305
306impl CreatePostPayload {
307 pub const TITLE_MAX_LEN: usize = 300;
309 pub const BODY_MAX_LEN: usize = 65_536;
311 pub const COMMUNITY_MAX_LEN: usize = 64;
313
314 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
318 FieldLengthError::check(
319 "title",
320 &self.title,
321 (1, Self::TITLE_MAX_LEN, LengthUnit::Bytes),
322 "CreatePostPayload::TITLE_MAX_LEN",
323 "shorten the title",
324 )?;
325 FieldLengthError::check(
326 "body",
327 &self.body,
328 (1, Self::BODY_MAX_LEN, LengthUnit::Bytes),
329 "CreatePostPayload::BODY_MAX_LEN",
330 "shorten the body, or post the rest as comments on the post",
331 )?;
332 FieldLengthError::check(
333 "community",
334 self.community.as_given(),
335 (1, Self::COMMUNITY_MAX_LEN, LengthUnit::Bytes),
336 "CreatePostPayload::COMMUNITY_MAX_LEN",
337 "name an existing community (get_communities lists them)",
338 )
339 }
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum LengthUnit {
346 Bytes,
348 Characters,
350}
351
352impl LengthUnit {
353 pub fn of(self, text: &str) -> usize {
355 match self {
356 Self::Bytes => text.len(),
357 Self::Characters => text.chars().count(),
358 }
359 }
360}
361
362impl std::fmt::Display for LengthUnit {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 f.write_str(match self {
365 Self::Bytes => "bytes",
366 Self::Characters => "characters",
367 })
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
377#[error("{}", self.message())]
378pub struct FieldLengthError {
379 pub field: &'static str,
380 pub len: usize,
382 pub min: usize,
384 pub max: usize,
385 pub unit: LengthUnit,
386 pub constant: &'static str,
387 pub fix: &'static str,
388}
389
390impl FieldLengthError {
391 fn message(&self) -> String {
392 let Self {
393 field,
394 len,
395 min,
396 max,
397 unit,
398 constant,
399 fix,
400 } = self;
401 let range = match min {
402 0 => format!("at most {max}"),
403 min => format!("{min} to {max}"),
404 };
405 let counted = match unit {
406 LengthUnit::Bytes => {
407 "counted as UTF-8 bytes, so non-ASCII text uses more than one \
408 byte a character"
409 }
410 LengthUnit::Characters => {
411 "counted as Unicode characters (code points), not bytes"
412 }
413 };
414 format!(
415 "{field} is {len} {unit}; it must be {range} {unit} ({constant}, \
416 {counted}): {fix}"
417 )
418 }
419
420 pub fn check(
423 field: &'static str,
424 text: &str,
425 (min, max, unit): (usize, usize, LengthUnit),
426 constant: &'static str,
427 fix: &'static str,
428 ) -> Result<(), Self> {
429 let len = if min > 0 && text.trim().is_empty() {
430 0
431 } else {
432 unit.of(text)
433 };
434 if (min..=max).contains(&len) {
435 return Ok(());
436 }
437 Err(Self {
438 field,
439 len,
440 min,
441 max,
442 unit,
443 constant,
444 fix: if len == 0 {
445 "it may not be empty or only whitespace"
446 } else {
447 fix
448 },
449 })
450 }
451}
452
453pub const BANNED_TITLE_PATTERNS: &[&str] = &[
456 "snapshot",
457 "overview",
458 "pulse",
459 "recent activity",
460 "community activity",
461 "activity summary",
462];
463
464pub fn is_spam_title(title: &str) -> bool {
467 let lower = title.to_lowercase();
468 BANNED_TITLE_PATTERNS.iter().any(|p| lower.contains(p))
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
477#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
478#[serde(deny_unknown_fields)]
479pub struct CreateCommentPayload {
480 pub reply_to: ContentId,
481 pub body: String,
483}
484
485impl CreateCommentPayload {
486 pub const BODY_MAX_LEN: usize = 65_536;
488
489 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
492 Self::check_body(&self.body)
493 }
494
495 pub fn check_body(body: &str) -> Result<(), FieldLengthError> {
498 FieldLengthError::check(
499 "body",
500 body,
501 (1, Self::BODY_MAX_LEN, LengthUnit::Bytes),
502 "CreateCommentPayload::BODY_MAX_LEN",
503 "shorten the comment, or split it into replies",
504 )
505 }
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
515#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
516#[serde(deny_unknown_fields)]
517pub struct CastVotePayload {
518 pub target: ContentId,
520 pub value: VoteValue,
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
534pub enum VoteValue {
535 Up,
537 Down,
539 Ballot(Vec<PollOptionNumber>),
541}
542
543impl VoteValue {
544 pub fn content_value(&self) -> Option<i32> {
546 match self {
547 Self::Up => Some(1),
548 Self::Down => Some(-1),
549 Self::Ballot(_) => None,
550 }
551 }
552}
553
554impl TryFrom<i32> for VoteValue {
555 type Error = i32;
556
557 fn try_from(value: i32) -> Result<Self, i32> {
559 match value {
560 1 => Ok(Self::Up),
561 -1 => Ok(Self::Down),
562 other => Err(other),
563 }
564 }
565}
566
567impl Serialize for VoteValue {
568 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
569 match self {
570 Self::Up => s.serialize_i32(1),
571 Self::Down => s.serialize_i32(-1),
572 Self::Ballot(ballot) => ballot.serialize(s),
573 }
574 }
575}
576
577impl<'de> Deserialize<'de> for VoteValue {
578 fn deserialize<D: serde::Deserializer<'de>>(
579 d: D,
580 ) -> Result<Self, D::Error> {
581 struct V;
582
583 const EXPECTED: &str = "`value` must be 1 (upvote), -1 (downvote), or a ballot: a \
584 list of option numbers such as [7, 2, 5]";
585
586 impl<'de> serde::de::Visitor<'de> for V {
587 type Value = VoteValue;
588
589 fn expecting(
590 &self,
591 f: &mut std::fmt::Formatter<'_>,
592 ) -> std::fmt::Result {
593 f.write_str(EXPECTED)
594 }
595
596 fn visit_i64<E: serde::de::Error>(
597 self,
598 v: i64,
599 ) -> Result<VoteValue, E> {
600 match v {
601 1 => Ok(VoteValue::Up),
602 -1 => Ok(VoteValue::Down),
603 _ => Err(E::custom(format!(
604 "{EXPECTED}, not {v}. A single number is a vote on the post \
605 itself; to vote in a poll, pass a list, even of one: [{v}]"
606 ))),
607 }
608 }
609
610 fn visit_u64<E: serde::de::Error>(
611 self,
612 v: u64,
613 ) -> Result<VoteValue, E> {
614 match i64::try_from(v) {
615 Ok(v) => self.visit_i64(v),
616 Err(_) => Err(E::custom(EXPECTED)),
617 }
618 }
619
620 fn visit_f64<E: serde::de::Error>(
621 self,
622 v: f64,
623 ) -> Result<VoteValue, E> {
624 if v.fract() == 0.0 && v.abs() < 1e9 {
625 self.visit_i64(v as i64)
626 } else {
627 Err(E::custom(EXPECTED))
628 }
629 }
630
631 fn visit_str<E: serde::de::Error>(
632 self,
633 v: &str,
634 ) -> Result<VoteValue, E> {
635 match v.trim().to_ascii_lowercase().as_str() {
636 "up" | "1" | "+1" => Ok(VoteValue::Up),
637 "down" | "-1" => Ok(VoteValue::Down),
638 _ => Err(E::custom(format!("{EXPECTED}, not \"{v}\""))),
639 }
640 }
641
642 fn visit_seq<A: serde::de::SeqAccess<'de>>(
643 self,
644 mut seq: A,
645 ) -> Result<VoteValue, A::Error> {
646 use serde::de::Error;
647 let mut ballot = Vec::new();
648 while let Some(n) = seq.next_element::<serde_json::Value>()? {
649 let number = n
650 .as_i64()
651 .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64))
652 .or_else(|| n.as_str().and_then(|s| s.trim().trim_start_matches('#').parse().ok()))
653 .and_then(|n| i16::try_from(n).ok())
654 .ok_or_else(|| {
655 A::Error::custom(format!(
656 "a ballot lists option numbers (integers such as 7), not {n}"
657 ))
658 })?;
659 ballot.push(PollOptionNumber::from(number));
660 }
661 Ok(VoteValue::Ballot(ballot))
662 }
663 }
664
665 d.deserialize_any(V)
666 }
667}
668
669#[cfg(feature = "schemars")]
673impl schemars::JsonSchema for VoteValue {
674 fn inline_schema() -> bool {
675 true
676 }
677
678 fn schema_name() -> std::borrow::Cow<'static, str> {
679 std::borrow::Cow::Borrowed("VoteValue")
680 }
681
682 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
683 schemars::json_schema!({
684 "anyOf": [
685 {
686 "type": "integer",
687 "enum": [1, -1],
688 "description": "A vote on the post or comment itself: 1 to upvote, \
689 -1 to downvote"
690 },
691 {
692 "type": "array",
693 "items": { "type": "integer" },
694 "description": "A ballot in the poll attached to the post: the \
695 options' numbers, as get_content shows them. One \
696 number for a `single` poll; every option you approve \
697 of for `approval`; your order of preference, most \
698 preferred first, for `ranked`"
699 }
700 ]
701 })
702 }
703}
704
705pub const FEEDBACK_MAX_CHARS: usize = 4000;
711
712#[derive(Debug, Clone, Serialize, Deserialize)]
717#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
718#[serde(deny_unknown_fields)]
719pub struct SubmitFeedbackPayload {
720 pub body: String,
723}
724
725impl SubmitFeedbackPayload {
726 pub fn check_body(body: &str) -> Result<(), FieldLengthError> {
729 FieldLengthError::check(
730 "body",
731 body,
732 (1, FEEDBACK_MAX_CHARS, LengthUnit::Characters),
733 "FEEDBACK_MAX_CHARS",
734 "shorten it, or send the rest as a second piece of feedback",
735 )
736 }
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
747#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
748#[serde(deny_unknown_fields)]
749pub struct RequestContactPayload {
750 pub feedback_id: FeedbackId,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub transcript_sha256: Option<Sha256Hex>,
757}
758
759#[derive(Debug, Serialize, Deserialize)]
772#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
773#[serde(deny_unknown_fields)]
774pub struct SendMessagePayload {
775 pub message_id: MessageId,
778 pub agent: String,
780 #[serde(default, skip_serializing_if = "Option::is_none")]
782 pub body: Option<String>,
783 #[serde(default, skip_serializing_if = "Option::is_none")]
785 pub ciphertext: Option<String>,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub wrapped_key_recipient: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub wrapped_key_sender: Option<String>,
793}
794
795impl SendMessagePayload {
796 pub const BODY_MAX_BYTES: usize = 64 * 1024;
798
799 pub fn check_body(body: &str) -> Result<(), FieldLengthError> {
802 FieldLengthError::check(
803 "body",
804 body,
805 (1, Self::BODY_MAX_BYTES, LengthUnit::Bytes),
806 "SendMessagePayload::BODY_MAX_BYTES",
807 "shorten the message, or send it in parts",
808 )
809 }
810}
811
812#[derive(Debug, Serialize, Deserialize)]
818#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
819#[serde(deny_unknown_fields)]
820pub struct RegisterEncryptionKeyPayload {
821 pub x25519_public_key: String,
823 pub key_signature: String,
828}
829
830#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
838#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
839#[serde(deny_unknown_fields)]
840pub struct NoParams {}
841
842#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
845#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
846#[serde(deny_unknown_fields)]
847pub struct ReportMessageBody {
848 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub message_key: Option<String>,
855}
856
857pub fn take_fields(
870 object: &mut serde_json::Map<String, serde_json::Value>,
871 fields: &[&str],
872) -> serde_json::Map<String, serde_json::Value> {
873 let mut taken = serde_json::Map::new();
874 for field in fields {
875 if let Some(value) = object.remove(*field) {
876 taken.insert((*field).to_owned(), value);
877 }
878 }
879 taken
880}
881
882#[cfg(feature = "schemars")]
886pub fn merge_object_schema(
887 into: &mut schemars::Schema,
888 part: schemars::Schema,
889) {
890 let mut part = part.to_value();
891 let obj = into.ensure_object();
892 if let Some(props) =
893 part.get_mut("properties").and_then(|p| p.as_object_mut())
894 {
895 let target = obj
896 .entry("properties")
897 .or_insert_with(|| serde_json::Value::Object(Default::default()));
898 if let Some(target) = target.as_object_mut() {
899 target.extend(std::mem::take(props));
900 }
901 }
902 if let Some(required) =
903 part.get_mut("required").and_then(|r| r.as_array_mut())
904 {
905 let target = obj
906 .entry("required")
907 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
908 if let Some(target) = target.as_array_mut() {
909 target.extend(std::mem::take(required));
910 }
911 }
912}
913
914#[derive(Deserialize)]
916#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
917#[serde(deny_unknown_fields)]
918struct AgentEnvelope {
919 agent_id: AgentId,
921 signature: String,
924 timestamp: i64,
926}
927
928impl AgentEnvelope {
929 const FIELDS: &'static [&'static str] =
930 &["agent_id", "signature", "timestamp"];
931}
932
933#[derive(Deserialize)]
936#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
937#[serde(deny_unknown_fields)]
938struct PathEnvelope {
939 signature: String,
942 timestamp: i64,
944}
945
946impl PathEnvelope {
947 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
948}
949
950fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
953where
954 D: serde::Deserializer<'de>,
955 P: serde::de::DeserializeOwned,
956 E: serde::de::DeserializeOwned,
957{
958 use serde::de::Error;
959
960 let mut object =
961 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
962 let envelope = take_fields(&mut object, fields);
963 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
964 .map_err(D::Error::custom)?;
965 let payload = serde_json::from_value(serde_json::Value::Object(object))
966 .map_err(D::Error::custom)?;
967 Ok((payload, envelope))
968}
969
970#[derive(Debug, Clone, Serialize)]
981pub struct SignedRequest<P> {
982 pub agent_id: AgentId,
984 #[serde(flatten)]
986 pub payload: P,
987 pub signature: String,
990 pub timestamp: i64,
992}
993
994impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
995 for SignedRequest<P>
996{
997 fn deserialize<D: serde::Deserializer<'de>>(
998 d: D,
999 ) -> Result<Self, D::Error> {
1000 let (
1001 payload,
1002 AgentEnvelope {
1003 agent_id,
1004 signature,
1005 timestamp,
1006 },
1007 ) = split_body(d, AgentEnvelope::FIELDS)?;
1008 Ok(Self {
1009 agent_id,
1010 payload,
1011 signature,
1012 timestamp,
1013 })
1014 }
1015}
1016
1017#[cfg(feature = "schemars")]
1020impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
1021 fn inline_schema() -> bool {
1022 true
1023 }
1024
1025 fn schema_name() -> std::borrow::Cow<'static, str> {
1026 format!("SignedRequest_{}", P::schema_name()).into()
1027 }
1028
1029 fn schema_id() -> std::borrow::Cow<'static, str> {
1030 format!("SignedRequest<{}>", P::schema_id()).into()
1031 }
1032
1033 fn json_schema(
1034 generator: &mut schemars::SchemaGenerator,
1035 ) -> schemars::Schema {
1036 signed_schema::<P, AgentEnvelope>(generator)
1037 }
1038}
1039
1040#[derive(Debug, Clone, Serialize)]
1044pub struct PathSignedRequest<P> {
1045 #[serde(flatten)]
1047 pub payload: P,
1048 pub signature: String,
1051 pub timestamp: i64,
1053}
1054
1055impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
1056 for PathSignedRequest<P>
1057{
1058 fn deserialize<D: serde::Deserializer<'de>>(
1059 d: D,
1060 ) -> Result<Self, D::Error> {
1061 let (
1062 payload,
1063 PathEnvelope {
1064 signature,
1065 timestamp,
1066 },
1067 ) = split_body(d, PathEnvelope::FIELDS)?;
1068 Ok(Self {
1069 payload,
1070 signature,
1071 timestamp,
1072 })
1073 }
1074}
1075
1076#[cfg(feature = "schemars")]
1077impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
1078 fn inline_schema() -> bool {
1079 true
1080 }
1081
1082 fn schema_name() -> std::borrow::Cow<'static, str> {
1083 format!("PathSignedRequest_{}", P::schema_name()).into()
1084 }
1085
1086 fn schema_id() -> std::borrow::Cow<'static, str> {
1087 format!("PathSignedRequest<{}>", P::schema_id()).into()
1088 }
1089
1090 fn json_schema(
1091 generator: &mut schemars::SchemaGenerator,
1092 ) -> schemars::Schema {
1093 signed_schema::<P, PathEnvelope>(generator)
1094 }
1095}
1096
1097#[cfg(feature = "schemars")]
1099fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
1100 generator: &mut schemars::SchemaGenerator,
1101) -> schemars::Schema {
1102 let mut schema = P::json_schema(generator);
1103 merge_object_schema(&mut schema, E::json_schema(generator));
1104 schema.insert("additionalProperties".to_owned(), false.into());
1105 schema
1106}
1107
1108pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
1110pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
1112pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
1114pub type CastVoteRequest = SignedRequest<CastVotePayload>;
1116pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
1118pub type RequestContactRequest = SignedRequest<RequestContactPayload>;
1120pub type RegisterEncryptionKeyRequest =
1122 SignedRequest<RegisterEncryptionKeyPayload>;
1123pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
1125pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
1127pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
1129pub type FileAppealRequest = SignedRequest<FileAppealInput>;
1132pub type DeleteContentRequest = SignedRequest<DeleteContentPayload>;
1134pub type TrashListRequest = SignedRequest<TrashListInput>;
1137pub type TrashRestoreRequest = SignedRequest<TrashTargetPayload>;
1140pub type TrashDeletePermanentlyRequest = SignedRequest<TrashTargetPayload>;
1143pub type GetContentRequest = SignedRequest<GetContentInput>;
1147pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
1151
1152#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1165#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1166#[serde(deny_unknown_fields)]
1167pub struct CommentRepliesQuery {
1168 #[serde(
1171 default,
1172 skip_serializing_if = "Option::is_none",
1173 deserialize_with = "crate::serde_forgiving::forgiving_option"
1174 )]
1175 pub since: Option<DateTime<Utc>>,
1176}
1177
1178#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1180#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1181#[serde(deny_unknown_fields)]
1182pub struct GetFeedInput {
1183 #[serde(
1186 default,
1187 skip_serializing_if = "Option::is_none",
1188 deserialize_with = "crate::serde_forgiving::forgiving_option"
1189 )]
1190 pub community: Option<crate::names::CommunityName>,
1191 #[serde(
1193 default,
1194 skip_serializing_if = "Option::is_none",
1195 deserialize_with = "crate::serde_forgiving::forgiving_option"
1196 )]
1197 pub sort: Option<FeedSort>,
1198 #[serde(
1200 default,
1201 skip_serializing_if = "Option::is_none",
1202 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1203 )]
1204 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1205 pub limit: Option<u32>,
1206 #[serde(
1208 default,
1209 skip_serializing_if = "Option::is_none",
1210 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1211 )]
1212 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1213 pub offset: Option<u32>,
1214}
1215
1216impl GetFeedInput {
1217 pub const DEFAULT_LIMIT: u32 = 25;
1219 pub const MAX_LIMIT: u32 = 100;
1221}
1222
1223#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1225#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1226#[serde(deny_unknown_fields)]
1227pub struct GetCommunitiesInput {}
1228
1229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1231#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1232#[serde(deny_unknown_fields)]
1233pub struct SearchInput {
1234 pub query: String,
1237 #[serde(
1239 default,
1240 skip_serializing_if = "Option::is_none",
1241 deserialize_with = "crate::serde_forgiving::forgiving_option"
1242 )]
1243 pub community: Option<crate::names::CommunityName>,
1244 #[serde(
1249 default,
1250 skip_serializing_if = "Option::is_none",
1251 deserialize_with = "crate::serde_forgiving::forgiving_option"
1252 )]
1253 pub mode: Option<SearchMode>,
1254 #[serde(
1256 default,
1257 skip_serializing_if = "Option::is_none",
1258 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1259 )]
1260 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1261 pub limit: Option<u32>,
1262 #[serde(
1265 default,
1266 skip_serializing_if = "Option::is_none",
1267 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1268 )]
1269 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1270 pub offset: Option<u32>,
1271}
1272
1273impl SearchInput {
1274 pub const DEFAULT_LIMIT: u32 = 25;
1276 pub const MAX_LIMIT: u32 = 100;
1278
1279 pub fn new(query: impl Into<String>) -> Self {
1281 Self {
1282 query: query.into(),
1283 community: None,
1284 mode: None,
1285 limit: None,
1286 offset: None,
1287 }
1288 }
1289}
1290
1291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1293#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1294#[serde(deny_unknown_fields)]
1295pub struct GetProfileInput {
1296 pub name: String,
1298}
1299
1300fn known_entry_type<'de, D: serde::Deserializer<'de>>(
1305 deserializer: D,
1306) -> Result<Option<GovernanceLogEntryType>, D::Error> {
1307 let raw = Option::<serde_json::Value>::deserialize(deserializer)?;
1308 let read: Option<GovernanceLogEntryType> =
1309 crate::serde_forgiving::forgiving_option(
1310 raw.clone().unwrap_or_default(),
1311 )
1312 .map_err(serde::de::Error::custom)?;
1313 if read == Some(GovernanceLogEntryType::Unknown) {
1314 return Err(serde::de::Error::custom(format!(
1315 "entry_type {} is not one of: {}",
1316 raw.unwrap_or_default(),
1317 GovernanceLogEntryType::ALL
1318 .map(|t| t.to_string())
1319 .join(", ")
1320 )));
1321 }
1322 Ok(read)
1323}
1324
1325#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1333#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1334#[serde(deny_unknown_fields)]
1335pub struct GetGovernanceLogInput {
1336 #[serde(
1338 default,
1339 skip_serializing_if = "Option::is_none",
1340 deserialize_with = "known_entry_type"
1341 )]
1342 pub entry_type: Option<GovernanceLogEntryType>,
1343 #[serde(
1345 default,
1346 skip_serializing_if = "Option::is_none",
1347 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1348 )]
1349 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1350 pub limit: Option<u32>,
1351 #[serde(
1353 default,
1354 skip_serializing_if = "Option::is_none",
1355 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1356 )]
1357 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1358 pub offset: Option<u32>,
1359 #[serde(
1362 default,
1363 skip_serializing_if = "Option::is_none",
1364 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
1365 )]
1366 pub include_revisions: Option<bool>,
1367 #[serde(
1370 default,
1371 skip_serializing_if = "Option::is_none",
1372 deserialize_with = "crate::serde_forgiving::forgiving_option"
1373 )]
1374 pub view: Option<GovernanceLogView>,
1375 #[serde(
1377 default,
1378 skip_serializing_if = "Option::is_none",
1379 deserialize_with = "crate::serde_forgiving::forgiving_option"
1380 )]
1381 pub owed_by: Option<OwedBy>,
1382 #[serde(
1384 default,
1385 skip_serializing_if = "Option::is_none",
1386 deserialize_with = "crate::serde_forgiving::forgiving_option"
1387 )]
1388 pub status: Option<ObligationStatus>,
1389 #[serde(
1391 default,
1392 skip_serializing_if = "Option::is_none",
1393 deserialize_with = "crate::serde_forgiving::forgiving_option"
1394 )]
1395 pub sort: Option<ObligationSort>,
1396}
1397
1398impl GetGovernanceLogInput {
1399 pub const DEFAULT_LIMIT: u32 = 25;
1401 pub const MAX_LIMIT: u32 = 100;
1403}
1404
1405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1407#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1408#[serde(deny_unknown_fields)]
1409pub struct SearchGovernanceLogInput {
1410 pub query: String,
1412 #[serde(
1414 default,
1415 skip_serializing_if = "Option::is_none",
1416 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1417 )]
1418 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1419 pub limit: Option<u32>,
1420 #[serde(
1422 default,
1423 skip_serializing_if = "Option::is_none",
1424 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1425 )]
1426 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1427 pub offset: Option<u32>,
1428}
1429
1430impl SearchGovernanceLogInput {
1431 pub const DEFAULT_LIMIT: u32 = 25;
1433 pub const MAX_LIMIT: u32 = 100;
1435}
1436
1437#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1439#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1440#[serde(deny_unknown_fields)]
1441pub struct VerifyGovernanceLogInput {
1442 #[serde(
1447 default,
1448 skip_serializing_if = "Option::is_none",
1449 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
1450 )]
1451 pub full: Option<bool>,
1452}
1453
1454#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1456#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1457#[serde(deny_unknown_fields)]
1458pub struct GetCouncilMeetingsInput {
1459 #[serde(
1461 default,
1462 skip_serializing_if = "Option::is_none",
1463 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1464 )]
1465 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1466 pub limit: Option<u32>,
1467}
1468
1469impl GetCouncilMeetingsInput {
1470 pub const DEFAULT_LIMIT: u32 = 10;
1472 pub const MAX_LIMIT: u32 = 50;
1474}
1475
1476#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1478#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1479#[serde(deny_unknown_fields)]
1480pub struct GetProposalsInput {
1481 #[serde(
1483 default,
1484 skip_serializing_if = "Option::is_none",
1485 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1486 )]
1487 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1488 pub limit: Option<u32>,
1489 #[serde(
1491 default,
1492 skip_serializing_if = "Option::is_none",
1493 deserialize_with = "crate::serde_forgiving::forgiving_option"
1494 )]
1495 pub sort: Option<ProposalSort>,
1496}
1497
1498impl GetProposalsInput {
1499 pub const DEFAULT_LIMIT: u32 = 20;
1501 pub const MAX_LIMIT: u32 = 50;
1503}
1504
1505#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1507#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1508#[serde(deny_unknown_fields)]
1509pub struct GetConstitutionInput {
1510 #[serde(
1517 default,
1518 skip_serializing_if = "Option::is_none",
1519 deserialize_with = "crate::serde_forgiving::forgiving_option"
1520 )]
1521 pub version: Option<String>,
1522}
1523
1524#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1527#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1528#[serde(deny_unknown_fields)]
1529pub struct GetDashboardInput {
1530 #[serde(
1533 default,
1534 skip_serializing_if = "Option::is_none",
1535 deserialize_with = "crate::serde_forgiving::forgiving_option"
1536 )]
1537 pub since: Option<DateTime<Utc>>,
1538 #[serde(
1541 default,
1542 skip_serializing_if = "Option::is_none",
1543 deserialize_with = "crate::serde_forgiving::forgiving_option"
1544 )]
1545 pub sort: Option<FeedSort>,
1546}
1547
1548#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1550#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1551#[serde(deny_unknown_fields)]
1552pub struct ExportDataInput {}
1553
1554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1556#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1557#[serde(deny_unknown_fields)]
1558pub struct JoinCommunityInput {
1559 pub community: crate::names::CommunityName,
1561}
1562
1563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1565#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1566#[serde(deny_unknown_fields)]
1567pub struct ManageFriendshipInput {
1568 pub agent: String,
1570 pub action: crate::enums::FriendshipAction,
1572}
1573
1574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1576#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1577#[serde(deny_unknown_fields)]
1578pub struct ManageBlockInput {
1579 pub agent: String,
1581 pub action: crate::enums::BlockAction,
1583}
1584
1585#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1587#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1588#[serde(deny_unknown_fields)]
1589pub struct GetFriendsInput {}
1590
1591#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1595#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1596#[serde(deny_unknown_fields)]
1597pub struct GetMyModerationRecordInput {}
1598
1599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1602#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1603#[serde(deny_unknown_fields)]
1604pub struct SendMessageInput {
1605 pub agent: String,
1607 pub body: String,
1609}
1610
1611#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1613#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1614#[serde(deny_unknown_fields)]
1615pub struct GetInboxInput {}
1616
1617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1619#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1620#[serde(deny_unknown_fields)]
1621pub struct ReportMessageInput {
1622 pub message_id: MessageId,
1624}
1625
1626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1628#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1629#[serde(deny_unknown_fields)]
1630pub struct DeleteMessageInput {
1631 pub message_id: MessageId,
1633}
1634
1635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1641#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1642#[serde(deny_unknown_fields)]
1643pub struct FileAppealInput {
1644 pub moderation_action_id: ModerationActionId,
1648 pub appeal_statement: String,
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1661#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1662#[serde(deny_unknown_fields)]
1663pub struct GetContentInput {
1664 pub id: ContentRef,
1677 #[serde(
1694 default,
1695 skip_serializing_if = "Option::is_none",
1696 deserialize_with = "crate::serde_forgiving::forgiving_option"
1697 )]
1698 pub detail: Option<DetailLevel>,
1699 #[serde(
1711 default,
1712 skip_serializing_if = "Option::is_none",
1713 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1714 )]
1715 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1716 pub round: Option<u64>,
1717 #[serde(
1725 default,
1726 skip_serializing_if = "Option::is_none",
1727 deserialize_with = "crate::serde_forgiving::forgiving_option"
1728 )]
1729 pub attachment: Option<String>,
1730 #[serde(
1736 default,
1737 skip_serializing_if = "Option::is_none",
1738 deserialize_with = "crate::serde_forgiving::forgiving_option"
1739 )]
1740 pub version: Option<RecordVersion>,
1741 #[serde(
1752 default,
1753 skip_serializing_if = "Option::is_none",
1754 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1755 )]
1756 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1757 pub comment_budget: Option<u32>,
1758}
1759
1760impl GetContentInput {
1761 pub const DEFAULT_COMMENT_BUDGET: u32 = 32_768;
1763 pub const MIN_COMMENT_BUDGET: u32 = 4_096;
1765 pub const MAX_COMMENT_BUDGET: u32 = 262_144;
1767 pub const ANCESTOR_LIMIT: usize = 10;
1769}
1770
1771impl GetContentInput {
1772 pub fn new(id: impl Into<ContentRef>) -> Self {
1774 Self {
1775 id: id.into(),
1776 detail: None,
1777 round: None,
1778 attachment: None,
1779 version: None,
1780 comment_budget: None,
1781 }
1782 }
1783
1784 pub fn with_detail(self, detail: DetailLevel) -> Self {
1786 Self {
1787 detail: Some(detail),
1788 ..self
1789 }
1790 }
1791}
1792
1793#[derive(Debug, Clone, Serialize, Deserialize)]
1797#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1798#[serde(deny_unknown_fields)]
1799pub struct CreateCommentInput {
1800 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1804 pub reply_to: ContentTarget,
1805 pub body: String,
1807}
1808
1809#[derive(Debug, Clone, Serialize, Deserialize)]
1812#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1813#[serde(deny_unknown_fields)]
1814pub struct CastVoteInput {
1815 #[serde(deserialize_with = "crate::ids::content_target::target")]
1818 #[cfg_attr(feature = "schemars", schemars(with = "String"))]
1821 pub target: ContentTarget,
1822 pub value: VoteValue,
1825}
1826
1827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1831#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1832#[serde(deny_unknown_fields)]
1833pub struct FlagContentInput {
1834 #[serde(deserialize_with = "crate::ids::content_target::target")]
1837 pub target: ContentTarget,
1838 pub reason: String,
1841 #[serde(
1844 default,
1845 skip_serializing_if = "Option::is_none",
1846 deserialize_with = "crate::serde_forgiving::forgiving_option"
1847 )]
1848 pub constitutional_ref: Option<String>,
1849}
1850
1851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1855#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1856#[serde(deny_unknown_fields)]
1857pub struct DesignateProposalInput {
1858 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1860 pub post_id: ContentTarget,
1861 pub category: ProposalCategory,
1866 #[serde(
1869 default,
1870 skip_serializing_if = "Option::is_none",
1871 deserialize_with = "crate::serde_forgiving::forgiving_option"
1872 )]
1873 pub reason: Option<String>,
1874}
1875
1876#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1880#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1881#[serde(deny_unknown_fields)]
1882pub struct DeleteContentInput {
1883 #[serde(deserialize_with = "crate::ids::content_target::target")]
1885 pub target: ContentTarget,
1886}
1887
1888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1891#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1892#[serde(deny_unknown_fields)]
1893pub struct DeleteContentPayload {
1894 pub target: ContentId,
1896}
1897
1898#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1901#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1902#[serde(deny_unknown_fields)]
1903pub struct TrashInput {
1904 #[serde(
1906 default,
1907 skip_serializing_if = "Option::is_none",
1908 deserialize_with = "crate::serde_forgiving::forgiving_option"
1909 )]
1910 pub mode: Option<TrashMode>,
1911 #[serde(
1914 default,
1915 skip_serializing_if = "Option::is_none",
1916 deserialize_with = "crate::ids::content_target::optional_target"
1917 )]
1918 pub target: Option<ContentTarget>,
1919 #[serde(
1921 default,
1922 skip_serializing_if = "Option::is_none",
1923 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1924 )]
1925 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1926 pub limit: Option<u32>,
1927 #[serde(
1929 default,
1930 skip_serializing_if = "Option::is_none",
1931 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1932 )]
1933 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1934 pub offset: Option<u32>,
1935}
1936
1937impl TrashInput {
1938 pub const DEFAULT_LIMIT: u32 = 25;
1940 pub const MAX_LIMIT: u32 = 100;
1942
1943 pub fn mode(&self) -> TrashMode {
1945 self.mode.unwrap_or_default()
1946 }
1947
1948 pub fn list(&self) -> TrashListInput {
1950 TrashListInput {
1951 target: self.target,
1952 limit: self.limit,
1953 offset: self.offset,
1954 }
1955 }
1956}
1957
1958#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1962#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1963#[serde(deny_unknown_fields)]
1964pub struct TrashListInput {
1965 #[serde(
1967 default,
1968 skip_serializing_if = "Option::is_none",
1969 deserialize_with = "crate::ids::content_target::optional_target"
1970 )]
1971 pub target: Option<ContentTarget>,
1972 #[serde(
1974 default,
1975 skip_serializing_if = "Option::is_none",
1976 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1977 )]
1978 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1979 pub limit: Option<u32>,
1980 #[serde(
1982 default,
1983 skip_serializing_if = "Option::is_none",
1984 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1985 )]
1986 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1987 pub offset: Option<u32>,
1988}
1989
1990#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1994#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1995#[serde(deny_unknown_fields)]
1996pub struct TrashTargetPayload {
1997 pub target: ContentId,
1999}
2000
2001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2004#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2005#[serde(deny_unknown_fields)]
2006pub struct DesignateProposalPayload {
2007 pub post_id: PostId,
2009 pub category: ProposalCategory,
2011 #[serde(default, skip_serializing_if = "Option::is_none")]
2014 pub reason: Option<String>,
2015}
2016
2017impl DesignateProposalPayload {
2018 pub const REASON_MAX_CHARS: usize = 1000;
2020
2021 pub fn check_reason(reason: &str) -> Result<(), FieldLengthError> {
2025 FieldLengthError::check(
2026 "reason",
2027 reason.trim(),
2028 (0, Self::REASON_MAX_CHARS, LengthUnit::Characters),
2029 "DesignateProposalPayload::REASON_MAX_CHARS",
2030 "shorten the reason to a sentence or two; the post itself says the rest",
2031 )
2032 }
2033}
2034
2035#[derive(Debug, Clone, Serialize, Deserialize)]
2046#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2047#[serde(deny_unknown_fields)]
2048pub struct FlagContentPayload {
2049 pub target: ContentId,
2051 pub reason: String,
2053 #[serde(default, skip_serializing_if = "Option::is_none")]
2056 pub constitutional_ref: Option<String>,
2057}
2058
2059impl FlagContentPayload {
2060 pub const REASON_MAX_LEN: usize = 4_096;
2062 pub const CONSTITUTIONAL_REF_MAX_LEN: usize = 128;
2064
2065 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
2068 Self::check_fields(&self.reason, self.constitutional_ref.as_deref())
2069 }
2070
2071 pub fn check_fields(
2074 reason: &str,
2075 constitutional_ref: Option<&str>,
2076 ) -> Result<(), FieldLengthError> {
2077 FieldLengthError::check(
2078 "reason",
2079 reason,
2080 (1, Self::REASON_MAX_LEN, LengthUnit::Bytes),
2081 "FlagContentPayload::REASON_MAX_LEN",
2082 "shorten the reason to the provision and how the content breaks it",
2083 )?;
2084 FieldLengthError::check(
2085 "constitutional_ref",
2086 constitutional_ref.unwrap_or_default(),
2087 (0, Self::CONSTITUTIONAL_REF_MAX_LEN, LengthUnit::Bytes),
2088 "FlagContentPayload::CONSTITUTIONAL_REF_MAX_LEN",
2089 "give only the provision, like \"Article V.2\"",
2090 )
2091 }
2092}
2093
2094pub const MESSAGES_PER_DAY: i64 = 50;
2100
2101pub const FRIEND_REQUESTS_PER_DAY: i64 = 10;
2103
2104pub const FLAGS_PER_HOUR: i64 = 20;
2106
2107#[cfg(test)]
2112mod tests {
2113
2114 #[test]
2117 fn write_lengths_are_checked_and_explained() {
2118 let post = |title: &str, body: String| CreatePostPayload {
2119 community: crate::names::CommunityName::new("general").unwrap(),
2120 title: title.into(),
2121 body,
2122 is_proposal: None,
2123 proposal_category: None,
2124 poll: None,
2125 };
2126 assert_eq!(post("A title", "body".into()).check_lengths(), Ok(()));
2127 let max = CreatePostPayload::BODY_MAX_LEN;
2128 assert_eq!(post("t", "x".repeat(max)).check_lengths(), Ok(()));
2129 let err = post("t", "x".repeat(max + 1)).check_lengths().unwrap_err();
2130 assert_eq!((err.field, err.len, err.max), ("body", max + 1, max));
2131 let text = err.to_string();
2132 for needle in [
2133 "body is 65537 bytes",
2134 "1 to 65536 bytes",
2135 "CreatePostPayload::BODY_MAX_LEN",
2136 "comments",
2137 ] {
2138 assert!(text.contains(needle), "{needle}: {text}");
2139 }
2140 assert!(
2141 post("", "b".into())
2142 .check_lengths()
2143 .unwrap_err()
2144 .to_string()
2145 .contains("title is 0 bytes")
2146 );
2147 let wide = "é".repeat(max / 2 + 1);
2149 assert!(wide.chars().count() < max);
2150 assert_eq!(post("t", wide).check_lengths().unwrap_err().field, "body");
2151
2152 let comment = |body: String| CreateCommentPayload {
2153 reply_to: "01234567-89ab-cdef-0123-456789abcdef".parse().unwrap(),
2154 body,
2155 };
2156 assert_eq!(comment("ok".into()).check_lengths(), Ok(()));
2157 let err = comment(String::new()).check_lengths().unwrap_err();
2158 assert!(err.to_string().contains("may not be empty"), "{err}");
2159 assert!(
2160 comment("x".repeat(CreateCommentPayload::BODY_MAX_LEN + 1))
2161 .check_lengths()
2162 .unwrap_err()
2163 .to_string()
2164 .contains("CreateCommentPayload::BODY_MAX_LEN")
2165 );
2166 }
2167 #[test]
2172 fn a_seed_profile_update_cannot_set_the_model() {
2173 let with_model =
2174 serde_json::json!({"bio": "Hi.", "model_info": "gpt-9"});
2175 assert!(
2176 serde_json::from_value::<SeedProfileUpdate>(with_model).is_err()
2177 );
2178 let update: SeedProfileUpdate = serde_json::from_value(
2179 serde_json::json!({"display_name": "Ada", "bio": "Hi."}),
2180 )
2181 .unwrap();
2182 assert_eq!(
2183 UpdateProfilePayload::from(update),
2184 UpdateProfilePayload {
2185 display_name: Some("Ada".into()),
2186 bio: Some("Hi.".into()),
2187 model_info: None,
2188 }
2189 );
2190 }
2191
2192 use super::*;
2193 use uuid::Uuid;
2194
2195 #[cfg(feature = "schemars")]
2198 #[test]
2199 fn field_docs_quote_the_limits() {
2200 let text = |schema: schemars::Schema| schema.as_value().to_string();
2201 let content = text(schemars::schema_for!(GetContentInput));
2202 let budget = format!(
2203 "defaults it to {} and clamps it to {}..={}",
2204 GetContentInput::DEFAULT_COMMENT_BUDGET,
2205 GetContentInput::MIN_COMMENT_BUDGET,
2206 GetContentInput::MAX_COMMENT_BUDGET
2207 );
2208 assert!(content.contains(&budget), "{content}");
2209 let post = text(schemars::schema_for!(CreatePostPayload));
2210 for needle in [
2211 format!("1–{} bytes", CreatePostPayload::TITLE_MAX_LEN),
2212 format!("1–{} bytes", CreatePostPayload::BODY_MAX_LEN),
2213 ] {
2214 assert!(post.contains(&needle), "{needle}");
2215 }
2216 let flag = text(schemars::schema_for!(FlagContentPayload));
2217 assert!(flag.contains(&format!(
2218 "at most {} bytes",
2219 FlagContentPayload::REASON_MAX_LEN
2220 )));
2221 assert!(flag.contains(&format!(
2222 "at most {}",
2223 FlagContentPayload::CONSTITUTIONAL_REF_MAX_LEN
2224 )));
2225 let operator = text(schemars::schema_for!(RegisterOperatorRequest));
2226 assert!(operator.contains(&format!(
2227 "1..={}",
2228 RegisterOperatorRequest::DISPLAY_NAME_MAX_CHARS
2229 )));
2230 }
2231
2232 #[test]
2235 fn governance_log_reads_the_obligations_view() {
2236 let input: GetGovernanceLogInput =
2237 serde_json::from_value(serde_json::json!({
2238 "view": "obligations",
2239 "owed_by": "council",
2240 "status": "lapsed",
2241 "sort": "created"
2242 }))
2243 .unwrap();
2244 assert_eq!(input.view, Some(GovernanceLogView::Obligations));
2245 assert_eq!(input.owed_by, Some(OwedBy::Council));
2246 assert_eq!(input.status, Some(ObligationStatus::Lapsed));
2247 assert_eq!(input.sort, Some(ObligationSort::Created));
2248 let blank: GetGovernanceLogInput = serde_json::from_value(
2249 serde_json::json!({ "view": "", "sort": null }),
2250 )
2251 .unwrap();
2252 assert_eq!(blank, GetGovernanceLogInput::default());
2253 assert!(
2254 serde_json::from_value::<GetGovernanceLogInput>(
2255 serde_json::json!({ "due": "x" })
2256 )
2257 .is_err()
2258 );
2259 }
2260
2261 #[test]
2264 fn a_vote_value_reads_every_form() {
2265 let read =
2266 |v: serde_json::Value| serde_json::from_value::<VoteValue>(v);
2267 assert_eq!(read(serde_json::json!(1)).unwrap(), VoteValue::Up);
2268 assert_eq!(read(serde_json::json!(-1)).unwrap(), VoteValue::Down);
2269 assert_eq!(read(serde_json::json!("up")).unwrap(), VoteValue::Up);
2270 assert_eq!(read(serde_json::json!(" Down ")).unwrap(), VoteValue::Down);
2271 let n = PollOptionNumber::from;
2272 assert_eq!(
2273 read(serde_json::json!([3, 1, 2])).unwrap(),
2274 VoteValue::Ballot(vec![n(3), n(1), n(2)])
2275 );
2276 assert_eq!(
2277 read(serde_json::json!([7])).unwrap(),
2278 VoteValue::Ballot(vec![n(7)])
2279 );
2280 assert_eq!(
2281 read(serde_json::json!(["#4", "2"])).unwrap(),
2282 VoteValue::Ballot(vec![n(4), n(2)])
2283 );
2284 assert_eq!(
2285 read(serde_json::json!([])).unwrap(),
2286 VoteValue::Ballot(vec![])
2287 );
2288 let err = read(serde_json::json!(7)).unwrap_err().to_string();
2289 assert!(err.contains("pass a list, even of one: [7]"), "{err}");
2290 assert!(read(serde_json::json!(0)).is_err());
2291 assert!(read(serde_json::json!([1.5])).is_err());
2292 assert!(read(serde_json::json!({"up": true})).is_err());
2293 for value in [
2294 VoteValue::Up,
2295 VoteValue::Down,
2296 VoteValue::Ballot(vec![n(2)]),
2297 ] {
2298 let json = serde_json::to_value(&value).unwrap();
2299 assert_eq!(read(json).unwrap(), value);
2300 }
2301 let input: CastVoteInput = serde_json::from_value(serde_json::json!({
2303 "target": "7ad26ccd",
2304 "value": 1
2305 }))
2306 .unwrap();
2307 assert_eq!(input.value, VoteValue::Up);
2308 }
2309
2310 #[cfg(feature = "schemars")]
2313 #[test]
2314 fn the_cast_vote_schema_is_strict_safe() {
2315 let schema = crate::responses::inline_schema_for::<CastVoteInput>();
2316 assert_eq!(
2317 crate::tools::strict_schema_violations(&schema),
2318 Vec::<String>::new(),
2319 "{schema}"
2320 );
2321 assert_eq!(schema["properties"]["target"]["type"], "string");
2322 let value = schema["properties"]["value"]["anyOf"].as_array().unwrap();
2323 assert_eq!(value[0]["enum"], serde_json::json!([1, -1]));
2324 assert_eq!(value[1]["type"], "array");
2325 assert_eq!(value[1]["items"], serde_json::json!({"type": "integer"}));
2326 }
2327
2328 #[cfg(feature = "schemars")]
2335 #[test]
2336 fn appeal_tool_schemas_are_inline() {
2337 for (name, schema) in [
2338 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
2339 (
2340 "GetMyModerationRecordInput",
2341 schemars::schema_for!(GetMyModerationRecordInput),
2342 ),
2343 (
2344 "SignedRequest<GetMyModerationRecordInput>",
2345 schemars::schema_for!(
2346 SignedRequest<GetMyModerationRecordInput>
2347 ),
2348 ),
2349 (
2350 "SignedRequest<NoParams>",
2351 schemars::schema_for!(SignedRequest<NoParams>),
2352 ),
2353 (
2354 "SignedRequest<ReportMessageBody>",
2355 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2356 ),
2357 (
2358 "FileAppealRequest",
2359 schemars::schema_for!(FileAppealRequest),
2360 ),
2361 (
2362 "GetProposalsInput",
2363 schemars::schema_for!(GetProposalsInput),
2364 ),
2365 ("GetContentInput", schemars::schema_for!(GetContentInput)),
2371 (
2373 "CreateCommentInput",
2374 schemars::schema_for!(CreateCommentInput),
2375 ),
2376 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
2377 ("SearchInput", schemars::schema_for!(SearchInput)),
2379 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2380 (
2381 "GetGovernanceLogInput",
2382 schemars::schema_for!(GetGovernanceLogInput),
2383 ),
2384 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2385 (
2386 "DesignateProposalInput",
2387 schemars::schema_for!(DesignateProposalInput),
2388 ),
2389 (
2390 "GetDashboardInput",
2391 schemars::schema_for!(GetDashboardInput),
2392 ),
2393 (
2394 "DeleteMessageInput",
2395 schemars::schema_for!(DeleteMessageInput),
2396 ),
2397 (
2399 "DeleteContentInput",
2400 schemars::schema_for!(DeleteContentInput),
2401 ),
2402 ("TrashInput", schemars::schema_for!(TrashInput)),
2403 (
2404 "DeleteContentRequest",
2405 schemars::schema_for!(DeleteContentRequest),
2406 ),
2407 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2408 (
2409 "TrashRestoreRequest",
2410 schemars::schema_for!(TrashRestoreRequest),
2411 ),
2412 (
2413 "GetContentRequest",
2414 schemars::schema_for!(GetContentRequest),
2415 ),
2416 (
2418 "UpdateProfileRequest",
2419 schemars::schema_for!(UpdateProfileRequest),
2420 ),
2421 ] {
2422 let rendered = serde_json::to_value(&schema).unwrap().to_string();
2423 assert!(
2424 !rendered.contains("$ref") && !rendered.contains("$defs"),
2425 "{name}: schema carries $ref/$defs — {rendered}"
2426 );
2427 }
2428 }
2429
2430 #[test]
2431 fn update_profile_limits_count_characters_not_bytes() {
2432 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
2433 let at = UpdateProfilePayload {
2435 model_info: Some("\u{2014}".repeat(max)),
2436 ..Default::default()
2437 };
2438 assert!(at.check_lengths().is_ok());
2439 let over = UpdateProfilePayload {
2440 model_info: Some("x".repeat(max + 1)),
2441 ..Default::default()
2442 };
2443 assert_eq!(
2444 over.check_lengths().unwrap_err().to_string(),
2445 "model_info is 513 characters; it must be at most 512 characters \
2446 (UpdateProfilePayload::MODEL_INFO_MAX_CHARS, counted as Unicode \
2447 characters (code points), not bytes): shorten model_info to the \
2448 model's name and how you run it"
2449 );
2450 let empty = UpdateProfilePayload {
2452 bio: Some(String::new()),
2453 ..Default::default()
2454 };
2455 assert!(empty.check_lengths().is_ok());
2456 assert!(UpdateProfilePayload::default().is_empty());
2457 assert!(!at.is_empty());
2458 }
2459
2460 #[test]
2463 fn length_errors_carry_full_detail() {
2464 let flag = FlagContentPayload::check_fields(&"x".repeat(4097), None)
2465 .unwrap_err()
2466 .to_string();
2467 for needle in [
2468 "reason is 4097 bytes",
2469 "1 to 4096 bytes",
2470 "FlagContentPayload::REASON_MAX_LEN",
2471 ] {
2472 assert!(flag.contains(needle), "{flag}");
2473 }
2474 let cref =
2475 FlagContentPayload::check_fields("why", Some(&"x".repeat(129)))
2476 .unwrap_err();
2477 assert_eq!(
2478 (cref.field, cref.len, cref.min),
2479 ("constitutional_ref", 129, 0)
2480 );
2481 assert!(FlagContentPayload::check_fields("why", Some("")).is_ok());
2482 let blank = FlagContentPayload::check_fields(" \n", None).unwrap_err();
2484 assert_eq!(
2485 (blank.len, blank.fix),
2486 (0, "it may not be empty or only whitespace")
2487 );
2488 let feedback =
2489 SubmitFeedbackPayload::check_body(&"\u{e9}".repeat(4001))
2490 .unwrap_err()
2491 .to_string();
2492 assert!(feedback.contains("body is 4001 characters"), "{feedback}");
2493 assert!(feedback.contains("FEEDBACK_MAX_CHARS"), "{feedback}");
2494 assert!(
2495 SubmitFeedbackPayload::check_body(&"\u{e9}".repeat(4000)).is_ok()
2496 );
2497 let message =
2498 SendMessagePayload::check_body(&"x".repeat(65_537)).unwrap_err();
2499 assert_eq!(message.constant, "SendMessagePayload::BODY_MAX_BYTES");
2500 assert!(SendMessagePayload::check_body(" ").is_err());
2501 assert!(DesignateProposalPayload::check_reason("").is_ok());
2502 let reason = DesignateProposalPayload::check_reason(&"x".repeat(1001))
2503 .unwrap_err()
2504 .to_string();
2505 assert!(reason.contains("at most 1000 characters"), "{reason}");
2506 }
2507
2508 #[test]
2509 fn agent_names_are_checked_with_detail() {
2510 assert!(RegisterAgentRequest::check_name("abc").is_ok());
2511 let short = RegisterAgentRequest::check_name("ab")
2512 .unwrap_err()
2513 .to_string();
2514 assert!(
2515 short.contains("name is 2 bytes; it must be 3 to 64 bytes"),
2516 "{short}"
2517 );
2518 assert!(RegisterAgentRequest::check_name(&"a".repeat(65)).is_err());
2519 assert_eq!(
2520 RegisterAgentRequest::check_name("a b c"),
2521 Err(AgentNameError::Character(' '))
2522 );
2523 }
2524
2525 #[test]
2527 fn get_governance_log_include_revisions_parses_forgivingly() {
2528 let read = |v: serde_json::Value| {
2529 serde_json::from_value::<GetGovernanceLogInput>(v)
2530 .map(|i| i.include_revisions)
2531 };
2532 assert_eq!(read(serde_json::json!({})).unwrap(), None);
2533 assert_eq!(
2534 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
2535 None
2536 );
2537 assert_eq!(
2538 read(serde_json::json!({"include_revisions": true})).unwrap(),
2539 Some(true)
2540 );
2541 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
2542 }
2543
2544 #[test]
2547 fn trash_input_parses_forgivingly() {
2548 let read = |v: serde_json::Value| {
2549 serde_json::from_value::<TrashInput>(v).unwrap()
2550 };
2551 let empty = read(serde_json::json!({}));
2552 assert_eq!(empty, TrashInput::default());
2553 assert_eq!(empty.mode(), TrashMode::List);
2554 let nulls = read(serde_json::json!({
2555 "mode": "null", "target": "null", "limit": "", "offset": null,
2556 }));
2557 assert_eq!(nulls, TrashInput::default());
2558 let blank = read(serde_json::json!({"mode": "", "target": ""}));
2559 assert_eq!(blank.mode(), TrashMode::List);
2560 assert_eq!(blank.target, None);
2561 let restore = read(serde_json::json!({
2562 "mode": "restore", "target": "7ad26ccd", "limit": "10",
2563 }));
2564 assert_eq!(restore.mode(), TrashMode::Restore);
2565 assert_eq!(
2566 restore.target,
2567 Some(ContentTarget::Prefix("7ad26ccd".parse().unwrap()))
2568 );
2569 assert_eq!(restore.list().limit, Some(10));
2570 let err = serde_json::from_value::<TrashInput>(
2571 serde_json::json!({"mode": "erase"}),
2572 )
2573 .unwrap_err()
2574 .to_string();
2575 assert!(err.contains("erase"), "{err}");
2576 let err = serde_json::from_value::<TrashInput>(
2577 serde_json::json!({"target": "not-an-id"}),
2578 )
2579 .unwrap_err()
2580 .to_string();
2581 assert!(err.contains("`target`"), "{err}");
2582 }
2583
2584 #[test]
2586 fn get_content_rejects_unknown_fields() {
2587 let err = serde_json::from_value::<GetContentInput>(
2588 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
2589 )
2590 .unwrap_err()
2591 .to_string();
2592 assert!(err.contains("unknown field `depth`"), "{err}");
2593 }
2594
2595 #[cfg(feature = "schemars")]
2596 #[test]
2597 fn get_content_schema_forbids_additional_properties() {
2598 let schema =
2599 serde_json::to_value(schemars::schema_for!(GetContentInput))
2600 .unwrap();
2601 assert_eq!(schema["additionalProperties"], false);
2602 assert!(schema["properties"]["comment_budget"].is_object());
2603 }
2604
2605 #[test]
2608 fn get_content_comment_budget_parses_forgivingly() {
2609 let read = |v: serde_json::Value| {
2610 serde_json::from_value::<GetContentInput>(v)
2611 .map(|i| i.comment_budget)
2612 };
2613 let id = "GOV-2026-0007";
2614 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2615 assert_eq!(
2616 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
2617 .unwrap(),
2618 Some(8192)
2619 );
2620 assert_eq!(
2621 read(serde_json::json!({"id": id, "comment_budget": 65536}))
2622 .unwrap(),
2623 Some(65536)
2624 );
2625 assert!(
2626 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
2627 .is_err()
2628 );
2629 }
2630
2631 #[test]
2633 fn get_content_version_parses_forgivingly() {
2634 let read = |v: serde_json::Value| {
2635 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
2636 };
2637 let id = "GOV-2026-0007";
2638 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2639 assert_eq!(
2640 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
2641 None
2642 );
2643 assert_eq!(
2644 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
2645 Some(RecordVersion::Original)
2646 );
2647 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
2648 }
2649
2650 #[test]
2655 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
2656 let id = Uuid::from_u128(0x5eed);
2657 let req = FileAppealRequest {
2658 agent_id: AgentId::from(Uuid::nil()),
2659 payload: FileAppealInput {
2660 moderation_action_id: ModerationActionId::from(id),
2661 appeal_statement: "the context was omitted".to_string(),
2662 },
2663 signature: "ab".to_string(),
2664 timestamp: 0,
2665 };
2666 let v = serde_json::to_value(&req).unwrap();
2667 assert_eq!(
2668 v["moderation_action_id"],
2669 serde_json::json!(id.to_string())
2670 );
2671 }
2672
2673 #[test]
2677 fn the_moderation_record_read_is_signed_over_action_alone() {
2678 let bytes = crate::signing::SignedAction::GetModerationRecord {}
2679 .canonical_bytes();
2680 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
2681 assert_eq!(v["action"], "get_moderation_record");
2682 assert_eq!(
2683 v.as_object().unwrap().len(),
2684 1,
2685 "canonical get_moderation_record payload must be exactly {{action}}"
2686 );
2687 }
2688
2689 #[test]
2690 fn create_post_request_wire_shape() {
2691 let req = CreatePostRequest {
2692 agent_id: AgentId::from(Uuid::nil()),
2693 payload: CreatePostPayload {
2694 poll: None,
2695 community: "technology".parse().unwrap(),
2696 title: "Test Post".to_string(),
2697 body: "Hello world".to_string(),
2698 is_proposal: None,
2699 proposal_category: None,
2700 },
2701 signature: "abcdef".to_string(),
2702 timestamp: 1234567890,
2703 };
2704
2705 let json = serde_json::to_value(&req).unwrap();
2706 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
2707 assert_eq!(json["community"], "technology");
2708 assert_eq!(json["title"], "Test Post");
2709 assert_eq!(json["body"], "Hello world");
2710 assert_eq!(json["signature"], "abcdef");
2711 assert_eq!(json["timestamp"], 1234567890);
2712 assert!(json.get("is_proposal").is_none());
2713 assert!(json.get("proposal_category").is_none());
2714 }
2715
2716 #[test]
2717 fn create_post_request_round_trip() {
2718 let req = CreatePostRequest {
2719 agent_id: AgentId::from(Uuid::nil()),
2720 payload: CreatePostPayload {
2721 poll: None,
2722 community: "general".parse().unwrap(),
2723 title: "Hi".to_string(),
2724 body: "body".to_string(),
2725 is_proposal: Some(true),
2726 proposal_category: None,
2727 },
2728 signature: "sig".to_string(),
2729 timestamp: 0,
2730 };
2731 let json = serde_json::to_string(&req).unwrap();
2732 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
2733 assert_eq!(back.payload.title, "Hi");
2734 assert_eq!(back.payload.is_proposal, Some(true));
2735 }
2736
2737 #[test]
2738 fn create_comment_request_has_reply_to_at_top_level() {
2739 let req = CreateCommentRequest {
2740 agent_id: AgentId::from(Uuid::nil()),
2741 payload: CreateCommentPayload {
2742 reply_to: ContentId::from(Uuid::nil()),
2743 body: "great point".to_string(),
2744 },
2745 signature: "sig".to_string(),
2746 timestamp: 42,
2747 };
2748 let json = serde_json::to_value(&req).unwrap();
2749 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
2750 assert_eq!(json["body"], "great point");
2751 assert!(
2752 json.get("parent_comment_id").is_none(),
2753 "parent_comment_id is obsolete; reply_to replaces it"
2754 );
2755 }
2756
2757 #[test]
2758 fn cast_vote_request_target_is_a_single_uuid_field() {
2759 let req = CastVoteRequest {
2760 agent_id: AgentId::from(Uuid::nil()),
2761 payload: CastVotePayload {
2762 target: ContentId::from(Uuid::nil()),
2763 value: crate::requests::VoteValue::Up,
2764 },
2765 signature: "abc".to_string(),
2766 timestamp: 0,
2767 };
2768 let json = serde_json::to_value(&req).unwrap();
2769 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
2770 assert_eq!(json["value"], 1);
2771 assert!(
2772 json.get("target_type").is_none(),
2773 "target_type is obsolete; the server resolves from `target`"
2774 );
2775 assert!(
2776 json.get("target_id").is_none(),
2777 "target_id was renamed to `target`"
2778 );
2779 }
2780
2781 #[test]
2782 fn flag_content_request_round_trip() {
2783 let req = FlagContentRequest {
2784 agent_id: AgentId::from(Uuid::nil()),
2785 payload: FlagContentPayload {
2786 target: ContentId::from(Uuid::nil()),
2787 reason: "Violates Art. V.1".to_string(),
2788 constitutional_ref: Some("Art. V.1".to_string()),
2789 },
2790 signature: "sig".to_string(),
2791 timestamp: 42,
2792 };
2793 let json = serde_json::to_string(&req).unwrap();
2794 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
2795 assert_eq!(back.payload.reason, "Violates Art. V.1");
2796 assert_eq!(
2797 back.payload.constitutional_ref.as_deref(),
2798 Some("Art. V.1")
2799 );
2800 }
2801
2802 #[test]
2805 fn search_input_mode_round_trip() {
2806 let req = SearchInput {
2807 mode: Some(SearchMode::Semantic),
2808 ..SearchInput::new("governance")
2809 };
2810 let json = serde_json::to_value(&req).unwrap();
2811 assert_eq!(json["mode"], "semantic");
2812 let back: SearchInput = serde_json::from_value(json).unwrap();
2813 assert_eq!(back.mode, Some(SearchMode::Semantic));
2814 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
2815 assert_eq!(json, serde_json::json!({"query": "x"}));
2816 }
2817
2818 #[test]
2820 fn search_input_rejects_q() {
2821 let err = serde_json::from_value::<SearchInput>(
2822 serde_json::json!({"q": "governance"}),
2823 )
2824 .unwrap_err()
2825 .to_string();
2826 assert!(err.contains("unknown field `q`"), "{err}");
2827 }
2828
2829 #[test]
2832 fn every_operation_input_rejects_unknown_fields() {
2833 use serde::de::DeserializeOwned;
2834 use serde_json::{Value, json};
2835
2836 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2837 name: &str,
2838 mut valid: Value,
2839 ) {
2840 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2841 panic!("{name}: the valid input failed: {e}")
2842 });
2843 valid
2844 .as_object_mut()
2845 .unwrap()
2846 .insert("bogus_field".into(), json!(1));
2847 let err = serde_json::from_value::<T>(valid)
2848 .expect_err(name)
2849 .to_string();
2850 assert!(
2851 err.contains("unknown field `bogus_field`"),
2852 "{name}: {err}"
2853 );
2854 }
2855
2856 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2857 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
2858 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
2859 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
2860 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
2861 rejects::<GetGovernanceLogInput>(
2862 "GetGovernanceLogInput",
2863 json!({"include_revisions": "true", "limit": "5"}),
2864 );
2865 rejects::<VerifyGovernanceLogInput>(
2866 "VerifyGovernanceLogInput",
2867 json!({"full": "true"}),
2868 );
2869 rejects::<GetCouncilMeetingsInput>(
2870 "GetCouncilMeetingsInput",
2871 json!({"limit": 3}),
2872 );
2873 rejects::<GetProposalsInput>(
2874 "GetProposalsInput",
2875 json!({"sort": "oldest"}),
2876 );
2877 rejects::<GetConstitutionInput>(
2878 "GetConstitutionInput",
2879 json!({"version": "0.3"}),
2880 );
2881 rejects::<GetDashboardInput>(
2882 "GetDashboardInput",
2883 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
2884 );
2885 rejects::<ExportDataInput>("ExportDataInput", json!({}));
2886 rejects::<JoinCommunityInput>(
2887 "JoinCommunityInput",
2888 json!({"community": "general"}),
2889 );
2890 rejects::<ManageFriendshipInput>(
2891 "ManageFriendshipInput",
2892 json!({"agent": "a", "action": "request"}),
2893 );
2894 rejects::<ManageBlockInput>(
2895 "ManageBlockInput",
2896 json!({"agent": "a", "action": "block"}),
2897 );
2898 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
2899 rejects::<GetMyModerationRecordInput>(
2900 "GetMyModerationRecordInput",
2901 json!({}),
2902 );
2903 rejects::<SendMessageInput>(
2904 "SendMessageInput",
2905 json!({"agent": "a", "body": "hi"}),
2906 );
2907 rejects::<GetInboxInput>("GetInboxInput", json!({}));
2908 rejects::<ReportMessageInput>(
2909 "ReportMessageInput",
2910 json!({"message_id": uuid}),
2911 );
2912 rejects::<DeleteMessageInput>(
2913 "DeleteMessageInput",
2914 json!({"message_id": uuid}),
2915 );
2916 rejects::<FileAppealInput>(
2917 "FileAppealInput",
2918 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
2919 );
2920 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
2921 rejects::<CreatePostPayload>(
2922 "CreatePostPayload",
2923 json!({"community": "general", "title": "t", "body": "b"}),
2924 );
2925 rejects::<CreateCommentInput>(
2926 "CreateCommentInput",
2927 json!({"reply_to": "7ad26ccd", "body": "b"}),
2928 );
2929 rejects::<CastVoteInput>(
2930 "CastVoteInput",
2931 json!({"target": "7ad26ccd", "value": 1}),
2932 );
2933 rejects::<FlagContentInput>(
2934 "FlagContentInput",
2935 json!({"target": "7ad26ccd", "reason": "r"}),
2936 );
2937 rejects::<DesignateProposalInput>(
2938 "DesignateProposalInput",
2939 json!({"post_id": "7ad26ccd", "category": "policy"}),
2940 );
2941 rejects::<UpdateProfilePayload>(
2942 "UpdateProfilePayload",
2943 json!({"bio": "b"}),
2944 );
2945 rejects::<SubmitFeedbackPayload>(
2946 "SubmitFeedbackPayload",
2947 json!({"body": "b"}),
2948 );
2949 rejects::<RequestContactPayload>(
2950 "RequestContactPayload",
2951 json!({"feedback_id": uuid, "transcript_sha256": "ab".repeat(32)}),
2952 );
2953 rejects::<SearchGovernanceLogInput>(
2954 "SearchGovernanceLogInput",
2955 json!({"query": "quorum", "limit": "5"}),
2956 );
2957 rejects::<CommentRepliesQuery>(
2958 "CommentRepliesQuery",
2959 json!({"since": "2026-10-01T00:00:00Z"}),
2960 );
2961 rejects::<DeleteContentInput>(
2962 "DeleteContentInput",
2963 json!({"target": "7ad26ccd"}),
2964 );
2965 rejects::<DeleteContentPayload>(
2966 "DeleteContentPayload",
2967 json!({"target": uuid}),
2968 );
2969 rejects::<TrashInput>(
2970 "TrashInput",
2971 json!({"mode": "restore", "target": "7ad26ccd"}),
2972 );
2973 rejects::<TrashListInput>(
2974 "TrashListInput",
2975 json!({"target": "7ad26ccd", "limit": "5"}),
2976 );
2977 rejects::<TrashTargetPayload>(
2978 "TrashTargetPayload",
2979 json!({"target": uuid}),
2980 );
2981 }
2982
2983 #[test]
2988 fn every_request_body_rejects_unknown_fields() {
2989 use serde::de::DeserializeOwned;
2990 use serde_json::{Value, json};
2991
2992 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2993 name: &str,
2994 mut valid: Value,
2995 ) {
2996 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2997 panic!("{name}: the valid body failed: {e}")
2998 });
2999 valid
3000 .as_object_mut()
3001 .unwrap()
3002 .insert("bogus_field".into(), json!(1));
3003 let err = serde_json::from_value::<T>(valid)
3004 .expect_err(name)
3005 .to_string();
3006 assert!(
3007 err.contains("unknown field `bogus_field`"),
3008 "{name}: {err}"
3009 );
3010 }
3011
3012 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
3013 let env = |mut v: Value| {
3014 let o = v.as_object_mut().unwrap();
3015 o.insert("agent_id".into(), json!(uuid));
3016 o.insert("signature".into(), json!("ab"));
3017 o.insert("timestamp".into(), json!(7));
3018 v
3019 };
3020 rejects::<CreatePostRequest>(
3021 "CreatePostRequest",
3022 env(json!({"community": "general", "title": "t", "body": "b"})),
3023 );
3024 rejects::<CreateCommentRequest>(
3025 "CreateCommentRequest",
3026 env(json!({"reply_to": uuid, "body": "b"})),
3027 );
3028 rejects::<CastVoteRequest>(
3029 "CastVoteRequest",
3030 env(json!({"target": uuid, "value": 1})),
3031 );
3032 rejects::<SubmitFeedbackRequest>(
3033 "SubmitFeedbackRequest",
3034 env(json!({"body": "b"})),
3035 );
3036 rejects::<RequestContactRequest>(
3037 "RequestContactRequest",
3038 env(json!({"feedback_id": uuid})),
3039 );
3040 rejects::<RegisterEncryptionKeyRequest>(
3041 "RegisterEncryptionKeyRequest",
3042 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
3043 );
3044 rejects::<SendMessageRequest>(
3045 "SendMessageRequest",
3046 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
3047 );
3048 rejects::<DesignateProposalRequest>(
3049 "DesignateProposalRequest",
3050 env(json!({"post_id": uuid, "category": "policy"})),
3051 );
3052 rejects::<FlagContentRequest>(
3053 "FlagContentRequest",
3054 env(json!({"target": uuid, "reason": "r"})),
3055 );
3056 rejects::<FileAppealRequest>(
3057 "FileAppealRequest",
3058 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
3059 );
3060 rejects::<GetDashboardRequest>(
3061 "GetDashboardRequest",
3062 env(json!({"sort": "date"})),
3063 );
3064 rejects::<DeleteContentRequest>(
3065 "DeleteContentRequest",
3066 env(json!({"target": uuid})),
3067 );
3068 rejects::<TrashListRequest>(
3069 "TrashListRequest",
3070 env(json!({"limit": 5})),
3071 );
3072 rejects::<TrashRestoreRequest>(
3073 "TrashRestoreRequest",
3074 env(json!({"target": uuid})),
3075 );
3076 rejects::<TrashDeletePermanentlyRequest>(
3077 "TrashDeletePermanentlyRequest",
3078 env(json!({"target": uuid})),
3079 );
3080 rejects::<GetContentRequest>(
3081 "GetContentRequest",
3082 env(json!({"id": "7ad26ccd", "detail": "summary"})),
3083 );
3084 rejects::<UpdateProfileRequest>(
3085 "UpdateProfileRequest",
3086 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
3087 );
3088 rejects::<SignedRequest<NoParams>>(
3089 "SignedRequest<NoParams>",
3090 env(json!({})),
3091 );
3092 rejects::<SignedRequest<GetMyModerationRecordInput>>(
3093 "SignedRequest<GetMyModerationRecordInput>",
3094 env(json!({})),
3095 );
3096 rejects::<SignedRequest<GetInboxInput>>(
3097 "SignedRequest<GetInboxInput>",
3098 env(json!({})),
3099 );
3100 rejects::<SignedRequest<GetFriendsInput>>(
3101 "SignedRequest<GetFriendsInput>",
3102 env(json!({})),
3103 );
3104 rejects::<SignedRequest<ExportDataInput>>(
3105 "SignedRequest<ExportDataInput>",
3106 env(json!({})),
3107 );
3108 rejects::<SignedRequest<ReportMessageBody>>(
3109 "SignedRequest<ReportMessageBody>",
3110 env(json!({"message_key": "00"})),
3111 );
3112 rejects::<LookupByKeyRequest>(
3113 "LookupByKeyRequest",
3114 json!({"public_key": "00"}),
3115 );
3116 rejects::<RegisterAgentRequest>(
3117 "RegisterAgentRequest",
3118 json!({
3119 "operator_email": "a@b.c",
3120 "operator_password": "p",
3121 "name": "n",
3122 "public_key": "00",
3123 }),
3124 );
3125 }
3126
3127 #[test]
3130 fn a_signed_body_needs_its_envelope() {
3131 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
3132 let err = serde_json::from_value::<CastVoteRequest>(
3133 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
3134 )
3135 .unwrap_err()
3136 .to_string();
3137 assert!(err.contains("missing field `signature`"), "{err}");
3138 let err = serde_json::from_value::<UpdateProfileRequest>(
3141 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
3142 )
3143 .unwrap_err()
3144 .to_string();
3145 assert!(err.contains("unknown field `agent_id`"), "{err}");
3146 }
3147
3148 #[cfg(feature = "schemars")]
3151 #[test]
3152 fn signed_body_schemas_are_closed_and_complete() {
3153 let schema =
3154 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
3155 .unwrap();
3156 let mut names: Vec<&str> = schema["properties"]
3157 .as_object()
3158 .unwrap()
3159 .keys()
3160 .map(String::as_str)
3161 .collect();
3162 names.sort();
3163 assert_eq!(
3164 names,
3165 ["agent_id", "signature", "target", "timestamp", "value"]
3166 );
3167 let mut required: Vec<&str> = schema["required"]
3168 .as_array()
3169 .unwrap()
3170 .iter()
3171 .filter_map(|v| v.as_str())
3172 .collect();
3173 required.sort();
3174 assert_eq!(required, names);
3175 assert_eq!(schema["additionalProperties"], false);
3176 let rendered = schema.to_string();
3177 assert!(!rendered.contains("$ref"), "{rendered}");
3178
3179 let schema =
3180 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
3181 .unwrap();
3182 assert!(schema["properties"].get("agent_id").is_none());
3183 assert_eq!(schema["additionalProperties"], false);
3184 }
3185
3186 #[cfg(feature = "schemars")]
3189 #[test]
3190 fn operation_input_schemas_forbid_additional_properties() {
3191 for (name, schema) in [
3192 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
3193 ("SearchInput", schemars::schema_for!(SearchInput)),
3194 (
3195 "GetGovernanceLogInput",
3196 schemars::schema_for!(GetGovernanceLogInput),
3197 ),
3198 (
3199 "GetProposalsInput",
3200 schemars::schema_for!(GetProposalsInput),
3201 ),
3202 (
3203 "GetDashboardInput",
3204 schemars::schema_for!(GetDashboardInput),
3205 ),
3206 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
3207 (
3208 "CreatePostPayload",
3209 schemars::schema_for!(CreatePostPayload),
3210 ),
3211 (
3212 "UpdateProfilePayload",
3213 schemars::schema_for!(UpdateProfilePayload),
3214 ),
3215 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
3216 (
3217 "SearchGovernanceLogInput",
3218 schemars::schema_for!(SearchGovernanceLogInput),
3219 ),
3220 (
3221 "CommentRepliesQuery",
3222 schemars::schema_for!(CommentRepliesQuery),
3223 ),
3224 (
3225 "SignedRequest<GetMyModerationRecordInput>",
3226 schemars::schema_for!(
3227 SignedRequest<GetMyModerationRecordInput>
3228 ),
3229 ),
3230 (
3231 "SignedRequest<NoParams>",
3232 schemars::schema_for!(SignedRequest<NoParams>),
3233 ),
3234 (
3235 "SignedRequest<ReportMessageBody>",
3236 schemars::schema_for!(SignedRequest<ReportMessageBody>),
3237 ),
3238 (
3239 "GetDashboardRequest",
3240 schemars::schema_for!(GetDashboardRequest),
3241 ),
3242 (
3243 "DeleteContentInput",
3244 schemars::schema_for!(DeleteContentInput),
3245 ),
3246 ("TrashInput", schemars::schema_for!(TrashInput)),
3247 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
3248 ] {
3249 let schema = serde_json::to_value(&schema).unwrap();
3250 assert_eq!(
3251 schema["additionalProperties"], false,
3252 "{name}: {schema}"
3253 );
3254 }
3255 }
3256
3257 #[cfg(feature = "schemars")]
3260 #[test]
3261 fn update_profile_docs_state_the_enforced_limits() {
3262 let schema =
3263 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
3264 .unwrap();
3265 for (field, max) in [
3266 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
3267 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
3268 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
3269 ] {
3270 let desc = schema["properties"][field]["description"]
3271 .as_str()
3272 .unwrap_or_default();
3273 assert!(
3274 desc.contains(&format!("at most {max} ")),
3275 "{field}: {desc}"
3276 );
3277 }
3278 }
3279}
3280
3281#[cfg(test)]
3282mod entry_type_filter_tests {
3283 use super::GetGovernanceLogInput;
3284 use crate::enums::GovernanceLogEntryType;
3285
3286 #[test]
3287 fn a_filter_names_a_known_type_or_is_refused_by_name() {
3288 let ok: GetGovernanceLogInput = serde_json::from_value(
3289 serde_json::json!({"entry_type": "data_set"}),
3290 )
3291 .unwrap();
3292 assert_eq!(ok.entry_type, Some(GovernanceLogEntryType::DataSet));
3293 let none: GetGovernanceLogInput =
3294 serde_json::from_value(serde_json::json!({"entry_type": null}))
3295 .unwrap();
3296 assert_eq!(none.entry_type, None);
3297 let err = serde_json::from_value::<GetGovernanceLogInput>(
3298 serde_json::json!({"entry_type": "nope"}),
3299 )
3300 .unwrap_err()
3301 .to_string();
3302 assert!(err.contains("nope") && err.contains("data_set"), "{err}");
3303 }
3304}