Skip to content

Commit 27bda41

Browse files
committed
new plugin: maven
Signed-off-by: Tuan Anh Tran <[email protected]>
1 parent 919efb1 commit 27bda41

File tree

7 files changed

+648
-0
lines changed

7 files changed

+648
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasip1"

examples/plugins/maven/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
debug/
4+
target/
5+
6+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8+
Cargo.lock
9+
10+
# These are backup files generated by rustfmt
11+
**/*.rs.bk
12+
13+
# MSVC Windows builds of rustc generate these, which store debugging information
14+
*.pdb
15+
16+
# RustRover
17+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19+
# and can be added to the global gitignore or merged into this file. For a more nuclear
20+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
21+
#.idea/

examples/plugins/maven/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "maven"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
name = "plugin"
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
quick-xml = "0.31"
12+
extism-pdk = "=1.4.0"
13+
serde = { version = "1.0.219", features = ["derive"] }
14+
serde_json = "1.0.140"
15+
base64-serde = "0.8.0"
16+
base64 = "0.22.1"

examples/plugins/maven/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM rust:1.86-slim AS builder
2+
3+
RUN rustup target add wasm32-wasip1 && \
4+
rustup component add rust-std --target wasm32-wasip1 && \
5+
cargo install cargo-auditable
6+
7+
WORKDIR /workspace
8+
COPY . .
9+
RUN cargo fetch
10+
RUN cargo auditable build --release --target wasm32-wasip1
11+
12+
FROM scratch
13+
WORKDIR /
14+
COPY --from=builder /workspace/target/wasm32-wasip1/release/plugin.wasm /plugin.wasm

examples/plugins/maven/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# maven
2+
3+
A plugin that fetches the dependencies of a Maven package (from Maven Central) given its group, artifact, and version.
4+
5+
## What it does
6+
7+
Given a Maven package (groupId, artifactId, version), fetches its POM file from Maven Central and returns its dependencies as JSON.
8+
9+
## Usage
10+
11+
Call with:
12+
```json
13+
{
14+
"plugins": [
15+
{
16+
"name": "mvn_fetch_deps",
17+
"path": "oci://ghcr.io/tuananh/maven-plugin:latest",
18+
"runtime_config": {
19+
"allowed_hosts": ["repo1.maven.org"]
20+
}
21+
}
22+
]
23+
}
24+
```
25+
26+
### Example input
27+
28+
```json
29+
{
30+
"name": "mvn_fetch_deps",
31+
"arguments": {
32+
"group": "org.springframework.boot",
33+
"artifact": "spring-boot-starter-web",
34+
"version": "3.5.0"
35+
}
36+
}
37+
```
38+
39+
### Example output
40+
41+
```json
42+
{
43+
"dependencies": [
44+
{
45+
"groupId": "org.springframework.boot",
46+
"artifactId": "spring-boot-starter",
47+
"version": "3.5.0",
48+
"scope": "compile"
49+
},
50+
{
51+
"groupId": "org.springframework.boot",
52+
"artifactId": "spring-boot-starter-json",
53+
"version": "3.5.0",
54+
"scope": "compile"
55+
}
56+
// ...
57+
]
58+
}
59+
```
60+
61+
Return the list of dependencies of the given Maven package.

examples/plugins/maven/src/lib.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
mod pdk;
2+
3+
use std::collections::BTreeMap;
4+
5+
use extism_pdk::*;
6+
use json::Value;
7+
use pdk::types::{
8+
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
9+
};
10+
use serde_json::json;
11+
12+
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
13+
match input.params.name.as_str() {
14+
"mvn_fetch_deps" => mvn_fetch_deps(input),
15+
_ => Ok(CallToolResult {
16+
is_error: Some(true),
17+
content: vec![Content {
18+
annotations: None,
19+
text: Some(format!("Unknown tool: {}", input.params.name)),
20+
mime_type: None,
21+
r#type: ContentType::Text,
22+
data: None,
23+
}],
24+
}),
25+
}
26+
}
27+
28+
fn mvn_fetch_deps(input: CallToolRequest) -> Result<CallToolResult, Error> {
29+
use quick_xml::Reader;
30+
use quick_xml::events::Event;
31+
use serde_json::json;
32+
33+
let args = input.params.arguments.unwrap_or_default();
34+
let group = match args.get("group") {
35+
Some(Value::String(s)) => s,
36+
_ => {
37+
return Ok(CallToolResult {
38+
is_error: Some(true),
39+
content: vec![Content {
40+
annotations: None,
41+
text: Some("Missing 'group' argument".into()),
42+
mime_type: None,
43+
r#type: ContentType::Text,
44+
data: None,
45+
}],
46+
});
47+
}
48+
};
49+
let artifact = match args.get("artifact") {
50+
Some(Value::String(s)) => s,
51+
_ => {
52+
return Ok(CallToolResult {
53+
is_error: Some(true),
54+
content: vec![Content {
55+
annotations: None,
56+
text: Some("Missing 'artifact' argument".into()),
57+
mime_type: None,
58+
r#type: ContentType::Text,
59+
data: None,
60+
}],
61+
});
62+
}
63+
};
64+
let version = match args.get("version") {
65+
Some(Value::String(s)) => s,
66+
_ => {
67+
return Ok(CallToolResult {
68+
is_error: Some(true),
69+
content: vec![Content {
70+
annotations: None,
71+
text: Some("Missing 'version' argument".into()),
72+
mime_type: None,
73+
r#type: ContentType::Text,
74+
data: None,
75+
}],
76+
});
77+
}
78+
};
79+
80+
let group_path = group.replace('.', "/");
81+
let pom_url = format!(
82+
"https://repo1.maven.org/maven2/{}/{}/{}/{}-{}.pom",
83+
group_path, artifact, version, artifact, version
84+
);
85+
86+
let mut req = HttpRequest {
87+
url: pom_url.clone(),
88+
headers: BTreeMap::new(),
89+
method: Some("GET".to_string()),
90+
};
91+
req.headers
92+
.insert("User-Agent".to_string(), "maven-plugin/1.0".to_string());
93+
94+
let res = match http::request::<()>(&req, None) {
95+
Ok(r) => r,
96+
Err(e) => {
97+
return Ok(CallToolResult {
98+
is_error: Some(true),
99+
content: vec![Content {
100+
annotations: None,
101+
text: Some(format!("Failed to fetch POM: {}", e)),
102+
mime_type: None,
103+
r#type: ContentType::Text,
104+
data: None,
105+
}],
106+
});
107+
}
108+
};
109+
let body = res.body();
110+
let xml = String::from_utf8_lossy(body.as_slice());
111+
112+
// Parse dependencies
113+
let mut reader = Reader::from_str(&xml);
114+
reader.trim_text(true);
115+
let mut buf = Vec::new();
116+
let mut dependencies = Vec::new();
117+
let mut in_dependencies = false;
118+
let mut current = serde_json::Map::new();
119+
let mut current_tag = String::new();
120+
121+
loop {
122+
match reader.read_event_into(&mut buf) {
123+
Ok(Event::Start(ref e)) => {
124+
let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
125+
if tag == "dependencies" {
126+
in_dependencies = true;
127+
} else if in_dependencies && tag == "dependency" {
128+
current = serde_json::Map::new();
129+
} else if in_dependencies {
130+
current_tag = tag;
131+
}
132+
}
133+
Ok(Event::End(ref e)) => {
134+
let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
135+
if tag == "dependencies" {
136+
in_dependencies = false;
137+
} else if in_dependencies && tag == "dependency" {
138+
dependencies.push(json!(current));
139+
} else if in_dependencies {
140+
current_tag.clear();
141+
}
142+
}
143+
Ok(Event::Text(e)) => {
144+
if in_dependencies && !current_tag.is_empty() {
145+
current.insert(current_tag.clone(), json!(e.unescape().unwrap_or_default()));
146+
}
147+
}
148+
Ok(Event::Eof) => break,
149+
Err(_) => break,
150+
_ => {}
151+
}
152+
buf.clear();
153+
}
154+
155+
Ok(CallToolResult {
156+
is_error: None,
157+
content: vec![Content {
158+
annotations: None,
159+
text: Some(json!({"dependencies": dependencies}).to_string()),
160+
mime_type: Some("application/json".to_string()),
161+
r#type: ContentType::Text,
162+
data: None,
163+
}],
164+
})
165+
}
166+
167+
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
168+
Ok(ListToolsResult{
169+
tools: vec![
170+
ToolDescription {
171+
name: "mvn_fetch_deps".into(),
172+
description: "Fetches the dependencies of a Maven package by group, artifact, and version from Maven Central.".into(),
173+
input_schema: json!({
174+
"type": "object",
175+
"properties": {
176+
"group": {
177+
"type": "string",
178+
"description": "The Maven groupId",
179+
},
180+
"artifact": {
181+
"type": "string",
182+
"description": "The Maven artifactId",
183+
},
184+
"version": {
185+
"type": "string",
186+
"description": "The Maven version",
187+
},
188+
},
189+
"required": ["group", "artifact", "version"],
190+
})
191+
.as_object()
192+
.unwrap()
193+
.clone(),
194+
},
195+
],
196+
})
197+
}

0 commit comments

Comments
 (0)