feat(types): add optional schemars support

This commit is contained in:
oxalica 2024-10-17 06:54:49 -04:00
parent ee85112fb6
commit ea69062a6d
7 changed files with 134 additions and 2 deletions

43
Cargo.lock generated
View file

@ -273,6 +273,7 @@ dependencies = [
"mock_instant",
"rand",
"rusqlite",
"schemars",
"serde",
"serde_jcs",
"serde_json",
@ -697,6 +698,12 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d"
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "ed25519"
version = "2.2.3"
@ -1931,6 +1938,31 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"schemars_derive",
"serde",
"serde_json",
"url",
]
[[package]]
name = "schemars_derive"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -2012,6 +2044,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_jcs"
version = "0.1.0"

View file

@ -6,6 +6,7 @@ edition = "2021"
[features]
default = []
unsafe_use_mock_instant_for_testing = ["dep:mock_instant"]
schemars = ["dep:schemars"]
[[bench]]
name = "crypto_ops"
@ -27,6 +28,11 @@ serde_with = "3"
thiserror = "1"
url = { version = "2", features = ["serde"] }
[dependencies.schemars]
version = "0.8"
optional = true
features = ["url"]
[dev-dependencies]
criterion = "0.5"
ed25519-dalek = { version = "2", features = ["rand_core"] }

View file

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
/// User pubkey pair to uniquely identity a user.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UserKey {
/// The identity key (`id_key`).
pub id_key: PubKey,
@ -24,6 +25,8 @@ pub struct UserKey {
#[serde(transparent)]
pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);
impl_json_schema_as!(PubKey => String);
impl FromStr for PubKey {
type Err = hex::FromHexError;
@ -59,14 +62,21 @@ impl From<&VerifyingKey> for PubKey {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Signed<T> {
#[serde(with = "hex::serde")]
// Workaround: https://github.com/GREsau/schemars/issues/89
#[serde(
serialize_with = "hex::serde::serialize",
deserialize_with = "hex::serde::deserialize"
)]
#[cfg_attr(feature = "schemars", schemars(with = "String"))]
pub sig: [u8; SIGNATURE_LENGTH],
pub signee: Signee<T>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Signee<T> {
pub nonce: u32,

View file

@ -12,6 +12,7 @@ use crate::{PubKey, Signed};
/// User identity description structure.
// TODO: Revise and shrink duplicates (pubkey fields).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UserIdentityDesc {
/// User primary identity key, only for signing action keys.
pub id_key: PubKey,
@ -93,6 +94,7 @@ impl UserIdentityDesc {
/// Description of an action key.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "user_act_key")]
pub struct UserActKeyDesc {
/// Per-device action key for signing msgs.
@ -105,6 +107,7 @@ pub struct UserActKeyDesc {
/// User profile describing their non-cryptographic metadata.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "user_profile")]
pub struct UserProfile {
/// Preferred chat servers ordered by decreasing preference, for starting private chats.
@ -122,6 +125,8 @@ pub struct UserProfile {
#[serde(try_from = "Url")]
pub struct IdUrl(Url);
impl_json_schema_as!(IdUrl => Url);
impl fmt::Display for IdUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)

View file

@ -6,6 +6,31 @@ pub use url;
pub use crypto::{get_timestamp, PubKey, SignExt, Signed, Signee, UserKey};
pub use msg::Id;
#[cfg(not(feature = "schemars"))]
macro_rules! impl_json_schema_as {
($($tt:tt)*) => {};
}
// Workaround: https://github.com/GREsau/schemars/issues/267
#[cfg(feature = "schemars")]
macro_rules! impl_json_schema_as {
($ty:ident => $as_ty:ty) => {
impl schemars::JsonSchema for $ty {
fn schema_name() -> String {
stringify!($ty).into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::", stringify!($ty)).into()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
gen.subschema_for::<$as_ty>()
}
}
};
}
pub mod crypto;
pub mod identity;
pub mod msg;

View file

@ -18,6 +18,8 @@ use crate::{PubKey, Signed};
#[serde(transparent)]
pub struct Id(#[serde_as(as = "DisplayFromStr")] pub i64);
impl_json_schema_as!(Id => String);
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
@ -39,6 +41,7 @@ impl Id {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WithMsgId<T> {
pub cid: Id,
#[serde(flatten)]
@ -53,6 +56,7 @@ impl<T> WithMsgId<T> {
/// Register a user on a chat server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "user_register")]
pub struct UserRegisterPayload {
/// The normalized server URL to register on.
@ -70,6 +74,7 @@ pub struct UserRegisterPayload {
/// The server-specific challenge data for registration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum UserRegisterChallengeResponse {
/// Proof of work challenge containing the same nonce from server challenge request.
@ -82,6 +87,7 @@ pub enum UserRegisterChallengeResponse {
// FIXME: `deny_unknown_fields` breaks this.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "chat")]
pub struct ChatPayload {
pub rich_text: RichText,
@ -93,6 +99,8 @@ pub struct ChatPayload {
#[serde(transparent)]
pub struct RichText(pub Vec<RichTextPiece>);
impl_json_schema_as!(RichText => Vec<RichTextPieceRaw>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RichTextPiece {
pub attrs: TextAttrs,
@ -112,8 +120,9 @@ impl Serialize for RichTextPiece {
}
}
/// The protocol representation of `RichTextPiece`.
/// The representation on wire of `RichTextPiece`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
enum RichTextPieceRaw {
Text(String),
@ -157,6 +166,7 @@ impl<'de> Deserialize<'de> for RichText {
// TODO: This protocol format is quite large. Could use bitflags for database.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TextAttrs {
#[serde(default, rename = "b", skip_serializing_if = "is_default")]
pub bold: bool,
@ -280,6 +290,7 @@ pub type SignedChatMsg = Signed<ChatPayload>;
pub type SignedChatMsgWithId = WithMsgId<SignedChatMsg>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ")]
pub enum CreateRoomPayload {
#[serde(rename = "create_room")]
@ -290,6 +301,7 @@ pub enum CreateRoomPayload {
/// Multi-user room.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateGroup {
pub attrs: RoomAttrs,
pub title: String,
@ -297,11 +309,13 @@ pub struct CreateGroup {
/// Peer-to-peer chat room with exactly two symmetric users.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreatePeerChat {
pub peer: PubKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "delete_room")]
pub struct DeleteRoomPayload {
pub room: Id,
@ -311,6 +325,7 @@ pub struct DeleteRoomPayload {
/// 1. Sorted by userkeys.
/// 2. No duplicated users.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(try_from = "Vec<RoomMember>")]
pub struct RoomMemberList(pub Vec<RoomMember>);
@ -336,6 +351,7 @@ impl TryFrom<Vec<RoomMember>> for RoomMemberList {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomMember {
pub permission: MemberPermission,
pub user: PubKey,
@ -345,11 +361,13 @@ pub struct RoomMember {
///
/// TODO: Should we use JWT here instead?
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename = "auth")]
pub struct AuthPayload {}
// FIXME: Remove this.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
// `typ` is provided by `RoomAdminOp`.
pub struct RoomAdminPayload {
#[serde(flatten)]
@ -357,6 +375,7 @@ pub struct RoomAdminPayload {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename_all = "snake_case", rename = "remove_member")]
pub struct RemoveMemberPayload {
pub room: Id,
@ -366,6 +385,7 @@ pub struct RemoveMemberPayload {
// TODO: Maybe disallow adding other user without consent?
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename_all = "snake_case", rename = "add_member")]
pub struct AddMemberPayload {
pub room: Id,
@ -374,6 +394,7 @@ pub struct AddMemberPayload {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "typ", rename_all = "snake_case", rename = "update_member")]
pub struct UpdateMemberPayload {
pub room: Id,
@ -382,6 +403,7 @@ pub struct UpdateMemberPayload {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum RoomAdminOp {
AddMember(AddMemberPayload),
@ -437,6 +459,10 @@ impl_serde_for_bitflags!(ServerPermission);
impl_serde_for_bitflags!(MemberPermission);
impl_serde_for_bitflags!(RoomAttrs);
impl_json_schema_as!(ServerPermission => i32);
impl_json_schema_as!(MemberPermission => i32);
impl_json_schema_as!(RoomAttrs => i32);
#[cfg(feature = "rusqlite")]
mod sql_impl {
use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH};

View file

@ -10,6 +10,7 @@ use crate::PubKey;
/// The response object returned as body on HTTP error status.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ErrorResponse<S = String> {
/// The error object.
pub error: ErrorObject<S>,
@ -18,6 +19,7 @@ pub struct ErrorResponse<S = String> {
/// The response object of `/_blah/user/me` endpoint on HTTP error status.
/// It contains additional registration information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ErrorResponseWithChallenge<S = String> {
/// The error object.
pub error: ErrorObject<S>,
@ -29,10 +31,14 @@ pub struct ErrorResponseWithChallenge<S = String> {
/// The error object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ErrorObject<S = String> {
/// A machine-readable error code string.
#[cfg_attr(feature = "schemars", schemars(with = "String"))]
pub code: S,
/// A human-readable error message.
#[cfg_attr(feature = "schemars", schemars(with = "String"))]
pub message: S,
}
@ -50,6 +56,7 @@ impl<S: fmt::Display + fmt::Debug> std::error::Error for ErrorObject<S> {}
/// It may contains extra fields and clients should ignore them for future compatibility.
/// Chat Servers can also include any custom fields here as long they have a `_` prefix.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ServerMetadata {
/// A server-defined version string indicating its implementation name and the version.
///
@ -60,6 +67,7 @@ pub struct ServerMetadata {
///
/// It is expected to be a public accessible maybe-compressed tarball link without
/// access control.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub src_url: Option<Url>,
/// The server capabilities set.
@ -67,6 +75,7 @@ pub struct ServerMetadata {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ServerCapabilities {
/// Whether registration is open to public.
pub allow_public_register: bool,
@ -74,6 +83,7 @@ pub struct ServerCapabilities {
/// Registration challenge information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum UserRegisterChallenge {
/// Proof-of-work (PoW) challenge.
@ -86,6 +96,7 @@ pub enum UserRegisterChallenge {
/// Response to list rooms.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomList {
/// Result list of rooms.
pub rooms: Vec<RoomMetadata>,
@ -96,6 +107,7 @@ pub struct RoomList {
/// The metadata of a room.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomMetadata {
/// Room id.
pub rid: Id,
@ -127,6 +139,7 @@ pub struct RoomMetadata {
/// Response to list room msgs.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomMsgs {
/// Result list of msgs ordered in reverse of server-received time.
pub msgs: Vec<SignedChatMsgWithId>,
@ -137,6 +150,7 @@ pub struct RoomMsgs {
/// Response to list room members.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomMemberList {
/// Result list of members.
pub members: Vec<RoomMember>,
@ -147,6 +161,7 @@ pub struct RoomMemberList {
/// The description of a room member.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoomMember {
/// The identity key of the member user.
pub id_key: PubKey,
@ -159,6 +174,7 @@ pub struct RoomMember {
/// A server-to-client event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ServerEvent {
/// A message from a joined room.
@ -170,5 +186,6 @@ pub enum ServerEvent {
/// A client-to-server event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ClientEvent {}