concinnity_cook/
asset_api.rs1use crate::ecs::{AssetKind, AssetOrigin, BlobAssetDef};
6use crate::registry::RegisteredType;
7use crate::registry::Registration;
8use crate::result::CnResult;
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct AssetRequest {
13 pub asset_type: String,
16 #[serde(default)]
18 pub args: Option<serde_json::Value>,
19}
20
21pub fn create_asset_def(req: &AssetRequest) -> Result<BlobAssetDef, CnResult> {
32 if let Some(ct) = RegisteredType::parse(&req.asset_type) {
33 let reg = ct.registration();
34 if reg.origin != AssetOrigin::External {
35 return Err(CnResult::InvalidArgument);
36 }
37 let discriminant = ct.discriminant().ok_or(CnResult::InvalidArgument)?;
40 let args = resolve_args(®, &req.args);
41 let args_bytes = match crate::registry::bake_divergent(ct, &args)? {
45 Some(bytes) => bytes,
46 None => ct.reserialize_args(&args)?,
47 };
48 return Ok(BlobAssetDef {
49 name: None,
50 kind: AssetKind::Component,
51 discriminant,
52 args_bytes,
53 payload: None,
54 });
55 }
56
57 tracing::error!("asset_api: unknown asset type '{}'", req.asset_type);
58 Err(CnResult::AssetInvalidType)
59}
60
61fn resolve_args(reg: &Registration, supplied: &Option<serde_json::Value>) -> serde_json::Value {
67 let mut base = reg
68 .default_args
69 .clone()
70 .unwrap_or_else(|| serde_json::Value::Object(Default::default()));
71
72 if let Some(serde_json::Value::Object(supplied_map)) = supplied {
73 if let serde_json::Value::Object(ref mut base_map) = base {
74 for (k, v) in supplied_map {
75 base_map.insert(k.clone(), v.clone());
76 }
77 }
78 } else if let Some(v) = supplied {
79 base = v.clone();
80 }
81
82 base
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn shader_reg() -> Registration {
90 Registration {
91 type_name: "VertexStage",
92 origin: AssetOrigin::External,
93 payload: crate::ecs::AssetPayload::Compiled,
94 default_args: Some(serde_json::json!({ "source": "user.metal" })),
95 }
96 }
97
98 #[test]
99 fn resolve_args_none_uses_default() {
100 let reg = shader_reg();
101 let result = resolve_args(®, &None);
102 assert_eq!(result["source"], "user.metal");
103 }
104
105 #[test]
106 fn resolve_args_empty_object_fills_from_default() {
107 let reg = shader_reg();
108 let supplied = Some(serde_json::json!({}));
109 let result = resolve_args(®, &supplied);
110 assert_eq!(result["source"], "user.metal");
111 }
112
113 #[test]
114 fn resolve_args_supplied_value_wins() {
115 let reg = shader_reg();
116 let supplied = Some(serde_json::json!({ "source": "custom.metal" }));
117 let result = resolve_args(®, &supplied);
118 assert_eq!(result["source"], "custom.metal");
119 }
120
121 #[test]
122 fn resolve_args_partial_keeps_default_for_missing_keys() {
123 let reg = Registration {
124 type_name: "Fake",
125 origin: AssetOrigin::External,
126 payload: crate::ecs::AssetPayload::None,
127 default_args: Some(serde_json::json!({ "a": 1, "b": 2 })),
128 };
129 let supplied = Some(serde_json::json!({ "b": 99 }));
130 let result = resolve_args(®, &supplied);
131 assert_eq!(result["a"], 1);
132 assert_eq!(result["b"], 99);
133 }
134
135 #[test]
136 fn resolve_args_non_object_supplied_replaces_the_default() {
137 let reg = shader_reg();
138 let supplied = Some(serde_json::json!([1, 2, 3]));
139 let result = resolve_args(®, &supplied);
140 assert_eq!(result, serde_json::json!([1, 2, 3]));
141 }
142
143 #[test]
146 fn resolve_args_keeps_a_non_object_default_when_an_object_is_supplied() {
147 let reg = Registration {
148 type_name: "Fake",
149 origin: AssetOrigin::External,
150 payload: crate::ecs::AssetPayload::None,
151 default_args: Some(serde_json::json!([1, 2, 3])),
152 };
153 let supplied = Some(serde_json::json!({ "a": 1 }));
154 assert_eq!(resolve_args(®, &supplied), serde_json::json!([1, 2, 3]));
155 }
156
157 #[test]
158 fn create_asset_def_rejects_unknown_types() {
159 let req = AssetRequest {
160 asset_type: "NotARealAsset".to_string(),
161 args: None,
162 };
163 assert_eq!(
164 create_asset_def(&req).unwrap_err(),
165 CnResult::AssetInvalidType
166 );
167 }
168
169 #[test]
170 fn create_asset_def_rejects_runtime_only_types() {
171 let req = AssetRequest {
173 asset_type: "Transform".to_string(),
174 args: None,
175 };
176 assert_eq!(
177 create_asset_def(&req).unwrap_err(),
178 CnResult::InvalidArgument
179 );
180 }
181
182 #[test]
183 fn create_asset_def_builds_a_component_def() {
184 let req = AssetRequest {
185 asset_type: "ProceduralMesh".to_string(),
186 args: None,
187 };
188 let def = create_asset_def(&req).unwrap();
189 assert_eq!(def.kind, AssetKind::Component);
190 assert_eq!(
191 Some(def.discriminant),
192 RegisteredType::parse("ProceduralMesh")
193 .unwrap()
194 .discriminant()
195 );
196 assert!(def.name.is_none());
197 assert!(def.payload.is_none());
198 postcard::from_bytes::<crate::components::ProceduralMesh>(&def.args_bytes).unwrap();
200 }
201
202 #[test]
207 fn every_addable_component_type_builds_a_baked_def() {
208 for (ct, _) in RegisteredType::addable_types().filter(|(t, _)| !t.is_resource()) {
209 let def = create_asset_def(&AssetRequest {
210 asset_type: ct.as_str().to_string(),
211 args: None,
212 })
213 .unwrap();
214 assert_eq!(def.kind, AssetKind::Component, "{}", ct.as_str());
215 assert!(!def.args_bytes.is_empty(), "{}", ct.as_str());
216 }
217 }
218
219 #[test]
220 fn create_asset_def_merges_supplied_args_over_defaults() {
221 let req = AssetRequest {
222 asset_type: "ProceduralMesh".to_string(),
223 args: Some(serde_json::json!({ "generator": "box" })),
224 };
225 let def = create_asset_def(&req).unwrap();
226 let baked: crate::components::ProceduralMesh =
227 postcard::from_bytes(&def.args_bytes).unwrap();
228 assert_eq!(baked.generator, "box");
229 let defaults = crate::components::ProceduralMesh::default();
231 assert_eq!(baked.half_width, defaults.half_width);
232 assert_eq!(baked.ceiling_height, defaults.ceiling_height);
233 }
234
235 #[test]
236 fn create_asset_def_rejects_mistyped_args() {
237 let req = AssetRequest {
238 asset_type: "ProceduralMesh".to_string(),
239 args: Some(serde_json::json!({ "generator": 42 })),
240 };
241 assert_eq!(
242 create_asset_def(&req).unwrap_err(),
243 CnResult::InvalidArgument
244 );
245 }
246
247 #[test]
251 fn addable_types_are_external_only() {
252 let names: Vec<&str> = RegisteredType::addable_types()
253 .inspect(|(_, reg)| assert_eq!(reg.origin, AssetOrigin::External))
254 .map(|(ct, _)| ct.as_str())
255 .collect();
256 assert!(!names.is_empty());
257 assert!(names.contains(&"ProceduralMesh"));
258 assert!(!names.contains(&"Transform"));
259 }
260}