Skip to content

Commit f90f3a7

Browse files
committed
Color gRPC JSON descriptor output
1 parent 0d9c2ea commit f90f3a7

3 files changed

Lines changed: 136 additions & 11 deletions

File tree

src/http/response.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,14 @@ pub(super) async fn finish_response(
143143
));
144144
}
145145
if should_stream_formatted_grpc_stdout(cli, &response_headers, stdout_is_terminal) {
146+
let use_color = stdio.stdout_color(cli.color.as_deref());
146147
let streamed = stream_response_to_formatted_grpc_stdout(
147148
response,
148149
response_headers.clone(),
149150
compression,
150151
cli.copy,
151152
grpc_method.map(|method| method.output()),
153+
use_color,
152154
)
153155
.await?;
154156
return Ok(finalize_streamed_response(

src/http/response/formatters.rs

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ pub(super) async fn stream_response_to_formatted_grpc_stdout(
8787
compression: CompressionMode,
8888
copy: bool,
8989
grpc_response_desc: Option<prost_reflect::MessageDescriptor>,
90+
use_color: bool,
9091
) -> Result<StreamedOutput, FetchError> {
91-
let formatter = FormattedGrpcStream::new(&response_headers, grpc_response_desc);
92+
let formatter = FormattedGrpcStream::new(&response_headers, grpc_response_desc, use_color);
9293
super::stream::stream_formatted_response_to_stdout(
9394
response,
9495
response_headers,
@@ -235,6 +236,7 @@ struct FormattedGrpcStream {
235236
decoder: crate::grpc::framing::FrameDecoder,
236237
grpc_message_encoding: grpc_encoding::MessageEncoding,
237238
grpc_response_desc: Option<prost_reflect::MessageDescriptor>,
239+
use_color: bool,
238240
frame_index: usize,
239241
descriptor_wrote_any: bool,
240242
descriptor_output_ends_with_newline: bool,
@@ -244,11 +246,13 @@ impl FormattedGrpcStream {
244246
fn new(
245247
response_headers: &HeaderMap,
246248
grpc_response_desc: Option<prost_reflect::MessageDescriptor>,
249+
use_color: bool,
247250
) -> Self {
248251
Self {
249252
decoder: crate::grpc::framing::FrameDecoder::new(),
250253
grpc_message_encoding: grpc_encoding::MessageEncoding::from_headers(response_headers),
251254
grpc_response_desc,
255+
use_color,
252256
frame_index: 0,
253257
descriptor_wrote_any: false,
254258
descriptor_output_ends_with_newline: true,
@@ -257,15 +261,18 @@ impl FormattedGrpcStream {
257261

258262
fn format_frame(&mut self, frame: &crate::grpc::framing::Frame) -> Result<Vec<u8>, FetchError> {
259263
if let Some(desc) = self.grpc_response_desc.as_ref() {
260-
let formatted =
261-
proto::format_grpc_frame_with_descriptor(frame, desc, &self.grpc_message_encoding)
262-
.map_err(|err| FetchError::Message(err.to_string()))?;
264+
let formatted = format_grpc_frame_with_descriptor_json(
265+
frame,
266+
desc,
267+
&self.grpc_message_encoding,
268+
self.use_color,
269+
)?;
263270
let mut output = Vec::new();
264271
if self.descriptor_wrote_any && !self.descriptor_output_ends_with_newline {
265272
output.push(b'\n');
266273
}
267-
output.extend_from_slice(formatted.as_bytes());
268-
self.descriptor_output_ends_with_newline = formatted.ends_with('\n');
274+
self.descriptor_output_ends_with_newline = formatted.ends_with(b"\n");
275+
output.extend_from_slice(&formatted);
269276
self.descriptor_wrote_any = true;
270277
return Ok(output);
271278
}
@@ -282,6 +289,44 @@ impl FormattedGrpcStream {
282289
}
283290
}
284291

292+
fn format_grpc_frame_with_descriptor_json(
293+
frame: &crate::grpc::framing::Frame,
294+
desc: &prost_reflect::MessageDescriptor,
295+
message_encoding: &grpc_encoding::MessageEncoding,
296+
use_color: bool,
297+
) -> Result<Vec<u8>, FetchError> {
298+
let formatted = proto::format_grpc_frame_with_descriptor(frame, desc, message_encoding)
299+
.map_err(|err| FetchError::Message(err.to_string()))?;
300+
Ok(format_printer_bytes(use_color, |out| {
301+
json::format_json_to(formatted.as_bytes(), out)
302+
})
303+
.unwrap_or_else(|_| formatted.into_bytes()))
304+
}
305+
306+
fn format_grpc_stream_with_descriptor_json(
307+
bytes: &[u8],
308+
desc: &prost_reflect::MessageDescriptor,
309+
message_encoding: &grpc_encoding::MessageEncoding,
310+
use_color: bool,
311+
) -> Result<Vec<u8>, FetchError> {
312+
let frames = crate::grpc::framing::read_frames(bytes)
313+
.map_err(|err| FetchError::Message(format!("failed to read gRPC stream: {err}")))?;
314+
let mut out = Vec::new();
315+
let mut wrote_any = false;
316+
let mut output_ends_with_newline = true;
317+
for frame in &frames {
318+
let formatted =
319+
format_grpc_frame_with_descriptor_json(frame, desc, message_encoding, use_color)?;
320+
if wrote_any && !output_ends_with_newline {
321+
out.push(b'\n');
322+
}
323+
output_ends_with_newline = formatted.ends_with(b"\n");
324+
out.extend_from_slice(&formatted);
325+
wrote_any = true;
326+
}
327+
Ok(out)
328+
}
329+
285330
impl StdoutStreamFormatter for FormattedGrpcStream {
286331
fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<Vec<u8>>, FetchError> {
287332
let frames = self
@@ -481,9 +526,12 @@ pub(super) fn format_stdout_bytes_with_terminal(
481526
ContentType::Grpc => {
482527
let grpc_message_encoding = grpc_encoding::MessageEncoding::from_headers(headers);
483528
if let Some(desc) = grpc_response_desc {
484-
proto::format_grpc_stream_with_descriptor(&bytes, &desc, &grpc_message_encoding)
485-
.map(|formatted| formatted.into_bytes())
486-
.map_err(|err| FetchError::Message(err.to_string()))
529+
format_grpc_stream_with_descriptor_json(
530+
&bytes,
531+
&desc,
532+
&grpc_message_encoding,
533+
use_color,
534+
)
487535
} else {
488536
grpc_format::format_grpc_stream(&bytes, &grpc_message_encoding)
489537
.map(|formatted| formatted.into_bytes())
@@ -618,6 +666,20 @@ mod tests {
618666
.output()
619667
}
620668

669+
fn test_response_body(text: &str, count: i64) -> Vec<u8> {
670+
let desc = test_response_descriptor();
671+
let mut msg = DynamicMessage::new(desc.clone());
672+
msg.set_field(
673+
&desc.get_field_by_name("response_text").unwrap(),
674+
ReflectValue::String(text.to_string()),
675+
);
676+
msg.set_field(
677+
&desc.get_field_by_name("count").unwrap(),
678+
ReflectValue::I64(count),
679+
);
680+
msg.encode_to_vec()
681+
}
682+
621683
#[test]
622684
fn image_off_returns_raw_image_bytes() {
623685
let mut headers = HeaderMap::new();
@@ -989,6 +1051,60 @@ mod tests {
9891051
assert!(!text.contains("1:"));
9901052
}
9911053

1054+
#[test]
1055+
fn grpc_descriptor_response_uses_json_color_policy() {
1056+
let mut headers = HeaderMap::new();
1057+
headers.insert(
1058+
CONTENT_TYPE,
1059+
HeaderValue::from_static("application/grpc+proto"),
1060+
);
1061+
let cli = Cli::try_parse_from([
1062+
"fetch",
1063+
"--grpc",
1064+
"--format",
1065+
"on",
1066+
"--color",
1067+
"on",
1068+
"https://example.com/testpkg.TestService/Get",
1069+
])
1070+
.unwrap();
1071+
let body = crate::grpc::framing::frame(&test_response_body("hello", 7), false).unwrap();
1072+
1073+
let out = format_stdout_bytes_with_terminal(
1074+
&cli,
1075+
&headers,
1076+
&body,
1077+
Some(test_response_descriptor()),
1078+
false,
1079+
0,
1080+
)
1081+
.unwrap();
1082+
let out = String::from_utf8(out.bytes).unwrap();
1083+
1084+
assert!(out.contains("\x1b[34m\x1b[1mresponse_text\x1b[0m"));
1085+
assert!(out.contains("\x1b[32mhello\x1b[0m"));
1086+
assert!(!out.contains("1:"));
1087+
}
1088+
1089+
#[test]
1090+
fn streaming_grpc_descriptor_response_uses_json_color_policy() {
1091+
let mut headers = HeaderMap::new();
1092+
headers.insert(
1093+
CONTENT_TYPE,
1094+
HeaderValue::from_static("application/grpc+proto"),
1095+
);
1096+
let mut formatter =
1097+
FormattedGrpcStream::new(&headers, Some(test_response_descriptor()), true);
1098+
let body = crate::grpc::framing::frame(&test_response_body("hello", 7), false).unwrap();
1099+
1100+
let chunks = formatter.push_chunk(&body).unwrap();
1101+
let out = String::from_utf8(chunks.into_iter().flatten().collect()).unwrap();
1102+
1103+
assert!(out.contains("\x1b[34m\x1b[1mresponse_text\x1b[0m"));
1104+
assert!(out.contains("\x1b[32mhello\x1b[0m"));
1105+
assert!(!out.contains("1:"));
1106+
}
1107+
9921108
#[test]
9931109
fn protobuf_descriptor_decode_failure_falls_back_to_raw_bytes_like_go() {
9941110
let mut headers = HeaderMap::new();

tests/grpc.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,10 @@ fn formatted_grpc_outputs_frames_before_stream_ends() {
389389
drop(pty.slave);
390390
let capture = start_pty_capture(&pty.master);
391391

392-
capture.wait_for(r#""count": "1""#, Duration::from_secs(5));
392+
capture.wait_for(
393+
"\"\x1b[34m\x1b[1mcount\x1b[0m\": \"\x1b[32m1\x1b[0m\"",
394+
Duration::from_secs(5),
395+
);
393396
assert!(
394397
wait_child(&mut child, Duration::from_millis(100)).is_none(),
395398
"fetch exited before the gRPC stream closed; PTY output:\n{}",
@@ -411,7 +414,11 @@ fn formatted_grpc_outputs_frames_before_stream_ends() {
411414
"fetch exited with {status}; PTY output:\n{}",
412415
capture.output()
413416
);
414-
assert!(capture.output().contains(r#""count": "2""#));
417+
assert!(
418+
capture
419+
.output()
420+
.contains("\"\x1b[34m\x1b[1mcount\x1b[0m\": \"\x1b[32m2\x1b[0m\"")
421+
);
415422
drop(pty.master);
416423
capture.close();
417424
}

0 commit comments

Comments
 (0)