macroonz-compiler 0.2.0

Deterministic Rust code generation for procedural macros: plan, render, close, explain, and bind one sealed expansion from declared input.
Documentation
//! The standard roster and transition companion projection.

use super::super::names::companion_constant;
use super::super::{
    ProjectionError, Recipe, RecipeMember, RecipeRelation, RecipeRelationPayload,
    RecipeRelationPayloadKind, RecipeRelationRow, RecipeTransitionEffect, RecipeVocabulary,
};
use super::tokens::{comma_separated, comma_tokens, public, super_path, variant};
use crate::token::{
    GeneratedDelimiter, GeneratedToken, GeneratedTree, constant, decorated, documentation, group,
};

pub(super) fn companions(recipe: &Recipe) -> Result<GeneratedTree, ProjectionError> {
    let transition = recipe.transition_account();
    let mut tokens = Vec::new();
    for vocabulary in recipe.vocabularies() {
        let name = companion_constant(vocabulary.name(), "VARIANTS");
        tokens.extend(roster_constant(
            name.as_str(),
            vocabulary.name_token(),
            vocabulary.members().members(),
        )?);
    }
    for relation in recipe.relations() {
        if let Some((states, events, transition_relation)) = transition
            && relation.name() == transition_relation.name()
        {
            tokens.extend(transition_constant(states, events, relation.rows())?);
            continue;
        }
        let Some(left) = recipe.vocabulary(relation.left_vocabulary()) else {
            return Err(nothing_rendered());
        };
        let Some(right) = recipe.vocabulary(relation.right_vocabulary()) else {
            return Err(nothing_rendered());
        };
        tokens.extend(relation_constant(left, right, relation)?);
        if relation.payload_kind() != RecipeRelationPayloadKind::Unlabeled {
            tokens.extend(relation_payload_constant(relation)?);
        }
    }
    GeneratedTree::assembled(tokens).map_err(ProjectionError::Tokens)
}

fn roster_constant<'name>(
    constant_name: &str,
    vocabulary: &GeneratedToken,
    members: impl Iterator<Item = &'name RecipeMember>,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
    let sentence = "The caller-authored vocabulary variants in declared order.";
    let mut kind = vec![GeneratedToken::alone('&')];
    kind.push(group(GeneratedDelimiter::Bracket, super_path(vocabulary))?);
    let mut value = vec![GeneratedToken::alone('&')];
    value.push(group(
        GeneratedDelimiter::Bracket,
        comma_separated(
            members
                .map(|member| variant(vocabulary, member.name_token()))
                .collect(),
        ),
    )?);
    Ok(decorated(
        vec![documentation(sentence)?],
        public(),
        constant(constant_name, kind, value),
    ))
}

fn transition_constant<'rows>(
    states: &RecipeVocabulary,
    events: &RecipeVocabulary,
    rows: impl Iterator<Item = &'rows RecipeRelationRow>,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
    let mut kind = vec![GeneratedToken::alone('&')];
    let row_type = group(
        GeneratedDelimiter::Parenthesis,
        comma_separated(vec![
            super_path(states.name_token()),
            super_path(events.name_token()),
            super_path(states.name_token()),
        ]),
    )?;
    kind.push(group(GeneratedDelimiter::Bracket, vec![row_type])?);
    let rows = rows
        .map(|row| {
            let Some((_target, target_name, _effect)) = row.payload().transition_parts() else {
                return Err(ProjectionError::Render(
                    crate::render::RenderError::NothingRendered,
                ));
            };
            Ok(group(
                GeneratedDelimiter::Parenthesis,
                comma_separated(vec![
                    variant(states.name_token(), row.left_name_token()),
                    variant(events.name_token(), row.right_name_token()),
                    variant(states.name_token(), target_name),
                ]),
            )?)
        })
        .collect::<Result<Vec<_>, _>>()?;
    let value = vec![
        GeneratedToken::alone('&'),
        group(GeneratedDelimiter::Bracket, comma_tokens(rows))?,
    ];
    Ok(decorated(
        vec![documentation(
            "The informed transition rows in caller-authored order.",
        )?],
        public(),
        constant("TRANSITIONS", kind, value),
    ))
}

fn relation_constant(
    left: &RecipeVocabulary,
    right: &RecipeVocabulary,
    relation: &RecipeRelation,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
    let mut kind = vec![GeneratedToken::alone('&')];
    let row_type = group(
        GeneratedDelimiter::Parenthesis,
        comma_separated(vec![
            super_path(left.name_token()),
            super_path(right.name_token()),
        ]),
    )?;
    kind.push(group(GeneratedDelimiter::Bracket, vec![row_type])?);
    let rows = relation
        .rows()
        .map(|row| {
            group(
                GeneratedDelimiter::Parenthesis,
                comma_separated(vec![
                    variant(left.name_token(), row.left_name_token()),
                    variant(right.name_token(), row.right_name_token()),
                ]),
            )
        })
        .collect::<Result<Vec<_>, _>>()?;
    let value = vec![
        GeneratedToken::alone('&'),
        group(GeneratedDelimiter::Bracket, comma_tokens(rows))?,
    ];
    let name = companion_constant(relation.name(), "ROWS");
    Ok(decorated(
        vec![documentation(
            "The informed relation endpoint rows in caller-authored order.",
        )?],
        public(),
        constant(name.as_str(), kind, value),
    ))
}

fn relation_payload_constant(
    relation: &RecipeRelation,
) -> Result<Vec<GeneratedToken>, ProjectionError> {
    let mut kind = vec![GeneratedToken::alone('&')];
    kind.push(group(
        GeneratedDelimiter::Bracket,
        vec![GeneratedToken::alone('&'), GeneratedToken::word("str")],
    )?);
    let rows = relation
        .rows()
        .map(|row| {
            let payload = match row.payload() {
                RecipeRelationPayload::Path(path) | RecipeRelationPayload::ExactRust(path) => {
                    path.tokens().to_vec()
                }
                RecipeRelationPayload::Transition {
                    target_name,
                    effect: RecipeTransitionEffect::Path(effect),
                    ..
                } => {
                    let mut tokens = vec![target_name.clone(), GeneratedToken::alone(',')];
                    tokens.extend(effect.tokens().iter().cloned());
                    tokens
                }
                RecipeRelationPayload::Transition {
                    target_name,
                    effect:
                        RecipeTransitionEffect::ExactRust {
                            target_binding,
                            body,
                        },
                    ..
                } => {
                    let mut tokens = vec![target_name.clone(), GeneratedToken::word("with")];
                    tokens.push(group(
                        GeneratedDelimiter::Parenthesis,
                        vec![target_binding.clone()],
                    )?);
                    tokens.push(group(GeneratedDelimiter::Brace, body.tokens().to_vec())?);
                    tokens
                }
                RecipeRelationPayload::Unlabeled => return Err(nothing_rendered()),
            };
            Ok(vec![
                GeneratedToken::word("stringify"),
                GeneratedToken::alone('!'),
                group(GeneratedDelimiter::Parenthesis, payload)?,
            ])
        })
        .collect::<Result<Vec<_>, ProjectionError>>()?;
    let value = vec![
        GeneratedToken::alone('&'),
        group(GeneratedDelimiter::Bracket, comma_separated(rows))?,
    ];
    let name = companion_constant(relation.name(), "PAYLOADS");
    Ok(decorated(
        vec![documentation(
            "The exact caller-authored relation payloads as Rust spellings.",
        )?],
        public(),
        constant(name.as_str(), kind, value),
    ))
}

const fn nothing_rendered() -> ProjectionError {
    ProjectionError::Render(crate::render::RenderError::NothingRendered)
}