1extern crate alloc;
37
38use crate::atom::{AtomTableOps, AtomError, atoms};
39use crate::term::{AtomIndex, TermValue};
40use alloc::{string::String, string::ToString, vec, vec::Vec, format};
41use core::fmt;
42
43#[derive(Debug, Clone, PartialEq)]
47pub enum TaggedError {
48 AtomError(AtomError),
50 WrongType { expected: &'static str, found: &'static str },
52 OutOfBounds { index: usize, max: usize },
54 MissingField(String),
56 TypeMismatch { expected: String, found: String },
58 InvalidVariant { enum_name: String, variant: String },
60 OutOfMemory,
62 InvalidUtf8,
64 NestedError { path: String, source: alloc::boxed::Box<TaggedError> },
66 Other(String),
68}
69
70impl TaggedError {
71 pub fn nested(path: impl Into<String>, source: TaggedError) -> Self {
73 TaggedError::NestedError {
74 path: path.into(),
75 source: alloc::boxed::Box::new(source),
76 }
77 }
78
79 pub fn type_mismatch(expected: impl Into<String>, found: impl Into<String>) -> Self {
81 TaggedError::TypeMismatch {
82 expected: expected.into(),
83 found: found.into(),
84 }
85 }
86
87 pub fn missing_field(field: impl Into<String>) -> Self {
89 TaggedError::MissingField(field.into())
90 }
91
92 pub fn invalid_variant(enum_name: impl Into<String>, variant: impl Into<String>) -> Self {
94 TaggedError::InvalidVariant {
95 enum_name: enum_name.into(),
96 variant: variant.into(),
97 }
98 }
99}
100
101impl fmt::Display for TaggedError {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 TaggedError::AtomError(e) => write!(f, "atom error: {}", e),
105 TaggedError::WrongType { expected, found } =>
106 write!(f, "wrong type: expected {}, found {}", expected, found),
107 TaggedError::OutOfBounds { index, max } =>
108 write!(f, "index {} out of bounds (max: {})", index, max),
109 TaggedError::MissingField(field) =>
110 write!(f, "missing required field: {}", field),
111 TaggedError::TypeMismatch { expected, found } =>
112 write!(f, "type mismatch: expected {}, found {}", expected, found),
113 TaggedError::InvalidVariant { enum_name, variant } =>
114 write!(f, "invalid variant '{}' for enum {}", variant, enum_name),
115 TaggedError::OutOfMemory => write!(f, "out of memory"),
116 TaggedError::InvalidUtf8 => write!(f, "invalid UTF-8"),
117 TaggedError::NestedError { path, source } =>
118 write!(f, "error at {}: {}", path, source),
119 TaggedError::Other(msg) => write!(f, "{}", msg),
120 }
121 }
122}
123
124impl From<AtomError> for TaggedError {
125 fn from(error: AtomError) -> Self {
126 TaggedError::AtomError(error)
127 }
128}
129
130pub type TaggedResult<T> = core::result::Result<T, TaggedError>;
132
133pub trait TaggedMap: Sized {
139 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue>;
144
145 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self>;
150
151 fn type_name() -> &'static str;
153}
154
155pub fn to_snake_case(name: &str) -> String {
164 let mut result = String::new();
165 let chars: Vec<char> = name.chars().collect();
166
167 for (i, &ch) in chars.iter().enumerate() {
168 if ch.is_uppercase() {
169 let should_add_underscore = if i == 0 {
171 false } else {
173 let prev_char = chars[i - 1];
174 prev_char.is_lowercase()
176 };
177
178 if should_add_underscore {
179 result.push('_');
180 }
181
182 result.push(ch.to_lowercase().next().unwrap());
183 } else {
184 result.push(ch);
185 }
186 }
187
188 result
189}
190
191pub fn get_type_atom<T: AtomTableOps>(type_name: &str, table: &T) -> TaggedResult<AtomIndex> {
193 let atom_index = table.ensure_atom_str(type_name).map_err(TaggedError::from)?;
194 Ok(atom_index)
195}
196
197pub fn type_field_atom<T: AtomTableOps>(table: &T) -> TaggedResult<AtomIndex> {
199 let atom_index = table.ensure_atom_str("type").map_err(TaggedError::from)?;
200 Ok(atom_index)
201}
202
203pub fn variant_field_atom<T: AtomTableOps>(table: &T) -> TaggedResult<AtomIndex> {
205 let atom_index = table.ensure_atom_str("variant").map_err(TaggedError::from)?;
206 Ok(atom_index)
207}
208
209pub fn get_map_value(map: &TermValue, key_atom: AtomIndex) -> TaggedResult<&TermValue> {
211 match map {
212 TermValue::Map(pairs) => {
213 let key = TermValue::Atom(key_atom);
214 pairs.iter()
215 .find(|(k, _)| k == &key)
216 .map(|(_, v)| v)
217 .ok_or_else(|| TaggedError::Other(format!("key not found in map")))
218 }
219 _ => Err(TaggedError::WrongType { expected: "map", found: "other" }),
220 }
221}
222
223pub fn extract_string_field<T: AtomTableOps>(map: &TermValue, field_name: &str, table: &T) -> TaggedResult<String> {
225 let field_atom = get_type_atom(field_name, table)?;
226 let value = get_map_value(map, field_atom)?;
227
228 match value {
229 TermValue::Binary(bytes) => {
230 String::from_utf8(bytes.clone()).map_err(|_| TaggedError::InvalidUtf8)
231 }
232 _ => Err(TaggedError::WrongType { expected: "binary/string", found: "other" }),
233 }
234}
235
236pub fn extract_int_field<T: AtomTableOps>(map: &TermValue, field_name: &str, table: &T) -> TaggedResult<i32> {
238 let field_atom = get_type_atom(field_name, table)?;
239 let value = get_map_value(map, field_atom)?;
240
241 match value {
242 TermValue::SmallInt(i) => Ok(*i),
243 _ => Err(TaggedError::WrongType { expected: "integer", found: "other" }),
244 }
245}
246
247pub fn extract_float_field<T: AtomTableOps>(map: &TermValue, field_name: &str, table: &T) -> TaggedResult<f64> {
249 let field_atom = get_type_atom(field_name, table)?;
250 let value = get_map_value(map, field_atom)?;
251
252 match value {
253 TermValue::Float(f) => Ok(*f),
254 TermValue::SmallInt(i) => Ok(*i as f64), _ => Err(TaggedError::WrongType { expected: "float", found: "other" }),
256 }
257}
258
259pub fn extract_bool_field<T: AtomTableOps>(map: &TermValue, field_name: &str, table: &T) -> TaggedResult<bool> {
261 let field_atom = get_type_atom(field_name, table)?;
262 let value = get_map_value(map, field_atom)?;
263
264 let true_atom = atoms::true_atom(table).map_err(TaggedError::from)?;
265 let false_atom = atoms::false_atom(table).map_err(TaggedError::from)?;
266
267 match value {
268 TermValue::Atom(atom_idx) => {
269 if *atom_idx == true_atom {
270 Ok(true)
271 } else if *atom_idx == false_atom {
272 Ok(false)
273 } else {
274 Err(TaggedError::WrongType { expected: "boolean", found: "other atom" })
275 }
276 }
277 _ => Err(TaggedError::WrongType { expected: "boolean", found: "other" }),
278 }
279}
280
281pub fn extract_optional_field<R, F, A>(
283 map: &TermValue,
284 field_name: &str,
285 table: &A,
286 extractor: F
287) -> TaggedResult<Option<R>>
288where
289 F: FnOnce(&TermValue, &A) -> TaggedResult<R>,
290 A: AtomTableOps,
291{
292 let field_atom = get_type_atom(field_name, table)?;
293
294 match get_map_value(map, field_atom) {
295 Ok(value) => {
296 let nil_atom = atoms::nil(table).map_err(TaggedError::from)?;
297 match value {
298 TermValue::Atom(atom_idx) if *atom_idx == nil_atom => Ok(None),
299 _ => extractor(value, table).map(Some),
300 }
301 }
302 Err(_) => Ok(None), }
304}
305
306pub fn validate_type_discriminator<T: AtomTableOps>(map: &TermValue, expected_type: &str, table: &T) -> TaggedResult<()> {
308 let type_atom = type_field_atom(table)?;
309 let expected_type_atom = get_type_atom(expected_type, table)?;
310
311 let type_value = get_map_value(map, type_atom)?;
312
313 match type_value {
314 TermValue::Atom(actual_type_atom) => {
315 if *actual_type_atom == expected_type_atom {
316 Ok(())
317 } else {
318 let actual_name = match table.get_atom_string(*actual_type_atom) {
320 Ok(atom_ref) => atom_ref.as_str().unwrap_or("unknown").to_string(),
321 Err(_) => "unknown".to_string(),
322 };
323 Err(TaggedError::type_mismatch(expected_type, actual_name))
324 }
325 }
326 _ => Err(TaggedError::WrongType { expected: "atom", found: "other" }),
327 }
328}
329
330impl TaggedMap for i32 {
335 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue> {
336 let type_atom = get_type_atom("i32", table)?;
337 let value_atom = get_type_atom("value", table)?;
338
339 let pairs = alloc::vec![
340 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
341 (TermValue::Atom(value_atom), TermValue::SmallInt(*self)),
342 ];
343
344 Ok(TermValue::Map(pairs))
345 }
346
347 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self> {
348 validate_type_discriminator(&map, "i32", table)?;
349 extract_int_field(&map, "value", table)
350 }
351
352 fn type_name() -> &'static str {
353 "i32"
354 }
355}
356
357impl TaggedMap for String {
358 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue> {
359 let type_atom = get_type_atom("string", table)?;
360 let value_atom = get_type_atom("value", table)?;
361
362 let pairs = alloc::vec![
363 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
364 (TermValue::Atom(value_atom), TermValue::Binary(self.as_bytes().to_vec())),
365 ];
366
367 Ok(TermValue::Map(pairs))
368 }
369
370 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self> {
371 validate_type_discriminator(&map, "string", table)?;
372 extract_string_field(&map, "value", table)
373 }
374
375 fn type_name() -> &'static str {
376 "string"
377 }
378}
379
380impl TaggedMap for bool {
381 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue> {
382 let type_atom = get_type_atom("bool", table)?;
383 let value_atom = get_type_atom("value", table)?;
384 let bool_atom = if *self {
385 atoms::true_atom(table).map_err(TaggedError::from)?
386 } else {
387 atoms::false_atom(table).map_err(TaggedError::from)?
388 };
389
390 let pairs = alloc::vec![
391 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
392 (TermValue::Atom(value_atom), TermValue::Atom(bool_atom)),
393 ];
394
395 Ok(TermValue::Map(pairs))
396 }
397
398 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self> {
399 validate_type_discriminator(&map, "bool", table)?;
400 extract_bool_field(&map, "value", table)
401 }
402
403 fn type_name() -> &'static str {
404 "bool"
405 }
406}
407
408impl<U: TaggedMap> TaggedMap for Option<U> {
409 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue> {
410 match self {
411 Some(value) => {
412 let inner_map = value.to_tagged_map(table)?;
413 let type_atom = get_type_atom("option", table)?;
414 let variant_atom = variant_field_atom(table)?;
415 let some_atom = get_type_atom("some", table)?;
416 let value_atom = get_type_atom("value", table)?;
417
418 let pairs = alloc::vec![
419 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
420 (TermValue::Atom(variant_atom), TermValue::Atom(some_atom)),
421 (TermValue::Atom(value_atom), inner_map),
422 ];
423
424 Ok(TermValue::Map(pairs))
425 }
426 None => {
427 let type_atom = get_type_atom("option", table)?;
428 let variant_atom = variant_field_atom(table)?;
429 let none_atom = atoms::nil(table).map_err(TaggedError::from)?;
430
431 let pairs = alloc::vec![
432 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
433 (TermValue::Atom(variant_atom), TermValue::Atom(none_atom)),
434 ];
435
436 Ok(TermValue::Map(pairs))
437 }
438 }
439 }
440
441 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self> {
442 validate_type_discriminator(&map, "option", table)?;
443
444 let variant_atom = variant_field_atom(table)?;
445 let variant_value = get_map_value(&map, variant_atom)?;
446
447 let some_atom = get_type_atom("some", table)?;
448 let none_atom = atoms::nil(table).map_err(TaggedError::from)?;
449
450 match variant_value {
451 TermValue::Atom(atom_idx) if *atom_idx == some_atom => {
452 let value_atom = get_type_atom("value", table)?;
453 let inner_map = get_map_value(&map, value_atom)?;
454 let inner_value = U::from_tagged_map(inner_map.clone(), table)?;
455 Ok(Some(inner_value))
456 }
457 TermValue::Atom(atom_idx) if *atom_idx == none_atom => {
458 Ok(None)
459 }
460 _ => Err(TaggedError::invalid_variant("Option", "unknown")),
461 }
462 }
463
464 fn type_name() -> &'static str {
465 "option"
466 }
467}
468
469impl<U: TaggedMap> TaggedMap for Vec<U> {
470 fn to_tagged_map<T: AtomTableOps>(&self, table: &T) -> TaggedResult<TermValue> {
471 let type_atom = get_type_atom("vec", table)?;
472 let elements_atom = get_type_atom("elements", table)?;
473
474 let mut element_maps = Vec::new();
476 for item in self {
477 element_maps.push(item.to_tagged_map(table)?);
478 }
479
480 let elements_list = TermValue::from_vec(element_maps);
481
482 let pairs = alloc::vec![
483 (TermValue::Atom(type_field_atom(table)?), TermValue::Atom(type_atom)),
484 (TermValue::Atom(elements_atom), elements_list),
485 ];
486
487 Ok(TermValue::Map(pairs))
488 }
489
490 fn from_tagged_map<T: AtomTableOps>(map: TermValue, table: &T) -> TaggedResult<Self> {
491 validate_type_discriminator(&map, "vec", table)?;
492
493 let elements_atom = get_type_atom("elements", table)?;
494 let elements_value = get_map_value(&map, elements_atom)?;
495
496 let elements_vec = elements_value.list_to_vec();
497 let mut result = Vec::new();
498
499 for element_map in elements_vec {
500 let item = U::from_tagged_map(element_map, table)?;
501 result.push(item);
502 }
503
504 Ok(result)
505 }
506
507 fn type_name() -> &'static str {
508 "vec"
509 }
510}
511
512#[cfg(feature = "derive")]
516pub use avmnif_derive::TaggedMap;