use super::super::{
EffectiveProjection, ProjectionError, Recipe, RecipeRelation, RecipeRelationRow,
RecipeTransitionEffect, RecipeVocabulary,
};
use super::tokens::{call_variant, comma_separated, derive, public, super_path, variant};
use crate::relation::AbsencePosture;
use crate::token::{
GeneratedDelimiter, GeneratedToken, GeneratedTree, decorated, documentation, enumeration,
function_item, function_signature, group, match_arm, match_expression, result_type,
typed_parameter, unit_variant, use_item,
};
pub(super) fn dispatch(
recipe: &Recipe,
effective: &EffectiveProjection,
) -> Result<GeneratedTree, ProjectionError> {
let Some((states, events, relation)) = recipe.transition_account() else {
return Err(ProjectionError::Render(
crate::render::RenderError::NothingRendered,
));
};
if relation.requirements().absence() != Some(AbsencePosture::Refusal) {
return Err(ProjectionError::Render(
crate::render::RenderError::NothingRendered,
));
}
let refusal = decorated(
vec![
documentation("Why generated dispatch did not find an admitted transition row.")?,
derive(&["Debug", "Clone", "Copy", "PartialEq", "Eq"])?,
],
public(),
enumeration(
GeneratedToken::word("TransitionRefusal"),
Vec::new(),
Vec::new(),
vec![decorated(
vec![documentation(
"No declared transition occupies the supplied state and event seat.",
)?],
Vec::new(),
unit_variant(GeneratedToken::word("Absent")),
)],
)?,
);
let mut tokens = refusal;
tokens.extend(dispatch_function(states, events, relation, effective)?);
GeneratedTree::assembled(tokens).map_err(ProjectionError::Tokens)
}
fn dispatch_function(
states: &RecipeVocabulary,
events: &RecipeVocabulary,
relation: &RecipeRelation,
effective: &EffectiveProjection,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
let name = effective.name().unwrap_or("apply");
let parameters = vec![
typed_parameter(
vec![GeneratedToken::word("state")],
super_path(states.name_token()),
),
typed_parameter(
vec![GeneratedToken::word("event")],
super_path(events.name_token()),
),
];
let result = result_type(
super_path(states.name_token()),
vec![GeneratedToken::word("TransitionRefusal")],
);
let mut arms = Vec::new();
for row in relation.rows() {
arms.push(dispatch_arm(states, events, row)?);
}
arms.push(absent_arm()?);
let bindings = effective.dispatch_binding_tokens();
let state = bindings.map_or_else(
|| GeneratedToken::word("state"),
|bindings| bindings[0].clone(),
);
let event = bindings.map_or_else(
|| GeneratedToken::word("event"),
|bindings| bindings[1].clone(),
);
let body = match_expression(
vec![group(
GeneratedDelimiter::Parenthesis,
comma_separated(vec![vec![state], vec![event]]),
)?],
arms,
)?;
if let Some(exact) = effective.exact_rust() {
let mut tokens = exact_dispatch_vocabulary_imports(states, events, effective);
tokens.extend(exact.tokens().iter().cloned());
tokens.push(group(GeneratedDelimiter::Brace, body)?);
return Ok(tokens);
}
Ok(decorated(
vec![documentation(
"Applies one declared transition or returns typed absence.",
)?],
public(),
function_item(
function_signature(
Vec::new(),
GeneratedToken::word(name),
parameters,
Vec::new(),
Some(result),
Vec::new(),
)?,
body,
)?,
))
}
fn exact_dispatch_vocabulary_imports(
states: &RecipeVocabulary,
events: &RecipeVocabulary,
effective: &EffectiveProjection,
) -> Vec<GeneratedToken> {
let Some([import_states, import_events]) = effective.dispatch_subject_imports() else {
return Vec::new();
};
let mut imports = Vec::new();
if import_states {
imports.extend(use_item(super_path(states.name_token()), None));
}
if import_events && states.name() != events.name() {
imports.extend(use_item(super_path(events.name_token()), None));
}
imports
}
fn dispatch_arm(
states: &RecipeVocabulary,
events: &RecipeVocabulary,
row: &RecipeRelationRow,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
let Some((_target, target_name, effect)) = row.payload().transition_parts() else {
return Err(ProjectionError::Render(
crate::render::RenderError::NothingRendered,
));
};
let pattern = group(
GeneratedDelimiter::Parenthesis,
comma_separated(vec![
variant(states.name_token(), row.left_name_token()),
variant(events.name_token(), row.right_name_token()),
]),
)?;
let body = match effect {
RecipeTransitionEffect::Path(effect) => {
let mut body = effect.tokens().to_vec();
body.push(group(GeneratedDelimiter::Parenthesis, Vec::new())?);
body.push(GeneratedToken::alone(';'));
body.extend(call_variant("Ok", states.name_token(), target_name)?);
body
}
RecipeTransitionEffect::ExactRust {
target_binding,
body,
} => {
let mut exact = vec![GeneratedToken::word("let"), target_binding.clone()];
exact.push(GeneratedToken::alone('='));
exact.extend(variant(states.name_token(), target_name));
exact.push(GeneratedToken::alone(';'));
exact.extend(body.tokens().iter().cloned());
exact
}
};
Ok(match_arm(
vec![pattern],
None,
vec![group(GeneratedDelimiter::Brace, body)?],
))
}
fn absent_arm() -> Result<Vec<GeneratedToken>, ProjectionError> {
Ok(match_arm(
vec![GeneratedToken::word("_")],
None,
vec![
GeneratedToken::word("Err"),
group(
GeneratedDelimiter::Parenthesis,
vec![
GeneratedToken::word("TransitionRefusal"),
GeneratedToken::joint(':'),
GeneratedToken::alone(':'),
GeneratedToken::word("Absent"),
],
)?,
],
))
}