Skip to main content

river_data_core/models/
mod.rs

1mod annotations;
2mod backend;
3mod config;
4mod measurement;
5mod protocol;
6mod replicates;
7mod status;
8mod streams;
9mod vocabulary;
10
11pub use annotations::{AnnotationMapping, AnnotationUpsert, NoteMapping, NoteUpsert};
12pub use backend::{
13    DeclinedChannel, SourceCandidate, SourceInventory, SourceWindow, StreamDescriptor,
14    StreamFetchRequest, StreamReadings, StreamStatusEvents,
15};
16pub use config::RunnerConfig;
17pub use measurement::MeasurementType;
18pub use protocol::{
19    CommandUpdateRequest, EnrollRequest, EnrollResponse, HeartbeatRequest, HeartbeatResponse,
20    PendingCommand, SyncEventCreate, SyncEventRef, SyncEventUpdate, SyncResult, SyncTrigger,
21};
22pub use replicates::{
23    ColumnAssignment, CurveMapping, GroupAudit, ReplicateSpec, SensorUpsert, StandardCurveUpsert,
24};
25pub use status::{CommandStatus, ServiceStatus, SyncEventStatus, SyncEventType};
26pub use streams::{
27    DataStream, IngestReading, IngestStatusEvent, InstrumentGranularity, RegisterStreamRequest,
28};
29pub use vocabulary::UnknownValue;
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    /// Which shapes refuse a field the sender gained. The API receives all nine and its own
36    /// structs split the same way: a receiver that refuses answers a version skew with a 400,
37    /// one that does not drops the field and stores a row missing what the source sent.
38    #[test]
39    fn the_shapes_that_refuse_an_unknown_field() {
40        fn refuses<D: serde::de::DeserializeOwned>(mut json: serde_json::Value) -> bool {
41            json["a_field_the_sender_gained"] = serde_json::json!(1);
42            serde_json::from_value::<D>(json).is_err()
43        }
44
45        assert!(refuses::<IngestReading>(serde_json::json!({
46            "time": "2026-01-15T10:00:00Z", "raw_value": 1.0
47        })));
48        assert!(refuses::<IngestStatusEvent>(serde_json::json!({
49            "time": "2026-01-15T10:00:00Z", "value": "unreachable"
50        })));
51        assert!(refuses::<SourceWindow>(serde_json::json!({
52            "from": "2026-01-01T00:00:00Z", "to": "2026-02-01T00:00:00Z", "source_rows_read": 1
53        })));
54        assert!(refuses::<GroupAudit>(serde_json::json!({
55            "time": "2026-01-15T10:00:00Z"
56        })));
57        assert!(refuses::<SensorUpsert>(serde_json::json!({
58            "source_key": "sensor_inventory:62", "name": "DOC corr", "is_lab_instrument": true
59        })));
60        assert!(refuses::<AnnotationUpsert>(serde_json::json!({
61            "source_key": "annotations:9",
62            "stream_id": "00000000-0000-0000-0000-000000000006",
63            "time": "2026-01-15T10:00:00Z", "category": "audit", "text": "x"
64        })));
65        assert!(refuses::<NoteUpsert>(serde_json::json!({
66            "source_key": "notes:1", "site_name": "FP1", "text": "x", "verified": true
67        })));
68
69        assert!(!refuses::<RegisterStreamRequest>(serde_json::json!({
70            "source_system": "cnet", "source_key": "FP1:DOC",
71            "source_name": null, "source_path": null, "metadata": {}
72        })));
73        assert!(!refuses::<StandardCurveUpsert>(serde_json::json!({
74            "source_key": "standard_curves:17", "instrument_label": "DOC corr",
75            "slope": 1.0, "intercept": 0.0
76        })));
77        assert!(!refuses::<ColumnAssignment>(serde_json::json!({
78            "column": "DOC_A", "index": 0
79        })));
80    }
81
82    /// The three fields the API accepts and this crate could not express: a synced instrument's
83    /// cadence, a portal curve's note, and the instrument a status event describes.
84    #[test]
85    fn the_fields_the_api_accepts_travel() {
86        let instrument = SensorUpsert {
87            source_key: "sensor_inventory:62".to_string(),
88            name: "DOC corr".to_string(),
89            serial_number: None,
90            manufacturer: None,
91            model: None,
92            notes: None,
93            is_lab_instrument: true,
94            data_frequency: Some("low".to_string()),
95            metadata: None,
96        };
97        let json = serde_json::to_value(&instrument).unwrap();
98        assert_eq!(json["data_frequency"], "low");
99
100        let curve = StandardCurveUpsert {
101            source_key: "standard_curves:17".to_string(),
102            instrument_label: "DOC corr".to_string(),
103            slope: 1.0,
104            intercept: 0.0,
105            r_squared: None,
106            name: None,
107            fitted_on: None,
108            notes: Some("re-fitted after the lamp change".to_string()),
109        };
110        let json = serde_json::to_value(&curve).unwrap();
111        assert_eq!(json["notes"], "re-fitted after the lamp change");
112
113        let event = IngestStatusEvent {
114            time: chrono::Utc::now(),
115            value: "unreachable".to_string(),
116            sensor_id: Some(uuid::Uuid::nil()),
117        };
118        let json = serde_json::to_value(&event).unwrap();
119        assert_eq!(json["sensor_id"], uuid::Uuid::nil().to_string());
120    }
121
122    /// A source that declares none of the three sends none of them, so an API that predates them
123    /// reads exactly what it read before.
124    #[test]
125    fn an_undeclared_field_is_not_sent() {
126        let instrument = SensorUpsert {
127            source_key: "sensor_inventory:62".to_string(),
128            name: "DOC corr".to_string(),
129            serial_number: None,
130            manufacturer: None,
131            model: None,
132            notes: None,
133            is_lab_instrument: false,
134            data_frequency: None,
135            metadata: None,
136        };
137        let json = serde_json::to_value(&instrument).unwrap();
138        assert!(json.get("data_frequency").is_none());
139
140        let event = IngestStatusEvent {
141            time: chrono::Utc::now(),
142            value: "ok".to_string(),
143            sensor_id: None,
144        };
145        let json = serde_json::to_value(&event).unwrap();
146        assert!(json.get("sensor_id").is_none());
147    }
148}