1#![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#[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 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#[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 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}