Skip to content

Commit

Permalink
Render error details
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewhickman committed Jun 18, 2023
1 parent 6a31019 commit b7b9c41
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "grpc"]
path = grpc
url = https://github.com/grpc/grpc
[submodule "googleapis"]
path = googleapis
url = https://github.com/googleapis/googleapis
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ include = [
"build.rs",
"src/**/*.rs",
"!src/**/tests.rs",
"proto/*.proto",
"googleapis/google/rpc/status.proto",
"googleapis/google/rpc/error_details.proto",
"grpc/src/proto/grpc/health/v1/health.proto",
"grpc/src/proto/grpc/reflection/v1/reflection.proto",
]

[profile.release]
Expand Down Expand Up @@ -65,9 +70,10 @@ windows = { version = "0.48.0", features = ["Win32_System_LibraryLoader", "Win32

[build-dependencies]
anyhow = "1.0.71"
protox = "0.3.3"
vergen = { version = "8.2.1", features = ["git", "gitoxide"] }
windows = { version = "0.48.0", features = ["Win32_UI_WindowsAndMessaging"] }
winres = "0.1.12"

[patch.crates-io]
druid = { git = "https://github.com/andrewhickman/druid", branch = "master" }
druid = { git = "https://github.com/andrewhickman/druid", branch = "master" }
28 changes: 28 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
use std::{env, fs, path::PathBuf};

fn main() -> anyhow::Result<()> {
vergen::EmitBuilder::builder().git_sha(true).emit()?;

let mut compiler = protox::Compiler::new(["grpc/src/proto", "googleapis", "proto"]).unwrap();
compiler
.include_source_info(false)
.include_imports(true)
.open_files([
"grpc/health/v1/health.proto",
"grpc/reflection/v1/reflection.proto",
"google/rpc/status.proto",
"google/rpc/error_details.proto",
"lanquetta.proto",
])
.unwrap();

for file in compiler.files() {
if let Some(path) = file.path() {
println!("cargo:rerun-if-changed={}", path.display());
}
}

fs::write(
PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("proto.bin"),
compiler.encode_file_descriptor_set(),
)
.unwrap();

#[cfg(windows)]
winres::WindowsResource::new()
.set_icon_with_id(
"img/logo.ico",
&(windows::Win32::UI::WindowsAndMessaging::IDI_APPLICATION.0 as u32).to_string(),
)
.compile()?;
println!("cargo:rerun-if-changed=img/logo.ico");

Ok(())
}
1 change: 1 addition & 0 deletions googleapis
Submodule googleapis added at 3f11cd
8 changes: 8 additions & 0 deletions proto/lanquetta.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
syntax = "proto3";

package lanquetta;

message UnknownAny {
string type_url = 1 [json_name = "@type"];
bytes value = 2;
}
6 changes: 4 additions & 2 deletions src/app/body/method/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,12 @@ impl MethodTabController {

let json_result = JsonText::short(response.to_json());

data.stream.add_response(Ok(json_result), duration);
data.stream
.add_response(data.method.parent_pool(), Ok(json_result), duration);
}
grpc::ResponseResult::Error(error, metadata) => {
data.stream.add_response(Err(error), None);
data.stream
.add_response(data.method.parent_pool(), Err(error), None);
data.stream.add_metadata(metadata);
self.call = None;
}
Expand Down
114 changes: 101 additions & 13 deletions src/app/body/method/stream/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ use std::mem::{self, Discriminant};
use anyhow::Error;
use druid::{
lens::Field,
widget::{Label, LineBreaking, TextBox, ViewSwitcher},
widget::{CrossAxisAlignment, Flex, Label, LineBreaking, Maybe, TextBox, ViewSwitcher},
Application, Data, Env, Lens, Widget, WidgetExt as _,
};
use prost_reflect::{DescriptorPool, DynamicMessage, Value};
use serde::{Deserialize, Serialize};
use tonic::{metadata::MetadataMap, Code, Status};

use crate::{app::body::fmt_connect_err, theme};
use crate::{
app::body::fmt_connect_err,
grpc,
theme::{self, INVALID},
widget::Empty,
};
use crate::{
app::metadata,
json::{self, JsonText},
Expand All @@ -19,10 +25,17 @@ use crate::{
pub(in crate::app) enum State {
#[serde(deserialize_with = "json::serde::deserialize_short")]
Payload(JsonText),
Error(String),
Error(ErrorDetail),
Metadata(metadata::State),
}

#[derive(Debug, Clone, Lens, Data, Serialize, Deserialize)]
pub struct ErrorDetail {
message: String,
#[serde(deserialize_with = "json::serde::deserialize_short_opt")]
details: Option<JsonText>,
}

pub(in crate::app) fn build() -> impl Widget<State> {
ViewSwitcher::new(
|data: &State, _: &Env| mem::discriminant(data),
Expand All @@ -34,12 +47,31 @@ pub(in crate::app) fn build() -> impl Widget<State> {
)
.lens(State::lens_payload())
.boxed(),
State::Error(_) => theme::error_label_scope(
Label::dynamic(|data: &String, _: &Env| data.clone())
.with_line_break_mode(LineBreaking::WordWrap),
)
.lens(State::lens_error())
.boxed(),
State::Error(_) => Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Fill)
.with_child(
theme::error_label_scope(
Label::dynamic(|data: &String, _: &Env| data.clone())
.with_line_break_mode(LineBreaking::WordWrap),
)
.lens(ErrorDetail::message),
)
.with_child(
Maybe::new(
|| {
theme::text_box_scope(
TextBox::multiline()
.readonly()
.with_font(theme::EDITOR_FONT),
)
.env_scope(|env: &mut Env, _: &JsonText| env.set(INVALID, true))
},
|| Empty,
)
.lens(ErrorDetail::details),
)
.lens(State::lens_error())
.boxed(),
State::Metadata(_) => metadata::build().lens(State::lens_metadata()).boxed(),
},
)
Expand All @@ -59,7 +91,7 @@ impl State {
)
}

fn lens_error() -> impl Lens<State, String> {
fn lens_error() -> impl Lens<State, ErrorDetail> {
Field::new(
|data| match data {
State::Error(err) => err,
Expand Down Expand Up @@ -89,10 +121,18 @@ impl State {
State::Payload(json)
}

pub fn from_response(result: Result<JsonText, Error>) -> Self {
pub fn from_response(pool: &DescriptorPool, result: Result<JsonText, Error>) -> Self {
match result {
Ok(payload) => State::Payload(payload),
Err(err) => State::Error(fmt_grpc_err(&err)),
Err(err) => {
let message = fmt_grpc_err(&err);
let details = error_details(pool, &err).map(|payload| {
let response = grpc::Response::new(payload);
JsonText::short(response.to_json())
});

State::Error(ErrorDetail { message, details })
}
}
}

Expand All @@ -110,14 +150,62 @@ impl State {
pub fn set_clipboard(&self) {
let data = match self {
State::Payload(payload) => payload.original_data(),
State::Error(err) => err.as_str(),
State::Error(err) => {
if let Some(detail) = &err.details {
detail.original_data()
} else {
err.message.as_str()
}
}
State::Metadata(_) => return,
};

Application::global().clipboard().put_string(data);
}
}

fn error_details(pool: &DescriptorPool, err: &anyhow::Error) -> Option<DynamicMessage> {
let Some(status) = err.downcast_ref::<Status>() else {
return None
};

if status.details().is_empty() {
return None;
};

let Some(desc) = pool.get_message_by_name("google.rpc.Status") else {
return None
};

let Ok(mut payload) = DynamicMessage::decode(desc, status.details()) else {
return None
};

for detail in payload.get_field_by_name_mut("details")?.as_list_mut()? {
let Some(message) = detail.as_message_mut() else { return None };

let type_url = message.get_field_by_name("type_url")?.as_str()?.to_owned();
if pool
.get_message_by_name(type_url.strip_prefix("type.googleapis.com/")?)
.is_none()
{
let value = message.get_field_by_name("value")?.as_bytes()?.clone();

let mut unknown_message =
DynamicMessage::new(pool.get_message_by_name("lanquetta.UnknownAny")?);
unknown_message
.try_set_field_by_name("type_url", Value::String(type_url))
.ok()?;
unknown_message
.try_set_field_by_name("value", Value::Bytes(value))
.ok()?;
*message = unknown_message;
}
}

Some(payload)
}

fn fmt_grpc_err(err: &anyhow::Error) -> String {
if let Some(status) = err.downcast_ref::<Status>() {
if status.message().is_empty() {
Expand Down
10 changes: 8 additions & 2 deletions src/app/body/method/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use druid::{
},
ArcStr, Data, Lens, WidgetExt,
};
use prost_reflect::DescriptorPool;
use serde::{Deserialize, Serialize};
use tonic::metadata::MetadataMap;

Expand Down Expand Up @@ -115,7 +116,12 @@ impl State {
});
}

pub fn add_response(&mut self, result: Result<JsonText, Error>, duration: Option<Duration>) {
pub fn add_response(
&mut self,
pool: &DescriptorPool,
result: Result<JsonText, Error>,
duration: Option<Duration>,
) {
for item in self.items.iter_mut() {
item.expanded = false;
}
Expand All @@ -125,7 +131,7 @@ impl State {
self.items.push_back(ItemExpanderState {
label: name,
expanded: true,
data: item::State::from_response(result),
data: item::State::from_response(pool, result),
kind: ItemKind::Response,
duration: duration.map(format_duration).unwrap_or_default().into(),
});
Expand Down
4 changes: 2 additions & 2 deletions src/app/sidebar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ impl State {

impl ServiceListState {
pub fn add_from_path(&mut self, path: &Path) -> Result<()> {
let file_set = protoc::load_file(path)?;
let file = protoc::load_file(path)?;

self.services
.extend(file_set.services().map(service::ServiceState::from));
.extend(file.services().map(service::ServiceState::from));
Ok(())
}

Expand Down
8 changes: 8 additions & 0 deletions src/json/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ where
let s = String::deserialize(deserializer)?;
Ok(JsonText::short(s))
}

pub fn deserialize_short_opt<'de, D>(deserializer: D) -> Result<Option<JsonText>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
Ok(s.map(JsonText::short))
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use anyhow::{Context, Result};
use tokio::runtime::Runtime;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

const PROTOS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/proto.bin"));

pub fn main() -> Result<()> {
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;

Expand Down
30 changes: 24 additions & 6 deletions src/protoc.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use std::{ffi::OsStr, path::Path};

use anyhow::{bail, Result};
use prost_reflect::DescriptorPool;
use prost_reflect::{DescriptorPool, FileDescriptor};

pub fn load_file(path: &Path) -> Result<DescriptorPool> {
match compile(path) {
use crate::PROTOS;

pub fn load_file(path: &Path) -> Result<FileDescriptor> {
let mut pool = load_pool(path)?;

let primary_file = pool.files().last().unwrap().name().to_owned();

if let Err(err) = pool.decode_file_descriptor_set(PROTOS) {
tracing::warn!(
"failed to add additional protos to pool from {}: {:#}",
path.display(),
err
);
}

Ok(pool.get_file_by_name(&primary_file).unwrap())
}

fn load_pool(path: &Path) -> Result<DescriptorPool> {
match compile_proto(path) {
Ok(pool) => Ok(pool),
Err(err) if err.is_parse() => match load(path) {
Err(err) if err.is_parse() => match compile_file_set(path) {
Ok(pool) => Ok(pool),
Err(_) => {
if path.extension() == Some(OsStr::new("proto")) {
Expand All @@ -20,7 +38,7 @@ pub fn load_file(path: &Path) -> Result<DescriptorPool> {
}
}

fn compile(path: &Path) -> Result<DescriptorPool, protox::Error> {
fn compile_proto(path: &Path) -> Result<DescriptorPool, protox::Error> {
match path.parent() {
Some(include) => Ok(protox::Compiler::new([include])?
.include_imports(true)
Expand All @@ -31,7 +49,7 @@ fn compile(path: &Path) -> Result<DescriptorPool, protox::Error> {
}
}

fn load(path: &Path) -> Result<DescriptorPool> {
fn compile_file_set(path: &Path) -> Result<DescriptorPool> {
let bytes = fs_err::read(path)?;
DescriptorPool::decode(bytes.as_slice()).map_err(|err| anyhow::anyhow!("{:?}", err))
}

0 comments on commit b7b9c41

Please sign in to comment.