apollo_federation/connectors/spec/
mod.rs

1//! The GraphQL spec for Connectors. Includes parsing of directives and injection of required definitions.
2pub(crate) mod connect;
3pub(crate) mod errors;
4pub(crate) mod http;
5pub(crate) mod source;
6mod type_and_directive_specifications;
7
8use std::fmt::Display;
9use std::sync::LazyLock;
10
11use apollo_compiler::Name;
12use apollo_compiler::Schema;
13use apollo_compiler::ast::Directive;
14use apollo_compiler::name;
15use apollo_compiler::schema::Component;
16pub use connect::ConnectHTTPArguments;
17pub(crate) use connect::extract_connect_directive_arguments;
18use itertools::Itertools;
19pub use source::SourceHTTPArguments;
20pub(crate) use source::extract_source_directive_arguments;
21use strum::IntoEnumIterator;
22use strum_macros::EnumIter;
23
24use self::connect::CONNECT_DIRECTIVE_NAME_IN_SPEC;
25use self::source::SOURCE_DIRECTIVE_NAME_IN_SPEC;
26use crate::connectors::spec::type_and_directive_specifications::directive_specifications;
27use crate::connectors::spec::type_and_directive_specifications::type_specifications;
28use crate::connectors::validation::Code;
29use crate::connectors::validation::Message;
30use crate::error::FederationError;
31use crate::link::Link;
32use crate::link::Purpose;
33use crate::link::spec::APOLLO_SPEC_DOMAIN;
34use crate::link::spec::Identity;
35use crate::link::spec::Url;
36use crate::link::spec::Version;
37use crate::link::spec_definition::SpecDefinition;
38use crate::link::spec_definition::SpecDefinitions;
39use crate::schema::type_and_directive_specification::TypeAndDirectiveSpecification;
40
41const CONNECT_IDENTITY_NAME: Name = name!("connect");
42
43/// The `@link` in a subgraph which enables connectors
44#[derive(Clone, Debug)]
45pub(crate) struct ConnectLink {
46    pub(crate) spec: ConnectSpec,
47    pub(crate) source_directive_name: Name,
48    pub(crate) connect_directive_name: Name,
49    pub(crate) directive: Component<Directive>,
50    pub(crate) link: Link,
51}
52
53impl<'schema> ConnectLink {
54    /// Find the connect link, if any, and validate it.
55    /// Returns `None` if this is not a connectors subgraph.
56    ///
57    /// # Errors
58    /// - Unknown spec version
59    pub(super) fn new(schema: &'schema Schema) -> Option<Result<Self, Message>> {
60        let (link, directive) = Link::for_identity(schema, &ConnectSpec::identity())?;
61
62        let spec = match ConnectSpec::try_from(&link.url.version) {
63            Err(err) => {
64                let message = format!(
65                    "{err}; should be one of {available_versions}.",
66                    available_versions = ConnectSpec::iter().map(ConnectSpec::as_str).join(", "),
67                );
68                return Some(Err(Message {
69                    code: Code::UnknownConnectorsVersion,
70                    message,
71                    locations: directive
72                        .line_column_range(&schema.sources)
73                        .into_iter()
74                        .collect(),
75                }));
76            }
77            Ok(spec) => spec,
78        };
79        let source_directive_name = link.directive_name_in_schema(&SOURCE_DIRECTIVE_NAME_IN_SPEC);
80        let connect_directive_name = link.directive_name_in_schema(&CONNECT_DIRECTIVE_NAME_IN_SPEC);
81        Some(Ok(Self {
82            spec,
83            source_directive_name,
84            connect_directive_name,
85            directive: directive.clone(),
86            link,
87        }))
88    }
89}
90
91pub(crate) fn connect_spec_from_schema(schema: &Schema) -> Option<ConnectSpec> {
92    let connect_identity = ConnectSpec::identity();
93    Link::for_identity(schema, &connect_identity)
94        .and_then(|(link, _directive)| ConnectSpec::try_from(&link.url.version).ok())
95}
96
97impl Display for ConnectLink {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        write!(f, "{}", self.link)
100    }
101}
102
103/// The known versions of the connect spec
104#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, EnumIter)]
105pub enum ConnectSpec {
106    V0_1,
107    V0_2,
108    V0_3,
109    V0_4,
110}
111
112impl PartialOrd for ConnectSpec {
113    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
114        let self_version: Version = (*self).into();
115        let other_version: Version = (*other).into();
116        self_version.partial_cmp(&other_version)
117    }
118}
119
120impl ConnectSpec {
121    /// Returns the most recently released [`ConnectSpec`]. Used only in tests
122    /// because using it production code leads to sudden accidental upgrades.
123    #[cfg(test)]
124    pub(crate) fn latest() -> Self {
125        Self::V0_2
126    }
127
128    /// Returns the next version of the [`ConnectSpec`] to be released.
129    /// Test-only!
130    #[cfg(test)]
131    pub(crate) fn next() -> Self {
132        Self::V0_3
133    }
134
135    pub const fn as_str(self) -> &'static str {
136        match self {
137            Self::V0_1 => "0.1",
138            Self::V0_2 => "0.2",
139            Self::V0_3 => "0.3",
140            Self::V0_4 => "0.4",
141        }
142    }
143
144    pub(crate) fn identity() -> Identity {
145        Identity {
146            domain: APOLLO_SPEC_DOMAIN.to_string(),
147            name: CONNECT_IDENTITY_NAME,
148        }
149    }
150
151    pub(crate) fn url(&self) -> Url {
152        Url {
153            identity: Self::identity(),
154            version: (*self).into(),
155        }
156    }
157}
158
159impl TryFrom<&Version> for ConnectSpec {
160    type Error = String;
161    fn try_from(version: &Version) -> Result<Self, Self::Error> {
162        match (version.major, version.minor) {
163            (0, 1) => Ok(Self::V0_1),
164            (0, 2) => Ok(Self::V0_2),
165            (0, 3) => Ok(Self::V0_3),
166            (0, 4) => Ok(Self::V0_4),
167            _ => Err(format!("Unknown connect version: {version}")),
168        }
169    }
170}
171
172impl Display for ConnectSpec {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.write_str(self.as_str())
175    }
176}
177
178impl From<ConnectSpec> for Version {
179    fn from(spec: ConnectSpec) -> Self {
180        match spec {
181            ConnectSpec::V0_1 => Version { major: 0, minor: 1 },
182            ConnectSpec::V0_2 => Version { major: 0, minor: 2 },
183            ConnectSpec::V0_3 => Version { major: 0, minor: 3 },
184            ConnectSpec::V0_4 => Version { major: 0, minor: 4 },
185        }
186    }
187}
188
189pub(crate) struct ConnectSpecDefinition {
190    minimum_federation_version: Version,
191    url: Url,
192}
193
194impl ConnectSpecDefinition {
195    pub(crate) fn new(version: Version, minimum_federation_version: Version) -> Self {
196        Self {
197            url: Url {
198                identity: ConnectSpec::identity(),
199                version,
200            },
201            minimum_federation_version,
202        }
203    }
204
205    pub(crate) fn from_directive(
206        directive: &Directive,
207    ) -> Result<Option<&'static Self>, FederationError> {
208        let Some(url) = directive
209            .specified_argument_by_name("url")
210            .and_then(|a| a.as_str())
211        else {
212            return Ok(None);
213        };
214
215        let url: Url = url.parse()?;
216        if url.identity.domain != APOLLO_SPEC_DOMAIN || url.identity.name != CONNECT_IDENTITY_NAME {
217            return Ok(None);
218        }
219
220        Ok(CONNECT_VERSIONS.find(&url.version))
221    }
222}
223
224impl SpecDefinition for ConnectSpecDefinition {
225    fn url(&self) -> &Url {
226        &self.url
227    }
228
229    fn directive_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
230        directive_specifications()
231    }
232
233    fn type_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
234        type_specifications()
235    }
236
237    fn minimum_federation_version(&self) -> &Version {
238        &self.minimum_federation_version
239    }
240
241    fn purpose(&self) -> Option<Purpose> {
242        Some(Purpose::EXECUTION)
243    }
244}
245
246pub(crate) static CONNECT_VERSIONS: LazyLock<SpecDefinitions<ConnectSpecDefinition>> =
247    LazyLock::new(|| {
248        let mut definitions = SpecDefinitions::new(Identity::connect_identity());
249        definitions.add(ConnectSpecDefinition::new(
250            Version { major: 0, minor: 1 },
251            Version {
252                major: 2,
253                minor: 10,
254            },
255        ));
256        definitions.add(ConnectSpecDefinition::new(
257            Version { major: 0, minor: 2 },
258            Version {
259                major: 2,
260                minor: 11,
261            },
262        ));
263        definitions.add(ConnectSpecDefinition::new(
264            Version { major: 0, minor: 3 },
265            Version {
266                major: 2,
267                minor: 12,
268            },
269        ));
270        definitions.add(ConnectSpecDefinition::new(
271            Version { major: 0, minor: 4 },
272            Version {
273                major: 2,
274                minor: 13,
275            },
276        ));
277        definitions
278    });