relay_knowledge/domain/core/
ontology.rs1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3
4use super::{DomainError, SourceScope};
5
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum OntologyEntityKind {
10 #[default]
11 Untyped,
12 BusinessDomain,
13 BusinessTerm,
14}
15
16impl OntologyEntityKind {
17 pub const fn as_str(self) -> &'static str {
19 match self {
20 Self::Untyped => "untyped",
21 Self::BusinessDomain => "business_domain",
22 Self::BusinessTerm => "business_term",
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct OntologyIdentity {
30 pub source_scope: SourceScope,
31 pub domain_id: String,
32 pub entity_id: String,
33 pub entity_kind: OntologyEntityKind,
34}
35
36impl OntologyIdentity {
37 pub fn new(
39 source_scope: SourceScope,
40 domain_id: impl Into<String>,
41 entity_id: impl Into<String>,
42 entity_kind: OntologyEntityKind,
43 ) -> Result<Self, DomainError> {
44 let domain_id = validate_identity_text("domain_id", domain_id.into())?;
45 let entity_id = validate_identity_text("entity_id", entity_id.into())?;
46 if entity_kind == OntologyEntityKind::Untyped {
47 return Err(DomainError::invalid(
48 "entity_kind",
49 "scoped ontology identities must be typed",
50 ));
51 }
52 Ok(Self {
53 source_scope,
54 domain_id,
55 entity_id,
56 entity_kind,
57 })
58 }
59
60 pub fn stable_entity_id(&self) -> String {
62 let mut digest = Sha256::new();
63 for part in [
64 self.source_scope.as_str(),
65 self.domain_id.as_str(),
66 self.entity_id.as_str(),
67 self.entity_kind.as_str(),
68 ] {
69 digest.update((part.len() as u64).to_be_bytes());
70 digest.update(part.as_bytes());
71 }
72 format!("ontology:{:x}", digest.finalize())
73 }
74}
75
76fn validate_identity_text(field: &'static str, value: String) -> Result<String, DomainError> {
77 let value = value.trim();
78 if value.is_empty() {
79 return Err(DomainError::invalid(field, "must not be empty"));
80 }
81 if value.len() > 128 {
82 return Err(DomainError::invalid(field, "must be 128 bytes or less"));
83 }
84 if value.contains('\0') {
85 return Err(DomainError::invalid(field, "must not contain NUL bytes"));
86 }
87 Ok(value.to_owned())
88}
89
90#[cfg(test)]
91#[path = "ontology_tests.rs"]
92mod tests;