1use 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#[derive(Debug, thiserror::Error)]
45#[non_exhaustive]
46pub enum ComponentError {
47 #[error("unknown component {name:?}; known components: {}", .known.join(", "))]
49 Unknown {
50 name: String,
52 known: Vec<&'static str>,
54 },
55
56 #[error("entity {entity:?} has no {component} component: {source}")]
58 Missing {
59 entity: Entity,
61 component: &'static str,
63 source: EntityAccessError,
66 },
67
68 #[error(transparent)]
70 Allocate(#[from] AllocError),
71
72 #[error(transparent)]
74 Reflect(#[from] ReflectError),
75
76 #[error(transparent)]
78 Materialize(#[from] ShapeMismatchError),
79
80 #[error("{component} cannot be written as JSON: {}", describe(.source))]
82 Encode {
83 component: &'static str,
85 source: SerializeError<JsonSerializeError>,
87 },
88
89 #[error("{component} cannot be read from JSON: {source}")]
91 Decode {
92 component: &'static str,
94 source: Box<DeserializeError>,
98 },
99}
100
101#[derive(Debug)]
107pub struct ComponentEntry {
108 name: &'static str,
110 module_path: Option<&'static str>,
113 encode_json: fn(&Scope<'_>, Entity) -> Result<String, ComponentError>,
115 default_partial: fn() -> Result<Partial<'static, false>, ComponentError>,
118 current_partial: fn(&Scope<'_>, Entity) -> Result<Partial<'static, false>, ComponentError>,
121 set: fn(&Scope<'_>, Entity, Partial<'static, false>) -> Result<(), ComponentError>,
123 push: fn(&mut DynamicBundle, Partial<'static, false>) -> Result<(), ComponentError>,
125}
126
127impl ComponentEntry {
128 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 pub const fn name(&self) -> &'static str {
150 self.name
151 }
152}
153
154inventory::collect!(ComponentEntry);
155
156pub fn entries() -> impl Iterator<Item = &'static ComponentEntry> {
158 inventory::iter::<ComponentEntry>.into_iter()
159}
160
161pub fn names() -> impl Iterator<Item = &'static str> {
163 entries().map(ComponentEntry::name)
164}
165
166pub fn contains(name: &str) -> bool {
170 find(name).is_some()
171}
172
173pub 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
192pub 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
201pub 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
213pub 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
224pub fn default_partial(name: &str) -> Result<Partial<'static, false>, ComponentError> {
232 (entry(name)?.default_partial)()
233}
234
235pub 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
248pub 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
258pub 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
267fn 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
279fn 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
297fn default_partial_component<C: Facet<'static>>() -> Result<Partial<'static, false>, ComponentError>
299{
300 let partial = Partial::alloc_owned::<C>()?;
301 if C::SHAPE.is(Characteristic::Default) {
305 Ok(partial.set_default()?)
306 } else {
307 Ok(partial)
308 }
309}
310
311fn 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
349fn 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
359fn 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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}