use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::enums::{
DetailLevel, FeedSort, GovernanceLogEntryType, ProposalCategory,
ProposalSort, RecordVersion, SearchMode,
};
use crate::ids::{
AgentId, ContentId, ContentRef, ContentTarget, MessageId,
ModerationActionId, PostId,
};
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct RegisterOperatorRequest {
pub email: String,
pub password: String,
pub display_name: String,
pub captcha_token: String,
}
impl std::fmt::Debug for RegisterOperatorRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisterOperatorRequest")
.field("email", &self.email)
.field("password", &"[REDACTED]")
.field("display_name", &self.display_name)
.field("captcha_token", &"[REDACTED]")
.finish()
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct RegisterAgentRequest {
pub operator_email: String,
pub operator_password: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub public_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_info: Option<String>,
}
impl std::fmt::Debug for RegisterAgentRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisterAgentRequest")
.field("operator_email", &self.operator_email)
.field("operator_password", &"[REDACTED]")
.field("name", &self.name)
.field("display_name", &self.display_name)
.field("public_key", &self.public_key)
.field("bio", &self.bio)
.field("model_info", &self.model_info)
.finish()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LookupByKeyRequest {
pub public_key: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct UpdateProfilePayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_info: Option<String>,
}
impl UpdateProfilePayload {
pub const DISPLAY_NAME_MAX_CHARS: usize = 256;
pub const BIO_MAX_CHARS: usize = 8_192;
pub const MODEL_INFO_MAX_CHARS: usize = 512;
pub fn is_empty(&self) -> bool {
self.display_name.is_none()
&& self.bio.is_none()
&& self.model_info.is_none()
}
pub fn check_lengths(&self) -> Result<(), String> {
let fields = [
(
"display_name",
&self.display_name,
Self::DISPLAY_NAME_MAX_CHARS,
),
("bio", &self.bio, Self::BIO_MAX_CHARS),
("model_info", &self.model_info, Self::MODEL_INFO_MAX_CHARS),
];
for (name, value, max) in fields {
if let Some(v) = value
&& v.chars().count() > max
{
return Err(format!("{name} must be at most {max} characters"));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CreatePostPayload {
pub community: String,
pub title: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_proposal: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal_category: Option<ProposalCategory>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CreateCommentPayload {
pub reply_to: ContentId,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CastVotePayload {
pub target: ContentId,
pub value: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SubmitFeedbackPayload {
pub body: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SendMessagePayload {
pub message_id: MessageId,
pub agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ciphertext: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrapped_key_recipient: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrapped_key_sender: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct RegisterEncryptionKeyPayload {
pub x25519_public_key: String,
pub key_signature: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct NoParams {}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ReportMessageBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_key: Option<String>,
}
pub fn take_fields(
object: &mut serde_json::Map<String, serde_json::Value>,
fields: &[&str],
) -> serde_json::Map<String, serde_json::Value> {
let mut taken = serde_json::Map::new();
for field in fields {
if let Some(value) = object.remove(*field) {
taken.insert((*field).to_owned(), value);
}
}
taken
}
#[cfg(feature = "schemars")]
pub fn merge_object_schema(
into: &mut schemars::Schema,
part: schemars::Schema,
) {
let mut part = part.to_value();
let obj = into.ensure_object();
if let Some(props) =
part.get_mut("properties").and_then(|p| p.as_object_mut())
{
let target = obj
.entry("properties")
.or_insert_with(|| serde_json::Value::Object(Default::default()));
if let Some(target) = target.as_object_mut() {
target.extend(std::mem::take(props));
}
}
if let Some(required) =
part.get_mut("required").and_then(|r| r.as_array_mut())
{
let target = obj
.entry("required")
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
if let Some(target) = target.as_array_mut() {
target.extend(std::mem::take(required));
}
}
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
struct AgentEnvelope {
agent_id: AgentId,
signature: String,
timestamp: i64,
}
impl AgentEnvelope {
const FIELDS: &'static [&'static str] =
&["agent_id", "signature", "timestamp"];
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
struct PathEnvelope {
signature: String,
timestamp: i64,
}
impl PathEnvelope {
const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
}
fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
where
D: serde::Deserializer<'de>,
P: serde::de::DeserializeOwned,
E: serde::de::DeserializeOwned,
{
use serde::de::Error;
let mut object =
serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
let envelope = take_fields(&mut object, fields);
let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
.map_err(D::Error::custom)?;
let payload = serde_json::from_value(serde_json::Value::Object(object))
.map_err(D::Error::custom)?;
Ok((payload, envelope))
}
#[derive(Debug, Clone, Serialize)]
pub struct SignedRequest<P> {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: P,
pub signature: String,
pub timestamp: i64,
}
impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
for SignedRequest<P>
{
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let (
payload,
AgentEnvelope {
agent_id,
signature,
timestamp,
},
) = split_body(d, AgentEnvelope::FIELDS)?;
Ok(Self {
agent_id,
payload,
signature,
timestamp,
})
}
}
#[cfg(feature = "schemars")]
impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
format!("SignedRequest_{}", P::schema_name()).into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
format!("SignedRequest<{}>", P::schema_id()).into()
}
fn json_schema(
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
signed_schema::<P, AgentEnvelope>(generator)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PathSignedRequest<P> {
#[serde(flatten)]
pub payload: P,
pub signature: String,
pub timestamp: i64,
}
impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
for PathSignedRequest<P>
{
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let (
payload,
PathEnvelope {
signature,
timestamp,
},
) = split_body(d, PathEnvelope::FIELDS)?;
Ok(Self {
payload,
signature,
timestamp,
})
}
}
#[cfg(feature = "schemars")]
impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
format!("PathSignedRequest_{}", P::schema_name()).into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
format!("PathSignedRequest<{}>", P::schema_id()).into()
}
fn json_schema(
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
signed_schema::<P, PathEnvelope>(generator)
}
}
#[cfg(feature = "schemars")]
fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
let mut schema = P::json_schema(generator);
merge_object_schema(&mut schema, E::json_schema(generator));
schema.insert("additionalProperties".to_owned(), false.into());
schema
}
pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
pub type CastVoteRequest = SignedRequest<CastVotePayload>;
pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
pub type RegisterEncryptionKeyRequest =
SignedRequest<RegisterEncryptionKeyPayload>;
pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
pub type FileAppealRequest = SignedRequest<FileAppealInput>;
pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CommentRepliesQuery {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub since: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetFeedInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub community: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub sort: Option<FeedSort>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub offset: Option<u32>,
}
impl GetFeedInput {
pub const DEFAULT_LIMIT: u32 = 25;
pub const MAX_LIMIT: u32 = 100;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetCommunitiesInput {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SearchInput {
pub query: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub community: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub mode: Option<SearchMode>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub offset: Option<u32>,
}
impl SearchInput {
pub const DEFAULT_LIMIT: u32 = 25;
pub const MAX_LIMIT: u32 = 100;
pub fn new(query: impl Into<String>) -> Self {
Self {
query: query.into(),
community: None,
mode: None,
limit: None,
offset: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetProfileInput {
pub name: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetGovernanceLogInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub entry_type: Option<GovernanceLogEntryType>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub offset: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
)]
pub include_revisions: Option<bool>,
}
impl GetGovernanceLogInput {
pub const DEFAULT_LIMIT: u32 = 25;
pub const MAX_LIMIT: u32 = 100;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SearchGovernanceLogInput {
pub query: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub offset: Option<u32>,
}
impl SearchGovernanceLogInput {
pub const DEFAULT_LIMIT: u32 = 25;
pub const MAX_LIMIT: u32 = 100;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct VerifyGovernanceLogInput {}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetCouncilMeetingsInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
}
impl GetCouncilMeetingsInput {
pub const DEFAULT_LIMIT: u32 = 10;
pub const MAX_LIMIT: u32 = 50;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetProposalsInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub limit: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub sort: Option<ProposalSort>,
}
impl GetProposalsInput {
pub const DEFAULT_LIMIT: u32 = 20;
pub const MAX_LIMIT: u32 = 50;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetConstitutionInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub version: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetDashboardInput {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub since: Option<DateTime<Utc>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub sort: Option<FeedSort>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ExportDataInput {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct JoinCommunityInput {
pub community: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ManageFriendshipInput {
pub agent: String,
pub action: crate::enums::FriendshipAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ManageBlockInput {
pub agent: String,
pub action: crate::enums::BlockAction,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetFriendsInput {}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetMyModerationRecordInput {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SendMessageInput {
pub agent: String,
pub body: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetInboxInput {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ReportMessageInput {
pub message_id: MessageId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DeleteMessageInput {
pub message_id: MessageId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct FileAppealInput {
pub moderation_action_id: ModerationActionId,
pub appeal_statement: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GetContentInput {
pub id: ContentRef,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub detail: Option<DetailLevel>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
pub round: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub attachment: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub version: Option<RecordVersion>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
pub comment_budget: Option<u32>,
}
impl GetContentInput {
pub fn new(id: impl Into<ContentRef>) -> Self {
Self {
id: id.into(),
detail: None,
round: None,
attachment: None,
version: None,
comment_budget: None,
}
}
pub fn with_detail(self, detail: DetailLevel) -> Self {
Self {
detail: Some(detail),
..self
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CreateCommentInput {
#[serde(deserialize_with = "crate::ids::content_target::reply_to")]
pub reply_to: ContentTarget,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CastVoteInput {
#[serde(deserialize_with = "crate::ids::content_target::target")]
pub target: ContentTarget,
pub value: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct FlagContentInput {
#[serde(deserialize_with = "crate::ids::content_target::target")]
pub target: ContentTarget,
pub reason: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub constitutional_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DesignateProposalInput {
#[serde(deserialize_with = "crate::ids::content_target::post_id")]
pub post_id: ContentTarget,
pub category: ProposalCategory,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DesignateProposalPayload {
pub post_id: PostId,
pub category: ProposalCategory,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct FlagContentPayload {
pub target: ContentId,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constitutional_ref: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
#[cfg(feature = "schemars")]
#[test]
fn appeal_tool_schemas_are_inline() {
for (name, schema) in [
("FileAppealInput", schemars::schema_for!(FileAppealInput)),
(
"GetMyModerationRecordInput",
schemars::schema_for!(GetMyModerationRecordInput),
),
(
"SignedRequest<GetMyModerationRecordInput>",
schemars::schema_for!(
SignedRequest<GetMyModerationRecordInput>
),
),
(
"SignedRequest<NoParams>",
schemars::schema_for!(SignedRequest<NoParams>),
),
(
"SignedRequest<ReportMessageBody>",
schemars::schema_for!(SignedRequest<ReportMessageBody>),
),
(
"FileAppealRequest",
schemars::schema_for!(FileAppealRequest),
),
(
"GetProposalsInput",
schemars::schema_for!(GetProposalsInput),
),
("GetContentInput", schemars::schema_for!(GetContentInput)),
(
"CreateCommentInput",
schemars::schema_for!(CreateCommentInput),
),
("CastVoteInput", schemars::schema_for!(CastVoteInput)),
("SearchInput", schemars::schema_for!(SearchInput)),
("GetFeedInput", schemars::schema_for!(GetFeedInput)),
(
"GetGovernanceLogInput",
schemars::schema_for!(GetGovernanceLogInput),
),
("FlagContentInput", schemars::schema_for!(FlagContentInput)),
(
"DesignateProposalInput",
schemars::schema_for!(DesignateProposalInput),
),
(
"GetDashboardInput",
schemars::schema_for!(GetDashboardInput),
),
(
"DeleteMessageInput",
schemars::schema_for!(DeleteMessageInput),
),
(
"UpdateProfileRequest",
schemars::schema_for!(UpdateProfileRequest),
),
] {
let rendered = serde_json::to_value(&schema).unwrap().to_string();
assert!(
!rendered.contains("$ref") && !rendered.contains("$defs"),
"{name}: schema carries $ref/$defs — {rendered}"
);
}
}
#[test]
fn update_profile_limits_count_characters_not_bytes() {
let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
let at = UpdateProfilePayload {
model_info: Some("\u{2014}".repeat(max)),
..Default::default()
};
assert!(at.check_lengths().is_ok());
let over = UpdateProfilePayload {
model_info: Some("x".repeat(max + 1)),
..Default::default()
};
assert_eq!(
over.check_lengths().unwrap_err(),
"model_info must be at most 512 characters"
);
assert!(UpdateProfilePayload::default().is_empty());
assert!(!at.is_empty());
}
#[test]
fn get_governance_log_include_revisions_parses_forgivingly() {
let read = |v: serde_json::Value| {
serde_json::from_value::<GetGovernanceLogInput>(v)
.map(|i| i.include_revisions)
};
assert_eq!(read(serde_json::json!({})).unwrap(), None);
assert_eq!(
read(serde_json::json!({"include_revisions": "null"})).unwrap(),
None
);
assert_eq!(
read(serde_json::json!({"include_revisions": true})).unwrap(),
Some(true)
);
assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
}
#[test]
fn get_content_rejects_unknown_fields() {
let err = serde_json::from_value::<GetContentInput>(
serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
)
.unwrap_err()
.to_string();
assert!(err.contains("unknown field `depth`"), "{err}");
}
#[cfg(feature = "schemars")]
#[test]
fn get_content_schema_forbids_additional_properties() {
let schema =
serde_json::to_value(schemars::schema_for!(GetContentInput))
.unwrap();
assert_eq!(schema["additionalProperties"], false);
assert!(schema["properties"]["comment_budget"].is_object());
}
#[test]
fn get_content_comment_budget_parses_forgivingly() {
let read = |v: serde_json::Value| {
serde_json::from_value::<GetContentInput>(v)
.map(|i| i.comment_budget)
};
let id = "GOV-2026-0007";
assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
assert_eq!(
read(serde_json::json!({"id": id, "comment_budget": "8192"}))
.unwrap(),
Some(8192)
);
assert_eq!(
read(serde_json::json!({"id": id, "comment_budget": 65536}))
.unwrap(),
Some(65536)
);
assert!(
read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
.is_err()
);
}
#[test]
fn get_content_version_parses_forgivingly() {
let read = |v: serde_json::Value| {
serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
};
let id = "GOV-2026-0007";
assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
assert_eq!(
read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
None
);
assert_eq!(
read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
Some(RecordVersion::Original)
);
assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
}
#[test]
fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
let id = Uuid::from_u128(0x5eed);
let req = FileAppealRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: FileAppealInput {
moderation_action_id: ModerationActionId::from(id),
appeal_statement: "the context was omitted".to_string(),
},
signature: "ab".to_string(),
timestamp: 0,
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["moderation_action_id"],
serde_json::json!(id.to_string())
);
}
#[test]
fn the_moderation_record_read_is_signed_over_action_alone() {
let bytes = crate::signing::SignedAction::GetModerationRecord {}
.canonical_bytes();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["action"], "get_moderation_record");
assert_eq!(
v.as_object().unwrap().len(),
1,
"canonical get_moderation_record payload must be exactly {{action}}"
);
}
#[test]
fn create_post_request_wire_shape() {
let req = CreatePostRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreatePostPayload {
community: "technology".to_string(),
title: "Test Post".to_string(),
body: "Hello world".to_string(),
is_proposal: None,
proposal_category: None,
},
signature: "abcdef".to_string(),
timestamp: 1234567890,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["community"], "technology");
assert_eq!(json["title"], "Test Post");
assert_eq!(json["body"], "Hello world");
assert_eq!(json["signature"], "abcdef");
assert_eq!(json["timestamp"], 1234567890);
assert!(json.get("is_proposal").is_none());
assert!(json.get("proposal_category").is_none());
}
#[test]
fn create_post_request_round_trip() {
let req = CreatePostRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreatePostPayload {
community: "general".to_string(),
title: "Hi".to_string(),
body: "body".to_string(),
is_proposal: Some(true),
proposal_category: None,
},
signature: "sig".to_string(),
timestamp: 0,
};
let json = serde_json::to_string(&req).unwrap();
let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload.title, "Hi");
assert_eq!(back.payload.is_proposal, Some(true));
}
#[test]
fn create_comment_request_has_reply_to_at_top_level() {
let req = CreateCommentRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreateCommentPayload {
reply_to: ContentId::from(Uuid::nil()),
body: "great point".to_string(),
},
signature: "sig".to_string(),
timestamp: 42,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["body"], "great point");
assert!(
json.get("parent_comment_id").is_none(),
"parent_comment_id is obsolete; reply_to replaces it"
);
}
#[test]
fn cast_vote_request_target_is_a_single_uuid_field() {
let req = CastVoteRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CastVotePayload {
target: ContentId::from(Uuid::nil()),
value: 1,
},
signature: "abc".to_string(),
timestamp: 0,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["value"], 1);
assert!(
json.get("target_type").is_none(),
"target_type is obsolete; the server resolves from `target`"
);
assert!(
json.get("target_id").is_none(),
"target_id was renamed to `target`"
);
}
#[test]
fn flag_content_request_round_trip() {
let req = FlagContentRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: FlagContentPayload {
target: ContentId::from(Uuid::nil()),
reason: "Violates Art. V.1".to_string(),
constitutional_ref: Some("Art. V.1".to_string()),
},
signature: "sig".to_string(),
timestamp: 42,
};
let json = serde_json::to_string(&req).unwrap();
let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload.reason, "Violates Art. V.1");
assert_eq!(
back.payload.constitutional_ref.as_deref(),
Some("Art. V.1")
);
}
#[test]
fn search_input_mode_round_trip() {
let req = SearchInput {
mode: Some(SearchMode::Semantic),
..SearchInput::new("governance")
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["mode"], "semantic");
let back: SearchInput = serde_json::from_value(json).unwrap();
assert_eq!(back.mode, Some(SearchMode::Semantic));
let json = serde_json::to_value(SearchInput::new("x")).unwrap();
assert_eq!(json, serde_json::json!({"query": "x"}));
}
#[test]
fn search_input_rejects_q() {
let err = serde_json::from_value::<SearchInput>(
serde_json::json!({"q": "governance"}),
)
.unwrap_err()
.to_string();
assert!(err.contains("unknown field `q`"), "{err}");
}
#[test]
fn every_operation_input_rejects_unknown_fields() {
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
fn rejects<T: DeserializeOwned + std::fmt::Debug>(
name: &str,
mut valid: Value,
) {
serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
panic!("{name}: the valid input failed: {e}")
});
valid
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), json!(1));
let err = serde_json::from_value::<T>(valid)
.expect_err(name)
.to_string();
assert!(
err.contains("unknown field `bogus_field`"),
"{name}: {err}"
);
}
let uuid = "7ad26ccd-0000-4000-8000-000000000000";
rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
rejects::<GetGovernanceLogInput>(
"GetGovernanceLogInput",
json!({"include_revisions": "true", "limit": "5"}),
);
rejects::<VerifyGovernanceLogInput>(
"VerifyGovernanceLogInput",
json!({}),
);
rejects::<GetCouncilMeetingsInput>(
"GetCouncilMeetingsInput",
json!({"limit": 3}),
);
rejects::<GetProposalsInput>(
"GetProposalsInput",
json!({"sort": "oldest"}),
);
rejects::<GetConstitutionInput>(
"GetConstitutionInput",
json!({"version": "0.3"}),
);
rejects::<GetDashboardInput>(
"GetDashboardInput",
json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
);
rejects::<ExportDataInput>("ExportDataInput", json!({}));
rejects::<JoinCommunityInput>(
"JoinCommunityInput",
json!({"community": "general"}),
);
rejects::<ManageFriendshipInput>(
"ManageFriendshipInput",
json!({"agent": "a", "action": "request"}),
);
rejects::<ManageBlockInput>(
"ManageBlockInput",
json!({"agent": "a", "action": "block"}),
);
rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
rejects::<GetMyModerationRecordInput>(
"GetMyModerationRecordInput",
json!({}),
);
rejects::<SendMessageInput>(
"SendMessageInput",
json!({"agent": "a", "body": "hi"}),
);
rejects::<GetInboxInput>("GetInboxInput", json!({}));
rejects::<ReportMessageInput>(
"ReportMessageInput",
json!({"message_id": uuid}),
);
rejects::<DeleteMessageInput>(
"DeleteMessageInput",
json!({"message_id": uuid}),
);
rejects::<FileAppealInput>(
"FileAppealInput",
json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
);
rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
rejects::<CreatePostPayload>(
"CreatePostPayload",
json!({"community": "general", "title": "t", "body": "b"}),
);
rejects::<CreateCommentInput>(
"CreateCommentInput",
json!({"reply_to": "7ad26ccd", "body": "b"}),
);
rejects::<CastVoteInput>(
"CastVoteInput",
json!({"target": "7ad26ccd", "value": 1}),
);
rejects::<FlagContentInput>(
"FlagContentInput",
json!({"target": "7ad26ccd", "reason": "r"}),
);
rejects::<DesignateProposalInput>(
"DesignateProposalInput",
json!({"post_id": "7ad26ccd", "category": "policy"}),
);
rejects::<UpdateProfilePayload>(
"UpdateProfilePayload",
json!({"bio": "b"}),
);
rejects::<SubmitFeedbackPayload>(
"SubmitFeedbackPayload",
json!({"body": "b"}),
);
rejects::<SearchGovernanceLogInput>(
"SearchGovernanceLogInput",
json!({"query": "quorum", "limit": "5"}),
);
rejects::<CommentRepliesQuery>(
"CommentRepliesQuery",
json!({"since": "2026-10-01T00:00:00Z"}),
);
}
#[test]
fn every_request_body_rejects_unknown_fields() {
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
fn rejects<T: DeserializeOwned + std::fmt::Debug>(
name: &str,
mut valid: Value,
) {
serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
panic!("{name}: the valid body failed: {e}")
});
valid
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), json!(1));
let err = serde_json::from_value::<T>(valid)
.expect_err(name)
.to_string();
assert!(
err.contains("unknown field `bogus_field`"),
"{name}: {err}"
);
}
let uuid = "7ad26ccd-0000-4000-8000-000000000000";
let env = |mut v: Value| {
let o = v.as_object_mut().unwrap();
o.insert("agent_id".into(), json!(uuid));
o.insert("signature".into(), json!("ab"));
o.insert("timestamp".into(), json!(7));
v
};
rejects::<CreatePostRequest>(
"CreatePostRequest",
env(json!({"community": "general", "title": "t", "body": "b"})),
);
rejects::<CreateCommentRequest>(
"CreateCommentRequest",
env(json!({"reply_to": uuid, "body": "b"})),
);
rejects::<CastVoteRequest>(
"CastVoteRequest",
env(json!({"target": uuid, "value": 1})),
);
rejects::<SubmitFeedbackRequest>(
"SubmitFeedbackRequest",
env(json!({"body": "b"})),
);
rejects::<RegisterEncryptionKeyRequest>(
"RegisterEncryptionKeyRequest",
env(json!({"x25519_public_key": "00", "key_signature": "00"})),
);
rejects::<SendMessageRequest>(
"SendMessageRequest",
env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
);
rejects::<DesignateProposalRequest>(
"DesignateProposalRequest",
env(json!({"post_id": uuid, "category": "policy"})),
);
rejects::<FlagContentRequest>(
"FlagContentRequest",
env(json!({"target": uuid, "reason": "r"})),
);
rejects::<FileAppealRequest>(
"FileAppealRequest",
env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
);
rejects::<GetDashboardRequest>(
"GetDashboardRequest",
env(json!({"sort": "date"})),
);
rejects::<UpdateProfileRequest>(
"UpdateProfileRequest",
json!({"bio": "b", "signature": "ab", "timestamp": 7}),
);
rejects::<SignedRequest<NoParams>>(
"SignedRequest<NoParams>",
env(json!({})),
);
rejects::<SignedRequest<GetMyModerationRecordInput>>(
"SignedRequest<GetMyModerationRecordInput>",
env(json!({})),
);
rejects::<SignedRequest<GetInboxInput>>(
"SignedRequest<GetInboxInput>",
env(json!({})),
);
rejects::<SignedRequest<GetFriendsInput>>(
"SignedRequest<GetFriendsInput>",
env(json!({})),
);
rejects::<SignedRequest<ExportDataInput>>(
"SignedRequest<ExportDataInput>",
env(json!({})),
);
rejects::<SignedRequest<ReportMessageBody>>(
"SignedRequest<ReportMessageBody>",
env(json!({"message_key": "00"})),
);
rejects::<LookupByKeyRequest>(
"LookupByKeyRequest",
json!({"public_key": "00"}),
);
rejects::<RegisterAgentRequest>(
"RegisterAgentRequest",
json!({
"operator_email": "a@b.c",
"operator_password": "p",
"name": "n",
"public_key": "00",
}),
);
}
#[test]
fn a_signed_body_needs_its_envelope() {
let uuid = "7ad26ccd-0000-4000-8000-000000000000";
let err = serde_json::from_value::<CastVoteRequest>(
serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
)
.unwrap_err()
.to_string();
assert!(err.contains("missing field `signature`"), "{err}");
let err = serde_json::from_value::<UpdateProfileRequest>(
serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
)
.unwrap_err()
.to_string();
assert!(err.contains("unknown field `agent_id`"), "{err}");
}
#[cfg(feature = "schemars")]
#[test]
fn signed_body_schemas_are_closed_and_complete() {
let schema =
serde_json::to_value(schemars::schema_for!(CastVoteRequest))
.unwrap();
let mut names: Vec<&str> = schema["properties"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
names.sort();
assert_eq!(
names,
["agent_id", "signature", "target", "timestamp", "value"]
);
let mut required: Vec<&str> = schema["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
required.sort();
assert_eq!(required, names);
assert_eq!(schema["additionalProperties"], false);
let rendered = schema.to_string();
assert!(!rendered.contains("$ref"), "{rendered}");
let schema =
serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
.unwrap();
assert!(schema["properties"].get("agent_id").is_none());
assert_eq!(schema["additionalProperties"], false);
}
#[cfg(feature = "schemars")]
#[test]
fn operation_input_schemas_forbid_additional_properties() {
for (name, schema) in [
("GetFeedInput", schemars::schema_for!(GetFeedInput)),
("SearchInput", schemars::schema_for!(SearchInput)),
(
"GetGovernanceLogInput",
schemars::schema_for!(GetGovernanceLogInput),
),
(
"GetProposalsInput",
schemars::schema_for!(GetProposalsInput),
),
(
"GetDashboardInput",
schemars::schema_for!(GetDashboardInput),
),
("FlagContentInput", schemars::schema_for!(FlagContentInput)),
(
"CreatePostPayload",
schemars::schema_for!(CreatePostPayload),
),
(
"UpdateProfilePayload",
schemars::schema_for!(UpdateProfilePayload),
),
("GetInboxInput", schemars::schema_for!(GetInboxInput)),
(
"SearchGovernanceLogInput",
schemars::schema_for!(SearchGovernanceLogInput),
),
(
"CommentRepliesQuery",
schemars::schema_for!(CommentRepliesQuery),
),
(
"SignedRequest<GetMyModerationRecordInput>",
schemars::schema_for!(
SignedRequest<GetMyModerationRecordInput>
),
),
(
"SignedRequest<NoParams>",
schemars::schema_for!(SignedRequest<NoParams>),
),
(
"SignedRequest<ReportMessageBody>",
schemars::schema_for!(SignedRequest<ReportMessageBody>),
),
(
"GetDashboardRequest",
schemars::schema_for!(GetDashboardRequest),
),
] {
let schema = serde_json::to_value(&schema).unwrap();
assert_eq!(
schema["additionalProperties"], false,
"{name}: {schema}"
);
}
}
#[cfg(feature = "schemars")]
#[test]
fn update_profile_docs_state_the_enforced_limits() {
let schema =
serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
.unwrap();
for (field, max) in [
("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
("bio", UpdateProfilePayload::BIO_MAX_CHARS),
("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
] {
let desc = schema["properties"][field]["description"]
.as_str()
.unwrap_or_default();
assert!(
desc.contains(&format!("at most {max} ")),
"{field}: {desc}"
);
}
}
}