use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use cargo_metadata::Package;
use serde::Deserialize;
use serde_json::Value;
use waos_package::ProgramId;
#[derive(Debug)]
pub struct WaosMetadata {
pub id: ProgramId,
pub name: String,
pub icon: PathBuf,
}
#[derive(Deserialize)]
struct Fields {
id: String,
name: String,
icon: PathBuf,
}
#[derive(Deserialize)]
struct IdField {
id: String,
}
impl WaosMetadata {
pub fn of(package: &Package) -> Result<WaosMetadata> {
let folder = package
.manifest_path
.parent()
.expect("a manifest is in a folder");
WaosMetadata::from_table(table_of(package)?, folder.as_std_path())
.with_context(|| format!("[package.metadata.waos] of {}", package.name))
}
pub fn id_of(package: &Package) -> Result<ProgramId> {
WaosMetadata::id_from_table(table_of(package)?)
.with_context(|| format!("[package.metadata.waos] of {}", package.name))
}
fn id_from_table(table: &Value) -> Result<ProgramId> {
let field: IdField = serde_json::from_value(table.clone())?;
Ok(field.id.parse()?)
}
fn from_table(table: &Value, folder: &Path) -> Result<WaosMetadata> {
let fields: Fields = serde_json::from_value(table.clone())?;
Ok(WaosMetadata {
id: fields.id.parse()?,
name: fields.name,
icon: folder.join(fields.icon),
})
}
}
fn table_of(package: &Package) -> Result<&Value> {
package
.metadata
.get("waos")
.with_context(|| missing_table(&package.name))
}
fn missing_table(package: &str) -> String {
format!(
"{package}'s Cargo.toml has no [package.metadata.waos], which a package's id, name and icon come from, like:\n\n[package.metadata.waos]\nid = \"dev.example.{package}\"\nname = \"{package}\"\nicon = \"icon.png\"\n\nThe id is reverse-DNS and names the program for good; the icon is a square PNG at least 256 pixels on a side"
)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn the_table_gives_the_id_the_name_and_the_icon_beside_the_manifest() {
let table =
json!({"id": "dev.example.triangle", "name": "Triangle", "icon": "assets/icon.png"});
let metadata = WaosMetadata::from_table(&table, Path::new("/programs/triangle")).unwrap();
assert_eq!(metadata.id.as_str(), "dev.example.triangle");
assert_eq!(metadata.name, "Triangle");
assert_eq!(
metadata.icon,
Path::new("/programs/triangle/assets/icon.png")
);
}
#[test]
fn a_missing_field_is_named() {
let table = json!({"id": "dev.example.triangle", "name": "Triangle"});
let error = WaosMetadata::from_table(&table, Path::new("/")).unwrap_err();
assert!(error.to_string().contains("`icon`"), "{error}");
}
#[test]
fn a_run_reads_the_id_alone() {
let table = json!({"id": "dev.example.triangle"});
let id = WaosMetadata::id_from_table(&table).unwrap();
assert_eq!(id.as_str(), "dev.example.triangle");
let error = WaosMetadata::id_from_table(&json!({"name": "Triangle"})).unwrap_err();
assert!(error.to_string().contains("`id`"), "{error}");
}
#[test]
fn an_id_that_is_not_reverse_dns_is_refused() {
let table = json!({"id": "triangle", "name": "Triangle", "icon": "icon.png"});
let error = WaosMetadata::from_table(&table, Path::new("/")).unwrap_err();
assert!(error.to_string().contains("reverse-DNS"), "{error}");
}
}