use alloc::format;
use alloc::string::String;
use zerodds_ast::TypeNode;
use zerodds_ast_core::Id;
use zerodds_types::dynamic::{
DynamicError, DynamicType, DynamicTypeBuilder, ExtensibilityKind, NewMember, TypeKind,
};
use crate::ProtoFieldType;
use crate::descriptor::{Field, Label, Message};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MapError {
UnsupportedField {
field: String,
reason: &'static str,
},
Build(String),
UnknownType(String),
Cyclic(String),
}
pub(crate) fn build_error(error: &DynamicError) -> MapError {
MapError::Build(format!("{error}"))
}
pub(crate) fn element_type(
builder: &mut DynamicTypeBuilder,
field: &Field,
) -> Result<Id<TypeNode>, MapError> {
let ty = field.ty.ok_or_else(|| MapError::UnsupportedField {
field: field.name.clone(),
reason: "unknown field type code",
})?;
match ty {
ProtoFieldType::String => builder.string(false, None).map_err(|e| build_error(&e)),
ProtoFieldType::Bytes => {
let octet = builder
.primitive(TypeKind::Byte)
.map_err(|e| build_error(&e))?;
builder.sequence(octet, None).map_err(|e| build_error(&e))
}
ProtoFieldType::Message | ProtoFieldType::Group | ProtoFieldType::Enum => {
Err(MapError::UnsupportedField {
field: field.name.clone(),
reason: "nested message / enum needs the type registry (next step)",
})
}
scalar => {
let kind = scalar
.scalar_type_kind()
.ok_or_else(|| MapError::UnsupportedField {
field: field.name.clone(),
reason: "no primitive kind",
})?;
builder.primitive(kind).map_err(|e| build_error(&e))
}
}
}
pub(crate) fn field_type(
builder: &mut DynamicTypeBuilder,
field: &Field,
) -> Result<Id<TypeNode>, MapError> {
let element = element_type(builder, field)?;
match field.label {
Label::Repeated => builder.sequence(element, None).map_err(|e| build_error(&e)),
Label::Optional | Label::Required => Ok(element),
}
}
pub fn message_to_type(
message: &Message,
extensibility: ExtensibilityKind,
) -> Result<DynamicType, MapError> {
let mut builder = DynamicTypeBuilder::new();
let structure = builder
.structure(&message.name, extensibility)
.map_err(|e| build_error(&e))?;
for field in &message.fields {
let id = u32::try_from(field.number).map_err(|_| MapError::UnsupportedField {
field: field.name.clone(),
reason: "field number out of range",
})?;
let member_type = field_type(&mut builder, field)?;
builder
.add_member(
structure,
&NewMember::new(&field.name, member_type).with_id(id),
)
.map_err(|e| build_error(&e))?;
}
builder.finish_as(structure).map_err(|e| build_error(&e))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use alloc::string::ToString;
use alloc::vec;
fn field(name: &str, number: i32, ty: ProtoFieldType, label: Label) -> Field {
Field {
name: name.to_string(),
number,
label,
ty: Some(ty),
type_name: String::new(),
oneof_index: None,
}
}
#[test]
fn maps_scalar_string_and_repeated_members() {
let msg = Message {
name: "Sensor".to_string(),
fields: vec![
field("label", 1, ProtoFieldType::String, Label::Optional),
field("value", 2, ProtoFieldType::Int32, Label::Optional),
field("samples", 5, ProtoFieldType::Double, Label::Repeated),
field("blob", 7, ProtoFieldType::Bytes, Label::Optional),
],
nested: vec![],
enums: vec![],
map_entry: false,
oneofs: vec![],
};
let dt = message_to_type(&msg, ExtensibilityKind::Mutable).unwrap();
assert_eq!(dt.member_count(), 4);
assert_eq!(dt.member_by_name("label").unwrap().id(), 1);
assert_eq!(dt.member_by_name("value").unwrap().id(), 2);
assert_eq!(dt.member_by_name("samples").unwrap().id(), 5);
assert_eq!(dt.member_by_name("blob").unwrap().id(), 7);
assert_eq!(dt.member_by_id(5).unwrap().name(), "samples");
}
#[test]
fn nested_message_field_is_unsupported_for_now() {
let msg = Message {
name: "Outer".to_string(),
fields: vec![Field {
name: "inner".to_string(),
number: 1,
label: Label::Optional,
ty: Some(ProtoFieldType::Message),
type_name: ".pkg.Inner".to_string(),
oneof_index: None,
}],
nested: vec![],
enums: vec![],
map_entry: false,
oneofs: vec![],
};
assert!(matches!(
message_to_type(&msg, ExtensibilityKind::Appendable),
Err(MapError::UnsupportedField { .. })
));
}
}