Skip to main content

runmat_package/source/
inventory.rs

1use crate::{ContentDigest, IdentityError, NormalizedRelativePath};
2use runmat_config::project::ProjectSourceIndex;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5
6pub const SOURCE_INVENTORY_SCHEMA_VERSION: u32 = 1;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct SourceInventoryEntry {
11    pub source_root: NormalizedRelativePath,
12    pub relative_path: NormalizedRelativePath,
13    pub qualified_name: String,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub package_path: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub class_name: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub class_qualified_name: Option<String>,
20    pub is_private: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct SourceInventory {
26    pub schema_version: u32,
27    pub tree_digest: ContentDigest,
28    pub entries: Vec<SourceInventoryEntry>,
29    pub package_dirs: Vec<NormalizedRelativePath>,
30    pub class_dirs: Vec<NormalizedRelativePath>,
31    pub private_dirs: Vec<NormalizedRelativePath>,
32}
33
34impl SourceInventory {
35    pub fn from_project_index(
36        tree_digest: ContentDigest,
37        index: ProjectSourceIndex,
38    ) -> Result<Self, IdentityError> {
39        let mut entries = index
40            .files
41            .into_iter()
42            .map(|source| {
43                Ok(SourceInventoryEntry {
44                    source_root: NormalizedRelativePath::new(source.source_root)?,
45                    relative_path: NormalizedRelativePath::new(source.relative_path)?,
46                    qualified_name: source.qualified_name,
47                    package_path: source.package_path,
48                    class_name: source.class_name,
49                    class_qualified_name: source.class_qualified_name,
50                    is_private: source.is_private,
51                })
52            })
53            .collect::<Result<Vec<_>, IdentityError>>()?;
54        entries.sort();
55        let package_dirs = normalize_paths(index.package_dirs)?;
56        let class_dirs = normalize_paths(index.class_dirs)?;
57        let private_dirs = normalize_paths(index.private_dirs)?;
58        let inventory = Self {
59            schema_version: SOURCE_INVENTORY_SCHEMA_VERSION,
60            tree_digest,
61            entries,
62            package_dirs,
63            class_dirs,
64            private_dirs,
65        };
66        inventory
67            .validate()
68            .map_err(|reason| IdentityError::InvalidRelativePath {
69                value: "source inventory".to_string(),
70                reason,
71            })?;
72        Ok(inventory)
73    }
74
75    pub fn validate(&self) -> Result<(), &'static str> {
76        if self.schema_version != SOURCE_INVENTORY_SCHEMA_VERSION {
77            return Err("unsupported source inventory schema");
78        }
79        if self.entries.windows(2).any(|pair| {
80            pair[0] >= pair[1]
81                || (&pair[0].source_root, &pair[0].relative_path)
82                    == (&pair[1].source_root, &pair[1].relative_path)
83        }) {
84            return Err("source inventory entries must be strictly sorted");
85        }
86        if !strict_paths(&self.package_dirs)
87            || !strict_paths(&self.class_dirs)
88            || !strict_paths(&self.private_dirs)
89        {
90            return Err("source inventory directories must be strictly sorted");
91        }
92        if self
93            .entries
94            .iter()
95            .any(|entry| entry.qualified_name.trim().is_empty())
96        {
97            return Err("source inventory qualified names must be non-empty");
98        }
99        Ok(())
100    }
101}
102
103fn normalize_paths(
104    paths: Vec<std::path::PathBuf>,
105) -> Result<Vec<NormalizedRelativePath>, IdentityError> {
106    paths
107        .into_iter()
108        .map(NormalizedRelativePath::new)
109        .collect::<Result<BTreeSet<_>, _>>()
110        .map(|paths| paths.into_iter().collect())
111}
112
113fn strict_paths(paths: &[NormalizedRelativePath]) -> bool {
114    paths.windows(2).all(|pair| pair[0] < pair[1])
115}