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;
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(),
}
];
}