wireman_core/features/
grpcurl.rs

1use std::collections::HashMap;
2use std::fmt::Write;
3
4use http::Uri;
5use prost_reflect::MethodDescriptor;
6
7/// Generate a `grpcurl` command as a string for sending a `gRPC` request.
8///
9/// This function constructs a `grpcurl` command that can be used to send a `gRPC` request
10/// to a specified `gRPC` server. The generated command includes information such as include
11/// directories, URI, request message in JSON format, method descriptor, and metadata headers.
12///
13/// # Parameters
14///
15/// - `includes`: A list of include directories used to locate .proto files.
16/// - `uri`: The address URI of the `gRPC` server (e.g., "localhost:50051").
17/// - `message`: The request data in JSON format.
18/// - `method_desc`: The method descriptor for the `gRPC` method.
19/// - `metadata`: Key-value metadata headers to be included in the request.
20#[allow(clippy::implicit_hasher)]
21pub fn grpcurl<T: Into<Uri>>(
22    includes: &[String],
23    uri: T,
24    message: &str,
25    method_desc: &MethodDescriptor,
26    metadata: &HashMap<String, String>,
27) -> String {
28    // The includes
29    let imports = includes.iter().fold(String::new(), |mut result, include| {
30        let _ = write!(result, "-import-path {include} ");
31        result
32    });
33
34    // The name of the proto file
35    let file_desc = method_desc.parent_file();
36    let proto = file_desc.file_descriptor_proto().name();
37
38    // The host
39    let uri = uri.into();
40    let host = uri.host().unwrap_or("");
41    let port = uri.port_u16().unwrap_or(80);
42
43    // The method name
44    let method = method_desc.full_name();
45
46    // The metadata if available
47    let metadata = metadata
48        .iter()
49        .fold(String::new(), |mut result, (key, val)| {
50            let _ = write!(result, " -H \"{key}: {val}\"");
51            result
52        });
53
54    format!(
55        "grpcurl -d @ {imports}-proto {proto}{metadata} -plaintext {host}:{port} {method} <<EOM\n{message}\nEOM"
56    )
57}
58
59#[cfg(test)]
60mod test {
61    use crate::descriptor::RequestMessage;
62    use crate::ProtoDescriptor;
63
64    use super::*;
65
66    #[test]
67    fn test_request_as_grpcurl() {
68        // given
69        let includes = vec!["/Users/myworkspace".to_string()];
70        let given_uri = Uri::from_static("http://localhost:50051");
71        let test_message = load_test_message("Simple");
72        let given_method = test_message.method_descriptor();
73        let given_message = "{\n  \"number\": 0\n}";
74        let expected = "grpcurl -d @ -import-path /Users/myworkspace -proto test_files/test.proto -plaintext localhost:50051 proto.TestService.Simple <<EOM\n{\n  \"number\": 0\n}\nEOM";
75
76        // when
77        let cmd = grpcurl(
78            &includes,
79            given_uri,
80            given_message,
81            &given_method,
82            &HashMap::new(),
83        );
84
85        // then
86        assert_eq!(cmd, expected);
87    }
88
89    fn load_test_message(method: &str) -> RequestMessage {
90        let files = vec!["test_files/test.proto"];
91        let includes = vec!["."];
92
93        let desc = ProtoDescriptor::new(includes, files).unwrap();
94
95        let method = desc
96            .get_method_by_name("proto.TestService", method)
97            .unwrap();
98        let request = method.input();
99        RequestMessage::new(request, method)
100    }
101}