use std::{collections::BTreeMap, error::Error, fmt};
use boxferry_model::{Identifier, ModelError, SourceId};
use quadlet_lens::{
model::{NamedQuadletDocument, QuadletDocument, QuadletDocumentSet, UnitFileName},
source::SourceId as QuadletSourceId,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletDocumentInput {
name: String,
source_id: QuadletSourceId,
text: String,
}
impl QuadletDocumentInput {
#[must_use]
pub fn new(name: impl Into<String>, source_id: QuadletSourceId, text: impl Into<String>) -> Self {
Self {
name: name.into(),
source_id,
text: text.into(),
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn source_id(&self) -> QuadletSourceId {
self.source_id
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletSource {
application_name: Identifier,
documents: QuadletDocumentSet,
source_ids: BTreeMap<QuadletSourceId, SourceId>,
fallback_source_id: SourceId,
}
impl QuadletSource {
pub fn parse(
application_name: Identifier,
inputs: impl IntoIterator<Item = QuadletDocumentInput>,
) -> Result<Self, QuadletSourceError> {
let mut documents = Vec::new();
let mut source_names = Vec::new();
for input in inputs {
let filename = UnitFileName::new(input.name.clone())
.map_err(|error| QuadletSourceError::native(input.name.clone(), error))?;
let parsed = QuadletDocument::parse(filename.unit_type(), input.source_id, input.text)
.map_err(|error| QuadletSourceError::native(input.name.clone(), error))?;
if !parsed.is_valid() {
return Err(QuadletSourceError::InvalidDocument {
name: input.name,
syntax_diagnostics: parsed.syntax().diagnostics().len(),
model_diagnostics: parsed.model_diagnostics().len(),
});
}
source_names.push((input.source_id, filename.as_str().to_owned()));
documents.push(
NamedQuadletDocument::new(filename.as_str(), parsed.document().clone())
.map_err(|error| QuadletSourceError::native(filename.as_str(), error))?,
);
}
let documents =
QuadletDocumentSet::new(documents).map_err(|error| QuadletSourceError::native("document set", error))?;
let mut source = Self::from_validated_documents(application_name, documents)?;
for (native, name) in source_names {
source = source.with_source_id(native, SourceId::new(name)?);
}
Ok(source)
}
pub fn from_validated_documents(
application_name: Identifier,
documents: QuadletDocumentSet,
) -> Result<Self, ModelError> {
let fallback_source_id = SourceId::new("quadlet-source")?;
let source_ids = documents
.documents()
.iter()
.map(|document| document.document().source_id())
.map(|source_id| {
SourceId::new(format!("quadlet-source-{}", source_id.get())).map(|neutral| (source_id, neutral))
})
.collect::<Result<_, _>>()?;
Ok(Self {
application_name,
documents,
source_ids,
fallback_source_id,
})
}
#[must_use]
pub fn with_source_id(mut self, quadlet: QuadletSourceId, neutral: SourceId) -> Self {
self.source_ids.insert(quadlet, neutral);
self
}
#[must_use]
pub const fn application_name(&self) -> &Identifier {
&self.application_name
}
#[must_use]
pub const fn documents(&self) -> &QuadletDocumentSet {
&self.documents
}
#[must_use]
pub fn source_id(&self, quadlet: QuadletSourceId) -> &SourceId {
self.source_ids.get(&quadlet).unwrap_or(&self.fallback_source_id)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum QuadletSourceError {
Native {
name: String,
reason: String,
},
InvalidDocument {
name: String,
syntax_diagnostics: usize,
model_diagnostics: usize,
},
Model(ModelError),
}
impl QuadletSourceError {
fn native(name: impl Into<String>, error: impl fmt::Display) -> Self {
Self::Native {
name: name.into(),
reason: error.to_string(),
}
}
}
impl fmt::Display for QuadletSourceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Native { name, reason } => write!(formatter, "invalid Quadlet input `{name}`: {reason}"),
Self::InvalidDocument {
name,
syntax_diagnostics,
model_diagnostics,
} => write!(
formatter,
"Quadlet input `{name}` is invalid ({syntax_diagnostics} syntax diagnostic(s), {model_diagnostics} model diagnostic(s))"
),
Self::Model(error) => write!(formatter, "invalid neutral Quadlet source identity: {error}"),
}
}
}
impl Error for QuadletSourceError {}
impl From<ModelError> for QuadletSourceError {
fn from(error: ModelError) -> Self {
Self::Model(error)
}
}