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