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    /// Instance count from the v3 `resourcesPerOntology` route, populated only
78    /// when `--count` is passed to `resource-type list`. `None` when the flag was
79    /// not used, or when the class was absent from the v3 payload (e.g. a
80    /// built-in with `--include-builtins`).
81    pub count: Option<u64>,
82}
83
84/// A data-model as shown by `dsp vre data-model describe` — the rich projection.
85///
86/// Contrast `DataModel` (the lean `list` index projection). Carries identity
87/// (`name` + `iri`), the server's `label` and `last_modified`, and a **summary**
88/// (count + names) of the data-model's child resource-types — NOT their fields.
89/// No `serde` derive: wire deserialization stays in `src/client/http.rs`.
90/// See ADR-0001 / ADR-0008 and the CONTEXT.md "Data Model" / "Resource Type" entries.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct DataModelDetail {
93    /// Short name of the data-model (e.g. `beol`), derived from the IRI.
94    pub name: String,
95    /// Full IRI of the data-model.
96    pub iri: String,
97    /// Server-supplied human label (`rdfs:label`), if any.
98    pub label: Option<String>,
99    /// Last-modification timestamp as a raw RFC3339 string, if supplied.
100    pub last_modified: Option<String>,
101    /// Lean references to the data-model's child resource-types, sorted by name.
102    pub resource_types: Vec<ResourceTypeSummary>,
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn data_model_full_construction_and_equality() {
111        let dm = DataModel {
112            name: "beol".into(),
113            iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
114            label: Some("The BEOL data-model".into()),
115            last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
116            is_builtin: false,
117        };
118        let cloned = dm.clone();
119        assert_eq!(dm, cloned);
120        assert_eq!(dm.name, "beol");
121        assert_eq!(dm.iri, "http://api.dasch.swiss/ontology/0801/beol/v2");
122        assert_eq!(dm.label.as_deref(), Some("The BEOL data-model"));
123        assert_eq!(
124            dm.last_modified.as_deref(),
125            Some("2024-05-27T13:43:26.233048Z")
126        );
127        assert!(!dm.is_builtin);
128    }
129
130    #[test]
131    fn data_model_all_none_builtin() {
132        let dm = DataModel {
133            name: "knora-api".into(),
134            iri: "http://api.knora.org/ontology/knora-api/v2".into(),
135            label: None,
136            last_modified: None,
137            is_builtin: true,
138        };
139        let cloned = dm.clone();
140        assert_eq!(dm, cloned);
141        assert_eq!(dm.name, "knora-api");
142        assert_eq!(dm.iri, "http://api.knora.org/ontology/knora-api/v2");
143        assert_eq!(dm.label, None);
144        assert_eq!(dm.last_modified, None);
145        assert!(dm.is_builtin);
146    }
147
148    #[test]
149    fn data_model_label_some_last_modified_none() {
150        let dm = DataModel {
151            name: "limc".into(),
152            iri: "http://api.dasch.swiss/ontology/0603/limc/v2".into(),
153            label: Some("LIMC data-model".into()),
154            last_modified: None,
155            is_builtin: false,
156        };
157        let cloned = dm.clone();
158        assert_eq!(dm, cloned);
159        assert_eq!(dm.label.as_deref(), Some("LIMC data-model"));
160        assert_eq!(dm.last_modified, None);
161        assert!(!dm.is_builtin);
162    }
163
164    // --- ResourceType tests ---
165
166    #[test]
167    fn resource_type_with_label_not_builtin() {
168        let rt = ResourceType {
169            name: "letter".into(),
170            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
171            label: Some("Letter".into()),
172            is_builtin: false,
173            count: None,
174        };
175        let cloned = rt.clone();
176        assert_eq!(rt, cloned);
177        assert_eq!(rt.name, "letter");
178        assert_eq!(
179            rt.iri,
180            "http://api.dasch.swiss/ontology/0801/beol/v2#letter"
181        );
182        assert_eq!(rt.label.as_deref(), Some("Letter"));
183        assert!(!rt.is_builtin);
184    }
185
186    #[test]
187    fn resource_type_label_none_not_builtin() {
188        let rt = ResourceType {
189            name: "Archive".into(),
190            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
191            label: None,
192            is_builtin: false,
193            count: None,
194        };
195        let cloned = rt.clone();
196        assert_eq!(rt, cloned);
197        assert_eq!(rt.name, "Archive");
198        assert_eq!(rt.label, None);
199        assert!(!rt.is_builtin);
200    }
201
202    #[test]
203    fn resource_type_with_label_builtin() {
204        let rt = ResourceType {
205            name: "Region".into(),
206            iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
207            label: Some("Region".into()),
208            is_builtin: true,
209            count: None,
210        };
211        let cloned = rt.clone();
212        assert_eq!(rt, cloned);
213        assert_eq!(rt.name, "Region");
214        assert_eq!(rt.iri, "http://api.knora.org/ontology/knora-api/v2#Region");
215        assert_eq!(rt.label.as_deref(), Some("Region"));
216        assert!(rt.is_builtin);
217    }
218
219    #[test]
220    fn resource_type_label_none_builtin() {
221        // Edge-case: built-in with no label (shouldn't happen in practice, but the type
222        // must round-trip cleanly regardless).
223        let rt = ResourceType {
224            name: "LinkObj".into(),
225            iri: "http://api.knora.org/ontology/knora-api/v2#LinkObj".into(),
226            label: None,
227            is_builtin: true,
228            count: None,
229        };
230        let cloned = rt.clone();
231        assert_eq!(rt, cloned);
232        assert_eq!(rt.name, "LinkObj");
233        assert_eq!(rt.label, None);
234        assert!(rt.is_builtin);
235    }
236
237    // --- ResourceTypeSummary tests ---
238
239    #[test]
240    fn resource_type_summary_with_label() {
241        let rt = ResourceTypeSummary {
242            name: "letter".into(),
243            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
244            label: Some("Letter".into()),
245        };
246        let cloned = rt.clone();
247        assert_eq!(rt, cloned);
248        assert_eq!(rt.name, "letter");
249        assert_eq!(
250            rt.iri,
251            "http://api.dasch.swiss/ontology/0801/beol/v2#letter"
252        );
253        assert_eq!(rt.label.as_deref(), Some("Letter"));
254    }
255
256    #[test]
257    fn resource_type_summary_without_label() {
258        let rt = ResourceTypeSummary {
259            name: "Archive".into(),
260            iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
261            label: None,
262        };
263        let cloned = rt.clone();
264        assert_eq!(rt, cloned);
265        assert_eq!(rt.name, "Archive");
266        assert_eq!(
267            rt.iri,
268            "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
269        );
270        assert_eq!(rt.label, None);
271    }
272
273    // --- DataModelDetail tests ---
274
275    #[test]
276    fn data_model_detail_full_construction_and_equality() {
277        let detail = DataModelDetail {
278            name: "beol".into(),
279            iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
280            label: Some("The BEOL data-model".into()),
281            last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
282            resource_types: vec![
283                ResourceTypeSummary {
284                    name: "Archive".into(),
285                    iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
286                    label: Some("Archive".into()),
287                },
288                ResourceTypeSummary {
289                    name: "letter".into(),
290                    iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
291                    label: Some("Letter".into()),
292                },
293            ],
294        };
295        let cloned = detail.clone();
296        assert_eq!(detail, cloned);
297        assert_eq!(detail.name, "beol");
298        assert_eq!(detail.iri, "http://api.dasch.swiss/ontology/0801/beol/v2");
299        assert_eq!(detail.label.as_deref(), Some("The BEOL data-model"));
300        assert_eq!(
301            detail.last_modified.as_deref(),
302            Some("2024-05-27T13:43:26.233048Z")
303        );
304        assert_eq!(detail.resource_types.len(), 2);
305    }
306
307    #[test]
308    fn data_model_detail_label_none_last_modified_none() {
309        let detail = DataModelDetail {
310            name: "minimal".into(),
311            iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
312            label: None,
313            last_modified: None,
314            resource_types: vec![ResourceTypeSummary {
315                name: "Thing".into(),
316                iri: "http://api.dasch.swiss/ontology/0000/minimal/v2#Thing".into(),
317                label: None,
318            }],
319        };
320        let cloned = detail.clone();
321        assert_eq!(detail, cloned);
322        assert_eq!(detail.label, None);
323        assert_eq!(detail.last_modified, None);
324        assert_eq!(detail.resource_types.len(), 1);
325        assert_eq!(detail.resource_types[0].label, None);
326    }
327
328    #[test]
329    fn data_model_detail_empty_resource_types() {
330        let detail = DataModelDetail {
331            name: "empty".into(),
332            iri: "http://api.dasch.swiss/ontology/9999/empty/v2".into(),
333            label: Some("Empty data-model".into()),
334            last_modified: None,
335            resource_types: vec![],
336        };
337        let cloned = detail.clone();
338        assert_eq!(detail, cloned);
339        assert!(detail.resource_types.is_empty());
340        assert_eq!(detail.label.as_deref(), Some("Empty data-model"));
341        assert_eq!(detail.last_modified, None);
342    }
343}