Skip to main content

dsp_cli/model/
project.rs

1//! Project domain shapes — shared across the client → action boundary.
2//!
3//! Types here are the dsp-cli vocabulary for project 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/// Minimal project reference. Phase 4 expands this into the full Project model.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ProjectRef {
10    /// The project's IRI (e.g. `http://rdfh.ch/projects/0001`).
11    pub iri: String,
12    /// Four-hex-digit shortcode (e.g. `0001`).
13    pub shortcode: String,
14    /// Human-readable shortname (e.g. `anything`).
15    pub shortname: String,
16}
17
18/// One language-tagged description value (dsp-cli vocabulary for the API's
19/// `description: [{value, language}]`). `language` is optional on the wire.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ProjectDescription {
22    /// The description text (may contain HTML markup as returned by the server).
23    pub value: String,
24    /// BCP-47 language tag, if the server provides one (e.g. `"en"`).
25    pub language: Option<String>,
26}
27
28/// A lean reference to a child data-model, as surfaced by `project describe`.
29///
30/// Full data-model detail belongs to the future `data-model list/describe`
31/// commands. `name` is derived from the data-model's IRI by the HTTP client
32/// layer at the dsp-cli/ADR-0001 boundary — it is NOT a server-supplied field.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DataModelSummary {
35    /// Short name of the data-model (e.g. `beol`), derived from the IRI.
36    pub name: String,
37    /// Full IRI of the data-model (e.g. `http://api.dasch.swiss/ontology/0801/beol/v2`).
38    pub iri: String,
39}
40
41/// A project as shown by `dsp vre project describe` — the rich projection.
42///
43/// Contrast `Project` (the lean `list` index projection). Carries identity,
44/// description, keywords, and a data-models summary (count + names).
45/// No `serde` derive: wire deserialization stays in `src/client/http.rs`.
46/// See dsp-cli/ADR-0001 and dsp-cli/ADR-0008.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ProjectDetail {
49    /// The project's IRI (e.g. `http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF`).
50    pub iri: String,
51    /// Four-hex-digit shortcode (e.g. `0801`).
52    pub shortcode: String,
53    /// Human-readable shortname (e.g. `beol`).
54    pub shortname: String,
55    /// Human-readable long name, if the server supplies one.
56    pub longname: Option<String>,
57    /// Language-tagged description values (may contain HTML markup).
58    pub description: Vec<ProjectDescription>,
59    /// Free-form keywords associated with the project.
60    pub keywords: Vec<String>,
61    /// Lean references to the project's child data-models, sorted by name.
62    pub data_models: Vec<DataModelSummary>,
63}
64
65/// A project as shown by `dsp vre project list` — the lean index projection.
66///
67/// Rich detail (description, keywords, licences, …) belongs to the future
68/// `describe` model, not here. No `serde` derive: wire deserialization stays
69/// in `src/client/http.rs` (the private `ProjectListItemDto`). See dsp-cli/ADR-0001
70/// and dsp-cli/ADR-0008.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Project {
73    /// The project's IRI (e.g. `http://rdfh.ch/projects/0001`).
74    pub iri: String,
75    /// Four-hex-digit shortcode (e.g. `0001`).
76    pub shortcode: String,
77    /// Human-readable shortname (e.g. `anything`).
78    pub shortname: String,
79    /// Human-readable long name, if the server supplies one.
80    pub longname: Option<String>,
81    /// Number of data-models (DSP-API "ontologies") attached to the project.
82    pub data_models: usize,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn project_ref_construction_and_equality() {
91        let a = ProjectRef {
92            iri: "http://rdfh.ch/projects/0001".into(),
93            shortcode: "0001".into(),
94            shortname: "anything".into(),
95        };
96        let b = a.clone();
97        assert_eq!(a, b);
98        assert_eq!(a.iri, "http://rdfh.ch/projects/0001");
99        assert_eq!(a.shortcode, "0001");
100        assert_eq!(a.shortname, "anything");
101    }
102
103    #[test]
104    fn project_construction_and_equality() {
105        let p = Project {
106            iri: "http://rdfh.ch/projects/0001".into(),
107            shortcode: "0001".into(),
108            shortname: "anything".into(),
109            longname: Some("Anything Project".into()),
110            data_models: 3,
111        };
112        let cloned = p.clone();
113        assert_eq!(p, cloned);
114        assert_eq!(p.iri, "http://rdfh.ch/projects/0001");
115        assert_eq!(p.shortcode, "0001");
116        assert_eq!(p.shortname, "anything");
117        assert_eq!(p.longname.as_deref(), Some("Anything Project"));
118        assert_eq!(p.data_models, 3);
119    }
120
121    #[test]
122    fn project_longname_none() {
123        let p = Project {
124            iri: "http://rdfh.ch/projects/0002".into(),
125            shortcode: "0002".into(),
126            shortname: "images".into(),
127            longname: None,
128            data_models: 0,
129        };
130        assert_eq!(p.longname, None);
131        assert_eq!(p.data_models, 0);
132    }
133
134    #[test]
135    fn project_description_with_language() {
136        let d = ProjectDescription {
137            value: "<b>Project Metadata</b>".into(),
138            language: Some("en".into()),
139        };
140        let cloned = d.clone();
141        assert_eq!(d, cloned);
142        assert_eq!(d.value, "<b>Project Metadata</b>");
143        assert_eq!(d.language.as_deref(), Some("en"));
144    }
145
146    #[test]
147    fn project_description_without_language() {
148        let d = ProjectDescription { value: "Beschreibung".into(), language: None };
149        let cloned = d.clone();
150        assert_eq!(d, cloned);
151        assert_eq!(d.language, None);
152    }
153
154    #[test]
155    fn data_model_summary_construction_and_equality() {
156        let dm = DataModelSummary {
157            name: "beol".into(),
158            iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
159        };
160        let cloned = dm.clone();
161        assert_eq!(dm, cloned);
162        assert_eq!(dm.name, "beol");
163        assert_eq!(dm.iri, "http://api.dasch.swiss/ontology/0801/beol/v2");
164    }
165
166    #[test]
167    fn project_detail_construction_and_equality() {
168        let detail = ProjectDetail {
169            iri: "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF".into(),
170            shortcode: "0801".into(),
171            shortname: "beol".into(),
172            longname: Some("Bernoulli-Euler Online".into()),
173            description: vec![ProjectDescription {
174                value: "<b>Project Metadata</b>".into(),
175                language: Some("en".into()),
176            }],
177            keywords: vec!["Bernoulli".into(), "Condorcet".into(), "Euler".into()],
178            data_models: vec![
179                DataModelSummary {
180                    name: "beol".into(),
181                    iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
182                },
183                DataModelSummary {
184                    name: "biblio".into(),
185                    iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
186                },
187            ],
188        };
189        let cloned = detail.clone();
190        assert_eq!(detail, cloned);
191        assert_eq!(detail.iri, "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF");
192        assert_eq!(detail.shortcode, "0801");
193        assert_eq!(detail.shortname, "beol");
194        assert_eq!(detail.longname.as_deref(), Some("Bernoulli-Euler Online"));
195        assert_eq!(detail.description.len(), 1);
196        assert_eq!(detail.keywords.len(), 3);
197        assert_eq!(detail.data_models.len(), 2);
198    }
199
200    #[test]
201    fn project_detail_empty_case() {
202        let detail = ProjectDetail {
203            iri: "http://rdfh.ch/projects/0000".into(),
204            shortcode: "0000".into(),
205            shortname: "minimal".into(),
206            longname: None,
207            description: vec![],
208            keywords: vec![],
209            data_models: vec![],
210        };
211        assert_eq!(detail.longname, None);
212        assert!(detail.description.is_empty());
213        assert!(detail.keywords.is_empty());
214        assert!(detail.data_models.is_empty());
215    }
216}