A better Rust ATProto crate

fixes in the derive macro

Orual 07712e06 b2ed3efb

Changed files
+233 -21
crates
jacquard-common
src
types
jacquard-derive
jacquard-lexicon
+109
crates/jacquard-common/src/types/value.rs
··· 117 117 Data::from_json(&json).map(|data| data.into_static()) 118 118 } 119 119 120 + /// Get as object if this is an Object variant 121 + pub fn as_object(&self) -> Option<&Object<'s>> { 122 + if let Data::Object(obj) = self { 123 + Some(obj) 124 + } else { 125 + None 126 + } 127 + } 128 + 129 + /// Get as array if this is an Array variant 130 + pub fn as_array(&self) -> Option<&Array<'s>> { 131 + if let Data::Array(arr) = self { 132 + Some(arr) 133 + } else { 134 + None 135 + } 136 + } 137 + 138 + /// Get as string if this is a String variant 139 + pub fn as_str(&self) -> Option<&str> { 140 + if let Data::String(s) = self { 141 + Some(s.as_str()) 142 + } else { 143 + None 144 + } 145 + } 146 + 147 + /// Get as integer if this is an Integer variant 148 + pub fn as_integer(&self) -> Option<i64> { 149 + if let Data::Integer(i) = self { 150 + Some(*i) 151 + } else { 152 + None 153 + } 154 + } 155 + 156 + /// Get as boolean if this is a Boolean variant 157 + pub fn as_boolean(&self) -> Option<bool> { 158 + if let Data::Boolean(b) = self { 159 + Some(*b) 160 + } else { 161 + None 162 + } 163 + } 164 + 165 + /// Check if this is a null value 166 + pub fn is_null(&self) -> bool { 167 + matches!(self, Data::Null) 168 + } 169 + 170 + /// Serialize to canonical DAG-CBOR bytes for CID computation 171 + /// 172 + /// This produces the deterministic CBOR encoding used for content-addressing. 173 + pub fn to_dag_cbor( 174 + &self, 175 + ) -> Result<Vec<u8>, serde_ipld_dagcbor::EncodeError<std::collections::TryReserveError>> { 176 + serde_ipld_dagcbor::to_vec(self) 177 + } 178 + 120 179 /// Parse a Data value from an IPLD value (CBOR) 121 180 pub fn from_cbor(cbor: &'s Ipld) -> Result<Self, AtDataError> { 122 181 Ok(match cbor { ··· 358 417 } 359 418 360 419 impl<'d> RawData<'d> { 420 + /// Get as object if this is an Object variant 421 + pub fn as_object(&self) -> Option<&BTreeMap<SmolStr, RawData<'d>>> { 422 + if let RawData::Object(obj) = self { 423 + Some(obj) 424 + } else { 425 + None 426 + } 427 + } 428 + 429 + /// Get as array if this is an Array variant 430 + pub fn as_array(&self) -> Option<&Vec<RawData<'d>>> { 431 + if let RawData::Array(arr) = self { 432 + Some(arr) 433 + } else { 434 + None 435 + } 436 + } 437 + 438 + /// Get as string if this is a String variant 439 + pub fn as_str(&self) -> Option<&str> { 440 + if let RawData::String(s) = self { 441 + Some(s.as_ref()) 442 + } else { 443 + None 444 + } 445 + } 446 + 447 + /// Get as boolean if this is a Boolean variant 448 + pub fn as_boolean(&self) -> Option<bool> { 449 + if let RawData::Boolean(b) = self { 450 + Some(*b) 451 + } else { 452 + None 453 + } 454 + } 455 + 456 + /// Check if this is a null value 457 + pub fn is_null(&self) -> bool { 458 + matches!(self, RawData::Null) 459 + } 460 + 461 + /// Serialize to canonical DAG-CBOR bytes for CID computation 462 + /// 463 + /// This produces the deterministic CBOR encoding used for content-addressing. 464 + pub fn to_dag_cbor( 465 + &self, 466 + ) -> Result<Vec<u8>, serde_ipld_dagcbor::EncodeError<std::collections::TryReserveError>> { 467 + serde_ipld_dagcbor::to_vec(self) 468 + } 469 + 361 470 /// Convert a CBOR-encoded byte slice into a `RawData` value. 362 471 /// Parse a Data value from an IPLD value (CBOR) 363 472 pub fn from_cbor(cbor: &'d Ipld) -> Result<Self, AtDataError> {
+117
crates/jacquard-common/src/types/value/tests.rs
··· 813 813 assert_eq!(result.text, "null test"); 814 814 assert_eq!(result.langs, None); 815 815 } 816 + 817 + #[test] 818 + fn test_data_accessors() { 819 + // Test as_object 820 + let mut map = BTreeMap::new(); 821 + map.insert(SmolStr::new_static("key"), Data::Integer(42)); 822 + let obj_data = Data::Object(Object(map.clone())); 823 + assert!(obj_data.as_object().is_some()); 824 + assert_eq!(obj_data.as_object().unwrap().0.len(), 1); 825 + assert!(Data::Null.as_object().is_none()); 826 + 827 + // Test as_array 828 + let arr_data = Data::Array(Array(vec![Data::Integer(1), Data::Integer(2)])); 829 + assert!(arr_data.as_array().is_some()); 830 + assert_eq!(arr_data.as_array().unwrap().0.len(), 2); 831 + assert!(Data::Null.as_array().is_none()); 832 + 833 + // Test as_str 834 + let str_data = Data::String(AtprotoStr::String("hello".into())); 835 + assert_eq!(str_data.as_str(), Some("hello")); 836 + assert!(Data::Null.as_str().is_none()); 837 + 838 + // Test as_integer 839 + let int_data = Data::Integer(42); 840 + assert_eq!(int_data.as_integer(), Some(42)); 841 + assert!(Data::Null.as_integer().is_none()); 842 + 843 + // Test as_boolean 844 + let bool_data = Data::Boolean(true); 845 + assert_eq!(bool_data.as_boolean(), Some(true)); 846 + assert!(Data::Null.as_boolean().is_none()); 847 + 848 + // Test is_null 849 + assert!(Data::Null.is_null()); 850 + assert!(!Data::Integer(0).is_null()); 851 + } 852 + 853 + #[test] 854 + fn test_rawdata_accessors() { 855 + // Test as_object 856 + let mut map = BTreeMap::new(); 857 + map.insert(SmolStr::new_static("key"), RawData::SignedInt(42)); 858 + let obj_data = RawData::Object(map.clone()); 859 + assert!(obj_data.as_object().is_some()); 860 + assert_eq!(obj_data.as_object().unwrap().len(), 1); 861 + assert!(RawData::Null.as_object().is_none()); 862 + 863 + // Test as_array 864 + let arr_data = RawData::Array(vec![RawData::SignedInt(1), RawData::SignedInt(2)]); 865 + assert!(arr_data.as_array().is_some()); 866 + assert_eq!(arr_data.as_array().unwrap().len(), 2); 867 + assert!(RawData::Null.as_array().is_none()); 868 + 869 + // Test as_str 870 + let str_data = RawData::String("hello".into()); 871 + assert_eq!(str_data.as_str(), Some("hello")); 872 + assert!(RawData::Null.as_str().is_none()); 873 + 874 + // Test as_boolean 875 + let bool_data = RawData::Boolean(true); 876 + assert_eq!(bool_data.as_boolean(), Some(true)); 877 + assert!(RawData::Null.as_boolean().is_none()); 878 + 879 + // Test is_null 880 + assert!(RawData::Null.is_null()); 881 + assert!(!RawData::SignedInt(0).is_null()); 882 + } 883 + 884 + #[test] 885 + fn test_data_to_dag_cbor() { 886 + // Test simple types 887 + let null_data = Data::Null; 888 + assert!(null_data.to_dag_cbor().is_ok()); 889 + 890 + let int_data = Data::Integer(42); 891 + assert!(int_data.to_dag_cbor().is_ok()); 892 + 893 + let str_data = Data::String(AtprotoStr::String("hello".into())); 894 + assert!(str_data.to_dag_cbor().is_ok()); 895 + 896 + // Test complex types 897 + let mut map = BTreeMap::new(); 898 + map.insert(SmolStr::new_static("num"), Data::Integer(42)); 899 + map.insert(SmolStr::new_static("text"), Data::String(AtprotoStr::String("test".into()))); 900 + let obj_data = Data::Object(Object(map)); 901 + let cbor_result = obj_data.to_dag_cbor(); 902 + assert!(cbor_result.is_ok()); 903 + assert!(!cbor_result.unwrap().is_empty()); 904 + 905 + // Test array 906 + let arr_data = Data::Array(Array(vec![Data::Integer(1), Data::Integer(2), Data::Integer(3)])); 907 + let arr_cbor = arr_data.to_dag_cbor(); 908 + assert!(arr_cbor.is_ok()); 909 + assert!(!arr_cbor.unwrap().is_empty()); 910 + } 911 + 912 + #[test] 913 + fn test_rawdata_to_dag_cbor() { 914 + // Test simple types 915 + let null_data = RawData::Null; 916 + assert!(null_data.to_dag_cbor().is_ok()); 917 + 918 + let int_data = RawData::SignedInt(42); 919 + assert!(int_data.to_dag_cbor().is_ok()); 920 + 921 + let str_data = RawData::String("hello".into()); 922 + assert!(str_data.to_dag_cbor().is_ok()); 923 + 924 + // Test complex types 925 + let mut map = BTreeMap::new(); 926 + map.insert(SmolStr::new_static("num"), RawData::SignedInt(42)); 927 + map.insert(SmolStr::new_static("text"), RawData::String("test".into())); 928 + let obj_data = RawData::Object(map); 929 + let cbor_result = obj_data.to_dag_cbor(); 930 + assert!(cbor_result.is_ok()); 931 + assert!(!cbor_result.unwrap().is_empty()); 932 + }
+1 -1
crates/jacquard-derive/src/lib.rs
··· 167 167 /// **What it generates:** 168 168 /// - `impl LexiconSchema` with `nsid()`, `schema_id()`, and `lexicon_doc()` methods 169 169 /// - `validate()` method that checks constraints at runtime 170 - /// - `inventory::submit!` registration for schema discovery (Phase 3) 170 + /// - `inventory::submit!` registration for schema discovery 171 171 /// 172 172 /// **Attributes:** `#[lexicon(...)]` and `#[nsid = "..."]` on types and fields. 173 173 /// See crate docs for full attribute reference and examples.
+1 -9
crates/jacquard-derive/tests/lexicon_schema_derive.rs
··· 1 1 use jacquard_common::CowStr; 2 2 use jacquard_common::types::string::Datetime; 3 3 use jacquard_derive::{LexiconSchema, open_union}; 4 - use jacquard_lexicon::schema::{LexiconGenerator, LexiconSchema as LexiconSchemaTrait}; 4 + use jacquard_lexicon::schema::LexiconSchema as LexiconSchemaTrait; 5 5 use serde::{Deserialize, Serialize}; 6 6 7 7 #[test] ··· 18 18 assert_eq!(SimpleRecord::nsid(), "com.example.simple"); 19 19 assert_eq!(SimpleRecord::schema_id().as_ref(), "com.example.simple"); 20 20 21 - let mut generator = LexiconGenerator::new(SimpleRecord::nsid()); 22 21 let doc = SimpleRecord::lexicon_doc(); 23 22 24 23 assert_eq!(doc.id.as_ref(), "com.example.simple"); ··· 46 45 pub score: i64, 47 46 } 48 47 49 - let mut generator = LexiconGenerator::new(ConstrainedRecord::nsid()); 50 48 let doc = ConstrainedRecord::lexicon_doc(); 51 49 52 50 let json = serde_json::to_string_pretty(&doc).unwrap(); ··· 109 107 pub some_field: i64, 110 108 pub another_field: i64, 111 109 } 112 - 113 - let mut generator = LexiconGenerator::new(RenamedRecord::nsid()); 114 110 let doc = RenamedRecord::lexicon_doc(); 115 111 116 112 let json = serde_json::to_string_pretty(&doc).unwrap(); ··· 184 180 Unknown(jacquard_common::types::value::Data<'a>), 185 181 } 186 182 187 - let mut generator = LexiconGenerator::new(OpenUnion::nsid()); 188 183 let doc = OpenUnion::lexicon_doc(); 189 184 190 185 let json = serde_json::to_string_pretty(&doc).unwrap(); ··· 206 201 Video, 207 202 } 208 203 209 - let mut generator = LexiconGenerator::new(RenamedUnion::nsid()); 210 204 let doc = RenamedUnion::lexicon_doc(); 211 205 212 206 let json = serde_json::to_string_pretty(&doc).unwrap(); ··· 229 223 #[allow(dead_code)] 230 224 VariantTwo, 231 225 } 232 - 233 - let mut generator = LexiconGenerator::new(FragmentUnion::nsid()); 234 226 let doc = FragmentUnion::lexicon_doc(); 235 227 236 228 let json = serde_json::to_string_pretty(&doc).unwrap();
+1 -1
crates/jacquard-lexicon/src/codegen/schema_impl.rs
··· 2 2 3 3 use crate::derive_impl::doc_to_tokens; 4 4 use crate::lexicon::{ 5 - LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexRecordRecord, LexString, 5 + LexInteger, LexObject, LexObjectProperty, LexRecordRecord, LexString, 6 6 LexUserType, LexiconDoc, 7 7 }; 8 8 use crate::schema::from_ast::{ConstraintCheck, ValidationCheck};
+1 -3
crates/jacquard-lexicon/src/codegen/structs.rs
··· 1 1 use crate::error::Result; 2 2 use crate::lexicon::{ 3 - LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString, LexUserType, 4 - Lexicon, LexiconDoc, 3 + LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString, 5 4 }; 6 5 use heck::{ToPascalCase, ToSnakeCase}; 7 6 use proc_macro2::TokenStream; 8 7 use quote::quote; 9 - use std::collections::BTreeMap; 10 8 11 9 use super::CodeGenerator; 12 10 use super::utils::{make_ident, value_to_variant_name};
+2 -6
crates/jacquard-lexicon/src/derive_impl/lexicon_schema.rs
··· 69 69 } 70 70 71 71 fn lexicon_doc( 72 - _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator 73 72 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 74 73 #doc_tokens 75 74 } ··· 84 83 ::jacquard_lexicon::schema::LexiconSchemaRef { 85 84 nsid: #nsid, 86 85 provider: || { 87 - let mut generator = ::jacquard_lexicon::schema::LexiconGenerator::new(#nsid); 88 - <#name as ::jacquard_lexicon::schema::LexiconSchema>::lexicon_doc(&mut generator) 86 + <#name as ::jacquard_lexicon::schema::LexiconSchema>::lexicon_doc() 89 87 }, 90 88 } 91 89 } ··· 124 122 } 125 123 126 124 fn lexicon_doc( 127 - _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator 128 125 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 129 126 #doc_tokens 130 127 } ··· 138 135 ::jacquard_lexicon::schema::LexiconSchemaRef { 139 136 nsid: #nsid, 140 137 provider: || { 141 - let mut generator = ::jacquard_lexicon::schema::LexiconGenerator::new(#nsid); 142 - <#name as ::jacquard_lexicon::schema::LexiconSchema>::lexicon_doc(&mut generator) 138 + <#name as ::jacquard_lexicon::schema::LexiconSchema>::lexicon_doc() 143 139 }, 144 140 } 145 141 }
+1 -1
crates/jacquard-lexicon/src/schema.rs
··· 58 58 pub mod from_ast; 59 59 pub mod type_mapping; 60 60 61 - use crate::lexicon::{LexUserType, Lexicon, LexiconDoc}; 61 + use crate::lexicon::LexiconDoc; 62 62 63 63 /// Trait for types that can generate lexicon schemas 64 64 pub trait LexiconSchema {