Skip to main content

ahri_tre_protocol/
refs.rs

1use crate::ids::{PublicName, PublicUuid};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(untagged, deny_unknown_fields)]
6pub enum SessionSelector {
7    Name {
8        name: PublicName,
9    },
10    Reference {
11        #[serde(rename = "ref")]
12        reference: SessionRef,
13    },
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct SessionRef {
19    pub deployment_id: PublicUuid,
20    pub runtime_login_id: PublicUuid,
21    pub session_id: PublicUuid,
22}
23
24impl SessionSelector {
25    pub fn name(&self) -> Option<&PublicName> {
26        match self {
27            Self::Name { name } => Some(name),
28            Self::Reference { .. } => None,
29        }
30    }
31}
32
33impl From<SessionRef> for SessionSelector {
34    fn from(reference: SessionRef) -> Self {
35        Self::Reference { reference }
36    }
37}
38
39/// A reusable catalogue reference retains its durable Datastore origin.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct ObjectRef {
43    pub datastore_id: PublicUuid,
44    pub kind: ObjectKind,
45    pub id: PublicUuid,
46}
47
48impl ObjectRef {
49    /// Context validation is separate from object visibility and authorization.
50    #[allow(clippy::result_large_err)]
51    pub fn validate_context(
52        self,
53        datastore_id: PublicUuid,
54        kind: ObjectKind,
55    ) -> Result<(), crate::ProtocolError> {
56        if self.kind != kind {
57            return Err(crate::ProtocolError::new(
58                crate::ProtocolErrorCode::ValidationFailed,
59                "Object reference has the wrong kind",
60            ));
61        }
62        if self.datastore_id != datastore_id {
63            return Err(crate::ProtocolError::new(
64                crate::ProtocolErrorCode::Conflict,
65                "Object reference belongs to another Datastore",
66            ));
67        }
68        Ok(())
69    }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum ObjectKind {
75    Domain,
76    Study,
77    StudyType,
78    Asset,
79    AssetVersion,
80    Variable,
81    ValueType,
82    Vocabulary,
83    VocabularyItem,
84    VocabularyMapping,
85    Entity,
86    Relation,
87    EntityInstance,
88    RelationInstance,
89    Transformation,
90    Tag,
91    StudyAccessRequest,
92    Operation,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(untagged, deny_unknown_fields)]
97pub enum ObjectSelector {
98    Name {
99        name: PublicName,
100    },
101    Reference {
102        #[serde(rename = "ref")]
103        reference: ObjectRef,
104    },
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
108pub struct DatastoreRef {
109    pub id: PublicUuid,
110}
111
112pub type DomainRef = ObjectRef;
113
114pub type StudyRef = ObjectRef;
115
116pub type AssetRef = ObjectRef;
117
118pub type DataFileRef = ObjectRef;
119
120pub type DatasetRef = ObjectRef;
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
123pub struct ExportPackageRef {
124    pub id: PublicUuid,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
128pub struct TabularResultRef {
129    pub id: PublicUuid,
130}
131
132pub type VersionRef = ObjectRef;
133
134pub type VariableRef = ObjectRef;
135
136pub type VocabularyRef = ObjectRef;
137
138pub type TransformationRef = ObjectRef;
139
140pub type EntityRef = ObjectRef;
141
142pub type RelationRef = ObjectRef;
143
144pub type EntityInstanceRef = ObjectRef;
145pub type RelationInstanceRef = ObjectRef;
146pub type ValueTypeRef = ObjectRef;
147pub type VocabularyItemRef = ObjectRef;
148pub type VocabularyMappingRef = ObjectRef;
149
150pub type TagRef = ObjectRef;
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
153#[serde(tag = "kind", rename_all = "snake_case")]
154pub enum TagSelector {
155    Id { tag: TagRef },
156    Label { scope: TagScope, label: PublicName },
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum TagScope {
162    Global,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct TagSummary {
167    pub tag: TagRef,
168    pub scope: TagScope,
169    pub label: String,
170}
171
172/// Legacy namespace assignments used by established Browser UUIDs.
173///
174/// These values are protocol compatibility commitments. New public references
175/// should use the scope-bound encoders below instead.
176pub mod legacy_browser_public_id_namespace {
177    pub const DOMAIN: u128 = 0x0000_0000_0000_5000;
178    pub const TAG: u128 = 0x0000_0000_0000_5001;
179    pub const VARIABLE: u128 = 0x0000_0000_0000_5002;
180    pub const VOCABULARY: u128 = 0x0000_0000_0000_5003;
181    pub const VOCABULARY_ITEM: u128 = 0x0000_0000_0000_5004;
182    pub const VOCABULARY_MAPPING: u128 = 0x0000_0000_0000_5005;
183    pub const VALUE_TYPE: u128 = 0x0000_0000_0000_5006;
184    pub const ENTITY: u128 = 0x0000_0000_0000_5007;
185    pub const RELATION: u128 = 0x0000_0000_0000_5008;
186    pub const TRANSFORMATION: u128 = 0x0000_0000_0000_5009;
187}
188
189/// Encodes a store-local integer identifier as a stable scope-bound public
190/// reference. The scope prevents a reference for one catalogue type from
191/// being accepted as another type.
192pub fn encode_scoped_integer_ref(scope: &str, id: i64) -> PublicUuid {
193    let mut bytes = [0_u8; 16];
194    bytes[..8].copy_from_slice(&stable_scope_hash(scope).to_be_bytes());
195    bytes[8..].copy_from_slice(&id.to_be_bytes());
196    PublicUuid::from_uuid(uuid::Uuid::from_bytes(bytes))
197}
198
199/// Preserves the pre-scope-hash browser reference encoding for established
200/// HTTP DTOs. New protocol fields should use [`encode_scoped_integer_ref`].
201pub fn encode_legacy_namespaced_integer_ref(namespace: u128, id: i64) -> PublicUuid {
202    PublicUuid::from_uuid(uuid::Uuid::from_u128((namespace << 64) | id as u64 as u128))
203}
204
205/// Decodes a stable scope-bound integer reference, returning `None` when the
206/// reference belongs to a different scope.
207pub fn decode_scoped_integer_ref(scope: &str, reference: PublicUuid) -> Option<i64> {
208    let bytes = reference.as_uuid().into_bytes();
209    let mut encoded_scope = [0_u8; 8];
210    encoded_scope.copy_from_slice(&bytes[..8]);
211    if u64::from_be_bytes(encoded_scope) != stable_scope_hash(scope) {
212        return None;
213    }
214    let mut id = [0_u8; 8];
215    id.copy_from_slice(&bytes[8..]);
216    Some(i64::from_be_bytes(id))
217}
218
219/// Encodes a string value as a stable opaque reference within one scope.
220pub fn encode_scoped_string_ref(scope: &str, value: &str) -> PublicUuid {
221    let mut bytes = [0_u8; 16];
222    bytes[..8].copy_from_slice(&stable_scope_hash(scope).to_be_bytes());
223    bytes[8..].copy_from_slice(&stable_scope_hash(value).to_be_bytes());
224    PublicUuid::from_uuid(uuid::Uuid::from_bytes(bytes))
225}
226
227fn stable_scope_hash(scope: &str) -> u64 {
228    let mut hash = 0xcbf29ce484222325_u64;
229    for byte in scope.as_bytes() {
230        hash ^= u64::from(*byte);
231        hash = hash.wrapping_mul(0x100000001b3);
232    }
233    hash
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn scoped_integer_refs_round_trip_only_with_their_original_scope() {
242        let reference = encode_scoped_integer_ref("domain", -42);
243
244        assert_eq!(decode_scoped_integer_ref("domain", reference), Some(-42));
245        assert_eq!(decode_scoped_integer_ref("tag", reference), None);
246    }
247
248    #[test]
249    fn legacy_namespaced_integer_refs_remain_byte_stable() {
250        assert_eq!(
251            encode_legacy_namespaced_integer_ref(legacy_browser_public_id_namespace::DOMAIN, 42)
252                .to_string(),
253            "00000000-0000-5000-0000-00000000002a"
254        );
255    }
256
257    #[test]
258    fn legacy_browser_public_id_namespaces_remain_byte_stable() {
259        use legacy_browser_public_id_namespace::{
260            DOMAIN, ENTITY, RELATION, TAG, TRANSFORMATION, VALUE_TYPE, VARIABLE, VOCABULARY,
261            VOCABULARY_ITEM, VOCABULARY_MAPPING,
262        };
263
264        assert_eq!(
265            [
266                encode_legacy_namespaced_integer_ref(DOMAIN, 42).to_string(),
267                encode_legacy_namespaced_integer_ref(TAG, 42).to_string(),
268                encode_legacy_namespaced_integer_ref(VARIABLE, 42).to_string(),
269                encode_legacy_namespaced_integer_ref(VOCABULARY, 42).to_string(),
270                encode_legacy_namespaced_integer_ref(VOCABULARY_ITEM, 42).to_string(),
271                encode_legacy_namespaced_integer_ref(VOCABULARY_MAPPING, 42).to_string(),
272                encode_legacy_namespaced_integer_ref(VALUE_TYPE, 42).to_string(),
273                encode_legacy_namespaced_integer_ref(ENTITY, 42).to_string(),
274                encode_legacy_namespaced_integer_ref(RELATION, 42).to_string(),
275                encode_legacy_namespaced_integer_ref(TRANSFORMATION, 42).to_string(),
276            ],
277            [
278                "00000000-0000-5000-0000-00000000002a",
279                "00000000-0000-5001-0000-00000000002a",
280                "00000000-0000-5002-0000-00000000002a",
281                "00000000-0000-5003-0000-00000000002a",
282                "00000000-0000-5004-0000-00000000002a",
283                "00000000-0000-5005-0000-00000000002a",
284                "00000000-0000-5006-0000-00000000002a",
285                "00000000-0000-5007-0000-00000000002a",
286                "00000000-0000-5008-0000-00000000002a",
287                "00000000-0000-5009-0000-00000000002a",
288            ]
289        );
290    }
291
292    #[test]
293    fn scoped_string_refs_are_stable_and_scope_bound() {
294        let reference = encode_scoped_string_ref("datastore", "ahri-tre-test");
295
296        assert_eq!(
297            reference,
298            encode_scoped_string_ref("datastore", "ahri-tre-test")
299        );
300        assert_ne!(
301            reference,
302            encode_scoped_string_ref("deployment", "ahri-tre-test")
303        );
304    }
305}