Skip to main content

knowledge_base_snapshot/
lib.rs

1//! Read-only snapshots of canonical knowledge-base repository resources.
2//!
3//! Loading verifies repository structure and YAML deserialization, but does not
4//! perform generic or domain semantic validation.
5
6#![forbid(unsafe_code)]
7
8use knowledge_base_models::{Entity, EntityId, EntityType, EntityTypeId, IdAllocation, Property, PropertyId, Reference, ReferenceId};
9use std::collections::BTreeMap;
10use std::fmt;
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14
15/// An error loading a structured repository resource.
16#[derive(Debug)]
17#[non_exhaustive]
18pub enum Error {
19    Read { path: PathBuf, source: io::Error },
20    ParseEntity { path: PathBuf, source: serde_yaml::Error },
21    ParseEntityType { path: PathBuf, source: serde_yaml::Error },
22    ParseProperty { path: PathBuf, source: serde_yaml::Error },
23    ParseReference { path: PathBuf, source: serde_yaml::Error },
24    ParseAllocation { path: PathBuf, source: serde_yaml::Error },
25    InvalidSnapshot { path: PathBuf, message: String },
26}
27
28impl Error {
29    /// The absolute or caller-provided path associated with this failure.
30    pub fn path(&self) -> &Path {
31        match self {
32            Self::Read { path, .. }
33            | Self::ParseEntity { path, .. }
34            | Self::ParseEntityType { path, .. }
35            | Self::ParseProperty { path, .. }
36            | Self::ParseReference { path, .. }
37            | Self::ParseAllocation { path, .. }
38            | Self::InvalidSnapshot { path, .. } => path,
39        }
40    }
41}
42
43impl fmt::Display for Error {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Read { path, source } => write!(formatter, "cannot read {}: {source}", path.display()),
47            Self::ParseEntity { path, source } => write!(formatter, "cannot parse entity {}: {source}", path.display()),
48            Self::ParseEntityType { path, source } => write!(formatter, "cannot parse entity type {}: {source}", path.display()),
49            Self::ParseProperty { path, source } => write!(formatter, "cannot parse property {}: {source}", path.display()),
50            Self::ParseReference { path, source } => write!(formatter, "cannot parse reference {}: {source}", path.display()),
51            Self::ParseAllocation { path, source } => write!(formatter, "cannot parse identifier allocation {}: {source}", path.display()),
52            Self::InvalidSnapshot { path, message } => write!(formatter, "cannot load repository snapshot at {}: {message}", path.display()),
53        }
54    }
55}
56
57impl std::error::Error for Error {
58    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59        match self {
60            Self::Read { source, .. } => Some(source),
61            Self::ParseEntity { source, .. }
62            | Self::ParseEntityType { source, .. }
63            | Self::ParseProperty { source, .. }
64            | Self::ParseReference { source, .. }
65            | Self::ParseAllocation { source, .. } => Some(source),
66            Self::InvalidSnapshot { .. } => None,
67        }
68    }
69}
70
71/// A complete, immutable, structurally valid view of a knowledge-base repository.
72///
73/// Resources are indexed by identifiers and iterate in identifier order.
74#[derive(Clone, Debug)]
75pub struct RepositorySnapshot {
76    entities: BTreeMap<EntityId, Entity>,
77    entity_types: BTreeMap<EntityTypeId, EntityType>,
78    properties: BTreeMap<PropertyId, Property>,
79    references: BTreeMap<ReferenceId, Reference>,
80    allocation: IdAllocation,
81}
82
83impl RepositorySnapshot {
84    /// Loads all managed structured resources from `root`.
85    pub fn load(root: impl AsRef<Path>) -> Result<Self, Error> {
86        let root = root.as_ref();
87        Ok(Self {
88            entities: load_resources(root, "entities", |path, source| {
89                serde_yaml::from_slice(source).map_err(|source| Error::ParseEntity { path, source })
90            })?,
91            entity_types: load_resources(root, "entity_types", |path, source| {
92                serde_yaml::from_slice(source).map_err(|source| Error::ParseEntityType { path, source })
93            })?,
94            properties: load_resources(root, "properties", |path, source| {
95                serde_yaml::from_slice(source).map_err(|source| Error::ParseProperty { path, source })
96            })?,
97            references: load_resources(root, "references", |path, source| {
98                serde_yaml::from_slice(source).map_err(|source| Error::ParseReference { path, source })
99            })?,
100            allocation: load_allocation(root)?,
101        })
102    }
103
104    pub fn entities(&self) -> &BTreeMap<EntityId, Entity> {
105        &self.entities
106    }
107    pub fn entity_types(&self) -> &BTreeMap<EntityTypeId, EntityType> {
108        &self.entity_types
109    }
110    pub fn properties(&self) -> &BTreeMap<PropertyId, Property> {
111        &self.properties
112    }
113    pub fn references(&self) -> &BTreeMap<ReferenceId, Reference> {
114        &self.references
115    }
116    pub fn allocation(&self) -> &IdAllocation {
117        &self.allocation
118    }
119}
120
121trait Identified {
122    type Id: Ord + Clone + fmt::Display;
123    fn id(&self) -> &Self::Id;
124}
125impl Identified for Entity {
126    type Id = EntityId;
127    fn id(&self) -> &Self::Id {
128        &self.id
129    }
130}
131impl Identified for EntityType {
132    type Id = EntityTypeId;
133    fn id(&self) -> &Self::Id {
134        &self.id
135    }
136}
137impl Identified for Property {
138    type Id = PropertyId;
139    fn id(&self) -> &Self::Id {
140        &self.id
141    }
142}
143impl Identified for Reference {
144    type Id = ReferenceId;
145    fn id(&self) -> &Self::Id {
146        &self.id
147    }
148}
149
150fn load_resources<T: Identified>(root: &Path, directory: &str, parse: impl Fn(PathBuf, &[u8]) -> Result<T, Error>) -> Result<BTreeMap<T::Id, T>, Error> {
151    let directory_path = root.join(directory);
152    let entries = fs::read_dir(&directory_path).map_err(|source| Error::Read {
153        path: directory_path.clone(),
154        source,
155    })?;
156    let mut paths = Vec::new();
157    for entry in entries {
158        let entry = entry.map_err(|source| Error::Read {
159            path: directory_path.clone(),
160            source,
161        })?;
162        let path = entry.path();
163        let file_type = entry.file_type().map_err(|source| Error::Read { path: path.clone(), source })?;
164        if !file_type.is_file() || path.extension().and_then(|extension| extension.to_str()) != Some("yaml") {
165            return Err(Error::InvalidSnapshot {
166                path,
167                message: format!("managed directory {directory} may contain only YAML files"),
168            });
169        }
170        paths.push(path);
171    }
172    paths.sort();
173    let mut resources = BTreeMap::new();
174    for path in paths {
175        let source = fs::read(&path).map_err(|source| Error::Read { path: path.clone(), source })?;
176        let resource = parse(path.clone(), &source)?;
177        if path.file_stem().and_then(|stem| stem.to_str()) != Some(resource.id().to_string().as_str()) {
178            return Err(Error::InvalidSnapshot {
179                path,
180                message: format!("filename must match declared identifier {}", resource.id()),
181            });
182        }
183        if resources.insert(resource.id().clone(), resource).is_some() {
184            return Err(Error::InvalidSnapshot {
185                path,
186                message: "duplicate resource identifier".to_owned(),
187            });
188        }
189    }
190    Ok(resources)
191}
192
193fn load_allocation(root: &Path) -> Result<IdAllocation, Error> {
194    let path = root.join("id_allocation.yaml");
195    let source = fs::read(&path).map_err(|source| Error::Read { path: path.clone(), source })?;
196    serde_yaml::from_slice(&source).map_err(|source| Error::ParseAllocation { path, source })
197}