pub mod build_only;
use crate::result::CnResult;
pub use build_only::BuildOnlyAsset;
pub use concinnity_core::ecs::{AssetOrigin, AssetPayload};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Registration {
pub type_name: &'static str,
pub origin: AssetOrigin,
pub payload: AssetPayload,
pub default_args: Option<serde_json::Value>,
}
impl Registration {
pub fn addable(&self) -> bool {
self.origin == AssetOrigin::External
}
pub(crate) fn needs_compilation(&self) -> bool {
self.payload == AssetPayload::Compiled
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeResolution {
Column(RegisteredType),
Consumed,
Expanded,
Resource,
}
pub(crate) fn parse_expected_variants(msg: &str) -> Option<Vec<String>> {
let after = msg.split_once("expected")?.1;
let mut out = Vec::new();
let mut rest = after;
while let Some(open) = rest.find('`') {
let tail = &rest[open + 1..];
let close = tail.find('`')?;
out.push(tail[..close].to_string());
rest = &tail[close + 1..];
}
(!out.is_empty()).then_some(out)
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct NoArgs {}
macro_rules! __meta_origin {
() => { AssetOrigin::RuntimeOnly };
(external $($r:tt)*) => { AssetOrigin::External };
(runtime $($r:tt)*) => { AssetOrigin::RuntimeOnly };
($t:tt $($r:tt)*) => { __meta_origin!($($r)*) };
}
macro_rules! __meta_payload {
() => { AssetPayload::None };
(compiled $($r:tt)*) => { AssetPayload::Compiled };
($t:tt $($r:tt)*) => { __meta_payload!($($r)*) };
}
macro_rules! __meta_args_ty {
($default:path;) => { $default };
($default:path; runtime $($r:tt)*) => { NoArgs };
($default:path; args: $a:ident $($r:tt)*) => { concinnity_core::components::cook::$a };
($default:path; $t:tt $($r:tt)*) => { __meta_args_ty!($default; $($r)*) };
}
macro_rules! __meta_args_name {
($default:ident;) => { stringify!($default) };
($default:ident; args: $a:ident $($r:tt)*) => { concat!(stringify!($a), "Args") };
($default:ident; $t:tt $($r:tt)*) => { __meta_args_name!($default; $($r)*) };
}
macro_rules! __meta_validate {
($val:expr;) => { $val };
($val:expr; validate: $f:ident $($r:tt)*) => { crate::authoring::validate::$f($val) };
($val:expr; $t:tt $($r:tt)*) => { __meta_validate!($val; $($r)*) };
}
macro_rules! __meta_refs {
() => { &[] };
(refs: [ $( ($fld:literal, $tgt:literal) ),+ $(,)? ] $($r:tt)*) => { &[ $( ($fld, $tgt) ),+ ] };
($t:tt $($r:tt)*) => { __meta_refs!($($r)*) };
}
macro_rules! __meta_singleton {
() => { false };
(singleton $($r:tt)*) => { true };
($t:tt $($r:tt)*) => { crate::authoring::registry::__meta_singleton!($($r)*) };
}
macro_rules! __meta_useful_blank {
() => { false };
(useful_blank $($r:tt)*) => { true };
($t:tt $($r:tt)*) => { crate::authoring::registry::__meta_useful_blank!($($r)*) };
}
macro_rules! __meta_renders {
() => { false };
(renders $($r:tt)*) => { true };
($t:tt $($r:tt)*) => { crate::authoring::registry::__meta_renders!($($r)*) };
}
macro_rules! __meta_live {
() => { false };
(live $($r:tt)*) => { true };
($t:tt $($r:tt)*) => { crate::authoring::registry::__meta_live!($($r)*) };
}
pub(crate) use {__meta_live, __meta_renders, __meta_singleton, __meta_useful_blank};
macro_rules! __meta_surviving {
($variant:ident;) => { ScopeResolution::Column(RegisteredType::$variant) };
($variant:ident; consumed: $surviving:ident $($r:tt)*) => {
ScopeResolution::Column(RegisteredType::$surviving)
};
($variant:ident; consumed $($r:tt)*) => { ScopeResolution::Consumed };
($variant:ident; $t:tt $($r:tt)*) => { crate::authoring::registry::__meta_surviving!($variant; $($r)*) };
}
pub(crate) use __meta_surviving;
macro_rules! __group_surviving {
(stored; $variant:ident; $($meta:tt)*) => { __meta_surviving!($variant; $($meta)*) };
(build_only; $variant:ident; $($meta:tt)*) => { ScopeResolution::Expanded };
(resource; $variant:ident; $($meta:tt)*) => { ScopeResolution::Resource };
}
macro_rules! __group_discriminant {
(stored; $variant:ident) => {
Some(crate::ecs::ComponentTag::$variant as u8)
};
(build_only; $variant:ident) => {
None
};
(resource; $variant:ident) => {
None
};
}
macro_rules! __group_origin {
(stored; $($meta:tt)*) => { __meta_origin!($($meta)*) };
(build_only; $($meta:tt)*) => { AssetOrigin::BuildOnly };
(resource; $($meta:tt)*) => { AssetOrigin::External };
}
macro_rules! __group_payload {
(stored; $($meta:tt)*) => { __meta_payload!($($meta)*) };
(build_only; $($meta:tt)*) => { __meta_payload!($($meta)*) };
(resource; $($meta:tt)*) => { AssetPayload::Compiled };
}
macro_rules! __meta_resource_kind {
() => { None };
(resource: $kind:ident $($r:tt)*) => { Some(crate::ecs::ResourceKind::$kind) };
($t:tt $($r:tt)*) => { __meta_resource_kind!($($r)*) };
}
macro_rules! __meta_is_data {
() => { false };
(data $($r:tt)*) => { true };
($t:tt $($r:tt)*) => { __meta_is_data!($($r)*) };
}
macro_rules! __append_build_only {
($cb:ident; $($core_groups:tt)*) => {
$crate::for_each_build_only_type!($cb, $($core_groups)*);
};
}
macro_rules! for_each_authored_type {
($cb:ident) => {
concinnity_core::for_each_component!(__append_build_only; $cb;);
};
}
macro_rules! define_registered_type {
(
stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? },
build_only: { $( $bvariant:ident => $bty:path { $($bmeta:tt)* } ),+ $(,)? } $(,)?
) => {
define_registered_type!(@all
$( $variant => $ty { $($meta)* } [stored] ),+ ,
$( $bvariant => $bty { $($bmeta)* } [build_only] ),+ ,
$( $rvariant => $rty { $($rmeta)* } [resource] ),+
);
};
(@all $( $variant:ident => $ty:path { $($meta:tt)* } [$group:ident] ),+ $(,)? ) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[expect(missing_docs, reason = "one variant per registered component type, named for that type")]
pub enum RegisteredType {
$( $variant ),+
}
impl RegisteredType {
pub fn as_str(self) -> &'static str {
match self { $( Self::$variant => stringify!($variant) ),+ }
}
pub fn discriminant(self) -> Option<u8> {
match self {
$( Self::$variant => __group_discriminant!($group; $variant) ),+
}
}
pub(crate) fn from_discriminant(val: u8) -> Option<Self> {
$(
if __group_discriminant!($group; $variant) == Some(val) {
return Some(Self::$variant);
}
)+
None
}
pub fn scope_resolution(self) -> ScopeResolution {
match self {
$( Self::$variant => __group_surviving!($group; $variant; $($meta)*) ),+
}
}
pub fn parse(s: &str) -> Option<Self> {
$(
if s == stringify!($variant) { return Some(Self::$variant); }
)+
None
}
pub fn args_struct_name(self) -> &'static str {
match self {
$( Self::$variant => __meta_args_name!($variant; $($meta)*) ),+
}
}
pub fn registration(self) -> Registration {
match self {
$(
Self::$variant => Registration {
type_name: stringify!($variant),
origin: __group_origin!($group; $($meta)*),
payload: __group_payload!($group; $($meta)*),
default_args: serde_json::to_value(
<__meta_args_ty!($ty; $($meta)*) as Default>::default(),
)
.ok(),
}
),+
}
}
pub fn reserialize_args(self, args: &serde_json::Value) -> Result<Vec<u8>, CnResult> {
crate::ecs::asset_id::ensure_name_resolver();
match self {
$(
Self::$variant => {
let typed = serde_json::from_value::<__meta_args_ty!($ty; $($meta)*)>(
args.clone(),
)
.map_err(json_args_err)?;
Ok(postcard::to_allocvec(&__meta_validate!(typed; $($meta)*))?)
}
),+
}
}
pub fn normalized_args(
self,
args: &serde_json::Value,
) -> Result<serde_json::Value, CnResult> {
crate::ecs::asset_id::ensure_name_resolver();
match self {
$(
Self::$variant => {
let typed = serde_json::from_value::<__meta_args_ty!($ty; $($meta)*)>(
args.clone(),
)
.map_err(json_args_err)?;
serde_json::to_value(&__meta_validate!(typed; $($meta)*))
.map_err(json_args_err)
}
),+
}
}
pub fn field_enum_variants(self, field: &str) -> Option<Vec<String>> {
const SENTINEL: &str = "\u{0}__cn_enum_probe_sentinel__";
match self {
$(
Self::$variant => {
let mut probe = match serde_json::to_value(
<__meta_args_ty!($ty; $($meta)*) as Default>::default(),
) {
Ok(serde_json::Value::Object(m)) => m,
_ => return None,
};
probe.get(field)?;
probe.insert(
field.to_string(),
serde_json::Value::String(SENTINEL.to_string()),
);
match serde_json::from_value::<__meta_args_ty!($ty; $($meta)*)>(
serde_json::Value::Object(probe),
) {
Ok(_) => None,
Err(e) => parse_expected_variants(&e.to_string()),
}
}
),+
}
}
pub(crate) fn resource_kind(self) -> Option<crate::ecs::ResourceKind> {
match self {
$( Self::$variant => __meta_resource_kind!($($meta)*) ),+
}
}
pub fn is_resource(self) -> bool {
self.resource_kind().is_some()
}
pub(crate) fn is_data(self) -> bool {
match self {
$( Self::$variant => __meta_is_data!($($meta)*) ),+
}
}
pub fn ref_fields(self) -> &'static [(&'static str, &'static str)] {
match self {
$(
Self::$variant => __meta_refs!($($meta)*)
),+
}
}
pub fn singleton(self) -> bool {
match self {
$( Self::$variant => __meta_singleton!($($meta)*) ),+
}
}
pub fn useful_blank(self) -> bool {
match self {
$( Self::$variant => __meta_useful_blank!($($meta)*) ),+
}
}
pub fn renders(self) -> bool {
match self {
$( Self::$variant => __meta_renders!($($meta)*) ),+
}
}
pub fn live(self) -> bool {
match self {
$( Self::$variant => __meta_live!($($meta)*) ),+
}
}
pub fn addable(self) -> bool {
self.registration().addable()
}
pub fn all() -> &'static [RegisteredType] {
&[ $( Self::$variant ),+ ]
}
pub fn addable_types() -> impl Iterator<Item = (RegisteredType, Registration)> {
Self::all()
.iter()
.map(|t| (*t, t.registration()))
.filter(|(_, reg)| reg.addable())
}
}
};
}
for_each_authored_type!(define_registered_type);
pub trait Authored: serde::Serialize {
const TYPE: &'static str;
}
macro_rules! __authored_component {
(
stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? },
build_only: { $( $bvariant:ident => $bty:path { $($bmeta:tt)* } ),+ $(,)? } $(,)?
) => {
$( __authored_component!(@one $variant $ty { $($meta)* }); )+
$( __authored_component!(@one $rvariant $rty { $($rmeta)* }); )+
$( __authored_component!(@one $bvariant $bty { $($bmeta)* }); )+
};
(@one $variant:ident $ty:path { runtime $($rest:tt)* }) => {};
(@one $variant:ident $ty:path { $($meta:tt)* }) => {
impl Authored for __meta_args_ty!($ty; $($meta)*) {
const TYPE: &'static str = stringify!($variant);
}
};
}
for_each_authored_type!(__authored_component);
pub fn asset_line<T: Authored>(name: &str, value: &T) -> std::io::Result<String> {
let bad =
|e: serde_json::Error| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string());
let args = serde_json::to_value(value).map_err(bad)?;
let line = serde_json::json!({ "name": name, "type": T::TYPE, "args": args });
let mut out = serde_json::to_string(&line).map_err(bad)?;
out.push('\n');
Ok(out)
}
pub fn set_reference(line: &str, field: &str, target: &str) -> std::io::Result<String> {
let bad = |msg: String| std::io::Error::new(std::io::ErrorKind::InvalidData, msg);
let mut value: serde_json::Value =
serde_json::from_str(line).map_err(|e| bad(format!("asset line: {e}")))?;
value
.get_mut("args")
.and_then(|args| args.as_object_mut())
.ok_or_else(|| bad(format!("asset line has no args to hold '{field}'")))?
.insert(
field.to_string(),
serde_json::Value::String(target.to_string()),
);
let mut out = serde_json::to_string(&value).map_err(|e| bad(e.to_string()))?;
out.push('\n');
Ok(out)
}
#[cfg(test)]
mod authored_tests {
use super::*;
#[test]
fn set_reference_names_a_field_the_typed_value_cannot_carry() {
let line = asset_line("hero_shape", &crate::components::CharacterShape::default())
.expect("serializes");
let patched = set_reference(&line, "target", "hero").expect("patched");
assert!(patched.ends_with('\n'));
let value: serde_json::Value = serde_json::from_str(&patched).expect("parses");
assert_eq!(value["args"]["target"], "hero");
assert_eq!(value["name"], "hero_shape");
assert_eq!(value["type"], "CharacterShape");
let err = set_reference("7", "target", "hero").expect_err("not an asset line");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let err = set_reference(r#"{"name":"x"}"#, "target", "hero").expect_err("no args");
assert!(err.to_string().contains("no args"), "{err}");
}
#[test]
fn authored_types_report_their_registered_name() {
assert_eq!(
<concinnity_core::components::cook::Room as Authored>::TYPE,
"Room"
);
assert_eq!(
<crate::components::DirectionalLight as Authored>::TYPE,
"DirectionalLight"
);
assert_eq!(<crate::components::Texture as Authored>::TYPE, "Texture");
assert_eq!(
<crate::components::EnvironmentMap as Authored>::TYPE,
"EnvironmentMap"
);
}
#[test]
fn the_reported_name_round_trips_through_the_registry() {
for name in [
<concinnity_core::components::cook::Room as Authored>::TYPE,
<concinnity_core::components::cook::Camera3D as Authored>::TYPE,
<crate::components::DirectionalLight as Authored>::TYPE,
] {
assert!(
RegisteredType::parse(name).is_some(),
"{name} is not a registered component type"
);
}
}
}
fn json_args_err(e: serde_json::Error) -> CnResult {
tracing::error!("JSON args error: {}", e);
CnResult::InvalidArgument
}
pub fn bake_divergent(
ct: RegisteredType,
args: &serde_json::Value,
) -> Result<Option<Vec<u8>>, CnResult> {
crate::ecs::asset_id::ensure_name_resolver();
macro_rules! bake {
($ty:ty, $args_ty:ty) => {{
let typed = serde_json::from_value::<$args_ty>(args.clone()).map_err(json_args_err)?;
Ok(Some(postcard::to_allocvec(&<$ty>::bake(typed))?))
}};
}
match ct {
RegisteredType::Camera3D => {
bake!(
crate::components::Camera3D,
concinnity_core::components::cook::Camera3D
)
}
RegisteredType::Room => bake!(
crate::components::Room,
concinnity_core::components::cook::Room
),
RegisteredType::File => bake!(
crate::components::File,
concinnity_core::components::cook::File
),
RegisteredType::Spawner => {
bake!(
crate::components::Spawner,
concinnity_core::components::cook::Spawner
)
}
RegisteredType::AppConfig => {
bake!(
crate::components::AppConfig,
concinnity_core::components::cook::AppConfig
)
}
_ => Ok(None),
}
}
pub fn type_renders(asset_type: &str) -> bool {
let norm: String = asset_type.chars().filter(|c| *c != '_').collect();
let matches = |name: &str| name.eq_ignore_ascii_case(&norm);
RegisteredType::all()
.iter()
.any(|t| t.renders() && matches(t.as_str()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registration_predicates_follow_origin_and_payload() {
let reg = |origin, payload| Registration {
type_name: "T",
origin,
payload,
default_args: None,
};
let external = reg(AssetOrigin::External, AssetPayload::Compiled);
assert!(external.addable());
assert!(external.needs_compilation());
let runtime = reg(AssetOrigin::RuntimeOnly, AssetPayload::None);
assert!(!runtime.addable());
assert!(!runtime.needs_compilation());
let build = reg(AssetOrigin::BuildOnly, AssetPayload::None);
assert!(!build.addable());
assert!(!build.needs_compilation());
}
#[test]
fn bake_divergent_round_trips_through_from_baked() {
use crate::ecs::Component;
use crate::ecs::asset_id;
let args = serde_json::json!({"size": [16.0, 20.0, 3.5]});
let bytes = bake_divergent(RegisteredType::Room, &args)
.unwrap()
.expect("Room bakes divergently");
let baked = crate::components::Room::from_baked(&bytes).unwrap();
assert_eq!(baked.half_width, 8.0);
assert_eq!(baked.half_depth, 10.0);
assert_eq!(baked.ceiling_height, 3.5);
let args = serde_json::json!({"position": [1.0, 2.0, 3.0], "yaw": 0.5});
let bytes = bake_divergent(RegisteredType::Camera3D, &args)
.unwrap()
.expect("Camera3D bakes divergently");
let baked = crate::components::Camera3D::from_baked(&bytes).unwrap();
assert_eq!(baked.position, [1.0, 2.0, 3.0]);
let expected = crate::components::Camera3D::bake(
serde_json::from_value(serde_json::json!({"position": [1.0, 2.0, 3.0], "yaw": 0.5}))
.unwrap(),
);
assert_eq!(baked.view_matrix, expected.view_matrix);
let args = serde_json::json!({"path": "tri.obj"});
let bytes = bake_divergent(RegisteredType::File, &args)
.unwrap()
.expect("File bakes divergently");
let baked = crate::components::File::from_baked(&bytes).unwrap();
assert!(baked.kind.is_some());
asset_id::reset_interner();
let args = serde_json::json!({"template": "crate", "interval": -1.0, "lifetime": 2.0});
let bytes = bake_divergent(RegisteredType::Spawner, &args)
.unwrap()
.expect("Spawner bakes divergently");
let baked = crate::components::Spawner::from_baked(&bytes).unwrap();
assert_eq!(baked.interval, 0.0);
assert_eq!(baked.elapsed, 0.0);
assert_eq!(baked.count, 0);
assert!(
bake_divergent(RegisteredType::PointLight, &serde_json::json!({}))
.unwrap()
.is_none()
);
}
#[test]
fn component_types_round_trip_name_and_discriminant() {
for &ty in RegisteredType::all() {
assert_eq!(RegisteredType::parse(ty.as_str()), Some(ty));
match ty.discriminant() {
Some(d) => assert_eq!(RegisteredType::from_discriminant(d), Some(ty)),
None => assert!(
ty.registration().origin == AssetOrigin::BuildOnly || ty.is_resource(),
"{} has no discriminant but is neither build-only nor a resource",
ty.as_str()
),
}
}
assert_eq!(RegisteredType::parse("NotARealComponent"), None);
assert_eq!(RegisteredType::from_discriminant(255), None);
}
#[test]
fn only_stored_types_carry_a_discriminant() {
for &ty in RegisteredType::all() {
let stored = !(ty.registration().origin == AssetOrigin::BuildOnly || ty.is_resource());
assert_eq!(
ty.discriminant().is_some(),
stored,
"{} disagrees about whether it is stored in a column",
ty.as_str()
);
}
}
#[test]
fn component_discriminants_are_unique_and_in_range() {
let mut seen = std::collections::HashSet::new();
for &ty in RegisteredType::all() {
let Some(d) = ty.discriminant() else { continue };
assert!(
d < 128,
"{} discriminant {} outside the component range",
ty.as_str(),
d
);
assert!(seen.insert(d), "duplicate discriminant {d}");
}
}
#[test]
fn a_divergent_asset_names_the_schema_struct_the_docs_render() {
assert_eq!(RegisteredType::Room.args_struct_name(), "RoomArgs");
assert_eq!(RegisteredType::Camera3D.args_struct_name(), "Camera3DArgs");
assert_eq!(RegisteredType::File.args_struct_name(), "FileArgs");
assert_eq!(RegisteredType::Spawner.args_struct_name(), "SpawnerArgs");
assert_eq!(
RegisteredType::AppConfig.args_struct_name(),
"AppConfigArgs"
);
assert_eq!(RegisteredType::PointLight.args_struct_name(), "PointLight");
assert_eq!(RegisteredType::Prefab.args_struct_name(), "Prefab");
assert_eq!(RegisteredType::Texture.args_struct_name(), "Texture");
}
#[test]
fn reserialize_args_round_trips_and_rejects_bad_types() {
let ty = RegisteredType::parse("ProceduralMesh").unwrap();
let bytes = ty
.reserialize_args(&serde_json::json!({ "source": "a.glb" }))
.unwrap();
let back: crate::components::ProceduralMesh = postcard::from_bytes(&bytes).unwrap();
assert_eq!(back.source.as_deref(), Some("a.glb"));
assert_eq!(
ty.reserialize_args(&serde_json::json!({ "source": 42 }))
.unwrap_err(),
CnResult::InvalidArgument
);
}
#[test]
fn normalized_args_fills_defaults_and_rejects_bad_types() {
let ty = RegisteredType::parse("ProceduralMesh").unwrap();
let back = ty
.normalized_args(&serde_json::json!({ "generator": "box" }))
.unwrap();
assert_eq!(back["generator"], "box");
assert!(back.get("half_width").is_some(), "defaults fill in");
assert_eq!(
ty.normalized_args(&serde_json::json!({ "generator": 42 }))
.unwrap_err(),
CnResult::InvalidArgument
);
}
#[test]
fn declarable_assets_have_object_args_schemas() {
for &ty in RegisteredType::all() {
let reg = ty.registration();
if !reg.addable() {
continue;
}
let default_args = reg.default_args.as_ref().unwrap_or_else(|| {
panic!(
"{}: Args::default() failed to serialize to JSON",
ty.as_str()
)
});
assert!(
default_args.is_object(),
"{}: args schema is not a JSON object (got {default_args}). A declarable \
asset's args must be a JSON object of common types.",
ty.as_str()
);
}
}
#[test]
fn field_enum_variants_reports_string_enum_values() {
assert_eq!(
RegisteredType::Sprite.field_enum_variants("fit"),
Some(vec!["fit".into(), "cover".into(), "bottom".into()])
);
assert_eq!(
RegisteredType::TextLabel.field_enum_variants("align"),
Some(vec!["left".into(), "center".into(), "right".into()])
);
assert_eq!(
RegisteredType::AudioCue.field_enum_variants("kind"),
Some(vec!["music".into(), "sound".into()])
);
assert_eq!(
RegisteredType::HitRegion.field_enum_variants("action"),
None
);
assert_eq!(RegisteredType::KeyBinding.field_enum_variants("key"), None);
assert_eq!(RegisteredType::Sprite.field_enum_variants("nope"), None);
assert_eq!(RegisteredType::Sprite.field_enum_variants("x"), None);
}
#[test]
fn ref_fields_name_real_target_types() {
assert_eq!(
RegisteredType::Decal.ref_fields(),
&[("texture", "Texture")]
);
assert_eq!(
RegisteredType::AudioEmitter.ref_fields(),
&[("clip", "AudioClip"), ("prop", "Prop")]
);
assert!(RegisteredType::PointLight.ref_fields().is_empty());
for &ty in RegisteredType::all() {
let default_args = ty.registration().default_args;
for &(field, target) in ty.ref_fields() {
assert!(
RegisteredType::parse(target).is_some(),
"{}.{field} targets unknown type {target}",
ty.as_str()
);
if let Some(serde_json::Value::Object(m)) = &default_args {
assert!(
m.contains_key(field),
"{}.{field} is not an arg of {}",
ty.as_str(),
ty.as_str()
);
}
}
}
}
#[test]
fn structural_flags_mark_the_curated_sets() {
let flagged = |f: fn(RegisteredType) -> bool| -> Vec<&'static str> {
RegisteredType::all()
.iter()
.copied()
.filter(|&t| f(t))
.map(RegisteredType::as_str)
.collect()
};
assert_eq!(
flagged(RegisteredType::singleton),
[
"Window",
"GraphicsConfig",
"PostProcessConfig",
"StreamingConfig",
"PhysicsConfig",
"AppConfig",
"Variables",
"LoadingOverlay",
"EngineDefaults",
]
);
assert_eq!(
flagged(RegisteredType::renders),
[
"GraphicsConfig",
"Prop",
"TextLabel",
"InstancedProp",
"VoxelWorld",
"Sprite",
"WaterSurface",
"SdfVolume",
"LayoutContainer",
"StatHud",
"DebugHud",
"TextInput",
"LoadingOverlay",
"MainMenu",
"EnvironmentMap",
"SkinnedMesh",
]
);
for &ty in RegisteredType::all() {
if ty.useful_blank() {
assert!(
ty.addable(),
"{} is offered for a plain add but is not External",
ty.as_str()
);
}
if ty.singleton() {
assert!(
ty.registration().origin != AssetOrigin::RuntimeOnly,
"{} is a singleton but never declarable",
ty.as_str()
);
}
assert!(
!(ty.singleton() && ty.useful_blank()),
"{} cannot be both a singleton and a plain addable",
ty.as_str()
);
}
}
#[test]
fn type_renders_spans_both_registries() {
assert!(type_renders("TextLabel"));
assert!(type_renders("text_label"));
assert!(type_renders("GraphicsConfig"));
assert!(type_renders("EnvironmentMap"));
assert!(type_renders("SkinnedMesh"));
assert!(type_renders("skinned_mesh"));
assert!(!type_renders("Window"));
assert!(!type_renders("Mesh"));
assert!(!type_renders("NotARealType"));
}
#[test]
fn parse_expected_variants_handles_serde_phrasings() {
assert_eq!(
parse_expected_variants("unknown variant `z`, expected one of `a`, `b`, `c`"),
Some(vec!["a".into(), "b".into(), "c".into()])
);
assert_eq!(
parse_expected_variants("unknown variant `z`, expected `a` or `b`"),
Some(vec!["a".into(), "b".into()])
);
assert_eq!(
parse_expected_variants("unknown variant `z`, expected `only`"),
Some(vec!["only".into()])
);
assert_eq!(
parse_expected_variants("invalid type: string \"z\", expected u32"),
None
);
}
#[test]
fn per_instance_components_are_runtime_only() {
for ty in [
RegisteredType::Transform,
RegisteredType::MeshRenderer,
RegisteredType::ModelRenderer,
RegisteredType::Collider,
RegisteredType::Interactable,
RegisteredType::Pickup,
RegisteredType::Parent,
RegisteredType::Children,
RegisteredType::SceneMember,
RegisteredType::GlobalTransform,
RegisteredType::RenderHandle,
RegisteredType::Held,
] {
assert!(
!ty.registration().addable(),
"{} must be RuntimeOnly (not declarable)",
ty.as_str()
);
}
}
}