use std::io::Read;
use std::path::PathBuf;
use baseplate::{java_test, patterns, registry};
use serde::Deserialize;
use serde_json::{json, Value};
const USAGE: &str = "usage: baseplate <op>\n java-test analyze {paths} \
-> per-path is_java_test\n patterns match {pattern, content, case_insensitive?} -> \
compiles + matches\n registry load {registry_yaml, overrides_yaml?} -> parsed \
summary\nreads a JSON request on stdin, writes an ADR-0052 response envelope on stdout.";
fn ok_envelope(body: Value) -> String {
json!({ "schema_version": "1", "status": "ok", "body": body }).to_string()
}
fn error_envelope(message: &str) -> String {
json!({ "schema_version": "1", "status": "error", "body": { "message": message } }).to_string()
}
struct TempFile(PathBuf);
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn write_temp(content: &str, label: &str, nonce: u8) -> Result<TempFile, String> {
use std::io::Write as _;
let path = std::env::temp_dir().join(format!(
"baseplate-{label}-{}-{nonce}.yaml",
std::process::id()
));
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|e| format!("create temp {label} file: {e}"))?;
file.write_all(content.as_bytes())
.map_err(|e| format!("write temp {label} file: {e}"))?;
Ok(TempFile(path))
}
fn run_java_test_analyze(input: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct Req {
#[serde(default)]
paths: Vec<String>,
}
let req: Req =
serde_json::from_str(input).map_err(|e| format!("invalid java-test request JSON: {e}"))?;
let results: Vec<Value> = req
.paths
.iter()
.map(|p| json!({ "path": p, "is_java_test": java_test::is_java_test_file(p) }))
.collect();
Ok(ok_envelope(json!({ "results": results })))
}
fn run_patterns_match(input: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct Req {
pattern: String,
#[serde(default)]
content: String,
#[serde(default)]
case_insensitive: bool,
}
let req: Req =
serde_json::from_str(input).map_err(|e| format!("invalid patterns request JSON: {e}"))?;
let compiled = if req.case_insensitive {
patterns::compile_ci(&req.pattern)
} else {
patterns::compile_cs(&req.pattern)
};
let body = match compiled {
Ok(re) => {
let matches: Vec<Value> = re
.find_iter(&req.content)
.map(|m| json!({ "start": m.start(), "end": m.end(), "text": m.as_str() }))
.collect();
json!({ "compiles": true, "match_count": matches.len(), "matches": matches })
}
Err(e) => json!({ "compiles": false, "error": e.to_string(), "matches": [] }),
};
Ok(ok_envelope(body))
}
fn run_registry_load(input: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct Req {
registry_yaml: String,
#[serde(default)]
overrides_yaml: Option<String>,
}
let req: Req =
serde_json::from_str(input).map_err(|e| format!("invalid registry request JSON: {e}"))?;
let reg_tmp = write_temp(&req.registry_yaml, "registry", 0)?;
let ov_tmp = match &req.overrides_yaml {
Some(content) => Some(write_temp(content, "overrides", 1)?),
None => None,
};
let ov_path = ov_tmp.as_ref().map(|t| t.0.as_path());
let body = match registry::load(®_tmp.0, ov_path) {
Ok(reg) => {
let promises: Vec<Value> = reg
.promises
.iter()
.map(|(id, spec)| {
json!({
"id": id,
"type": spec.promise_type, "enabled": spec.enabled,
"method": spec.method_raw,
"description": spec.description,
})
})
.collect();
json!({
"valid": true,
"version": reg.version,
"promise_count": promises.len(),
"promises": promises,
})
}
Err(e) => json!({ "valid": false, "error": e.to_string() }),
};
Ok(ok_envelope(body))
}
type Op = fn(&str) -> Result<String, String>;
fn main() {
let args: Vec<String> = std::env::args().collect();
let op = (
args.get(1).map(String::as_str),
args.get(2).map(String::as_str),
);
let handler: Option<Op> = match op {
(Some("java-test"), Some("analyze")) => Some(run_java_test_analyze),
(Some("patterns"), Some("match")) => Some(run_patterns_match),
(Some("registry"), Some("load")) => Some(run_registry_load),
_ => None,
};
match handler {
Some(run) => {
let mut input = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
println!("{}", error_envelope(&format!("failed to read stdin: {e}")));
std::process::exit(1);
}
match run(&input) {
Ok(out) => println!("{out}"),
Err(e) => {
println!("{}", error_envelope(&e));
std::process::exit(1);
}
}
}
None => match args.get(1).map(String::as_str) {
Some("--help") | Some("-h") | Some("help") => println!("{USAGE}"),
_ => {
eprintln!("{USAGE}");
std::process::exit(2);
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn body_of(out: &str) -> Value {
let v: Value = serde_json::from_str(out).expect("output is JSON");
assert_eq!(v["schema_version"], "1");
assert_eq!(v["status"], "ok");
v["body"].clone()
}
#[test]
fn java_test_classifies_test_and_main_paths() {
let req = json!({"paths": ["src/test/java/x/FooTest.java", "src/main/java/x/Foo.java"]})
.to_string();
let body = body_of(&run_java_test_analyze(&req).unwrap());
let results = body["results"].as_array().unwrap();
assert_eq!(results[0]["is_java_test"], true);
assert_eq!(results[1]["is_java_test"], false);
}
#[test]
fn patterns_match_reports_hits_and_a_bad_pattern_is_a_result_not_an_error() {
let hit = json!({"pattern": "TODO|FIXME", "content": "x TODO y"}).to_string();
let body = body_of(&run_patterns_match(&hit).unwrap());
assert_eq!(body["compiles"], true);
assert!(body["match_count"].as_u64().unwrap() >= 1);
let bad = json!({"pattern": "(unclosed", "content": "x"}).to_string();
let body = body_of(&run_patterns_match(&bad).unwrap());
assert_eq!(body["compiles"], false);
}
#[test]
fn registry_load_summarizes_a_valid_registry_and_flags_an_invalid_one() {
let yaml = "version: \"1\"\npromises:\n read-before-write:\n type: structural\n \
method: grep\n description: reads precede writes\n";
let req = json!({"registry_yaml": yaml}).to_string();
let body = body_of(&run_registry_load(&req).unwrap());
assert_eq!(body["valid"], true);
assert_eq!(body["version"], "1");
assert!(body["promises"]
.as_array()
.unwrap()
.iter()
.any(|p| p["id"] == "read-before-write" && p["type"] == "structural"));
let invalid = json!({"registry_yaml": "foo: bar\n"}).to_string();
let body = body_of(&run_registry_load(&invalid).unwrap());
assert_eq!(body["valid"], false);
}
#[test]
fn invalid_request_json_is_a_hard_error_not_a_false_clean_pass() {
assert!(run_java_test_analyze("not json").is_err());
assert!(run_patterns_match("not json").is_err());
assert!(run_registry_load("not json").is_err());
}
}