Skip to main content

ahri_tre_app/service/
study_context.rs

1use super::*;
2use ahri_tre_protocol::{
3    PublicUuid,
4    refs::{ObjectKind, ObjectRef, ObjectSelector, decode_scoped_integer_ref},
5};
6
7impl AppService {
8    /// Captures an explicit Study or a previously observed Session default using
9    /// this operation's authenticated metadata capability. Never falls back
10    /// after an explicit selector fails.
11    pub async fn resolve_session_study(
12        &self,
13        datastore_id: uuid::Uuid,
14        explicit: Option<ahri_tre_protocol::study::StudySelector>,
15        current: Option<ObjectRef>,
16    ) -> Result<ObjectRef, ahri_tre_protocol::ProtocolError> {
17        use ahri_tre_protocol::{domain::DomainSelector, study::StudySelector};
18        let (study, domain) = match explicit {
19            Some(StudySelector::Id { study }) => {
20                (ObjectSelector::Reference { reference: study }, None)
21            }
22            Some(StudySelector::Name { domain, name }) => (
23                ObjectSelector::Name { name },
24                domain.map(|domain| match domain {
25                    DomainSelector::Id { domain } => {
26                        ObjectSelector::Reference { reference: domain }
27                    }
28                    DomainSelector::Name { name } => ObjectSelector::Name { name },
29                }),
30            ),
31            None => (
32                ObjectSelector::Reference {
33                    reference: current.ok_or_else(|| {
34                        ahri_tre_protocol::ProtocolError::new(
35                            ahri_tre_protocol::ProtocolErrorCode::PreconditionFailed,
36                            "Current study is required",
37                        )
38                        .with_target("current_study")
39                    })?,
40                },
41                None,
42            ),
43        };
44        self.resolve_study_context(datastore_id, study, domain)
45            .await
46            .map(|(study, _)| study)
47            .map_err(crate::projections::study_context_protocol_error)
48    }
49
50    /// Resolves scope and current authority through the same Study resolver used
51    /// by catalogue workflows. Explicit constraints never consume a default.
52    pub async fn resolve_study_context(
53        &self,
54        datastore_id: uuid::Uuid,
55        study: ObjectSelector,
56        domain: Option<ObjectSelector>,
57    ) -> Result<(ObjectRef, String), AppError> {
58        let check = |reference: ObjectRef, kind| {
59            if reference.kind != kind {
60                Err(AppError::Validation(
61                    "Object reference has the wrong kind".into(),
62                ))
63            } else if reference.datastore_id.as_uuid() != datastore_id {
64                Err(AppError::Conflict(
65                    "Object reference disagrees with Study context".into(),
66                ))
67            } else {
68                Ok(reference.id)
69            }
70        };
71        let domain = domain
72            .map(|domain| {
73                Ok(match domain {
74                    ObjectSelector::Name { name } => DomainSelector::Name { name: name.into() },
75                    ObjectSelector::Reference { reference } => DomainSelector::Id {
76                        domain_id: ahri_tre_types::DomainId(
77                            decode_scoped_integer_ref(
78                                "domain",
79                                check(reference, ObjectKind::Domain)?,
80                            )
81                            .ok_or_else(|| {
82                                AppError::Validation("Invalid Domain reference".into())
83                            })?,
84                        ),
85                    },
86                })
87            })
88            .transpose()?;
89        let selector = match study {
90            ObjectSelector::Name { name } => StudySelector::Name {
91                domain: None,
92                name: name.into(),
93            },
94            ObjectSelector::Reference { reference } => StudySelector::Id {
95                study_id: ahri_tre_types::StudyId(check(reference, ObjectKind::Study)?.as_uuid()),
96            },
97        };
98        let study = self
99            .resolve_study_selector(selector, "Current study")
100            .await?;
101        if let Some(domain) = domain {
102            let domain = self.resolve_domain_selector(domain, "Study Domain").await?;
103            self.ensure_study_domain(study.study_id, domain.domain_id, "Current study")
104                .await
105                .map_err(|error| match error {
106                    AppError::Validation(_) => {
107                        AppError::Conflict("Domain is not linked to the Study".into())
108                    }
109                    error => error,
110                })?;
111        }
112        // The live Session's authenticated metadata adapter enforces Study
113        // visibility on the lookup above. Do not reinterpret PostgreSQL role
114        // grants using an OIDC subject, or require custodian-only grant reads.
115        Ok((
116            ObjectRef {
117                datastore_id: PublicUuid::from_uuid(datastore_id),
118                kind: ObjectKind::Study,
119                id: PublicUuid::from_uuid(study.study_id.0),
120            },
121            study.name.as_str().to_owned(),
122        ))
123    }
124}