use std::collections::{BTreeMap, BTreeSet};
use std::{error::Error, fmt};
use crate::diagnostic::{Diagnostic, DiagnosticCode, Label, Severity};
use crate::source::{SourceId, SourceSpan};
use super::{QuadletDocument, QuadletUnitType, UnitReferenceKind, ValueKind};
const MISSING_REFERENCE: DiagnosticCode = DiagnosticCode::new("QLG0001");
const AMBIGUOUS_REFERENCE: DiagnosticCode = DiagnosticCode::new("QLG0002");
const DUPLICATE_UNIT_NAME: DiagnosticCode = DiagnosticCode::new("QLG0003");
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UnitFileName {
value: String,
unit_type: QuadletUnitType,
}
impl UnitFileName {
pub fn new(value: impl Into<String>) -> Result<Self, DocumentSetError> {
let value = value.into();
if value.is_empty() || value.contains('/') || value.contains('\\') {
return Err(DocumentSetError::InvalidUnitFileName(value));
}
let Some((stem, extension)) = value.rsplit_once('.') else {
return Err(DocumentSetError::InvalidUnitFileName(value));
};
if stem.is_empty() || extension.is_empty() {
return Err(DocumentSetError::InvalidUnitFileName(value));
}
let unit_type = QuadletUnitType::from_extension(extension)
.ok_or_else(|| DocumentSetError::UnsupportedUnitFileExtension(value.clone()))?;
Ok(Self { value, unit_type })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.value
}
#[must_use]
pub const fn unit_type(&self) -> QuadletUnitType {
self.unit_type
}
}
impl fmt::Display for UnitFileName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NamedQuadletDocument {
name: UnitFileName,
document: QuadletDocument,
}
impl NamedQuadletDocument {
pub fn new(name: impl Into<String>, document: QuadletDocument) -> Result<Self, DocumentSetError> {
let name = UnitFileName::new(name)?;
if name.unit_type() != document.unit_type() {
return Err(DocumentSetError::UnitTypeMismatch {
name: name.as_str().to_owned(),
filename_type: name.unit_type(),
document_type: document.unit_type(),
});
}
Ok(Self { name, document })
}
#[must_use]
pub const fn name(&self) -> &UnitFileName {
&self.name
}
#[must_use]
pub const fn document(&self) -> &QuadletDocument {
&self.document
}
#[must_use]
pub fn into_parts(self) -> (UnitFileName, QuadletDocument) {
(self.name, self.document)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ReferenceResolution {
Resolved {
document_index: usize,
},
Missing,
Ambiguous {
candidates: usize,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnitReference {
source_document: usize,
target_name: String,
kind: UnitReferenceKind,
span: SourceSpan,
resolution: ReferenceResolution,
}
impl UnitReference {
#[must_use]
pub const fn source_document(&self) -> usize {
self.source_document
}
#[must_use]
pub fn target_name(&self) -> &str {
&self.target_name
}
#[must_use]
pub const fn kind(&self) -> UnitReferenceKind {
self.kind
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub const fn resolution(&self) -> ReferenceResolution {
self.resolution
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DependencyEdge {
source_document: usize,
target_document: usize,
kind: UnitReferenceKind,
span: SourceSpan,
}
impl DependencyEdge {
#[must_use]
pub const fn source_document(self) -> usize {
self.source_document
}
#[must_use]
pub const fn target_document(self) -> usize {
self.target_document
}
#[must_use]
pub const fn kind(self) -> UnitReferenceKind {
self.kind
}
#[must_use]
pub const fn span(self) -> SourceSpan {
self.span
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DependencyGraph {
references: Vec<UnitReference>,
edges: Vec<DependencyEdge>,
}
impl DependencyGraph {
#[must_use]
pub fn references(&self) -> &[UnitReference] {
&self.references
}
#[must_use]
pub fn edges(&self) -> &[DependencyEdge] {
&self.edges
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.references
.iter()
.all(|reference| matches!(reference.resolution, ReferenceResolution::Resolved { .. }))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletDocumentSet {
documents: Vec<NamedQuadletDocument>,
graph: DependencyGraph,
diagnostics: Vec<Diagnostic>,
}
impl QuadletDocumentSet {
pub fn new(documents: impl IntoIterator<Item = NamedQuadletDocument>) -> Result<Self, DocumentSetError> {
let documents: Vec<_> = documents.into_iter().collect();
ensure_unique_source_ids(&documents)?;
let mut by_name: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for (index, document) in documents.iter().enumerate() {
by_name
.entry(document.name().as_str().to_owned())
.or_default()
.push(index);
}
let mut diagnostics = duplicate_name_diagnostics(&documents, &by_name);
let mut references = Vec::new();
let mut edges = Vec::new();
for (source_document, named_document) in documents.iter().enumerate() {
for entry in named_document.document().entries() {
let ValueKind::UnitReference(kind) = entry.value_kind() else {
continue;
};
let Some(target_name) = entry.unit_reference_name() else {
continue;
};
let candidates = by_name.get(target_name).map_or(&[][..], Vec::as_slice);
let resolution = match candidates {
[] => {
diagnostics.push(Diagnostic::new(
MISSING_REFERENCE,
Severity::Error,
"Quadlet unit reference has no matching document",
Label::new(
entry.value().primary().span(),
"add the referenced unit file to this document set",
),
));
ReferenceResolution::Missing
}
[target_document] => {
edges.push(DependencyEdge {
source_document,
target_document: *target_document,
kind,
span: entry.value().primary().span(),
});
ReferenceResolution::Resolved {
document_index: *target_document,
}
}
multiple => {
diagnostics.push(Diagnostic::new(
AMBIGUOUS_REFERENCE,
Severity::Error,
"Quadlet unit reference matches multiple documents",
Label::new(
entry.value().primary().span(),
"make unit-file basenames unique in this document set",
),
));
ReferenceResolution::Ambiguous {
candidates: multiple.len(),
}
}
};
references.push(UnitReference {
source_document,
target_name: target_name.to_owned(),
kind,
span: entry.value().primary().span(),
resolution,
});
}
}
Ok(Self {
documents,
graph: DependencyGraph { references, edges },
diagnostics,
})
}
#[must_use]
pub fn documents(&self) -> &[NamedQuadletDocument] {
&self.documents
}
#[must_use]
pub const fn graph(&self) -> &DependencyGraph {
&self.graph
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.graph.is_complete()
&& self
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
#[must_use]
pub fn document(&self, name: &str) -> Option<&NamedQuadletDocument> {
let mut matching = self
.documents
.iter()
.filter(|document| document.name().as_str() == name);
let first = matching.next()?;
matching.next().is_none().then_some(first)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DocumentSetError {
InvalidUnitFileName(String),
UnsupportedUnitFileExtension(String),
UnitTypeMismatch {
name: String,
filename_type: QuadletUnitType,
document_type: QuadletUnitType,
},
DuplicateSourceId(SourceId),
}
impl fmt::Display for DocumentSetError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidUnitFileName(name) => write!(formatter, "invalid Quadlet unit-file basename `{name}`"),
Self::UnsupportedUnitFileExtension(name) => {
write!(formatter, "unsupported Quadlet unit-file extension in `{name}`")
}
Self::UnitTypeMismatch {
name,
filename_type,
document_type,
} => write!(
formatter,
"Quadlet filename `{name}` implies {filename_type:?}, but the document is {document_type:?}"
),
Self::DuplicateSourceId(source_id) => {
write!(formatter, "duplicate Quadlet source identity {}", source_id.get())
}
}
}
}
impl Error for DocumentSetError {}
fn ensure_unique_source_ids(documents: &[NamedQuadletDocument]) -> Result<(), DocumentSetError> {
let mut source_ids = BTreeSet::new();
for document in documents {
let source_id = document.document().source_id();
if !source_ids.insert(source_id) {
return Err(DocumentSetError::DuplicateSourceId(source_id));
}
}
Ok(())
}
fn duplicate_name_diagnostics(
documents: &[NamedQuadletDocument],
by_name: &BTreeMap<String, Vec<usize>>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for indexes in by_name.values().filter(|indexes| indexes.len() > 1) {
for index in indexes.iter().skip(1) {
let document = &documents[*index];
diagnostics.push(Diagnostic::new(
DUPLICATE_UNIT_NAME,
Severity::Error,
"document set contains a duplicate Quadlet unit-file basename",
Label::new(
document.document().source_span(),
"give this document a unique unit-file basename",
),
));
}
}
diagnostics
}