apollo_federation/connectors/models/
source.rs1use 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#[derive(Clone, Eq)]
27pub struct SourceName {
28 pub value: Arc<str>,
29 node: Option<Arc<Node<Value>>>,
30}
31
32impl SourceName {
33 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 #[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}