use std::{error::Error, fmt};
use crate::{
diagnostic::Diagnostic,
model::{
ContainerKey, EntryKind, NetworkKey, PodKey, QuadletDocument, QuadletParseResult, QuadletUnitType, SectionKind,
TypedModelError, VolumeKey,
},
source::SourceId,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryValue(String);
impl EntryValue {
pub fn new(value: impl Into<String>) -> Result<Self, RenderError> {
let value = value.into();
if value.bytes().any(|byte| matches!(byte, 0 | b'\n' | b'\r')) {
return Err(RenderError::InvalidValue);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SystemdSection {
Unit,
Service,
Install,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SystemdUnitKey {
Requires,
Wants,
After,
}
impl SystemdUnitKey {
const fn name(self) -> &'static str {
match self {
Self::Requires => "Requires",
Self::Wants => "Wants",
Self::After => "After",
}
}
}
impl SystemdSection {
const fn kind(self) -> SectionKind {
match self {
Self::Unit => SectionKind::Unit,
Self::Service => SectionKind::Service,
Self::Install => SectionKind::Install,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GeneratedEntry {
section: SectionKind,
kind: EntryKind,
key: String,
value: EntryValue,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletDocumentBuilder {
unit_type: QuadletUnitType,
entries: Vec<GeneratedEntry>,
}
impl QuadletDocumentBuilder {
#[must_use]
pub const fn new(unit_type: QuadletUnitType) -> Self {
Self {
unit_type,
entries: Vec::new(),
}
}
#[must_use]
pub const fn unit_type(&self) -> QuadletUnitType {
self.unit_type
}
pub fn push_container(&mut self, key: ContainerKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Container,
SectionKind::Container,
EntryKind::Container(key),
container_key_name(key),
value,
)
}
pub fn push_pod(&mut self, key: PodKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Pod,
SectionKind::Pod,
EntryKind::Pod(key),
pod_key_name(key),
value,
)
}
pub fn push_network(&mut self, key: NetworkKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Network,
SectionKind::Network,
EntryKind::Network(key),
network_key_name(key),
value,
)
}
pub fn push_volume(&mut self, key: VolumeKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Volume,
SectionKind::Volume,
EntryKind::Volume(key),
volume_key_name(key),
value,
)
}
pub fn push_systemd(
&mut self,
section: SystemdSection,
key: impl Into<String>,
value: EntryValue,
) -> Result<(), RenderError> {
let key = key.into();
if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return Err(RenderError::InvalidKey(key));
}
self.entries.push(GeneratedEntry {
section: section.kind(),
kind: EntryKind::GenericSystemd,
key,
value,
});
Ok(())
}
pub fn push_systemd_unit(&mut self, key: SystemdUnitKey, value: EntryValue) -> Result<(), RenderError> {
self.push_systemd(SystemdSection::Unit, key.name(), value)
}
pub fn build(&self, source_id: SourceId) -> Result<GeneratedQuadletDocument, RenderError> {
let text = self.render_text();
let parsed = QuadletDocument::parse(self.unit_type, source_id, text).map_err(RenderError::TypedModel)?;
if !parsed.is_valid() {
let mut diagnostics = parsed.syntax().diagnostics().to_vec();
diagnostics.extend_from_slice(parsed.model_diagnostics());
return Err(RenderError::InvalidDocument(diagnostics));
}
Ok(GeneratedQuadletDocument { parsed })
}
fn push_native(
&mut self,
required: QuadletUnitType,
section: SectionKind,
kind: EntryKind,
key: &'static str,
value: EntryValue,
) -> Result<(), RenderError> {
if self.unit_type != required {
return Err(RenderError::WrongUnitType {
document: self.unit_type,
entry: required,
});
}
if !kind.is_repeatable() && self.entries.iter().any(|entry| entry.kind == kind) {
return Err(RenderError::DuplicateSingleton(key.to_owned()));
}
self.entries.push(GeneratedEntry {
section,
kind,
key: key.to_owned(),
value,
});
Ok(())
}
fn render_text(&self) -> String {
let native = self.unit_type.native_section();
let sections = [SectionKind::Unit, native, SectionKind::Service, SectionKind::Install];
let mut output = String::new();
let mut wrote_section = false;
for section in sections {
let entries: Vec<_> = self.entries.iter().filter(|entry| entry.section == section).collect();
if entries.is_empty() && section != native {
continue;
}
if wrote_section {
output.push('\n');
}
wrote_section = true;
output.push('[');
output.push_str(section_name(section));
output.push_str("]\n");
for entry in entries {
output.push_str(&entry.key);
output.push('=');
output.push_str(entry.value.as_str());
output.push('\n');
}
}
output
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GeneratedQuadletDocument {
parsed: QuadletParseResult,
}
impl GeneratedQuadletDocument {
#[must_use]
pub fn text(&self) -> &str {
self.parsed.syntax().document().render_preserved()
}
#[must_use]
pub const fn document(&self) -> &QuadletDocument {
self.parsed.document()
}
#[must_use]
pub const fn parse_result(&self) -> &QuadletParseResult {
&self.parsed
}
#[must_use]
pub fn into_parse_result(self) -> QuadletParseResult {
self.parsed
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RenderError {
InvalidValue,
InvalidKey(String),
WrongUnitType {
document: QuadletUnitType,
entry: QuadletUnitType,
},
DuplicateSingleton(String),
InvalidDocument(Vec<Diagnostic>),
TypedModel(TypedModelError),
}
impl RenderError {
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
match self {
Self::InvalidDocument(diagnostics) => diagnostics,
_ => &[],
}
}
}
impl fmt::Display for RenderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidValue => formatter.write_str("generated Quadlet values must fit on one physical line"),
Self::InvalidKey(key) => write!(formatter, "invalid generic systemd key `{key}`"),
Self::WrongUnitType { document, entry } => {
write!(formatter, "cannot add a {entry:?} entry to a {document:?} document")
}
Self::DuplicateSingleton(key) => write!(formatter, "singleton Quadlet key `{key}` is repeated"),
Self::InvalidDocument(diagnostics) => {
write!(
formatter,
"generated Quadlet document has {} diagnostic(s)",
diagnostics.len()
)
}
Self::TypedModel(error) => write!(formatter, "generated Quadlet model is inconsistent: {error}"),
}
}
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::TypedModel(error) => Some(error),
_ => None,
}
}
}
const fn container_key_name(key: ContainerKey) -> &'static str {
match key {
ContainerKey::AddHost => "AddHost",
ContainerKey::Image => "Image",
ContainerKey::Exec => "Exec",
ContainerKey::Environment => "Environment",
ContainerKey::EnvironmentFile => "EnvironmentFile",
ContainerKey::PublishPort => "PublishPort",
ContainerKey::Volume => "Volume",
ContainerKey::Network => "Network",
ContainerKey::Pod => "Pod",
ContainerKey::HealthCmd => "HealthCmd",
ContainerKey::Notify => "Notify",
ContainerKey::HealthInterval => "HealthInterval",
ContainerKey::HealthRetries => "HealthRetries",
ContainerKey::HealthStartPeriod => "HealthStartPeriod",
ContainerKey::HealthTimeout => "HealthTimeout",
ContainerKey::PodmanArgs => "PodmanArgs",
ContainerKey::User => "User",
ContainerKey::Group => "Group",
ContainerKey::UserNS => "UserNS",
ContainerKey::GroupAdd => "GroupAdd",
ContainerKey::WorkingDir => "WorkingDir",
ContainerKey::ReadOnly => "ReadOnly",
}
}
const fn pod_key_name(key: PodKey) -> &'static str {
match key {
PodKey::AddHost => "AddHost",
PodKey::PodName => "PodName",
PodKey::PublishPort => "PublishPort",
PodKey::Network => "Network",
PodKey::Volume => "Volume",
}
}
const fn network_key_name(key: NetworkKey) -> &'static str {
match key {
NetworkKey::NetworkName => "NetworkName",
}
}
const fn volume_key_name(key: VolumeKey) -> &'static str {
match key {
VolumeKey::VolumeName => "VolumeName",
}
}
const fn section_name(section: SectionKind) -> &'static str {
match section {
SectionKind::Unit => "Unit",
SectionKind::Container => "Container",
SectionKind::Pod => "Pod",
SectionKind::Network => "Network",
SectionKind::Volume => "Volume",
SectionKind::Service => "Service",
SectionKind::Install => "Install",
SectionKind::Unknown => "Unknown",
}
}