axioval_engine/
relationships.rs1use std::sync::Arc;
4
5use axioval_ir::{Evidence, ObjectId};
6use thiserror::Error;
7
8#[derive(Clone, Debug, Error, PartialEq, Eq)]
10pub enum RelationshipSelectionError {
11 #[error("relationship selection request is invalid")]
13 InvalidRequest,
14 #[error("relationship selection contains a duplicate candidate")]
16 DuplicateCandidate,
17 #[error("relationship selection contains duplicate evidence")]
19 DuplicateEvidence,
20 #[error("relationship selection response does not match its request")]
22 ResponseRequestMismatch,
23 #[error("relationship selection evidence is not exact and reviewable")]
25 InexactEvidence,
26 #[error("relationship selection unavailable: {0}")]
28 Unavailable(String),
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct SemanticRelationship(String);
34
35impl SemanticRelationship {
36 pub fn try_new(value: impl Into<String>) -> Result<Self, RelationshipSelectionError> {
38 let value = value.into();
39 if value.trim().is_empty() {
40 return Err(RelationshipSelectionError::InvalidRequest);
41 }
42 Ok(Self(value))
43 }
44
45 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum TraversalDirection {
55 Forward,
57 Backward,
59 Either,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub enum RelationshipQuery {
66 SharedGroup {
68 relationship: SemanticRelationship,
70 },
71 Related {
73 relationship: SemanticRelationship,
75 direction: TraversalDirection,
77 follow_chain: bool,
79 },
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
84pub struct RelationshipSelectionRequest {
85 anchor: ObjectId,
86 candidate_universe: Vec<ObjectId>,
87 query: RelationshipQuery,
88}
89
90impl RelationshipSelectionRequest {
91 pub fn try_new(
93 anchor: ObjectId,
94 mut candidate_universe: Vec<ObjectId>,
95 query: RelationshipQuery,
96 ) -> Result<Self, RelationshipSelectionError> {
97 candidate_universe.sort();
98 if candidate_universe.windows(2).any(|pair| pair[0] == pair[1]) {
99 return Err(RelationshipSelectionError::DuplicateCandidate);
100 }
101 Ok(Self {
102 anchor,
103 candidate_universe,
104 query,
105 })
106 }
107
108 #[must_use]
110 pub fn anchor(&self) -> &ObjectId {
111 &self.anchor
112 }
113
114 #[must_use]
116 pub fn candidate_universe(&self) -> &[ObjectId] {
117 &self.candidate_universe
118 }
119
120 #[must_use]
122 pub fn query(&self) -> &RelationshipQuery {
123 &self.query
124 }
125
126 fn contains_candidate(&self, candidate: &ObjectId) -> bool {
127 self.candidate_universe.binary_search(candidate).is_ok()
128 }
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct CompleteRelationshipSelection {
134 request: RelationshipSelectionRequest,
135 candidates: Vec<ObjectId>,
136 evidence: Vec<Evidence>,
137}
138
139impl CompleteRelationshipSelection {
140 pub fn try_new(
142 request: RelationshipSelectionRequest,
143 mut candidates: Vec<ObjectId>,
144 mut evidence: Vec<Evidence>,
145 ) -> Result<Self, RelationshipSelectionError> {
146 candidates.sort();
147 if candidates.windows(2).any(|pair| pair[0] == pair[1]) {
148 return Err(RelationshipSelectionError::DuplicateCandidate);
149 }
150 if candidates
151 .iter()
152 .any(|candidate| !request.contains_candidate(candidate))
153 {
154 return Err(RelationshipSelectionError::ResponseRequestMismatch);
155 }
156 if evidence.is_empty() || evidence.iter().any(|item| !reviewable(item)) {
157 return Err(RelationshipSelectionError::InexactEvidence);
158 }
159 evidence.sort_by(|left, right| {
160 (&left.source, &left.locator).cmp(&(&right.source, &right.locator))
161 });
162 if evidence.windows(2).any(|pair| pair[0] == pair[1]) {
163 return Err(RelationshipSelectionError::DuplicateEvidence);
164 }
165 Ok(Self {
166 request,
167 candidates,
168 evidence,
169 })
170 }
171
172 #[must_use]
174 pub fn request(&self) -> &RelationshipSelectionRequest {
175 &self.request
176 }
177
178 #[must_use]
180 pub fn candidates(&self) -> &[ObjectId] {
181 &self.candidates
182 }
183
184 #[must_use]
186 pub fn evidence(&self) -> &[Evidence] {
187 &self.evidence
188 }
189}
190
191pub trait RelationshipSelectionService: Send + Sync {
193 fn select(
195 &self,
196 request: &RelationshipSelectionRequest,
197 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError>;
198}
199
200#[derive(Clone)]
202pub struct RelationshipSelectionServiceHandle(Arc<dyn RelationshipSelectionService>);
203
204impl RelationshipSelectionServiceHandle {
205 #[must_use]
207 pub fn new(service: Arc<dyn RelationshipSelectionService>) -> Self {
208 Self(service)
209 }
210
211 pub fn select(
213 &self,
214 request: &RelationshipSelectionRequest,
215 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
216 let selection = self.0.select(request)?;
217 if selection.request() != request
218 || selection
219 .candidates()
220 .iter()
221 .any(|candidate| !request.contains_candidate(candidate))
222 {
223 return Err(RelationshipSelectionError::ResponseRequestMismatch);
224 }
225 if selection
226 .candidates()
227 .windows(2)
228 .any(|pair| pair[0] >= pair[1])
229 {
230 return Err(RelationshipSelectionError::DuplicateCandidate);
231 }
232 if selection.evidence().is_empty()
233 || selection.evidence().iter().any(|item| !reviewable(item))
234 {
235 return Err(RelationshipSelectionError::InexactEvidence);
236 }
237 Ok(selection)
238 }
239}
240
241fn reviewable(evidence: &Evidence) -> bool {
242 evidence.exact && !evidence.locator.trim().is_empty()
243}