vpp-plugin-api-gen 0.2.1

Rust code generator for VPP API files.
Documentation
use std::sync::Arc;

use indexmap::IndexMap;
use serde::{
    Serialize,
    ser::{SerializeMap, SerializeSeq},
};

use crate::parser::{
    ApiParser, EnumVariant, Field, FieldSize, Message, OptionStatement, Service, TypeDetails,
    TypeEntry,
};

#[derive(Debug, Serialize)]
struct ApiFile<'a> {
    types: Vec<JsonTypeEntry>,
    messages: &'a [Message],
    unions: Vec<JsonTypeEntry>,
    enums: Vec<JsonTypeEntry>,
    enumflags: Vec<JsonTypeEntry>,
    services: IndexMap<String, Service>,
    options: IndexMap<String, Option<String>>,
    aliases: IndexMap<String, JsonAlias>,
    vl_api_version: String,
    imports: &'a [String],
    counters: &'a [()],
    paths: &'a [()],
}

#[derive(Debug, Serialize)]
struct ApiMessageInfo<'a> {
    crc: String,
    options: IndexMap<String, Option<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    comment: Option<&'a str>,
}

fn option_statements_to_json_map(options: &[OptionStatement]) -> IndexMap<String, Option<String>> {
    options
        .iter()
        .map(|opt| {
            (
                opt.option().to_string(),
                opt.value().map(|s| String::from(s).replace("\"", "")),
            )
        })
        .collect()
}

impl Serialize for Message {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let info = ApiMessageInfo {
            crc: format!("0x{:08x}", self.crc()),
            options: option_statements_to_json_map(self.options()),
            comment: self.comment(),
        };
        let mut seq = serializer.serialize_seq(Some(1 + self.fields().len() + 1))?;

        seq.serialize_element(self.name())?;
        for field in self.fields() {
            seq.serialize_element(field)?;
        }
        seq.serialize_element(&info)?;

        seq.end()
    }
}

impl Serialize for Service {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut len = 1;
        if self.stream() {
            len += 1;
        }
        if self.stream_message().is_some() {
            len += 1;
        }
        if !self.events().is_empty() {
            len += 1;
        }

        let mut map = serializer.serialize_map(Some(len))?;
        map.serialize_entry("reply", self.reply())?;
        if self.stream() {
            map.serialize_entry("stream", &self.stream())?;
        }
        if let Some(stream_msg) = self.stream_message() {
            map.serialize_entry("stream_msg", stream_msg)?;
        }
        if !self.events().is_empty() {
            map.serialize_entry("events", self.events())?;
        }

        map.end()
    }
}

impl Serialize for Field {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut len = 2;
        if !self.options.is_empty() {
            len += 1;
        }
        len += match &self.size {
            None => 0,
            Some(FieldSize::Fixed(_)) => 1,
            Some(FieldSize::Variable(None)) => 1,
            Some(FieldSize::Variable(Some(_))) => 2,
        };
        let mut seq = serializer.serialize_seq(Some(len))?;
        seq.serialize_element(&self.r#type)?;
        seq.serialize_element(&self.name)?;
        match &self.size {
            None => {}
            Some(FieldSize::Fixed(length)) => seq.serialize_element(&length)?,
            Some(FieldSize::Variable(None)) => seq.serialize_element(&0u32)?,
            Some(FieldSize::Variable(Some(variable))) => {
                seq.serialize_element(&0u32)?;
                seq.serialize_element(variable)?;
            }
        }
        if !self.options.is_empty() {
            seq.serialize_element(&option_statements_to_json_map(&self.options))?;
        }
        seq.end()
    }
}

#[derive(Debug)]
struct JsonAlias(Arc<TypeEntry>);

impl Serialize for JsonAlias {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match &self.0.details {
            TypeDetails::Typedef { r#type, size, .. } => {
                let mut len = 2;
                len += match size {
                    Some(FieldSize::Fixed(_)) => 1,
                    Some(FieldSize::Variable(None)) => 1,
                    _ => 0,
                };
                let mut seq = serializer.serialize_map(Some(len))?;
                seq.serialize_entry("type", r#type)?;
                match size {
                    Some(FieldSize::Fixed(length)) => seq.serialize_entry("length", &length)?,
                    Some(FieldSize::Variable(None)) => seq.serialize_entry("length", &0u32)?,
                    _ => {}
                }
                seq.end()
            }
            _ => serializer.serialize_none(),
        }
    }
}

#[derive(Debug)]
struct JsonTypeEntry(String, Arc<TypeEntry>);

#[derive(Debug, Serialize)]
struct JsonEnumInfo<'a> {
    enumtype: &'a str,
}

impl Serialize for JsonTypeEntry {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match &self.1.details {
            TypeDetails::TypedefBlock { fields, .. } => {
                let mut seq = serializer.serialize_seq(Some(1 + fields.len()))?;
                seq.serialize_element(&self.0)?;
                for field in fields {
                    seq.serialize_element(field)?;
                }
                seq.end()
            }
            TypeDetails::Typedef { .. } => unreachable!(),
            TypeDetails::Union { fields, .. } => {
                let mut seq = serializer.serialize_seq(Some(1 + fields.len()))?;
                seq.serialize_element(&self.0)?;
                for field in fields {
                    seq.serialize_element(field)?;
                }
                seq.end()
            }
            TypeDetails::Enum { size, variants } => {
                let len = 1 + variants.len() + 1;
                let mut seq = serializer.serialize_seq(Some(len))?;
                seq.serialize_element(&self.0)?;
                for variant in variants {
                    seq.serialize_element(variant)?;
                }
                let enuminfo = JsonEnumInfo { enumtype: size };
                seq.serialize_element(&enuminfo)?;
                seq.end()
            }
            TypeDetails::EnumFlag { size, variants } => {
                let len = 1 + variants.len() + 1;
                let mut seq = serializer.serialize_seq(Some(len))?;
                seq.serialize_element(&self.0)?;
                for variant in variants {
                    seq.serialize_element(variant)?;
                }
                let enuminfo = JsonEnumInfo { enumtype: size };
                seq.serialize_element(&enuminfo)?;
                seq.end()
            }
        }
    }
}

impl Serialize for EnumVariant {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(2))?;
        seq.serialize_element(&self.id)?;
        seq.serialize_element(&self.value)?;
        seq.end()
    }
}

pub(crate) fn generate_json(parser: &ApiParser) -> Result<String, serde_json::Error> {
    let services = parser
        .services()
        .iter()
        .map(|service| (service.caller().to_string(), service.clone()))
        .collect();
    let types = parser
        .global_types()
        .filter_map(|(n, t)| {
            if matches!(t.details, TypeDetails::TypedefBlock { .. }) {
                Some(JsonTypeEntry(n.clone(), t.clone()))
            } else {
                None
            }
        })
        .collect();
    let unions = parser
        .global_types()
        .filter_map(|(n, t)| {
            if matches!(t.details, TypeDetails::Union { .. }) {
                Some(JsonTypeEntry(n.clone(), t.clone()))
            } else {
                None
            }
        })
        .collect();
    let enums = parser
        .global_types()
        .filter_map(|(n, t)| {
            if matches!(t.details, TypeDetails::Enum { .. }) {
                Some(JsonTypeEntry(n.clone(), t.clone()))
            } else {
                None
            }
        })
        .collect();
    let enumflags = parser
        .global_types()
        .filter_map(|(n, t)| {
            if matches!(t.details, TypeDetails::EnumFlag { .. }) {
                Some(JsonTypeEntry(n.clone(), t.clone()))
            } else {
                None
            }
        })
        .collect();
    let aliases = parser
        .global_types()
        .filter_map(|(n, t)| {
            if matches!(t.details, TypeDetails::Typedef { .. }) {
                Some((n.clone(), JsonAlias(t.clone())))
            } else {
                None
            }
        })
        .collect();
    let api_file = ApiFile {
        types,
        messages: parser.messages(),
        unions,
        enums,
        enumflags,
        services,
        options: option_statements_to_json_map(parser.options()),
        aliases,
        vl_api_version: format!("0x{:08x}", parser.file_crc()),
        imports: parser.imports(),
        counters: &[],
        paths: &[],
    };
    serde_json::to_string_pretty(&api_file)
}

#[cfg(test)]
mod tests {
    use std::fs::File;

    use crate::{json::generate_json, parser::ApiParser};
    use pretty_assertions::assert_eq;

    #[test]
    fn sanity() {
        let api = ApiParser::new(&format!("{}/tests/sanity.api", env!("CARGO_MANIFEST_DIR")))
            .expect("failed");
        let json = generate_json(&api).expect("generating JSON failed");

        let expected_json: serde_json::Value = serde_json::from_reader(
            File::open(format!(
                "{}/tests/sanity.api.json",
                env!("CARGO_MANIFEST_DIR")
            ))
            .expect("unable to open tests/sanity.api.json"),
        )
        .expect("parsing expected JSON failed");
        let expected_json = serde_json::to_string_pretty(&expected_json)
            .expect("generating expected JSON string failed");

        assert_eq!(json, expected_json);
    }
}