1use std::path::{Path, PathBuf};
34
35use thiserror::Error;
36
37use crate::{RObject, RStr, RValue};
38
39mod namespace;
40
41#[cfg(feature = "lazyload")]
42mod installed_code;
43
44#[cfg(feature = "lazyload")]
45pub use crate::inspection::{
46 BodyValidation, DefaultPresence, FailureCause, FailurePhase, Formal, FormalsInspection,
47 FormalsNotApplicable, FormalsUnavailable, FunctionFormals, InspectionExtent, PrefixFailure,
48 StoredKind, StoredObjectInspection,
49};
50
51#[cfg(feature = "lazyload")]
52pub use installed_code::{
53 CodeDbGeneration, CodeDbProvenance, InstalledCodeDb, InstalledCodeError, InstalledCodeOptions,
54 StoredBinding,
55};
56
57pub use namespace::{
58 ImportedName, MetadataField, NamespaceExport, NamespaceImport, NamespaceMetadata, S3MethodName,
59 S3Registration,
60};
61
62#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum InstalledMetadataError {
72 #[error("failed to read installed metadata at {path}: {source}")]
74 Read {
75 path: PathBuf,
76 #[source]
77 source: crate::file::ReadError,
78 },
79 #[error("invalid installed metadata at {path}: {source}")]
82 View {
83 path: PathBuf,
84 #[source]
85 source: ViewError,
86 },
87}
88
89impl InstalledMetadataError {
90 #[must_use]
92 pub fn path(&self) -> &Path {
93 match self {
94 Self::Read { path, .. } | Self::View { path, .. } => path,
95 }
96 }
97}
98
99#[derive(Debug, Error, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum ViewError {
103 #[error("missing value at {path}")]
104 Missing { path: String, field: Option<String> },
105 #[error("unexpected type at {path}: expected {expected}, got {actual}")]
106 UnexpectedType {
107 path: String,
108 field: Option<String>,
109 expected: &'static str,
110 actual: &'static str,
111 },
112 #[error("unexpected length at {path}: expected {expected}, got {actual}")]
113 UnexpectedLength {
114 path: String,
115 field: Option<String>,
116 expected: String,
117 actual: usize,
118 },
119 #[error("duplicate name at {path}")]
120 DuplicateName { path: String, field: Option<String> },
121 #[error("invalid string encoding at {path}")]
122 InvalidStringEncoding {
123 path: String,
124 field: Option<String>,
125 row: Option<usize>,
126 column: Option<String>,
127 },
128 #[error("invalid dimensions at {path}: {reason}")]
129 InvalidDimensions {
130 path: String,
131 field: Option<String>,
132 reason: String,
133 },
134 #[error("invalid package version at {path}: {reason}")]
135 InvalidPackageVersion {
136 path: String,
137 field: Option<String>,
138 reason: String,
139 },
140}
141
142impl ViewError {
143 pub fn path(&self) -> String {
145 match self {
146 Self::Missing { path, .. }
147 | Self::UnexpectedType { path, .. }
148 | Self::UnexpectedLength { path, .. }
149 | Self::DuplicateName { path, .. }
150 | Self::InvalidStringEncoding { path, .. }
151 | Self::InvalidDimensions { path, .. }
152 | Self::InvalidPackageVersion { path, .. } => path.clone(),
153 }
154 }
155
156 pub fn field(&self) -> Option<&str> {
158 match self {
159 Self::Missing { field, .. }
160 | Self::UnexpectedType { field, .. }
161 | Self::UnexpectedLength { field, .. }
162 | Self::DuplicateName { field, .. }
163 | Self::InvalidStringEncoding { field, .. }
164 | Self::InvalidDimensions { field, .. }
165 | Self::InvalidPackageVersion { field, .. } => field.as_deref(),
166 }
167 }
168
169 pub fn row(&self) -> Option<usize> {
171 match self {
172 Self::InvalidStringEncoding { row, .. } => *row,
173 _ => None,
174 }
175 }
176
177 pub fn column(&self) -> Option<&str> {
179 match self {
180 Self::InvalidStringEncoding { column, .. } => column.as_deref(),
181 _ => None,
182 }
183 }
184}
185
186mod meta;
187mod packages;
188
189pub use meta::{Built, PackageMeta, PackageVersion};
190pub use packages::{PackagesColumn, PackagesMatrix, PackagesRow};
191
192fn read_installed_object(
193 package_dir: impl AsRef<Path>,
194 artifact: &str,
195 options: &crate::file::ReadOptions,
196) -> Result<(PathBuf, RObject), InstalledMetadataError> {
197 let path = package_dir.as_ref().join("Meta").join(artifact);
198 let object = crate::file::read_with_options(&path, options).map_err(|source| {
199 InstalledMetadataError::Read {
200 path: path.clone(),
201 source,
202 }
203 })?;
204 Ok((path, object))
205}
206
207fn expect_list<'a>(
208 object: &'a RObject,
209 path: &str,
210 field: Option<&str>,
211) -> Result<&'a [RObject], ViewError> {
212 match &object.value() {
213 RValue::List(values) => Ok(values),
214 value => Err(unexpected_type(path, field, "list", value.kind_name())),
215 }
216}
217
218fn named_values<'a>(
219 object: &'a RObject,
220 path: &str,
221 field: Option<&str>,
222) -> Result<&'a [RStr], ViewError> {
223 let Some(attribute) = object.attributes().get("names") else {
224 return Err(missing(format!("{path}.names"), field.map(str::to_owned)));
225 };
226 match &attribute.value() {
227 RValue::Character(values) => Ok(values),
228 value => Err(unexpected_type(
229 &format!("{path}.names"),
230 field,
231 "character vector",
232 value.kind_name(),
233 )),
234 }
235}
236
237fn decode_optional(
238 value: &RStr,
239 path: &str,
240 field: Option<&str>,
241) -> Result<Option<String>, ViewError> {
242 match value.as_str() {
243 None => Ok(None),
244 Some(Ok(value)) => Ok(Some(value.into_owned())),
245 Some(Err(_)) => Err(ViewError::InvalidStringEncoding {
246 path: path.to_owned(),
247 field: field.map(str::to_owned),
248 row: None,
249 column: None,
250 }),
251 }
252}
253
254fn invalid_dimensions(path: &str, reason: &str) -> ViewError {
255 ViewError::InvalidDimensions {
256 path: path.to_owned(),
257 field: None,
258 reason: reason.to_owned(),
259 }
260}
261
262fn decode_required(value: &RStr, path: &str, field: Option<&str>) -> Result<String, ViewError> {
263 match value.as_str() {
264 None => Err(unexpected_type(path, field, "non-NA string", "NA")),
265 Some(Ok(value)) => Ok(value.into_owned()),
266 Some(Err(_)) => Err(ViewError::InvalidStringEncoding {
267 path: path.to_owned(),
268 field: field.map(str::to_owned),
269 row: None,
270 column: None,
271 }),
272 }
273}
274
275fn missing(path: impl Into<String>, field: Option<String>) -> ViewError {
276 ViewError::Missing {
277 path: path.into(),
278 field,
279 }
280}
281
282fn unexpected_type(
283 path: &str,
284 field: Option<&str>,
285 expected: &'static str,
286 actual: &'static str,
287) -> ViewError {
288 ViewError::UnexpectedType {
289 path: path.to_owned(),
290 field: field.map(str::to_owned),
291 expected,
292 actual,
293 }
294}
295
296fn unexpected_length(
297 path: &str,
298 field: Option<&str>,
299 expected: String,
300 actual: usize,
301) -> ViewError {
302 ViewError::UnexpectedLength {
303 path: path.to_owned(),
304 field: field.map(str::to_owned),
305 expected,
306 actual,
307 }
308}
309
310fn duplicate(path: &str, field: Option<String>) -> ViewError {
311 ViewError::DuplicateName {
312 path: path.to_owned(),
313 field,
314 }
315}
316
317trait ValueKindName {
318 fn kind_name(&self) -> &'static str;
319}
320
321impl ValueKindName for RValue {
322 fn kind_name(&self) -> &'static str {
323 match self {
324 Self::Null => "null",
325 Self::Logical(_) => "logical vector",
326 Self::Integer(_) => "integer vector",
327 Self::Real(_) => "real vector",
328 Self::Character(_) => "character vector",
329 Self::List(_) => "list",
330 Self::Symbol(_) => "symbol",
331 Self::Persisted(_) => "persisted value",
332 Self::Environment(_) => "environment",
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests;