Skip to main content

spatialrust_records/
record.rs

1//! Owned spatial record envelope.
2
3use spatialrust_core::{PointCloud, SpatialMetadata};
4
5use crate::{RecordProvenance, RecordsError, RecordsResult, SchemaDescriptor};
6
7/// One versioned point-cloud observation with attached metadata.
8#[derive(Clone, Debug, PartialEq)]
9pub struct SpatialRecord {
10    schema: SchemaDescriptor,
11    cloud: PointCloud,
12    provenance: RecordProvenance,
13}
14
15impl SpatialRecord {
16    /// Creates a record after validating that the cloud matches the descriptor schema.
17    pub fn try_new(schema: SchemaDescriptor, cloud: PointCloud) -> RecordsResult<Self> {
18        Self::try_new_with_provenance(schema, cloud, RecordProvenance::default())
19    }
20
21    /// Creates a record with explicit source lineage after schema validation.
22    pub fn try_new_with_provenance(
23        schema: SchemaDescriptor,
24        cloud: PointCloud,
25        provenance: RecordProvenance,
26    ) -> RecordsResult<Self> {
27        provenance.validate()?;
28        if cloud.schema() != schema.point_schema() {
29            return Err(RecordsError::SchemaMismatch(
30                "point cloud schema must equal the record schema descriptor".into(),
31            ));
32        }
33        cloud.validate()?;
34        Ok(Self { schema, cloud, provenance })
35    }
36
37    /// Builds a record from a cloud using an explicit schema id/version.
38    pub fn try_from_cloud(
39        id: impl Into<crate::SchemaId>,
40        version: crate::SchemaVersion,
41        cloud: PointCloud,
42    ) -> RecordsResult<Self> {
43        Self::try_from_cloud_with_provenance(id, version, cloud, RecordProvenance::default())
44    }
45
46    /// Builds a record from a cloud and explicit source lineage.
47    pub fn try_from_cloud_with_provenance(
48        id: impl Into<crate::SchemaId>,
49        version: crate::SchemaVersion,
50        cloud: PointCloud,
51        provenance: RecordProvenance,
52    ) -> RecordsResult<Self> {
53        let schema = SchemaDescriptor::try_new(id, version, cloud.schema().clone())?;
54        Self::try_new_with_provenance(schema, cloud, provenance)
55    }
56
57    /// Returns the schema descriptor.
58    #[must_use]
59    pub fn schema(&self) -> &SchemaDescriptor {
60        &self.schema
61    }
62
63    /// Returns the owned point cloud.
64    #[must_use]
65    pub fn cloud(&self) -> &PointCloud {
66        &self.cloud
67    }
68
69    /// Returns source lineage attached to this record.
70    #[must_use]
71    pub fn provenance(&self) -> &RecordProvenance {
72        &self.provenance
73    }
74
75    /// Replaces source lineage without changing schema or cloud ownership.
76    #[must_use]
77    pub fn with_provenance(mut self, provenance: RecordProvenance) -> Self {
78        self.provenance = provenance;
79        self
80    }
81
82    /// Consumes the record into schema, cloud, and source lineage.
83    #[must_use]
84    pub fn into_parts(self) -> (SchemaDescriptor, PointCloud, RecordProvenance) {
85        (self.schema, self.cloud, self.provenance)
86    }
87
88    /// Consumes the record into its cloud.
89    #[must_use]
90    pub fn into_cloud(self) -> PointCloud {
91        self.cloud
92    }
93
94    /// Returns spatial metadata attached to the cloud.
95    #[must_use]
96    pub fn metadata(&self) -> &SpatialMetadata {
97        self.cloud.metadata()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::SpatialRecord;
104    use crate::SchemaVersion;
105    use spatialrust_core::{
106        PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas,
107    };
108
109    #[test]
110    fn record_rejects_schema_mismatch() {
111        let mut buffers = PointBufferSet::new();
112        buffers.insert("x", PointBuffer::from_f32(vec![0.0]));
113        buffers.insert("y", PointBuffer::from_f32(vec![0.0]));
114        buffers.insert("z", PointBuffer::from_f32(vec![0.0]));
115        let cloud = PointCloud::try_from_parts(
116            StandardSchemas::point_xyz(),
117            buffers,
118            SpatialMetadata::default(),
119        )
120        .unwrap();
121        let rich = SpatialRecord::try_from_cloud("p", SchemaVersion::new(1, 0), {
122            let mut buffers = PointBufferSet::new();
123            buffers.insert("x", PointBuffer::from_f32(vec![0.0]));
124            buffers.insert("y", PointBuffer::from_f32(vec![0.0]));
125            buffers.insert("z", PointBuffer::from_f32(vec![0.0]));
126            buffers.insert("intensity", PointBuffer::from_f32(vec![1.0]));
127            PointCloud::try_from_parts(
128                StandardSchemas::point_xyzi(),
129                buffers,
130                SpatialMetadata::default(),
131            )
132            .unwrap()
133        });
134        assert!(rich.is_ok());
135        let _ = cloud;
136    }
137
138    #[test]
139    fn record_preserves_explicit_provenance() {
140        let mut buffers = PointBufferSet::new();
141        buffers.insert("x", PointBuffer::from_f32(vec![0.0]));
142        buffers.insert("y", PointBuffer::from_f32(vec![0.0]));
143        buffers.insert("z", PointBuffer::from_f32(vec![0.0]));
144        let cloud = PointCloud::try_from_parts(
145            StandardSchemas::point_xyz(),
146            buffers,
147            SpatialMetadata::default(),
148        )
149        .unwrap();
150        let provenance = crate::RecordProvenance::try_new("source-1")
151            .unwrap()
152            .with_stream_id("lidar")
153            .with_sequence(Some(2));
154        let record = SpatialRecord::try_from_cloud_with_provenance(
155            "point",
156            SchemaVersion::new(1, 0),
157            cloud,
158            provenance.clone(),
159        )
160        .unwrap();
161        assert_eq!(record.provenance(), &provenance);
162        assert_eq!(record.with_provenance(provenance.clone()).provenance(), &provenance);
163    }
164}