A better Rust ATProto crate
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 13 kB View raw
1use jacquard_common::CowStr; 2use jacquard_common::types::string::Datetime; 3use jacquard_derive::{LexiconSchema, open_union}; 4use jacquard_lexicon::schema::LexiconSchema as LexiconSchemaTrait; 5use serde::{Deserialize, Serialize}; 6extern crate alloc; 7 8#[test] 9fn test_simple_struct() { 10 #[derive(LexiconSchema)] 11 #[lexicon(nsid = "com.example.simple", record, key = "tid")] 12 struct SimpleRecord<'a> { 13 #[allow(dead_code)] 14 pub text: CowStr<'a>, 15 #[allow(dead_code)] 16 pub created_at: Datetime, 17 } 18 19 assert_eq!(SimpleRecord::nsid(), "com.example.simple"); 20 assert_eq!(SimpleRecord::schema_id().as_ref(), "com.example.simple"); 21 22 let doc = SimpleRecord::lexicon_doc(); 23 24 assert_eq!(doc.id.as_ref(), "com.example.simple"); 25 assert!(doc.defs.contains_key("main")); 26 27 // Serialize to JSON to verify structure 28 let json = serde_json::to_string_pretty(&doc).unwrap(); 29 println!("{}", json); 30 31 // Should contain record type 32 assert!(json.contains("\"type\": \"record\"")); 33 // Should have camelCase field names (default) 34 assert!(json.contains("\"createdAt\"")); 35} 36 37#[test] 38fn test_struct_with_constraints() { 39 #[derive(LexiconSchema)] 40 #[lexicon(nsid = "com.example.constrained", record)] 41 struct ConstrainedRecord<'a> { 42 #[lexicon(max_graphemes = 300, max_length = 3000)] 43 pub text: CowStr<'a>, 44 45 #[lexicon(minimum = 0, maximum = 100)] 46 pub score: i64, 47 } 48 49 let doc = ConstrainedRecord::lexicon_doc(); 50 51 let json = serde_json::to_string_pretty(&doc).unwrap(); 52 println!("{}", json); 53 54 // Verify constraints are in schema 55 assert!(json.contains("\"maxGraphemes\": 300")); 56 assert!(json.contains("\"maxLength\": 3000")); 57 assert!(json.contains("\"minimum\": 0")); 58 assert!(json.contains("\"maximum\": 100")); 59} 60 61#[test] 62fn test_validation() { 63 #[derive(LexiconSchema)] 64 #[lexicon(nsid = "com.example.validated", record)] 65 struct ValidatedRecord<'a> { 66 #[lexicon(max_length = 100)] 67 pub text: CowStr<'a>, 68 69 #[lexicon(minimum = 0, maximum = 10)] 70 pub count: i64, 71 } 72 73 // Valid 74 let valid = ValidatedRecord { 75 text: "hello".into(), 76 count: 5, 77 }; 78 assert!(valid.validate().is_ok()); 79 80 // Text too long 81 let invalid_text = ValidatedRecord { 82 text: "a".repeat(150).into(), 83 count: 5, 84 }; 85 assert!(invalid_text.validate().is_err()); 86 87 // Count too high 88 let invalid_count = ValidatedRecord { 89 text: "hello".into(), 90 count: 15, 91 }; 92 assert!(invalid_count.validate().is_err()); 93 94 // Count too low 95 let invalid_low = ValidatedRecord { 96 text: "hello".into(), 97 count: -5, 98 }; 99 assert!(invalid_low.validate().is_err()); 100} 101 102#[test] 103fn test_serde_rename() { 104 #[derive(Serialize, Deserialize, LexiconSchema)] 105 #[lexicon(nsid = "com.example.renamed", record)] 106 #[serde(rename_all = "snake_case")] 107 struct RenamedRecord { 108 pub some_field: i64, 109 pub another_field: i64, 110 } 111 let doc = RenamedRecord::lexicon_doc(); 112 113 let json = serde_json::to_string_pretty(&doc).unwrap(); 114 println!("{}", json); 115 116 // Should use snake_case not camelCase 117 assert!(json.contains("\"some_field\"")); 118 assert!(json.contains("\"another_field\"")); 119} 120 121#[test] 122fn test_default_camel_case() { 123 #[derive(LexiconSchema)] 124 #[lexicon(nsid = "com.example.camel", record)] 125 struct CamelCaseRecord { 126 #[allow(dead_code)] 127 pub field_one: i64, 128 #[allow(dead_code)] 129 pub field_two: i64, 130 } 131 132 let doc = CamelCaseRecord::lexicon_doc(); 133 134 let json = serde_json::to_string_pretty(&doc).unwrap(); 135 println!("{}", json); 136 137 // Should default to camelCase 138 assert!(json.contains("\"fieldOne\"")); 139 assert!(json.contains("\"fieldTwo\"")); 140} 141 142#[test] 143fn test_basic_enum() { 144 #[derive(LexiconSchema)] 145 #[lexicon(nsid = "com.example.union")] 146 enum BasicUnion { 147 #[nsid = "com.example.variant.one"] 148 #[allow(dead_code)] 149 VariantOne, 150 151 #[nsid = "com.example.variant.two"] 152 #[allow(dead_code)] 153 VariantTwo, 154 } 155 156 let doc = BasicUnion::lexicon_doc(); 157 158 let json = serde_json::to_string_pretty(&doc).unwrap(); 159 println!("{}", json); 160 161 // Should be a union type 162 assert!(json.contains("\"type\": \"union\"")); 163 // Should have refs 164 assert!(json.contains("com.example.variant.one")); 165 assert!(json.contains("com.example.variant.two")); 166 // Should be closed by default 167 assert!(json.contains("\"closed\": true")); 168} 169 170#[test] 171fn test_open_union_attribute_adds_unknown_variant() { 172 #[open_union] 173 #[derive(Serialize, Deserialize, LexiconSchema)] 174 #[lexicon(nsid = "com.example.open")] 175 enum OpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 176 #[nsid = "com.example.variant"] 177 #[allow(dead_code)] 178 Variant, 179 } 180 181 let doc = OpenUnion::<jacquard_common::DefaultStr>::lexicon_doc(); 182 183 let json = serde_json::to_string_pretty(&doc).unwrap(); 184 println!("{}", json); 185 186 // Should be a union type with known refs, while remaining open by omitting closed. 187 assert!(json.contains("\"type\": \"union\"")); 188 assert!(json.contains("com.example.variant")); 189 assert!(!json.contains("\"closed\"")); 190} 191 192#[test] 193fn test_open_union_detects_existing_unknown_variant() { 194 #[derive(LexiconSchema)] 195 #[lexicon(nsid = "com.example.open_existing")] 196 enum OpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 197 #[nsid = "com.example.variant"] 198 #[allow(dead_code)] 199 Variant, 200 201 #[allow(dead_code)] 202 Unknown(jacquard_common::types::value::Data<S>), 203 } 204 205 let doc = OpenUnion::<jacquard_common::DefaultStr>::lexicon_doc(); 206 207 let json = serde_json::to_string_pretty(&doc).unwrap(); 208 println!("{}", json); 209 210 assert!(json.contains("\"type\": \"union\"")); 211 assert!(json.contains("com.example.variant")); 212 assert!(!json.contains("\"closed\"")); 213} 214 215#[test] 216fn test_generic_record() { 217 #[derive(LexiconSchema)] 218 #[lexicon(nsid = "com.example.generic", record)] 219 struct GenericRecord<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 220 #[lexicon(max_length = 100)] 221 #[allow(dead_code)] 222 text: S, 223 } 224 225 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc(); 226 let json = serde_json::to_string_pretty(&doc).unwrap(); 227 println!("{}", json); 228 229 assert!(json.contains("\"type\": \"string\"")); 230 assert!(json.contains("\"maxLength\": 100")); 231 232 let record = GenericRecord { 233 text: jacquard_common::DefaultStr::from("hello"), 234 }; 235 assert!(record.validate().is_ok()); 236} 237 238#[test] 239fn test_optional_generic_record() { 240 #[derive(LexiconSchema)] 241 #[lexicon(nsid = "com.example.optional_generic", record)] 242 struct OptionalGeneric<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 243 #[lexicon(max_length = 100)] 244 #[allow(dead_code)] 245 text: Option<S>, 246 } 247 248 let doc = OptionalGeneric::<jacquard_common::DefaultStr>::lexicon_doc(); 249 let json = serde_json::to_string_pretty(&doc).unwrap(); 250 println!("{}", json); 251 252 assert!(json.contains("\"type\": \"string\"")); 253 assert!(json.contains("\"maxLength\": 100")); 254 assert!(!json.contains("\"required\"")); 255} 256 257#[test] 258fn test_generic_array_record() { 259 #[derive(LexiconSchema)] 260 #[lexicon(nsid = "com.example.generic_array", record)] 261 struct GenericArray<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 262 #[lexicon(max_items = 5)] 263 #[allow(dead_code)] 264 tags: Vec<S>, 265 } 266 267 let doc = GenericArray::<jacquard_common::DefaultStr>::lexicon_doc(); 268 let json = serde_json::to_string_pretty(&doc).unwrap(); 269 println!("{}", json); 270 271 assert!(json.contains("\"type\": \"array\"")); 272 assert!(json.contains("\"items\"")); 273 assert!(json.contains("\"type\": \"string\"")); 274 assert!(json.contains("\"maxLength\": 5")); 275} 276 277#[test] 278fn test_non_s_bos_type_parameter_maps_to_string() { 279 #[derive(LexiconSchema)] 280 #[lexicon(nsid = "com.example.non_s_generic", record)] 281 struct GenericRecord<Text: jacquard_common::BosStr = jacquard_common::DefaultStr> { 282 #[allow(dead_code)] 283 text: Text, 284 } 285 286 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc(); 287 let json = serde_json::to_string_pretty(&doc).unwrap(); 288 println!("{}", json); 289 290 assert!(json.contains("\"type\": \"string\"")); 291} 292 293#[test] 294fn test_old_style_bos_bound_maps_to_string() { 295 #[derive(LexiconSchema)] 296 #[lexicon(nsid = "com.example.old_style_bos", record)] 297 struct GenericRecord<S: jacquard_common::Bos<str> + AsRef<str> = jacquard_common::DefaultStr> { 298 #[allow(dead_code)] 299 text: S, 300 } 301 302 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc(); 303 let json = serde_json::to_string_pretty(&doc).unwrap(); 304 println!("{}", json); 305 306 assert!(json.contains("\"type\": \"string\"")); 307} 308 309#[test] 310fn test_lifetime_and_bos_type_parameter_record() { 311 #[derive(LexiconSchema)] 312 #[lexicon(nsid = "com.example.mixed_generics", record)] 313 struct Mixed<'a, S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 314 #[allow(dead_code)] 315 borrowed: CowStr<'a>, 316 #[allow(dead_code)] 317 text: S, 318 } 319 320 let doc = Mixed::<'static, jacquard_common::DefaultStr>::lexicon_doc(); 321 let json = serde_json::to_string_pretty(&doc).unwrap(); 322 println!("{}", json); 323 324 assert_eq!(json.matches("\"type\": \"string\"").count(), 2); 325} 326 327#[test] 328fn test_generic_open_union_schema_and_unknown_roundtrip() { 329 use jacquard_common::types::value::{Data, Object}; 330 use std::collections::BTreeMap; 331 332 #[open_union] 333 #[derive(Serialize, Deserialize, Debug, PartialEq, LexiconSchema)] 334 #[serde(tag = "$type")] 335 #[lexicon(nsid = "com.example.generic_open_roundtrip")] 336 enum GenericOpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> { 337 #[serde(rename = "com.example.known")] 338 #[nsid = "com.example.known"] 339 Known { value: S }, 340 } 341 342 let doc = GenericOpenUnion::<jacquard_common::DefaultStr>::lexicon_doc(); 343 let json = serde_json::to_string_pretty(&doc).unwrap(); 344 println!("{}", json); 345 346 assert!(json.contains("\"type\": \"union\"")); 347 assert!(json.contains("com.example.known")); 348 assert!(!json.contains("\"closed\"")); 349 350 let unknown_json = r#"{"$type":"com.example.unknown","value":"hello"}"#; 351 let parsed: GenericOpenUnion = serde_json::from_str(unknown_json).unwrap(); 352 match parsed { 353 GenericOpenUnion::Unknown(Data::Object(obj)) => { 354 assert!(obj.0.contains_key("$type")); 355 assert!(obj.0.contains_key("value")); 356 } 357 _ => panic!("expected Unknown variant"), 358 } 359 360 let mut map = BTreeMap::new(); 361 map.insert( 362 "$type".into(), 363 Data::String(jacquard_common::types::string::AtprotoStr::String( 364 jacquard_common::DefaultStr::from("com.example.other"), 365 )), 366 ); 367 map.insert("count".into(), Data::Integer(7)); 368 369 let union = GenericOpenUnion::Unknown(Data::Object(Object(map))); 370 let serialized = serde_json::to_string(&union).unwrap(); 371 let roundtripped: GenericOpenUnion = serde_json::from_str(&serialized).unwrap(); 372 assert!(matches!( 373 roundtripped, 374 GenericOpenUnion::Unknown(Data::Object(_)) 375 )); 376} 377 378#[test] 379fn test_enum_with_serde_rename() { 380 #[derive(Serialize, Deserialize, LexiconSchema)] 381 #[lexicon(nsid = "com.example.renamed_union")] 382 enum RenamedUnion { 383 #[serde(rename = "app.bsky.embed.images")] 384 Images, 385 386 #[serde(rename = "app.bsky.embed.video")] 387 Video, 388 } 389 390 let doc = RenamedUnion::lexicon_doc(); 391 392 let json = serde_json::to_string_pretty(&doc).unwrap(); 393 println!("{}", json); 394 395 // Should use serde rename values 396 assert!(json.contains("app.bsky.embed.images")); 397 assert!(json.contains("app.bsky.embed.video")); 398} 399 400#[test] 401fn test_enum_fragment_inference() { 402 #[derive(LexiconSchema)] 403 #[lexicon(nsid = "com.example.fragments")] 404 enum FragmentUnion { 405 // Should generate com.example.fragments#variantOne 406 #[allow(dead_code)] 407 VariantOne, 408 // Should generate com.example.fragments#variantTwo 409 #[allow(dead_code)] 410 VariantTwo, 411 } 412 let doc = FragmentUnion::lexicon_doc(); 413 414 let json = serde_json::to_string_pretty(&doc).unwrap(); 415 println!("{}", json); 416 417 // Should have fragment refs 418 assert!(json.contains("com.example.fragments#variantOne")); 419 assert!(json.contains("com.example.fragments#variantTwo")); 420}