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 super::*;

pub(super) fn validate_reachable_fragment_graph<'ast>(
    fragments: &'ast HashMap<Name, Positioned<FragmentDefinition>>,
    document: &ClientDocument,
    selection_set: &'ast Positioned<SelectionSet>,
) -> Result<(), ClientCompileError> {
    validate_fragment_selection_set(
        fragments,
        document,
        &mut FragmentGraphState::default(),
        selection_set,
        1,
    )
}

pub(super) fn validate_fragment_selection_set<'ast>(
    fragments: &'ast HashMap<Name, Positioned<FragmentDefinition>>,
    document: &ClientDocument,
    state: &mut FragmentGraphState,
    selection_set: &'ast Positioned<SelectionSet>,
    depth: usize,
) -> Result<(), ClientCompileError> {
    check_expansion_depth(depth, document, selection_set.pos)?;
    for selection in &selection_set.node.items {
        match &selection.node {
            Selection::Field(field) => {
                if !field.node.selection_set.node.items.is_empty() {
                    validate_fragment_selection_set(
                        fragments,
                        document,
                        state,
                        &field.node.selection_set,
                        depth + 1,
                    )?;
                }
            }
            Selection::InlineFragment(fragment) => {
                validate_fragment_selection_set(
                    fragments,
                    document,
                    state,
                    &fragment.node.selection_set,
                    depth + 1,
                )?;
            }
            Selection::FragmentSpread(spread) => {
                let name = spread.node.fragment_name.node.as_str();
                let Some(definition) = fragments.get(name) else {
                    return Err(source_error(
                        "client.graphql.fragment_undefined",
                        format!("fragment spread `{name}` has no definition in this document"),
                        document,
                        spread.node.fragment_name.pos,
                    ));
                };
                if let Some(cycle_start) = state
                    .active_fragments
                    .iter()
                    .position(|active| active == name)
                {
                    let mut cycle = state.active_fragments[cycle_start..].to_vec();
                    cycle.push(name.to_string());
                    return Err(source_error(
                        "client.graphql.fragment_cycle",
                        format!("fragment expansion cycle: {}", cycle.join(" -> ")),
                        document,
                        spread.node.fragment_name.pos,
                    ));
                }
                if state.completed_fragments.contains(name) {
                    continue;
                }
                check_expansion_depth(depth + 1, document, spread.pos)?;
                state.active_fragments.push(name.to_string());
                let result = validate_fragment_selection_set(
                    fragments,
                    document,
                    state,
                    &definition.node.selection_set,
                    depth + 1,
                );
                state.active_fragments.pop();
                result?;
                state.completed_fragments.insert(name.to_string());
            }
        }
    }
    Ok(())
}

impl<'ast, 'source> FragmentExpander<'ast, 'source> {
    pub(super) fn new(
        fragments: &'ast HashMap<Name, Positioned<FragmentDefinition>>,
        document: &'source ClientDocument,
    ) -> Self {
        Self {
            fragments,
            document,
            state: ExpansionState::default(),
        }
    }

    pub(super) fn merge_object(
        &mut self,
        selection_sets: &[&'ast Positioned<SelectionSet>],
        typename: &str,
        depth: usize,
        field_owner: &str,
    ) -> Result<Vec<MergedField<'ast>>, ClientCompileError> {
        let position = selection_sets
            .first()
            .map_or_else(Pos::default, |selection_set| selection_set.pos);
        check_expansion_depth(depth, self.document, position)?;
        count_expansion_unit(&mut self.state, self.document, position)?;

        let mut fields = Vec::new();
        let mut response_keys = BTreeMap::new();
        for selection_set in selection_sets {
            expand_selection_set(
                self.fragments,
                self.document,
                &mut self.state,
                selection_set,
                typename,
                depth,
                field_owner,
                &mut fields,
                &mut response_keys,
            )?;
        }
        Ok(fields)
    }

    pub(super) fn reject_unused_fragments(&self) -> Result<(), ClientCompileError> {
        let unused = self
            .fragments
            .iter()
            .filter(|(name, _)| !self.state.used_fragments.contains(name.as_str()))
            .min_by(|left, right| {
                (left.1.pos, left.0.as_str()).cmp(&(right.1.pos, right.0.as_str()))
            });
        let Some((name, definition)) = unused else {
            return Ok(());
        };
        Err(source_error(
            "client.graphql.fragment_unused",
            format!("fragment `{name}` is not reachable from the document operation"),
            self.document,
            definition.pos,
        ))
    }
}

#[allow(clippy::too_many_arguments)]
pub(super) fn expand_selection_set<'ast>(
    fragments: &'ast HashMap<Name, Positioned<FragmentDefinition>>,
    document: &ClientDocument,
    state: &mut ExpansionState,
    selection_set: &'ast Positioned<SelectionSet>,
    typename: &str,
    depth: usize,
    field_owner: &str,
    fields: &mut Vec<MergedField<'ast>>,
    response_keys: &mut BTreeMap<String, usize>,
) -> Result<(), ClientCompileError> {
    check_expansion_depth(depth, document, selection_set.pos)?;
    for selection in &selection_set.node.items {
        count_expansion_unit(state, document, selection.pos)?;
        match &selection.node {
            Selection::Field(field) => {
                merge_field(document, field, field_owner, fields, response_keys)?
            }
            Selection::FragmentSpread(spread) => {
                reject_directives(
                    &spread.node.directives,
                    &format!(
                        "fragment spread `{}`",
                        spread.node.fragment_name.node.as_str()
                    ),
                    document,
                )?;
                let name = spread.node.fragment_name.node.as_str();
                let Some(definition) = fragments.get(name) else {
                    return Err(source_error(
                        "client.graphql.fragment_undefined",
                        format!("fragment spread `{name}` has no definition in this document"),
                        document,
                        spread.node.fragment_name.pos,
                    ));
                };
                state.used_fragments.insert(name.to_string());
                reject_directives(
                    &definition.node.directives,
                    &format!("fragment definition `{name}`"),
                    document,
                )?;
                require_fragment_type(
                    definition.node.type_condition.node.on.node.as_str(),
                    typename,
                    name,
                    document,
                    definition.node.type_condition.pos,
                )?;
                if let Some(cycle_start) = state
                    .active_fragments
                    .iter()
                    .position(|active| active == name)
                {
                    let mut cycle = state.active_fragments[cycle_start..].to_vec();
                    cycle.push(name.to_string());
                    return Err(source_error(
                        "client.graphql.fragment_cycle",
                        format!("fragment expansion cycle: {}", cycle.join(" -> ")),
                        document,
                        spread.node.fragment_name.pos,
                    ));
                }
                check_expansion_depth(depth + 1, document, spread.pos)?;
                state.active_fragments.push(name.to_string());
                let result = expand_selection_set(
                    fragments,
                    document,
                    state,
                    &definition.node.selection_set,
                    typename,
                    depth + 1,
                    field_owner,
                    fields,
                    response_keys,
                );
                state.active_fragments.pop();
                result?;
            }
            Selection::InlineFragment(fragment) => {
                reject_directives(&fragment.node.directives, "inline fragment", document)?;
                if let Some(condition) = &fragment.node.type_condition {
                    require_fragment_type(
                        condition.node.on.node.as_str(),
                        typename,
                        "inline fragment",
                        document,
                        condition.pos,
                    )?;
                }
                check_expansion_depth(depth + 1, document, fragment.pos)?;
                expand_selection_set(
                    fragments,
                    document,
                    state,
                    &fragment.node.selection_set,
                    typename,
                    depth + 1,
                    field_owner,
                    fields,
                    response_keys,
                )?;
            }
        }
    }
    Ok(())
}

pub(super) fn merge_field<'ast>(
    document: &ClientDocument,
    field: &'ast Positioned<Field>,
    field_owner: &str,
    fields: &mut Vec<MergedField<'ast>>,
    response_keys: &mut BTreeMap<String, usize>,
) -> Result<(), ClientCompileError> {
    reject_directives(&field.node.directives, field_owner, document)?;
    let response_key = field.node.response_key().node.as_str();
    let canonical_arguments = canonical_field_arguments(&field.node, document)?;
    let is_object = !field.node.selection_set.node.items.is_empty();

    if let Some(index) = response_keys.get(response_key).copied() {
        let first = &mut fields[index];
        let first_is_object = !first.selection_sets.is_empty();
        if first.first.node.name.node != field.node.name.node
            || first.canonical_arguments != canonical_arguments
            || first_is_object != is_object
        {
            let first_position = first.first.node.response_key().pos;
            return Err(source_error(
                "client.selection.conflict",
                format!(
                    "response key `{response_key}` conflicts with its first selection at {}:{}",
                    first_position.line.max(1),
                    first_position.column.max(1)
                ),
                document,
                field.node.response_key().pos,
            ));
        }
        if is_object {
            first.selection_sets.push(&field.node.selection_set);
        }
        return Ok(());
    }

    response_keys.insert(response_key.to_string(), fields.len());
    fields.push(MergedField {
        first: field,
        selection_sets: is_object
            .then_some(&field.node.selection_set)
            .into_iter()
            .collect(),
        canonical_arguments,
    });
    Ok(())
}

pub(super) fn canonical_field_arguments(
    field: &Field,
    document: &ClientDocument,
) -> Result<Vec<(String, String)>, ClientCompileError> {
    let mut arguments = field
        .arguments
        .iter()
        .map(|(name, value)| {
            Ok((
                name.node.to_string(),
                render_value(&value.node, document, value.pos)?,
            ))
        })
        .collect::<Result<Vec<_>, ClientCompileError>>()?;
    arguments.sort();
    Ok(arguments)
}

pub(super) fn require_fragment_type(
    actual: &str,
    expected: &str,
    fragment: &str,
    document: &ClientDocument,
    position: Pos,
) -> Result<(), ClientCompileError> {
    if actual == expected {
        return Ok(());
    }
    Err(source_error(
        "client.graphql.fragment_type",
        format!(
            "{fragment} has type condition `{actual}` but the current concrete type is `{expected}`"
        ),
        document,
        position,
    ))
}

pub(super) fn check_expansion_depth(
    depth: usize,
    document: &ClientDocument,
    position: Pos,
) -> Result<(), ClientCompileError> {
    if depth <= MAX_OBJECT_DEPTH {
        return Ok(());
    }
    Err(source_error(
        "client.selection.depth",
        format!("selection expansion exceeds the supported {MAX_OBJECT_DEPTH}-level depth"),
        document,
        position,
    ))
}

pub(super) fn count_expansion_unit(
    state: &mut ExpansionState,
    document: &ClientDocument,
    position: Pos,
) -> Result<(), ClientCompileError> {
    if state.expanded_units >= MAX_EXPANDED_SELECTIONS {
        return Err(source_error(
            "client.selection.expansion_bound",
            format!(
                "expanded selection exceeds the supported {MAX_EXPANDED_SELECTIONS}-unit bound"
            ),
            document,
            position,
        ));
    }
    state.expanded_units += 1;
    Ok(())
}