sim_incremental_core/projection/
builtin.rs1use std::{collections::BTreeSet, sync::Arc};
2
3use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
4use sim_conformance_core::OwnerBindingId;
5use sim_kernel::{ContentId, Datum, Symbol};
6
7use super::{
8 PackageIdentity, ProjectionError, ProjectionInputs, ProjectionKindRef, ProjectionOutput,
9 ProjectionProvider, ProjectionRegistry,
10};
11
12pub const BASELINE_PROJECTION_KINDS: &[&str] = &[
14 "world/path-set-v1",
15 "world/manifest-dependencies-v1",
16 "world/public-api-v1",
17 "world/exact-command-environment-v1",
18 "world/generated-ownership-v1",
19 "world/package-assembly-v1",
20 "world/git-refs-v1",
21 "world/index-routes-v1",
22 "world/external-release-facts-v1",
23 "no-v3/disclosure-policy-v1",
24];
25
26#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct PathSelectionRules {
34 includes: Vec<String>,
35 ignores: Vec<String>,
36}
37
38impl PathSelectionRules {
39 pub fn new(
41 includes: impl IntoIterator<Item = String>,
42 ignores: impl IntoIterator<Item = String>,
43 ) -> Result<Self, ProjectionError> {
44 let includes = includes.into_iter().collect::<Vec<_>>();
45 let ignores = ignores.into_iter().collect::<Vec<_>>();
46 if includes.is_empty() {
47 return Err(ProjectionError::InvalidPathSelection(
48 "at least one include glob is required".to_owned(),
49 ));
50 }
51 compile_globs(&includes)?;
52 compile_globs(&ignores)?;
53 Ok(Self { includes, ignores })
54 }
55
56 pub fn select(
58 &self,
59 paths: impl IntoIterator<Item = String>,
60 ) -> Result<BTreeSet<super::FactId>, ProjectionError> {
61 let includes = compile_globs(&self.includes)?;
62 let ignores = compile_globs(&self.ignores)?;
63 let mut selected = BTreeSet::new();
64 for path in paths {
65 validate_logical_path(&path)?;
66 if includes.is_match(&path) && !ignores.is_match(&path) {
67 selected.insert(super::FactId::new(format!("path/{path}"))?);
68 }
69 }
70 Ok(selected)
71 }
72
73 #[must_use]
75 pub fn config(&self) -> Datum {
76 Datum::Node {
77 tag: Symbol::qualified("projection", "path-selection-v1"),
78 fields: vec![
79 (
80 Symbol::new("include"),
81 Datum::Vector(self.includes.iter().cloned().map(Datum::String).collect()),
82 ),
83 (
84 Symbol::new("ignore"),
85 Datum::Vector(self.ignores.iter().cloned().map(Datum::String).collect()),
86 ),
87 ],
88 }
89 }
90}
91
92fn compile_globs(patterns: &[String]) -> Result<GlobSet, ProjectionError> {
93 let mut builder = GlobSetBuilder::new();
94 for pattern in patterns {
95 if pattern.starts_with('/') || pattern.contains('\\') {
96 return Err(ProjectionError::InvalidPathSelection(format!(
97 "glob is not a canonical logical path pattern: {pattern}"
98 )));
99 }
100 let glob = GlobBuilder::new(pattern)
101 .literal_separator(true)
102 .backslash_escape(false)
103 .build()
104 .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))?;
105 builder.add(glob);
106 }
107 builder
108 .build()
109 .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))
110}
111
112fn validate_logical_path(path: &str) -> Result<(), ProjectionError> {
113 if path.is_empty()
114 || path.starts_with('/')
115 || path.contains('\\')
116 || path
117 .split('/')
118 .any(|part| part.is_empty() || part == "." || part == "..")
119 {
120 return Err(ProjectionError::InvalidPathSelection(format!(
121 "path is not a canonical relative logical path: {path}"
122 )));
123 }
124 Ok(())
125}
126
127pub struct SelectFactsProvider {
133 kind: ProjectionKindRef,
134 config_shape: ContentId,
135}
136
137impl SelectFactsProvider {
138 pub fn new(kind: impl Into<String>, config_shape: ContentId) -> Result<Self, ProjectionError> {
140 Ok(Self {
141 kind: ProjectionKindRef::new(kind)?,
142 config_shape,
143 })
144 }
145}
146
147impl ProjectionProvider for SelectFactsProvider {
148 fn kind(&self) -> &ProjectionKindRef {
149 &self.kind
150 }
151
152 fn config_shape(&self) -> &ContentId {
153 &self.config_shape
154 }
155
156 fn project(
157 &self,
158 inputs: &ProjectionInputs,
159 config: &Datum,
160 ) -> Result<ProjectionOutput, ProjectionError> {
161 let mut dependencies = BTreeSet::new();
162 let facts = inputs
163 .iter()
164 .map(|(id, value)| {
165 dependencies.insert(id.clone());
166 Datum::Node {
167 tag: Symbol::qualified("projection", "fact-v1"),
168 fields: vec![
169 (Symbol::new("id"), Datum::String(id.as_str().to_owned())),
170 (Symbol::new("value"), value.clone()),
171 ],
172 }
173 })
174 .collect();
175 Ok(ProjectionOutput {
176 value: Datum::Node {
177 tag: Symbol::qualified("projection", "selected-facts-v1"),
178 fields: vec![
179 (
180 Symbol::new("kind"),
181 Datum::String(self.kind.as_str().to_owned()),
182 ),
183 (Symbol::new("config"), config.clone()),
184 (Symbol::new("facts"), Datum::Vector(facts)),
185 ],
186 },
187 dependencies,
188 })
189 }
190}
191
192pub fn install_baseline_providers(
214 registry: &mut ProjectionRegistry,
215 config_shape: ContentId,
216 package: PackageIdentity,
217) -> Result<PackageIdentity, ProjectionError> {
218 let code = builtin_source_identity()?;
219 let dependencies = builtin_dependency_identity()?;
220 let owner_binding = OwnerBindingId::from_text("sim-incremental-core/projection/builtin")
221 .map_err(|error| ProjectionError::UnqualifiedProjector(error.to_string()))?;
222 let registered = PackageIdentity { code, ..package };
223 for kind in BASELINE_PROJECTION_KINDS {
224 let kind_ref = ProjectionKindRef::new(*kind)?;
225 registry.register(
226 registered.clone(),
227 Arc::new(SelectFactsProvider::new(*kind, config_shape.clone())?),
228 )?;
229 registry
230 .admit_bootstrap_native(
231 &kind_ref,
232 owner_binding.clone(),
233 registered.code.clone(),
234 dependencies.clone(),
235 )
236 .map_err(|error| ProjectionError::UnqualifiedProjector(error.to_string()))?;
237 }
238 Ok(registered)
239}
240
241fn builtin_source_identity() -> Result<ContentId, ProjectionError> {
252 Datum::String(
253 [
254 include_str!("builtin.rs"),
255 include_str!("model.rs"),
256 include_str!("admission.rs"),
257 include_str!("engine.rs"),
258 ]
259 .concat(),
260 )
261 .content_id()
262 .map_err(|error| ProjectionError::UnqualifiedProjector(error.to_string()))
263}
264
265fn builtin_dependency_identity() -> Result<ContentId, ProjectionError> {
266 Datum::Node {
267 tag: Symbol::qualified("world", "native-dependency-closure-v1"),
268 fields: vec![(
269 Symbol::new("sim-incremental-core"),
270 Datum::String(env!("CARGO_PKG_VERSION").to_owned()),
271 )],
272 }
273 .content_id()
274 .map_err(|error| ProjectionError::UnqualifiedProjector(error.to_string()))
275}