vrl 0.32.0

Vector Remap Language
Documentation
use super::util::example_path_or_basename;
use crate::compiler::prelude::*;
use crate::protobuf::descriptor::get_message_descriptor;
use crate::protobuf::parse::parse_proto;
use crate::stdlib::json_utils::json_type_def::json_type_def;
use prost_reflect::MessageDescriptor;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;

#[derive(Clone, Copy, Debug)]
pub struct ParseProto;

// This needs to be static because parse_proto needs to read a file
// and the file path needs to be a literal.
static EXAMPLE_PARSE_PROTO_EXPR: LazyLock<&str> = LazyLock::new(|| {
    let path = example_path_or_basename("protobuf/test_protobuf/v1/test_protobuf.desc");

    Box::leak(
        format!(
            r#"parse_proto!(decode_base64!("Cgdzb21lb25lIggKBjEyMzQ1Ng=="), "{path}", "test_protobuf.v1.Person")"#
        )
        .into_boxed_str(),
    )
});

static EXAMPLES: LazyLock<Vec<Example>> = LazyLock::new(|| {
    vec![example! {
        title: "Parse proto",
        source: &EXAMPLE_PARSE_PROTO_EXPR,
        result: Ok(r#"{ "name": "someone", "phones": [{"number": "123456"}] }"#),
    }]
});

impl Function for ParseProto {
    fn identifier(&self) -> &'static str {
        "parse_proto"
    }

    fn summary(&self) -> &'static str {
        "parse a string to a protobuf based type"
    }

    fn usage(&self) -> &'static str {
        "Parses the `value` as a protocol buffer payload."
    }

    fn category(&self) -> &'static str {
        Category::Parse.as_ref()
    }

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &[
            "`value` is not a valid proto payload.",
            "`desc_file` file does not exist.",
            "`message_type` message type does not exist in the descriptor file.",
        ]
    }

    fn return_kind(&self) -> u16 {
        kind::OBJECT
    }

    fn notices(&self) -> &'static [&'static str] {
        &["Only proto messages are parsed and returned."]
    }

    fn parameters(&self) -> &'static [Parameter] {
        const PARAMETERS: &[Parameter] = &[
            Parameter::required(
                "value",
                kind::BYTES,
                "The protocol buffer payload to parse.",
            ),
            Parameter::required(
                "desc_file",
                kind::BYTES,
                "The path to the protobuf descriptor set file. Must be a literal string.

This file is the output of protoc -o <path> ...",
            ),
            Parameter::required(
                "message_type",
                kind::BYTES,
                "The name of the message type to use for serializing.

Must be a literal string.",
            ),
        ];
        PARAMETERS
    }

    fn examples(&self) -> &'static [Example] {
        EXAMPLES.as_slice()
    }

    fn compile(
        &self,
        state: &state::TypeState,
        _ctx: &mut FunctionCompileContext,
        arguments: ArgumentList,
    ) -> Compiled {
        let value = arguments.required("value");
        let desc_file = arguments.required_literal("desc_file", state)?;
        let desc_file_str = desc_file
            .try_bytes_utf8_lossy()
            .expect("descriptor file must be a string");
        let message_type = arguments.required_literal("message_type", state)?;
        let message_type_str = message_type
            .try_bytes_utf8_lossy()
            .expect("message_type must be a string");
        let os_string: OsString = desc_file_str.into_owned().into();
        let path_buf = PathBuf::from(os_string);
        let path = Path::new(&path_buf);
        let descriptor =
            get_message_descriptor(path, &message_type_str).expect("message type not found");

        Ok(ParseProtoFn { descriptor, value }.as_expr())
    }
}

#[derive(Debug, Clone)]
struct ParseProtoFn {
    descriptor: MessageDescriptor,
    value: Box<dyn Expression>,
}

impl FunctionExpression for ParseProtoFn {
    fn resolve(&self, ctx: &mut Context) -> Resolved {
        let value = self.value.resolve(ctx)?;
        parse_proto(&self.descriptor, value)
    }

    fn type_def(&self, _: &state::TypeState) -> TypeDef {
        json_type_def()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value;
    use std::{env, fs};

    fn test_data_dir() -> PathBuf {
        PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("tests/data/protobuf")
    }

    fn read_pb_file(protobuf_bin_message_path: &str) -> String {
        fs::read_to_string(test_data_dir().join(protobuf_bin_message_path)).unwrap()
    }

    test_function![
        parse_proto => ParseProto;

        parses {
            args: func_args![ value: read_pb_file("test_protobuf/v1/input/person_someone.pb"),
                desc_file: test_data_dir().join("test_protobuf/v1/test_protobuf.desc").to_str().unwrap().to_owned(),
                message_type: "test_protobuf.v1.Person"],
            want: Ok(value!({ name: "Someone", phones: [{number: "123-456"}] })),
            tdef: json_type_def(),
        }

        parses_proto3 {
            args: func_args![ value: read_pb_file("test_protobuf3/v1/input/person_someone.pb"),
                desc_file: test_data_dir().join("test_protobuf3/v1/test_protobuf3.desc").to_str().unwrap().to_owned(),
                message_type: "test_protobuf3.v1.Person"],
            want: Ok(value!({ name: "Someone", phones: [{number: "123-456", type: "PHONE_TYPE_MOBILE"}] })),
            tdef: json_type_def(),
        }
    ];
}