aion-server 0.29.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
use aion_awl::{Document, TypeBody, TypeRef, builtins::BUILTIN_TYPES, doc_text};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct StudioProjection {
    pub builtins: &'static [&'static str],
    pub types: Vec<StudioType>,
    pub workers: Vec<StudioWorker>,
}

#[derive(Debug, Serialize)]
pub struct StudioType {
    pub name: String,
    pub kind: StudioTypeKind,
    /// The `///` prose the declaration carries, or `null` when it carries
    /// none. Never a synthesized sentence: an undocumented declaration reads
    /// as undocumented on every surface (ADR-001).
    pub documentation: Option<String>,
    pub fields: Vec<StudioField>,
    pub variants: Vec<StudioVariant>,
}

/// One enum variant of a projected type, with its own prose.
#[derive(Debug, Serialize)]
pub struct StudioVariant {
    pub name: String,
    /// The variant's `///` prose, or `null`.
    pub documentation: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StudioTypeKind {
    Record,
    Enum,
    Schema,
}

#[derive(Debug, Serialize)]
pub struct StudioField {
    pub name: String,
    #[serde(rename = "type")]
    pub ty: String,
    /// The field's or parameter's `///` prose, or `null`.
    pub documentation: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct StudioWorker {
    pub name: String,
    /// The `worker` block's `///` prose, or `null`.
    pub documentation: Option<String>,
    pub actions: Vec<StudioAction>,
}

#[derive(Debug, Serialize)]
pub struct StudioAction {
    pub name: String,
    /// The `action` declaration's `///` prose, or `null`.
    pub documentation: Option<String>,
    pub params: Vec<StudioField>,
    pub return_type: String,
}

pub(super) fn build(document: &Document) -> StudioProjection {
    StudioProjection {
        builtins: &BUILTIN_TYPES,
        types: document
            .types
            .iter()
            .map(|declaration| {
                let (kind, fields, variants) = match &declaration.body {
                    TypeBody::Record { fields } => (
                        StudioTypeKind::Record,
                        fields
                            .iter()
                            .map(|field| StudioField {
                                name: field.name.clone(),
                                ty: render_type(&field.ty),
                                documentation: prose(&field.docs),
                            })
                            .collect(),
                        Vec::new(),
                    ),
                    TypeBody::Enum { variants } => (
                        StudioTypeKind::Enum,
                        Vec::new(),
                        variants
                            .iter()
                            .map(|variant| StudioVariant {
                                name: variant.name.clone(),
                                documentation: prose(&variant.docs),
                            })
                            .collect(),
                    ),
                    TypeBody::SchemaInline { .. } | TypeBody::SchemaImport { .. } => {
                        (StudioTypeKind::Schema, Vec::new(), Vec::new())
                    }
                };
                StudioType {
                    name: declaration.name.clone(),
                    kind,
                    documentation: prose(&declaration.docs),
                    fields,
                    variants,
                }
            })
            .collect(),
        workers: document
            .workers
            .iter()
            .map(|worker| StudioWorker {
                name: worker.name.clone(),
                documentation: prose(&worker.docs),
                actions: worker
                    .actions
                    .iter()
                    .map(|action| StudioAction {
                        name: action.name.clone(),
                        documentation: prose(&action.docs),
                        params: action
                            .params
                            .iter()
                            .map(|parameter| StudioField {
                                name: parameter.name.clone(),
                                ty: render_type(&parameter.ty),
                                documentation: prose(&parameter.docs),
                            })
                            .collect(),
                        return_type: render_type(&action.returns),
                    })
                    .collect(),
            })
            .collect(),
    }
}

/// An author's prose, or `None` when they wrote none.
///
/// Read from the AST's own doc lines, which is the same text
/// `SemanticDeclaration.documentation` carries — one join, one answer, and
/// no lookup that could miss a field (the semantic index declares types,
/// workers and actions; a record FIELD is not one of its declarations).
fn prose(docs: &[aion_awl::DocLine]) -> Option<String> {
    let text = doc_text(docs);
    (!text.is_empty()).then_some(text)
}

fn render_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Named { name, .. } => name.clone(),
        TypeRef::List { inner, .. } => format!("[{}]", render_type(inner)),
        TypeRef::Optional { inner, .. } => format!("{}?", render_type(inner)),
    }
}

#[cfg(test)]
#[path = "studio_projection_tests.rs"]
mod studio_projection_tests;