apollo_federation/connectors/models/
source.rs

1use std::fmt;
2use std::fmt::Debug;
3use std::fmt::Display;
4use std::fmt::Formatter;
5use std::hash::Hash;
6use std::ops::Range;
7use std::sync::Arc;
8
9use apollo_compiler::Name;
10use apollo_compiler::Node;
11use apollo_compiler::ast::Directive;
12use apollo_compiler::ast::Value;
13use apollo_compiler::parser::LineColumn;
14use apollo_compiler::parser::SourceMap;
15use apollo_compiler::schema::Component;
16use serde::Deserialize;
17use serde::Deserializer;
18use serde::Serialize;
19
20use crate::connectors::spec::connect::CONNECT_SOURCE_ARGUMENT_NAME;
21use crate::connectors::spec::source::SOURCE_NAME_ARGUMENT_NAME;
22use crate::connectors::validation::Code;
23use crate::connectors::validation::Message;
24
25/// The `name` argument of a `@source` directive.
26#[derive(Clone, Eq)]
27pub struct SourceName {
28    pub value: Arc<str>,
29    node: Option<Arc<Node<Value>>>,
30}
31
32impl SourceName {
33    /// Create a `SourceName`, but without checking most validations.
34    ///
35    /// Useful for speeding up parsing at runtime & tests.
36    ///
37    /// For enhanced validity checks, use [`SourceName::from_directive`]
38    pub(crate) fn from_directive_permissive(
39        directive: &Component<Directive>,
40        sources: &SourceMap,
41    ) -> Result<Self, Message> {
42        Self::parse_basics(directive, sources)
43    }
44
45    /// Cast a string into a `SourceName` for when they don't come from directives
46    #[must_use]
47    pub fn cast(name: &str) -> Self {
48        Self {
49            value: Arc::from(name),
50            node: None,
51        }
52    }
53    fn parse_basics(
54        directive: &Component<Directive>,
55        sources: &SourceMap,
56    ) -> Result<Self, Message> {
57        let coordinate = NameCoordinate {
58            directive_name: &directive.name,
59            value: None,
60        };
61        let Some(arg) = directive
62            .arguments
63            .iter()
64            .find(|arg| arg.name == SOURCE_NAME_ARGUMENT_NAME)
65        else {
66            return Err(Message {
67                code: Code::GraphQLError,
68                message: format!("The {coordinate} argument is required.",),
69                locations: directive.line_column_range(sources).into_iter().collect(),
70            });
71        };
72        let node = &arg.value;
73        let Some(str_value) = node.as_str() else {
74            return Err(Message {
75                message: format!("{coordinate} is invalid; source names must be strings.",),
76                code: Code::InvalidSourceName,
77                locations: node.line_column_range(sources).into_iter().collect(),
78            });
79        };
80        Ok(Self {
81            value: Arc::from(str_value),
82            node: Some(Arc::new(node.clone())),
83        })
84    }
85
86    pub(crate) fn from_connect(directive: &Node<Directive>) -> Option<Self> {
87        let arg = directive
88            .arguments
89            .iter()
90            .find(|arg| arg.name == CONNECT_SOURCE_ARGUMENT_NAME)?;
91        let node = &arg.value;
92        let str_value = node.as_str()?;
93        Some(Self {
94            value: Arc::from(str_value),
95            node: Some(Arc::new(node.clone())),
96        })
97    }
98    pub(crate) fn from_directive(
99        directive: &Component<Directive>,
100        sources: &SourceMap,
101    ) -> (Option<Self>, Option<Message>) {
102        let name = match Self::parse_basics(directive, sources) {
103            Ok(name) => name,
104            Err(message) => return (None, Some(message)),
105        };
106
107        let coordinate = NameCoordinate {
108            directive_name: &directive.name,
109            value: Some(name.value.clone()),
110        };
111
112        let Some(first_char) = name.value.chars().next() else {
113            let locations = name.locations(sources);
114            return (
115                Some(name),
116                Some(Message {
117                    code: Code::EmptySourceName,
118                    message: format!("The value for {coordinate} can't be empty.",),
119                    locations,
120                }),
121            );
122        };
123        let message = if !first_char.is_ascii_alphabetic() {
124            Some(Message {
125                message: format!(
126                    "{coordinate} is invalid; source names must start with an ASCII letter (a-z or A-Z)",
127                ),
128                code: Code::InvalidSourceName,
129                locations: name.locations(sources),
130            })
131        } else if name.value.len() > 64 {
132            Some(Message {
133                message: format!(
134                    "{coordinate} is invalid; source names must be 64 characters or fewer",
135                ),
136                code: Code::InvalidSourceName,
137                locations: name.locations(sources),
138            })
139        } else {
140            name.value
141                .chars()
142                .find(|c| !c.is_ascii_alphanumeric() && *c != '_' && *c != '-').map(|unacceptable| Message {
143                message: format!(
144                    "{coordinate} can't contain `{unacceptable}`; only ASCII letters, numbers, underscores, or hyphens are allowed",
145                ),
146                code: Code::InvalidSourceName,
147                locations: name.locations(sources),
148            })
149        };
150        (Some(name), message)
151    }
152
153    #[must_use]
154    pub fn as_str(&self) -> &str {
155        &self.value
156    }
157
158    pub(crate) fn locations(&self, sources: &SourceMap) -> Vec<Range<LineColumn>> {
159        self.node
160            .as_ref()
161            .map(|node| node.line_column_range(sources))
162            .into_iter()
163            .flatten()
164            .collect()
165    }
166}
167
168impl Display for SourceName {
169    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
170        write!(f, "{}", self.as_str())
171    }
172}
173
174impl Debug for SourceName {
175    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
176        Debug::fmt(&self.as_str(), f)
177    }
178}
179
180impl PartialEq<Node<Value>> for SourceName {
181    fn eq(&self, other: &Node<Value>) -> bool {
182        other
183            .as_str()
184            .is_some_and(|value| value == self.value.as_ref())
185    }
186}
187
188impl PartialEq<SourceName> for SourceName {
189    fn eq(&self, other: &SourceName) -> bool {
190        self.value == other.value
191    }
192}
193
194impl Hash for SourceName {
195    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
196        self.value.hash(state);
197    }
198}
199
200impl Serialize for SourceName {
201    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202    where
203        S: serde::Serializer,
204    {
205        serializer.serialize_str(&self.value)
206    }
207}
208
209impl<'de> Deserialize<'de> for SourceName {
210    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211    where
212        D: Deserializer<'de>,
213    {
214        let value = Arc::deserialize(deserializer)?;
215        Ok(Self { value, node: None })
216    }
217}
218
219struct NameCoordinate<'schema> {
220    directive_name: &'schema Name,
221    value: Option<Arc<str>>,
222}
223
224impl Display for NameCoordinate<'_> {
225    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
226        if let Some(value) = &self.value {
227            write!(
228                f,
229                "`@{}({SOURCE_NAME_ARGUMENT_NAME}: \"{value}\")`",
230                self.directive_name,
231            )
232        } else {
233            write!(
234                f,
235                "`@{}({SOURCE_NAME_ARGUMENT_NAME}:)`",
236                self.directive_name
237            )
238        }
239    }
240}