Skip to main content

cjseq/
city_object.rs

1use crate::geometry::Geometry;
2use crate::metadata::GeographicalExtent;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// The known 1st- and 2nd-level City Object types (§ 2 The different City
7/// Objects). Every variant is a unit variant and every identifier is already
8/// spelled exactly as the spec requires (eg `TINRelief`, `WaterBody`,
9/// `CityFurniture`), so serde's default (non-untagged) derive collapses each
10/// one to its bare name string with no `#[serde(rename)]` needed.
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
12pub enum KnownCityObjectType {
13    Bridge,
14    BridgePart,
15    BridgeInstallation,
16    BridgeConstructiveElement,
17    BridgeRoom,
18    BridgeFurniture,
19    Building,
20    BuildingPart,
21    BuildingInstallation,
22    BuildingConstructiveElement,
23    BuildingFurniture,
24    BuildingStorey,
25    BuildingRoom,
26    BuildingUnit,
27    CityFurniture,
28    CityObjectGroup,
29    GenericCityObject,
30    LandUse,
31    OtherConstruction,
32    PlantCover,
33    SolitaryVegetationObject,
34    TINRelief,
35    Road,
36    Railway,
37    Waterway,
38    TransportSquare,
39    Tunnel,
40    TunnelPart,
41    TunnelInstallation,
42    TunnelConstructiveElement,
43    TunnelHollowSpace,
44    TunnelFurniture,
45    WaterBody,
46}
47
48/// The `type` member of a City Object (§ 2 The different City Objects).
49///
50/// `Known` covers the 1st- and 2nd-level City Object types defined by the
51/// CityJSON 2.0.1 spec. Any other value is a CityJSON Extension type, which
52/// the spec requires to start with `"+"` (§ 8 Extensions) -- `Extension`
53/// carries that string verbatim so a file using an Extension (eg
54/// `"+NoiseCityFurnitureSegment"`) round-trips byte for byte.
55///
56/// This is `#[serde(untagged)]` over a *nested* known-variants enum, not a
57/// single flat enum mixing unit variants with `Extension(String)` directly:
58/// under `#[serde(untagged)]`, a bare unit variant (de)serializes as JSON
59/// `null`, not its name (this is a documented serde quirk -- untagged
60/// dispatch tries each variant's own `Deserialize` standalone, and a unit
61/// variant's is `deserialize_unit`, which only accepts `null`). A flat
62/// mixed enum was tried and empirically failed this exact way: every known
63/// name fell through to `Extension` because none of the unit variants ever
64/// matched a JSON string. Nesting the known names inside their own
65/// unit-only enum sidesteps this, because *that* enum, derived without
66/// `#[serde(untagged)]`, gets the ordinary external-tagging collapse (a
67/// unit-only enum's tag+content is just the bare name string).
68///
69/// # Match on the value, not on a reference
70///
71/// Each known name is also available flat, as an associated `const` (eg
72/// [`CityObjectType::BuildingPart`]) -- see the `impl` block below for the
73/// full list. They are `const`s, not variants: this enum has exactly the two
74/// variants above. That distinction bites in exactly one place, and it bites
75/// silently, so it belongs here rather than in the small print:
76///
77/// ```
78/// # use cjseq::{CityObjectType, KnownCityObjectType};
79/// # let t = &CityObjectType::Building;
80/// match *t {
81///     CityObjectType::Building => { /* .. */ }
82///     CityObjectType::Extension(ref s) => { let _ = s; }
83///     _ => {}
84/// }
85/// ```
86///
87/// Note the `*`. Matching a `const` path against a `&CityObjectType` -- ie
88/// `match t` where `t: &CityObjectType`, or `match &t` -- is `E0308`
89/// ("mismatched types"), and rustc's fix-it suggests renaming the arm to a
90/// fresh binding. That *compiles*, and silently turns the arm into a
91/// catch-all that matches everything, because `Building` in pattern position
92/// reads as a new irrefutable binding rather than as the constant. The `_`
93/// arm is still required after `Extension(ref s)`: the match is not
94/// exhaustive over just the known names, since `Known` and `Extension` are
95/// the real variants.
96#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
97#[serde(untagged)]
98pub enum CityObjectType {
99    Known(KnownCityObjectType),
100    /// Not a core CityJSON type: a CityJSON Extension type, always spelled
101    /// with a leading `"+"`.
102    Extension(String),
103}
104
105/// Flat, spec-spelling access to each known variant (eg
106/// `CityObjectType::BuildingPart`), so call sites read exactly like the
107/// CityJSON type name rather than `CityObjectType::Known(KnownCityObjectType::BuildingPart)`.
108/// These are associated `const`s, not enum variants -- `CityObjectType`
109/// itself has exactly the two variants above -- but a `const` path is usable
110/// wherever a variant path would be: in expressions (`x == CityObjectType::Bridge`)
111/// and, because the type derives `PartialEq`/`Eq`, in match patterns too --
112/// but only when matching on a *value*. See
113/// [`CityObjectType`][CityObjectType#match-on-the-value-not-on-a-reference]
114/// for why that matters and how it fails silently otherwise.
115#[allow(non_upper_case_globals)]
116impl CityObjectType {
117    pub const Bridge: CityObjectType = CityObjectType::Known(KnownCityObjectType::Bridge);
118    pub const BridgePart: CityObjectType = CityObjectType::Known(KnownCityObjectType::BridgePart);
119    pub const BridgeInstallation: CityObjectType =
120        CityObjectType::Known(KnownCityObjectType::BridgeInstallation);
121    pub const BridgeConstructiveElement: CityObjectType =
122        CityObjectType::Known(KnownCityObjectType::BridgeConstructiveElement);
123    pub const BridgeRoom: CityObjectType = CityObjectType::Known(KnownCityObjectType::BridgeRoom);
124    pub const BridgeFurniture: CityObjectType =
125        CityObjectType::Known(KnownCityObjectType::BridgeFurniture);
126    pub const Building: CityObjectType = CityObjectType::Known(KnownCityObjectType::Building);
127    pub const BuildingPart: CityObjectType =
128        CityObjectType::Known(KnownCityObjectType::BuildingPart);
129    pub const BuildingInstallation: CityObjectType =
130        CityObjectType::Known(KnownCityObjectType::BuildingInstallation);
131    pub const BuildingConstructiveElement: CityObjectType =
132        CityObjectType::Known(KnownCityObjectType::BuildingConstructiveElement);
133    pub const BuildingFurniture: CityObjectType =
134        CityObjectType::Known(KnownCityObjectType::BuildingFurniture);
135    pub const BuildingStorey: CityObjectType =
136        CityObjectType::Known(KnownCityObjectType::BuildingStorey);
137    pub const BuildingRoom: CityObjectType =
138        CityObjectType::Known(KnownCityObjectType::BuildingRoom);
139    pub const BuildingUnit: CityObjectType =
140        CityObjectType::Known(KnownCityObjectType::BuildingUnit);
141    pub const CityFurniture: CityObjectType =
142        CityObjectType::Known(KnownCityObjectType::CityFurniture);
143    pub const CityObjectGroup: CityObjectType =
144        CityObjectType::Known(KnownCityObjectType::CityObjectGroup);
145    pub const GenericCityObject: CityObjectType =
146        CityObjectType::Known(KnownCityObjectType::GenericCityObject);
147    pub const LandUse: CityObjectType = CityObjectType::Known(KnownCityObjectType::LandUse);
148    pub const OtherConstruction: CityObjectType =
149        CityObjectType::Known(KnownCityObjectType::OtherConstruction);
150    pub const PlantCover: CityObjectType = CityObjectType::Known(KnownCityObjectType::PlantCover);
151    pub const SolitaryVegetationObject: CityObjectType =
152        CityObjectType::Known(KnownCityObjectType::SolitaryVegetationObject);
153    pub const TINRelief: CityObjectType = CityObjectType::Known(KnownCityObjectType::TINRelief);
154    pub const Road: CityObjectType = CityObjectType::Known(KnownCityObjectType::Road);
155    pub const Railway: CityObjectType = CityObjectType::Known(KnownCityObjectType::Railway);
156    pub const Waterway: CityObjectType = CityObjectType::Known(KnownCityObjectType::Waterway);
157    pub const TransportSquare: CityObjectType =
158        CityObjectType::Known(KnownCityObjectType::TransportSquare);
159    pub const Tunnel: CityObjectType = CityObjectType::Known(KnownCityObjectType::Tunnel);
160    pub const TunnelPart: CityObjectType = CityObjectType::Known(KnownCityObjectType::TunnelPart);
161    pub const TunnelInstallation: CityObjectType =
162        CityObjectType::Known(KnownCityObjectType::TunnelInstallation);
163    pub const TunnelConstructiveElement: CityObjectType =
164        CityObjectType::Known(KnownCityObjectType::TunnelConstructiveElement);
165    pub const TunnelHollowSpace: CityObjectType =
166        CityObjectType::Known(KnownCityObjectType::TunnelHollowSpace);
167    pub const TunnelFurniture: CityObjectType =
168        CityObjectType::Known(KnownCityObjectType::TunnelFurniture);
169    pub const WaterBody: CityObjectType = CityObjectType::Known(KnownCityObjectType::WaterBody);
170}
171
172#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
173pub struct CityObject {
174    #[serde(rename = "type")]
175    pub thetype: CityObjectType,
176    #[serde(rename = "geographicalExtent")]
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub geographical_extent: Option<GeographicalExtent>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub attributes: Option<Value>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub geometry: Option<Vec<Geometry>>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub children: Option<Vec<String>>,
185    /// § 2.5 CityObjectGroup only: the role of each id in `children`, in the
186    /// same order. `cityobjects.schema.json` types each item as
187    /// `["string", "null"]`, so an unspecified role is `null`, not simply
188    /// absent from the array.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub children_roles: Option<Vec<Option<String>>>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub parents: Option<Vec<String>>,
193    /// Members the schema does not name, carried verbatim so they survive a
194    /// round trip. `pub`, like every other `#[serde(flatten)]` catch-all in
195    /// this crate, so a `CityObject` can be written as a struct literal.
196    #[serde(flatten)]
197    pub other: serde_json::Value,
198}
199
200impl CityObject {
201    /// A City Object of `thetype` carrying nothing else.
202    ///
203    /// `other` is deliberately not a constructor parameter: it is the
204    /// `#[serde(flatten)]` catch-all for members the schema does not name, and
205    /// a value built in code has none. Callers that need one set the named
206    /// fields directly, which are all `pub`.
207    ///
208    /// This is a convenience, not a necessity: every field is `pub`, so a
209    /// struct literal works too. It saves writing seven `None`s and getting
210    /// the `other` sentinel right (see below).
211    ///
212    /// `other` is an empty *object*, not `Value::Null`: that is what
213    /// deserializing a City Object with no unrecognized members produces, and
214    /// the derived `PartialEq` compares it. A `Null` here would serialize
215    /// identically but make every constructed object compare unequal to its
216    /// parsed twin -- silently, and only ever noticed by a test that compares
217    /// whole objects.
218    pub fn new(thetype: CityObjectType) -> Self {
219        CityObject {
220            thetype,
221            geographical_extent: None,
222            attributes: None,
223            geometry: None,
224            children: None,
225            children_roles: None,
226            parents: None,
227            other: Value::Object(serde_json::Map::new()),
228        }
229    }
230
231    pub fn get_type(&self) -> CityObjectType {
232        self.thetype.clone()
233    }
234    pub(crate) fn is_toplevel(&self) -> bool {
235        match &self.parents {
236            Some(x) => {
237                if x.is_empty() {
238                    return true;
239                } else {
240                    return false;
241                }
242            }
243            None => return true,
244        }
245    }
246    pub(crate) fn get_children_keys(&self) -> Vec<String> {
247        let mut re: Vec<String> = Vec::new();
248        match &self.children {
249            Some(x) => {
250                for each in x {
251                    re.push(each.to_string());
252                }
253            }
254            None => (),
255        }
256        re
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    /// A constructed City Object must equal the one deserializing the
265    /// equivalent minimal JSON produces. Serializing both and comparing the
266    /// output is not enough: `other: Value::Null` and `other: Value::Object({})`
267    /// both serialize to nothing, so only `==` distinguishes them -- and `==`
268    /// is what a caller round-tripping a document through its own encoder will
269    /// reach for.
270    #[test]
271    fn a_constructed_city_object_equals_its_parsed_twin() {
272        let constructed = CityObject::new(CityObjectType::Building);
273        let parsed: CityObject = serde_json::from_str(r#"{"type":"Building"}"#).unwrap();
274        assert_eq!(constructed, parsed);
275        assert_eq!(
276            serde_json::to_value(&constructed).unwrap(),
277            serde_json::json!({"type": "Building"})
278        );
279
280        // And the same for an Extension type, whose name lives in the tag
281        // rather than in `other`.
282        let constructed = CityObject::new(CityObjectType::Extension("+Noise".into()));
283        let parsed: CityObject = serde_json::from_str(r#"{"type":"+Noise"}"#).unwrap();
284        assert_eq!(constructed, parsed);
285    }
286
287    #[test]
288    fn extension_city_object_type_roundtrips_with_its_plus() {
289        let t: CityObjectType =
290            serde_json::from_value(serde_json::json!("+NoiseCityFurnitureSegment")).unwrap();
291        assert_eq!(
292            t,
293            CityObjectType::Extension("+NoiseCityFurnitureSegment".into())
294        );
295        assert_eq!(
296            serde_json::to_value(&t).unwrap(),
297            serde_json::json!("+NoiseCityFurnitureSegment")
298        );
299    }
300
301    #[test]
302    fn known_city_object_type_is_a_unit_variant() {
303        let t: CityObjectType = serde_json::from_value(serde_json::json!("BuildingPart")).unwrap();
304        assert_eq!(t, CityObjectType::BuildingPart);
305    }
306
307    /// A known name prefixed with `+` is not the known variant: extensions are
308    /// spelled with a leading `+` precisely so they never collide with a core
309    /// type, and ordering (known arm declared first) must not accidentally
310    /// swallow it either.
311    #[test]
312    fn plus_prefixed_known_name_lands_in_extension_not_the_known_variant() {
313        let t: CityObjectType = serde_json::from_value(serde_json::json!("+Building")).unwrap();
314        assert_eq!(t, CityObjectType::Extension("+Building".into()));
315        assert_ne!(t, CityObjectType::Building);
316    }
317
318    /// The stated critical requirement names a specific shape: a `CityObject`
319    /// (inside a `CityJSONFeature`, since that is how `flatcitybuf`'s
320    /// `noise_extension` fixture is shaped) whose `type` is an Extension
321    /// string, where `CityObjectType` sits behind `CityObject`'s
322    /// `#[serde(flatten)] other` field. The bare-`CityObjectType` tests above
323    /// don't exercise that flatten path at all, so this pins it at the
324    /// document level, asserting on the serialized *string* -- not a
325    /// re-parsed `Value` -- so a change that reorders or reformats output
326    /// would fail this test even though it would pass a structural check.
327    ///
328    /// The extra `"+noiseLevel"` member is deliberate and load-bearing: it is
329    /// *not* `"attributes"` (that member has its own dedicated,
330    /// already-typed `CityObject` field and so would never touch `other` at
331    /// all) but a genuinely unrecognized top-level member, so it is the one
332    /// that actually exercises `#[serde(flatten)] other: serde_json::Value`.
333    /// It carries a nested object (not a bare scalar) to prove nested
334    /// content survives too, and there is exactly one such extra member,
335    /// because `other`'s `serde_json::Map` has no stable iteration order
336    /// once it holds two or more keys.
337    #[test]
338    fn extension_typed_city_object_roundtrips_byte_for_byte_inside_a_feature() {
339        let input = concat!(
340            r#"{"type":"CityJSONFeature","id":"id-1","#,
341            r#""CityObjects":{"id-1":{"type":"+NoiseCityFurnitureSegment","#,
342            r#""+noiseLevel":{"dB":55}}},"#,
343            r#""vertices":[]}"#
344        );
345        let parsed: crate::cityjson::CityJSONFeature = serde_json::from_str(input).unwrap();
346        assert_eq!(
347            parsed.city_objects["id-1"].thetype,
348            CityObjectType::Extension("+NoiseCityFurnitureSegment".into())
349        );
350        let reserialized = serde_json::to_string(&parsed).unwrap();
351        assert_eq!(
352            reserialized, input,
353            "an Extension-typed CityObject with an extra flattened member inside a \
354             CityJSONFeature must round-trip byte-for-byte"
355        );
356    }
357
358    /// Every public type in this crate derives `PartialEq`; `CityObject` was
359    /// missing it.
360    #[test]
361    fn city_object_with_equal_fields_is_equal() {
362        let input = serde_json::json!({"type": "Building"});
363        let a: CityObject = serde_json::from_value(input.clone()).unwrap();
364        let b: CityObject = serde_json::from_value(input).unwrap();
365        assert_eq!(a, b);
366        let mut c = b.clone();
367        c.children = Some(vec!["id-2".to_string()]);
368        assert_ne!(a, c);
369    }
370
371    /// `cityobjects.schema.json`'s `_AbstractCityObject.geographicalExtent`
372    /// has `minItems`/`maxItems` 6 -- a fixed-length array, not an arbitrary
373    /// `Vec<f64>` -- so a 5- or 7-element extent must fail to deserialize
374    /// rather than being silently accepted.
375    #[test]
376    fn city_object_geographical_extent_rejects_the_wrong_length() {
377        let too_short = serde_json::json!({
378            "type": "Building",
379            "geographicalExtent": [0.0, 0.0, 0.0, 1.0, 1.0]
380        });
381        assert!(serde_json::from_value::<CityObject>(too_short).is_err());
382
383        let too_long = serde_json::json!({
384            "type": "Building",
385            "geographicalExtent": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0]
386        });
387        assert!(serde_json::from_value::<CityObject>(too_long).is_err());
388    }
389
390    #[test]
391    fn city_object_geographical_extent_round_trips_as_a_six_element_array() {
392        let input = serde_json::json!({
393            "type": "Building",
394            "geographicalExtent": [84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9]
395        });
396        let co: CityObject = serde_json::from_value(input.clone()).unwrap();
397        assert_eq!(
398            co.geographical_extent,
399            Some([84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9])
400        );
401        assert_eq!(serde_json::to_value(&co).unwrap(), input);
402    }
403
404    /// § 2.5 CityObjectGroup: a group `CityObject` may carry a `children_roles`
405    /// member, one entry per id in `children`, describing that child's role
406    /// in the group. `cityobjects.schema.json`'s `CityObjectGroup` types each
407    /// item as `["string", "null"]`, so an unspecified role is `null`, not an
408    /// absent array entry -- this must round-trip too.
409    #[test]
410    fn city_object_group_children_roles_round_trips_including_null_entries() {
411        let input = serde_json::json!({
412            "type": "CityObjectGroup",
413            "children": ["building1", "building3"],
414            "children_roles": ["residential building", null]
415        });
416        let co: CityObject = serde_json::from_value(input.clone()).unwrap();
417        assert_eq!(
418            co.children_roles,
419            Some(vec![Some("residential building".to_string()), None])
420        );
421        assert_eq!(serde_json::to_value(&co).unwrap(), input);
422    }
423
424    /// Every known CityObjectType name round-trips through its unit variant,
425    /// not through Extension -- a typo'd rename would otherwise silently fall
426    /// through to Extension and no other test would catch it.
427    #[test]
428    fn every_known_city_object_type_round_trips_as_its_unit_variant() {
429        let known: &[(&str, CityObjectType)] = &[
430            ("Bridge", CityObjectType::Bridge),
431            ("BridgePart", CityObjectType::BridgePart),
432            ("BridgeInstallation", CityObjectType::BridgeInstallation),
433            (
434                "BridgeConstructiveElement",
435                CityObjectType::BridgeConstructiveElement,
436            ),
437            ("BridgeRoom", CityObjectType::BridgeRoom),
438            ("BridgeFurniture", CityObjectType::BridgeFurniture),
439            ("Building", CityObjectType::Building),
440            ("BuildingPart", CityObjectType::BuildingPart),
441            ("BuildingInstallation", CityObjectType::BuildingInstallation),
442            (
443                "BuildingConstructiveElement",
444                CityObjectType::BuildingConstructiveElement,
445            ),
446            ("BuildingFurniture", CityObjectType::BuildingFurniture),
447            ("BuildingStorey", CityObjectType::BuildingStorey),
448            ("BuildingRoom", CityObjectType::BuildingRoom),
449            ("BuildingUnit", CityObjectType::BuildingUnit),
450            ("CityFurniture", CityObjectType::CityFurniture),
451            ("CityObjectGroup", CityObjectType::CityObjectGroup),
452            ("GenericCityObject", CityObjectType::GenericCityObject),
453            ("LandUse", CityObjectType::LandUse),
454            ("OtherConstruction", CityObjectType::OtherConstruction),
455            ("PlantCover", CityObjectType::PlantCover),
456            (
457                "SolitaryVegetationObject",
458                CityObjectType::SolitaryVegetationObject,
459            ),
460            ("TINRelief", CityObjectType::TINRelief),
461            ("Road", CityObjectType::Road),
462            ("Railway", CityObjectType::Railway),
463            ("Waterway", CityObjectType::Waterway),
464            ("TransportSquare", CityObjectType::TransportSquare),
465            ("Tunnel", CityObjectType::Tunnel),
466            ("TunnelPart", CityObjectType::TunnelPart),
467            ("TunnelInstallation", CityObjectType::TunnelInstallation),
468            (
469                "TunnelConstructiveElement",
470                CityObjectType::TunnelConstructiveElement,
471            ),
472            ("TunnelHollowSpace", CityObjectType::TunnelHollowSpace),
473            ("TunnelFurniture", CityObjectType::TunnelFurniture),
474            ("WaterBody", CityObjectType::WaterBody),
475        ];
476        for (name, expected) in known {
477            let parsed: CityObjectType = serde_json::from_value(serde_json::json!(name)).unwrap();
478            assert_eq!(&parsed, expected, "{name} did not parse to its own variant");
479            assert_eq!(
480                serde_json::to_value(&parsed).unwrap(),
481                serde_json::json!(name),
482                "{name} did not round-trip back to its own string"
483            );
484        }
485    }
486}