diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a6858..b3702f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ Notable changes, in the terms [docs/versioning.md](docs/versioning.md) sets out. **Calendar** entries change what a day reports, whatever the version number does. +## 1.1.2 + +### Fixed + +- The published description says what the package covers, and says the same + thing in both registries. PyPI takes its summary from `pyproject.toml`, which + 1.1.1 did not update, so it went out with the old one-line text while npm got + the new one. The two READMEs had drifted apart as well: npm's opened without + mentioning the Byzantine Catholic churches at all. All four now carry one + sentence, and a test fails if any of them moves without the others. + ## 1.1.1 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 1b09d27..a474676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,7 +404,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "typikon" -version = "1.1.1" +version = "1.1.2" dependencies = [ "clap", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index ee78538..d4e543d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typikon" -version = "1.1.1" +version = "1.1.2" edition = "2021" rust-version = "1.85" description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings." diff --git a/README.md b/README.md index 7417c92..f14e229 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # typikon The Orthodox liturgical day: feasts, seasons and the fast, computed rather -than fetched. The Byzantine Catholic churches are here too, on their own -calendars and, where the law has been sourced, under their own particular law. +than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic +churches, across the Julian, Revised Julian and Gregorian reckonings. Everything derives from the paschalion and the fixed calendar, so it needs no network, cannot go stale, and answers for any year you ask about. No API key, diff --git a/npm/README.md b/npm/README.md index 7173659..b0e8770 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,7 +1,10 @@ # typikon The Orthodox liturgical day: feasts, seasons and the fast, computed rather -than fetched. No network, no data files, no service to be up. +than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic +churches, across the Julian, Revised Julian and Gregorian reckonings. + +No network, no data files, no service to be up. ```sh npm install typikon diff --git a/pyproject.toml b/pyproject.toml index 56067ed..0fb13f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "typikon" -description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched" +description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings." readme = "README.md" license = "GPL-2.0-only" license-files = ["LICENSE"] diff --git a/tests/metadata.rs b/tests/metadata.rs new file mode 100644 index 0000000..8c24c49 --- /dev/null +++ b/tests/metadata.rs @@ -0,0 +1,103 @@ +//! The packaging metadata, which lives in three files and is published to two +//! registries. +//! +//! `Cargo.toml` describes the crate, `pyproject.toml` is what maturin puts on +//! PyPI, and `npm/package.json.in` is what npm shows. Nothing makes them agree +//! and nothing notices when they stop: a description edited in one of the three +//! reaches one registry and not the other, and the wrong one is then frozen +//! into a release, because neither registry lets published metadata be edited. +//! That has happened once already. +//! +//! The two READMEs open with the same sentence, because that paragraph is the +//! first thing shown on both registry pages and is what most people will read +//! instead of the description field. + +use std::fs; + +/// The value of a `key = "…"` line, from the first section that has one. +fn toml_string(path: &str, key: &str) -> String { + let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")); + for line in text.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix(key) else { continue }; + let Some(rest) = rest.trim_start().strip_prefix('=') else { continue }; + let rest = rest.trim(); + if let Some(v) = rest.strip_prefix('"').and_then(|v| v.strip_suffix('"')) { + return v.to_string(); + } + } + panic!("{path}: no `{key} = \"…\"` line"); +} + +/// The value of a `"key": "…"` line. Enough for a hand-written file with no +/// escapes in it, and the test below fails loudly rather than quietly if that +/// ever stops being true. +fn json_string(path: &str, key: &str) -> String { + let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")); + let needle = format!("\"{key}\":"); + for line in text.lines() { + let Some(rest) = line.trim().strip_prefix(&needle) else { continue }; + let rest = rest.trim().trim_end_matches(',').trim(); + if let Some(v) = rest.strip_prefix('"').and_then(|v| v.strip_suffix('"')) { + assert!(!v.contains('\\'), "{path}: {key} has escapes this reader cannot handle"); + return v.to_string(); + } + } + panic!("{path}: no `\"{key}\": \"…\"` line"); +} + +#[test] +fn the_three_descriptions_are_the_same_sentence() { + let cargo = toml_string("Cargo.toml", "description"); + let pyproject = toml_string("pyproject.toml", "description"); + let npm = json_string("npm/package.json.in", "description"); + + assert_eq!( + cargo, pyproject, + "Cargo.toml and pyproject.toml disagree; pyproject.toml is the one PyPI shows" + ); + assert_eq!( + cargo, npm, + "Cargo.toml and npm/package.json.in disagree; the latter is the one npm shows" + ); +} + +#[test] +fn the_description_says_what_the_crate_is_called() { + // Cheap guard against a description edited into something that no longer + // describes this package at all. + let d = toml_string("Cargo.toml", "description"); + assert!(d.len() > 40, "description is too short to be one: {d:?}"); + assert!(!d.ends_with(' '), "description has a trailing space"); + assert!(d.contains("Orthodox"), "description no longer mentions Orthodox: {d:?}"); +} + +/// The opening paragraph of a README, unwrapped to one line. +fn lede(path: &str) -> String { + let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")); + let body = text + .split("\n\n") + .find(|p| !p.trim().is_empty() && !p.trim_start().starts_with('#')) + .unwrap_or_else(|| panic!("{path}: no opening paragraph")); + body.split_whitespace().collect::>().join(" ") +} + +#[test] +fn both_readmes_open_with_the_description() { + // A registry page leads with the README, not with the description field, + // so the sentence a reader actually meets is this one. npm's README is a + // separate file from the crate's and had gone its own way: it described + // the package without mentioning that it covers Byzantine Catholic + // churches at all. + let description = toml_string("Cargo.toml", "description"); + assert_eq!( + lede("README.md"), + description, + "README.md does not open with the published description" + ); + assert_eq!( + lede("npm/README.md"), + description, + "npm/README.md does not open with the published description" + ); +}