1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use crate::executable::{
    document::{Error, Path, PathRoot, Rule, Visitor},
    Cache,
};
use bluejay_core::definition::{SchemaDefinition, TypeDefinitionReference};
use bluejay_core::executable::{
    ExecutableDocument, FragmentSpread, OperationDefinition, VariableDefinition,
};
use bluejay_core::{Argument, AsIter, ObjectValue, Value, ValueReference, Variable};
use itertools::Either;
use std::collections::{BTreeMap, BTreeSet, HashMap};

pub struct AllVariableUsesDefined<'a, E: ExecutableDocument, S: SchemaDefinition> {
    fragment_references: HashMap<&'a E::FragmentDefinition, BTreeSet<PathRoot<'a, E>>>,
    variable_usages:
        BTreeMap<PathRoot<'a, E>, Vec<&'a <E::Value<false> as Value<false>>::Variable>>,
    cache: &'a Cache<'a, E, S>,
}

impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Visitor<'a, E, S>
    for AllVariableUsesDefined<'a, E, S>
{
    fn new(_: &'a E, _: &'a S, cache: &'a Cache<'a, E, S>) -> Self {
        Self {
            fragment_references: HashMap::new(),
            variable_usages: BTreeMap::new(),
            cache,
        }
    }

    fn visit_variable_argument(
        &mut self,
        argument: &'a <E as ExecutableDocument>::Argument<false>,
        _: &'a <S as SchemaDefinition>::InputValueDefinition,
        path: &Path<'a, E>,
    ) {
        self.visit_value(argument.value(), *path.root());
    }

    fn visit_fragment_spread(
        &mut self,
        fragment_spread: &'a E::FragmentSpread,
        _: TypeDefinitionReference<'a, S::TypeDefinition>,
        path: &Path<'a, E>,
    ) {
        if let Some(fragment_definition) = self.cache.fragment_definition(fragment_spread.name()) {
            self.fragment_references
                .entry(fragment_definition)
                .or_default()
                .insert(*path.root());
        }
    }
}

impl<'a, E: ExecutableDocument, S: SchemaDefinition> AllVariableUsesDefined<'a, E, S> {
    fn visit_value(
        &mut self,
        value: &'a <E as ExecutableDocument>::Value<false>,
        root: PathRoot<'a, E>,
    ) {
        match value.as_ref() {
            ValueReference::Variable(v) => {
                self.variable_usages.entry(root).or_default().push(v);
            }
            ValueReference::List(l) => l.iter().for_each(|value| self.visit_value(value, root)),
            ValueReference::Object(o) => o
                .iter()
                .for_each(|(_, value)| self.visit_value(value, root)),
            _ => {}
        }
    }

    fn operation_definitions_where_fragment_used(
        &self,
        fragment_definition: &'a E::FragmentDefinition,
    ) -> impl Iterator<Item = &'a E::OperationDefinition> {
        let mut references = BTreeSet::new();
        self.visit_fragment_references(fragment_definition, &mut references);
        references
            .into_iter()
            .filter_map(|reference| match reference {
                PathRoot::Operation(o) => Some(o),
                PathRoot::Fragment(_) => None,
            })
    }

    fn visit_fragment_references(
        &self,
        fragment_definition: &'a E::FragmentDefinition,
        visited: &mut BTreeSet<PathRoot<'a, E>>,
    ) {
        if let Some(references) = self.fragment_references.get(fragment_definition) {
            references.iter().for_each(|reference| {
                if visited.insert(*reference) {
                    if let PathRoot::Fragment(f) = reference {
                        self.visit_fragment_references(f, visited);
                    }
                }
            });
        }
    }
}

impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Rule<'a, E, S>
    for AllVariableUsesDefined<'a, E, S>
{
    type Error = Error<'a, E, S>;
    type Errors = std::vec::IntoIter<Error<'a, E, S>>;

    fn into_errors(self) -> Self::Errors {
        self.variable_usages
            .iter()
            .filter(|(_, variables)| !variables.is_empty())
            .flat_map(|(root, variables)| {
                let operation_definitions: Either<std::iter::Once<&'a E::OperationDefinition>, _> =
                    match root {
                        PathRoot::Operation(operation_definition) => {
                            Either::Left(std::iter::once(operation_definition))
                        }
                        PathRoot::Fragment(fragment_definition) => Either::Right(
                            self.operation_definitions_where_fragment_used(fragment_definition),
                        ),
                    };
                operation_definitions.flat_map(|operation_definition| {
                    variables.iter().copied().filter_map(|variable| {
                        operation_definition
                            .as_ref()
                            .variable_definitions()
                            .map_or(true, |variable_definitions| {
                                variable_definitions.iter().all(|variable_definition| {
                                    variable_definition.variable() != variable.name()
                                })
                            })
                            .then_some(Error::VariableNotDefined {
                                variable,
                                operation_definition,
                            })
                    })
                })
            })
            .collect::<Vec<Error<'a, E, S>>>()
            .into_iter()
    }
}