distributed_cli 4.0.0

The `distributed` CLI for Distributed applications: contracts check/accept, scaffold projects, describe manifests, compile clients, and render schema artifacts. Also a library so other CLIs (e.g. hops) can mount its commands.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use super::*;

pub(crate) fn canonicalize_root(root: &mut ManifestRoot) -> Result<(), ClientCompileError> {
    canonicalize_arguments(
        &mut root.arguments,
        &format!("manifest root `{}` argument", root.name),
    )?;
    canonicalize_string_set(
        &mut root.dependencies,
        &format!("manifest root `{}` dependency", root.name),
    )?;
    if let Some(filter) = &mut root.filter {
        canonicalize_filter_semantics(filter)?;
    }
    if let Some(order) = &mut root.order {
        canonicalize_order_semantics(order)?;
    }
    if let Some(aggregate) = &mut root.aggregate {
        canonicalize_aggregate_semantics(aggregate)?;
    }
    Ok(())
}

pub(crate) fn validate_root_contract(
    root: &ManifestRoot,
    models: &BTreeMap<String, ManifestModel>,
) -> Result<(), ClientCompileError> {
    let expected_id = format!(
        "{}:{}",
        match root.operation {
            RootOperation::Query => "query",
            RootOperation::Subscription => "subscription",
        },
        root.name
    );
    if root.id != expected_id {
        return Err(ClientCompileError::manifest(
            "client.manifest.root_id",
            format!(
                "root `{}` id must be `{expected_id}`, received `{}`",
                root.name, root.id
            ),
        ));
    }
    validate_graphql_name(&root.name, "manifest root name")?;
    let model = models.get(&root.model).ok_or_else(|| {
        ClientCompileError::manifest(
            "client.manifest.root_model",
            format!(
                "manifest root `{}` references missing model `{}`",
                root.name, root.model
            ),
        )
    })?;
    validate_filter_argument_type(
        &root.arguments,
        model,
        &format!("manifest root `{}`", root.name),
    )?;
    validate_nonempty_strings(
        &root.dependencies,
        &format!("manifest root `{}` dependency", root.name),
    )?;
    require_dependency(
        &root.dependencies,
        &model.source_table,
        &format!("manifest root {}", root.name),
    )?;
    if root.operation == RootOperation::Subscription && root.kind != RootKind::List {
        return Err(ClientCompileError::manifest(
            "client.manifest.subscription_kind",
            format!(
                "subscription root `{}` must use list cardinality in manifest v7",
                root.name
            ),
        ));
    }
    if root.operation == RootOperation::Subscription && !root.live {
        return Err(ClientCompileError::manifest(
            "client.manifest.subscription_live",
            format!("subscription root `{}` must be marked live", root.name),
        ));
    }

    let has_filter_argument = root
        .arguments
        .iter()
        .any(|argument| argument.kind == ManifestArgumentKind::Filter);
    let has_order_argument = root
        .arguments
        .iter()
        .any(|argument| argument.kind == ManifestArgumentKind::Order);
    let has_limit_argument = root
        .arguments
        .iter()
        .any(|argument| argument.kind == ManifestArgumentKind::Limit);
    let has_offset_argument = root
        .arguments
        .iter()
        .any(|argument| argument.kind == ManifestArgumentKind::Offset);
    if root.filter.is_some() != has_filter_argument
        || root.order.is_some() != has_order_argument
        || root.pagination.is_some() != (has_limit_argument && has_offset_argument)
    {
        return Err(ClientCompileError::manifest(
            "client.manifest.root_arguments",
            format!(
                "root `{}` arguments do not match its filter/order/pagination semantics",
                root.name
            ),
        ));
    }
    if let Some(filter) = &root.filter {
        validate_filter_semantics(filter, model, models)?;
    }
    if let Some(order) = &root.order {
        validate_order_semantics(order, model)?;
    }
    match root.kind {
        RootKind::List => {
            root.filter.as_ref().ok_or_else(|| {
                ClientCompileError::manifest(
                    "client.manifest.root_filter",
                    format!("list root `{}` requires filter semantics", root.name),
                )
            })?;
            root.order.as_ref().ok_or_else(|| {
                ClientCompileError::manifest(
                    "client.manifest.root_order",
                    format!("list root `{}` requires order semantics", root.name),
                )
            })?;
            validate_pagination(
                root.pagination.as_ref().ok_or_else(|| {
                    ClientCompileError::manifest(
                        "client.manifest.root_pagination",
                        format!("list root `{}` requires pagination semantics", root.name),
                    )
                })?,
                &format!("root `{}`", root.name),
            )?;
            if root.aggregate.is_some() {
                return Err(ClientCompileError::manifest(
                    "client.manifest.root_aggregate",
                    format!(
                        "list root `{}` cannot declare aggregate semantics",
                        root.name
                    ),
                ));
            }
        }
        RootKind::ByPk => {
            if root.pagination.is_some()
                || root.aggregate.is_some()
                || root.filter.is_some()
                || root.order.is_some()
            {
                return Err(ClientCompileError::manifest(
                    "client.manifest.by_pk_semantics",
                    format!(
                        "by-pk root `{}` cannot declare filter, order, pagination, or aggregate semantics",
                        root.name
                    ),
                ));
            }
            if root.arguments.iter().any(|argument| {
                argument.kind != ManifestArgumentKind::PrimaryKey
                    || argument.nullable
                    || argument.list
            }) {
                return Err(ClientCompileError::manifest(
                    "client.manifest.by_pk_arguments",
                    format!(
                        "by-pk root `{}` may contain only non-null scalar primary-key arguments",
                        root.name
                    ),
                ));
            }
        }
        RootKind::Aggregate => {
            if root.pagination.is_some() || root.order.is_some() {
                return Err(ClientCompileError::manifest(
                    "client.manifest.aggregate_root_semantics",
                    format!(
                        "aggregate root `{}` cannot declare order or pagination semantics",
                        root.name
                    ),
                ));
            }
            validate_aggregate_semantics(
                root.aggregate.as_ref().ok_or_else(|| {
                    ClientCompileError::manifest(
                        "client.manifest.aggregate_root",
                        format!(
                            "aggregate root `{}` requires aggregate semantics",
                            root.name
                        ),
                    )
                })?,
                model,
            )?;
        }
    }
    Ok(())
}

pub(crate) fn validate_unique_arguments(
    root: &ManifestRoot,
    scalar_codecs: &BTreeMap<String, String>,
) -> Result<(), ClientCompileError> {
    validate_unique_arguments_for(
        &root.arguments,
        scalar_codecs,
        &format!("manifest root `{}`", root.name),
    )
}

pub(crate) fn validate_unique_arguments_for(
    arguments: &[ManifestArgument],
    scalar_codecs: &BTreeMap<String, String>,
    owner: &str,
) -> Result<(), ClientCompileError> {
    let mut names = BTreeSet::new();
    let mut kinds = BTreeSet::new();
    for argument in arguments {
        validate_graphql_name(&argument.name, "manifest argument")?;
        validate_graphql_name(&argument.type_name, "manifest argument type")?;
        if !names.insert(argument.name.as_str()) {
            return Err(ClientCompileError::manifest(
                "client.manifest.duplicate_argument",
                format!("{owner} repeats argument `{}`", argument.name),
            ));
        }
        if argument.kind != ManifestArgumentKind::PrimaryKey && !kinds.insert(argument.kind) {
            return Err(ClientCompileError::manifest(
                "client.manifest.duplicate_argument_kind",
                format!("{owner} repeats {:?} argument semantics", argument.kind),
            ));
        }
        if matches!(
            argument.kind,
            ManifestArgumentKind::Limit | ManifestArgumentKind::Offset
        ) && (argument.list || argument.type_name != "Int")
        {
            return Err(ClientCompileError::manifest(
                "client.manifest.pagination_argument",
                format!(
                    "{owner} pagination argument `{}` must use scalar Int",
                    argument.name
                ),
            ));
        }
        match (scalar_codecs.get(&argument.type_name), &argument.codec) {
            (Some(expected), Some(actual)) if actual == expected => {}
            (Some(expected), Some(actual)) => {
                return Err(ClientCompileError::manifest(
                    "client.manifest.argument_codec",
                    format!(
                        "{owner} argument `{}` codec `{actual}` does not match scalar `{}` inventory codec `{expected}`",
                        argument.name, argument.type_name
                    ),
                ));
            }
            (Some(_), None) => {
                return Err(ClientCompileError::manifest(
                    "client.manifest.argument_codec",
                    format!(
                        "{owner} scalar argument `{}` is missing its codec",
                        argument.name
                    ),
                ));
            }
            (None, Some(actual)) => {
                return Err(ClientCompileError::manifest(
                    "client.manifest.argument_codec",
                    format!(
                        "{owner} argument `{}` declares codec `{actual}` for non-scalar type `{}`",
                        argument.name, argument.type_name
                    ),
                ));
            }
            (None, None) => {}
        }
    }
    Ok(())
}