1pub use mant_ir::ReferenceTargetType;
4use mant_ir::{
5 ContentLocation, ContentReveal, DocumentAddress, LinkTarget, ReferenceScanReport,
6 ReferenceScanStop,
7};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "kebab-case")]
14pub enum ReferenceProjectionMode {
15 None,
17 #[default]
19 Summary,
20 All,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ReferenceProjection {
28 #[serde(default)]
30 pub mode: ReferenceProjectionMode,
31 #[serde(default = "default_target_types")]
33 pub target_types: Vec<ReferenceTargetType>,
34 #[serde(default)]
36 pub offset: u32,
37 #[serde(default = "default_limit")]
39 pub limit: u32,
40}
41
42fn default_target_types() -> Vec<ReferenceTargetType> {
43 vec![ReferenceTargetType::Document, ReferenceTargetType::Manual]
44}
45const fn default_limit() -> u32 {
46 100
47}
48
49impl Default for ReferenceProjection {
50 fn default() -> Self {
51 Self {
52 mode: ReferenceProjectionMode::Summary,
53 target_types: default_target_types(),
54 offset: 0,
55 limit: 100,
56 }
57 }
58}
59
60impl ReferenceProjection {
61 pub fn validate(&self) -> Result<(), &'static str> {
66 if self.limit == 0 || self.limit > 1000 {
67 return Err("reference limit must be between 1 and 1000");
68 }
69 if self.target_types.len() > 5 {
70 return Err("at most five reference target types are accepted");
71 }
72 for (index, kind) in self.target_types.iter().enumerate() {
73 if self.target_types[..index].contains(kind) {
74 return Err("reference target types must not repeat");
75 }
76 }
77 Ok(())
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "kebab-case")]
84pub enum ReferenceLimitReason {
85 Steps,
87 Depth,
89 Bytes,
91 Position,
93 InvalidRoot,
95 ConsumerStop,
97}
98
99impl From<ReferenceScanStop> for ReferenceLimitReason {
100 fn from(value: ReferenceScanStop) -> Self {
101 match value {
102 ReferenceScanStop::Steps => Self::Steps,
103 ReferenceScanStop::Depth => Self::Depth,
104 ReferenceScanStop::Bytes => Self::Bytes,
105 ReferenceScanStop::Position => Self::Position,
106 ReferenceScanStop::InvalidRoot => Self::InvalidRoot,
107 ReferenceScanStop::Visitor => Self::ConsumerStop,
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
115pub enum ReferenceCoverageStatus {
116 Complete {},
118 Limited {
120 reason: ReferenceLimitReason,
122 },
123 NotScanned {},
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct ReferenceCoverage {
131 pub steps: u32,
133 pub bytes: u32,
135 pub status: ReferenceCoverageStatus,
137}
138
139impl ReferenceCoverage {
140 #[must_use]
142 pub fn from_report(report: ReferenceScanReport) -> Self {
143 Self {
144 steps: u32::try_from(report.steps).unwrap_or(u32::MAX),
145 bytes: u32::try_from(report.bytes).unwrap_or(u32::MAX),
146 status: report
147 .stopped
148 .map_or(ReferenceCoverageStatus::Complete {}, |reason| {
149 ReferenceCoverageStatus::Limited {
150 reason: reason.into(),
151 }
152 }),
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
159#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
160pub enum ReferenceCount {
161 Exact {
163 value: u64,
165 },
166 LowerBound {
168 value: u64,
170 },
171 Unknown {
173 reason: ReferenceUnknownReason,
175 },
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
180#[serde(rename_all = "kebab-case")]
181pub enum ReferenceUnknownReason {
182 Disabled,
184 NotScanned,
186 InvalidPolicy,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "kebab-case")]
193pub enum ReferencePageLimit {
194 Records,
196 MaterializationBytes,
198 Position,
200 Scan,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
206#[serde(rename_all = "camelCase", deny_unknown_fields)]
207pub struct ReferencePage {
208 pub offset: u32,
210 pub returned: u32,
212 pub limited: Option<ReferencePageLimit>,
214 pub next_offset: Option<u32>,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
221pub enum ReferenceAssociation {
222 Unrecorded {},
224 Valid {
226 owner: ContentReveal,
229 forms: Vec<u32>,
231 },
232 Invalid {},
234 Limited {},
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
241pub enum UnloadedFragment {
242 Absent {},
244 Unchecked {},
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
250#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
251pub enum LoadedFragment {
252 Absent {},
254 Valid {
256 reveal: ContentReveal,
258 },
259 Missing {},
261 Ambiguous {},
263 Limited {},
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
269#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
270pub enum ReferenceResolution {
271 NotApplicable {},
273 NotQueried {
275 fragment: UnloadedFragment,
277 },
278 MissingContext {
280 fragment: UnloadedFragment,
282 },
283 LogicalAddress {
285 address: DocumentAddress,
287 fragment: UnloadedFragment,
289 },
290 Loaded {
292 address: Option<DocumentAddress>,
294 fragment: LoadedFragment,
296 },
297 Restricted {},
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304pub struct ReferenceRecord {
305 pub origin: ContentLocation,
307 pub source_read: crate::ContentSelector,
310 pub owner: Option<ContentReveal>,
312 pub label: String,
314 pub label_truncated: bool,
316 pub target: LinkTarget,
318 pub association: ReferenceAssociation,
320 pub resolution: ReferenceResolution,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
326#[serde(rename_all = "camelCase", deny_unknown_fields)]
327pub struct ReferenceInventory {
328 pub policy: ReferenceProjection,
330 pub coverage: ReferenceCoverage,
332 pub target_coverage: Option<ReferenceCoverage>,
335 pub occurrences: ReferenceCount,
337 pub targets: ReferenceCount,
339 pub page: ReferencePage,
341 pub records: Vec<ReferenceRecord>,
343}
344
345impl ReferenceInventory {
346 #[must_use]
348 pub fn not_scanned(policy: ReferenceProjection) -> Self {
349 let reason = if policy.mode == ReferenceProjectionMode::None {
350 ReferenceUnknownReason::Disabled
351 } else {
352 ReferenceUnknownReason::NotScanned
353 };
354 Self {
355 page: ReferencePage {
356 offset: policy.offset,
357 returned: 0,
358 limited: None,
359 next_offset: None,
360 },
361 policy,
362 coverage: ReferenceCoverage {
363 steps: 0,
364 bytes: 0,
365 status: ReferenceCoverageStatus::NotScanned {},
366 },
367 target_coverage: None,
368 occurrences: ReferenceCount::Unknown { reason },
369 targets: ReferenceCount::Unknown { reason },
370 records: Vec::new(),
371 }
372 }
373}
374
375impl Default for ReferenceInventory {
376 fn default() -> Self {
377 Self::not_scanned(ReferenceProjection::default())
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 #[test]
385 fn policy_is_bounded_and_resolution_stages_are_closed() {
386 assert!(ReferenceProjection::default().validate().is_ok());
387 assert!(
388 ReferenceProjection {
389 limit: 1001,
390 ..Default::default()
391 }
392 .validate()
393 .is_err()
394 );
395 assert!(
396 ReferenceProjection {
397 target_types: vec![ReferenceTargetType::Local; 2],
398 ..Default::default()
399 }
400 .validate()
401 .is_err()
402 );
403 for json in [
404 r#"{"kind":"logical-address","address":{"kind":"markdown","path":"x","origin":{"kind":"documents"}},"fragment":{"kind":"valid","reveal":{"kind":"document"}}}"#,
405 r#"{"kind":"not-applicable","resolved":true}"#,
406 r#"{"kind":"loaded","address":null,"fragment":{"kind":"absent","reveal":{"kind":"document"}}}"#,
407 ] {
408 assert!(
409 serde_json::from_str::<ReferenceResolution>(json).is_err(),
410 "{json}"
411 );
412 }
413 }
414
415 #[test]
416 fn count_precision_and_loaded_fragment_wire_have_no_contradictory_variants() {
417 for value in [
418 r#"{"kind":"exact","value":1,"reason":"disabled"}"#,
419 r#"{"kind":"lower-bound"}"#,
420 r#"{"kind":"unknown","reason":"disabled","value":0}"#,
421 ] {
422 assert!(
423 serde_json::from_str::<ReferenceCount>(value).is_err(),
424 "{value}"
425 );
426 }
427 for value in [
428 r#"{"kind":"missing","reveal":{"kind":"document"}}"#,
429 r#"{"kind":"valid"}"#,
430 r#"{"kind":"ambiguous","address":null}"#,
431 ] {
432 assert!(
433 serde_json::from_str::<LoadedFragment>(value).is_err(),
434 "{value}"
435 );
436 }
437 let valid = ReferenceResolution::LogicalAddress {
438 address: DocumentAddress::parse_catalog_path("documents/topic").unwrap(),
439 fragment: UnloadedFragment::Unchecked {},
440 };
441 let wire = serde_json::to_string(&valid).unwrap();
442 assert_eq!(
443 serde_json::from_str::<ReferenceResolution>(&wire).unwrap(),
444 valid
445 );
446 }
447}