From ea69062a6d1d51379570952a2620cfe0ab25a652 Mon Sep 17 00:00:00 2001 From: oxalica Date: Thu, 17 Oct 2024 06:54:49 -0400 Subject: [PATCH] feat(types): add optional schemars support --- Cargo.lock | 43 ++++++++++++++++++++++++++++++++++++++ blah-types/Cargo.toml | 6 ++++++ blah-types/src/crypto.rs | 12 ++++++++++- blah-types/src/identity.rs | 5 +++++ blah-types/src/lib.rs | 25 ++++++++++++++++++++++ blah-types/src/msg.rs | 28 ++++++++++++++++++++++++- blah-types/src/server.rs | 17 +++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71e97a3..be04a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/blah-types/Cargo.toml b/blah-types/Cargo.toml index cff810a..146cb1d 100644 --- a/blah-types/Cargo.toml +++ b/blah-types/Cargo.toml @@ -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"] } diff --git a/blah-types/src/crypto.rs b/blah-types/src/crypto.rs index f96208c..c58dda7 100644 --- a/blah-types/src/crypto.rs +++ b/blah-types/src/crypto.rs @@ -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 { - #[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, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(deny_unknown_fields)] pub struct Signee { pub nonce: u32, diff --git a/blah-types/src/identity.rs b/blah-types/src/identity.rs index c59a4ff..f617a6d 100644 --- a/blah-types/src/identity.rs +++ b/blah-types/src/identity.rs @@ -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) diff --git a/blah-types/src/lib.rs b/blah-types/src/lib.rs index 699f95b..dc52f50 100644 --- a/blah-types/src/lib.rs +++ b/blah-types/src/lib.rs @@ -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; diff --git a/blah-types/src/msg.rs b/blah-types/src/msg.rs index 8c7017c..b811754 100644 --- a/blah-types/src/msg.rs +++ b/blah-types/src/msg.rs @@ -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 { pub cid: Id, #[serde(flatten)] @@ -53,6 +56,7 @@ impl WithMsgId { /// 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); +impl_json_schema_as!(RichText => Vec); + #[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; pub type SignedChatMsgWithId = WithMsgId; #[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")] pub struct RoomMemberList(pub Vec); @@ -336,6 +351,7 @@ impl TryFrom> 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}; diff --git a/blah-types/src/server.rs b/blah-types/src/server.rs index 6e5673a..f0d9325 100644 --- a/blah-types/src/server.rs +++ b/blah-types/src/server.rs @@ -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 { /// The error object. pub error: ErrorObject, @@ -18,6 +19,7 @@ pub struct ErrorResponse { /// 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 { /// The error object. pub error: ErrorObject, @@ -29,10 +31,14 @@ pub struct ErrorResponseWithChallenge { /// The error object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ErrorObject { /// 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 std::error::Error for ErrorObject {} /// 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, /// 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, @@ -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, @@ -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, @@ -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 {}