Skip to main content

artifactize_core/config/
site.rs

1//! Where each Artifact is declared, and one Artifact's declarations read alone: what an
2//! Artifact's Detail needs without reading the whole workspace.
3
4use super::discovery::{FileTargets, Found, Reach, declaration, declare, discovery_ignore, walk};
5use super::*;
6
7/// Where an Artifact is declared: its kind and logical path, as a full read places it.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Site {
10    pub kind: ArtifactKind,
11    pub path: PathBuf,
12}
13
14impl Site {
15    /// The folder the Artifact's declaration is in.
16    pub fn folder(&self) -> &Path {
17        folder_of(self.kind, &self.path)
18    }
19
20    /// The declaration file's logical path.
21    pub fn declaration_path(&self) -> PathBuf {
22        declaration_of(self.kind, &self.path)
23    }
24}
25
26pub(super) fn folder_of(kind: ArtifactKind, path: &Path) -> &Path {
27    match kind {
28        ArtifactKind::Folder | ArtifactKind::Graph => path,
29        ArtifactKind::File => path.parent().unwrap_or(Path::new("")),
30    }
31}
32
33pub(super) fn declaration_of(kind: ArtifactKind, path: &Path) -> PathBuf {
34    match kind {
35        ArtifactKind::Graph => PathBuf::from(".artf"),
36        ArtifactKind::Folder => logical_join(path, std::ffi::OsStr::new(CONFIG_FILE)),
37        ArtifactKind::File => {
38            let mut path = path.as_os_str().to_owned();
39            path.push(".artf");
40            PathBuf::from(path)
41        }
42    }
43}
44
45/// Every Artifact's site in the workspace at `repo`, from its declarations' names alone:
46/// nothing else in them is parsed or validated, so this is no check of the workspace.
47pub fn read_sites(repo: &Path) -> Result<BTreeMap<ArtifactName, Site>, ConfigError> {
48    let root =
49        crate::platform::canonicalize(repo).map_err(|error| ConfigError::new(repo, error))?;
50    let ignored = discovery_ignore(&root)?;
51    let mut sites = BTreeMap::new();
52    walk(
53        &root,
54        &ignored,
55        Reach::Workspace,
56        FileTargets::DeclarationsOnly,
57        &mut |found: Found<'_>| {
58            let name = declared_name(&found.source)
59                .map_err(|error| ConfigError::new(found.file, error))?;
60            let site = Site {
61                kind: found.kind,
62                path: found.path,
63            };
64            if sites.insert(name.clone(), site).is_some() {
65                return Err(ConfigError::declaration(
66                    found.file,
67                    &["name"],
68                    format!("Duplicate Artifact name: {name}."),
69                ));
70            }
71            Ok(name)
72        },
73    )?;
74    Ok(sites)
75}
76
77/// The `name` of a declaration, read without deserializing or validating the rest.
78fn declared_name(source: &str) -> Result<ArtifactName, String> {
79    let table = toml::de::DeTable::parse(source).map_err(|error| error.to_string())?;
80    match table.get_ref().get("name").map(|name| name.get_ref()) {
81        Some(toml::de::DeValue::String(name)) => name.parse(),
82        Some(_) => Err("name must be a string.".into()),
83        None => Err("missing field `name`".into()),
84    }
85}
86
87/// Why [`read_artifact_scope`] read no declarations.
88#[derive(Debug, Error)]
89pub enum ScopeReadError {
90    /// The site no longer declares the Artifact: where it is now needs [`read_sites`] again.
91    #[error("{name} is no longer declared at {}.", crate::platform::path_text(.path))]
92    Moved { name: ArtifactName, path: PathBuf },
93    #[error(transparent)]
94    Config(#[from] ConfigError),
95}
96
97/// The declarations of Artifact `name`, declared at `site`, and of the Artifacts it composes,
98/// each parsed and validated as a full read does; no other declaration is read. Checks across
99/// the workspace (unique names, references, the graph) stay with the full read, so the config
100/// holds only these Artifacts, their children and no evals.
101pub(crate) fn read_artifact_scope(
102    repo: &Path,
103    name: &ArtifactName,
104    site: &Site,
105) -> Result<RepoConfig, ScopeReadError> {
106    let root =
107        crate::platform::canonicalize(repo).map_err(|error| ConfigError::new(repo, error))?;
108    let moved = || ScopeReadError::Moved {
109        name: name.clone(),
110        path: site.declaration_path(),
111    };
112    let ignored = discovery_ignore(&root)?;
113    let folder = site.folder();
114    if !crate::platform::path_kind(&root.join(site.declaration_path()))
115        .is_ok_and(|kind| kind == crate::platform::FileKind::File)
116        || !folder.as_os_str().is_empty()
117            && ignored
118                .matched_path_or_any_parents(root.join(folder), true)
119                .is_ignore()
120    {
121        return Err(moved());
122    }
123    let reach = match site.kind {
124        ArtifactKind::Folder => Reach::Folder(&site.path),
125        ArtifactKind::File | ArtifactKind::Graph => Reach::File(
126            folder,
127            site.path
128                .file_name()
129                .and_then(|target| target.to_str())
130                .unwrap_or_default(),
131        ),
132    };
133    let mut config = RepoConfig {
134        root: root.clone(),
135        artifacts: BTreeMap::new(),
136        evals: Vec::new(),
137        relations: Vec::new(),
138    };
139    let mut schemas = super::schemas::Schemas::default();
140    walk(
141        &root,
142        &ignored,
143        reach,
144        FileTargets::Required,
145        &mut |found: Found<'_>| {
146            let own = config.artifacts.is_empty();
147            if own && (found.kind != site.kind || found.path != site.path) {
148                return Err(moved());
149            }
150            let declaration = declaration(&found, &mut schemas)?;
151            if own && &declaration.name != name {
152                return Err(moved());
153            }
154            Ok(declare(&mut config, found, declaration)?)
155        },
156    )?;
157    if !config.artifacts.contains_key(name) {
158        return Err(moved());
159    }
160    config.evals.clear();
161    Ok(config)
162}
163
164#[cfg(test)]
165mod tests;