const MANIFEST: &str = include_str!("../Cargo.toml");
fn manifest_version(crate_name: &str) -> &'static str {
match parse_manifest_version(MANIFEST, crate_name) {
Some(version) => return version,
None => panic!(
"crate `{crate_name}` is not a declared dependency of oapi-codegen; cannot determine its version for the dependency report"
),
}
}
fn parse_manifest_version<'a>(manifest: &'a str, crate_name: &str) -> Option<&'a str> {
let mut inside_table_for_crate = false;
for line in manifest.lines() {
let line = line.trim();
if let Some(header) = line.strip_prefix('[').and_then(|rest| return rest.strip_suffix(']')) {
inside_table_for_crate = is_dependency_table_for(header, crate_name);
continue;
}
if inside_table_for_crate
&& let Some(rest) = line.strip_prefix("version")
&& let Some(value) = rest.trim_start().strip_prefix('=')
&& let Some(version) = first_quoted(value)
{
return Some(version);
}
let Some(rest) = line.strip_prefix(crate_name) else {
continue;
};
if !rest.starts_with([' ', '\t', '=']) {
continue;
}
let Some(value) = rest.trim_start().strip_prefix('=') else {
continue;
};
let value = value.trim_start();
let scan = match value.strip_prefix('{') {
Some(table) => match table.find("version =").or_else(|| return table.find("version=")) {
Some(index) => &table[index..],
None => continue,
},
None => value,
};
if let Some(version) = first_quoted(scan) {
return Some(version);
}
}
return None;
}
fn is_dependency_table_for(header: &str, crate_name: &str) -> bool {
for scope in ["dependencies.", "dev-dependencies.", "build-dependencies."] {
if let Some(name) = header.strip_prefix(scope)
&& name == crate_name
{
return true;
}
}
return false;
}
fn first_quoted(text: &str) -> Option<&str> {
let after = text.split_once('"')?.1;
return after.split_once('"').map(|(value, _)| return value);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dependency {
pub name: &'static str,
pub version: &'static str,
pub default_features: bool,
pub features: Vec<&'static str>,
}
impl Dependency {
pub fn toml(&self) -> String {
if self.default_features && self.features.is_empty() {
return format!("{} = \"{}\"", self.name, self.version);
}
let mut parts = vec![format!("version = \"{}\"", self.version)];
if !self.default_features {
parts.push("default-features = false".to_owned());
}
if !self.features.is_empty() {
let features = self
.features
.iter()
.map(|feature| return format!("\"{feature}\""))
.collect::<Vec<_>>()
.join(", ");
parts.push(format!("features = [{features}]"));
}
return format!("{} = {{ {} }}", self.name, parts.join(", "));
}
pub fn cargo_add(&self) -> String {
let mut command = format!("cargo add {}@{}", self.name, self.version);
if !self.default_features {
command.push_str(" --no-default-features");
}
if !self.features.is_empty() {
command.push_str(&format!(" --features {}", self.features.join(",")));
}
return command;
}
pub fn cargo_add_args(&self) -> Vec<String> {
let mut args = vec!["add".to_owned(), format!("{}@{}", self.name, self.version)];
if !self.default_features {
args.push("--no-default-features".to_owned());
}
if !self.features.is_empty() {
args.push("--features".to_owned());
args.push(self.features.join(","));
}
return args;
}
}
pub fn required_dependencies(code: &str) -> Vec<Dependency> {
let has = |needle: &str| return code.contains(needle);
let mut deps = Vec::new();
if has("serde::Serialize") || has("serde::Deserialize") {
deps.push(with_features("serde", true, vec!["derive"]));
}
if has("serde_json::") {
deps.push(plain("serde_json"));
}
if has("chrono::") {
deps.push(with_features("chrono", true, vec!["serde"]));
}
if has("uuid::") {
deps.push(with_features("uuid", true, vec!["serde"]));
}
if has("regex::") {
deps.push(with_features("regex", false, vec!["std", "perf", "unicode"]));
}
if has("http::") {
deps.push(plain("http"));
}
if has("axum::") {
let mut features = Vec::new();
if has("axum::extract::Multipart") {
features.push("multipart");
}
deps.push(with_features("axum", true, features));
}
if has("axum_extra::") {
let mut features = Vec::new();
if has("axum_extra::extract::Query") {
features.push("query");
}
if has("axum_extra::extract::CookieJar") {
features.push("cookie");
}
deps.push(with_features("axum-extra", true, features));
}
if has("reqwest::") {
let mut features = Vec::new();
if has("reqwest::blocking") {
features.push("blocking");
}
if has(".json(") {
features.push("json");
}
if has(".form(") {
features.push("form");
}
if has(".query(") {
features.push("query");
}
if has(".multipart(") || has("reqwest::blocking::multipart") {
features.push("multipart");
}
deps.push(with_features("reqwest", false, features));
}
if has("percent_encoding::") {
deps.push(plain("percent-encoding"));
}
if has("serde_urlencoded::") {
deps.push(plain("serde_urlencoded"));
}
return deps;
}
fn plain(name: &'static str) -> Dependency {
return with_features(name, true, Vec::new());
}
fn with_features(name: &'static str, default_features: bool, features: Vec<&'static str>) -> Dependency {
return Dependency {
name,
version: manifest_version(name),
default_features,
features,
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_key_matched_as_token_not_substring() {
let manifest = include_str!("../tests/fixtures/manifests/reordered_version_key.toml");
assert_eq!(parse_manifest_version(manifest, "axum"), Some("0.8.9"));
}
#[test]
fn a_version_is_found_in_the_layout_cargo_publishes() {
let manifest = r#"
[package]
name = "specimen"
version = "9.9.9"
[dependencies.plain]
version = "1.2.3"
[dependencies.with-features]
version = "4.5.6"
features = ["one", "two"]
[dependencies.multi-line-features]
version = "7.8.9"
features = [
"one",
"two",
]
[dependencies.prefix]
version = "0.1.0"
[dependencies.prefix_extended]
version = "0.2.0"
[dev-dependencies.only-for-tests]
version = "5.0.0"
[build-dependencies.only-for-build]
version = "6.0.0"
"#;
for (crate_name, expected) in [
("plain", Some("1.2.3")),
("with-features", Some("4.5.6")),
("multi-line-features", Some("7.8.9")),
("prefix", Some("0.1.0")),
("prefix_extended", Some("0.2.0")),
("only-for-tests", Some("5.0.0")),
("only-for-build", Some("6.0.0")),
("absent", None),
("specimen", None),
] {
assert_eq!(
parse_manifest_version(manifest, crate_name),
expected,
"the published layout should report `{crate_name}` as {expected:?}"
);
}
}
#[test]
fn toml_renders_short_and_table_forms() {
assert_eq!(
Dependency {
name: "http",
version: "1",
default_features: true,
features: vec![],
}
.toml(),
"http = \"1\""
);
assert_eq!(
Dependency {
name: "serde",
version: "1",
default_features: true,
features: vec!["derive"],
}
.toml(),
"serde = { version = \"1\", features = [\"derive\"] }"
);
assert_eq!(
Dependency {
name: "reqwest",
version: "0.13",
default_features: false,
features: vec!["blocking", "json"],
}
.toml(),
"reqwest = { version = \"0.13\", default-features = false, features = [\"blocking\", \"json\"] }"
);
}
#[test]
fn cargo_add_renders_flags() {
assert_eq!(
Dependency {
name: "http",
version: "1",
default_features: true,
features: vec![],
}
.cargo_add(),
"cargo add http@1"
);
assert_eq!(
Dependency {
name: "reqwest",
version: "0.13",
default_features: false,
features: vec!["blocking", "json"],
}
.cargo_add(),
"cargo add reqwest@0.13 --no-default-features --features blocking,json"
);
}
#[test]
fn cargo_add_args_split_for_process_execution() {
assert_eq!(
Dependency {
name: "http",
version: "1",
default_features: true,
features: vec![],
}
.cargo_add_args(),
vec!["add", "http@1"]
);
assert_eq!(
Dependency {
name: "reqwest",
version: "0.13",
default_features: false,
features: vec!["blocking", "json"],
}
.cargo_add_args(),
vec![
"add",
"reqwest@0.13",
"--no-default-features",
"--features",
"blocking,json"
]
);
}
#[test]
fn versions_come_from_the_manifest_not_hardcoded() {
assert_eq!(manifest_version("http"), extract_manifest_version("http"));
assert_eq!(manifest_version("axum"), extract_manifest_version("axum"));
assert!(!manifest_version("serde_urlencoded").is_empty());
}
#[test]
fn serde_prefix_does_not_match_serde_json_or_urlencoded() {
assert_eq!(manifest_version("serde"), extract_manifest_version("serde"));
assert_ne!(manifest_version("serde"), manifest_version("serde_json"));
}
#[test]
fn every_reportable_crate_has_a_manifest_version() {
let code = "\
serde::Serialize serde_json::Value chrono::DateTime uuid::Uuid http::StatusCode \
axum::extract::Multipart axum_extra::extract::Query axum_extra::extract::CookieJar \
reqwest::blocking::multipart .json( .form( .query( percent_encoding::utf8 serde_urlencoded::from_str";
for dep in required_dependencies(code) {
assert!(!dep.version.is_empty(), "{} has an empty version", dep.name);
}
}
fn extract_manifest_version(name: &str) -> String {
for line in MANIFEST.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix(name)
&& rest.starts_with([' ', '\t', '='])
{
let quoted: Vec<&str> = line.split('"').collect();
if quoted.len() >= 2 {
return quoted[1].to_owned();
}
}
}
panic!("`{name}` not found in manifest");
}
#[test]
fn detects_server_stack_from_generated_paths() {
let code = "axum::Json axum::extract::Multipart axum_extra::extract::Query http::StatusCode serde::Serialize serde_json::Value";
let deps = required_dependencies(code);
let names: Vec<&str> = deps.iter().map(|dep| return dep.name).collect();
assert_eq!(names, vec!["serde", "serde_json", "http", "axum", "axum-extra"]);
let axum = deps.iter().find(|dep| return dep.name == "axum").expect("axum present");
assert_eq!(axum.features, vec!["multipart"]);
let extra = deps
.iter()
.find(|dep| return dep.name == "axum-extra")
.expect("axum-extra present");
assert_eq!(extra.features, vec!["query"]);
}
#[test]
fn detects_client_stack_from_generated_paths() {
let code = "reqwest::blocking::Client request.json(&body) percent_encoding::utf8 serde::Deserialize";
let deps = required_dependencies(code);
let reqwest = deps
.iter()
.find(|dep| return dep.name == "reqwest")
.expect("reqwest present");
assert!(!reqwest.default_features);
assert_eq!(reqwest.features, vec!["blocking", "json"]);
assert!(deps.iter().any(|dep| return dep.name == "percent-encoding"));
}
#[test]
fn axum_marker_does_not_match_axum_extra() {
let deps = required_dependencies("axum_extra::extract::CookieJar");
assert!(
!deps.iter().any(|dep| return dep.name == "axum"),
"`axum_extra::` must not be mistaken for the `axum` crate",
);
let extra = deps
.iter()
.find(|dep| return dep.name == "axum-extra")
.expect("axum-extra present");
assert_eq!(extra.features, vec!["cookie"]);
}
#[test]
fn no_dependencies_for_dependency_free_output() {
assert!(required_dependencies("pub const SERVER_URL: &str = \"https://x\";").is_empty());
}
}