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) -> Result<TempFile, String> {
use std::io::Write as _;
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"baseplate-{label}-{}-{seq}.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 {
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,
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 overridden_ids(text: &str) -> Result<Vec<String>, String> {
let root: serde_yaml::Value = serde_yaml::from_str(text)
.map_err(|e| format!("overrides document does not parse as YAML: {e}"))?;
let overrides = root.get("overrides").and_then(|o| o.as_mapping()).ok_or(
"overrides document has no `overrides:` mapping at its root, so nothing in it \
would be applied (the overrides schema is not the registry schema)",
)?;
Ok(overrides
.keys()
.filter_map(|k| k.as_str().map(str::to_string))
.collect())
}
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 override_ids = match req.overrides_yaml.as_deref().map(overridden_ids) {
Some(Err(why)) => return Ok(ok_envelope(json!({ "valid": false, "error": why }))),
Some(Ok(ids)) => Some(ids),
None => None,
};
let reg_tmp = write_temp(&req.registry_yaml, "registry")?;
let ov_tmp = match &req.overrides_yaml {
Some(content) => Some(write_temp(content, "overrides")?),
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();
let mut body = json!({
"valid": true,
"version": reg.version,
"promise_count": promises.len(),
"promises": promises,
});
if let Some(ids) = &override_ids {
let unknown: Vec<&String> = ids
.iter()
.filter(|id| !reg.promises.contains_key(*id))
.collect();
body["overrides_unknown"] = json!(unknown);
}
body
}
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 two_temp_files_in_one_process_never_share_a_name() {
let a = write_temp("x", "registry").unwrap();
let b = write_temp("x", "registry").unwrap();
assert_ne!(a.0, b.0, "two calls in one process must not share a name");
}
#[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());
}
#[test]
fn an_omitted_required_field_is_an_error_not_an_empty_result() {
assert!(
run_java_test_analyze("{}").is_err(),
"absent paths must not answer results: []"
);
assert!(
run_patterns_match(&json!({"pattern": "abc"}).to_string()).is_err(),
"absent content must not answer match_count: 0"
);
let body = body_of(&run_java_test_analyze(&json!({"paths": []}).to_string()).unwrap());
assert_eq!(body["results"].as_array().unwrap().len(), 0);
let body = body_of(
&run_patterns_match(&json!({"pattern": "abc", "content": ""}).to_string()).unwrap(),
);
assert_eq!(body["match_count"], 0);
}
#[test]
fn a_discarded_overrides_document_is_reported_not_silently_dropped() {
let yaml = "version: \"1\"\npromises:\n a:\n type: standing\n method: grep\n \
pattern: x\n description: d\n";
let good = json!({"registry_yaml": yaml, "overrides_yaml": "overrides:\n a:\n enabled: false\n"});
let body = body_of(&run_registry_load(&good.to_string()).unwrap());
assert_eq!(body["valid"], true);
assert_eq!(body["promises"][0]["enabled"], false);
assert_eq!(body["overrides_unknown"].as_array().unwrap().len(), 0);
let bad = json!({"registry_yaml": yaml, "overrides_yaml": "[unclosed\n"});
let body = body_of(&run_registry_load(&bad.to_string()).unwrap());
assert_eq!(
body["valid"], false,
"an unparseable overrides document is not a valid load"
);
assert!(
body["error"]
.as_str()
.unwrap()
.contains("does not parse as YAML"),
"error was: {}",
body["error"]
);
let wrong_shape = json!({"registry_yaml": yaml, "overrides_yaml": yaml});
let body = body_of(&run_registry_load(&wrong_shape.to_string()).unwrap());
assert_eq!(
body["valid"], false,
"an overrides document with no `overrides:` root is discarded"
);
assert!(
body["error"]
.as_str()
.unwrap()
.contains("no `overrides:` mapping at its root"),
"error was: {}",
body["error"]
);
let typo = json!({"registry_yaml": yaml, "overrides_yaml": "overrides:\n aa:\n enabled: false\n"});
let body = body_of(&run_registry_load(&typo.to_string()).unwrap());
assert_eq!(body["valid"], true);
assert_eq!(body["overrides_unknown"], json!(["aa"]));
}
}