You've already forked decomp.dev
mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-03-30 11:06:20 -07:00
Add badge support
This commit is contained in:
Generated
+44
-2
@@ -47,6 +47,15 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -352,6 +361,29 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "badge-maker"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d1c1803a02b28fbbf44a1498fd1043655b0da3bcd315437fcb7776302b2917c"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.20",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"itoa",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"seahash",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -389,6 +421,15 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@@ -918,6 +959,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"badge-maker",
|
||||
"blake3",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -3365,7 +3407,7 @@ version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-automata 0.4.7",
|
||||
"regex-syntax 0.8.4",
|
||||
@@ -3386,7 +3428,7 @@ version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.4",
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.7"
|
||||
badge-maker = "0.3"
|
||||
blake3 = "1.5"
|
||||
bytes = "1.7"
|
||||
chrono = "0.4"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use image::{buffer::ConvertBuffer, ImageFormat, RgbImage, RgbaImage};
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory};
|
||||
use resvg::{
|
||||
tiny_skia::{PixmapMut, Transform},
|
||||
usvg::{Options, Tree},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::ReportFile;
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct ShieldParams {
|
||||
label: Option<String>,
|
||||
label_color: Option<String>,
|
||||
color: Option<String>,
|
||||
style: Option<String>,
|
||||
measure: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ShieldResponse {
|
||||
schema_version: u32,
|
||||
label: String,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
style: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
label_color: Option<String>,
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
params: &ShieldParams,
|
||||
) -> Result<ShieldResponse> {
|
||||
let label = if let Some(label) = params.label.clone() {
|
||||
label
|
||||
} else if let Some(category) = current_category {
|
||||
category.name.clone()
|
||||
} else {
|
||||
report.project.short_name().to_string()
|
||||
};
|
||||
let message = if let Some(measure) = ¶ms.measure {
|
||||
match measure.as_str() {
|
||||
"code" => format!("{:.2}%", measures.matched_code_percent),
|
||||
"data" => format!("{:.2}%", measures.matched_data_percent),
|
||||
"functions" => format!("{:.2}%", measures.matched_functions_percent),
|
||||
"complete_code" => format!("{:.2}%", measures.complete_code_percent),
|
||||
"complete_data" => format!("{:.2}%", measures.complete_data_percent),
|
||||
_ => return Err(anyhow!("Unknown measure")),
|
||||
}
|
||||
} else {
|
||||
format!("{:.2}%", measures.matched_code_percent)
|
||||
};
|
||||
Ok(ShieldResponse {
|
||||
schema_version: 1,
|
||||
label,
|
||||
message,
|
||||
color: Some(params.color.clone().unwrap_or_else(|| "informational".to_string())),
|
||||
style: params.style.clone(),
|
||||
label_color: params.label_color.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_svg(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
params: &ShieldParams,
|
||||
) -> Result<String> {
|
||||
let response = render(report, measures, current_category, params)?;
|
||||
let mut builder = badge_maker::BadgeBuilder::new();
|
||||
builder.label(&response.label).message(&response.message);
|
||||
if let Some(color) = &response.color {
|
||||
builder.color_parse(color);
|
||||
}
|
||||
if let Some(style) = &response.style {
|
||||
builder.style_parse(style);
|
||||
}
|
||||
if let Some(label_color) = &response.label_color {
|
||||
builder.label_color_parse(label_color);
|
||||
}
|
||||
let badge = builder.build()?;
|
||||
Ok(badge.svg())
|
||||
}
|
||||
|
||||
pub fn render_image(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
params: &ShieldParams,
|
||||
format: ImageFormat,
|
||||
) -> Result<Vec<u8>> {
|
||||
let svg = render_svg(report, measures, current_category, params)?;
|
||||
let opt = Options::default();
|
||||
let tree = Tree::from_str(&svg, &opt).context("Failed to parse SVG")?;
|
||||
let rect = tree.root().abs_layer_bounding_box();
|
||||
let w = rect.width() as u32;
|
||||
let h = rect.height() as u32;
|
||||
let mut image = RgbaImage::new(w, h);
|
||||
let mut pixmap = PixmapMut::from_bytes(image.as_mut(), w, h)
|
||||
.ok_or_else(|| anyhow!("Failed to create pixmap"))?;
|
||||
resvg::render(&tree, Transform::identity(), &mut pixmap);
|
||||
let mut bytes = Vec::new();
|
||||
let image: RgbImage = image.convert();
|
||||
image.write_to(&mut Cursor::new(&mut bytes), format)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use prost::Message;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
mod badge;
|
||||
mod css;
|
||||
mod graph;
|
||||
mod js;
|
||||
|
||||
+84
-10
@@ -13,9 +13,8 @@ use objdiff_core::bindings::report::{Measures, ReportCategory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use super::{graph::layout_units, parse_accept, AppError, FullUri, Protobuf, PROTOBUF};
|
||||
use super::{badge, graph, parse_accept, AppError, FullUri, Protobuf, PROTOBUF};
|
||||
use crate::{
|
||||
handlers::graph::{render_image, render_svg, unit_color},
|
||||
models::{Project, ProjectInfo, ReportFile},
|
||||
templates::render,
|
||||
util::UrlExt,
|
||||
@@ -35,9 +34,12 @@ const DEFAULT_IMAGE_HEIGHT: u32 = 475;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReportQuery {
|
||||
mode: Option<String>,
|
||||
category: Option<String>,
|
||||
w: Option<u32>,
|
||||
h: Option<u32>,
|
||||
#[serde(flatten)]
|
||||
shield: badge::ShieldParams,
|
||||
}
|
||||
|
||||
impl ReportQuery {
|
||||
@@ -203,17 +205,36 @@ pub async fn get_report(
|
||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||
};
|
||||
|
||||
let (w, h) = query.size();
|
||||
let (measures, current_category, units) = apply_category(&report, &query)?;
|
||||
if let Some(mode) = query.mode.as_deref() {
|
||||
match mode.to_ascii_lowercase().as_str() {
|
||||
"shield" => mode_shield(report, query, headers, ext),
|
||||
"report" => mode_report(report, project_info, &state, uri, query, headers, start, ext),
|
||||
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||
}
|
||||
} else {
|
||||
mode_report(report, project_info, &state, uri, query, headers, start, ext)
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_report(
|
||||
report: ReportFile,
|
||||
project_info: ProjectInfo,
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
query: ReportQuery,
|
||||
headers: HeaderMap,
|
||||
start: Instant,
|
||||
ext: Option<String>,
|
||||
) -> Result<Response, AppError> {
|
||||
let (measures, current_category, units) = apply_category(&report, &query)?;
|
||||
let acceptable = if let Some(ext) = ext {
|
||||
vec![match ext.to_ascii_lowercase().as_str() {
|
||||
"json" => mime::APPLICATION_JSON,
|
||||
"binpb" | "proto" => Mime::from_str("application/x-protobuf").unwrap(),
|
||||
"binpb" | "proto" => Mime::from_str("application/x-protobuf")?,
|
||||
"svg" => mime::IMAGE_SVG,
|
||||
_ => {
|
||||
if let Some(format) = ImageFormat::from_extension(ext) {
|
||||
Mime::from_str(format.to_mime_type()).unwrap()
|
||||
Mime::from_str(format.to_mime_type())?
|
||||
} else {
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
@@ -246,7 +267,8 @@ pub async fn get_report(
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == PROTOBUF {
|
||||
return Ok(Protobuf(report.report).into_response());
|
||||
} else if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
||||
let svg = render_svg(&units, w, h, &state)?;
|
||||
let (w, h) = query.size();
|
||||
let svg = graph::render_svg(&units, w, h, &state)?;
|
||||
return Ok(([(header::CONTENT_TYPE, mime::IMAGE_SVG.as_ref())], svg).into_response());
|
||||
} else if mime.type_() == mime::IMAGE {
|
||||
let format = if mime.subtype() == mime::STAR {
|
||||
@@ -256,7 +278,59 @@ pub async fn get_report(
|
||||
ImageFormat::from_mime_type(mime.essence_str())
|
||||
.ok_or_else(|| AppError::Status(StatusCode::NOT_ACCEPTABLE))?
|
||||
};
|
||||
let data = render_image(&units, w, h, &state, format)?;
|
||||
let (w, h) = query.size();
|
||||
let data = graph::render_image(&units, w, h, &state, format)?;
|
||||
return Ok(([(header::CONTENT_TYPE, format.to_mime_type())], data).into_response());
|
||||
}
|
||||
}
|
||||
Err(AppError::Status(StatusCode::NOT_ACCEPTABLE))
|
||||
}
|
||||
|
||||
fn mode_shield(
|
||||
report: ReportFile,
|
||||
query: ReportQuery,
|
||||
headers: HeaderMap,
|
||||
ext: Option<String>,
|
||||
) -> Result<Response, AppError> {
|
||||
let (measures, current_category, _units) = apply_category(&report, &query)?;
|
||||
let acceptable = if let Some(ext) = ext {
|
||||
vec![match ext.to_ascii_lowercase().as_str() {
|
||||
"json" => mime::APPLICATION_JSON,
|
||||
"svg" => mime::IMAGE_SVG,
|
||||
_ => {
|
||||
if let Some(format) = ImageFormat::from_extension(ext) {
|
||||
Mime::from_str(format.to_mime_type())?
|
||||
} else {
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
}
|
||||
}]
|
||||
} else {
|
||||
if !headers.contains_key(header::ACCEPT) {
|
||||
return Ok(Json(report.report).into_response());
|
||||
}
|
||||
parse_accept(&headers)
|
||||
};
|
||||
for mime in acceptable {
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
let data = badge::render_svg(&report, measures, current_category, &query.shield)?;
|
||||
return Ok(([(header::CONTENT_TYPE, mime::IMAGE_SVG.as_ref())], data).into_response());
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
let data = badge::render(&report, measures, current_category, &query.shield)?;
|
||||
return Ok(Json(data).into_response());
|
||||
} else if mime.type_() == mime::IMAGE {
|
||||
let format = if mime.subtype() == mime::STAR {
|
||||
// Default to PNG
|
||||
ImageFormat::Png
|
||||
} else {
|
||||
ImageFormat::from_mime_type(mime.essence_str())
|
||||
.ok_or_else(|| AppError::Status(StatusCode::NOT_ACCEPTABLE))?
|
||||
};
|
||||
let data =
|
||||
badge::render_image(&report, measures, current_category, &query.shield, format)?;
|
||||
return Ok(([(header::CONTENT_TYPE, format.to_mime_type())], data).into_response());
|
||||
}
|
||||
}
|
||||
@@ -296,7 +370,7 @@ fn apply_category<'a>(
|
||||
}
|
||||
let (w, h) = query.size();
|
||||
let aspect = w as f32 / h as f32;
|
||||
let units = layout_units(&report.report, w, h, |item| {
|
||||
let units = graph::layout_units(&report.report, w, h, |item| {
|
||||
if let Some(category_id) = &category_id_filter {
|
||||
item.metadata
|
||||
.as_ref()
|
||||
@@ -321,7 +395,7 @@ fn apply_category<'a>(
|
||||
ReportTemplateUnit {
|
||||
name: &unit.name,
|
||||
fuzzy_match_percent: match_percent,
|
||||
color: unit_color(match_percent),
|
||||
color: graph::unit_color(match_percent),
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
w: bounds.w,
|
||||
|
||||
Reference in New Issue
Block a user