Experiments in applying Entity-Component-System patterns to durable data storage APIs.
1use serde::{Deserialize, Serialize};
2use tracing::{debug, error, info, instrument};
3
4use crate::{self as ecsdb, Component, Ecs, Entity, query};
5
6use core::marker::PhantomData;
7use std::{
8 borrow::{Borrow, Cow},
9 ops::Deref,
10};
11
12#[derive(Serialize, Deserialize, Component, Debug, PartialEq, Eq, Hash)]
13pub struct Name(pub String);
14
15#[derive(Serialize, Deserialize, Component, Debug)]
16pub struct LastRun(pub chrono::DateTime<chrono::Utc>);
17
18pub trait System: Send + Sync {
19 fn name(&self) -> Cow<'static, str>;
20 fn run_system(&self, app: &Ecs) -> Result<(), anyhow::Error>;
21}
22
23pub trait IntoSystem<Marker>: Sized {
24 type System: System;
25 fn into_system(self) -> Self::System;
26
27 fn into_boxed_system(self) -> BoxedSystem
28 where
29 Self::System: 'static,
30 {
31 Box::new(self.into_system())
32 }
33}
34
35impl<S: System> IntoSystem<()> for S {
36 type System = S;
37
38 fn into_system(self) -> Self::System {
39 self
40 }
41}
42
43impl<'a, S: System> System for &'a S {
44 fn name(&self) -> Cow<'static, str> {
45 (*self).name()
46 }
47
48 fn run_system(&self, app: &Ecs) -> Result<(), anyhow::Error> {
49 (*self).run_system(app)
50 }
51}
52
53pub type BoxedSystem = Box<dyn System>;
54
55impl System for BoxedSystem {
56 fn name(&self) -> Cow<'static, str> {
57 System::name(self.as_ref())
58 }
59
60 fn run_system(&self, app: &Ecs) -> Result<(), anyhow::Error> {
61 System::run_system(self.as_ref(), app)
62 }
63}
64
65#[doc(hidden)]
66pub struct FunctionSystemMarker;
67
68impl<Marker, F> IntoSystem<(Marker, FunctionSystemMarker)> for F
69where
70 Marker: 'static,
71 F: SystemParamFunction<Marker>,
72{
73 type System = FunctionSystem<Marker, F>;
74
75 fn into_system(self) -> Self::System {
76 FunctionSystem {
77 system: self,
78 params: PhantomData,
79 }
80 }
81}
82
83pub struct FunctionSystem<Marker, F>
84where
85 F: 'static,
86{
87 system: F,
88 params: PhantomData<fn() -> Marker>,
89}
90
91impl<Marker, F> System for FunctionSystem<Marker, F>
92where
93 Marker: 'static,
94 F: SystemParamFunction<Marker>,
95{
96 fn name(&self) -> Cow<'static, str> {
97 Cow::Borrowed(std::any::type_name::<F>())
98 }
99
100 fn run_system(&self, app: &Ecs) -> Result<(), anyhow::Error> {
101 SystemParamFunction::run_system(&self.system, F::Params::get_param(app, &self.name()))
102 .into_result()
103 }
104}
105
106pub trait SystemParamFunction<Marker>: Send + Sync + 'static {
107 type Params: SystemParam;
108 fn run_system(
109 &self,
110 param: <Self::Params as SystemParam>::Item<'_>,
111 ) -> Result<(), anyhow::Error>;
112}
113
114pub trait SystemOutput {
115 fn into_result(self) -> Result<(), anyhow::Error>;
116}
117
118impl SystemOutput for () {
119 fn into_result(self) -> Result<(), anyhow::Error> {
120 Ok(())
121 }
122}
123
124impl SystemOutput for Result<(), anyhow::Error> {
125 fn into_result(self) -> Result<(), anyhow::Error> {
126 self
127 }
128}
129
130impl<F, Out> SystemParamFunction<()> for F
131where
132 F: Fn() -> Out + Send + Sync + 'static,
133 Out: SystemOutput,
134{
135 type Params = ();
136 fn run_system(&self, _app: ()) -> Result<(), anyhow::Error> {
137 self().into_result()
138 }
139}
140
141type SystemParamItem<'world, P> = <P as SystemParam>::Item<'world>;
142
143macro_rules! impl_system_function {
144 ($($param: ident),*) => {
145 impl<F, Out, $($param: SystemParam),*> SystemParamFunction<($($param,)*)> for F
146 where
147 F: Send + Sync + 'static,
148 for<'a> &'a F:
149 Fn($($param),*) -> Out
150 +
151 Fn($(SystemParamItem<$param>),*) -> Out,
152 Out: SystemOutput,
153 {
154 type Params = ($($param,)*);
155
156 #[allow(non_snake_case)]
157 #[allow(clippy::too_many_arguments)]
158 fn run_system(&self, p: SystemParamItem<($($param,)*)>) -> Result<(), anyhow::Error> {
159 let ($($param,)*) = p;
160 (&self)( $($param),*).into_result()
161 }
162 }
163
164 impl<$($param: SystemParam,)*> SystemParam for ($($param,)*) {
165 type Item<'world> = ($($param::Item<'world>,)*);
166
167 fn get_param<'world>(world: &'world Ecs, system: &str) -> Self::Item<'world> {
168 ($($param::get_param(world, system),)*)
169 }
170 }
171 };
172}
173
174impl_system_function!(P1);
175impl_system_function!(P1, P2);
176impl_system_function!(P1, P2, P3);
177impl_system_function!(P1, P2, P3, P4);
178impl_system_function!(P1, P2, P3, P4, P5);
179impl_system_function!(P1, P2, P3, P4, P5, P6);
180impl_system_function!(P1, P2, P3, P4, P5, P6, P7);
181
182pub trait SystemParam: Sized {
183 type Item<'world>: SystemParam;
184 fn get_param<'world>(world: &'world Ecs, system: &str) -> Self::Item<'world>;
185}
186
187impl SystemParam for () {
188 type Item<'world> = ();
189
190 fn get_param<'world>(_world: &'world Ecs, _system: &str) -> Self::Item<'world> {}
191}
192
193impl Ecs {
194 #[deprecated(note = "use Ecs::run_system")]
195 pub fn run<Marker, F: IntoSystem<Marker>>(&self, system: F) -> Result<(), anyhow::Error> {
196 self.run_system(system)
197 }
198
199 pub fn run_system<'a, Marker, F: IntoSystem<Marker> + 'a>(
200 &'a self,
201 system: F,
202 ) -> Result<(), anyhow::Error> {
203 let system = system.into_system();
204 self.run_dyn_system(&system)
205 }
206
207 #[instrument(level="info", name="run_system", skip_all, fields(name = %system.name()))]
208 pub(crate) fn run_dyn_system(&self, system: &dyn System) -> Result<(), anyhow::Error> {
209 let started = std::time::Instant::now();
210
211 let system_entity = self.get_or_create_system_entity(&system.name());
212
213 info!("Running");
214
215 if let Err(e) = system.run_system(self) {
216 error!(?e);
217 return Err(e);
218 }
219
220 system_entity.attach(LastRun(chrono::Utc::now()));
221
222 debug!(elapsed_ms = started.elapsed().as_millis(), "Finished",);
223
224 Ok(())
225 }
226
227 pub fn system_entities<'a>(&'a self) -> impl Iterator<Item = (String, Entity<'a>)> {
228 self.query::<(Entity, Name), ()>()
229 .map(|(e, name)| (name.0, e))
230 }
231
232 pub fn system_entity<'a>(&'a self, name: &str) -> Option<Entity<'a>> {
233 self.query::<(Entity, Name), ()>()
234 .find_map(|(e, s)| (s.0 == name).then_some(e))
235 }
236 pub(crate) fn get_or_create_system_entity<'a>(&'a self, system: &str) -> Entity<'a> {
237 self.system_entity(system)
238 .unwrap_or_else(|| self.new_entity().attach(Name(system.to_string())))
239 }
240}
241
242#[derive(Debug, Clone, Copy)]
243pub struct SystemEntity<'a>(pub Entity<'a>);
244
245impl<'a> AsRef<Entity<'a>> for SystemEntity<'a> {
246 fn as_ref(&self) -> &Entity<'a> {
247 &self.0
248 }
249}
250
251impl<'a> Deref for SystemEntity<'a> {
252 type Target = Entity<'a>;
253
254 fn deref(&self) -> &Self::Target {
255 &self.0
256 }
257}
258
259impl SystemParam for SystemEntity<'_> {
260 type Item<'world> = SystemEntity<'world>;
261
262 fn get_param<'world>(world: &'world Ecs, system: &str) -> Self::Item<'world> {
263 let Some(entity) = world.system_entity(system) else {
264 panic!("Couldn't find SystemEntity for {system:?}. This should not happen.");
265 };
266
267 SystemEntity(entity)
268 }
269}
270
271impl SystemParam for &'_ Ecs {
272 type Item<'world> = &'world Ecs;
273
274 fn get_param<'world>(world: &'world Ecs, _system: &str) -> Self::Item<'world> {
275 world
276 }
277}
278
279impl<D, F> SystemParam for query::Query<'_, D, F>
280where
281 F: query::QueryFilter + Default,
282{
283 type Item<'world> = query::Query<'world, D, F>;
284
285 fn get_param<'world>(world: &'world Ecs, _system: &str) -> Self::Item<'world> {
286 query::Query::new(world)
287 }
288}
289
290impl SystemParam for LastRun {
291 type Item<'world> = LastRun;
292
293 fn get_param<'world>(world: &'world Ecs, system: &str) -> Self::Item<'world> {
294 let never = LastRun(chrono::DateTime::<chrono::Utc>::MIN_UTC);
295
296 world
297 .system_entity(system)
298 .and_then(|entity| entity.component())
299 .unwrap_or(never)
300 }
301}
302
303impl AsRef<chrono::DateTime<chrono::Utc>> for LastRun {
304 fn as_ref(&self) -> &chrono::DateTime<chrono::Utc> {
305 &self.0
306 }
307}
308
309impl Borrow<chrono::DateTime<chrono::Utc>> for LastRun {
310 fn borrow(&self) -> &chrono::DateTime<chrono::Utc> {
311 &self.0
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::marker::PhantomData;
318
319 use crate::query::With;
320 use crate::{Ecs, Entity, IntoSystem, System, SystemEntity, query};
321
322 #[test]
323 fn run_system() {
324 let ecs = Ecs::open_in_memory().unwrap();
325 ecs.run_system(|| ()).unwrap();
326 }
327
328 #[test]
329 fn run_system_boxed() {
330 let ecs = Ecs::open_in_memory().unwrap();
331 let system = IntoSystem::into_boxed_system(|| ());
332 ecs.run_system(&system).unwrap();
333 ecs.run_system(system).unwrap();
334 }
335
336 #[test]
337 fn run_dyn_system() {
338 let ecs = Ecs::open_in_memory().unwrap();
339 let system = IntoSystem::into_boxed_system(|| ());
340 ecs.run_dyn_system(&system).unwrap();
341 ecs.run_dyn_system(system.as_ref()).unwrap();
342 }
343
344 #[test]
345 fn non_static_system() {
346 let ecs = Ecs::open_in_memory().unwrap();
347
348 struct NonStaticSystem<'a>(PhantomData<&'a ()>);
349 #[rustfmt::skip]
350 impl<'a> System for NonStaticSystem<'a> {
351 fn name(&self) -> std::borrow::Cow<'static, str> { "".into() }
352 fn run_system(&self, _app: &Ecs) -> Result<(), anyhow::Error> { Ok(()) }
353 }
354
355 let non_static: NonStaticSystem<'_> = NonStaticSystem(PhantomData);
356 ecs.run_system(&non_static).unwrap();
357 }
358
359 #[test]
360 fn no_param() {
361 let ecs = Ecs::open_in_memory().unwrap();
362 ecs.run_system(|| ()).unwrap();
363 }
364
365 #[test]
366 fn ecs_param() {
367 let ecs = Ecs::open_in_memory().unwrap();
368 ecs.run_system(|_ecs: &Ecs| ()).unwrap();
369 // ecs.run_system(|_ecs: &Ecs| ());
370 }
371
372 #[test]
373 fn query_param() {
374 let ecs = Ecs::open_in_memory().unwrap();
375 ecs.run_system(|_q: query::Query<()>| ()).unwrap();
376 }
377
378 #[test]
379 fn multiple_params() {
380 let ecs = Ecs::open_in_memory().unwrap();
381 ecs.run_system(|_ecs: &Ecs, _q: query::Query<()>| ())
382 .unwrap();
383 ecs.run_system(|_: &Ecs, _: &Ecs| ()).unwrap();
384 ecs.run_system(|_: &Ecs, _: &Ecs, _: &Ecs| ()).unwrap();
385 ecs.run_system(|_: &Ecs, _: &Ecs, _: &Ecs, _: &Ecs| ())
386 .unwrap();
387 ecs.run_system(|_: &Ecs, _: &Ecs, _: &Ecs, _: &Ecs, _: &Ecs| ())
388 .unwrap();
389 }
390
391 use crate as ecsdb;
392 use ecsdb::Component;
393 use serde::{Deserialize, Serialize};
394
395 #[derive(Debug, Serialize, Deserialize, Component)]
396 struct A;
397
398 #[derive(Debug, Serialize, Deserialize, Component)]
399 struct B;
400
401 #[derive(Debug, Serialize, Deserialize, Component)]
402 struct Seen;
403
404 #[test]
405 fn run_query_param() {
406 let db = Ecs::open_in_memory().unwrap();
407 fn system(query: query::Query<Entity, With<(A, B)>>) {
408 for entity in query.try_iter().unwrap() {
409 entity.attach(Seen);
410 }
411 }
412
413 // db.register(system);
414
415 let a_and_b = db.new_entity().attach(A).attach(B);
416 let a = db.new_entity().attach(A);
417
418 db.run_system(system).unwrap();
419
420 assert!(a_and_b.component::<Seen>().is_some());
421 assert!(a.component::<Seen>().is_none());
422 }
423
424 #[test]
425 fn run_ecs_param() {
426 let db = Ecs::open_in_memory().unwrap();
427 fn system(ecs: &Ecs) {
428 ecs.new_entity().attach(Seen);
429 }
430
431 db.run_system(system).unwrap();
432
433 assert!(db.query::<Seen, ()>().next().is_some());
434 }
435
436 #[test]
437 fn run_system_entity_param() {
438 let db = Ecs::open_in_memory().unwrap();
439 fn system(ecs: &Ecs, system: SystemEntity<'_>) {
440 assert_eq!(
441 system.component::<crate::system::Name>().unwrap().0,
442 "ecsdb::system::tests::run_system_entity_param::system"
443 );
444
445 ecs.new_entity().attach(Seen);
446 }
447
448 db.run_system(system).unwrap();
449
450 assert!(db.query::<Seen, ()>().next().is_some());
451 }
452}