use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::entry::{EntryMetaType, FROZEN_FIELD, META_FIELD, META_TYPE_FIELD, RawEntry};
use crate::identifier::EntryAddress;
use crate::structural::StructuralSettings;
pub const META_FILE_NAME: &str = "meta.toml";
pub const META_FILE_SCHEMA: u32 = 1;
const META_FILE_HEADER: &str = "\
# This file is a generated Sirno lockfile.
# Sirno rewrites it from lake entry metadata when the registry changes.
# Edit the lake entries that define meta fields.
";
pub type IntrinsicFieldMap = IndexMap<String, EntryAddress>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MetaRegistry {
intrinsics: IntrinsicFieldMap,
structural: StructuralSettings,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaFile {
pub schema: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub intrinsics: Vec<MetaFieldRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub structural: Vec<MetaFieldRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetaFieldRecord {
pub field: String,
pub entry: EntryAddress,
}
impl MetaFieldRecord {
fn new(field: impl Into<String>, entry: EntryAddress) -> Self {
Self { field: field.into(), entry }
}
}
impl MetaRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn from_parts(
intrinsics: impl IntoIterator<Item = (impl Into<String>, EntryAddress)>,
structural: StructuralSettings,
) -> Self {
Self {
intrinsics: intrinsics
.into_iter()
.map(|(field, entry)| (field.into(), entry))
.collect(),
structural,
}
}
pub fn standard() -> Self {
let mut registry = Self::new();
registry.set_intrinsic_entry(
crate::entry::NAME_FIELD,
EntryAddress::new(crate::entry::NAME_FIELD)
.expect("standard name entry address is valid"),
);
registry.set_intrinsic_entry(
crate::entry::DESC_FIELD,
EntryAddress::new(crate::entry::DESC_FIELD)
.expect("standard desc entry address is valid"),
);
registry
}
pub fn from_raw_entries<'a>(entries: impl IntoIterator<Item = &'a RawEntry>) -> Self {
let mut intrinsic_entries = Vec::new();
let mut structural_entries = Vec::new();
for entry in entries {
match entry.meta_type() {
| Ok(Some(EntryMetaType::Intrinsic))
if validate_intrinsic_field_name(entry.id.as_str()).is_ok() =>
{
intrinsic_entries.push(entry.id.clone());
}
| Ok(Some(EntryMetaType::Structural))
if validate_meta_field_name(entry.id.as_str()).is_ok() =>
{
structural_entries.push(entry.id.clone());
}
| Ok(Some(_)) | Ok(None) | Err(_) => {}
}
}
intrinsic_entries.sort();
structural_entries.sort();
let mut registry = Self::new();
for entry in intrinsic_entries {
registry.set_intrinsic_entry(entry.as_str().to_owned(), entry);
}
registry.structural = StructuralSettings::from_relations(
structural_entries.into_iter().map(|entry| (entry.as_str().to_owned(), entry)),
);
registry
}
pub fn structural(&self) -> &StructuralSettings {
&self.structural
}
pub fn intrinsic_fields(&self) -> impl Iterator<Item = (&str, &EntryAddress)> {
self.intrinsics.iter().map(|(field, entry)| (field.as_str(), entry))
}
pub fn contains_intrinsic_field(&self, field: &str) -> bool {
self.intrinsics.contains_key(field)
}
pub fn contains_intrinsic_entry(&self, entry: &EntryAddress) -> bool {
self.intrinsics.values().any(|defined| defined == entry)
}
pub fn intrinsic_entry_for_field(&self, field: &str) -> Option<&EntryAddress> {
self.intrinsics.get(field)
}
pub fn set_intrinsic_entry(&mut self, field: impl Into<String>, entry: EntryAddress) -> bool {
let field = field.into();
let changed = self.intrinsics.get(&field) != Some(&entry);
self.intrinsics.insert(field, entry);
changed
}
pub fn to_file(&self) -> MetaFile {
MetaFile {
schema: META_FILE_SCHEMA,
intrinsics: self
.intrinsics
.iter()
.map(|(field, entry)| MetaFieldRecord::new(field.clone(), entry.clone()))
.collect(),
structural: self
.structural
.relations()
.map(|(field, entry)| MetaFieldRecord::new(field.to_owned(), entry.clone()))
.collect(),
}
}
pub fn to_toml(&self) -> Result<String, MetaRegistryError> {
let mut source = String::from(META_FILE_HEADER);
source
.push_str(&toml::to_string_pretty(&self.to_file()).map_err(MetaRegistryError::Render)?);
Ok(source)
}
pub fn write(&self, path: impl AsRef<Path>) -> Result<(), MetaRegistryError> {
let path = path.as_ref();
let source = self.to_toml()?;
match fs::read_to_string(path) {
| Ok(existing) if existing == source => return Ok(()),
| Ok(_) => {}
| Err(source) if source.kind() == ErrorKind::NotFound => {}
| Err(source) => {
return Err(MetaRegistryError::Read { path: path.to_path_buf(), source });
}
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|source| MetaRegistryError::CreateDirectory {
path: parent.to_path_buf(),
source,
})?;
}
fs::write(path, source)
.map_err(|source| MetaRegistryError::Write { path: path.to_path_buf(), source })
}
}
pub fn validate_intrinsic_field_name(field: &str) -> Result<(), MetaFieldNameError> {
validate_meta_field_name(field)
}
pub fn validate_meta_field_name(field: &str) -> Result<(), MetaFieldNameError> {
if field.is_empty() || field.contains('\n') || field.contains('\r') || field.contains(',') {
return Err(MetaFieldNameError::Invalid(field.to_owned()));
}
if field == META_FIELD || field == FROZEN_FIELD || field.starts_with("meta.") {
return Err(MetaFieldNameError::Reserved(field.to_owned()));
}
if field == META_TYPE_FIELD {
return Err(MetaFieldNameError::Reserved(field.to_owned()));
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum MetaFieldNameError {
#[error("meta field name must be a non-empty single-line metadata key: {0}")]
Invalid(String),
#[error("meta field name is reserved for Sirno metadata: {0}")]
Reserved(String),
}
#[derive(Debug, Error)]
pub enum MetaRegistryError {
#[error("failed to create meta registry lockfile directory {path}")]
CreateDirectory {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to render meta registry lockfile")]
Render(#[source] toml::ser::Error),
#[error("failed to read meta registry lockfile {path}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to write meta registry lockfile {path}")]
Write {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn raw_entry(id: &str, meta_type: &str) -> RawEntry {
RawEntry::from_markdown(
EntryAddress::new(id).unwrap(),
&format!(
"\
---
name: {id}
desc: Test entry.
meta.type: \"{meta_type}\"
---
Body.
"
),
)
.unwrap()
}
#[test]
fn discovers_meta_entries_in_entry_address_order() {
let registry = MetaRegistry::from_raw_entries([
&raw_entry("name", "intrinsic"),
&raw_entry("category", "structural"),
&raw_entry("desc", "intrinsic"),
&raw_entry("belongs", "structural"),
]);
let intrinsics = registry.intrinsic_fields().map(|(field, _)| field).collect::<Vec<_>>();
let structural =
registry.structural().relations().map(|(field, _)| field).collect::<Vec<_>>();
assert_eq!(intrinsics, ["desc", "name"]);
assert_eq!(structural, ["belongs", "category"]);
}
#[test]
fn renders_generated_registry_lockfile_toml() {
let registry = MetaRegistry::from_raw_entries([
&raw_entry("name", "intrinsic"),
&raw_entry("category", "structural"),
]);
let source = registry.to_toml().unwrap();
let parsed: MetaFile = toml::from_str(&source).unwrap();
assert_eq!(parsed.schema, META_FILE_SCHEMA);
assert_eq!(parsed.intrinsics[0].field, "name");
assert_eq!(parsed.structural[0].field, "category");
assert!(source.starts_with("# This file is a generated Sirno lockfile."));
}
}