#![expect(
clippy::expect_used,
clippy::indexing_slicing,
reason = "a test that cannot build its fixture should fail loudly and name it"
)]
use std::path::{Path, PathBuf};
use typed_openapi::Document;
use typed_openapi::generate::{GenerateError, Settings};
const TOY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/toy.yaml");
const CORRECTIONS: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/corrections.yaml"
);
const CLI: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/cli.yaml");
fn out(name: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
drop(std::fs::remove_dir_all(&dir));
dir
}
fn layered() -> Settings {
Settings::new(TOY).overlay(CORRECTIONS).overlay(CLI)
}
fn read(path: &Path) -> String {
std::fs::read_to_string(path).expect("a file the bless step reported writing")
}
fn wrote(dir: &Path, name: &str, contents: &str) -> PathBuf {
let path = dir.join(name);
std::fs::create_dir_all(dir).expect("a directory to write the document into");
std::fs::write(&path, contents).expect("a document this test writes");
path
}
const PATTERNED: &str = r##"
openapi: 3.0.3
info: { title: Patterned, version: "1.0" }
servers: [{ url: "http://localhost:9999" }]
paths:
/things/{sku}:
get:
operationId: getThing
parameters:
- name: sku
in: path
required: true
schema: { $ref: "#/components/schemas/Sku" }
responses:
"200": { description: OK }
components:
schemas:
Sku:
type: string
pattern: "^[A-Z]{3}-[0-9]{4}$"
"##;
#[test]
fn a_generated_newtype_over_a_string_prints_and_needs_no_engine_of_its_own() {
let dir = out("patterned");
let document = wrote(&dir, "sku.yaml", PATTERNED);
Settings::new(&document)
.write_to(&dir)
.expect("the document generates");
let types = read(&dir.join("src/types.rs"));
assert!(
types.contains("impl ::std::fmt::Display for Sku"),
"a generated string newtype cannot be written to a `format!`:\n{types}"
);
assert!(
types.contains("::typed_openapi::regress::Regex"),
"the generated pattern check does not reach the engine through this crate:\n{types}"
);
assert!(
!types.contains("<::regress::Regex>"),
"the generated code names an engine the crate holding it would have to \
depend on:\n{types}"
);
}
#[test]
fn the_written_model_is_the_written_documents_reduction() {
let dir = out("agree");
let written = layered().write_to(&dir).expect("the fixtures generate");
assert_eq!(
written,
vec![
dir.join("spec/toy.overlaid.yaml"),
dir.join("src/types.rs"),
dir.join("src/ops.rs"),
dir.join("src/model.postcard"),
],
"the artefacts, in the layout a generated crate embeds them by"
);
let blob = std::fs::read(&written[3]).expect("the reduced model");
let shipped = Document::from_blob(&blob).expect("the written blob is a reduction");
let fresh =
Document::load(&read(&written[0]), &[]).expect("the written document is a document");
assert!(
shipped == fresh,
"the reduction a binary reads is not the written document's"
);
}
const FORMATTED: &str = r#"
openapi: 3.0.3
info: { title: Formatted, version: "1.0" }
servers: [{ url: "http://localhost:9999" }]
paths:
/prices:
get:
operationId: listPrices
responses:
"200": { description: OK }
components:
schemas:
Price:
type: object
required: [currency]
properties:
currency: { type: string, format: currency }
"#;
#[test]
fn a_replaced_format_becomes_a_type_the_generated_code_never_defines() {
let with = out("replaced");
Settings::new(wrote(&with, "prices.yaml", FORMATTED))
.replace("currency", "crate::Currency")
.write_to(&with)
.expect("the document generates");
let types = read(&with.join("src/types.rs"));
assert!(
types.contains("crate::Currency"),
"`format: currency` did not become the adopter's own type:\n{types}"
);
assert!(
!types.contains("struct Currency"),
"typify defined a type the adopter owns:\n{types}"
);
let without = out("unreplaced");
Settings::new(wrote(&without, "prices.yaml", FORMATTED))
.write_to(&without)
.expect("the document generates");
let types = read(&without.join("src/types.rs"));
assert!(
!types.contains("crate::Currency"),
"a type the adopter never asked for:\n{types}"
);
assert!(
types.contains("pub currency: ::std::string::String"),
"an unreplaced format is the string the document declares:\n{types}"
);
}
#[test]
fn every_generated_file_names_the_command_that_rewrites_it() {
let dir = out("header");
layered()
.regenerated_by("just bless")
.write_to(&dir)
.expect("the fixtures generate");
for file in ["spec/toy.overlaid.yaml", "src/types.rs", "src/ops.rs"] {
assert!(
read(&dir.join(file)).contains("Generated by `just bless`"),
"{file} does not name the command that rewrites it"
);
}
assert!(
read(&dir.join("src/ops.rs"))
.contains("belongs in `fixtures/corrections.yaml` and `fixtures/cli.yaml`"),
"the generated Rust does not point at the Overlay it was corrected by"
);
}
#[test]
fn a_document_that_is_not_there_is_named() {
let error = Settings::new("nowhere.yaml")
.overlay(CORRECTIONS)
.write_to(out("missing"))
.expect_err("no document to generate from");
assert!(
matches!(&error, GenerateError::Read { path, .. } if path.ends_with("nowhere.yaml")),
"{error}"
);
}