Skip to main content

ahri_tre_app/projections/
semantic.rs

1use crate::{RegisteredVariable, RegisteredVocabulary};
2use ahri_tre_protocol::refs::{EntityRef, RelationRef};
3use ahri_tre_protocol::refs::{TagRef, TagScope};
4use ahri_tre_protocol::{
5    PublicUuid, dictionary,
6    refs::{DomainRef, VariableRef, VocabularyRef, encode_scoped_integer_ref as protocol_int_ref},
7};
8use std::collections::BTreeMap;
9pub fn semantic_variable(
10    datastore_id: PublicUuid,
11    variable: RegisteredVariable,
12) -> ahri_tre_protocol::dictionary::VariableDetail {
13    let value_format = variable.variable.value_format.clone();
14    let note = variable.variable.note.clone();
15    let ontology_namespace = variable.variable.ontology_namespace.clone();
16    ahri_tre_protocol::dictionary::VariableDetail {
17        vocabulary_items_withheld: true,
18        summary: protocol_variable_summary(datastore_id, variable.clone()),
19        value_format,
20        note,
21        ontology_namespace,
22        // Stored items can be inferred from rows; their origin is not retained.
23        // The safe catalogue route cannot release them without admission.
24        vocabulary_items: Vec::new(),
25        domain_detail: None,
26    }
27}
28
29fn protocol_variable_summary(
30    datastore_id: PublicUuid,
31    variable: RegisteredVariable,
32) -> ahri_tre_protocol::dictionary::VariableSummary {
33    ahri_tre_protocol::dictionary::VariableSummary {
34        variable: VariableRef {
35            datastore_id,
36            kind: ahri_tre_protocol::refs::ObjectKind::Variable,
37            id: protocol_int_ref("variable", variable.variable.variable_id.0),
38        },
39        domain: DomainRef {
40            datastore_id,
41            kind: ahri_tre_protocol::refs::ObjectKind::Domain,
42            id: protocol_int_ref("domain", variable.variable.domain_id.0),
43        },
44        name: variable.variable.name.as_str().to_string(),
45        value_type: variable.value_type.value_type,
46        vocabulary: variable
47            .vocabulary
48            .map(|value| protocol_vocabulary_metadata_summary(datastore_id, value)),
49        key_role: Some(format!("{:?}", variable.variable.key_role).to_ascii_lowercase()),
50        description: variable.variable.description,
51        ontology_class: variable.variable.ontology_class,
52        tags: Vec::new(),
53    }
54}
55
56fn protocol_vocabulary_metadata_summary(
57    datastore_id: PublicUuid,
58    vocabulary: ahri_tre_types::VocabularyRecord,
59) -> ahri_tre_protocol::dictionary::VocabularySummary {
60    ahri_tre_protocol::dictionary::VocabularySummary {
61        vocabulary: VocabularyRef {
62            datastore_id,
63            kind: ahri_tre_protocol::refs::ObjectKind::Vocabulary,
64            id: protocol_int_ref("vocabulary", vocabulary.vocabulary_id.0),
65        },
66        domain: DomainRef {
67            datastore_id,
68            kind: ahri_tre_protocol::refs::ObjectKind::Domain,
69            id: protocol_int_ref("domain", vocabulary.domain_id.0),
70        },
71        name: vocabulary.name.as_str().to_string(),
72        item_count: None,
73        description: vocabulary.description,
74        tags: Vec::new(),
75    }
76}
77
78pub fn semantic_vocabulary(
79    datastore_id: PublicUuid,
80    vocabulary: RegisteredVocabulary,
81) -> dictionary::VocabularyDetail {
82    dictionary::VocabularyDetail {
83        items_withheld: true,
84        summary: protocol_vocabulary_metadata_summary(datastore_id, vocabulary.vocabulary),
85        items: Vec::new(),
86    }
87}
88
89pub fn semantic_entity(
90    datastore_id: PublicUuid,
91    entity: ahri_tre_types::EntityRecord,
92) -> ahri_tre_protocol::model::EntityDetail {
93    ahri_tre_protocol::model::EntityDetail {
94        summary: protocol_entity_summary(datastore_id, entity.clone()),
95        agent_instructions: entity.agent_instructions,
96        ontology_namespace: entity.ontology_namespace,
97    }
98}
99
100fn protocol_entity_summary(
101    datastore_id: PublicUuid,
102    entity: ahri_tre_types::EntityRecord,
103) -> ahri_tre_protocol::model::EntitySummary {
104    ahri_tre_protocol::model::EntitySummary {
105        entity: protocol_entity_ref(datastore_id, entity.entity_id.0),
106        domain: protocol_domain_ref(datastore_id, entity.domain_id.0),
107        name: entity.name.as_str().to_owned(),
108        description: entity.description,
109        ontology_class: entity.ontology_class,
110        tags: Vec::new(),
111    }
112}
113
114pub fn semantic_relation(
115    datastore_id: PublicUuid,
116    relation: ahri_tre_types::EntityRelationRecord,
117    endpoint_names: &BTreeMap<i64, String>,
118) -> ahri_tre_protocol::model::RelationDefinitionDetail {
119    ahri_tre_protocol::model::RelationDefinitionDetail {
120        summary: protocol_relation_summary(datastore_id, relation.clone(), endpoint_names),
121        agent_instructions: relation.agent_instructions,
122        ontology_namespace: relation.ontology_namespace,
123    }
124}
125
126fn protocol_relation_summary(
127    datastore_id: PublicUuid,
128    relation: ahri_tre_types::EntityRelationRecord,
129    endpoint_names: &BTreeMap<i64, String>,
130) -> ahri_tre_protocol::model::RelationDefinitionSummary {
131    ahri_tre_protocol::model::RelationDefinitionSummary {
132        relation: protocol_relation_ref(datastore_id, relation.entity_relation_id.0),
133        domain: protocol_domain_ref(datastore_id, relation.domain_id.0),
134        name: relation.name.as_str().to_owned(),
135        subject_entity: protocol_entity_ref(datastore_id, relation.subject_entity_id.0),
136        object_entity: protocol_entity_ref(datastore_id, relation.object_entity_id.0),
137        subject_entity_name: endpoint_names.get(&relation.subject_entity_id.0).cloned(),
138        object_entity_name: endpoint_names.get(&relation.object_entity_id.0).cloned(),
139        description: relation.description,
140        ontology_class: relation.ontology_class,
141        tags: Vec::new(),
142    }
143}
144
145fn protocol_entity_ref(datastore_id: PublicUuid, id: i64) -> EntityRef {
146    EntityRef {
147        datastore_id,
148        kind: ahri_tre_protocol::refs::ObjectKind::Entity,
149        id: protocol_int_ref("entity", id),
150    }
151}
152
153fn protocol_relation_ref(datastore_id: PublicUuid, id: i64) -> RelationRef {
154    RelationRef {
155        datastore_id,
156        kind: ahri_tre_protocol::refs::ObjectKind::Relation,
157        id: protocol_int_ref("relation", id),
158    }
159}
160
161fn protocol_domain_ref(datastore_id: PublicUuid, id: i64) -> DomainRef {
162    DomainRef {
163        datastore_id,
164        kind: ahri_tre_protocol::refs::ObjectKind::Domain,
165        id: protocol_int_ref("domain", id),
166    }
167}
168
169pub fn tag_summary(
170    datastore_id: PublicUuid,
171    tag: ahri_tre_types::TagRecord,
172) -> ahri_tre_protocol::refs::TagSummary {
173    ahri_tre_protocol::refs::TagSummary {
174        tag: TagRef {
175            datastore_id,
176            kind: ahri_tre_protocol::refs::ObjectKind::Tag,
177            id: protocol_int_ref("tag", tag.tag_id.0),
178        },
179        scope: TagScope::Global,
180        label: tag.name,
181    }
182}
183
184pub fn semantic_transformation(
185    datastore_id: PublicUuid,
186    value: crate::TransformationSummary,
187) -> ahri_tre_protocol::transformation::TransformationSummary {
188    use ahri_tre_protocol::{
189        refs::{ObjectKind, ObjectRef},
190        transformation::{TransformationDetail, TransformationSummary},
191    };
192    let version = |id| ObjectRef {
193        datastore_id,
194        kind: ObjectKind::AssetVersion,
195        id: PublicUuid::from_uuid(id),
196    };
197    TransformationSummary {
198        transformation: TransformationDetail {
199            transformation_id: ObjectRef {
200                datastore_id,
201                kind: ObjectKind::Transformation,
202                id: protocol_int_ref("transformation", value.transformation.transformation_id.0),
203            },
204            transformation_type: value.transformation.transformation_type,
205            description: value.transformation.description,
206            date_created: value.transformation.date_created,
207            created_by: value.transformation.created_by,
208        },
209        inputs: value
210            .inputs
211            .into_iter()
212            .map(|link| version(link.version_id.0))
213            .collect(),
214        outputs: value
215            .outputs
216            .into_iter()
217            .map(|link| version(link.version_id.0))
218            .collect(),
219    }
220}
221
222/// The same canonical lineage shape is used by standalone and nested workflows.
223pub fn workflow_lineage(
224    datastore_id: PublicUuid,
225    value: ahri_tre_types::TransformationLineageRecord,
226) -> ahri_tre_protocol::transformation::TransformationSummary {
227    semantic_transformation(
228        datastore_id,
229        crate::TransformationSummary {
230            transformation: value.transformation,
231            inputs: value.inputs,
232            outputs: value.outputs,
233        },
234    )
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use ahri_tre_types::{
241        DomainId, NcName, VocabularyId, VocabularyItemId, VocabularyItemRecord, VocabularyRecord,
242    };
243
244    #[test]
245    fn catalogue_vocabulary_never_releases_unproved_values_or_counts() {
246        let datastore_id = PublicUuid::from_uuid(uuid::Uuid::from_u128(1));
247        let detail = semantic_vocabulary(
248            datastore_id,
249            RegisteredVocabulary {
250                vocabulary: VocabularyRecord {
251                    vocabulary_id: VocabularyId(4),
252                    domain_id: DomainId(2),
253                    name: NcName::parse("status").unwrap(),
254                    description: None,
255                },
256                items: vec![VocabularyItemRecord {
257                    vocabulary_item_id: VocabularyItemId(5),
258                    vocabulary_id: VocabularyId(4),
259                    value: 1,
260                    code: "content-must-not-leave".into(),
261                    description: None,
262                }],
263            },
264        );
265        assert!(detail.items_withheld);
266        assert!(detail.items.is_empty());
267        assert_eq!(detail.summary.item_count, None);
268        assert_eq!(detail.summary.vocabulary.datastore_id, datastore_id);
269        let json = serde_json::to_string(&detail).unwrap();
270        assert!(!json.contains("content-must-not-leave"));
271        assert!(!json.contains("item_count"));
272    }
273}