use crate::error::{Error, Result};
use crate::field::FieldStore;
use crate::field::document_format::DocumentFormat;
use crate::field::index::{FsIndexStore, IndexEntry, SEL_PAGE, SelectorKey, lookup};
use crate::field::manifest::FieldRoot;
use crate::store::NodeId;
use super::observe::{ObserveRequest, Representation, Selector, derived_nodes};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanShape {
CachedObservation,
IndexLookup,
SourceSlice,
NodeMaterialize,
DeepenThenObserve,
FullMaterialize,
}
impl PlanShape {
pub const fn name(self) -> &'static str {
match self {
PlanShape::CachedObservation => "cached_observation",
PlanShape::IndexLookup => "index_lookup",
PlanShape::SourceSlice => "source_slice",
PlanShape::NodeMaterialize => "node_materialize",
PlanShape::DeepenThenObserve => "deepen_then_observe",
PlanShape::FullMaterialize => "full_materialize",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObservePlan {
pub shape: PlanShape,
pub index_reads: u64,
pub required_nodes: u64,
pub will_materialize: Vec<String>,
pub will_not_materialize: Vec<String>,
}
pub fn plan(manifest: &FieldRoot, store: &FieldStore, req: &ObserveRequest) -> Result<ObservePlan> {
if req.selector.is_common() {
return common_plan(manifest, req);
}
use Representation as R;
match (&req.selector, req.representation) {
(Selector::Document, R::FullDocument | R::ExactBytes) => Ok(ObservePlan {
shape: PlanShape::FullMaterialize,
index_reads: 0,
required_nodes: 1,
will_materialize: kinds(&["DocumentExact"]),
will_not_materialize: Vec::new(),
}),
(Selector::Document, R::Metadata) => Ok(ObservePlan {
shape: PlanShape::CachedObservation,
index_reads: 0,
required_nodes: 0,
will_materialize: Vec::new(),
will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
}),
(Selector::ByteRange { .. }, R::ExactBytes) => Ok(ObservePlan {
shape: PlanShape::SourceSlice,
index_reads: 0,
required_nodes: 0,
will_materialize: kinds(&["SourceSlice"]),
will_not_materialize: kinds(&["whole-document"]),
}),
(Selector::Object(_), R::ExactBytes | R::EncodedBytes) => Ok(index_plan("PdfObject")),
(Selector::Revision(_), R::ExactBytes) => Ok(index_plan("PdfRevision")),
(Selector::Member(_), R::EncodedBytes) => Ok(ObservePlan {
shape: PlanShape::IndexLookup,
index_reads: 1,
required_nodes: 1,
will_materialize: kinds(&["PackageMemberRaw"]),
will_not_materialize: kinds(&["other-members", "whole-document"]),
}),
(Selector::Member(_), R::DecodedBytes) => Ok(ObservePlan {
shape: PlanShape::NodeMaterialize,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["PackageMemberDecoded", "PackageMemberRaw"]),
will_not_materialize: kinds(&["other-members", "whole-document"]),
}),
(Selector::PackagePart(_), R::Metadata) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["PackageOpcModel"]),
will_not_materialize: kinds(&["other-parts", "whole-document"]),
}),
(Selector::PackagePart(_), R::ExactBytes) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 3,
will_materialize: kinds(&["PackageOpcModel", "PackageMemberRaw"]),
will_not_materialize: kinds(&["other-parts", "whole-document"]),
}),
(Selector::PackagePart(_), R::DecodedBytes) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 4,
will_materialize: kinds(&[
"PackageOpcModel",
"PackageMemberDecoded",
"PackageMemberRaw",
]),
will_not_materialize: kinds(&["other-parts", "whole-document"]),
}),
(Selector::Relationship(_), R::Metadata) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["PackageOpcModel"]),
will_not_materialize: kinds(&["other-relationships", "whole-document"]),
}),
(Selector::Relationship(_), R::ExactBytes) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 3,
will_materialize: kinds(&["PackageOpcModel", "PackageMemberRaw"]),
will_not_materialize: kinds(&["external-targets", "whole-document"]),
}),
(Selector::Relationship(_), R::DecodedBytes) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 4,
will_materialize: kinds(&[
"PackageOpcModel",
"PackageMemberDecoded",
"PackageMemberRaw",
]),
will_not_materialize: kinds(&["external-targets", "whole-document"]),
}),
(Selector::Stream(_), R::EncodedBytes) => Ok(index_plan("PdfStreamEncoded")),
(Selector::Stream(_), R::DecodedBytes) => Ok(ObservePlan {
shape: PlanShape::NodeMaterialize,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["PdfStreamDecoded", "PdfStreamEncoded"]),
will_not_materialize: kinds(&["images", "whole-document"]),
}),
(Selector::Stream(_), R::Operators) => Ok(ObservePlan {
shape: PlanShape::NodeMaterialize,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["PdfStreamDecoded", "ContentOperators"]),
will_not_materialize: kinds(&["images", "whole-document"]),
}),
(Selector::Page(n), R::Text) => page_plan(manifest, store, *n, PageRepr::Text),
(Selector::Page(n), R::Preview) => page_plan(manifest, store, *n, PageRepr::Preview),
(Selector::Page(n), R::Structure) => page_plan(manifest, store, *n, PageRepr::Structure),
(Selector::TextMatch(_), R::Text) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 1,
required_nodes: 3,
will_materialize: kinds(&["PageContent", "ContentOperators", "TextRuns"]),
will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
}),
#[cfg(feature = "docx")]
(Selector::DocxStory { .. }, R::Text | R::Structure | R::Metadata)
| (Selector::DocxParagraph { .. }, R::Text | R::Metadata)
| (Selector::DocxTable { .. }, R::Text | R::Metadata)
| (Selector::DocxCell { .. }, R::Text | R::Metadata)
| (Selector::DocxFind { .. }, R::Text) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 3,
required_nodes: 4,
will_materialize: kinds(&[
"DocxModel",
"DocxStory",
"PackageMemberDecoded",
"PackageMemberRaw",
]),
will_not_materialize: kinds(&["other-stories", "whole-document"]),
}),
#[cfg(feature = "epub")]
(Selector::EpubPackage, R::Metadata | R::Structure)
| (Selector::EpubNav, R::Metadata | R::Structure)
| (Selector::EpubNavNode { .. }, R::Metadata) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 1,
required_nodes: 3,
will_materialize: kinds(&["EpubModel", "PackageMemberDecoded", "PackageMemberRaw"]),
will_not_materialize: kinds(&["other-resources", "whole-document"]),
}),
#[cfg(feature = "epub")]
(Selector::EpubManifestItem { .. }, R::Metadata)
| (Selector::EpubSpineItem { .. }, R::Metadata)
| (Selector::EpubResource(_), R::Metadata) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 1,
required_nodes: 2,
will_materialize: kinds(&["EpubModel"]),
will_not_materialize: kinds(&["external-targets", "whole-document"]),
}),
#[cfg(feature = "epub")]
(Selector::EpubSpineItem { .. }, R::Text | R::Structure | R::Preview)
| (Selector::EpubBlock { .. }, R::Text | R::Metadata | R::Structure)
| (Selector::EpubCell { .. }, R::Text | R::Metadata)
| (Selector::EpubLink { .. }, R::Metadata)
| (Selector::EpubFind { .. }, R::Text) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 4,
will_materialize: kinds(&[
"EpubModel",
"EpubContent",
"PackageMemberDecoded",
"PackageMemberRaw",
]),
will_not_materialize: kinds(&["other-spine-items", "whole-document"]),
}),
#[cfg(feature = "epub")]
(Selector::EpubManifestItem { .. }, R::ExactBytes | R::DecodedBytes)
| (Selector::EpubSpineItem { .. }, R::ExactBytes | R::DecodedBytes)
| (Selector::EpubResource(_), R::ExactBytes | R::DecodedBytes) => Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 3,
will_materialize: kinds(&["EpubModel", "PackageMemberRaw", "PackageMemberDecoded"]),
will_not_materialize: kinds(&["external-targets", "whole-document"]),
}),
_ => Err(Error::unsupported_feature(format!(
"unsupported observation: selector {} with representation {}",
req.selector.canonical(),
req.representation.name()
))),
}
}
#[derive(Debug, Clone, Copy)]
enum PageRepr {
Text,
Preview,
Structure,
}
fn kinds(names: &[&str]) -> Vec<String> {
names.iter().map(|s| (*s).to_string()).collect()
}
fn index_plan(kind: &str) -> ObservePlan {
ObservePlan {
shape: PlanShape::IndexLookup,
index_reads: 1,
required_nodes: 1,
will_materialize: kinds(&[kind]),
will_not_materialize: kinds(&["images", "xobjects", "PageContent", "whole-document"]),
}
}
fn common_plan(manifest: &FieldRoot, req: &ObserveRequest) -> Result<ObservePlan> {
use crate::field::capabilities;
let fmt = DocumentFormat::from_provenance(&manifest.provenance).ok_or_else(|| {
Error::unsupported_feature(
"field manifest does not record a document format; common observations are unavailable",
)
})?;
if !capabilities::common_supported(fmt, &req.selector, req.representation) {
return Err(Error::unsupported_feature(format!(
"unsupported common observation: format {} does not support selector {} with representation {}",
fmt.name(),
req.selector.canonical(),
req.representation.name()
)));
}
if matches!(req.selector, Selector::Metadata) {
return Ok(ObservePlan {
shape: PlanShape::CachedObservation,
index_reads: 0,
required_nodes: 0,
will_materialize: Vec::new(),
will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
});
}
Ok(ObservePlan {
shape: PlanShape::DeepenThenObserve,
index_reads: 2,
required_nodes: 4,
will_materialize: kinds(common_materialize(fmt)),
will_not_materialize: kinds(&["other-spine-items", "other-stories", "whole-document"]),
})
}
fn common_materialize(fmt: DocumentFormat) -> &'static [&'static str] {
match fmt {
DocumentFormat::Pdf => &["PageContent", "TextRuns"],
DocumentFormat::Docx => &[
"DocxModel",
"DocxStory",
"PackageMemberDecoded",
"PackageMemberRaw",
],
DocumentFormat::Epub => &[
"EpubModel",
"EpubContent",
"PackageMemberDecoded",
"PackageMemberRaw",
],
DocumentFormat::Opaque => &[],
}
}
fn index_entries(
manifest: &FieldRoot,
store: &FieldStore,
key: SelectorKey,
) -> Result<Vec<IndexEntry>> {
if !manifest.has_index() {
return Ok(Vec::new());
}
let istore = FsIndexStore::open(store.root())?;
let root = NodeId::from_bytes(manifest.index_root);
lookup(&istore, &root, &key)
}
fn page_plan(
manifest: &FieldRoot,
store: &FieldStore,
page: u32,
repr: PageRepr,
) -> Result<ObservePlan> {
let entry = index_entries(manifest, store, SelectorKey::new(SEL_PAGE, page))?
.into_iter()
.next()
.ok_or_else(|| {
Error::unsupported_feature(format!(
"no page matching selector number {page} in the observation index"
))
})?;
let (_ops, text, preview) = derived_nodes(page, entry.node_id);
let target = match repr {
PageRepr::Text => text,
PageRepr::Preview | PageRepr::Structure => preview,
};
let present = store.seeds().contains_node(&target.content_id())?;
let shape = if present {
PlanShape::NodeMaterialize
} else {
PlanShape::DeepenThenObserve
};
let (materialize, required) = match repr {
PageRepr::Text => (kinds(&["PageContent", "ContentOperators", "TextRuns"]), 3),
PageRepr::Preview => (kinds(&["PageContent", "PagePreview"]), 2),
PageRepr::Structure => (
kinds(&["PageContent", "PagePreview", "ContentOperators"]),
3,
),
};
Ok(ObservePlan {
shape,
index_reads: 1,
required_nodes: required,
will_materialize: materialize,
will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_names_are_lower_snake_case() {
for shape in [
PlanShape::CachedObservation,
PlanShape::IndexLookup,
PlanShape::SourceSlice,
PlanShape::NodeMaterialize,
PlanShape::DeepenThenObserve,
PlanShape::FullMaterialize,
] {
let name = shape.name();
assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
}
}
}