1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4
5use miden_core::{Felt, Word};
6use semver::Version;
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::super::{
11 FeltSchema,
12 MapSlotSchema,
13 StorageSchema,
14 StorageSlotSchema,
15 StorageValueName,
16 ValueSlotSchema,
17 WordSchema,
18 WordValue,
19};
20use crate::account::StorageSlotName;
21use crate::account::component::storage::type_registry::SCHEMA_TYPE_REGISTRY;
22use crate::account::component::{AccountComponentMetadata, SchemaType};
23use crate::errors::ComponentMetadataError;
24
25mod init_storage_data;
26mod serde_impls;
27
28#[cfg(test)]
29mod tests;
30
31#[derive(Debug, Deserialize)]
35#[serde(rename_all = "kebab-case", deny_unknown_fields)]
36struct RawAccountComponentMetadata {
37 name: String,
38 description: String,
39 version: Version,
40 #[serde(rename = "storage")]
41 #[serde(default)]
42 storage: RawStorageSchema,
43}
44
45impl AccountComponentMetadata {
46 pub fn from_toml(toml_string: &str) -> Result<Self, ComponentMetadataError> {
54 let raw: RawAccountComponentMetadata = toml::from_str(toml_string)
55 .map_err(ComponentMetadataError::TomlDeserializationError)?;
56
57 if !raw.description.is_ascii() {
58 return Err(ComponentMetadataError::InvalidSchema(
59 "description must contain only ASCII characters".to_string(),
60 ));
61 }
62
63 let RawStorageSchema { slots } = raw.storage;
64 let mut fields = Vec::with_capacity(slots.len());
65
66 for slot in slots {
67 fields.push(slot.try_into_slot_schema()?);
68 }
69
70 let storage_schema = StorageSchema::new(fields)?;
71 Ok(Self::new(raw.name)
72 .with_description(raw.description)
73 .with_version(raw.version)
74 .with_storage_schema(storage_schema))
75 }
76
77 pub fn to_toml(&self) -> Result<String, ComponentMetadataError> {
79 let toml = toml::to_string(self).map_err(ComponentMetadataError::TomlSerializationError)?;
80 Ok(toml)
81 }
82}
83
84#[derive(Debug, Default, Deserialize, Serialize)]
95#[serde(rename_all = "kebab-case", deny_unknown_fields)]
96struct RawStorageSchema {
97 #[serde(default)]
98 slots: Vec<RawStorageSlotSchema>,
99}
100
101#[derive(Debug, Clone, Deserialize, Serialize)]
109#[serde(untagged)]
110enum RawSlotType {
111 Word(RawWordType),
112 Map(RawMapType),
113}
114
115#[derive(Debug, Clone, Deserialize, Serialize)]
117#[serde(untagged)]
118enum RawWordType {
119 TypeIdentifier(SchemaType),
120 FeltSchemaArray(Vec<FeltSchema>),
121}
122
123#[derive(Debug, Clone, Deserialize, Serialize)]
125#[serde(rename_all = "kebab-case", deny_unknown_fields)]
126struct RawMapType {
127 key: RawWordType,
128 value: RawWordType,
129}
130
131impl Serialize for StorageSchema {
135 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136 where
137 S: Serializer,
138 {
139 let slots = self
140 .slots()
141 .iter()
142 .map(|(slot_name, schema)| RawStorageSlotSchema::from_slot(slot_name, schema))
143 .collect();
144
145 RawStorageSchema { slots }.serialize(serializer)
146 }
147}
148
149impl<'de> Deserialize<'de> for StorageSchema {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: Deserializer<'de>,
153 {
154 let raw = RawStorageSchema::deserialize(deserializer)?;
156 let mut fields = Vec::with_capacity(raw.slots.len());
157
158 for slot in raw.slots {
159 let (slot_name, schema) = slot.try_into_slot_schema().map_err(D::Error::custom)?;
160 fields.push((slot_name, schema));
161 }
162
163 StorageSchema::new(fields).map_err(D::Error::custom)
164 }
165}
166
167#[derive(Debug, Deserialize, Serialize)]
174#[serde(rename_all = "kebab-case", deny_unknown_fields)]
175struct RawStorageSlotSchema {
176 name: String,
179 #[serde(default)]
180 description: Option<String>,
181 #[serde(rename = "type")]
189 r#type: RawSlotType,
190 #[serde(default)]
192 default_value: Option<WordValue>,
193 #[serde(default)]
198 default_values: Option<Vec<RawMapEntrySchema>>,
199}
200
201#[derive(Debug, Deserialize, Serialize)]
202#[serde(deny_unknown_fields)]
203struct RawMapEntrySchema {
204 key: WordValue,
205 value: WordValue,
206}
207
208impl RawStorageSlotSchema {
209 fn from_slot(slot_name: &StorageSlotName, schema: &StorageSlotSchema) -> Self {
213 match schema {
214 StorageSlotSchema::Value(schema) => Self::from_value_slot(slot_name, schema),
215 StorageSlotSchema::Map(schema) => Self::from_map_slot(slot_name, schema),
216 }
217 }
218
219 fn from_value_slot(slot_name: &StorageSlotName, schema: &ValueSlotSchema) -> Self {
220 let word = schema.word();
221 let (r#type, default_value) = match word {
222 WordSchema::Simple { r#type, default_value } => (
223 RawSlotType::Word(RawWordType::TypeIdentifier(r#type.clone())),
224 default_value.map(|word| WordValue::from_word(r#type, word)),
225 ),
226 WordSchema::Composite { value } => {
227 (RawSlotType::Word(RawWordType::FeltSchemaArray(value.to_vec())), None)
228 },
229 };
230
231 Self {
232 name: slot_name.as_str().to_string(),
233 description: schema.description().cloned(),
234 r#type,
235 default_value,
236 default_values: None,
237 }
238 }
239
240 fn from_map_slot(slot_name: &StorageSlotName, schema: &MapSlotSchema) -> Self {
241 let default_values = schema.default_values().map(|default_values| {
242 default_values
243 .into_iter()
244 .map(|(key, value)| RawMapEntrySchema {
245 key: WordValue::from_word(&schema.key_schema().word_type(), key),
246 value: WordValue::from_word(&schema.value_schema().word_type(), value),
247 })
248 .collect()
249 });
250
251 let key_type = match schema.key_schema() {
252 WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()),
253 WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()),
254 };
255
256 let value_type = match schema.value_schema() {
257 WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()),
258 WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()),
259 };
260
261 Self {
262 name: slot_name.as_str().to_string(),
263 description: schema.description().cloned(),
264 r#type: RawSlotType::Map(RawMapType { key: key_type, value: value_type }),
265 default_value: None,
266 default_values,
267 }
268 }
269
270 fn try_into_slot_schema(
275 self,
276 ) -> Result<(StorageSlotName, StorageSlotSchema), ComponentMetadataError> {
277 let RawStorageSlotSchema {
278 name,
279 description,
280 r#type,
281 default_value,
282 default_values,
283 } = self;
284
285 let slot_name_raw = name;
286 let slot_name = StorageSlotName::new(slot_name_raw.clone()).map_err(|err| {
287 ComponentMetadataError::InvalidSchema(format!(
288 "invalid storage slot name `{slot_name_raw}`: {err}"
289 ))
290 })?;
291
292 let description =
293 description.and_then(|d| if d.trim().is_empty() { None } else { Some(d) });
294
295 let slot_prefix = StorageValueName::from_slot_name(&slot_name);
296
297 if default_value.is_some() && default_values.is_some() {
298 return Err(ComponentMetadataError::InvalidSchema(
299 "storage slot schema cannot define both `default-value` and `default-values`"
300 .into(),
301 ));
302 }
303
304 match r#type {
305 RawSlotType::Map(map_type) => {
306 if default_value.is_some() {
307 return Err(ComponentMetadataError::InvalidSchema(
308 "map slots cannot define `default-value`".into(),
309 ));
310 }
311
312 let RawMapType { key: key_type, value: value_type } = map_type;
313 let key_schema = Self::parse_word_schema(key_type, "`type.key`")?;
314 let value_schema = Self::parse_word_schema(value_type, "`type.value`")?;
315
316 let default_values = default_values
317 .map(|entries| {
318 Self::parse_default_map_entries(
319 entries,
320 &key_schema,
321 &value_schema,
322 &slot_prefix,
323 )
324 })
325 .transpose()?;
326
327 Ok((
328 slot_name,
329 StorageSlotSchema::Map(MapSlotSchema::new(
330 description,
331 default_values,
332 key_schema,
333 value_schema,
334 )),
335 ))
336 },
337
338 RawSlotType::Word(word_type) => {
339 if default_values.is_some() {
340 return Err(ComponentMetadataError::InvalidSchema(
341 "`default-values` can be specified only for map slots (use `type = { ... }`)"
342 .into(),
343 ));
344 }
345
346 match word_type {
347 RawWordType::TypeIdentifier(r#type) => {
348 if r#type.as_str() == "map" {
349 return Err(ComponentMetadataError::InvalidSchema(
350 "value slots cannot use `type = \"map\"`; use `type = { key = <key-type>, value = <value-type>}` instead"
351 .into(),
352 ));
353 }
354
355 let word = default_value
356 .as_ref()
357 .map(|default_value| {
358 default_value.try_parse_as_typed_word(
359 &r#type,
360 &slot_prefix,
361 "default value",
362 )
363 })
364 .transpose()?;
365
366 let word_schema = match word {
367 Some(word) => WordSchema::new_simple_with_default(r#type, word),
368 None => WordSchema::new_simple(r#type),
369 };
370
371 Ok((
372 slot_name,
373 StorageSlotSchema::Value(ValueSlotSchema::new(
374 description,
375 word_schema,
376 )),
377 ))
378 },
379
380 RawWordType::FeltSchemaArray(elements) => {
381 if default_value.is_some() {
382 return Err(ComponentMetadataError::InvalidSchema(
383 "composite word slots cannot define `default-value`".into(),
384 ));
385 }
386
387 let elements = Self::parse_felt_schema_array(elements, "word slot `type`")?;
388 Ok((
389 slot_name,
390 StorageSlotSchema::Value(ValueSlotSchema::new(
391 description,
392 WordSchema::new_value(elements),
393 )),
394 ))
395 },
396 }
397 },
398 }
399 }
400
401 fn parse_word_schema(
402 raw: RawWordType,
403 label: &str,
404 ) -> Result<WordSchema, ComponentMetadataError> {
405 match raw {
406 RawWordType::TypeIdentifier(r#type) => Ok(WordSchema::new_simple(r#type)),
407 RawWordType::FeltSchemaArray(elements) => {
408 let elements = Self::parse_felt_schema_array(elements, label)?;
409 Ok(WordSchema::new_value(elements))
410 },
411 }
412 }
413
414 fn parse_felt_schema_array(
415 elements: Vec<FeltSchema>,
416 label: &str,
417 ) -> Result<[FeltSchema; 4], ComponentMetadataError> {
418 if elements.len() != 4 {
419 return Err(ComponentMetadataError::InvalidSchema(format!(
420 "{label} must be an array of 4 elements, got {}",
421 elements.len()
422 )));
423 }
424 Ok(elements.try_into().expect("length is 4"))
425 }
426
427 fn parse_default_map_entries(
428 entries: Vec<RawMapEntrySchema>,
429 key_schema: &WordSchema,
430 value_schema: &WordSchema,
431 slot_prefix: &StorageValueName,
432 ) -> Result<BTreeMap<Word, Word>, ComponentMetadataError> {
433 let mut map = BTreeMap::new();
434
435 let parse = |schema: &WordSchema, raw: &WordValue, label: &str| {
436 super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix).map_err(
437 |err| {
438 ComponentMetadataError::InvalidSchema(format!("invalid map `{label}`: {err}"))
439 },
440 )
441 };
442
443 for (index, entry) in entries.into_iter().enumerate() {
444 let key_label = format!("default-values[{index}].key");
445 let value_label = format!("default-values[{index}].value");
446
447 let key = parse(key_schema, &entry.key, &key_label)?;
448 let value = parse(value_schema, &entry.value, &value_label)?;
449
450 if map.insert(key, value).is_some() {
451 return Err(ComponentMetadataError::InvalidSchema(format!(
452 "map storage slot `default-values[{index}]` contains a duplicate key"
453 )));
454 }
455 }
456
457 Ok(map)
458 }
459}
460
461impl WordValue {
462 pub(super) fn try_parse_as_typed_word(
463 &self,
464 schema_type: &SchemaType,
465 slot_prefix: &StorageValueName,
466 label: &str,
467 ) -> Result<Word, ComponentMetadataError> {
468 let word = match self {
469 WordValue::FullyTyped(word) => *word,
470 WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY
471 .try_parse_word(schema_type, value)
472 .map_err(ComponentMetadataError::StorageValueParsingError)?,
473 WordValue::Elements(elements) => {
474 let felts = elements
475 .iter()
476 .map(|element| {
477 SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaType::native_felt(), element)
478 })
479 .collect::<Result<Vec<Felt>, _>>()
480 .map_err(ComponentMetadataError::StorageValueParsingError)?;
481 let felts: [Felt; 4] = felts.try_into().expect("length is 4");
482 Word::from(felts)
483 },
484 };
485
486 WordSchema::new_simple(schema_type.clone()).validate_word_value(
487 slot_prefix,
488 label,
489 word,
490 )?;
491 Ok(word)
492 }
493
494 pub(super) fn from_word(schema_type: &SchemaType, word: Word) -> Self {
495 WordValue::Atomic(SCHEMA_TYPE_REGISTRY.display_word(schema_type, word).value().to_string())
496 }
497}