plugins/native/znippy-plugin-maven/src/native.rs
use std::collections::HashMap;
use znippy_common::arrow::datatypes::{DataType, Field};
use znippy_common::plugin::{ArchiveTypePlugin, ExtensionRow, ExtensionValue, Claim, HandlerSpec};
use crate::pom::parse_pom_project;
pub struct NativeMavenPlugin;
/// The maven handler, stated once. Its columns come from `row::MAVEN_SCHEMA`
/// (the list the wasm `plugin_schema` export serializes too), so `fields` stays
/// empty here and `schema_fields` reads that one list.
pub static MAVEN_SPEC: HandlerSpec = HandlerSpec {
name: "maven",
aliases: &["java"],
type_id: 3,
ecosystem: "Java / Maven Central",
extensions: crate::row::MAVEN_EXTENSIONS,
claim: Claim::Extensions,
description: "Java Maven artifacts — GAV coordinates from embedded pom.xml or a .pom",
fields: &[],
commands: &[],
};
impl ArchiveTypePlugin for NativeMavenPlugin {
fn spec(&self) -> &HandlerSpec {
&MAVEN_SPEC
}
/// Columns this handler contributes — the maven coords the read-side
/// [`MavenView`](znippy_common::views::MavenView) resolves on. `classifier`
/// (e.g. `sources`/`javadoc`) is derived from the request path when a coord
/// has more than the GAV.
fn schema_fields(&self) -> Vec<Field> {
// Built from the SAME `MAVEN_SCHEMA` the wasm `plugin_schema` export
// serializes, so neither surface can grow a column the other lacks.
crate::row::MAVEN_SCHEMA
.iter()
.map(|(name, tag)| {
let dt = match *tag {
"u32" => DataType::UInt32,
"i8" => DataType::Int8,
_ => DataType::Utf8,
};
Field::new(*name, dt, true)
})
.collect()
}
fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
let coord = if path.ends_with(".pom") {
parse_pom_project(data)?
} else {
let entries = ljar::decompress_jar_filter(data, "pom.xml").ok()?;
let entry = entries
.iter()
.find(|e| e.name.starts_with("META-INF/maven/") && e.name.ends_with("/pom.xml"))?;
parse_pom_project(&entry.data)?
};
Some(make_row(crate::row::maven_fields(coord, path)))
}
}
/// Arrow-side serialization of the shared field list from [`crate::row`].
///
/// The LAST column, `classifier`, is the only optional one and lands as
/// `OptStr` so a primary artifact writes a null cell rather than an empty
/// string. Everything else is a plain `Str`; a `None` in a non-optional column
/// would mean the field writer changed, so it serializes to `OptStr(None)` too
/// rather than being silently dropped.
fn make_row(fields: Vec<(&'static str, Option<String>)>) -> ExtensionRow {
let mut out = HashMap::new();
for (name, value) in fields {
let v = match value {
Some(s) => ExtensionValue::Str(s),
None => ExtensionValue::OptStr(None),
};
out.insert(name.to_string(), v);
}
ExtensionRow { fields: out }
}