sim_incremental_core/projection/
builtin.rs1use std::{collections::BTreeSet, sync::Arc};
2
3use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
4use sim_kernel::{ContentId, Datum, Symbol};
5
6use super::{
7 PackageIdentity, ProjectionError, ProjectionInputs, ProjectionKindRef, ProjectionOutput,
8 ProjectionProvider, ProjectionRegistry,
9};
10
11pub const BASELINE_PROJECTION_KINDS: &[&str] = &[
13 "world/path-set-v1",
14 "world/manifest-dependencies-v1",
15 "world/public-api-v1",
16 "world/exact-command-environment-v1",
17 "world/generated-ownership-v1",
18 "world/package-assembly-v1",
19 "world/git-refs-v1",
20 "world/index-routes-v1",
21 "world/external-release-facts-v1",
22 "no-v3/disclosure-policy-v1",
23];
24
25#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct PathSelectionRules {
33 includes: Vec<String>,
34 ignores: Vec<String>,
35}
36
37impl PathSelectionRules {
38 pub fn new(
40 includes: impl IntoIterator<Item = String>,
41 ignores: impl IntoIterator<Item = String>,
42 ) -> Result<Self, ProjectionError> {
43 let includes = includes.into_iter().collect::<Vec<_>>();
44 let ignores = ignores.into_iter().collect::<Vec<_>>();
45 if includes.is_empty() {
46 return Err(ProjectionError::InvalidPathSelection(
47 "at least one include glob is required".to_owned(),
48 ));
49 }
50 compile_globs(&includes)?;
51 compile_globs(&ignores)?;
52 Ok(Self { includes, ignores })
53 }
54
55 pub fn select(
57 &self,
58 paths: impl IntoIterator<Item = String>,
59 ) -> Result<BTreeSet<super::FactId>, ProjectionError> {
60 let includes = compile_globs(&self.includes)?;
61 let ignores = compile_globs(&self.ignores)?;
62 let mut selected = BTreeSet::new();
63 for path in paths {
64 validate_logical_path(&path)?;
65 if includes.is_match(&path) && !ignores.is_match(&path) {
66 selected.insert(super::FactId::new(format!("path/{path}"))?);
67 }
68 }
69 Ok(selected)
70 }
71
72 #[must_use]
74 pub fn config(&self) -> Datum {
75 Datum::Node {
76 tag: Symbol::qualified("projection", "path-selection-v1"),
77 fields: vec![
78 (
79 Symbol::new("include"),
80 Datum::Vector(self.includes.iter().cloned().map(Datum::String).collect()),
81 ),
82 (
83 Symbol::new("ignore"),
84 Datum::Vector(self.ignores.iter().cloned().map(Datum::String).collect()),
85 ),
86 ],
87 }
88 }
89}
90
91fn compile_globs(patterns: &[String]) -> Result<GlobSet, ProjectionError> {
92 let mut builder = GlobSetBuilder::new();
93 for pattern in patterns {
94 if pattern.starts_with('/') || pattern.contains('\\') {
95 return Err(ProjectionError::InvalidPathSelection(format!(
96 "glob is not a canonical logical path pattern: {pattern}"
97 )));
98 }
99 let glob = GlobBuilder::new(pattern)
100 .literal_separator(true)
101 .backslash_escape(false)
102 .build()
103 .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))?;
104 builder.add(glob);
105 }
106 builder
107 .build()
108 .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))
109}
110
111fn validate_logical_path(path: &str) -> Result<(), ProjectionError> {
112 if path.is_empty()
113 || path.starts_with('/')
114 || path.contains('\\')
115 || path
116 .split('/')
117 .any(|part| part.is_empty() || part == "." || part == "..")
118 {
119 return Err(ProjectionError::InvalidPathSelection(format!(
120 "path is not a canonical relative logical path: {path}"
121 )));
122 }
123 Ok(())
124}
125
126pub struct SelectFactsProvider {
132 kind: ProjectionKindRef,
133 config_shape: ContentId,
134}
135
136impl SelectFactsProvider {
137 pub fn new(kind: impl Into<String>, config_shape: ContentId) -> Result<Self, ProjectionError> {
139 Ok(Self {
140 kind: ProjectionKindRef::new(kind)?,
141 config_shape,
142 })
143 }
144}
145
146impl ProjectionProvider for SelectFactsProvider {
147 fn kind(&self) -> &ProjectionKindRef {
148 &self.kind
149 }
150
151 fn config_shape(&self) -> &ContentId {
152 &self.config_shape
153 }
154
155 fn project(
156 &self,
157 inputs: &ProjectionInputs,
158 config: &Datum,
159 ) -> Result<ProjectionOutput, ProjectionError> {
160 let mut dependencies = BTreeSet::new();
161 let facts = inputs
162 .iter()
163 .map(|(id, value)| {
164 dependencies.insert(id.clone());
165 Datum::Node {
166 tag: Symbol::qualified("projection", "fact-v1"),
167 fields: vec![
168 (Symbol::new("id"), Datum::String(id.as_str().to_owned())),
169 (Symbol::new("value"), value.clone()),
170 ],
171 }
172 })
173 .collect();
174 Ok(ProjectionOutput {
175 value: Datum::Node {
176 tag: Symbol::qualified("projection", "selected-facts-v1"),
177 fields: vec![
178 (
179 Symbol::new("kind"),
180 Datum::String(self.kind.as_str().to_owned()),
181 ),
182 (Symbol::new("config"), config.clone()),
183 (Symbol::new("facts"), Datum::Vector(facts)),
184 ],
185 },
186 dependencies,
187 })
188 }
189}
190
191pub fn install_baseline_providers(
193 registry: &mut ProjectionRegistry,
194 config_shape: ContentId,
195 package: PackageIdentity,
196) -> Result<(), ProjectionError> {
197 for kind in BASELINE_PROJECTION_KINDS {
198 registry.register(
199 package.clone(),
200 Arc::new(SelectFactsProvider::new(*kind, config_shape.clone())?),
201 )?;
202 }
203 Ok(())
204}