Skip to main content

ahri_tre_app/service/
semantic_lifecycle.rs

1use super::*;
2use ahri_tre_protocol::{lifecycle as wire, refs::ObjectKind};
3
4impl AppService {
5    pub async fn plan_governed_semantic_delete(
6        &self,
7        request: wire::SemanticDeleteRequest,
8        authenticated_actor: &str,
9    ) -> Result<SemanticDeletePlanningResult, AppError> {
10        use wire::SemanticDeleteSelector as Selector;
11        let actor = super::lifecycle::lifecycle_actor(&request.intent, authenticated_actor)?;
12        let mut constraint = None;
13        let target = match request.target {
14            Selector::Reference { reference, domain } => {
15                let (kind, scope) = match reference.kind {
16                    ObjectKind::Domain => (SemanticDeleteTargetKind::Domain, "domain"),
17                    ObjectKind::Variable => (SemanticDeleteTargetKind::Variable, "variable"),
18                    ObjectKind::Vocabulary => (SemanticDeleteTargetKind::Vocabulary, "vocabulary"),
19                    ObjectKind::Entity => (SemanticDeleteTargetKind::Entity, "entity"),
20                    ObjectKind::Relation => (SemanticDeleteTargetKind::EntityRelation, "relation"),
21                    _ => {
22                        return Err(AppError::Validation(
23                            "Invalid semantic deletion kind".into(),
24                        ));
25                    }
26                };
27                let id = ahri_tre_protocol::refs::decode_scoped_integer_ref(
28                    scope,
29                    self.catalogue_reference_id(reference, reference.kind)?,
30                )
31                .ok_or_else(|| AppError::Validation("Invalid semantic reference".into()))?;
32                if let Some(domain) = domain {
33                    constraint = Some(
34                        self.get_domain(GetDomainRequest {
35                            domain: self.catalogue_domain_selector(domain)?,
36                        })
37                        .await?
38                        .ok_or_else(|| {
39                            AppError::NotFound("Supplied Domain was not found".into())
40                        })?,
41                    );
42                }
43                pinned_semantic_target(kind, id)
44            }
45            Selector::Domain { domain } => SemanticDeleteTarget::Domain {
46                domain: self.catalogue_domain_selector(domain)?,
47            },
48            Selector::Variable { domain, name } => SemanticDeleteTarget::Variable {
49                variable: VariableSelector::Name {
50                    domain: self.catalogue_domain_selector(domain)?,
51                    name: name.into(),
52                },
53            },
54            Selector::Vocabulary { domain, name } => SemanticDeleteTarget::Vocabulary {
55                vocabulary: VocabularySelector::Name {
56                    domain: self.catalogue_domain_selector(domain)?,
57                    name: name.into(),
58                },
59            },
60            Selector::Entity {
61                entity: ahri_tre_protocol::model::EntitySelector::Name { domain, name },
62            } => SemanticDeleteTarget::Entity {
63                entity: EntitySelector::Name {
64                    domain: self.catalogue_domain_selector(domain)?,
65                    name: name.into(),
66                },
67            },
68            Selector::Relation {
69                relation: ahri_tre_protocol::model::RelationSelector::Name { domain, name },
70            } => SemanticDeleteTarget::EntityRelation {
71                relation: EntityRelationSelector::Name {
72                    domain: self.catalogue_domain_selector(domain)?,
73                    name: name.into(),
74                },
75            },
76            _ => {
77                return Err(AppError::Validation(
78                    "Semantic deletion requires a canonical reference or qualified name".into(),
79                ));
80            }
81        };
82        let mut planning = self
83            .plan_semantic_delete(PlanSemanticDeleteRequest {
84                target,
85                reason: request.intent.reason,
86                actor: Some(actor),
87                dry_run: request.intent.dry_run,
88            })
89            .await?;
90        if let Some(plan) = &mut planning.plan {
91            let identity = &plan.target_identity;
92            if let Some(domain) = constraint
93                && identity.domain_id != domain.domain_id
94            {
95                return Err(AppError::Conflict(
96                    "Semantic target does not belong to the supplied Domain".into(),
97                ));
98            }
99            let id = identity.id.parse().map_err(|_| {
100                AppError::Infrastructure("Invalid resolved semantic identity".into())
101            })?;
102            plan.target = pinned_semantic_target(identity.kind, id);
103        }
104        Ok(planning)
105    }
106
107    pub async fn execute_governed_semantic_delete(
108        &self,
109        request: wire::SemanticDeleteRequest,
110        authenticated_actor: &str,
111    ) -> Result<GovernedSemanticDeleteOutcome, AppError> {
112        let dry_run = request.intent.dry_run;
113        let confirmed = request.intent.confirmed;
114        let planning = self
115            .plan_governed_semantic_delete(request, authenticated_actor)
116            .await?;
117        if dry_run {
118            return Ok(GovernedSemanticDeleteOutcome::Planned(planning));
119        }
120        let plan = planning
121            .plan
122            .ok_or_else(|| AppError::Validation("Semantic deletion plan was rejected".into()))?;
123        if !confirmed {
124            return Err(AppError::Validation(
125                "Semantic deletion requires confirmation".into(),
126            ));
127        }
128        if plan.disposition == SemanticDeleteDisposition::Blocked {
129            return Ok(GovernedSemanticDeleteOutcome::Blocked(plan));
130        }
131        self.execute_semantic_delete(ExecuteSemanticDeleteRequest { plan, confirmed })
132            .await
133            .map(GovernedSemanticDeleteOutcome::Executed)
134    }
135}
136
137pub enum GovernedSemanticDeleteOutcome {
138    Planned(SemanticDeletePlanningResult),
139    Blocked(SemanticDeletePlan),
140    Executed(ExecuteSemanticDeleteResult),
141}
142
143fn pinned_semantic_target(kind: SemanticDeleteTargetKind, id: i64) -> SemanticDeleteTarget {
144    match kind {
145        SemanticDeleteTargetKind::Domain => SemanticDeleteTarget::Domain {
146            domain: DomainSelector::Id {
147                domain_id: ahri_tre_types::DomainId(id),
148            },
149        },
150        SemanticDeleteTargetKind::Variable => SemanticDeleteTarget::Variable {
151            variable: VariableSelector::Id {
152                variable_id: ahri_tre_types::VariableId(id),
153            },
154        },
155        SemanticDeleteTargetKind::Vocabulary => SemanticDeleteTarget::Vocabulary {
156            vocabulary: VocabularySelector::Id {
157                vocabulary_id: ahri_tre_types::VocabularyId(id),
158            },
159        },
160        SemanticDeleteTargetKind::Entity => SemanticDeleteTarget::Entity {
161            entity: EntitySelector::Id {
162                entity_id: ahri_tre_types::EntityId(id),
163            },
164        },
165        SemanticDeleteTargetKind::EntityRelation => SemanticDeleteTarget::EntityRelation {
166            relation: EntityRelationSelector::Id {
167                entity_relation_id: ahri_tre_types::EntityRelationId(id),
168            },
169        },
170    }
171}