Skip to main content

morphir_core/metadata/
admission.rs

1//! Semantic admission of expanded facts using declarations supplied by the caller.
2//!
3//! A reader can retain an expanded fact before its declaration is available.
4//! Admission establishes when it has validated meaning; this module does not
5//! acquire or authenticate the declaration provider.
6
7use super::{Carrier, Fact, ObjectTerm};
8use crate::data_value::{DataValueError, DataValueValidator};
9use crate::ir::v4;
10use crate::naming::FQName;
11use crate::node_address::{NodeRoot, NodeUri};
12use serde_json::Value;
13use std::collections::{BTreeMap, BTreeSet};
14
15/// Semantic role of an addressed subject in its supplying IR artifact.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub enum SubjectRole {
18    TypeSpecification,
19    TypeDefinition,
20    ValueSpecification,
21    ValueDefinition,
22    TypeExpression,
23    ValueExpression,
24    Pattern,
25    Module,
26    Package,
27}
28
29/// The declaration's representation of one expanded object.
30#[derive(Debug, Clone)]
31pub enum ObjectDeclaration {
32    Data(v4::Type),
33    Json {
34        datatype: NodeUri,
35        ty: DeclaredDataType,
36    },
37    Node(NodeTargetKind),
38}
39
40impl ObjectDeclaration {
41    pub fn data(ty: v4::Type) -> Self {
42        Self::Data(ty)
43    }
44
45    /// One structured `@json` object checked against a V4 type.
46    pub fn json(datatype: NodeUri, ty: v4::Type) -> Self {
47        Self::Json {
48            datatype,
49            ty: DeclaredDataType::V4(Box::new(ty)),
50        }
51    }
52
53    /// A V3 sidecar entry point's closed type, projected as `@json`.
54    pub fn sidecar_json(datatype: NodeUri, entry_point: FQName) -> Self {
55        Self::Json {
56            datatype,
57            ty: DeclaredDataType::V3(entry_point),
58        }
59    }
60
61    pub fn node(target: NodeTargetKind) -> Self {
62        Self::Node(target)
63    }
64}
65
66#[derive(Debug, Clone)]
67pub enum DeclaredDataType {
68    V3(FQName),
69    V4(Box<v4::Type>),
70}
71
72/// The resolved target's broad IR node kind, supplied by the provider.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum NodeTargetKind {
75    Type,
76    Value,
77    Module,
78    Package,
79}
80
81/// A semantic interpreter supported by this bounded admission layer.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Interpreter {
84    TargetNameLanguageIds,
85}
86
87/// A predicate's semantic interpretation after its type has been checked.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Interpretation {
90    Descriptive,
91    Required(Interpreter),
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95enum DeclarationRole {
96    ValueSpecification,
97    SidecarTypeSpecification,
98}
99
100/// One verified predicate supplied by an external declaration closure.
101#[derive(Debug, Clone)]
102pub struct PredicateDeclaration {
103    uri: NodeUri,
104    role: DeclarationRole,
105    object: ObjectDeclaration,
106    subjects: BTreeSet<SubjectRole>,
107    interpretation: Interpretation,
108}
109
110impl PredicateDeclaration {
111    pub fn value(
112        uri: NodeUri,
113        object: ObjectDeclaration,
114        subjects: impl IntoIterator<Item = SubjectRole>,
115        interpretation: Interpretation,
116    ) -> Self {
117        Self {
118            uri,
119            role: DeclarationRole::ValueSpecification,
120            object,
121            subjects: subjects.into_iter().collect(),
122            interpretation,
123        }
124    }
125
126    /// One legacy decorator type specification admitted only through its sidecar.
127    pub fn sidecar_type(
128        uri: NodeUri,
129        object: ObjectDeclaration,
130        subjects: impl IntoIterator<Item = SubjectRole>,
131    ) -> Self {
132        Self {
133            uri,
134            role: DeclarationRole::SidecarTypeSpecification,
135            object,
136            subjects: subjects.into_iter().collect(),
137            interpretation: Interpretation::Descriptive,
138        }
139    }
140
141    /// The declared datatype needed by `@json` context expansion.
142    pub fn json_datatype(&self) -> Option<&NodeUri> {
143        match &self.object {
144            ObjectDeclaration::Json { datatype, .. } => Some(datatype),
145            _ => None,
146        }
147    }
148}
149
150/// A fact's semantic validation state.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Admission {
153    Validated,
154    PreservedUnvalidated(UnvalidatedReason),
155}
156
157/// Why an expanded fact remains available for roundtrip but lacks validated meaning.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum UnvalidatedReason {
160    PredicateDeclaration,
161    Target,
162    RequiredInterpreter(Interpreter),
163}
164
165/// A declaration or expanded fact violates the supplied closure.
166#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
167pub enum AdmissionError {
168    #[error("predicate URI root does not match its declaration role")]
169    InvalidDeclarationRole,
170    #[error("predicate closure contains a duplicate declaration")]
171    DuplicatePredicate,
172    #[error("@json datatype must name a type declaration")]
173    InvalidDatatypeDeclaration,
174    #[error("predicate declaration must allow at least one subject role")]
175    EmptySubjectRoles,
176    #[error("node-local carrier does not match the fact subject")]
177    CarrierSubjectMismatch,
178    #[error("type-specification predicate requires its matching sidecar carrier")]
179    DeclarationCarrierMismatch,
180    #[error("fact subject role is not allowed by predicate declaration")]
181    SubjectRoleMismatch,
182    #[error("predicate object does not match its declared object kind")]
183    ObjectKindMismatch,
184    #[error("typed JSON datatype does not match its predicate declaration")]
185    DatatypeMismatch,
186    #[error("node reference target kind does not match its predicate declaration")]
187    NodeTargetKindMismatch,
188    #[error("invalid language ID at {path}")]
189    InvalidLanguageId { path: String },
190    #[error("semantic interpreter received a value with the wrong structure")]
191    InterpreterInputMismatch,
192    #[error(transparent)]
193    DataValue(#[from] DataValueError),
194}
195
196/// Predicate declarations bound to the caller's authenticated provider set.
197#[derive(Debug, Default)]
198pub struct PredicateClosure {
199    declarations: BTreeMap<String, PredicateDeclaration>,
200}
201
202impl PredicateClosure {
203    pub fn new(
204        declarations: impl IntoIterator<Item = PredicateDeclaration>,
205    ) -> Result<Self, AdmissionError> {
206        let mut indexed = BTreeMap::new();
207        for declaration in declarations {
208            let expected_root = match declaration.role {
209                DeclarationRole::ValueSpecification => {
210                    declaration.uri.format().major() == 4
211                        && matches!(declaration.uri.root(), NodeRoot::Value { .. })
212                }
213                DeclarationRole::SidecarTypeSpecification => {
214                    matches!(declaration.uri.format().major(), 3 | 4)
215                        && matches!(declaration.uri.root(), NodeRoot::Type { .. })
216                        && matches!(declaration.object, ObjectDeclaration::Json { .. })
217                }
218            };
219            if !expected_root || !declaration.uri.steps().is_empty() {
220                return Err(AdmissionError::InvalidDeclarationRole);
221            }
222            if let ObjectDeclaration::Json { datatype, ty } = &declaration.object {
223                let type_major = match ty {
224                    DeclaredDataType::V3(_) => 3,
225                    DeclaredDataType::V4(_) => 4,
226                };
227                if !matches!(datatype.root(), NodeRoot::Type { .. })
228                    || !datatype.steps().is_empty()
229                    || datatype.format().major() != type_major
230                    || (matches!(ty, DeclaredDataType::V3(_))
231                        && declaration.role != DeclarationRole::SidecarTypeSpecification)
232                {
233                    return Err(AdmissionError::InvalidDatatypeDeclaration);
234                }
235            }
236            if declaration.subjects.is_empty() {
237                return Err(AdmissionError::EmptySubjectRoles);
238            }
239            let key = declaration.uri.to_string();
240            if indexed.insert(key, declaration).is_some() {
241                return Err(AdmissionError::DuplicatePredicate);
242            }
243        }
244        Ok(Self {
245            declarations: indexed,
246        })
247    }
248
249    /// Look up the typed JSON declaration for context expansion.
250    pub fn json_datatype(&self, predicate: &NodeUri) -> Option<NodeUri> {
251        self.declarations
252            .get(&predicate.to_string())
253            .and_then(PredicateDeclaration::json_datatype)
254            .cloned()
255    }
256
257    /// Validate a fact after context expansion and subject-role resolution.
258    pub fn admit(
259        &self,
260        fact: &Fact,
261        subject: SubjectRole,
262        carrier: &Carrier,
263        validator: &DataValueValidator,
264        available_interpreters: &[Interpreter],
265        target_kind: Option<NodeTargetKind>,
266    ) -> Result<Admission, AdmissionError> {
267        match carrier {
268            Carrier::AttributesFacts(uri) | Carrier::AnnotationsFacts(uri)
269                if uri != fact.subject() =>
270            {
271                return Err(AdmissionError::CarrierSubjectMismatch);
272            }
273            Carrier::Sidecar { target, .. } if target != fact.subject() => {
274                return Err(AdmissionError::CarrierSubjectMismatch);
275            }
276            _ => {}
277        }
278        let Some(declaration) = self.declarations.get(&fact.predicate().to_string()) else {
279            return Ok(Admission::PreservedUnvalidated(
280                UnvalidatedReason::PredicateDeclaration,
281            ));
282        };
283        match (declaration.role, carrier) {
284            (DeclarationRole::ValueSpecification, Carrier::Sidecar { .. }) => {
285                return Err(AdmissionError::DeclarationCarrierMismatch);
286            }
287            (DeclarationRole::SidecarTypeSpecification, Carrier::Sidecar { entry_point, .. })
288                if entry_point == fact.predicate() => {}
289            (DeclarationRole::SidecarTypeSpecification, _) => {
290                return Err(AdmissionError::DeclarationCarrierMismatch);
291            }
292            _ => {}
293        }
294        if !declaration.subjects.contains(&subject) {
295            return Err(AdmissionError::SubjectRoleMismatch);
296        }
297        match (&declaration.object, fact.object()) {
298            (ObjectDeclaration::Data(ty), ObjectTerm::Value(value))
299                if value.datatype().is_none() =>
300            {
301                validator.validate_v4_data(ty, value.value())?;
302            }
303            (ObjectDeclaration::Json { datatype, ty }, ObjectTerm::Value(value)) => {
304                if value.datatype() != Some(datatype) {
305                    return Err(AdmissionError::DatatypeMismatch);
306                }
307                match ty {
308                    DeclaredDataType::V3(entry_point) => {
309                        validator.validate_reference(entry_point, value.value())?;
310                    }
311                    DeclaredDataType::V4(ty) => {
312                        validator.validate_v4_data(ty, value.value())?;
313                    }
314                }
315            }
316            (ObjectDeclaration::Node(expected), ObjectTerm::NodeRef(target)) => {
317                if !target.steps().is_empty() || !target_root_matches(*expected, target.root()) {
318                    return Err(AdmissionError::NodeTargetKindMismatch);
319                }
320                match target_kind {
321                    Some(actual) if actual == *expected => {}
322                    Some(_) => return Err(AdmissionError::NodeTargetKindMismatch),
323                    None => {
324                        return Ok(Admission::PreservedUnvalidated(UnvalidatedReason::Target));
325                    }
326                }
327            }
328            _ => return Err(AdmissionError::ObjectKindMismatch),
329        }
330        if let Interpretation::Required(interpreter) = declaration.interpretation {
331            if !available_interpreters.contains(&interpreter) {
332                return Ok(Admission::PreservedUnvalidated(
333                    UnvalidatedReason::RequiredInterpreter(interpreter),
334                ));
335            }
336            match interpreter {
337                Interpreter::TargetNameLanguageIds => {
338                    validate_target_name_language_ids(fact.object())?;
339                }
340            }
341        }
342        Ok(Admission::Validated)
343    }
344}
345
346fn target_root_matches(expected: NodeTargetKind, root: &NodeRoot) -> bool {
347    matches!(
348        (expected, root),
349        (NodeTargetKind::Type, NodeRoot::Type { .. })
350            | (NodeTargetKind::Value, NodeRoot::Value { .. })
351            | (NodeTargetKind::Module, NodeRoot::Module { .. })
352            | (NodeTargetKind::Package, NodeRoot::Package)
353    )
354}
355
356fn validate_target_name_language_ids(object: &ObjectTerm) -> Result<(), AdmissionError> {
357    let ObjectTerm::Value(value) = object else {
358        return Err(AdmissionError::InterpreterInputMismatch);
359    };
360    let Some(record) = value.value().as_object() else {
361        return Err(AdmissionError::InterpreterInputMismatch);
362    };
363    for field in ["frontend", "backend"] {
364        let Some(languages) = record.get(field).and_then(Value::as_object) else {
365            return Err(AdmissionError::InterpreterInputMismatch);
366        };
367        for language in languages.keys() {
368            if !valid_language_id(language) {
369                return Err(AdmissionError::InvalidLanguageId {
370                    path: format!("$.{field}{}", json_member_path(language)),
371                });
372            }
373        }
374    }
375    Ok(())
376}
377
378fn valid_language_id(value: &str) -> bool {
379    let mut segments = value.split('-');
380    let Some(first) = segments.next() else {
381        return false;
382    };
383    let mut chars = first.bytes();
384    matches!(chars.next(), Some(b'a'..=b'z'))
385        && chars.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
386        && segments.all(|segment| {
387            !segment.is_empty()
388                && segment
389                    .bytes()
390                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
391        })
392}
393
394fn json_member_path(member: &str) -> String {
395    if member
396        .bytes()
397        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
398        && member
399            .bytes()
400            .next()
401            .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
402    {
403        format!(".{member}")
404    } else {
405        format!(
406            "[{}]",
407            serde_json::to_string(member).expect("JSON member names serialize")
408        )
409    }
410}