use std::{
collections::BTreeMap,
fmt::Write as _,
path::{Path, PathBuf},
};
use serde::Deserialize;
use sha2::{Digest, Sha256};
pub const MANIFEST_FILE: &str = "registry.toml";
pub const MANIFEST_VERSION: u32 = 1;
pub const DEFAULT_REGISTRY: &str = "topcoat";
pub const DEFAULT_REGISTRY_CRATE: &str = "topcoat-ui-registry";
#[derive(Deserialize)]
struct Manifest {
version: u32,
#[serde(default)]
themes: BTreeMap<String, ThemeEntry>,
#[serde(default)]
components: BTreeMap<String, Entry>,
}
#[derive(Deserialize)]
struct Entry {
source: String,
#[serde(default)]
dependencies: Vec<Dependency>,
}
#[derive(Deserialize)]
struct ThemeEntry {
source: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Same(String),
Other { registry: String, name: String },
}
pub struct Registry {
dir: PathBuf,
themes: BTreeMap<String, ThemeEntry>,
components: BTreeMap<String, Entry>,
}
impl Registry {
pub fn load(dir: PathBuf) -> Result<Self, Error> {
let manifest_path = dir.join(MANIFEST_FILE);
let raw = std::fs::read_to_string(&manifest_path).map_err(|source| Error::Read {
path: manifest_path,
source,
})?;
let manifest: Manifest = toml::from_str(&raw)?;
if manifest.version > MANIFEST_VERSION {
return Err(Error::UnsupportedVersion {
found: manifest.version,
supported: MANIFEST_VERSION,
});
}
Ok(Self {
dir,
themes: manifest.themes,
components: manifest.components,
})
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.components.keys().map(String::as_str)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Component<'_>> {
self.components
.get_key_value(name)
.map(|(name, entry)| Component {
name,
entry,
dir: &self.dir,
})
}
pub fn theme_names(&self) -> impl Iterator<Item = &str> {
self.themes.keys().map(String::as_str)
}
#[must_use]
pub fn theme(&self, name: &str) -> Option<Theme<'_>> {
self.themes.get_key_value(name).map(|(name, entry)| Theme {
name,
entry,
dir: &self.dir,
})
}
}
pub struct Component<'a> {
name: &'a str,
entry: &'a Entry,
dir: &'a Path,
}
impl Component<'_> {
#[must_use]
pub fn name(&self) -> &str {
self.name
}
pub fn hash(&self) -> Result<String, Error> {
Ok(content_hash(&self.read_source()?))
}
#[must_use]
pub fn file_name(&self) -> &str {
Path::new(&self.entry.source)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&self.entry.source)
}
pub fn read_source(&self) -> Result<String, Error> {
let path = self.dir.join(&self.entry.source);
std::fs::read_to_string(&path).map_err(|source| Error::Read { path, source })
}
#[must_use]
pub fn dependencies(&self) -> &[Dependency] {
&self.entry.dependencies
}
}
pub struct Theme<'a> {
name: &'a str,
entry: &'a ThemeEntry,
dir: &'a Path,
}
impl Theme<'_> {
#[must_use]
pub fn name(&self) -> &str {
self.name
}
#[must_use]
pub fn file_name(&self) -> &'static str {
"styles.css"
}
pub fn hash(&self) -> Result<String, Error> {
Ok(content_hash(&self.read_source()?))
}
pub fn read_source(&self) -> Result<String, Error> {
let path = self.dir.join(&self.entry.source);
std::fs::read_to_string(&path).map_err(|source| Error::Read { path, source })
}
}
#[must_use]
pub fn content_hash(source: &str) -> String {
format!("sha256:{}", hex(Sha256::digest(source.as_bytes()).as_ref()))
}
fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
write!(out, "{byte:02x}").expect("writing to a String cannot fail");
}
out
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to read {path:?}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse registry manifest")]
Parse(#[from] toml::de::Error),
#[error(
"registry manifest has format version {found}, but this build supports up to {supported}"
)]
UnsupportedVersion { found: u32, supported: u32 },
}