1use crate::spec::Identity;
2use crate::spec::Url;
3use apollo_compiler::ast::{Directive, Value};
4use std::fmt;
5use std::str;
6use std::{collections::HashMap, sync::Arc};
7use thiserror::Error;
8
9pub const DEFAULT_LINK_NAME: &str = "link";
10pub const DEFAULT_IMPORT_SCALAR_NAME: &str = "Import";
11pub const DEFAULT_PURPOSE_ENUM_NAME: &str = "Purpose";
12
13#[derive(Error, Debug, PartialEq)]
16pub enum LinkError {
17 #[error("Invalid use of @link in schema: {0}")]
18 BootstrapError(String),
19}
20
21#[derive(Eq, PartialEq, Debug)]
22pub enum Purpose {
23 SECURITY,
24 EXECUTION,
25}
26
27impl Purpose {
28 pub fn from_ast_value(value: &Value) -> Result<Purpose, LinkError> {
29 if let Value::Enum(value) = value {
30 Ok(value.parse::<Purpose>()?)
31 } else {
32 Err(LinkError::BootstrapError(
33 "invalid `purpose` value, should be an enum".to_string(),
34 ))
35 }
36 }
37}
38
39impl str::FromStr for Purpose {
40 type Err = LinkError;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 match s {
44 "SECURITY" => Ok(Purpose::SECURITY),
45 "EXECUTION" => Ok(Purpose::EXECUTION),
46 _ => Err(LinkError::BootstrapError(format!(
47 "invalid/unrecognized `purpose` value '{}'",
48 s
49 ))),
50 }
51 }
52}
53
54impl fmt::Display for Purpose {
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 let str = match self {
57 Purpose::SECURITY => "SECURITY",
58 Purpose::EXECUTION => "EXECUTION",
59 };
60 write!(f, "{}", str)
61 }
62}
63
64#[derive(Eq, PartialEq, Debug)]
65pub struct Import {
66 pub element: String,
71
72 pub is_directive: bool,
74
75 pub alias: Option<String>,
77}
78
79impl Import {
80 pub fn from_hir_value(value: &Value) -> Result<Import, LinkError> {
81 match value {
85 Value::String(str) => {
86 let is_directive = str.starts_with('@');
87 let element = if is_directive {
88 str.strip_prefix('@').unwrap().to_string()
89 } else {
90 str.to_string()
91 };
92 Ok(Import { element, is_directive, alias: None })
93 },
94 Value::Object(fields) => {
95 let mut name: Option<String> = None;
96 let mut alias: Option<String> = None;
97 for (k, v) in fields {
98 match k.as_str() {
99 "name" => {
100 name = Some(v.as_str().ok_or_else(|| {
101 LinkError::BootstrapError("invalid value for `name` field in @link(import:) argument: must be a string".to_string())
102 })?.to_owned())
103 },
104 "as" => {
105 alias = Some(v.as_str().ok_or_else(|| {
106 LinkError::BootstrapError("invalid value for `as` field in @link(import:) argument: must be a string".to_string())
107 })?.to_owned())
108 },
109 _ => Err(LinkError::BootstrapError(format!("unknown field `{k}` in @link(import:) argument")))?
110 }
111 }
112 if let Some(element) = name {
113 let is_directive = element.starts_with('@');
114 if is_directive {
115 let element = element.strip_prefix('@').unwrap().to_string();
116 if let Some(alias_str) = alias {
117 if !alias_str.starts_with('@') {
118 Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should start with '@' since the imported name does", alias_str, element)))?
119 }
120 alias = Some(alias_str.strip_prefix('@').unwrap().to_string());
121 }
122 Ok(Import { element, is_directive, alias })
123 } else {
124 if let Some(alias) = &alias {
125 if alias.starts_with('@') {
126 Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should not start with '@' (or, if {} is a directive, then the name should start with '@')", alias, element, element)))?
127 }
128 }
129 Ok(Import { element, is_directive, alias })
130 }
131 } else {
132 Err(LinkError::BootstrapError("invalid entry in @link(import:) argument, missing mandatory `name` field".to_string()))
133 }
134 },
135 _ => Err(LinkError::BootstrapError("invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form { name: \"<importedElement>\", as: \"<alias>\" }.".to_string()))
136 }
137 }
138
139 pub fn imported_name(&self) -> &String {
140 return self.alias.as_ref().unwrap_or(&self.element);
141 }
142
143 pub fn imported_display_name(&self) -> String {
144 if self.is_directive {
145 format!("@{}", self.imported_name())
146 } else {
147 self.imported_name().clone()
148 }
149 }
150}
151
152impl fmt::Display for Import {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 if self.alias.is_some() {
155 write!(
156 f,
157 r#"{{ name: "{}", as: "{}" }}"#,
158 if self.is_directive {
159 format!("@{}", self.element)
160 } else {
161 self.element.clone()
162 },
163 self.imported_display_name()
164 )
165 } else {
166 write!(f, r#""{}""#, self.imported_display_name())
167 }
168 }
169}
170
171#[derive(Debug, Eq, PartialEq)]
172pub struct Link {
173 pub url: Url,
174 pub spec_alias: Option<String>,
175 pub imports: Vec<Arc<Import>>,
176 pub purpose: Option<Purpose>,
177}
178
179impl Link {
180 pub fn spec_name_in_schema(&self) -> &String {
181 self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
182 }
183
184 pub fn directive_name_in_schema(&self, name: &str) -> String {
185 if let Some(import) = self.imports.iter().find(|i| i.element == name) {
190 import.alias.clone().unwrap_or(name.to_string())
191 } else if name == self.url.identity.name {
192 self.spec_name_in_schema().clone()
193 } else {
194 format!("{}__{}", self.spec_name_in_schema(), name)
195 }
196 }
197
198 pub fn type_name_in_schema(&self, name: &str) -> String {
199 if let Some(import) = self.imports.iter().find(|i| i.element == name) {
202 import.alias.clone().unwrap_or(name.to_string())
203 } else {
204 format!("{}__{}", self.spec_name_in_schema(), name)
205 }
206 }
207
208 pub fn from_directive_application(directive: &Directive) -> Result<Link, LinkError> {
209 let url = directive
210 .argument_by_name("url")
211 .and_then(|arg| arg.as_str())
212 .ok_or(LinkError::BootstrapError(
213 "the `url` argument for @link is mandatory".to_string(),
214 ))?;
215 let url: Url = url.parse::<Url>().map_err(|e| {
216 LinkError::BootstrapError(format!("invalid `url` argument (reason: {})", e))
217 })?;
218 let spec_alias = directive
219 .argument_by_name("as")
220 .and_then(|arg| arg.as_str())
221 .map(|s| s.to_owned());
222 let purpose = if let Some(value) = directive.argument_by_name("for") {
223 Some(Purpose::from_ast_value(value)?)
224 } else {
225 None
226 };
227 let mut imports = Vec::new();
228 if let Some(values) = directive
229 .argument_by_name("import")
230 .and_then(|arg| arg.as_list())
231 {
232 for v in values {
233 imports.push(Arc::new(Import::from_hir_value(v)?));
234 }
235 };
236 Ok(Link {
237 url,
238 spec_alias,
239 imports,
240 purpose,
241 })
242 }
243}
244
245impl fmt::Display for Link {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 let imported_types: Vec<String> = self
248 .imports
249 .iter()
250 .map(|import| import.to_string())
251 .collect::<Vec<String>>();
252 let imports = if imported_types.is_empty() {
253 "".to_string()
254 } else {
255 format!(r#", import: [{}]"#, imported_types.join(", "))
256 };
257 let alias = self
258 .spec_alias
259 .as_ref()
260 .map(|a| format!(r#", as: "{}""#, a))
261 .unwrap_or("".to_string());
262 let purpose = self
263 .purpose
264 .as_ref()
265 .map(|p| format!(r#", for: {}"#, p))
266 .unwrap_or("".to_string());
267 write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
268 }
269}
270
271#[derive(Debug)]
272pub struct LinkedElement {
273 pub link: Arc<Link>,
274 pub import: Option<Arc<Import>>,
275}
276
277#[derive(Default, Eq, PartialEq, Debug)]
278pub struct LinksMetadata {
279 pub(crate) links: Vec<Arc<Link>>,
280 pub(crate) by_identity: HashMap<Identity, Arc<Link>>,
281 pub(crate) by_name_in_schema: HashMap<String, Arc<Link>>,
282 pub(crate) types_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
283 pub(crate) directives_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
284}
285
286impl LinksMetadata {
287 pub fn all_links(&self) -> &[Arc<Link>] {
288 return self.links.as_ref();
289 }
290
291 pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
292 return self.by_identity.get(identity).cloned();
293 }
294
295 pub fn source_link_of_type(&self, type_name: &str) -> Option<LinkedElement> {
296 if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
299 Some(LinkedElement {
300 link: Arc::clone(link),
301 import: Some(Arc::clone(import)),
302 })
303 } else {
304 type_name.split_once("__").and_then(|(spec_name, _)| {
305 self.by_name_in_schema
306 .get(spec_name)
307 .map(|link| LinkedElement {
308 link: Arc::clone(link),
309 import: None,
310 })
311 })
312 }
313 }
314
315 pub fn source_link_of_directive(&self, directive_name: &str) -> Option<LinkedElement> {
316 if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
322 return Some(LinkedElement {
323 link: Arc::clone(link),
324 import: Some(Arc::clone(import)),
325 });
326 }
327
328 if let Some(link) = self.by_name_in_schema.get(directive_name) {
329 return Some(LinkedElement {
330 link: Arc::clone(link),
331 import: None,
332 });
333 }
334
335 directive_name.split_once("__").and_then(|(spec_name, _)| {
336 self.by_name_in_schema
337 .get(spec_name)
338 .map(|link| LinkedElement {
339 link: Arc::clone(link),
340 import: None,
341 })
342 })
343 }
344}