use std::path::{Path, PathBuf};
use assert_cmd::Command;
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.unwrap()
}
fn plugin_artifact(name: &str) -> PathBuf {
let path = repo_root()
.join("plugins/target/wasm32-wasip2/release")
.join(format!("{name}.wasm"));
if !path.exists() {
panic!(
"plugin artifact missing at {}.\nBuild plugins first:\n \
cargo build --release --manifest-path plugins/Cargo.toml",
path.display()
);
}
path
}
const SAMPLE_IR: &str = r#"{
"info": { "title": "test-api", "version": "1.0.0" },
"operations": [
{
"id": "getThing",
"method": "get",
"path_template": "/things/{id}",
"responses": []
}
],
"types": [],
"security_schemes": [],
"servers": []
}"#;
#[test]
fn generate_pipeline_writes_files() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
std::fs::write(project.join("ir.json"), SAMPLE_IR).unwrap();
let xform_wasm = plugin_artifact("transformer_noop");
let gen_wasm = plugin_artifact("generator_debug_dump");
let toml = format!(
r#"
[input]
ir = "ir.json"
[[transformers]]
wasm = "{xform}"
[generator]
wasm = "{gen}"
[output]
dir = "out"
"#,
xform = xform_wasm.display(),
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.success();
let out_path = project.join("out/ir.txt");
let out = std::fs::read_to_string(&out_path).expect("output file");
assert!(out.contains("title: test-api"), "body: {out}");
assert!(out.contains("GET /things/{id}"), "body: {out}");
}
#[test]
fn generate_from_petstore_spec() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let petstore = repo_root().join("fixtures/e2e/petstore/spec.json");
let spec = std::fs::read_to_string(&petstore).expect("read petstore spec");
std::fs::write(project.join("spec.json"), spec).unwrap();
let gen_wasm = plugin_artifact("generator_typescript_fetch");
let toml = format!(
r#"
[input]
spec = "spec.json"
[generator]
wasm = "{gen}"
config = {{ packageName = "petstore-client" }}
[output]
dir = "out"
"#,
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.success();
let out_root = project.join("out");
let client = std::fs::read_to_string(out_root.join("src/client.ts")).expect("client.ts");
assert!(client.contains("export class ApiClient"), "{client}");
assert!(client.contains("async listPets"), "{client}");
assert!(client.contains("async createPet"), "{client}");
assert!(client.contains("async showPetById"), "{client}");
let models = std::fs::read_to_string(out_root.join("src/models.ts")).expect("models.ts");
assert!(models.contains("export interface Pet {"), "{models}");
assert!(
models.contains("export type Pets = Array<Pet>;"),
"{models}"
);
let pkg = std::fs::read_to_string(out_root.join("package.json")).expect("package.json");
assert!(pkg.contains("\"name\": \"petstore-client\""), "{pkg}");
}
#[test]
fn unsupported_spec_feature_halts_with_diagnostic() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let bad_spec = r#"{
"openapi": "3.0.3",
"info": { "title": "x", "version": "1" },
"paths": {},
"components": {
"schemas": {
"Bad": {
"not": { "type": "string" }
}
}
}
}"#;
std::fs::write(project.join("spec.json"), bad_spec).unwrap();
let gen_wasm = plugin_artifact("generator_debug_dump");
let toml = format!(
r#"
[input]
spec = "spec.json"
[generator]
wasm = "{gen}"
[output]
dir = "out"
"#,
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.failure()
.stderr(predicates::str::contains("parser/E-COMPOSITION-NOT"));
}
#[test]
fn generator_config_passes_validation() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let petstore = repo_root().join("fixtures/e2e/petstore/spec.json");
let spec = std::fs::read_to_string(&petstore).expect("read petstore spec");
std::fs::write(project.join("spec.json"), spec).unwrap();
let gen_wasm = plugin_artifact("generator_typescript_fetch");
let toml = format!(
r#"
[input]
spec = "spec.json"
[generator]
wasm = "{gen}"
config = {{ packageName = "petstore-client", baseUrl = "https://example.com" }}
[output]
dir = "out"
"#,
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.success();
}
#[test]
fn generator_config_fails_validation() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let petstore = repo_root().join("fixtures/e2e/petstore/spec.json");
let spec = std::fs::read_to_string(&petstore).expect("read petstore spec");
std::fs::write(project.join("spec.json"), spec).unwrap();
let gen_wasm = plugin_artifact("generator_typescript_fetch");
let toml = format!(
r#"
[input]
spec = "spec.json"
[generator]
wasm = "{gen}"
config = {{ bogusKey = "nope" }}
[output]
dir = "out"
"#,
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.failure()
.stderr(predicates::str::contains("config validation failed"))
.stderr(predicates::str::contains("bogusKey"));
}
#[test]
fn generate_config_less_from_spec() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let petstore = repo_root().join("fixtures/e2e/petstore/spec.json");
let xform_wasm = plugin_artifact("transformer_noop");
let gen_wasm = plugin_artifact("generator_typescript_fetch");
let out_dir = project.join("out");
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg("-i")
.arg(&petstore)
.arg("--transformer")
.arg(&xform_wasm)
.arg("--generator")
.arg(&gen_wasm)
.arg("-o")
.arg(&out_dir)
.assert()
.success();
let client = std::fs::read_to_string(out_dir.join("src/client.ts")).expect("client.ts");
assert!(client.contains("export class ApiClient"), "{client}");
assert!(client.contains("async listPets"), "{client}");
assert!(
!project.join("forge.toml").exists(),
"config-less run must not depend on forge.toml"
);
}
#[test]
fn generate_no_config_no_args_fails() {
let dir = tempfile::tempdir().unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(dir.path())
.assert()
.failure()
.stderr(predicates::str::contains("forge.toml"));
}
#[test]
fn generate_config_less_requires_generator() {
let dir = tempfile::tempdir().unwrap();
let petstore = repo_root().join("fixtures/e2e/petstore/spec.json");
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg("-i")
.arg(&petstore)
.arg("-o")
.arg(dir.path().join("out"))
.assert()
.failure()
.stderr(predicates::str::contains("--generator is required"));
}
#[test]
fn ir_version_subcommand() {
Command::cargo_bin("forge")
.unwrap()
.arg("ir-version")
.assert()
.success()
.stdout(predicates::str::starts_with("0."));
}
#[test]
fn generate_from_split_document_spec() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let fixture = repo_root().join("fixtures/real-world/multi-tenant-shape");
let gen_wasm = plugin_artifact("generator_debug_dump");
let toml = format!(
r#"
[input]
spec = "{spec}"
[generator]
wasm = "{gen}"
[output]
dir = "out"
"#,
spec = fixture.join("spec.json").display(),
gen = gen_wasm.display(),
);
std::fs::write(project.join("forge.toml"), toml).unwrap();
Command::cargo_bin("forge")
.unwrap()
.arg("generate")
.arg(project)
.assert()
.success();
let out = std::fs::read_to_string(project.join("out/ir.txt")).expect("output");
assert!(out.contains("listUsers"), "missing listUsers: {out}");
assert!(out.contains("createUser"), "missing createUser: {out}");
assert!(out.contains("getDocument"), "missing getDocument: {out}");
assert!(
out.contains("uploadDocumentAttachment"),
"missing uploadDocumentAttachment: {out}"
);
assert!(
out.contains("updateNoteSavedView"),
"missing updateNoteSavedView: {out}"
);
}