pub use mant_ir::ReferenceTargetType;
use mant_ir::{
ContentLocation, ContentReveal, DocumentAddress, LinkTarget, ReferenceScanReport,
ReferenceScanStop,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ReferenceProjectionMode {
None,
#[default]
Summary,
All,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferenceProjection {
#[serde(default)]
pub mode: ReferenceProjectionMode,
#[serde(default = "default_target_types")]
pub target_types: Vec<ReferenceTargetType>,
#[serde(default)]
pub offset: u32,
#[serde(default = "default_limit")]
pub limit: u32,
}
fn default_target_types() -> Vec<ReferenceTargetType> {
vec![ReferenceTargetType::Document, ReferenceTargetType::Manual]
}
const fn default_limit() -> u32 {
100
}
impl Default for ReferenceProjection {
fn default() -> Self {
Self {
mode: ReferenceProjectionMode::Summary,
target_types: default_target_types(),
offset: 0,
limit: 100,
}
}
}
impl ReferenceProjection {
pub fn validate(&self) -> Result<(), &'static str> {
if self.limit == 0 || self.limit > 1000 {
return Err("reference limit must be between 1 and 1000");
}
if self.target_types.len() > 5 {
return Err("at most five reference target types are accepted");
}
for (index, kind) in self.target_types.iter().enumerate() {
if self.target_types[..index].contains(kind) {
return Err("reference target types must not repeat");
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ReferenceLimitReason {
Steps,
Depth,
Bytes,
Position,
InvalidRoot,
ConsumerStop,
}
impl From<ReferenceScanStop> for ReferenceLimitReason {
fn from(value: ReferenceScanStop) -> Self {
match value {
ReferenceScanStop::Steps => Self::Steps,
ReferenceScanStop::Depth => Self::Depth,
ReferenceScanStop::Bytes => Self::Bytes,
ReferenceScanStop::Position => Self::Position,
ReferenceScanStop::InvalidRoot => Self::InvalidRoot,
ReferenceScanStop::Visitor => Self::ConsumerStop,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ReferenceCoverageStatus {
Complete {},
Limited {
reason: ReferenceLimitReason,
},
NotScanned {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferenceCoverage {
pub steps: u32,
pub bytes: u32,
pub status: ReferenceCoverageStatus,
}
impl ReferenceCoverage {
#[must_use]
pub fn from_report(report: ReferenceScanReport) -> Self {
Self {
steps: u32::try_from(report.steps).unwrap_or(u32::MAX),
bytes: u32::try_from(report.bytes).unwrap_or(u32::MAX),
status: report
.stopped
.map_or(ReferenceCoverageStatus::Complete {}, |reason| {
ReferenceCoverageStatus::Limited {
reason: reason.into(),
}
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ReferenceCount {
Exact {
value: u64,
},
LowerBound {
value: u64,
},
Unknown {
reason: ReferenceUnknownReason,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ReferenceUnknownReason {
Disabled,
NotScanned,
InvalidPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ReferencePageLimit {
Records,
MaterializationBytes,
Position,
Scan,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferencePage {
pub offset: u32,
pub returned: u32,
pub limited: Option<ReferencePageLimit>,
pub next_offset: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ReferenceAssociation {
Unrecorded {},
Valid {
owner: ContentReveal,
forms: Vec<u32>,
},
Invalid {},
Limited {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum UnloadedFragment {
Absent {},
Unchecked {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum LoadedFragment {
Absent {},
Valid {
reveal: ContentReveal,
},
Missing {},
Ambiguous {},
Limited {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ReferenceResolution {
NotApplicable {},
NotQueried {
fragment: UnloadedFragment,
},
MissingContext {
fragment: UnloadedFragment,
},
LogicalAddress {
address: DocumentAddress,
fragment: UnloadedFragment,
},
Loaded {
address: Option<DocumentAddress>,
fragment: LoadedFragment,
},
Restricted {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferenceRecord {
pub origin: ContentLocation,
pub source_read: crate::ContentSelector,
pub owner: Option<ContentReveal>,
pub label: String,
pub label_truncated: bool,
pub target: LinkTarget,
pub association: ReferenceAssociation,
pub resolution: ReferenceResolution,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferenceInventory {
pub policy: ReferenceProjection,
pub coverage: ReferenceCoverage,
pub target_coverage: Option<ReferenceCoverage>,
pub occurrences: ReferenceCount,
pub targets: ReferenceCount,
pub page: ReferencePage,
pub records: Vec<ReferenceRecord>,
}
impl ReferenceInventory {
#[must_use]
pub fn not_scanned(policy: ReferenceProjection) -> Self {
let reason = if policy.mode == ReferenceProjectionMode::None {
ReferenceUnknownReason::Disabled
} else {
ReferenceUnknownReason::NotScanned
};
Self {
page: ReferencePage {
offset: policy.offset,
returned: 0,
limited: None,
next_offset: None,
},
policy,
coverage: ReferenceCoverage {
steps: 0,
bytes: 0,
status: ReferenceCoverageStatus::NotScanned {},
},
target_coverage: None,
occurrences: ReferenceCount::Unknown { reason },
targets: ReferenceCount::Unknown { reason },
records: Vec::new(),
}
}
}
impl Default for ReferenceInventory {
fn default() -> Self {
Self::not_scanned(ReferenceProjection::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn policy_is_bounded_and_resolution_stages_are_closed() {
assert!(ReferenceProjection::default().validate().is_ok());
assert!(
ReferenceProjection {
limit: 1001,
..Default::default()
}
.validate()
.is_err()
);
assert!(
ReferenceProjection {
target_types: vec![ReferenceTargetType::Local; 2],
..Default::default()
}
.validate()
.is_err()
);
for json in [
r#"{"kind":"logical-address","address":{"kind":"markdown","path":"x","origin":{"kind":"documents"}},"fragment":{"kind":"valid","reveal":{"kind":"document"}}}"#,
r#"{"kind":"not-applicable","resolved":true}"#,
r#"{"kind":"loaded","address":null,"fragment":{"kind":"absent","reveal":{"kind":"document"}}}"#,
] {
assert!(
serde_json::from_str::<ReferenceResolution>(json).is_err(),
"{json}"
);
}
}
#[test]
fn count_precision_and_loaded_fragment_wire_have_no_contradictory_variants() {
for value in [
r#"{"kind":"exact","value":1,"reason":"disabled"}"#,
r#"{"kind":"lower-bound"}"#,
r#"{"kind":"unknown","reason":"disabled","value":0}"#,
] {
assert!(
serde_json::from_str::<ReferenceCount>(value).is_err(),
"{value}"
);
}
for value in [
r#"{"kind":"missing","reveal":{"kind":"document"}}"#,
r#"{"kind":"valid"}"#,
r#"{"kind":"ambiguous","address":null}"#,
] {
assert!(
serde_json::from_str::<LoadedFragment>(value).is_err(),
"{value}"
);
}
let valid = ReferenceResolution::LogicalAddress {
address: DocumentAddress::parse_catalog_path("documents/topic").unwrap(),
fragment: UnloadedFragment::Unchecked {},
};
let wire = serde_json::to_string(&valid).unwrap();
assert_eq!(
serde_json::from_str::<ReferenceResolution>(&wire).unwrap(),
valid
);
}
}