new: drive the license menu, parsing and sniffing from data/licenses.yml

This commit is contained in:
2026-09-18 13:52:24 +02:00
parent 12407c8eac
commit dd1c70c91a
6 changed files with 496 additions and 100 deletions
+86 -19
View File
@@ -15,6 +15,7 @@ use crate::debian::DebianVersion;
use crate::debian::deps::{Deps, ParseOpts};
use crate::distro_info;
use crate::new::detect::{self, Detection};
use crate::new::licenses;
use crate::new::origin::{Forge, GitOrigin};
use crate::new::templates;
@@ -213,6 +214,10 @@ impl OrigOrigin {
/// Upstream license of the package: a curated SPDX list plus a free-text
/// fallback for anything else (including "unknown" until the user picks one).
/// The curated identifiers, their accepted spellings and the menu labels
/// come from the bundled license table (`data/licenses.yml`); this enum
/// stays code because [`NewOptions`] and the template rendering match on
/// its variants — a consistency test keeps the two in lockstep.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum License {
/// MIT
@@ -239,21 +244,34 @@ pub enum License {
}
impl License {
/// Map a license string to a [`License`]: curated SPDX identifiers are
/// matched case-insensitively, anything else becomes
/// [`License::Custom`] verbatim.
/// Map a license string to a [`License`]: the curated SPDX identifiers
/// of the bundled license table (`data/licenses.yml`, matched through
/// their spellings, case-insensitively) become their variant,
/// anything else becomes [`License::Custom`] verbatim (lowercased).
pub fn parse(s: &str) -> License {
match s.to_ascii_lowercase().as_str() {
"mit" => License::Mit,
"apache-2.0" => License::Apache2,
"gpl-2.0+" => License::Gpl2Plus,
"gpl-3.0+" => License::Gpl3Plus,
"lgpl-2.1+" => License::Lgpl21Plus,
"lgpl-3.0+" => License::Lgpl3Plus,
"bsd-2-clause" => License::Bsd2Clause,
"bsd-3-clause" => License::Bsd3Clause,
"isc" => License::Isc,
other => License::Custom(other.to_string()),
match licenses::entry_for_spelling(s) {
Some(entry) => Self::from_spdx(&entry.id)
.expect("licenses.yml ids and License variants are kept in sync by a test"),
None => License::Custom(s.to_ascii_lowercase()),
}
}
/// The variant of a curated license's SPDX identifier — the inverse of
/// [`License::spdx`] for the non-`Custom` variants, `None` for any
/// other string. The ids accepted here are locked to the bundled
/// license table by a consistency test.
fn from_spdx(id: &str) -> Option<License> {
match id {
"MIT" => Some(License::Mit),
"Apache-2.0" => Some(License::Apache2),
"GPL-2.0+" => Some(License::Gpl2Plus),
"GPL-3.0+" => Some(License::Gpl3Plus),
"LGPL-2.1+" => Some(License::Lgpl21Plus),
"LGPL-3.0+" => Some(License::Lgpl3Plus),
"BSD-2-Clause" => Some(License::Bsd2Clause),
"BSD-3-Clause" => Some(License::Bsd3Clause),
"ISC" => Some(License::Isc),
_ => None,
}
}
@@ -274,12 +292,11 @@ impl License {
}
/// SPDX license data page URL (without the trailing `+` of the
/// "or later" spellings), for the copyright reference paragraph.
/// "or later" spellings), for the copyright reference paragraph: the
/// identifier substituted into the URL template of the bundled
/// license table.
pub fn spdx_url(&self) -> String {
format!(
"https://spdx.org/licenses/{}.html",
self.spdx().trim_end_matches('+')
)
licenses::url_template().replace("{id}", self.spdx().trim_end_matches('+'))
}
}
@@ -1177,6 +1194,56 @@ mod tests {
);
}
/// The bundled license table and the `License` enum are two halves of
/// one list and must never drift apart: every entry id must map to a
/// variant (and the entry must accept its own id as a spelling, so
/// the menu answers parse back), every variant must have an entry,
/// and the counts must agree. The hardcoded variant list here is the
/// lock: adding a variant or a YAML entry without the other half
/// fails this test.
#[test]
fn licenses_table_and_enum_are_in_sync() {
let variants = [
License::Mit,
License::Apache2,
License::Gpl2Plus,
License::Gpl3Plus,
License::Lgpl21Plus,
License::Lgpl3Plus,
License::Bsd2Clause,
License::Bsd3Clause,
License::Isc,
];
assert_eq!(
licenses::entries().len(),
variants.len(),
"licenses.yml and the License enum carry a different number of licenses"
);
for variant in variants {
let entry = licenses::entries()
.iter()
.find(|entry| entry.id == variant.spdx())
.unwrap_or_else(|| panic!("no licenses.yml entry for '{}'", variant.spdx()));
assert_eq!(entry.id, variant.spdx());
assert!(
entry
.spellings
.iter()
.any(|s| s.eq_ignore_ascii_case(&entry.id)),
"entry '{}' must accept its own id as a spelling",
entry.id
);
// The menu label parses back into the same variant.
assert_eq!(License::parse(&entry.menu), variant);
}
for entry in licenses::entries() {
let variant = License::from_spdx(&entry.id).unwrap_or_else(|| {
panic!("licenses.yml entry '{}' has no License variant", entry.id)
});
assert_eq!(variant.spdx(), entry.id);
}
}
#[test]
fn collision_detection() {
assert!(check_file_collisions(&["a".into(), "b".into()]).is_ok());