Skip to main content

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`].
122    pub fn latest() -> Self {
123        Self::V0_3
124    }
125
126    /// Returns the next version of the [`ConnectSpec`] to be released.
127    /// Test-only!
128    #[cfg(test)]
129    pub(crate) fn next() -> Self {
130        Self::V0_4
131    }
132
133    pub const fn as_str(self) -> &'static str {
134        match self {
135            Self::V0_1 => "0.1",
136            Self::V0_2 => "0.2",
137            Self::V0_3 => "0.3",
138            Self::V0_4 => "0.4",
139        }
140    }
141
142    pub(crate) fn identity() -> Identity {
143        Identity {
144            domain: APOLLO_SPEC_DOMAIN.to_string(),
145            name: CONNECT_IDENTITY_NAME,
146        }
147    }
148
149    pub(crate) fn url(&self) -> Url {
150        Url {
151            identity: Self::identity(),
152            version: (*self).into(),
153        }
154    }
155}
156
157impl TryFrom<&Version> for ConnectSpec {
158    type Error = String;
159    fn try_from(version: &Version) -> Result<Self, Self::Error> {
160        match (version.major, version.minor) {
161            (0, 1) => Ok(Self::V0_1),
162            (0, 2) => Ok(Self::V0_2),
163            (0, 3) => Ok(Self::V0_3),
164            (0, 4) => Ok(Self::V0_4),
165            _ => Err(format!("Unknown connect version: {version}")),
166        }
167    }
168}
169
170impl Display for ConnectSpec {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.write_str(self.as_str())
173    }
174}
175
176impl From<ConnectSpec> for Version {
177    fn from(spec: ConnectSpec) -> Self {
178        match spec {
179            ConnectSpec::V0_1 => Version { major: 0, minor: 1 },
180            ConnectSpec::V0_2 => Version { major: 0, minor: 2 },
181            ConnectSpec::V0_3 => Version { major: 0, minor: 3 },
182            ConnectSpec::V0_4 => Version { major: 0, minor: 4 },
183        }
184    }
185}
186
187pub(crate) struct ConnectSpecDefinition {
188    minimum_federation_version: Version,
189    url: Url,
190}
191
192impl ConnectSpecDefinition {
193    pub(crate) fn new(version: Version, minimum_federation_version: Version) -> Self {
194        Self {
195            url: Url {
196                identity: ConnectSpec::identity(),
197                version,
198            },
199            minimum_federation_version,
200        }
201    }
202
203    pub(crate) fn from_directive(
204        directive: &Directive,
205    ) -> Result<Option<&'static Self>, FederationError> {
206        let Some(url) = directive
207            .specified_argument_by_name("url")
208            .and_then(|a| a.as_str())
209        else {
210            return Ok(None);
211        };
212
213        let url: Url = url.parse()?;
214        if url.identity.domain != APOLLO_SPEC_DOMAIN || url.identity.name != CONNECT_IDENTITY_NAME {
215            return Ok(None);
216        }
217
218        Ok(CONNECT_VERSIONS.find(&url.version))
219    }
220}
221
222impl SpecDefinition for ConnectSpecDefinition {
223    fn url(&self) -> &Url {
224        &self.url
225    }
226
227    fn directive_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
228        directive_specifications()
229    }
230
231    fn type_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
232        type_specifications()
233    }
234
235    fn minimum_federation_version(&self) -> &Version {
236        &self.minimum_federation_version
237    }
238
239    fn purpose(&self) -> Option<Purpose> {
240        Some(Purpose::EXECUTION)
241    }
242}
243
244pub(crate) static CONNECT_VERSIONS: LazyLock<SpecDefinitions<ConnectSpecDefinition>> =
245    LazyLock::new(|| {
246        let mut definitions = SpecDefinitions::new(Identity::connect_identity());
247        definitions.add(ConnectSpecDefinition::new(
248            Version { major: 0, minor: 1 },
249            Version {
250                major: 2,
251                minor: 10,
252            },
253        ));
254        definitions.add(ConnectSpecDefinition::new(
255            Version { major: 0, minor: 2 },
256            Version {
257                major: 2,
258                minor: 11,
259            },
260        ));
261        definitions.add(ConnectSpecDefinition::new(
262            Version { major: 0, minor: 3 },
263            Version {
264                major: 2,
265                minor: 12,
266            },
267        ));
268        definitions.add_preview(ConnectSpecDefinition::new(
269            Version { major: 0, minor: 4 },
270            Version {
271                major: 2,
272                minor: 13,
273            },
274        ));
275        definitions
276    });