vrl/stdlib/
parse_proto.rs

1use super::util::example_path_or_basename;
2use crate::compiler::prelude::*;
3use crate::protobuf::descriptor::get_message_descriptor;
4use crate::protobuf::parse::parse_proto;
5use crate::stdlib::json_utils::json_type_def::json_type_def;
6use prost_reflect::MessageDescriptor;
7use std::ffi::OsString;
8use std::path::Path;
9use std::path::PathBuf;
10use std::sync::LazyLock;
11
12#[derive(Clone, Copy, Debug)]
13pub struct ParseProto;
14
15// This needs to be static because parse_proto needs to read a file
16// and the file path needs to be a literal.
17static EXAMPLE_PARSE_PROTO_EXPR: LazyLock<&str> = LazyLock::new(|| {
18    let path = example_path_or_basename("protobuf/test_protobuf/v1/test_protobuf.desc");
19
20    Box::leak(
21        format!(
22            r#"parse_proto!(decode_base64!("Cgdzb21lb25lIggKBjEyMzQ1Ng=="), "{path}", "test_protobuf.v1.Person")"#
23        )
24        .into_boxed_str(),
25    )
26});
27
28static EXAMPLES: LazyLock<Vec<Example>> = LazyLock::new(|| {
29    vec![example! {
30        title: "Parse proto",
31        source: &EXAMPLE_PARSE_PROTO_EXPR,
32        result: Ok(r#"{ "name": "someone", "phones": [{"number": "123456"}] }"#),
33    }]
34});
35
36impl Function for ParseProto {
37    fn identifier(&self) -> &'static str {
38        "parse_proto"
39    }
40
41    fn summary(&self) -> &'static str {
42        "parse a string to a protobuf based type"
43    }
44
45    fn usage(&self) -> &'static str {
46        "Parses the `value` as a protocol buffer payload."
47    }
48
49    fn category(&self) -> &'static str {
50        Category::Parse.as_ref()
51    }
52
53    fn internal_failure_reasons(&self) -> &'static [&'static str] {
54        &[
55            "`value` is not a valid proto payload.",
56            "`desc_file` file does not exist.",
57            "`message_type` message type does not exist in the descriptor file.",
58        ]
59    }
60
61    fn return_kind(&self) -> u16 {
62        kind::OBJECT
63    }
64
65    fn notices(&self) -> &'static [&'static str] {
66        &["Only proto messages are parsed and returned."]
67    }
68
69    fn parameters(&self) -> &'static [Parameter] {
70        const PARAMETERS: &[Parameter] = &[
71            Parameter::required(
72                "value",
73                kind::BYTES,
74                "The protocol buffer payload to parse.",
75            ),
76            Parameter::required(
77                "desc_file",
78                kind::BYTES,
79                "The path to the protobuf descriptor set file. Must be a literal string.
80
81This file is the output of protoc -o <path> ...",
82            ),
83            Parameter::required(
84                "message_type",
85                kind::BYTES,
86                "The name of the message type to use for serializing.
87
88Must be a literal string.",
89            ),
90        ];
91        PARAMETERS
92    }
93
94    fn examples(&self) -> &'static [Example] {
95        EXAMPLES.as_slice()
96    }
97
98    fn compile(
99        &self,
100        state: &state::TypeState,
101        _ctx: &mut FunctionCompileContext,
102        arguments: ArgumentList,
103    ) -> Compiled {
104        let value = arguments.required("value");
105        let desc_file = arguments.required_literal("desc_file", state)?;
106        let desc_file_str = desc_file
107            .try_bytes_utf8_lossy()
108            .expect("descriptor file must be a string");
109        let message_type = arguments.required_literal("message_type", state)?;
110        let message_type_str = message_type
111            .try_bytes_utf8_lossy()
112            .expect("message_type must be a string");
113        let os_string: OsString = desc_file_str.into_owned().into();
114        let path_buf = PathBuf::from(os_string);
115        let path = Path::new(&path_buf);
116        let descriptor =
117            get_message_descriptor(path, &message_type_str).expect("message type not found");
118
119        Ok(ParseProtoFn { descriptor, value }.as_expr())
120    }
121}
122
123#[derive(Debug, Clone)]
124struct ParseProtoFn {
125    descriptor: MessageDescriptor,
126    value: Box<dyn Expression>,
127}
128
129impl FunctionExpression for ParseProtoFn {
130    fn resolve(&self, ctx: &mut Context) -> Resolved {
131        let value = self.value.resolve(ctx)?;
132        parse_proto(&self.descriptor, value)
133    }
134
135    fn type_def(&self, _: &state::TypeState) -> TypeDef {
136        json_type_def()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::value;
144    use std::{env, fs};
145
146    fn test_data_dir() -> PathBuf {
147        PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("tests/data/protobuf")
148    }
149
150    fn read_pb_file(protobuf_bin_message_path: &str) -> String {
151        fs::read_to_string(test_data_dir().join(protobuf_bin_message_path)).unwrap()
152    }
153
154    test_function![
155        parse_proto => ParseProto;
156
157        parses {
158            args: func_args![ value: read_pb_file("test_protobuf/v1/input/person_someone.pb"),
159                desc_file: test_data_dir().join("test_protobuf/v1/test_protobuf.desc").to_str().unwrap().to_owned(),
160                message_type: "test_protobuf.v1.Person"],
161            want: Ok(value!({ name: "Someone", phones: [{number: "123-456"}] })),
162            tdef: json_type_def(),
163        }
164
165        parses_proto3 {
166            args: func_args![ value: read_pb_file("test_protobuf3/v1/input/person_someone.pb"),
167                desc_file: test_data_dir().join("test_protobuf3/v1/test_protobuf3.desc").to_str().unwrap().to_owned(),
168                message_type: "test_protobuf3.v1.Person"],
169            want: Ok(value!({ name: "Someone", phones: [{number: "123-456", type: "PHONE_TYPE_MOBILE"}] })),
170            tdef: json_type_def(),
171        }
172    ];
173}