use anyhow::{Result, bail};
use serde::Serialize;
use crate::db::{Db, Kind, Project};
pub const FORMS: [&str; 5] = ["cli", "desktop", "web", "library", "service"];
#[derive(Debug, Serialize)]
pub struct Registry {
pub generator: &'static str,
pub version: &'static str,
pub products: Vec<Product>,
}
#[derive(Debug, Serialize)]
pub struct Product {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub about: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mark: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accent2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<String>,
}
pub fn build(db: &Db, about: impl Fn(&Project) -> Option<String>) -> Result<Registry> {
let mut products: Vec<Product> = db
.projects()?
.into_iter()
.filter(|p| p.kind == Kind::Repo)
.map(|p| Product {
about: about(&p),
name: p.name,
mark: p.mark_code,
accent: p.accent,
accent2: p.accent2,
form: p.form,
repository: p.remote,
docs: p.docs_url,
})
.collect();
products.sort_by(|a, b| a.name.cmp(&b.name));
Ok(Registry {
generator: "rigger",
version: env!("CARGO_PKG_VERSION"),
products,
})
}
pub fn check_public(registry: &Registry) -> Result<()> {
let json = serde_json::to_string(registry)?;
for mark in ["C:\\", "c:\\", "C:/", "\\\\", "/home/", "/Users/"] {
if json.contains(mark) {
bail!("the registry would publish `{mark}`, which is a path on a machine, not a fact about a product");
}
}
for product in ®istry.products {
if let Some(repo) = &product.repository
&& !repo.starts_with("https://")
&& !repo.starts_with("git@")
{
bail!("{}: `{repo}` is not a public repository address", product.name);
}
if let Some(docs) = &product.docs
&& !docs.starts_with("https://")
{
bail!("{}: `{docs}` is not a public documentation address", product.name);
}
}
Ok(())
}
pub fn check_accent(accent: &str) -> Result<()> {
let ok = accent.len() == 7 && accent.starts_with('#') && accent[1..].chars().all(|c| c.is_ascii_hexdigit());
if !ok {
bail!("`{accent}` is not a colour; the line writes them as #RRGGBB");
}
Ok(())
}
pub fn check_code(code: &str) -> Result<()> {
if code.chars().count() != 2 || !code.chars().all(|c| c.is_ascii_lowercase()) {
bail!("`{code}` is not a mark's code; they are two lowercase letters");
}
Ok(())
}
pub fn check_form(form: &str) -> Result<()> {
if !FORMS.contains(&form) {
bail!("`{form}` is not a form this line builds; they are {}", FORMS.join(", "));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn product(name: &str) -> Product {
Product {
name: name.into(),
about: Some("A sample product".into()),
mark: Some("sa".into()),
accent: Some("#3FA873".into()),
accent2: None,
form: Some("cli".into()),
repository: Some("https://github.com/example/sample.git".into()),
docs: Some("https://example.github.io/sample/".into()),
}
}
fn registry(products: Vec<Product>) -> Registry {
Registry {
generator: "rigger",
version: "0.0.0",
products,
}
}
#[test]
fn a_public_registry_passes() {
check_public(®istry(vec![product("sample")])).unwrap();
}
#[test]
fn a_path_on_a_machine_is_refused_however_it_got_in() {
let mut p = product("sample");
p.about = Some("the one in C:\\Projects\\sample".into());
let err = check_public(®istry(vec![p])).unwrap_err().to_string();
assert!(err.contains("path on a machine"), "{err}");
let mut p = product("sample");
p.about = Some("kept under /home/someone/work".into());
assert!(check_public(®istry(vec![p])).is_err());
}
#[test]
fn a_repository_that_is_not_an_address_is_refused() {
let mut p = product("sample");
p.repository = Some("../sample".into());
let err = check_public(®istry(vec![p])).unwrap_err().to_string();
assert!(err.contains("public repository"), "{err}");
}
#[test]
fn a_product_without_a_mark_is_still_published() {
let mut p = product("sample");
p.mark = None;
p.accent = None;
let json = serde_json::to_string(®istry(vec![p])).unwrap();
assert!(json.contains("sample"), "{json}");
assert!(!json.contains("accent"), "{json}");
}
#[test]
fn the_shapes_of_a_mark_are_checked_rather_than_trusted() {
check_code("rr").unwrap();
assert!(check_code("RR").is_err(), "a code is lowercase");
assert!(check_code("rrr").is_err(), "a code is two letters");
check_accent("#8A62F0").unwrap();
assert!(check_accent("8A62F0").is_err(), "a colour carries its hash");
assert!(check_accent("#8A62F").is_err(), "six digits, not five");
assert!(check_accent("#8A62FZ").is_err(), "hex digits only");
check_form("cli").unwrap();
let err = check_form("app").unwrap_err().to_string();
assert!(err.contains("desktop"), "the error names what there is: {err}");
}
}