atproto-record 0.11.3

AT Protocol record signature operations - cryptographic signing and verification for AT Protocol records
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! Location types for AT Protocol.
//!
//! This module provides various location representation types including
//! addresses, geographic coordinates, Foursquare places, and H3 hexagonal
//! hierarchical spatial indices.

use crate::{
    lexicon::com::atproto::repo::TypedStrongRef,
    typed::{LexiconType, TypedLexicon},
};
use serde::{Deserialize, Serialize};

/// Base namespace identifier for location types
pub const NSID: &str = "community.lexicon.location";
/// Namespace identifier for address locations
pub const ADDRESS_NSID: &str = "community.lexicon.location.address";
/// Namespace identifier for geographic coordinate locations
pub const GEO_NSID: &str = "community.lexicon.location.geo";
/// Namespace identifier for Foursquare locations
pub const FSQ_NSID: &str = "community.lexicon.location.fsq";
/// Namespace identifier for H3 locations
pub const HTHREE_NSID: &str = "community.lexicon.location.hthree";

/// Enum that can hold either a location reference or inline location data.
///
/// This type allows locations to be either embedded directly in a record
/// or referenced via a strong reference. Supports multiple location types
/// including addresses, coordinates, and third-party location identifiers.
///
/// # Example
///
/// ```ignore
/// use atproto_record::lexicon::community::lexicon::location::{LocationOrRef, TypedAddress, Address};
///
/// // Inline address
/// let address = Address {
///     country: "USA".to_string(),
///     postal_code: Some("12345".to_string()),
///     region: Some("CA".to_string()),
///     locality: Some("San Francisco".to_string()),
///     street: None,
///     name: None,
/// };
/// let location = LocationOrRef::InlineAddress(TypedAddress::new(address));
/// ```
#[derive(Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[serde(untagged)]
pub enum LocationOrRef {
    /// A reference to a location stored elsewhere
    Reference(TypedStrongRef),
    /// An inline address location
    InlineAddress(TypedAddress),
    /// An inline geographic coordinate location
    InlineGeo(TypedGeo),
    /// An inline H3 location
    InlineHthree(TypedHthree),
    /// An inline Foursquare location
    InlineFsq(TypedFsq),
}

/// A vector of locations that can be either inline or referenced.
///
/// This type alias is commonly used in records that support multiple
/// locations, such as events that might have both physical and virtual locations.
pub type Locations = Vec<LocationOrRef>;

/// Address location structure.
///
/// Represents a physical address with varying levels of detail.
/// Only the country field is required; all other fields are optional.
///
/// # Example
///
/// ```ignore
/// use atproto_record::lexicon::community::lexicon::location::Address;
///
/// let address = Address {
///     country: "United States".to_string(),
///     postal_code: Some("94102".to_string()),
///     region: Some("California".to_string()),
///     locality: Some("San Francisco".to_string()),
///     street: Some("123 Market St".to_string()),
///     name: Some("Tech Hub Building".to_string()),
/// };
/// ```
#[derive(Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct Address {
    /// Country name (required)
    pub country: String,

    /// Postal/ZIP code
    #[serde(
        rename = "postalCode",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub postal_code: Option<String>,

    /// State, province, or region
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub region: Option<String>,

    /// City or locality
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub locality: Option<String>,

    /// Street address
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub street: Option<String>,

    /// Location name (e.g., building or venue name)
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub name: Option<String>,
}

impl LexiconType for Address {
    fn lexicon_type() -> &'static str {
        ADDRESS_NSID
    }
}

/// Type alias for Address with automatic $type field handling
pub type TypedAddress = TypedLexicon<Address>;

/// Geographic coordinates location structure.
///
/// Represents a location using latitude and longitude coordinates.
/// Coordinates are stored as strings to preserve precision.
///
/// # Example
///
/// ```ignore
/// use atproto_record::lexicon::community::lexicon::location::Geo;
///
/// let location = Geo {
///     latitude: "37.7749".to_string(),
///     longitude: "-122.4194".to_string(),
///     name: Some("San Francisco".to_string()),
/// };
/// ```
#[derive(Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct Geo {
    /// Latitude coordinate as a string
    pub latitude: String,

    /// Longitude coordinate as a string
    pub longitude: String,

    /// Optional human-readable name for this location
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub name: Option<String>,
}

impl LexiconType for Geo {
    fn lexicon_type() -> &'static str {
        GEO_NSID
    }
}

/// Type alias for Geo with automatic $type field handling
pub type TypedGeo = TypedLexicon<Geo>;

/// Foursquare location structure.
///
/// Represents a location using Foursquare's place identifier system.
/// This allows integration with Foursquare's venue database.
///
/// # Example
///
/// ```ignore
/// use atproto_record::lexicon::community::lexicon::location::Fsq;
///
/// let location = Fsq {
///     fsq_place_id: "4a27f3d4f964a520a4891fe3".to_string(),
///     name: Some("Empire State Building".to_string()),
/// };
/// ```
#[derive(Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct Fsq {
    /// Foursquare place identifier
    pub fsq_place_id: String,

    /// Optional venue name from Foursquare
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub name: Option<String>,
}

impl LexiconType for Fsq {
    fn lexicon_type() -> &'static str {
        FSQ_NSID
    }
}

/// Type alias for Fsq with automatic $type field handling
pub type TypedFsq = TypedLexicon<Fsq>;

/// H3 location structure.
///
/// Represents a location using Uber's H3 hexagonal hierarchical spatial index.
/// H3 provides a way to represent geographic areas as hexagons at various resolutions.
///
/// # Example
///
/// ```ignore
/// use atproto_record::lexicon::community::lexicon::location::Hthree;
///
/// let location = Hthree {
///     value: "8a2a1072b59ffff".to_string(),
///     name: Some("Downtown Area".to_string()),
/// };
/// ```
#[derive(Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct Hthree {
    /// H3 hexagon identifier
    pub value: String,

    /// Optional human-readable name for this area
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub name: Option<String>,
}

impl LexiconType for Hthree {
    fn lexicon_type() -> &'static str {
        HTHREE_NSID
    }
}

/// Type alias for Hthree with automatic $type field handling
pub type TypedHthree = TypedLexicon<Hthree>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_typed_address() {
        // Create an Address without explicit $type field
        let address = Address {
            country: "USA".to_string(),
            postal_code: Some("12345".to_string()),
            region: Some("California".to_string()),
            locality: Some("San Francisco".to_string()),
            street: Some("123 Main St".to_string()),
            name: Some("Office Building".to_string()),
        };

        // Wrap it in TypedAddress
        let typed_address = TypedLexicon::new(address.clone());

        // Serialize and verify $type is added
        let json = serde_json::to_value(&typed_address).unwrap();
        assert_eq!(json["$type"], "community.lexicon.location.address");
        assert_eq!(json["country"], "USA");
        assert_eq!(json["postalCode"], "12345");
        assert_eq!(json["region"], "California");

        // Deserialize with $type field
        let json_str = r#"{
            "$type": "community.lexicon.location.address",
            "country": "Canada",
            "postalCode": "K1A 0B1",
            "region": "Ontario",
            "locality": "Ottawa"
        }"#;

        let deserialized: TypedAddress = serde_json::from_str(json_str).unwrap();
        assert_eq!(deserialized.inner.country, "Canada");
        assert_eq!(deserialized.inner.postal_code, Some("K1A 0B1".to_string()));
        assert!(deserialized.has_type_field());
    }

    #[test]
    fn test_typed_geo() {
        // Create a Geo without explicit $type field
        let geo = Geo {
            latitude: "37.7749".to_string(),
            longitude: "-122.4194".to_string(),
            name: Some("San Francisco".to_string()),
        };

        // Wrap it in TypedGeo
        let typed_geo = TypedLexicon::new(geo);

        // Serialize and verify $type is added
        let json = serde_json::to_value(&typed_geo).unwrap();
        assert_eq!(json["$type"], "community.lexicon.location.geo");
        assert_eq!(json["latitude"], "37.7749");
        assert_eq!(json["longitude"], "-122.4194");
        assert_eq!(json["name"], "San Francisco");

        // Deserialize with $type field
        let json_str = r#"{
            "$type": "community.lexicon.location.geo",
            "latitude": "40.7128",
            "longitude": "-74.0060",
            "name": "New York"
        }"#;

        let deserialized: TypedGeo = serde_json::from_str(json_str).unwrap();
        assert_eq!(deserialized.inner.latitude, "40.7128");
        assert_eq!(deserialized.inner.longitude, "-74.0060");
        assert_eq!(deserialized.inner.name, Some("New York".to_string()));
        assert!(deserialized.has_type_field());
    }

    #[test]
    fn test_typed_fsq() {
        // Create an Fsq without explicit $type field
        let fsq = Fsq {
            fsq_place_id: "4a27f3d4f964a520a4891fe3".to_string(),
            name: Some("Empire State Building".to_string()),
        };

        // Wrap it in TypedFsq
        let typed_fsq = TypedLexicon::new(fsq);

        // Serialize and verify $type is added
        let json = serde_json::to_value(&typed_fsq).unwrap();
        assert_eq!(json["$type"], "community.lexicon.location.fsq");
        assert_eq!(json["fsq_place_id"], "4a27f3d4f964a520a4891fe3");
        assert_eq!(json["name"], "Empire State Building");

        // Deserialize without name field
        let json_str = r#"{
            "$type": "community.lexicon.location.fsq",
            "fsq_place_id": "5642aef9498e51025cf4a7a5"
        }"#;

        let deserialized: TypedFsq = serde_json::from_str(json_str).unwrap();
        assert_eq!(deserialized.inner.fsq_place_id, "5642aef9498e51025cf4a7a5");
        assert_eq!(deserialized.inner.name, None);
        assert!(deserialized.has_type_field());
    }

    #[test]
    fn test_typed_hthree() {
        // Create an Hthree without explicit $type field
        let hthree = Hthree {
            value: "8a2a1072b59ffff".to_string(),
            name: Some("Downtown Area".to_string()),
        };

        // Wrap it in TypedHthree
        let typed_hthree = TypedLexicon::new(hthree);

        // Serialize and verify $type is added
        let json = serde_json::to_value(&typed_hthree).unwrap();
        assert_eq!(json["$type"], "community.lexicon.location.hthree");
        assert_eq!(json["value"], "8a2a1072b59ffff");
        assert_eq!(json["name"], "Downtown Area");

        // Deserialize without name field
        let json_str = r#"{
            "$type": "community.lexicon.location.hthree",
            "value": "8928308280fffff"
        }"#;

        let deserialized: TypedHthree = serde_json::from_str(json_str).unwrap();
        assert_eq!(deserialized.inner.value, "8928308280fffff");
        assert_eq!(deserialized.inner.name, None);
        assert!(deserialized.has_type_field());
    }

    #[test]
    fn test_optional_fields() {
        // Test Address with minimal fields
        let address = Address {
            country: "USA".to_string(),
            postal_code: None,
            region: None,
            locality: None,
            street: None,
            name: None,
        };

        let typed_address = TypedLexicon::new(address);
        let json = serde_json::to_value(&typed_address).unwrap();

        // Optional fields should not be present when None
        assert_eq!(json["$type"], "community.lexicon.location.address");
        assert_eq!(json["country"], "USA");
        assert!(!json.as_object().unwrap().contains_key("postalCode"));
        assert!(!json.as_object().unwrap().contains_key("region"));
        assert!(!json.as_object().unwrap().contains_key("locality"));
        assert!(!json.as_object().unwrap().contains_key("street"));
        assert!(!json.as_object().unwrap().contains_key("name"));

        // Test Geo with minimal fields
        let geo = Geo {
            latitude: "0.0".to_string(),
            longitude: "0.0".to_string(),
            name: None,
        };

        let typed_geo = TypedLexicon::new(geo);
        let json = serde_json::to_value(&typed_geo).unwrap();

        assert_eq!(json["$type"], "community.lexicon.location.geo");
        assert_eq!(json["latitude"], "0.0");
        assert_eq!(json["longitude"], "0.0");
        assert!(!json.as_object().unwrap().contains_key("name"));
    }
}