1use std::collections::BTreeMap;
3use std::fmt;
4use std::hash::Hash;
5use std::io;
6use std::io::BufReader;
7use std::iter;
8use std::net::IpAddr;
9use std::net::SocketAddr;
10use std::num::NonZeroU32;
11use std::num::NonZeroUsize;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use connector::ConnectorConfiguration;
17use derivative::Derivative;
18use displaydoc::Display;
19use itertools::Either;
20use itertools::Itertools;
21use once_cell::sync::Lazy;
22pub(crate) use persisted_queries::PersistedQueries;
23pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache;
24#[cfg(test)]
25pub(crate) use persisted_queries::PersistedQueriesSafelist;
26use regex::Regex;
27use rustls::ServerConfig;
28use rustls::pki_types::CertificateDer;
29use rustls::pki_types::PrivateKeyDer;
30use schemars::JsonSchema;
31use schemars::Schema;
32use schemars::SchemaGenerator;
33use serde::Deserialize;
34use serde::Deserializer;
35use serde::Serialize;
36use serde_json::Map;
37use serde_json::Value;
38use sha2::Digest;
39use thiserror::Error;
40
41use self::cors::Cors;
42use self::expansion::Expansion;
43pub(crate) use self::experimental::Discussed;
44pub(crate) use self::schema::generate_config_schema;
45pub(crate) use self::schema::generate_upgrade;
46pub(crate) use self::schema::validate_yaml_configuration;
47use self::server::Server;
48use self::subgraph::SubgraphConfiguration;
49use crate::ApolloRouterError;
50use crate::cache::DEFAULT_CACHE_CAPACITY;
51use crate::configuration::cooperative_cancellation::CooperativeCancellation;
52use crate::configuration::mode::Mode;
53use crate::graphql;
54use crate::plugin::plugins;
55use crate::plugins::chaos;
56use crate::plugins::chaos::Config;
57use crate::plugins::healthcheck::Config as HealthCheck;
58#[cfg(test)]
59use crate::plugins::healthcheck::test_listen;
60use crate::plugins::limits;
61use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
62use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
63use crate::plugins::subscription::SubscriptionConfig;
64use crate::plugins::subscription::notification::Notify;
65use crate::uplink::UplinkConfig;
66
67pub(crate) mod connector;
68pub(crate) mod cooperative_cancellation;
69pub(crate) mod cors;
70pub(crate) mod expansion;
71mod experimental;
72pub(crate) mod metrics;
73pub(crate) mod mode;
74mod persisted_queries;
75pub(crate) mod schema;
76pub(crate) mod server;
77pub(crate) mod shared;
78pub(crate) mod subgraph;
79#[cfg(test)]
80mod tests;
81mod upgrade;
82mod yaml;
83
84static HEARTBEAT_TIMEOUT_DURATION_SECONDS: u64 = 15;
86
87static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
88 Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
89 .expect("this regex to check the path is valid")
90});
91
92static TRAILING_SLASH: Lazy<Regex> =
93 Lazy::new(|| Regex::new(r"/+$").expect("this regex to check the path has no trailing slash"));
94
95#[derive(Debug, Error, Display)]
97#[non_exhaustive]
98pub enum ConfigurationError {
99 CannotExpandVariable { key: String, cause: String },
101 UnknownExpansionMode {
103 key: String,
104 supported_modes: String,
105 },
106 PluginUnknown(String),
108 PluginConfiguration { plugin: String, error: String },
110 InvalidConfiguration {
112 message: &'static str,
113 error: String,
114 },
115 DeserializeConfigError(serde_json::Error),
117
118 InvalidExpansionModeConfig,
120
121 MigrationFailure { error: String },
123
124 CertificateAuthorities { error: String },
126}
127
128impl From<proteus::Error> for ConfigurationError {
129 fn from(error: proteus::Error) -> Self {
130 Self::MigrationFailure {
131 error: error.to_string(),
132 }
133 }
134}
135
136impl From<proteus::parser::Error> for ConfigurationError {
137 fn from(error: proteus::parser::Error) -> Self {
138 Self::MigrationFailure {
139 error: error.to_string(),
140 }
141 }
142}
143
144#[derive(Clone, Derivative, Serialize, JsonSchema)]
149#[derivative(Debug)]
150pub struct Configuration {
152 #[serde(skip)]
154 pub(crate) validated_yaml: Option<Value>,
155
156 #[serde(skip)]
158 pub(crate) raw_yaml: Option<Arc<str>>,
159
160 #[serde(default)]
162 pub(crate) reload: Reload,
163
164 #[serde(default)]
166 pub(crate) health_check: HealthCheck,
167
168 #[serde(default)]
170 pub(crate) sandbox: Sandbox,
171
172 #[serde(default)]
174 pub(crate) homepage: Homepage,
175
176 #[serde(default)]
178 pub(crate) server: Server,
179
180 #[serde(default)]
182 pub(crate) supergraph: Supergraph,
183
184 #[serde(default)]
186 pub(crate) cors: Cors,
187
188 #[serde(default)]
189 pub(crate) tls: Tls,
190
191 #[serde(default)]
193 pub(crate) apq: Apq,
194
195 #[serde(default)]
197 pub persisted_queries: PersistedQueries,
198
199 #[serde(default)]
201 pub(crate) limits: limits::Config,
202
203 #[serde(default)]
206 pub(crate) experimental_chaos: Config,
207
208 #[serde(default)]
210 pub(crate) plugins: UserPlugins,
211
212 #[serde(default)]
214 #[serde(flatten)]
215 pub(crate) apollo_plugins: ApolloPlugins,
216
217 #[serde(skip)]
219 pub uplink: Option<UplinkConfig>,
220
221 #[serde(default, skip_serializing, skip_deserializing)]
224 pub(crate) notify: Notify<String, graphql::Response>,
225
226 #[serde(default)]
228 pub(crate) batching: Batching,
229
230 #[serde(default)]
232 pub(crate) experimental_type_conditioned_fetching: bool,
233
234 #[serde(default)]
239 pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
240}
241
242impl PartialEq for Configuration {
243 fn eq(&self, other: &Self) -> bool {
244 self.validated_yaml == other.validated_yaml
245 }
246}
247
248impl<'de> serde::Deserialize<'de> for Configuration {
249 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
250 where
251 D: serde::Deserializer<'de>,
252 {
253 #[derive(Deserialize, Default)]
256 #[serde(default)]
257 struct AdHocConfiguration {
258 reload: Reload,
259 health_check: HealthCheck,
260 sandbox: Sandbox,
261 homepage: Homepage,
262 server: Server,
263 supergraph: Supergraph,
264 cors: Cors,
265 plugins: UserPlugins,
266 #[serde(flatten)]
267 apollo_plugins: ApolloPlugins,
268 tls: Tls,
269 apq: Apq,
270 persisted_queries: PersistedQueries,
271 limits: limits::Config,
272 experimental_chaos: chaos::Config,
273 batching: Batching,
274 experimental_type_conditioned_fetching: bool,
275 experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
276 }
277 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
278
279 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
280 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
281
282 ad_hoc.apollo_plugins.plugins.insert(
285 "limits".to_string(),
286 serde_json::to_value(&ad_hoc.limits).unwrap(),
287 );
288 ad_hoc.apollo_plugins.plugins.insert(
289 "health_check".to_string(),
290 serde_json::to_value(&ad_hoc.health_check).unwrap(),
291 );
292
293 Configuration {
295 reload: ad_hoc.reload,
296 health_check: ad_hoc.health_check,
297 sandbox: ad_hoc.sandbox,
298 homepage: ad_hoc.homepage,
299 server: ad_hoc.server,
300 supergraph: ad_hoc.supergraph,
301 cors: ad_hoc.cors,
302 tls: ad_hoc.tls,
303 apq: ad_hoc.apq,
304 persisted_queries: ad_hoc.persisted_queries,
305 limits: ad_hoc.limits,
306 experimental_chaos: ad_hoc.experimental_chaos,
307 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
308 experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
309 plugins: ad_hoc.plugins,
310 apollo_plugins: ad_hoc.apollo_plugins,
311 batching: ad_hoc.batching,
312
313 notify,
315 uplink: None,
316 validated_yaml: None,
317 raw_yaml: None,
318 }
319 .validate()
320 .map_err(|e| serde::de::Error::custom(e.to_string()))
321 }
322}
323
324pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
325
326fn default_graphql_listen() -> ListenAddr {
327 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
328}
329
330#[cfg(test)]
331#[buildstructor::buildstructor]
332impl Configuration {
333 #[builder]
334 pub(crate) fn new(
335 supergraph: Option<Supergraph>,
336 health_check: Option<HealthCheck>,
337 sandbox: Option<Sandbox>,
338 homepage: Option<Homepage>,
339 cors: Option<Cors>,
340 plugins: Map<String, Value>,
341 apollo_plugins: Map<String, Value>,
342 tls: Option<Tls>,
343 apq: Option<Apq>,
344 persisted_query: Option<PersistedQueries>,
345 operation_limits: Option<limits::Config>,
346 chaos: Option<chaos::Config>,
347 uplink: Option<UplinkConfig>,
348 experimental_type_conditioned_fetching: Option<bool>,
349 experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
350 batching: Option<Batching>,
351 server: Option<Server>,
352 ) -> Result<Self, ConfigurationError> {
353 let notify = Self::notify(&apollo_plugins)?;
354
355 let conf = Self {
356 validated_yaml: Default::default(),
357 raw_yaml: None,
358 reload: Default::default(),
359 supergraph: supergraph.unwrap_or_default(),
360 server: server.unwrap_or_default(),
361 health_check: health_check.unwrap_or_default(),
362 sandbox: sandbox.unwrap_or_default(),
363 homepage: homepage.unwrap_or_default(),
364 cors: cors.unwrap_or_default(),
365 apq: apq.unwrap_or_default(),
366 persisted_queries: persisted_query.unwrap_or_default(),
367 limits: operation_limits.unwrap_or_default(),
368 experimental_chaos: chaos.unwrap_or_default(),
369 plugins: UserPlugins {
370 plugins: Some(plugins),
371 },
372 apollo_plugins: ApolloPlugins {
373 plugins: apollo_plugins,
374 },
375 tls: tls.unwrap_or_default(),
376 uplink,
377 batching: batching.unwrap_or_default(),
378 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
379 .unwrap_or_default(),
380 experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
381 notify,
382 };
383
384 conf.validate()
385 }
386}
387
388impl Configuration {
389 pub(crate) fn hash(&self) -> String {
390 let mut hasher = sha2::Sha256::new();
391 let defaulted_raw = self
392 .validated_yaml
393 .as_ref()
394 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
395 .unwrap_or_default();
396 hasher.update(defaulted_raw);
397 let hash: String = format!("{:x}", hasher.finalize());
398 hash
399 }
400
401 fn notify(
402 apollo_plugins: &Map<String, Value>,
403 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
404 if cfg!(test) {
405 return Ok(Notify::for_tests());
406 }
407 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
408 Some(plugin_conf) => {
409 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
410 .map_err(|err| ConfigurationError::PluginConfiguration {
411 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
412 error: format!("{err:?}"),
413 })?;
414 conf.queue_capacity
415 }
416 None => None,
417 };
418 Ok(Notify::builder()
419 .and_queue_size(notify_queue_cap)
420 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
421 .heartbeat_error_message(
422 graphql::Response::builder()
423 .errors(vec![
424 graphql::Error::builder()
425 .message("the connection has been closed because it hasn't heartbeat for a while")
426 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
427 .build()
428 ])
429 .build()
430 ).build())
431 }
432
433 pub(crate) fn rust_query_planner_config(
434 &self,
435 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
436 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
437 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
438 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
439
440 let max_evaluated_plans = self
441 .supergraph
442 .query_planning
443 .experimental_plans_limit
444 .and_then(NonZeroU32::new)
446 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
447
448 QueryPlannerConfig {
449 subgraph_graphql_validation: false,
450 generate_query_fragments: self.supergraph.generate_query_fragments,
451 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
452 enable_defer: self.supergraph.defer_support,
453 },
454 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
455 debug: QueryPlannerDebugConfig {
456 max_evaluated_plans,
457 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
458 },
459 }
460 }
461
462 fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
463 self.apollo_plugins
464 .plugins
465 .get(plugin_name)
466 .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
467 .and_then(|enabled| enabled.as_bool())
468 .unwrap_or(false)
469 }
470}
471
472impl Default for Configuration {
473 fn default() -> Self {
474 Configuration::from_str("").expect("default configuration must be valid")
476 }
477}
478
479#[cfg(test)]
480#[buildstructor::buildstructor]
481impl Configuration {
482 #[builder]
483 pub(crate) fn fake_new(
484 supergraph: Option<Supergraph>,
485 health_check: Option<HealthCheck>,
486 sandbox: Option<Sandbox>,
487 homepage: Option<Homepage>,
488 cors: Option<Cors>,
489 plugins: Map<String, Value>,
490 apollo_plugins: Map<String, Value>,
491 tls: Option<Tls>,
492 notify: Option<Notify<String, graphql::Response>>,
493 apq: Option<Apq>,
494 persisted_query: Option<PersistedQueries>,
495 operation_limits: Option<limits::Config>,
496 chaos: Option<chaos::Config>,
497 uplink: Option<UplinkConfig>,
498 batching: Option<Batching>,
499 experimental_type_conditioned_fetching: Option<bool>,
500 server: Option<Server>,
501 ) -> Result<Self, ConfigurationError> {
502 let configuration = Self {
503 validated_yaml: Default::default(),
504 reload: Default::default(),
505 server: server.unwrap_or_default(),
506 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
507 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
508 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
509 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
510 cors: cors.unwrap_or_default(),
511 limits: operation_limits.unwrap_or_default(),
512 experimental_chaos: chaos.unwrap_or_default(),
513 plugins: UserPlugins {
514 plugins: Some(plugins),
515 },
516 apollo_plugins: ApolloPlugins {
517 plugins: apollo_plugins,
518 },
519 tls: tls.unwrap_or_default(),
520 notify: notify.unwrap_or_default(),
521 apq: apq.unwrap_or_default(),
522 persisted_queries: persisted_query.unwrap_or_default(),
523 uplink,
524 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
525 .unwrap_or_default(),
526 experimental_hoist_orphan_errors: Default::default(),
527 batching: batching.unwrap_or_default(),
528 raw_yaml: None,
529 };
530
531 configuration.validate()
532 }
533}
534
535impl Configuration {
536 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
537 if self.sandbox.enabled && self.homepage.enabled {
539 return Err(ConfigurationError::InvalidConfiguration {
540 message: "sandbox and homepage cannot be enabled at the same time",
541 error: "disable the homepage if you want to enable sandbox".to_string(),
542 });
543 }
544 if self.sandbox.enabled && !self.supergraph.introspection {
546 return Err(ConfigurationError::InvalidConfiguration {
547 message: "sandbox requires introspection",
548 error: "sandbox needs introspection to be enabled".to_string(),
549 });
550 }
551 if !self.supergraph.path.starts_with('/') {
552 return Err(ConfigurationError::InvalidConfiguration {
553 message: "invalid 'server.graphql_path' configuration",
554 error: format!(
555 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
556 self.supergraph.path, self.supergraph.path
557 ),
558 });
559 }
560 if self.supergraph.path.ends_with('*')
561 && !self.supergraph.path.ends_with("/*")
562 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
563 {
564 return Err(ConfigurationError::InvalidConfiguration {
565 message: "invalid 'server.graphql_path' configuration",
566 error: format!(
567 "'{}' is invalid, you can only set a wildcard after a '/'",
568 self.supergraph.path
569 ),
570 });
571 }
572 if self.supergraph.path.contains("/*/") {
573 return Err(ConfigurationError::InvalidConfiguration {
574 message: "invalid 'server.graphql_path' configuration",
575 error: format!(
576 "'{}' is invalid, if you need to set a path like '/*/graphql' then specify it as a path parameter with a name, for example '/:my_project_key/graphql'",
577 self.supergraph.path
578 ),
579 });
580 }
581
582 if self.persisted_queries.enabled {
584 if self.persisted_queries.safelist.enabled && self.apq.enabled {
585 return Err(ConfigurationError::InvalidConfiguration {
586 message: "apqs must be disabled to enable safelisting",
587 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
588 });
589 } else if !self.persisted_queries.safelist.enabled
590 && self.persisted_queries.safelist.require_id
591 {
592 return Err(ConfigurationError::InvalidConfiguration {
593 message: "safelist must be enabled to require IDs",
594 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
595 });
596 }
597 } else {
598 if self.persisted_queries.safelist.enabled {
600 return Err(ConfigurationError::InvalidConfiguration {
601 message: "persisted queries must be enabled to enable safelisting",
602 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
603 });
604 } else if self.persisted_queries.log_unknown {
605 return Err(ConfigurationError::InvalidConfiguration {
606 message: "persisted queries must be enabled to enable logging unknown operations",
607 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
608 });
609 }
610 }
611
612 if self.apollo_plugin_enabled("response_cache")
614 && self.apollo_plugin_enabled("preview_entity_cache")
615 {
616 return Err(ConfigurationError::InvalidConfiguration {
617 message: "entity cache and response cache features are mutually exclusive",
618 error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
619 });
620 }
621
622 Ok(self)
623 }
624}
625
626impl FromStr for Configuration {
628 type Err = ConfigurationError;
629
630 fn from_str(s: &str) -> Result<Self, Self::Err> {
631 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
632 .validate()
633 }
634}
635
636fn gen_schema(
637 plugins: BTreeMap<String, Schema>,
638 hidden_plugins: Option<BTreeMap<String, Schema>>,
639) -> Schema {
640 schemars::json_schema!({
641 "type": "object",
642 "properties": plugins,
643 "additionalProperties": false,
644 "patternProperties": hidden_plugins
645 .unwrap_or_default()
646 .into_iter()
647 .map(|(k, v)| (format!("^{}$", k), v))
649 .collect::<BTreeMap<_, _>>()
650 })
651}
652
653#[derive(Clone, Debug, Default, Deserialize, Serialize)]
659#[serde(transparent)]
660pub(crate) struct ApolloPlugins {
661 pub(crate) plugins: Map<String, Value>,
662}
663
664impl JsonSchema for ApolloPlugins {
665 fn schema_name() -> std::borrow::Cow<'static, str> {
666 stringify!(Plugins).into()
667 }
668
669 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
670 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
674 .sorted_by_key(|factory| factory.name.clone())
675 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
676 .partition_map(|factory| {
677 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
678 let schema = factory.create_schema(generator);
679 if factory.hidden_from_config_json_schema {
681 Either::Right((key, schema))
682 } else {
683 Either::Left((key, schema))
684 }
685 });
686 gen_schema(
687 plugin_entries.into_iter().collect(),
688 Some(hidden_plugin_entries.into_iter().collect()),
689 )
690 }
691}
692
693#[derive(Clone, Debug, Default, Deserialize, Serialize)]
698#[serde(transparent)]
699pub(crate) struct UserPlugins {
700 pub(crate) plugins: Option<Map<String, Value>>,
701}
702
703impl JsonSchema for UserPlugins {
704 fn schema_name() -> std::borrow::Cow<'static, str> {
705 stringify!(Plugins).into()
706 }
707
708 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
709 let plugins = crate::plugin::plugins()
713 .sorted_by_key(|factory| factory.name.clone())
714 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
715 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
716 .collect();
717 gen_schema(plugins, None)
718 }
719}
720
721#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
723#[serde(deny_unknown_fields)]
724#[serde(default)]
725pub(crate) struct Supergraph {
726 pub(crate) listen: ListenAddr,
729
730 #[serde(deserialize_with = "humantime_serde::deserialize")]
732 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
733 pub(crate) connection_shutdown_timeout: Duration,
734
735 pub(crate) path: String,
738
739 pub(crate) introspection: bool,
742
743 pub(crate) redact_query_validation_errors: bool,
747
748 pub(crate) generate_query_fragments: bool,
751
752 pub(crate) defer_support: bool,
754
755 pub(crate) query_planning: QueryPlanning,
757
758 pub(crate) early_cancel: bool,
763
764 pub(crate) enable_result_coercion_errors: bool,
771
772 pub(crate) experimental_log_on_broken_pipe: bool,
775
776 pub(crate) strict_variable_validation: Mode,
780}
781
782const fn default_generate_query_fragments() -> bool {
783 true
784}
785
786fn default_defer_support() -> bool {
787 true
788}
789
790#[buildstructor::buildstructor]
791impl Supergraph {
792 #[builder]
793 pub(crate) fn new(
794 listen: Option<ListenAddr>,
795 path: Option<String>,
796 connection_shutdown_timeout: Option<Duration>,
797 introspection: Option<bool>,
798 defer_support: Option<bool>,
799 query_planning: Option<QueryPlanning>,
800 generate_query_fragments: Option<bool>,
801 early_cancel: Option<bool>,
802 experimental_log_on_broken_pipe: Option<bool>,
803 insert_result_coercion_errors: Option<bool>,
804 strict_variable_validation: Option<Mode>,
805 redact_query_validation_errors: Option<bool>,
806 ) -> Self {
807 Self {
808 listen: listen.unwrap_or_else(default_graphql_listen),
809 path: path.unwrap_or_else(default_graphql_path),
810 connection_shutdown_timeout: connection_shutdown_timeout
811 .unwrap_or_else(default_connection_shutdown_timeout),
812 introspection: introspection.unwrap_or_else(default_graphql_introspection),
813 defer_support: defer_support.unwrap_or_else(default_defer_support),
814 query_planning: query_planning.unwrap_or_default(),
815 generate_query_fragments: generate_query_fragments
816 .unwrap_or_else(default_generate_query_fragments),
817 early_cancel: early_cancel.unwrap_or_default(),
818 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
819 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
820 strict_variable_validation: strict_variable_validation
821 .unwrap_or_else(default_strict_variable_validation),
822 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
823 }
824 }
825}
826
827#[cfg(test)]
828#[buildstructor::buildstructor]
829impl Supergraph {
830 #[builder]
831 pub(crate) fn fake_new(
832 listen: Option<ListenAddr>,
833 path: Option<String>,
834 connection_shutdown_timeout: Option<Duration>,
835 introspection: Option<bool>,
836 defer_support: Option<bool>,
837 query_planning: Option<QueryPlanning>,
838 generate_query_fragments: Option<bool>,
839 early_cancel: Option<bool>,
840 experimental_log_on_broken_pipe: Option<bool>,
841 insert_result_coercion_errors: Option<bool>,
842 strict_variable_validation: Option<Mode>,
843 redact_query_validation_errors: Option<bool>,
844 ) -> Self {
845 Self {
846 listen: listen.unwrap_or_else(test_listen),
847 path: path.unwrap_or_else(default_graphql_path),
848 connection_shutdown_timeout: connection_shutdown_timeout
849 .unwrap_or_else(default_connection_shutdown_timeout),
850 introspection: introspection.unwrap_or_else(default_graphql_introspection),
851 defer_support: defer_support.unwrap_or_else(default_defer_support),
852 query_planning: query_planning.unwrap_or_default(),
853 generate_query_fragments: generate_query_fragments
854 .unwrap_or_else(default_generate_query_fragments),
855 early_cancel: early_cancel.unwrap_or_default(),
856 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
857 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
858 strict_variable_validation: strict_variable_validation
859 .unwrap_or_else(default_strict_variable_validation),
860 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
861 }
862 }
863}
864
865impl Default for Supergraph {
866 fn default() -> Self {
867 Self::builder().build()
868 }
869}
870
871impl Supergraph {
872 pub(crate) fn sanitized_path(&self) -> String {
874 let mut path = self.path.clone();
875 if self.path.ends_with("/*") {
876 path = format!("{}router_extra_path", self.path);
878 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
879 let new_path = SUPERGRAPH_ENDPOINT_REGEX
880 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
881 path = new_path.to_string();
882 } else if TRAILING_SLASH.is_match(&self.path) {
883 let new_path = TRAILING_SLASH.replace(&self.path, "");
884 if new_path.is_empty() {
885 path = "/".to_string();
886 } else {
887 path = new_path.to_string();
888 }
889 }
890
891 path
892 }
893}
894
895#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
897#[serde(deny_unknown_fields)]
898pub(crate) struct Router {
899 #[serde(default)]
900 pub(crate) cache: Cache,
901}
902
903#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
905#[serde(deny_unknown_fields, default)]
906pub(crate) struct Apq {
907 pub(crate) enabled: bool,
909
910 pub(crate) router: Router,
911
912 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
913}
914
915#[cfg(test)]
916#[buildstructor::buildstructor]
917impl Apq {
918 #[builder]
919 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
920 Self {
921 enabled: enabled.unwrap_or_else(default_apq),
922 ..Default::default()
923 }
924 }
925}
926
927#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
929#[serde(deny_unknown_fields, default)]
930pub(crate) struct SubgraphApq {
931 pub(crate) enabled: bool,
933}
934
935fn default_apq() -> bool {
936 true
937}
938
939impl Default for Apq {
940 fn default() -> Self {
941 Self {
942 enabled: default_apq(),
943 router: Default::default(),
944 subgraph: Default::default(),
945 }
946 }
947}
948
949#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
951#[serde(deny_unknown_fields, default)]
952pub(crate) struct QueryPlanning {
953 pub(crate) cache: QueryPlanCache,
955 pub(crate) warmed_up_queries: Option<usize>,
960
961 pub(crate) experimental_plans_limit: Option<u32>,
969
970 pub(crate) experimental_paths_limit: Option<u32>,
983
984 pub(crate) experimental_reuse_query_plans: bool,
987
988 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
992}
993
994#[buildstructor::buildstructor]
995impl QueryPlanning {
996 #[builder]
997 #[allow(dead_code)]
998 pub(crate) fn new(
999 cache: Option<QueryPlanCache>,
1000 warmed_up_queries: Option<usize>,
1001 experimental_plans_limit: Option<u32>,
1002 experimental_paths_limit: Option<u32>,
1003 experimental_reuse_query_plans: Option<bool>,
1004 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
1005 ) -> Self {
1006 Self {
1007 cache: cache.unwrap_or_default(),
1008 warmed_up_queries,
1009 experimental_plans_limit,
1010 experimental_paths_limit,
1011 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
1012 experimental_cooperative_cancellation: experimental_cooperative_cancellation
1013 .unwrap_or_default(),
1014 }
1015 }
1016}
1017
1018#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1020#[serde(deny_unknown_fields, default)]
1021pub(crate) struct QueryPlanCache {
1022 pub(crate) in_memory: InMemoryCache,
1024 pub(crate) redis: Option<QueryPlanRedisCache>,
1026}
1027
1028#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1029#[serde(deny_unknown_fields)]
1030pub(crate) struct QueryPlanRedisCache {
1032 pub(crate) urls: Vec<url::Url>,
1034
1035 pub(crate) username: Option<String>,
1037 pub(crate) password: Option<String>,
1039
1040 #[serde(
1041 deserialize_with = "humantime_serde::deserialize",
1042 default = "default_timeout"
1043 )]
1044 #[schemars(with = "Option<String>", default)]
1045 pub(crate) timeout: Duration,
1047
1048 #[serde(
1049 deserialize_with = "humantime_serde::deserialize",
1050 default = "default_query_plan_cache_ttl"
1051 )]
1052 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1053 pub(crate) ttl: Duration,
1055
1056 pub(crate) namespace: Option<String>,
1058
1059 #[serde(default)]
1060 pub(crate) tls: Option<TlsClient>,
1062
1063 #[serde(default = "default_required_to_start")]
1064 pub(crate) required_to_start: bool,
1066
1067 #[serde(default = "default_reset_ttl")]
1068 pub(crate) reset_ttl: bool,
1070
1071 #[serde(default = "default_query_planner_cache_pool_size")]
1072 pub(crate) pool_size: u32,
1074}
1075
1076fn default_query_plan_cache_ttl() -> Duration {
1077 Duration::from_secs(86400 * 30)
1079}
1080
1081fn default_query_planner_cache_pool_size() -> u32 {
1082 1
1083}
1084
1085#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1087#[serde(deny_unknown_fields, default)]
1088pub(crate) struct Cache {
1089 pub(crate) in_memory: InMemoryCache,
1091 pub(crate) redis: Option<RedisCache>,
1093}
1094
1095impl From<QueryPlanCache> for Cache {
1096 fn from(value: QueryPlanCache) -> Self {
1097 Cache {
1098 in_memory: value.in_memory,
1099 redis: value.redis.map(Into::into),
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1105#[serde(deny_unknown_fields)]
1106pub(crate) struct InMemoryCache {
1108 pub(crate) limit: NonZeroUsize,
1110}
1111
1112impl Default for InMemoryCache {
1113 fn default() -> Self {
1114 Self {
1115 limit: DEFAULT_CACHE_CAPACITY,
1116 }
1117 }
1118}
1119
1120#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1121#[serde(deny_unknown_fields)]
1122pub(crate) struct RedisCache {
1124 pub(crate) urls: Vec<url::Url>,
1126
1127 pub(crate) username: Option<String>,
1129 pub(crate) password: Option<String>,
1131
1132 #[serde(
1133 deserialize_with = "humantime_serde::deserialize",
1134 default = "default_timeout"
1135 )]
1136 #[schemars(with = "Option<String>", default)]
1137 pub(crate) timeout: Duration,
1139
1140 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1141 #[schemars(with = "Option<String>", default)]
1142 pub(crate) ttl: Option<Duration>,
1144
1145 pub(crate) namespace: Option<String>,
1147
1148 #[serde(default)]
1149 pub(crate) tls: Option<TlsClient>,
1151
1152 #[serde(default = "default_required_to_start")]
1153 pub(crate) required_to_start: bool,
1155
1156 #[serde(default = "default_reset_ttl")]
1157 pub(crate) reset_ttl: bool,
1159
1160 #[serde(default = "default_pool_size")]
1161 pub(crate) pool_size: u32,
1163 #[serde(
1164 deserialize_with = "humantime_serde::deserialize",
1165 default = "default_metrics_interval"
1166 )]
1167 #[schemars(with = "Option<String>", default)]
1168 pub(crate) metrics_interval: Duration,
1170}
1171
1172fn default_timeout() -> Duration {
1173 Duration::from_millis(500)
1174}
1175
1176pub(crate) fn default_required_to_start() -> bool {
1177 false
1178}
1179
1180pub(crate) fn default_pool_size() -> u32 {
1181 1
1182}
1183
1184pub(crate) fn default_metrics_interval() -> Duration {
1185 Duration::from_secs(1)
1186}
1187
1188impl From<QueryPlanRedisCache> for RedisCache {
1189 fn from(value: QueryPlanRedisCache) -> Self {
1190 RedisCache {
1191 urls: value.urls,
1192 username: value.username,
1193 password: value.password,
1194 timeout: value.timeout,
1195 ttl: Some(value.ttl),
1196 namespace: value.namespace,
1197 tls: value.tls,
1198 required_to_start: value.required_to_start,
1199 reset_ttl: value.reset_ttl,
1200 pool_size: value.pool_size,
1201 metrics_interval: default_metrics_interval(),
1202 }
1203 }
1204}
1205
1206fn default_reset_ttl() -> bool {
1207 true
1208}
1209
1210#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1212#[serde(deny_unknown_fields)]
1213#[serde(default)]
1214pub(crate) struct Tls {
1215 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1219 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1221 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1223}
1224
1225#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1227#[serde(deny_unknown_fields)]
1228pub(crate) struct TlsSupergraph {
1229 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1231 #[schemars(with = "String")]
1232 pub(crate) certificate: CertificateDer<'static>,
1233 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1235 #[schemars(with = "String")]
1236 pub(crate) key: PrivateKeyDer<'static>,
1237 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1239 #[schemars(with = "String")]
1240 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1241}
1242
1243impl TlsSupergraph {
1244 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1245 let mut certificates = vec![self.certificate.clone()];
1246 certificates.extend(self.certificate_chain.iter().cloned());
1247
1248 let mut config = ServerConfig::builder()
1249 .with_no_client_auth()
1250 .with_single_cert(certificates, self.key.clone_key())
1251 .map_err(ApolloRouterError::Rustls)?;
1252 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1253
1254 Ok(Arc::new(config))
1255 }
1256}
1257
1258fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1259where
1260 D: Deserializer<'de>,
1261{
1262 let data = String::deserialize(deserializer)?;
1263
1264 load_certs(&data)
1265 .map_err(serde::de::Error::custom)
1266 .and_then(|mut certs| {
1267 if certs.len() > 1 {
1268 Err(serde::de::Error::custom("expected exactly one certificate"))
1269 } else {
1270 certs
1271 .pop()
1272 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1273 }
1274 })
1275}
1276
1277fn deserialize_certificate_chain<'de, D>(
1278 deserializer: D,
1279) -> Result<Vec<CertificateDer<'static>>, D::Error>
1280where
1281 D: Deserializer<'de>,
1282{
1283 let data = String::deserialize(deserializer)?;
1284
1285 load_certs(&data).map_err(serde::de::Error::custom)
1286}
1287
1288fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1289where
1290 D: Deserializer<'de>,
1291{
1292 let data = String::deserialize(deserializer)?;
1293
1294 load_key(&data).map_err(serde::de::Error::custom)
1295}
1296
1297#[derive(thiserror::Error, Debug)]
1298#[error("could not load TLS certificate: {0}")]
1299struct LoadCertError(std::io::Error);
1300
1301pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1302 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1303 .collect::<Result<Vec<_>, _>>()
1304 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1305}
1306
1307pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1308 let mut reader = BufReader::new(data.as_bytes());
1309 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1310
1311 let private_key = match key_iterator.next() {
1312 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1313 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1314 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1315 Some(Err(e)) => {
1316 return Err(io::Error::new(
1317 io::ErrorKind::InvalidInput,
1318 format!("could not parse the key: {e}"),
1319 ));
1320 }
1321 Some(_) => {
1322 return Err(io::Error::new(
1323 io::ErrorKind::InvalidInput,
1324 "expected a private key",
1325 ));
1326 }
1327 None => {
1328 return Err(io::Error::new(
1329 io::ErrorKind::InvalidInput,
1330 "could not find a private key",
1331 ));
1332 }
1333 };
1334
1335 if key_iterator.next().is_some() {
1336 return Err(io::Error::new(
1337 io::ErrorKind::InvalidInput,
1338 "expected exactly one private key",
1339 ));
1340 }
1341 Ok(private_key)
1342}
1343
1344#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
1346#[serde(deny_unknown_fields)]
1347#[serde(default)]
1348pub(crate) struct TlsClient {
1349 pub(crate) certificate_authorities: Option<String>,
1351 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1353}
1354
1355#[buildstructor::buildstructor]
1356impl TlsClient {
1357 #[builder]
1358 pub(crate) fn new(
1359 certificate_authorities: Option<String>,
1360 client_authentication: Option<Arc<TlsClientAuth>>,
1361 ) -> Self {
1362 Self {
1363 certificate_authorities,
1364 client_authentication,
1365 }
1366 }
1367}
1368
1369impl Default for TlsClient {
1370 fn default() -> Self {
1371 Self::builder().build()
1372 }
1373}
1374
1375#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1377#[serde(deny_unknown_fields)]
1378pub(crate) struct TlsClientAuth {
1379 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1381 #[schemars(with = "String")]
1382 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1383 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1385 #[schemars(with = "String")]
1386 pub(crate) key: PrivateKeyDer<'static>,
1387}
1388
1389#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1391#[serde(deny_unknown_fields)]
1392#[serde(default)]
1393pub(crate) struct Reload {
1394 #[schemars(default = "default_reload_max_retries")]
1400 pub(crate) max_retries: Option<u32>,
1401
1402 #[serde(deserialize_with = "humantime_serde::deserialize")]
1405 #[schemars(with = "String", default = "default_reload_retry_delay")]
1406 pub(crate) retry_delay: Duration,
1407}
1408
1409fn default_reload_max_retries() -> Option<u32> {
1410 Some(5)
1411}
1412
1413fn default_reload_retry_delay() -> Duration {
1414 Duration::from_secs(10)
1415}
1416
1417impl Default for Reload {
1418 fn default() -> Self {
1419 Self {
1420 max_retries: default_reload_max_retries(),
1421 retry_delay: default_reload_retry_delay(),
1422 }
1423 }
1424}
1425
1426#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1428#[serde(deny_unknown_fields)]
1429#[serde(default)]
1430pub(crate) struct Sandbox {
1431 pub(crate) enabled: bool,
1433}
1434
1435fn default_sandbox() -> bool {
1436 false
1437}
1438
1439#[buildstructor::buildstructor]
1440impl Sandbox {
1441 #[builder]
1442 pub(crate) fn new(enabled: Option<bool>) -> Self {
1443 Self {
1444 enabled: enabled.unwrap_or_else(default_sandbox),
1445 }
1446 }
1447}
1448
1449#[cfg(test)]
1450#[buildstructor::buildstructor]
1451impl Sandbox {
1452 #[builder]
1453 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1454 Self {
1455 enabled: enabled.unwrap_or_else(default_sandbox),
1456 }
1457 }
1458}
1459
1460impl Default for Sandbox {
1461 fn default() -> Self {
1462 Self::builder().build()
1463 }
1464}
1465
1466#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1468#[serde(deny_unknown_fields)]
1469#[serde(default)]
1470pub(crate) struct Homepage {
1471 pub(crate) enabled: bool,
1473 pub(crate) graph_ref: Option<String>,
1476}
1477
1478fn default_homepage() -> bool {
1479 true
1480}
1481
1482#[buildstructor::buildstructor]
1483impl Homepage {
1484 #[builder]
1485 pub(crate) fn new(enabled: Option<bool>) -> Self {
1486 Self {
1487 enabled: enabled.unwrap_or_else(default_homepage),
1488 graph_ref: None,
1489 }
1490 }
1491}
1492
1493#[cfg(test)]
1494#[buildstructor::buildstructor]
1495impl Homepage {
1496 #[builder]
1497 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1498 Self {
1499 enabled: enabled.unwrap_or_else(default_homepage),
1500 graph_ref: None,
1501 }
1502 }
1503}
1504
1505impl Default for Homepage {
1506 fn default() -> Self {
1507 Self::builder().enabled(default_homepage()).build()
1508 }
1509}
1510
1511#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1513#[serde(untagged)]
1514pub enum ListenAddr {
1515 SocketAddr(SocketAddr),
1517 #[cfg(unix)]
1519 UnixSocket(std::path::PathBuf),
1520}
1521
1522impl ListenAddr {
1523 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1524 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1525 if let Self::SocketAddr(addr) = self {
1526 Some((addr.ip(), addr.port()))
1527 } else {
1528 None
1529 }
1530 }
1531}
1532
1533impl From<SocketAddr> for ListenAddr {
1534 fn from(addr: SocketAddr) -> Self {
1535 Self::SocketAddr(addr)
1536 }
1537}
1538
1539#[allow(clippy::from_over_into)]
1540impl Into<serde_json::Value> for ListenAddr {
1541 fn into(self) -> serde_json::Value {
1542 match self {
1543 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1546 #[cfg(unix)]
1547 Self::UnixSocket(path) => serde_json::Value::String(
1548 path.as_os_str()
1549 .to_str()
1550 .expect("unsupported non-UTF-8 path")
1551 .to_string(),
1552 ),
1553 }
1554 }
1555}
1556
1557#[cfg(unix)]
1558impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1559 for ListenAddr
1560{
1561 fn from(
1562 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1563 ) -> Self {
1564 match addr {
1565 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1566 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1567 addr.as_pathname()
1568 .map(ToOwned::to_owned)
1569 .unwrap_or_default(),
1570 ),
1571 }
1572 }
1573}
1574
1575impl fmt::Display for ListenAddr {
1576 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1577 match self {
1578 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1579 #[cfg(unix)]
1580 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1581 }
1582 }
1583}
1584
1585fn default_graphql_path() -> String {
1586 String::from("/")
1587}
1588
1589fn default_graphql_introspection() -> bool {
1590 false
1591}
1592
1593fn default_connection_shutdown_timeout() -> Duration {
1594 Duration::from_secs(60)
1595}
1596
1597fn default_strict_variable_validation() -> Mode {
1598 Mode::Enforce
1599}
1600
1601#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1602#[serde(deny_unknown_fields, rename_all = "snake_case")]
1603pub(crate) enum BatchingMode {
1604 #[default]
1606 BatchHttpLink,
1607}
1608
1609#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1611#[serde(deny_unknown_fields, default)]
1612pub(crate) struct Batching {
1613 pub(crate) enabled: bool,
1615
1616 pub(crate) mode: BatchingMode,
1618
1619 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1621
1622 pub(crate) maximum_size: Option<usize>,
1624}
1625
1626#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1628#[serde(deny_unknown_fields, default)]
1629pub(crate) struct CommonBatchingConfig {
1630 pub(crate) enabled: bool,
1632}
1633
1634impl Batching {
1635 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1637 match &self.subgraph {
1638 Some(subgraph_batching_config) => {
1639 if subgraph_batching_config.all.enabled {
1641 subgraph_batching_config
1645 .subgraphs
1646 .get(service_name)
1647 .is_none_or(|x| x.enabled)
1648 } else {
1649 subgraph_batching_config
1652 .subgraphs
1653 .get(service_name)
1654 .is_some_and(|x| x.enabled)
1655 }
1656 }
1657 None => false,
1658 }
1659 }
1660
1661 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1662 match self.maximum_size {
1663 Some(maximum_size) => batch.len() > maximum_size,
1664 None => false,
1665 }
1666 }
1667}
1668
1669#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1676#[serde(deny_unknown_fields)]
1677pub(crate) struct HoistOrphanErrors {
1678 #[serde(default)]
1680 pub(crate) enabled: bool,
1681}