Skip to main content

dsp_cli/model/
data_model.rs

1//! Data-model domain shape — shared across the client → action boundary.
2//!
3//! Types here are the dsp-cli vocabulary for data-model data. DSP-API wire types
4//! live inside `src/client/http.rs` and are never exposed above the client
5//! layer. See ADR-0001 and ADR-0008.
6
7/// A data-model as shown by `dsp vre data-model list`.
8///
9/// The list projection: identity (`name` + `iri`) plus the server's `label`
10/// and `last_modified` metadata, and whether it is a platform **built-in**
11/// (inherited by every project) rather than project-defined. `name` is derived
12/// from the IRI at the client boundary — not a wire field. No `serde` derive:
13/// wire deserialization stays in `src/client/http.rs`. See ADR-0001 / ADR-0008
14/// and the CONTEXT.md "Data Model" / "Built-in" entries.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DataModel {
17    /// Short name of the data-model (e.g. `beol`), derived from the IRI.
18    pub name: String,
19    /// Full IRI of the data-model (e.g. `http://api.dasch.swiss/ontology/0801/beol/v2`).
20    pub iri: String,
21    /// Server-supplied human label (`rdfs:label`), if any.
22    pub label: Option<String>,
23    /// Last-modification timestamp as a raw RFC3339 string, if the server
24    /// supplies one. Built-ins carry `None`.
25    pub last_modified: Option<String>,
26    /// `true` for platform built-ins (knora-api, standoff, salsah-gui);
27    /// `false` for project-defined data-models.
28    pub is_builtin: bool,
29}
30
31/// A lean reference to a child resource-type, as surfaced by
32/// `data-model describe`.
33///
34/// The describe-summary projection: identity (`name` + `iri`) plus the server's
35/// human `label`. Full resource-type detail (fields, value-types, cardinalities)
36/// is surfaced by the `dsp vre resource-type describe` leaf command — see
37/// `model::ResourceTypeDetail` / `Field`.
38/// `name` is derived from the resource-type's IRI by the HTTP client layer at the
39/// ADR-0001 boundary — it is NOT a server-supplied field. No `serde` derive: wire
40/// deserialization stays in `src/client/http.rs`.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ResourceTypeSummary {
43    /// Short name of the resource-type (e.g. `letter`), derived from the IRI.
44    pub name: String,
45    /// Full IRI of the resource-type
46    /// (e.g. `http://api.dasch.swiss/ontology/0801/beol/v2#letter`).
47    pub iri: String,
48    /// Server-supplied human label (`rdfs:label`), if any.
49    pub label: Option<String>,
50}
51
52/// A resource-type as shown by `dsp vre resource-type list` — the list projection.
53///
54/// Contrast `ResourceTypeSummary` (the lean describe-child inside `DataModelDetail`,
55/// which has no `is_builtin` concept because `data-model describe` only ever shows a
56/// project ontology's own classes). This type adds `is_builtin` so the list command
57/// can mix project-defined and platform built-in resource-types (Region, AudioSegment,
58/// VideoSegment, LinkObj) when `--include-builtins` is supplied, and callers can
59/// discriminate the two kinds. `name` is derived from the IRI fragment at the client
60/// boundary — not a wire field. No `serde` derive: wire deserialization stays in
61/// `src/client/http.rs`. See ADR-0001 / ADR-0008 and the CONTEXT.md "Resource Type" /
62/// "Built-in" entries.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct ResourceType {
65    /// Short name of the resource-type (e.g. `letter`), derived from the IRI fragment.
66    pub name: String,
67    /// Full IRI of the resource-type
68    /// (project: `http://api.dasch.swiss/ontology/0801/beol/v2#letter`;
69    /// built-in: `http://api.knora.org/ontology/knora-api/v2#Region`).
70    pub iri: String,
71    /// Server-supplied human label (`rdfs:label`), if any. All 4 platform built-ins
72    /// carry labels (Region, Audio Annotation, Video Annotation, Link Object).
73    pub label: Option<String>,
74    /// `true` for platform built-ins (knora-api: Region, AudioSegment, VideoSegment,
75    /// LinkObj); `false` for project-defined resource-types.
76    pub is_builtin: bool,
77}
78
79/// A data-model as shown by `dsp vre data-model describe` — the rich projection.
80///
81/// Contrast `DataModel` (the lean `list` index projection). Carries identity
82/// (`name` + `iri`), the server's `label` and `last_modified`, and a **summary**
83/// (count + names) of the data-model's child resource-types — NOT their fields.
84/// No `serde` derive: wire deserialization stays in `src/client/http.rs`.
85/// See ADR-0001 / ADR-0008 and the CONTEXT.md "Data Model" / "Resource Type" entries.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct DataModelDetail {
88    /// Short name of the data-model (e.g. `beol`), derived from the IRI.
89    pub name: String,
90    /// Full IRI of the data-model.
91    pub iri: String,
92    /// Server-supplied human label (`rdfs:label`), if any.
93    pub label: Option<String>,
94    /// Last-modification timestamp as a raw RFC3339 string, if supplied.
95    pub last_modified: Option<String>,
96    /// Lean references to the data-model's child resource-types, sorted by name.
97    pub resource_types: Vec<ResourceTypeSummary>,
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn data_model_full_construction_and_equality() {
106        let dm = DataModel {
107            name: "beol".into(),
108            iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
109            label: Some("The BEOL data-model".into()),
110            last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
111            is_builtin: false,
112        };
113        let cloned = dm.clone();
114        assert_eq!(dm, cloned);
115        assert_eq!(dm.name, "beol");
116        assert_eq!(dm.iri, "http://api.dasch.swiss/ontology/0801/beol/v2");
117        assert_eq!(dm.label.as_deref(), Some("The BEOL data-model"));
118        assert_eq!(
119            dm.last_modified.as_deref(),
120            Some("2024-05-27T13:43:26.233048Z")
121        );
122        assert!(!dm.is_builtin);
123    }
124
125    #[test]
126    fn data_model_all_none_builtin() {
127        let dm = DataModel {
128            name: "knora-api".into(),
129            iri: "http://api.knora.org/ontology/knora-api/v2".into(),
130            label: None,
131            last_modified: None,
132            is_builtin: true,
133        };
134        let cloned = dm.clone();
135        assert_eq!(dm, cloned);
136        assert_eq!(dm.name, "knora-api");
137        assert_eq!(dm.iri, "http://api.knora.org/ontology/knora-api/v2");
138        assert_eq!(dm.label, None);
139        assert_eq!(dm.last_modified, None);
140        assert!(dm.is_builtin);
141    }
142
143    #[test]
144    fn data_model_label_some_last_modified_none() {
145        let dm = DataModel {
146            name: "limc".into(),
147            iri: "http://api.dasch.swiss/ontology/0603/limc/v2".into(),
148            label: Some("LIMC data-model".into()),
149            last_modified: None,
150            is_builtin: false,
151        };
152        let cloned = dm.clone();
153        assert_eq!(dm, cloned);
154        assert_eq!(dm.label.as_deref(), Some("LIMC data-model"));
155        assert_eq!(dm.last_modified, None);
156        assert!(!dm.is_builtin);
157    }
158
159    // --- ResourceType tests ---
160
161    #[test]
162    fn resource_type_with_label_not_builtin() {
163        let rt = ResourceType {
164            name: "letter".into(),
165            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
166            label: Some("Letter".into()),
167            is_builtin: false,
168        };
169        let cloned = rt.clone();
170        assert_eq!(rt, cloned);
171        assert_eq!(rt.name, "letter");
172        assert_eq!(
173            rt.iri,
174            "http://api.dasch.swiss/ontology/0801/beol/v2#letter"
175        );
176        assert_eq!(rt.label.as_deref(), Some("Letter"));
177        assert!(!rt.is_builtin);
178    }
179
180    #[test]
181    fn resource_type_label_none_not_builtin() {
182        let rt = ResourceType {
183            name: "Archive".into(),
184            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
185            label: None,
186            is_builtin: false,
187        };
188        let cloned = rt.clone();
189        assert_eq!(rt, cloned);
190        assert_eq!(rt.name, "Archive");
191        assert_eq!(rt.label, None);
192        assert!(!rt.is_builtin);
193    }
194
195    #[test]
196    fn resource_type_with_label_builtin() {
197        let rt = ResourceType {
198            name: "Region".into(),
199            iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
200            label: Some("Region".into()),
201            is_builtin: true,
202        };
203        let cloned = rt.clone();
204        assert_eq!(rt, cloned);
205        assert_eq!(rt.name, "Region");
206        assert_eq!(rt.iri, "http://api.knora.org/ontology/knora-api/v2#Region");
207        assert_eq!(rt.label.as_deref(), Some("Region"));
208        assert!(rt.is_builtin);
209    }
210
211    #[test]
212    fn resource_type_label_none_builtin() {
213        // Edge-case: built-in with no label (shouldn't happen in practice, but the type
214        // must round-trip cleanly regardless).
215        let rt = ResourceType {
216            name: "LinkObj".into(),
217            iri: "http://api.knora.org/ontology/knora-api/v2#LinkObj".into(),
218            label: None,
219            is_builtin: true,
220        };
221        let cloned = rt.clone();
222        assert_eq!(rt, cloned);
223        assert_eq!(rt.name, "LinkObj");
224        assert_eq!(rt.label, None);
225        assert!(rt.is_builtin);
226    }
227
228    // --- ResourceTypeSummary tests ---
229
230    #[test]
231    fn resource_type_summary_with_label() {
232        let rt = ResourceTypeSummary {
233            name: "letter".into(),
234            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
235            label: Some("Letter".into()),
236        };
237        let cloned = rt.clone();
238        assert_eq!(rt, cloned);
239        assert_eq!(rt.name, "letter");
240        assert_eq!(
241            rt.iri,
242            "http://api.dasch.swiss/ontology/0801/beol/v2#letter"
243        );
244        assert_eq!(rt.label.as_deref(), Some("Letter"));
245    }
246
247    #[test]
248    fn resource_type_summary_without_label() {
249        let rt = ResourceTypeSummary {
250            name: "Archive".into(),
251            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
252            label: None,
253        };
254        let cloned = rt.clone();
255        assert_eq!(rt, cloned);
256        assert_eq!(rt.name, "Archive");
257        assert_eq!(
258            rt.iri,
259            "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
260        );
261        assert_eq!(rt.label, None);
262    }
263
264    // --- DataModelDetail tests ---
265
266    #[test]
267    fn data_model_detail_full_construction_and_equality() {
268        let detail = DataModelDetail {
269            name: "beol".into(),
270            iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
271            label: Some("The BEOL data-model".into()),
272            last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
273            resource_types: vec![
274                ResourceTypeSummary {
275                    name: "Archive".into(),
276                    iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
277                    label: Some("Archive".into()),
278                },
279                ResourceTypeSummary {
280                    name: "letter".into(),
281                    iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
282                    label: Some("Letter".into()),
283                },
284            ],
285        };
286        let cloned = detail.clone();
287        assert_eq!(detail, cloned);
288        assert_eq!(detail.name, "beol");
289        assert_eq!(detail.iri, "http://api.dasch.swiss/ontology/0801/beol/v2");
290        assert_eq!(detail.label.as_deref(), Some("The BEOL data-model"));
291        assert_eq!(
292            detail.last_modified.as_deref(),
293            Some("2024-05-27T13:43:26.233048Z")
294        );
295        assert_eq!(detail.resource_types.len(), 2);
296    }
297
298    #[test]
299    fn data_model_detail_label_none_last_modified_none() {
300        let detail = DataModelDetail {
301            name: "minimal".into(),
302            iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
303            label: None,
304            last_modified: None,
305            resource_types: vec![ResourceTypeSummary {
306                name: "Thing".into(),
307                iri: "http://api.dasch.swiss/ontology/0000/minimal/v2#Thing".into(),
308                label: None,
309            }],
310        };
311        let cloned = detail.clone();
312        assert_eq!(detail, cloned);
313        assert_eq!(detail.label, None);
314        assert_eq!(detail.last_modified, None);
315        assert_eq!(detail.resource_types.len(), 1);
316        assert_eq!(detail.resource_types[0].label, None);
317    }
318
319    #[test]
320    fn data_model_detail_empty_resource_types() {
321        let detail = DataModelDetail {
322            name: "empty".into(),
323            iri: "http://api.dasch.swiss/ontology/9999/empty/v2".into(),
324            label: Some("Empty data-model".into()),
325            last_modified: None,
326            resource_types: vec![],
327        };
328        let cloned = detail.clone();
329        assert_eq!(detail, cloned);
330        assert!(detail.resource_types.is_empty());
331        assert_eq!(detail.label.as_deref(), Some("Empty data-model"));
332        assert_eq!(detail.last_modified, None);
333    }
334}