use merman_core::{
Engine, OperationControl, OperationLedgerError, OperationPhase, OperationResourceDomain,
OperationResourceLimitExceeded, OperationResourceOverride, OperationResourceProvenance,
ParseMetadata, ParseOptions, ParsedDiagramRender,
resources::{InputResourceLimitExceeded, InputResourcePolicy},
runtime::OperationContext,
};
use crate::render::{RenderError, ResourceLimitCause, ResourceLimitExceeded};
#[derive(Debug)]
pub(crate) struct Operation {
engine: Engine,
pub(crate) control: OperationControl,
pub(crate) context: OperationContext,
resources: InputResourcePolicy,
}
impl Operation {
pub(crate) fn begin(
engine: &Engine,
source: &str,
control: OperationControl,
resources: InputResourcePolicy,
) -> Result<Self, RenderError> {
checkpoint(&control, OperationPhase::Admission)?;
if let Err(error) = resources.check_source_bytes(source) {
return Err(terminate_input_resource_error(
&control,
OperationPhase::Admission,
error,
));
}
let context = engine.begin_operation().map_err(RenderError::from)?;
checkpoint(&control, OperationPhase::Admission)?;
Ok(Self {
engine: engine.clone(),
control,
context,
resources,
})
}
pub(crate) fn parse_render_model(
self,
source: &str,
parse_options: ParseOptions,
) -> Result<Option<SemanticArtifact>, RenderError> {
checkpoint(&self.control, OperationPhase::Parse)?;
let parsed = self
.engine
.parse_diagram_for_render_model_controlled_in_context_sync(
source,
parse_options,
&self.control,
&self.context,
);
let parsed = match parsed {
Err(cancelled) => return Err(RenderError::Cancelled(cancelled)),
Ok(result) => result.map_err(RenderError::from)?,
};
let Some(parsed) = parsed else {
return Ok(None);
};
checkpoint(&self.control, OperationPhase::Semantic)?;
if let Err(error) = self.resources.check_parsed_render(&parsed) {
checkpoint(&self.control, OperationPhase::Semantic)?;
return Err(terminate_input_resource_error(
&self.control,
OperationPhase::Semantic,
error,
));
}
checkpoint(&self.control, OperationPhase::Semantic)?;
Ok(Some(SemanticArtifact {
state: Box::new(SemanticArtifactState {
parsed,
operation: OperationExecution {
control: self.control,
#[cfg(any(feature = "svg", feature = "ascii"))]
context: self.context,
},
}),
}))
}
}
pub(crate) fn checkpoint(
control: &OperationControl,
phase: OperationPhase,
) -> Result<(), RenderError> {
control
.terminal_checkpoint_at(phase)
.map_err(operation_terminal_error)
}
fn terminate_input_resource_error(
control: &OperationControl,
phase: OperationPhase,
error: InputResourceLimitExceeded,
) -> RenderError {
let projected = ResourceLimitExceeded::from_input(error.clone());
let operation_error = OperationResourceLimitExceeded {
id: error.limit,
phase,
resource_phase: projected.phase,
limit: saturating_u64(error.max),
consumed: 0,
requested: saturating_u64(error.actual),
provenance: OperationResourceProvenance::new(
OperationResourceDomain::Input,
Some(error.profile),
error
.explicit_overrides
.iter()
.map(|override_| OperationResourceOverride {
id: override_.id.as_str(),
value: saturating_u64(override_.value),
}),
),
};
let terminal = control.terminate_resource_limit(operation_error.clone());
if terminal == OperationLedgerError::ResourceLimitExceeded(operation_error) {
return RenderError::ResourceLimitExceeded(projected);
}
operation_terminal_error(terminal)
}
pub(crate) fn operation_terminal_error(error: OperationLedgerError) -> RenderError {
match error {
OperationLedgerError::Cancelled(error) => RenderError::Cancelled(error),
OperationLedgerError::ResourceLimitExceeded(error) => {
RenderError::ResourceLimitExceeded(ResourceLimitExceeded {
id: error.id,
phase: error.resource_phase,
actual: error.consumed.saturating_add(error.requested),
maximum: error.limit,
cause: ResourceLimitCause::Ceiling,
provenance: Some(error.provenance),
})
}
OperationLedgerError::ArithmeticOverflow {
id,
resource_phase,
actual,
maximum,
provenance,
..
} => RenderError::ResourceLimitExceeded(ResourceLimitExceeded {
id,
phase: resource_phase,
actual,
maximum,
cause: ResourceLimitCause::ArithmeticOverflow,
provenance: Some(provenance),
}),
}
}
fn saturating_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
#[derive(Debug)]
pub(crate) struct OperationExecution {
pub(crate) control: OperationControl,
#[cfg(any(feature = "svg", feature = "ascii"))]
pub(crate) context: OperationContext,
}
#[derive(Debug)]
pub struct SemanticArtifact {
state: Box<SemanticArtifactState>,
}
#[derive(Debug)]
struct SemanticArtifactState {
parsed: ParsedDiagramRender,
operation: OperationExecution,
}
impl SemanticArtifact {
pub fn metadata(&self) -> &ParseMetadata {
self.state.parsed.metadata()
}
pub fn semantic_kind(&self) -> &'static str {
self.state.parsed.model().kind()
}
pub fn diagram_type(&self) -> &str {
&self.metadata().diagram_type
}
pub(crate) fn parsed(&self) -> &ParsedDiagramRender {
&self.state.parsed
}
pub(crate) fn control(&self) -> &OperationControl {
&self.state.operation.control
}
#[cfg(any(feature = "svg", feature = "ascii"))]
pub(crate) fn into_parts(self) -> (ParsedDiagramRender, OperationExecution) {
let SemanticArtifactState { parsed, operation } = *self.state;
(parsed, operation)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_provenance(domain: OperationResourceDomain) -> OperationResourceProvenance {
OperationResourceProvenance::new(
domain,
Some(merman_core::resources::ResourceProfile::Interactive),
[OperationResourceOverride {
id: "max_svg_bytes",
value: 17,
}],
)
}
#[test]
fn facade_terminal_mapper_replays_original_target_resource_metadata() {
let ceiling_control = OperationControl::new();
ceiling_control.terminate_resource_limit(OperationResourceLimitExceeded {
id: "max_svg_bytes",
phase: OperationPhase::Postprocess,
resource_phase: "svg_postprocess",
limit: 17,
consumed: 10,
requested: 8,
provenance: test_provenance(OperationResourceDomain::Render),
});
assert_resource_error(
checkpoint(&ceiling_control, OperationPhase::Postprocess)
.expect_err("the SVG ceiling must replay"),
"max_svg_bytes",
"svg_postprocess",
18,
17,
ResourceLimitCause::Ceiling,
);
ceiling_control.cancel();
assert_resource_error(
checkpoint(&ceiling_control, OperationPhase::Emit)
.expect_err("later cancellation must not replace the SVG ceiling"),
"max_svg_bytes",
"svg_postprocess",
18,
17,
ResourceLimitCause::Ceiling,
);
let overflow_control = OperationControl::new();
overflow_control.terminate_resource_overflow(
"max_ascii_output_bytes",
OperationPhase::Emit,
"ascii_output",
u64::from(u32::MAX),
123,
test_provenance(OperationResourceDomain::Ascii),
);
assert_resource_error(
checkpoint(&overflow_control, OperationPhase::Layout)
.expect_err("the ASCII overflow must replay"),
"max_ascii_output_bytes",
"ascii_output",
u64::from(u32::MAX),
123,
ResourceLimitCause::ArithmeticOverflow,
);
}
fn assert_resource_error(
error: RenderError,
id: &'static str,
phase: &'static str,
actual: u64,
maximum: u64,
cause: ResourceLimitCause,
) {
assert!(matches!(
error,
RenderError::ResourceLimitExceeded(details)
if details.id == id
&& details.phase == phase
&& details.actual == actual
&& details.maximum == maximum
&& details.cause == cause
));
}
}