From e1d4b5ca1e4c3ab9e891086bd4e2200c549ff742 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 27 Jul 2024 22:17:44 +0000 Subject: [PATCH] Introduce `atrium_api::types::Unknown` for `unknown` fields Closes sugyan/atrium#204. --- atrium-api/CHANGELOG.md | 7 +++++ atrium-api/src/app/bsky/embed/record.rs | 2 +- .../get_recommended_did_credentials.rs | 4 +-- .../atproto/identity/sign_plc_operation.rs | 6 ++-- .../atproto/identity/submit_plc_operation.rs | 2 +- .../src/com/atproto/repo/apply_writes.rs | 4 +-- atrium-api/src/com/atproto/repo/get_record.rs | 2 +- .../src/com/atproto/repo/list_records.rs | 2 +- .../src/com/atproto/server/create_account.rs | 2 +- atrium-api/src/tools/ozone/moderation/defs.rs | 4 +-- atrium-api/src/types.rs | 26 +++++++++++++++++ lexicon/atrium-codegen/src/token_stream.rs | 29 ++++++++++++++----- 12 files changed, 68 insertions(+), 22 deletions(-) diff --git a/atrium-api/CHANGELOG.md b/atrium-api/CHANGELOG.md index 41083c3b..13df91e2 100644 --- a/atrium-api/CHANGELOG.md +++ b/atrium-api/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `atrium_api::types::Unknown` + +### Changed +- `unknown` field types that don't have a well-known format now have the type + `atrium_api::types::Unknown` instead of `atrium_api::records::Record`. + ## [0.23.2](https://github.com/sugyan/atrium/compare/atrium-api-v0.23.1...atrium-api-v0.23.2) - 2024-07-03 ### Added diff --git a/atrium-api/src/app/bsky/embed/record.rs b/atrium-api/src/app/bsky/embed/record.rs index e9585b8b..d06f15a5 100644 --- a/atrium-api/src/app/bsky/embed/record.rs +++ b/atrium-api/src/app/bsky/embed/record.rs @@ -46,7 +46,7 @@ pub struct ViewRecordData { pub repost_count: Option, pub uri: String, ///The record data itself. - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type ViewRecord = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/identity/get_recommended_did_credentials.rs b/atrium-api/src/com/atproto/identity/get_recommended_did_credentials.rs index dbbef5a2..5d623681 100644 --- a/atrium-api/src/com/atproto/identity/get_recommended_did_credentials.rs +++ b/atrium-api/src/com/atproto/identity/get_recommended_did_credentials.rs @@ -10,9 +10,9 @@ pub struct OutputData { #[serde(skip_serializing_if = "Option::is_none")] pub rotation_keys: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub services: Option, + pub services: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub verification_methods: Option, + pub verification_methods: Option, } pub type Output = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/identity/sign_plc_operation.rs b/atrium-api/src/com/atproto/identity/sign_plc_operation.rs index 15d7282b..5657c60b 100644 --- a/atrium-api/src/com/atproto/identity/sign_plc_operation.rs +++ b/atrium-api/src/com/atproto/identity/sign_plc_operation.rs @@ -9,19 +9,19 @@ pub struct InputData { #[serde(skip_serializing_if = "Option::is_none")] pub rotation_keys: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub services: Option, + pub services: Option, ///A token received through com.atproto.identity.requestPlcOperationSignature #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub verification_methods: Option, + pub verification_methods: Option, } pub type Input = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct OutputData { ///A signed DID PLC operation. - pub operation: crate::records::Record, + pub operation: crate::types::Unknown, } pub type Output = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/identity/submit_plc_operation.rs b/atrium-api/src/com/atproto/identity/submit_plc_operation.rs index 22ab75ba..e38b6937 100644 --- a/atrium-api/src/com/atproto/identity/submit_plc_operation.rs +++ b/atrium-api/src/com/atproto/identity/submit_plc_operation.rs @@ -4,7 +4,7 @@ pub const NSID: &str = "com.atproto.identity.submitPlcOperation"; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InputData { - pub operation: crate::records::Record, + pub operation: crate::types::Unknown, } pub type Input = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/repo/apply_writes.rs b/atrium-api/src/com/atproto/repo/apply_writes.rs index f1172f5f..811ecb36 100644 --- a/atrium-api/src/com/atproto/repo/apply_writes.rs +++ b/atrium-api/src/com/atproto/repo/apply_writes.rs @@ -41,7 +41,7 @@ pub struct CreateData { pub collection: crate::types::string::Nsid, #[serde(skip_serializing_if = "Option::is_none")] pub rkey: Option, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type Create = crate::types::Object; ///Operation which deletes an existing record. @@ -58,7 +58,7 @@ pub type Delete = crate::types::Object; pub struct UpdateData { pub collection: crate::types::string::Nsid, pub rkey: String, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type Update = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/repo/get_record.rs b/atrium-api/src/com/atproto/repo/get_record.rs index df7d5aad..5bc8e888 100644 --- a/atrium-api/src/com/atproto/repo/get_record.rs +++ b/atrium-api/src/com/atproto/repo/get_record.rs @@ -21,7 +21,7 @@ pub struct OutputData { #[serde(skip_serializing_if = "Option::is_none")] pub cid: Option, pub uri: String, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type Output = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/com/atproto/repo/list_records.rs b/atrium-api/src/com/atproto/repo/list_records.rs index d6fd21ae..ff1c28c1 100644 --- a/atrium-api/src/com/atproto/repo/list_records.rs +++ b/atrium-api/src/com/atproto/repo/list_records.rs @@ -45,6 +45,6 @@ impl std::fmt::Display for Error { pub struct RecordData { pub cid: crate::types::string::Cid, pub uri: String, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type Record = crate::types::Object; diff --git a/atrium-api/src/com/atproto/server/create_account.rs b/atrium-api/src/com/atproto/server/create_account.rs index 034928ec..4ab96b05 100644 --- a/atrium-api/src/com/atproto/server/create_account.rs +++ b/atrium-api/src/com/atproto/server/create_account.rs @@ -18,7 +18,7 @@ pub struct InputData { pub password: Option, ///A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. #[serde(skip_serializing_if = "Option::is_none")] - pub plc_op: Option, + pub plc_op: Option, ///DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. #[serde(skip_serializing_if = "Option::is_none")] pub recovery_key: Option, diff --git a/atrium-api/src/tools/ozone/moderation/defs.rs b/atrium-api/src/tools/ozone/moderation/defs.rs index 5989b6b7..b9239cd8 100644 --- a/atrium-api/src/tools/ozone/moderation/defs.rs +++ b/atrium-api/src/tools/ozone/moderation/defs.rs @@ -217,7 +217,7 @@ pub struct RecordViewData { pub moderation: Moderation, pub repo: RepoView, pub uri: String, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type RecordView = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] @@ -231,7 +231,7 @@ pub struct RecordViewDetailData { pub moderation: ModerationDetail, pub repo: RepoView, pub uri: String, - pub value: crate::records::Record, + pub value: crate::types::Unknown, } pub type RecordViewDetail = crate::types::Object; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/atrium-api/src/types.rs b/atrium-api/src/types.rs index 73d1d2b4..af47b973 100644 --- a/atrium-api/src/types.rs +++ b/atrium-api/src/types.rs @@ -128,6 +128,8 @@ pub enum Union { Unknown(UnknownData), } +/// Data with an unknown schema in an open [`Union`]. +/// /// The data of variants represented by a map and include a `$type` field indicating the variant type. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct UnknownData { @@ -137,6 +139,30 @@ pub struct UnknownData { pub data: Ipld, } +/// Arbitrary data with no specific validation and no type-specific fields. +/// +/// Corresponds to [the `unknown` field type]. +/// +/// [the `unknown` field type]: https://atproto.com/specs/lexicon#unknown +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(try_from = "Ipld")] +pub struct Unknown { + pub data: Ipld, +} + +impl TryFrom for Unknown { + type Error = &'static str; + + fn try_from(value: Ipld) -> Result { + // Enforce the ATProto data model. + // https://atproto.com/specs/data-model + match value { + Ipld::Float(_) => Err("Floats are not allowed in ATProto"), + data => Ok(Unknown { data }), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/lexicon/atrium-codegen/src/token_stream.rs b/lexicon/atrium-codegen/src/token_stream.rs index 12a1f547..16981a0d 100644 --- a/lexicon/atrium-codegen/src/token_stream.rs +++ b/lexicon/atrium-codegen/src/token_stream.rs @@ -320,7 +320,7 @@ fn lex_object_property( LexObjectProperty::Boolean(boolean) => boolean_type(boolean)?, LexObjectProperty::Integer(integer) => integer_type(integer)?, LexObjectProperty::String(string) => string_type(string)?, - LexObjectProperty::Unknown(unknown) => unknown_type(unknown, Some(name))?, + LexObjectProperty::Unknown(unknown) => unknown_type(unknown, NamedUnknown::Field(name))?, }; let field_name = format_ident!( "{}", @@ -401,7 +401,7 @@ fn array_type( let (_, item_type) = match &array.items { LexArrayItem::Integer(integer) => integer_type(integer)?, LexArrayItem::String(string) => string_type(string)?, - LexArrayItem::Unknown(unknown) => unknown_type(unknown, None)?, + LexArrayItem::Unknown(unknown) => unknown_type(unknown, NamedUnknown::Array(name))?, LexArrayItem::CidLink(cid_link) => cid_link_type(cid_link)?, LexArrayItem::Ref(r#ref) => ref_type(r#ref)?, LexArrayItem::Union(union) => union_type( @@ -562,13 +562,26 @@ fn string_type(string: &LexString) -> Result<(TokenStream, TokenStream)> { Ok((description, typ)) } -fn unknown_type(unknown: &LexUnknown, name: Option<&str>) -> Result<(TokenStream, TokenStream)> { +enum NamedUnknown<'s> { + Field(&'s str), + Array(&'s str), +} + +fn unknown_type( + unknown: &LexUnknown, + name: NamedUnknown<'_>, +) -> Result<(TokenStream, TokenStream)> { let description = description(&unknown.description); - if name == Some("didDoc") { - Ok((description, quote!(crate::did_doc::DidDocument))) - } else { - Ok((description, quote!(crate::records::Record))) - } + + let typ = match name { + NamedUnknown::Field("didDoc") => quote!(crate::did_doc::DidDocument), + NamedUnknown::Field("record") | NamedUnknown::Array("relatedRecords") => { + quote!(crate::records::Record) + } + _ => quote!(crate::types::Unknown), + }; + + Ok((description, typ)) } fn description(description: &Option) -> TokenStream {