use crate::spec::Identity;
use crate::spec::Url;
use apollo_compiler::ast::{Directive, Value};
use std::fmt;
use std::str;
use std::{collections::HashMap, sync::Arc};
use thiserror::Error;
pub const DEFAULT_LINK_NAME: &str = "link";
pub const DEFAULT_IMPORT_SCALAR_NAME: &str = "Import";
pub const DEFAULT_PURPOSE_ENUM_NAME: &str = "Purpose";
#[derive(Error, Debug, PartialEq)]
pub enum LinkError {
#[error("Invalid use of @link in schema: {0}")]
BootstrapError(String),
}
#[derive(Eq, PartialEq, Debug)]
pub enum Purpose {
SECURITY,
EXECUTION,
}
impl Purpose {
pub fn from_ast_value(value: &Value) -> Result<Purpose, LinkError> {
if let Value::Enum(value) = value {
Ok(value.parse::<Purpose>()?)
} else {
Err(LinkError::BootstrapError(
"invalid `purpose` value, should be an enum".to_string(),
))
}
}
}
impl str::FromStr for Purpose {
type Err = LinkError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"SECURITY" => Ok(Purpose::SECURITY),
"EXECUTION" => Ok(Purpose::EXECUTION),
_ => Err(LinkError::BootstrapError(format!(
"invalid/unrecognized `purpose` value '{}'",
s
))),
}
}
}
impl fmt::Display for Purpose {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let str = match self {
Purpose::SECURITY => "SECURITY",
Purpose::EXECUTION => "EXECUTION",
};
write!(f, "{}", str)
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct Import {
pub element: String,
pub is_directive: bool,
pub alias: Option<String>,
}
impl Import {
pub fn from_hir_value(value: &Value) -> Result<Import, LinkError> {
match value {
Value::String(str) => {
let is_directive = str.starts_with('@');
let element = if is_directive {
str.strip_prefix('@').unwrap().to_string()
} else {
str.to_string()
};
Ok(Import { element, is_directive, alias: None })
},
Value::Object(fields) => {
let mut name: Option<String> = None;
let mut alias: Option<String> = None;
for (k, v) in fields {
match k.as_str() {
"name" => {
name = Some(v.as_str().ok_or_else(|| {
LinkError::BootstrapError("invalid value for `name` field in @link(import:) argument: must be a string".to_string())
})?.to_owned())
},
"as" => {
alias = Some(v.as_str().ok_or_else(|| {
LinkError::BootstrapError("invalid value for `as` field in @link(import:) argument: must be a string".to_string())
})?.to_owned())
},
_ => Err(LinkError::BootstrapError(format!("unknown field `{k}` in @link(import:) argument")))?
}
}
if let Some(element) = name {
let is_directive = element.starts_with('@');
if is_directive {
let element = element.strip_prefix('@').unwrap().to_string();
if let Some(alias_str) = alias {
if !alias_str.starts_with('@') {
Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should start with '@' since the imported name does", alias_str, element)))?
}
alias = Some(alias_str.strip_prefix('@').unwrap().to_string());
}
Ok(Import { element, is_directive, alias })
} else {
if let Some(alias) = &alias {
if alias.starts_with('@') {
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)))?
}
}
Ok(Import { element, is_directive, alias })
}
} else {
Err(LinkError::BootstrapError("invalid entry in @link(import:) argument, missing mandatory `name` field".to_string()))
}
},
_ => 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()))
}
}
pub fn imported_name(&self) -> &String {
return self.alias.as_ref().unwrap_or(&self.element);
}
pub fn imported_display_name(&self) -> String {
if self.is_directive {
format!("@{}", self.imported_name())
} else {
self.imported_name().clone()
}
}
}
impl fmt::Display for Import {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.alias.is_some() {
write!(
f,
r#"{{ name: "{}", as: "{}" }}"#,
if self.is_directive {
format!("@{}", self.element)
} else {
self.element.clone()
},
self.imported_display_name()
)
} else {
write!(f, r#""{}""#, self.imported_display_name())
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Link {
pub url: Url,
pub spec_alias: Option<String>,
pub imports: Vec<Arc<Import>>,
pub purpose: Option<Purpose>,
}
impl Link {
pub fn spec_name_in_schema(&self) -> &String {
self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
}
pub fn directive_name_in_schema(&self, name: &str) -> String {
if let Some(import) = self.imports.iter().find(|i| i.element == name) {
import.alias.clone().unwrap_or(name.to_string())
} else if name == self.url.identity.name {
self.spec_name_in_schema().clone()
} else {
format!("{}__{}", self.spec_name_in_schema(), name)
}
}
pub fn type_name_in_schema(&self, name: &str) -> String {
if let Some(import) = self.imports.iter().find(|i| i.element == name) {
import.alias.clone().unwrap_or(name.to_string())
} else {
format!("{}__{}", self.spec_name_in_schema(), name)
}
}
pub fn from_directive_application(directive: &Directive) -> Result<Link, LinkError> {
let url = directive
.argument_by_name("url")
.and_then(|arg| arg.as_str())
.ok_or(LinkError::BootstrapError(
"the `url` argument for @link is mandatory".to_string(),
))?;
let url: Url = url.parse::<Url>().map_err(|e| {
LinkError::BootstrapError(format!("invalid `url` argument (reason: {})", e))
})?;
let spec_alias = directive
.argument_by_name("as")
.and_then(|arg| arg.as_str())
.map(|s| s.to_owned());
let purpose = if let Some(value) = directive.argument_by_name("for") {
Some(Purpose::from_ast_value(value)?)
} else {
None
};
let mut imports = Vec::new();
if let Some(values) = directive
.argument_by_name("import")
.and_then(|arg| arg.as_list())
{
for v in values {
imports.push(Arc::new(Import::from_hir_value(v)?));
}
};
Ok(Link {
url,
spec_alias,
imports,
purpose,
})
}
}
impl fmt::Display for Link {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let imported_types: Vec<String> = self
.imports
.iter()
.map(|import| import.to_string())
.collect::<Vec<String>>();
let imports = if imported_types.is_empty() {
"".to_string()
} else {
format!(r#", import: [{}]"#, imported_types.join(", "))
};
let alias = self
.spec_alias
.as_ref()
.map(|a| format!(r#", as: "{}""#, a))
.unwrap_or("".to_string());
let purpose = self
.purpose
.as_ref()
.map(|p| format!(r#", for: {}"#, p))
.unwrap_or("".to_string());
write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
}
}
#[derive(Debug)]
pub struct LinkedElement {
pub link: Arc<Link>,
pub import: Option<Arc<Import>>,
}
#[derive(Default, Eq, PartialEq, Debug)]
pub struct LinksMetadata {
pub(crate) links: Vec<Arc<Link>>,
pub(crate) by_identity: HashMap<Identity, Arc<Link>>,
pub(crate) by_name_in_schema: HashMap<String, Arc<Link>>,
pub(crate) types_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
pub(crate) directives_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
}
impl LinksMetadata {
pub fn all_links(&self) -> &[Arc<Link>] {
return self.links.as_ref();
}
pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
return self.by_identity.get(identity).cloned();
}
pub fn source_link_of_type(&self, type_name: &str) -> Option<LinkedElement> {
if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
Some(LinkedElement {
link: Arc::clone(link),
import: Some(Arc::clone(import)),
})
} else {
type_name.split_once("__").and_then(|(spec_name, _)| {
self.by_name_in_schema
.get(spec_name)
.map(|link| LinkedElement {
link: Arc::clone(link),
import: None,
})
})
}
}
pub fn source_link_of_directive(&self, directive_name: &str) -> Option<LinkedElement> {
if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
return Some(LinkedElement {
link: Arc::clone(link),
import: Some(Arc::clone(import)),
});
}
if let Some(link) = self.by_name_in_schema.get(directive_name) {
return Some(LinkedElement {
link: Arc::clone(link),
import: None,
});
}
directive_name.split_once("__").and_then(|(spec_name, _)| {
self.by_name_in_schema
.get(spec_name)
.map(|link| LinkedElement {
link: Arc::clone(link),
import: None,
})
})
}
}