Skip to main content

unlit_ecs/
reflect.rs

1//! The component registry: which components a bridge can address.
2//!
3//! A component is any `'static` type, so a world alone cannot name one. A
4//! component becomes addressable by submitting a [`ComponentEntry`] to a
5//! link-time table; the entry is the only place its name and its codec are
6//! written down.
7//!
8//! The codec speaks [Partial] values rather than text: a caller that has a
9//! component in reflection form writes it without ever spelling it out, and one
10//! that has it as text fills a partial first. JSON is one such spelling, and
11//! [`encode_json`], [`set_json`] and [`push_json`] are that front door; a
12//! bridge of another format uses the partials underneath it.
13//!
14//! For a component that derives [`facet::Facet`] the entry carries no field list
15//! at all: its own struct fields are the JSON shape, and [`ComponentEntry::new`]
16//! turns that reflection into the codec. A component whose fields cannot be
17//! reflected -- a GPU handle, a private field -- reflects through a proxy instead,
18//! and one that cannot be written makes its proxy's conversion fail, so a write
19//! reports why rather than inventing a value.
20//!
21//! Entries are collected from wherever they are written, so a crate that defines
22//! its own components registers them itself -- nothing has to be handed a
23//! registry. The table is link-time, so a crate whose entries are never
24//! reachable from the binary is not linked in either; a binary that wants a
25//! crate's entries has to name that crate.
26
27use facet::Characteristic;
28use facet::Facet;
29use facet::Partial;
30use facet::{AllocError, ReflectError, ShapeMismatchError};
31use facet_format::{DeserializeError, SerializeError};
32use facet_json::JsonSerializeError;
33
34#[cfg(test)]
35use crate::World;
36use crate::{DynamicBundle, Entity, EntityAccessError, Scope};
37
38/// Why the registry could not address a component.
39///
40/// A lookup fails for its own reason -- an unknown name, a handle the scope
41/// cannot reach, a value that will not fit its shape -- so each case keeps the
42/// handle, the component name and the underlying error rather than flattening
43/// them into one line of text.
44#[derive(Debug, thiserror::Error)]
45#[non_exhaustive]
46pub enum ComponentError {
47    /// No submitted entry answers to this name.
48    #[error("unknown component {name:?}; known components: {}", .known.join(", "))]
49    Unknown {
50        /// The name that was looked up.
51        name: String,
52        /// The bare names that are submitted.
53        known: Vec<&'static str>,
54    },
55
56    /// The entity does not carry the component, or this scope cannot reach it.
57    #[error("entity {entity:?} has no {component} component: {source}")]
58    Missing {
59        /// The handle that was looked up.
60        entity: Entity,
61        /// The component that was asked for.
62        component: &'static str,
63        /// Why the lookup was refused: a dead handle, one out of the scope's
64        /// reach, or one whose archetype lacks the component.
65        source: EntityAccessError,
66    },
67
68    /// The reflection could not lay out a value for the component.
69    #[error(transparent)]
70    Allocate(#[from] AllocError),
71
72    /// A partial value was navigated or built wrongly.
73    #[error(transparent)]
74    Reflect(#[from] ReflectError),
75
76    /// The built value was not the component it was built as.
77    #[error(transparent)]
78    Materialize(#[from] ShapeMismatchError),
79
80    /// The component could not be written as JSON.
81    #[error("{component} cannot be written as JSON: {}", describe(.source))]
82    Encode {
83        /// The component that was asked for.
84        component: &'static str,
85        /// Why the serializer gave up.
86        source: SerializeError<JsonSerializeError>,
87    },
88
89    /// The text could not be read into the component.
90    #[error("{component} cannot be read from JSON: {source}")]
91    Decode {
92        /// The component that was asked for.
93        component: &'static str,
94        /// Where in the text the read gave up, and why. Boxed: the deserializer
95        /// error is large enough that every result carrying one would grow with
96        /// it.
97        source: Box<DeserializeError>,
98    },
99}
100
101/// One component a bridge can address.
102///
103/// Build one with [ComponentEntry::new] and submit it with
104/// `inventory::submit!`. The constructor is `const`, so a submission is a
105/// static initializer and costs nothing at run time.
106#[derive(Debug)]
107pub struct ComponentEntry {
108    /// The bare type name a client addresses the component by.
109    name: &'static str,
110    /// The module the type is defined in, which with the name addresses the
111    /// component too and tells two same-named types apart.
112    module_path: Option<&'static str>,
113    /// The component's value as JSON text, or why it could not be read.
114    encode_json: fn(&Scope<'_>, Entity) -> Result<String, ComponentError>,
115    /// A partial value over the component's [Default], or over nothing when it
116    /// has none.
117    default_partial: fn() -> Result<Partial<'static, false>, ComponentError>,
118    /// A partial value over the entity's current component, or why it cannot be
119    /// read.
120    current_partial: fn(&Scope<'_>, Entity) -> Result<Partial<'static, false>, ComponentError>,
121    /// Overwrite the component with the value a partial holds.
122    set: fn(&Scope<'_>, Entity, Partial<'static, false>) -> Result<(), ComponentError>,
123    /// Push the value a partial holds into a builder.
124    push: fn(&mut DynamicBundle, Partial<'static, false>) -> Result<(), ComponentError>,
125}
126
127impl ComponentEntry {
128    /// The entry for C, using the reflection C derives.
129    ///
130    /// Spawning a C from a partial value needs a base to merge it over, which
131    /// is C's own [Default] when it has one: a [Default] C is spawned from only
132    /// the fields that matter, while a C without one has to name every field.
133    /// Which of the two applies is read from the reflection C derives, so a
134    /// component that gains a [Default] starts merging over it without the
135    /// submission changing.
136    pub const fn new<C: Facet<'static> + Clone>() -> Self {
137        Self {
138            name: C::SHAPE.type_identifier,
139            module_path: C::SHAPE.module_path,
140            encode_json: encode_json_component::<C>,
141            default_partial: default_partial_component::<C>,
142            current_partial: current_partial_component::<C>,
143            set: set_component::<C>,
144            push: push_component::<C>,
145        }
146    }
147
148    /// The bare type name a client addresses the component by.
149    pub const fn name(&self) -> &'static str {
150        self.name
151    }
152}
153
154inventory::collect!(ComponentEntry);
155
156/// Every submitted entry.
157pub fn entries() -> impl Iterator<Item = &'static ComponentEntry> {
158    inventory::iter::<ComponentEntry>.into_iter()
159}
160
161/// The bare names of every submitted component.
162pub fn names() -> impl Iterator<Item = &'static str> {
163    entries().map(ComponentEntry::name)
164}
165
166/// Whether a component with this name is submitted.
167///
168/// The name may be the bare type name or the module-qualified one.
169pub fn contains(name: &str) -> bool {
170    find(name).is_some()
171}
172
173/// The entry a name addresses, or [ComponentError::Unknown] naming the ones
174/// that exist.
175pub fn entry(name: &str) -> Result<&'static ComponentEntry, ComponentError> {
176    find(name).ok_or_else(|| ComponentError::Unknown {
177        name: name.to_owned(),
178        known: names().collect(),
179    })
180}
181
182fn find(name: &str) -> Option<&'static ComponentEntry> {
183    let (module, bare) = match name.rsplit_once("::") {
184        Some((module, bare)) => (Some(module), bare),
185        None => (None, name),
186    };
187    entries().find(|entry| {
188        entry.name == bare && module.is_none_or(|module| entry.module_path == Some(module))
189    })
190}
191
192/// Entity's name component as JSON text, or why either is missing.
193pub fn encode_json(
194    scope: &Scope<'_>,
195    entity: Entity,
196    name: &str,
197) -> Result<String, ComponentError> {
198    (entry(name)?.encode_json)(scope, entity)
199}
200
201/// Overwrite entity's name component from JSON text.
202pub fn set_json(
203    scope: &Scope<'_>,
204    entity: Entity,
205    name: &str,
206    patch: &str,
207) -> Result<(), ComponentError> {
208    let entry = entry(name)?;
209    let partial = fill((entry.current_partial)(scope, entity)?, patch, entry.name)?;
210    (entry.set)(scope, entity, partial)
211}
212
213/// Push a name component decoded from JSON text into builder.
214pub fn push_json(
215    builder: &mut DynamicBundle,
216    name: &str,
217    patch: &str,
218) -> Result<(), ComponentError> {
219    let entry = entry(name)?;
220    let partial = fill((entry.default_partial)()?, patch, entry.name)?;
221    (entry.push)(builder, partial)
222}
223
224/// A partial value over the named component's [Default], ready to be filled and
225/// handed to [push].
226///
227/// A component with a [Default] keeps it in the fields a fill leaves alone,
228/// while one without has to name every field. Which of the two applies is read
229/// from the reflection, so a component that gains a [Default] starts merging
230/// over it without the submission changing.
231pub fn default_partial(name: &str) -> Result<Partial<'static, false>, ComponentError> {
232    (entry(name)?.default_partial)()
233}
234
235/// A partial value over the entity's current component, ready to be filled and
236/// handed back to [set].
237///
238/// The fields a fill leaves alone keep the value the entity holds, which is what
239/// setting one property at a time means.
240pub fn current_partial(
241    scope: &Scope<'_>,
242    entity: Entity,
243    name: &str,
244) -> Result<Partial<'static, false>, ComponentError> {
245    (entry(name)?.current_partial)(scope, entity)
246}
247
248/// Overwrite entity's name component with the value `partial` holds.
249pub fn set(
250    scope: &Scope<'_>,
251    entity: Entity,
252    name: &str,
253    partial: Partial<'static, false>,
254) -> Result<(), ComponentError> {
255    (entry(name)?.set)(scope, entity, partial)
256}
257
258/// Push the name component, taken from `partial`, into builder.
259pub fn push(
260    builder: &mut DynamicBundle,
261    name: &str,
262    partial: Partial<'static, false>,
263) -> Result<(), ComponentError> {
264    (entry(name)?.push)(builder, partial)
265}
266
267/// The partial a JSON patch produces over `partial`.
268fn fill(
269    partial: Partial<'static, false>,
270    patch: &str,
271    component: &'static str,
272) -> Result<Partial<'static, false>, ComponentError> {
273    facet_json::from_str_into(patch, partial).map_err(|source| ComponentError::Decode {
274        component,
275        source: Box::new(source),
276    })
277}
278
279/// A value as JSON text, through the reflection it derives.
280fn to_json<C: Facet<'static>>(value: &C) -> Result<String, ComponentError> {
281    facet_json::to_string(value).map_err(|source| ComponentError::Encode {
282        component: C::SHAPE.type_identifier,
283        source,
284    })
285}
286
287fn encode_json_component<C: Facet<'static>>(
288    scope: &Scope<'_>,
289    entity: Entity,
290) -> Result<String, ComponentError> {
291    let mut component = scope
292        .query_one::<&C>(entity)
293        .map_err(|error| missing::<C>(entity, error))?;
294    to_json(component.get())
295}
296
297/// A partial over a fresh C, seeded with C's [Default] when it has one.
298fn default_partial_component<C: Facet<'static>>() -> Result<Partial<'static, false>, ComponentError>
299{
300    let partial = Partial::alloc_owned::<C>()?;
301    // A C that is Default merges a fill over its own default, so a spawn may
302    // name only the fields that matter. The reflection says whether C has one,
303    // which keeps this a runtime question rather than a bound on C.
304    if C::SHAPE.is(Characteristic::Default) {
305        Ok(partial.set_default()?)
306    } else {
307        Ok(partial)
308    }
309}
310
311/// A partial over the entity's current C, seeded with a copy of it.
312fn current_partial_component<C: Facet<'static> + Clone>(
313    scope: &Scope<'_>,
314    entity: Entity,
315) -> Result<Partial<'static, false>, ComponentError> {
316    let current = scope
317        .query_one::<&C>(entity)
318        .map(|mut component| component.get().clone())
319        .map_err(|error| missing::<C>(entity, error))?;
320    let partial = Partial::alloc_owned::<C>()?;
321    Ok(partial.set(current)?)
322}
323
324fn set_component<C: Facet<'static>>(
325    scope: &Scope<'_>,
326    entity: Entity,
327    partial: Partial<'static, false>,
328) -> Result<(), ComponentError> {
329    let mut component = scope
330        .query_one::<&mut C>(entity)
331        .map_err(|error| missing::<C>(entity, error))?;
332    *component.get() = materialize(partial)?;
333    Ok(())
334}
335
336fn push_component<C: Facet<'static>>(
337    builder: &mut DynamicBundle,
338    partial: Partial<'static, false>,
339) -> Result<(), ComponentError> {
340    builder.push_component(materialize::<C>(partial)?);
341    Ok(())
342}
343
344fn materialize<C: Facet<'static>>(partial: Partial<'static, false>) -> Result<C, ComponentError> {
345    let heap = partial.build()?;
346    Ok(heap.materialize::<C>()?)
347}
348
349/// Says which component was asked for and why the lookup failed: the handle
350/// may be dead or out of the scope's reach, and each has its own reply.
351fn missing<C: Facet<'static>>(entity: Entity, error: EntityAccessError) -> ComponentError {
352    ComponentError::Missing {
353        entity,
354        component: C::SHAPE.type_identifier,
355        source: error,
356    }
357}
358
359/// The backend's own message, which [SerializeError]'s [Display](core::fmt::Display)
360/// leaves out of the `Backend` variant.
361fn describe(error: &SerializeError<JsonSerializeError>) -> String {
362    match error {
363        SerializeError::Backend(backend) => backend.to_string(),
364        other => other.to_string(),
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::bundle;
372
373    /// A component defined here, so the registry is tested on its own rather
374    /// than through a crate above it.
375    #[derive(Clone, Debug, Default, facet::Facet)]
376    struct Position {
377        x: f32,
378        y: f32,
379    }
380
381    #[derive(Clone, Copy, Debug, Default, facet::Facet)]
382    struct Name(&'static str);
383
384    /// A component with no [Default], which a spawn therefore cannot merge a
385    /// partial value over.
386    #[derive(Clone, Debug, facet::Facet)]
387    struct Offset {
388        distance: f32,
389    }
390
391    inventory::submit! { ComponentEntry::new::<Position>() }
392    inventory::submit! { ComponentEntry::new::<Offset>() }
393
394    /// The reflected fields are the codec: a component's own struct is the only
395    /// place its field list is written down, so reading and writing cannot
396    /// drift.
397    #[test]
398    fn a_reflected_component_round_trips_through_its_own_fields() {
399        let mut world = World::new();
400        let entity = world.root_scope_mut().spawn(bundle![Position::default()]);
401
402        set_json(&world.root_scope(), entity, "Position", r#"{"x":1.0}"#)
403            .expect("a partial value is accepted");
404
405        // The fields the patch did not name keep their current value, which is
406        // the whole point of setting one property at a time.
407        let encoded =
408            encode_json(&world.root_scope(), entity, "Position").expect("the component encodes");
409        let value: Position = facet_json::from_str(&encoded).expect("the encoding is JSON");
410        assert_eq!(value.x, 1.0);
411        assert_eq!(value.y, 0.0);
412    }
413
414    /// The module-qualified name addresses the same component as the bare one,
415    /// so a client that spells the whole path is not turned away.
416    #[test]
417    fn a_qualified_name_addresses_the_same_component() {
418        let qualified = format!(
419            "{}::{}",
420            Position::SHAPE.module_path.expect("a module"),
421            Position::SHAPE.type_identifier
422        );
423        assert!(contains("Position"));
424        assert!(contains(&qualified));
425        assert!(!contains("unlit_ecs::other::Position"));
426    }
427
428    /// Spawning merges the given fields over the component's default, so only
429    /// the fields that matter have to be given.
430    #[test]
431    fn spawning_merges_over_the_default() {
432        let mut builder = DynamicBundle::new().named("reflect test");
433        push_json(&mut builder, "Position", r#"{"x":4.0}"#).expect("a partial value is accepted");
434        let mut world = World::new();
435        let entity = world.root_scope_mut().spawn(builder);
436        let encoded = encode_json(&world.root_scope(), entity, "Position")
437            .expect("the spawned component encodes");
438        let value: Position = facet_json::from_str(&encoded).expect("the encoding is JSON");
439        assert_eq!(value.x, 4.0);
440        assert_eq!(value.y, 0.0);
441    }
442
443    /// A component with no [Default] has nothing to merge a partial value over,
444    /// so a spawn has to name every field. Which of the two applies is read
445    /// from the reflection, not from the submission.
446    #[test]
447    fn a_component_without_a_default_must_name_every_field() {
448        let mut builder = DynamicBundle::new().named("reflect test");
449        push_json(&mut builder, "Offset", r#"{"distance":4.0}"#)
450            .expect("a whole value is accepted");
451        let mut world = World::new();
452        let entity = world.root_scope_mut().spawn(builder);
453        let encoded = encode_json(&world.root_scope(), entity, "Offset").expect("it encodes");
454        let value: Offset = facet_json::from_str(&encoded).expect("the encoding is JSON");
455        assert_eq!(value.distance, 4.0);
456
457        let mut builder = DynamicBundle::new().named("reflect test");
458        let error =
459            push_json(&mut builder, "Offset", "{}").expect_err("there is no default to merge over");
460        assert!(error.to_string().contains("distance"), "{error}");
461    }
462
463    /// A caller that already holds a component in reflection form writes it
464    /// without spelling it out: the entry takes the partial itself, and text is
465    /// only one of the ways to fill one.
466    #[test]
467    fn a_partial_is_written_without_going_through_text() {
468        let mut world = World::new();
469        let entity = world.root_scope_mut().spawn(bundle![Position::default()]);
470
471        let partial = current_partial(&world.root_scope(), entity, "Position")
472            .expect("the entity carries the component");
473        let partial = partial.set_field("y", 2.0_f32).expect("y is a field");
474        set(&world.root_scope(), entity, "Position", partial).expect("the partial is written");
475
476        let mut written = world
477            .root_scope()
478            .query_one::<&Position>(entity)
479            .expect("the component is there");
480        let value = written.get();
481        assert_eq!(value.x, 0.0);
482        assert_eq!(value.y, 2.0);
483    }
484
485    /// A component that is not submitted is reported, and the message names the
486    /// ones that are.
487    #[test]
488    fn an_unknown_component_names_the_known_ones() {
489        let error = entry("Nope").expect_err("an unknown component is refused");
490        assert!(matches!(error, ComponentError::Unknown { .. }), "{error}");
491        let message = error.to_string();
492        assert!(message.contains("Nope"), "{message}");
493        assert!(message.contains("Position"), "{message}");
494    }
495
496    /// A component the entity does not carry is reported rather than encoded as
497    /// an empty value.
498    #[test]
499    fn a_component_the_entity_lacks_is_reported() {
500        let world = World::new();
501        let entity = Entity::from_raw(0, 0);
502        let error = encode_json(&world.root_scope(), entity, "Position")
503            .expect_err("the entity has nothing");
504        assert!(matches!(error, ComponentError::Missing { .. }), "{error}");
505        let message = error.to_string();
506        assert!(message.contains("has no Position"), "{message}");
507    }
508
509    /// The reason the access was refused travels with the message, so a caller
510    /// can tell a dead handle from one the scope cannot see or one that lacks
511    /// the component: they have different replies.
512    #[test]
513    fn a_refused_access_names_its_reason() {
514        let mut world = World::new();
515        let scene = world.root_scope_mut().spawn(bundle![Name("scene")]);
516        let outside = world.root_scope_mut().spawn(bundle![Position::default()]);
517        let dead = world.root_scope_mut().spawn(bundle![Position::default()]);
518        world.root_scope_mut().despawn(dead).unwrap();
519
520        let hidden = encode_json(&world.scope(scene).unwrap(), outside, "Position")
521            .expect_err("the scene cannot see it");
522        assert!(
523            matches!(
524                hidden,
525                ComponentError::Missing {
526                    source: EntityAccessError::NotVisible { .. },
527                    ..
528                }
529            ),
530            "{hidden}"
531        );
532        let message = hidden.to_string();
533        assert!(message.contains("is not visible from"), "{message}");
534        assert!(message.contains("Position"), "{message}");
535
536        let gone = encode_json(&world.root_scope(), dead, "Position").expect_err("it is gone");
537        assert!(
538            matches!(
539                gone,
540                ComponentError::Missing {
541                    source: EntityAccessError::NotAlive { .. },
542                    ..
543                }
544            ),
545            "{gone}"
546        );
547        assert!(gone.to_string().contains("names no live entity"), "{gone}");
548    }
549
550    /// A patch that does not fit the component says which component and why,
551    /// rather than only that something went wrong.
552    #[test]
553    fn a_bad_patch_names_the_component_and_the_field() {
554        let mut world = World::new();
555        let entity = world.root_scope_mut().spawn(bundle![Position::default()]);
556
557        let error = set_json(&world.root_scope(), entity, "Position", r#"{"x":"one"}"#)
558            .expect_err("a number is not a string");
559        assert!(matches!(error, ComponentError::Decode { .. }), "{error}");
560        let message = error.to_string();
561        assert!(message.contains("Position"), "{message}");
562        assert!(message.contains("x"), "{message}");
563    }
564
565    /// Every submitted name is distinct, so addressing one component by name is
566    /// unambiguous.
567    #[test]
568    fn every_submitted_name_is_distinct() {
569        let mut names: Vec<&str> = names().collect();
570        names.sort_unstable();
571        let count = names.len();
572        names.dedup();
573        assert_eq!(names.len(), count, "component names are unique");
574        assert!(count >= 1, "a submitted component is listed");
575    }
576}