petri_artifacts_core/
lib.rs1#![forbid(unsafe_code)]
10
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15#[doc(hidden)]
17pub use paste;
18use std::cell::RefCell;
19use std::ffi::OsStr;
20use std::marker::PhantomData;
21use std::path::Path;
22
23pub trait ArtifactId: 'static {
29 #[doc(hidden)]
31 const GLOBAL_UNIQUE_ID: &'static str;
32
33 #[doc(hidden)]
36 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro();
37}
38
39#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
42pub struct ArtifactHandle<A: ArtifactId>(PhantomData<A>);
43
44impl<A: ArtifactId + std::fmt::Debug> std::fmt::Debug for ArtifactHandle<A> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 std::fmt::Debug::fmt(&self.erase(), f)
47 }
48}
49
50pub struct ResolvedArtifact<A = ()>(Option<PathBuf>, PhantomData<A>);
52
53impl<A> Clone for ResolvedArtifact<A> {
54 fn clone(&self) -> Self {
55 Self(self.0.clone(), self.1)
56 }
57}
58
59impl<A> std::fmt::Debug for ResolvedArtifact<A> {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_tuple("ResolvedArtifact").field(&self.0).finish()
62 }
63}
64
65impl<A> ResolvedArtifact<A> {
66 pub fn erase(self) -> ResolvedArtifact {
68 ResolvedArtifact(self.0, PhantomData)
69 }
70
71 #[track_caller]
73 pub fn get(&self) -> &Path {
74 self.0
75 .as_ref()
76 .expect("cannot get path in requirements phase")
77 }
78}
79
80impl<A> From<ResolvedArtifact<A>> for PathBuf {
81 #[track_caller]
82 fn from(ra: ResolvedArtifact<A>) -> PathBuf {
83 ra.0.expect("cannot get path in requirements phase")
84 }
85}
86
87impl<A> AsRef<Path> for ResolvedArtifact<A> {
88 #[track_caller]
89 fn as_ref(&self) -> &Path {
90 self.get()
91 }
92}
93
94impl<A> AsRef<OsStr> for ResolvedArtifact<A> {
95 #[track_caller]
96 fn as_ref(&self) -> &OsStr {
97 self.get().as_ref()
98 }
99}
100
101#[derive(Clone, Debug)]
103pub struct ResolvedOptionalArtifact<A = ()>(OptionalArtifactState, PhantomData<A>);
104
105#[derive(Clone, Debug)]
106enum OptionalArtifactState {
107 Collecting,
108 Missing,
109 Present(PathBuf),
110}
111
112impl<A> ResolvedOptionalArtifact<A> {
113 pub fn erase(self) -> ResolvedOptionalArtifact {
115 ResolvedOptionalArtifact(self.0, PhantomData)
116 }
117
118 #[track_caller]
120 pub fn get(&self) -> Option<&Path> {
121 match self.0 {
122 OptionalArtifactState::Collecting => panic!("cannot get path in requirements phase"),
123 OptionalArtifactState::Missing => None,
124 OptionalArtifactState::Present(ref path) => Some(path),
125 }
126 }
127}
128
129pub struct ArtifactResolver<'a>(ArtifactResolverInner<'a>);
132
133impl<'a> ArtifactResolver<'a> {
134 pub fn collector(requirements: &'a mut TestArtifactRequirements) -> Self {
137 ArtifactResolver(ArtifactResolverInner::Collecting(RefCell::new(
138 requirements,
139 )))
140 }
141
142 pub fn resolver(artifacts: &'a TestArtifacts) -> Self {
144 ArtifactResolver(ArtifactResolverInner::Resolving(artifacts))
145 }
146
147 pub fn require<A: ArtifactId>(&self, handle: ArtifactHandle<A>) -> ResolvedArtifact<A> {
149 match &self.0 {
150 ArtifactResolverInner::Collecting(requirements) => {
151 requirements.borrow_mut().require(handle.erase());
152 ResolvedArtifact(None, PhantomData)
153 }
154 ArtifactResolverInner::Resolving(artifacts) => {
155 ResolvedArtifact(Some(artifacts.get(handle).to_owned()), PhantomData)
156 }
157 }
158 }
159
160 pub fn try_require<A: ArtifactId>(
162 &self,
163 handle: ArtifactHandle<A>,
164 ) -> ResolvedOptionalArtifact<A> {
165 match &self.0 {
166 ArtifactResolverInner::Collecting(requirements) => {
167 requirements.borrow_mut().try_require(handle.erase());
168 ResolvedOptionalArtifact(OptionalArtifactState::Collecting, PhantomData)
169 }
170 ArtifactResolverInner::Resolving(artifacts) => ResolvedOptionalArtifact(
171 artifacts
172 .try_get(handle)
173 .map_or(OptionalArtifactState::Missing, |p| {
174 OptionalArtifactState::Present(p.to_owned())
175 }),
176 PhantomData,
177 ),
178 }
179 }
180}
181
182enum ArtifactResolverInner<'a> {
183 Collecting(RefCell<&'a mut TestArtifactRequirements>),
184 Resolving(&'a TestArtifacts),
185}
186
187#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
190pub struct ErasedArtifactHandle {
191 artifact_id_str: &'static str,
192}
193
194impl std::fmt::Debug for ErasedArtifactHandle {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(
201 f,
202 "{}",
203 self.artifact_id_str
204 .strip_suffix("__ty")
205 .unwrap_or(self.artifact_id_str)
206 )
207 }
208}
209
210impl<A: ArtifactId> PartialEq<ErasedArtifactHandle> for ArtifactHandle<A> {
211 fn eq(&self, other: &ErasedArtifactHandle) -> bool {
212 &self.erase() == other
213 }
214}
215
216impl<A: ArtifactId> PartialEq<ArtifactHandle<A>> for ErasedArtifactHandle {
217 fn eq(&self, other: &ArtifactHandle<A>) -> bool {
218 self == &other.erase()
219 }
220}
221
222impl<A: ArtifactId> ArtifactHandle<A> {
223 pub const fn new() -> Self {
226 Self(PhantomData)
227 }
228}
229
230pub trait AsArtifactHandle {
233 fn erase(&self) -> ErasedArtifactHandle;
235}
236
237impl AsArtifactHandle for ErasedArtifactHandle {
238 fn erase(&self) -> ErasedArtifactHandle {
239 *self
240 }
241}
242
243impl<A: ArtifactId> AsArtifactHandle for ArtifactHandle<A> {
244 fn erase(&self) -> ErasedArtifactHandle {
245 ErasedArtifactHandle {
246 artifact_id_str: A::GLOBAL_UNIQUE_ID,
247 }
248 }
249}
250
251#[macro_export]
253macro_rules! declare_artifacts {
254 (
255 $(
256 $(#[$doc:meta])*
257 $name:ident
258 ),*
259 $(,)?
260 ) => {
261 $(
262 $crate::paste::paste! {
263 $(#[$doc])*
264 #[expect(non_camel_case_types)]
265 pub const $name: $crate::ArtifactHandle<$name> = $crate::ArtifactHandle::new();
266
267 #[doc = concat!("Type-tag for [`", stringify!($name), "`]")]
268 #[expect(non_camel_case_types)]
269 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
270 pub enum $name {}
271
272 #[expect(non_snake_case)]
273 mod [< $name __ty >] {
274 impl $crate::ArtifactId for super::$name {
275 const GLOBAL_UNIQUE_ID: &'static str = module_path!();
276 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro() {}
277 }
278 }
279 }
280 )*
281 };
282}
283
284pub trait ResolveTestArtifact {
290 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf>;
295}
296
297impl<T: ResolveTestArtifact + ?Sized> ResolveTestArtifact for &T {
298 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
299 (**self).resolve(id)
300 }
301}
302
303#[derive(Clone)]
305pub struct TestArtifactRequirements {
306 artifacts: Vec<(ErasedArtifactHandle, bool)>,
307}
308
309impl TestArtifactRequirements {
310 pub fn new() -> Self {
312 TestArtifactRequirements {
313 artifacts: Vec::new(),
314 }
315 }
316
317 pub fn require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self {
319 self.artifacts.push((dependency.erase(), false));
320 self
321 }
322
323 pub fn try_require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self {
325 self.artifacts.push((dependency.erase(), true));
326 self
327 }
328
329 pub fn required_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
331 self.artifacts
332 .iter()
333 .filter_map(|&(a, optional)| (!optional).then_some(a))
334 }
335
336 pub fn optional_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
338 self.artifacts
339 .iter()
340 .filter_map(|&(a, optional)| optional.then_some(a))
341 }
342
343 pub fn resolve(&self, resolver: impl ResolveTestArtifact) -> anyhow::Result<TestArtifacts> {
345 let mut failed = String::new();
346 let mut resolved = HashMap::new();
347
348 for &(a, optional) in &self.artifacts {
349 match resolver.resolve(a) {
350 Ok(p) => {
351 resolved.insert(a, p);
352 }
353 Err(_) if optional => {}
354 Err(e) => failed.push_str(&format!("{:?} - {:#}\n", a, e)),
355 }
356 }
357
358 if !failed.is_empty() {
359 anyhow::bail!("Artifact resolution failed:\n{}", failed);
360 }
361
362 Ok(TestArtifacts {
363 artifacts: Arc::new(resolved),
364 })
365 }
366}
367
368#[derive(Clone)]
371pub struct TestArtifacts {
372 artifacts: Arc<HashMap<ErasedArtifactHandle, PathBuf>>,
373}
374
375impl TestArtifacts {
376 #[track_caller]
378 pub fn try_get(&self, artifact: impl AsArtifactHandle) -> Option<&Path> {
379 self.artifacts.get(&artifact.erase()).map(|p| p.as_ref())
380 }
381
382 #[track_caller]
384 pub fn get(&self, artifact: impl AsArtifactHandle) -> &Path {
385 self.try_get(artifact.erase())
386 .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase()))
387 }
388}