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) health_check: HealthCheck,
163
164 #[serde(default)]
166 pub(crate) sandbox: Sandbox,
167
168 #[serde(default)]
170 pub(crate) homepage: Homepage,
171
172 #[serde(default)]
174 pub(crate) server: Server,
175
176 #[serde(default)]
178 pub(crate) supergraph: Supergraph,
179
180 #[serde(default)]
182 pub(crate) cors: Cors,
183
184 #[serde(default)]
185 pub(crate) tls: Tls,
186
187 #[serde(default)]
189 pub(crate) apq: Apq,
190
191 #[serde(default)]
193 pub persisted_queries: PersistedQueries,
194
195 #[serde(default)]
197 pub(crate) limits: limits::Config,
198
199 #[serde(default)]
202 pub(crate) experimental_chaos: Config,
203
204 #[serde(default)]
206 pub(crate) plugins: UserPlugins,
207
208 #[serde(default)]
210 #[serde(flatten)]
211 pub(crate) apollo_plugins: ApolloPlugins,
212
213 #[serde(skip)]
215 pub uplink: Option<UplinkConfig>,
216
217 #[serde(default, skip_serializing, skip_deserializing)]
220 pub(crate) notify: Notify<String, graphql::Response>,
221
222 #[serde(default)]
224 pub(crate) batching: Batching,
225
226 #[serde(default)]
228 pub(crate) experimental_type_conditioned_fetching: bool,
229
230 #[serde(default)]
235 pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
236}
237
238impl PartialEq for Configuration {
239 fn eq(&self, other: &Self) -> bool {
240 self.validated_yaml == other.validated_yaml
241 }
242}
243
244impl<'de> serde::Deserialize<'de> for Configuration {
245 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246 where
247 D: serde::Deserializer<'de>,
248 {
249 #[derive(Deserialize, Default)]
252 #[serde(default)]
253 struct AdHocConfiguration {
254 health_check: HealthCheck,
255 sandbox: Sandbox,
256 homepage: Homepage,
257 server: Server,
258 supergraph: Supergraph,
259 cors: Cors,
260 plugins: UserPlugins,
261 #[serde(flatten)]
262 apollo_plugins: ApolloPlugins,
263 tls: Tls,
264 apq: Apq,
265 persisted_queries: PersistedQueries,
266 limits: limits::Config,
267 experimental_chaos: chaos::Config,
268 batching: Batching,
269 experimental_type_conditioned_fetching: bool,
270 experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
271 }
272 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
273
274 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
275 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
276
277 ad_hoc.apollo_plugins.plugins.insert(
280 "limits".to_string(),
281 serde_json::to_value(&ad_hoc.limits).unwrap(),
282 );
283 ad_hoc.apollo_plugins.plugins.insert(
284 "health_check".to_string(),
285 serde_json::to_value(&ad_hoc.health_check).unwrap(),
286 );
287
288 Configuration {
290 health_check: ad_hoc.health_check,
291 sandbox: ad_hoc.sandbox,
292 homepage: ad_hoc.homepage,
293 server: ad_hoc.server,
294 supergraph: ad_hoc.supergraph,
295 cors: ad_hoc.cors,
296 tls: ad_hoc.tls,
297 apq: ad_hoc.apq,
298 persisted_queries: ad_hoc.persisted_queries,
299 limits: ad_hoc.limits,
300 experimental_chaos: ad_hoc.experimental_chaos,
301 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
302 experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
303 plugins: ad_hoc.plugins,
304 apollo_plugins: ad_hoc.apollo_plugins,
305 batching: ad_hoc.batching,
306
307 notify,
309 uplink: None,
310 validated_yaml: None,
311 raw_yaml: None,
312 }
313 .validate()
314 .map_err(|e| serde::de::Error::custom(e.to_string()))
315 }
316}
317
318pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
319
320fn default_graphql_listen() -> ListenAddr {
321 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
322}
323
324#[cfg(test)]
325#[buildstructor::buildstructor]
326impl Configuration {
327 #[builder]
328 pub(crate) fn new(
329 supergraph: Option<Supergraph>,
330 health_check: Option<HealthCheck>,
331 sandbox: Option<Sandbox>,
332 homepage: Option<Homepage>,
333 cors: Option<Cors>,
334 plugins: Map<String, Value>,
335 apollo_plugins: Map<String, Value>,
336 tls: Option<Tls>,
337 apq: Option<Apq>,
338 persisted_query: Option<PersistedQueries>,
339 operation_limits: Option<limits::Config>,
340 chaos: Option<chaos::Config>,
341 uplink: Option<UplinkConfig>,
342 experimental_type_conditioned_fetching: Option<bool>,
343 experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
344 batching: Option<Batching>,
345 server: Option<Server>,
346 ) -> Result<Self, ConfigurationError> {
347 let notify = Self::notify(&apollo_plugins)?;
348
349 let conf = Self {
350 validated_yaml: Default::default(),
351 raw_yaml: None,
352 supergraph: supergraph.unwrap_or_default(),
353 server: server.unwrap_or_default(),
354 health_check: health_check.unwrap_or_default(),
355 sandbox: sandbox.unwrap_or_default(),
356 homepage: homepage.unwrap_or_default(),
357 cors: cors.unwrap_or_default(),
358 apq: apq.unwrap_or_default(),
359 persisted_queries: persisted_query.unwrap_or_default(),
360 limits: operation_limits.unwrap_or_default(),
361 experimental_chaos: chaos.unwrap_or_default(),
362 plugins: UserPlugins {
363 plugins: Some(plugins),
364 },
365 apollo_plugins: ApolloPlugins {
366 plugins: apollo_plugins,
367 },
368 tls: tls.unwrap_or_default(),
369 uplink,
370 batching: batching.unwrap_or_default(),
371 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
372 .unwrap_or_default(),
373 experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
374 notify,
375 };
376
377 conf.validate()
378 }
379}
380
381impl Configuration {
382 pub(crate) fn hash(&self) -> String {
383 let mut hasher = sha2::Sha256::new();
384 let defaulted_raw = self
385 .validated_yaml
386 .as_ref()
387 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
388 .unwrap_or_default();
389 hasher.update(defaulted_raw);
390 let hash: String = format!("{:x}", hasher.finalize());
391 hash
392 }
393
394 fn notify(
395 apollo_plugins: &Map<String, Value>,
396 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
397 if cfg!(test) {
398 return Ok(Notify::for_tests());
399 }
400 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
401 Some(plugin_conf) => {
402 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
403 .map_err(|err| ConfigurationError::PluginConfiguration {
404 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
405 error: format!("{err:?}"),
406 })?;
407 conf.queue_capacity
408 }
409 None => None,
410 };
411 Ok(Notify::builder()
412 .and_queue_size(notify_queue_cap)
413 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
414 .heartbeat_error_message(
415 graphql::Response::builder()
416 .errors(vec![
417 graphql::Error::builder()
418 .message("the connection has been closed because it hasn't heartbeat for a while")
419 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
420 .build()
421 ])
422 .build()
423 ).build())
424 }
425
426 pub(crate) fn rust_query_planner_config(
427 &self,
428 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
429 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
430 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
431 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
432
433 let max_evaluated_plans = self
434 .supergraph
435 .query_planning
436 .experimental_plans_limit
437 .and_then(NonZeroU32::new)
439 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
440
441 QueryPlannerConfig {
442 subgraph_graphql_validation: false,
443 generate_query_fragments: self.supergraph.generate_query_fragments,
444 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
445 enable_defer: self.supergraph.defer_support,
446 },
447 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
448 debug: QueryPlannerDebugConfig {
449 max_evaluated_plans,
450 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
451 },
452 }
453 }
454
455 fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
456 self.apollo_plugins
457 .plugins
458 .get(plugin_name)
459 .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
460 .and_then(|enabled| enabled.as_bool())
461 .unwrap_or(false)
462 }
463}
464
465impl Default for Configuration {
466 fn default() -> Self {
467 Configuration::from_str("").expect("default configuration must be valid")
469 }
470}
471
472#[cfg(test)]
473#[buildstructor::buildstructor]
474impl Configuration {
475 #[builder]
476 pub(crate) fn fake_new(
477 supergraph: Option<Supergraph>,
478 health_check: Option<HealthCheck>,
479 sandbox: Option<Sandbox>,
480 homepage: Option<Homepage>,
481 cors: Option<Cors>,
482 plugins: Map<String, Value>,
483 apollo_plugins: Map<String, Value>,
484 tls: Option<Tls>,
485 notify: Option<Notify<String, graphql::Response>>,
486 apq: Option<Apq>,
487 persisted_query: Option<PersistedQueries>,
488 operation_limits: Option<limits::Config>,
489 chaos: Option<chaos::Config>,
490 uplink: Option<UplinkConfig>,
491 batching: Option<Batching>,
492 experimental_type_conditioned_fetching: Option<bool>,
493 server: Option<Server>,
494 ) -> Result<Self, ConfigurationError> {
495 let configuration = Self {
496 validated_yaml: Default::default(),
497 server: server.unwrap_or_default(),
498 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
499 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
500 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
501 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
502 cors: cors.unwrap_or_default(),
503 limits: operation_limits.unwrap_or_default(),
504 experimental_chaos: chaos.unwrap_or_default(),
505 plugins: UserPlugins {
506 plugins: Some(plugins),
507 },
508 apollo_plugins: ApolloPlugins {
509 plugins: apollo_plugins,
510 },
511 tls: tls.unwrap_or_default(),
512 notify: notify.unwrap_or_default(),
513 apq: apq.unwrap_or_default(),
514 persisted_queries: persisted_query.unwrap_or_default(),
515 uplink,
516 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
517 .unwrap_or_default(),
518 experimental_hoist_orphan_errors: Default::default(),
519 batching: batching.unwrap_or_default(),
520 raw_yaml: None,
521 };
522
523 configuration.validate()
524 }
525}
526
527impl Configuration {
528 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
529 if self.sandbox.enabled && self.homepage.enabled {
531 return Err(ConfigurationError::InvalidConfiguration {
532 message: "sandbox and homepage cannot be enabled at the same time",
533 error: "disable the homepage if you want to enable sandbox".to_string(),
534 });
535 }
536 if self.sandbox.enabled && !self.supergraph.introspection {
538 return Err(ConfigurationError::InvalidConfiguration {
539 message: "sandbox requires introspection",
540 error: "sandbox needs introspection to be enabled".to_string(),
541 });
542 }
543 if !self.supergraph.path.starts_with('/') {
544 return Err(ConfigurationError::InvalidConfiguration {
545 message: "invalid 'server.graphql_path' configuration",
546 error: format!(
547 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
548 self.supergraph.path, self.supergraph.path
549 ),
550 });
551 }
552 if self.supergraph.path.ends_with('*')
553 && !self.supergraph.path.ends_with("/*")
554 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
555 {
556 return Err(ConfigurationError::InvalidConfiguration {
557 message: "invalid 'server.graphql_path' configuration",
558 error: format!(
559 "'{}' is invalid, you can only set a wildcard after a '/'",
560 self.supergraph.path
561 ),
562 });
563 }
564 if self.supergraph.path.contains("/*/") {
565 return Err(ConfigurationError::InvalidConfiguration {
566 message: "invalid 'server.graphql_path' configuration",
567 error: format!(
568 "'{}' 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'",
569 self.supergraph.path
570 ),
571 });
572 }
573
574 if self.persisted_queries.enabled {
576 if self.persisted_queries.safelist.enabled && self.apq.enabled {
577 return Err(ConfigurationError::InvalidConfiguration {
578 message: "apqs must be disabled to enable safelisting",
579 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
580 });
581 } else if !self.persisted_queries.safelist.enabled
582 && self.persisted_queries.safelist.require_id
583 {
584 return Err(ConfigurationError::InvalidConfiguration {
585 message: "safelist must be enabled to require IDs",
586 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
587 });
588 }
589 } else {
590 if self.persisted_queries.safelist.enabled {
592 return Err(ConfigurationError::InvalidConfiguration {
593 message: "persisted queries must be enabled to enable safelisting",
594 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
595 });
596 } else if self.persisted_queries.log_unknown {
597 return Err(ConfigurationError::InvalidConfiguration {
598 message: "persisted queries must be enabled to enable logging unknown operations",
599 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
600 });
601 }
602 }
603
604 if self.apollo_plugin_enabled("response_cache")
606 && self.apollo_plugin_enabled("preview_entity_cache")
607 {
608 return Err(ConfigurationError::InvalidConfiguration {
609 message: "entity cache and response cache features are mutually exclusive",
610 error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
611 });
612 }
613
614 Ok(self)
615 }
616}
617
618impl FromStr for Configuration {
620 type Err = ConfigurationError;
621
622 fn from_str(s: &str) -> Result<Self, Self::Err> {
623 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
624 .validate()
625 }
626}
627
628fn gen_schema(
629 plugins: BTreeMap<String, Schema>,
630 hidden_plugins: Option<BTreeMap<String, Schema>>,
631) -> Schema {
632 schemars::json_schema!({
633 "type": "object",
634 "properties": plugins,
635 "additionalProperties": false,
636 "patternProperties": hidden_plugins
637 .unwrap_or_default()
638 .into_iter()
639 .map(|(k, v)| (format!("^{}$", k), v))
641 .collect::<BTreeMap<_, _>>()
642 })
643}
644
645#[derive(Clone, Debug, Default, Deserialize, Serialize)]
651#[serde(transparent)]
652pub(crate) struct ApolloPlugins {
653 pub(crate) plugins: Map<String, Value>,
654}
655
656impl JsonSchema for ApolloPlugins {
657 fn schema_name() -> std::borrow::Cow<'static, str> {
658 stringify!(Plugins).into()
659 }
660
661 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
662 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
666 .sorted_by_key(|factory| factory.name.clone())
667 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
668 .partition_map(|factory| {
669 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
670 let schema = factory.create_schema(generator);
671 if factory.hidden_from_config_json_schema {
673 Either::Right((key, schema))
674 } else {
675 Either::Left((key, schema))
676 }
677 });
678 gen_schema(
679 plugin_entries.into_iter().collect(),
680 Some(hidden_plugin_entries.into_iter().collect()),
681 )
682 }
683}
684
685#[derive(Clone, Debug, Default, Deserialize, Serialize)]
690#[serde(transparent)]
691pub(crate) struct UserPlugins {
692 pub(crate) plugins: Option<Map<String, Value>>,
693}
694
695impl JsonSchema for UserPlugins {
696 fn schema_name() -> std::borrow::Cow<'static, str> {
697 stringify!(Plugins).into()
698 }
699
700 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
701 let plugins = crate::plugin::plugins()
705 .sorted_by_key(|factory| factory.name.clone())
706 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
707 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
708 .collect();
709 gen_schema(plugins, None)
710 }
711}
712
713#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
715#[serde(deny_unknown_fields)]
716#[serde(default)]
717pub(crate) struct Supergraph {
718 pub(crate) listen: ListenAddr,
721
722 #[serde(deserialize_with = "humantime_serde::deserialize")]
724 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
725 pub(crate) connection_shutdown_timeout: Duration,
726
727 pub(crate) path: String,
730
731 pub(crate) introspection: bool,
734
735 pub(crate) redact_query_validation_errors: bool,
739
740 pub(crate) generate_query_fragments: bool,
743
744 pub(crate) defer_support: bool,
746
747 pub(crate) query_planning: QueryPlanning,
749
750 pub(crate) early_cancel: bool,
755
756 pub(crate) enable_result_coercion_errors: bool,
763
764 pub(crate) experimental_log_on_broken_pipe: bool,
767
768 pub(crate) strict_variable_validation: Mode,
772}
773
774const fn default_generate_query_fragments() -> bool {
775 true
776}
777
778fn default_defer_support() -> bool {
779 true
780}
781
782#[buildstructor::buildstructor]
783impl Supergraph {
784 #[builder]
785 pub(crate) fn new(
786 listen: Option<ListenAddr>,
787 path: Option<String>,
788 connection_shutdown_timeout: Option<Duration>,
789 introspection: Option<bool>,
790 defer_support: Option<bool>,
791 query_planning: Option<QueryPlanning>,
792 generate_query_fragments: Option<bool>,
793 early_cancel: Option<bool>,
794 experimental_log_on_broken_pipe: Option<bool>,
795 insert_result_coercion_errors: Option<bool>,
796 strict_variable_validation: Option<Mode>,
797 redact_query_validation_errors: Option<bool>,
798 ) -> Self {
799 Self {
800 listen: listen.unwrap_or_else(default_graphql_listen),
801 path: path.unwrap_or_else(default_graphql_path),
802 connection_shutdown_timeout: connection_shutdown_timeout
803 .unwrap_or_else(default_connection_shutdown_timeout),
804 introspection: introspection.unwrap_or_else(default_graphql_introspection),
805 defer_support: defer_support.unwrap_or_else(default_defer_support),
806 query_planning: query_planning.unwrap_or_default(),
807 generate_query_fragments: generate_query_fragments
808 .unwrap_or_else(default_generate_query_fragments),
809 early_cancel: early_cancel.unwrap_or_default(),
810 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
811 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
812 strict_variable_validation: strict_variable_validation
813 .unwrap_or_else(default_strict_variable_validation),
814 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
815 }
816 }
817}
818
819#[cfg(test)]
820#[buildstructor::buildstructor]
821impl Supergraph {
822 #[builder]
823 pub(crate) fn fake_new(
824 listen: Option<ListenAddr>,
825 path: Option<String>,
826 connection_shutdown_timeout: Option<Duration>,
827 introspection: Option<bool>,
828 defer_support: Option<bool>,
829 query_planning: Option<QueryPlanning>,
830 generate_query_fragments: Option<bool>,
831 early_cancel: Option<bool>,
832 experimental_log_on_broken_pipe: Option<bool>,
833 insert_result_coercion_errors: Option<bool>,
834 strict_variable_validation: Option<Mode>,
835 redact_query_validation_errors: Option<bool>,
836 ) -> Self {
837 Self {
838 listen: listen.unwrap_or_else(test_listen),
839 path: path.unwrap_or_else(default_graphql_path),
840 connection_shutdown_timeout: connection_shutdown_timeout
841 .unwrap_or_else(default_connection_shutdown_timeout),
842 introspection: introspection.unwrap_or_else(default_graphql_introspection),
843 defer_support: defer_support.unwrap_or_else(default_defer_support),
844 query_planning: query_planning.unwrap_or_default(),
845 generate_query_fragments: generate_query_fragments
846 .unwrap_or_else(default_generate_query_fragments),
847 early_cancel: early_cancel.unwrap_or_default(),
848 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
849 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
850 strict_variable_validation: strict_variable_validation
851 .unwrap_or_else(default_strict_variable_validation),
852 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
853 }
854 }
855}
856
857impl Default for Supergraph {
858 fn default() -> Self {
859 Self::builder().build()
860 }
861}
862
863impl Supergraph {
864 pub(crate) fn sanitized_path(&self) -> String {
866 let mut path = self.path.clone();
867 if self.path.ends_with("/*") {
868 path = format!("{}router_extra_path", self.path);
870 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
871 let new_path = SUPERGRAPH_ENDPOINT_REGEX
872 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
873 path = new_path.to_string();
874 } else if TRAILING_SLASH.is_match(&self.path) {
875 let new_path = TRAILING_SLASH.replace(&self.path, "");
876 if new_path.is_empty() {
877 path = "/".to_string();
878 } else {
879 path = new_path.to_string();
880 }
881 }
882
883 path
884 }
885}
886
887#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
889#[serde(deny_unknown_fields)]
890pub(crate) struct Router {
891 #[serde(default)]
892 pub(crate) cache: Cache,
893}
894
895#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
897#[serde(deny_unknown_fields, default)]
898pub(crate) struct Apq {
899 pub(crate) enabled: bool,
901
902 pub(crate) router: Router,
903
904 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
905}
906
907#[cfg(test)]
908#[buildstructor::buildstructor]
909impl Apq {
910 #[builder]
911 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
912 Self {
913 enabled: enabled.unwrap_or_else(default_apq),
914 ..Default::default()
915 }
916 }
917}
918
919#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
921#[serde(deny_unknown_fields, default)]
922pub(crate) struct SubgraphApq {
923 pub(crate) enabled: bool,
925}
926
927fn default_apq() -> bool {
928 true
929}
930
931impl Default for Apq {
932 fn default() -> Self {
933 Self {
934 enabled: default_apq(),
935 router: Default::default(),
936 subgraph: Default::default(),
937 }
938 }
939}
940
941#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
943#[serde(deny_unknown_fields, default)]
944pub(crate) struct QueryPlanning {
945 pub(crate) cache: QueryPlanCache,
947 pub(crate) warmed_up_queries: Option<usize>,
952
953 pub(crate) experimental_plans_limit: Option<u32>,
961
962 pub(crate) experimental_paths_limit: Option<u32>,
975
976 pub(crate) experimental_reuse_query_plans: bool,
979
980 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
984}
985
986#[buildstructor::buildstructor]
987impl QueryPlanning {
988 #[builder]
989 #[allow(dead_code)]
990 pub(crate) fn new(
991 cache: Option<QueryPlanCache>,
992 warmed_up_queries: Option<usize>,
993 experimental_plans_limit: Option<u32>,
994 experimental_paths_limit: Option<u32>,
995 experimental_reuse_query_plans: Option<bool>,
996 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
997 ) -> Self {
998 Self {
999 cache: cache.unwrap_or_default(),
1000 warmed_up_queries,
1001 experimental_plans_limit,
1002 experimental_paths_limit,
1003 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
1004 experimental_cooperative_cancellation: experimental_cooperative_cancellation
1005 .unwrap_or_default(),
1006 }
1007 }
1008}
1009
1010#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1012#[serde(deny_unknown_fields, default)]
1013pub(crate) struct QueryPlanCache {
1014 pub(crate) in_memory: InMemoryCache,
1016 pub(crate) redis: Option<QueryPlanRedisCache>,
1018}
1019
1020#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1021#[serde(deny_unknown_fields)]
1022pub(crate) struct QueryPlanRedisCache {
1024 pub(crate) urls: Vec<url::Url>,
1026
1027 pub(crate) username: Option<String>,
1029 pub(crate) password: Option<String>,
1031
1032 #[serde(
1033 deserialize_with = "humantime_serde::deserialize",
1034 default = "default_timeout"
1035 )]
1036 #[schemars(with = "Option<String>", default)]
1037 pub(crate) timeout: Duration,
1039
1040 #[serde(
1041 deserialize_with = "humantime_serde::deserialize",
1042 default = "default_query_plan_cache_ttl"
1043 )]
1044 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1045 pub(crate) ttl: Duration,
1047
1048 pub(crate) namespace: Option<String>,
1050
1051 #[serde(default)]
1052 pub(crate) tls: Option<TlsClient>,
1054
1055 #[serde(default = "default_required_to_start")]
1056 pub(crate) required_to_start: bool,
1058
1059 #[serde(default = "default_reset_ttl")]
1060 pub(crate) reset_ttl: bool,
1062
1063 #[serde(default = "default_query_planner_cache_pool_size")]
1064 pub(crate) pool_size: u32,
1066}
1067
1068fn default_query_plan_cache_ttl() -> Duration {
1069 Duration::from_secs(86400 * 30)
1071}
1072
1073fn default_query_planner_cache_pool_size() -> u32 {
1074 1
1075}
1076
1077#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1079#[serde(deny_unknown_fields, default)]
1080pub(crate) struct Cache {
1081 pub(crate) in_memory: InMemoryCache,
1083 pub(crate) redis: Option<RedisCache>,
1085}
1086
1087impl From<QueryPlanCache> for Cache {
1088 fn from(value: QueryPlanCache) -> Self {
1089 Cache {
1090 in_memory: value.in_memory,
1091 redis: value.redis.map(Into::into),
1092 }
1093 }
1094}
1095
1096#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1097#[serde(deny_unknown_fields)]
1098pub(crate) struct InMemoryCache {
1100 pub(crate) limit: NonZeroUsize,
1102}
1103
1104impl Default for InMemoryCache {
1105 fn default() -> Self {
1106 Self {
1107 limit: DEFAULT_CACHE_CAPACITY,
1108 }
1109 }
1110}
1111
1112#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1113#[serde(deny_unknown_fields)]
1114pub(crate) struct RedisCache {
1116 pub(crate) urls: Vec<url::Url>,
1118
1119 pub(crate) username: Option<String>,
1121 pub(crate) password: Option<String>,
1123
1124 #[serde(
1125 deserialize_with = "humantime_serde::deserialize",
1126 default = "default_timeout"
1127 )]
1128 #[schemars(with = "Option<String>", default)]
1129 pub(crate) timeout: Duration,
1131
1132 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1133 #[schemars(with = "Option<String>", default)]
1134 pub(crate) ttl: Option<Duration>,
1136
1137 pub(crate) namespace: Option<String>,
1139
1140 #[serde(default)]
1141 pub(crate) tls: Option<TlsClient>,
1143
1144 #[serde(default = "default_required_to_start")]
1145 pub(crate) required_to_start: bool,
1147
1148 #[serde(default = "default_reset_ttl")]
1149 pub(crate) reset_ttl: bool,
1151
1152 #[serde(default = "default_pool_size")]
1153 pub(crate) pool_size: u32,
1155 #[serde(
1156 deserialize_with = "humantime_serde::deserialize",
1157 default = "default_metrics_interval"
1158 )]
1159 #[schemars(with = "Option<String>", default)]
1160 pub(crate) metrics_interval: Duration,
1162}
1163
1164fn default_timeout() -> Duration {
1165 Duration::from_millis(500)
1166}
1167
1168pub(crate) fn default_required_to_start() -> bool {
1169 false
1170}
1171
1172pub(crate) fn default_pool_size() -> u32 {
1173 1
1174}
1175
1176pub(crate) fn default_metrics_interval() -> Duration {
1177 Duration::from_secs(1)
1178}
1179
1180impl From<QueryPlanRedisCache> for RedisCache {
1181 fn from(value: QueryPlanRedisCache) -> Self {
1182 RedisCache {
1183 urls: value.urls,
1184 username: value.username,
1185 password: value.password,
1186 timeout: value.timeout,
1187 ttl: Some(value.ttl),
1188 namespace: value.namespace,
1189 tls: value.tls,
1190 required_to_start: value.required_to_start,
1191 reset_ttl: value.reset_ttl,
1192 pool_size: value.pool_size,
1193 metrics_interval: default_metrics_interval(),
1194 }
1195 }
1196}
1197
1198fn default_reset_ttl() -> bool {
1199 true
1200}
1201
1202#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1204#[serde(deny_unknown_fields)]
1205#[serde(default)]
1206pub(crate) struct Tls {
1207 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1211 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1213 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1215}
1216
1217#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1219#[serde(deny_unknown_fields)]
1220pub(crate) struct TlsSupergraph {
1221 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1223 #[schemars(with = "String")]
1224 pub(crate) certificate: CertificateDer<'static>,
1225 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1227 #[schemars(with = "String")]
1228 pub(crate) key: PrivateKeyDer<'static>,
1229 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1231 #[schemars(with = "String")]
1232 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1233}
1234
1235impl TlsSupergraph {
1236 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1237 let mut certificates = vec![self.certificate.clone()];
1238 certificates.extend(self.certificate_chain.iter().cloned());
1239
1240 let mut config = ServerConfig::builder()
1241 .with_no_client_auth()
1242 .with_single_cert(certificates, self.key.clone_key())
1243 .map_err(ApolloRouterError::Rustls)?;
1244 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1245
1246 Ok(Arc::new(config))
1247 }
1248}
1249
1250fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1251where
1252 D: Deserializer<'de>,
1253{
1254 let data = String::deserialize(deserializer)?;
1255
1256 load_certs(&data)
1257 .map_err(serde::de::Error::custom)
1258 .and_then(|mut certs| {
1259 if certs.len() > 1 {
1260 Err(serde::de::Error::custom("expected exactly one certificate"))
1261 } else {
1262 certs
1263 .pop()
1264 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1265 }
1266 })
1267}
1268
1269fn deserialize_certificate_chain<'de, D>(
1270 deserializer: D,
1271) -> Result<Vec<CertificateDer<'static>>, D::Error>
1272where
1273 D: Deserializer<'de>,
1274{
1275 let data = String::deserialize(deserializer)?;
1276
1277 load_certs(&data).map_err(serde::de::Error::custom)
1278}
1279
1280fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1281where
1282 D: Deserializer<'de>,
1283{
1284 let data = String::deserialize(deserializer)?;
1285
1286 load_key(&data).map_err(serde::de::Error::custom)
1287}
1288
1289#[derive(thiserror::Error, Debug)]
1290#[error("could not load TLS certificate: {0}")]
1291struct LoadCertError(std::io::Error);
1292
1293pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1294 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1295 .collect::<Result<Vec<_>, _>>()
1296 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1297}
1298
1299pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1300 let mut reader = BufReader::new(data.as_bytes());
1301 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1302
1303 let private_key = match key_iterator.next() {
1304 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1305 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1306 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1307 Some(Err(e)) => {
1308 return Err(io::Error::new(
1309 io::ErrorKind::InvalidInput,
1310 format!("could not parse the key: {e}"),
1311 ));
1312 }
1313 Some(_) => {
1314 return Err(io::Error::new(
1315 io::ErrorKind::InvalidInput,
1316 "expected a private key",
1317 ));
1318 }
1319 None => {
1320 return Err(io::Error::new(
1321 io::ErrorKind::InvalidInput,
1322 "could not find a private key",
1323 ));
1324 }
1325 };
1326
1327 if key_iterator.next().is_some() {
1328 return Err(io::Error::new(
1329 io::ErrorKind::InvalidInput,
1330 "expected exactly one private key",
1331 ));
1332 }
1333 Ok(private_key)
1334}
1335
1336#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
1338#[serde(deny_unknown_fields)]
1339#[serde(default)]
1340pub(crate) struct TlsClient {
1341 pub(crate) certificate_authorities: Option<String>,
1343 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1345}
1346
1347#[buildstructor::buildstructor]
1348impl TlsClient {
1349 #[builder]
1350 pub(crate) fn new(
1351 certificate_authorities: Option<String>,
1352 client_authentication: Option<Arc<TlsClientAuth>>,
1353 ) -> Self {
1354 Self {
1355 certificate_authorities,
1356 client_authentication,
1357 }
1358 }
1359}
1360
1361impl Default for TlsClient {
1362 fn default() -> Self {
1363 Self::builder().build()
1364 }
1365}
1366
1367#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1369#[serde(deny_unknown_fields)]
1370pub(crate) struct TlsClientAuth {
1371 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1373 #[schemars(with = "String")]
1374 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1375 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1377 #[schemars(with = "String")]
1378 pub(crate) key: PrivateKeyDer<'static>,
1379}
1380
1381#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1383#[serde(deny_unknown_fields)]
1384#[serde(default)]
1385pub(crate) struct Sandbox {
1386 pub(crate) enabled: bool,
1388}
1389
1390fn default_sandbox() -> bool {
1391 false
1392}
1393
1394#[buildstructor::buildstructor]
1395impl Sandbox {
1396 #[builder]
1397 pub(crate) fn new(enabled: Option<bool>) -> Self {
1398 Self {
1399 enabled: enabled.unwrap_or_else(default_sandbox),
1400 }
1401 }
1402}
1403
1404#[cfg(test)]
1405#[buildstructor::buildstructor]
1406impl Sandbox {
1407 #[builder]
1408 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1409 Self {
1410 enabled: enabled.unwrap_or_else(default_sandbox),
1411 }
1412 }
1413}
1414
1415impl Default for Sandbox {
1416 fn default() -> Self {
1417 Self::builder().build()
1418 }
1419}
1420
1421#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1423#[serde(deny_unknown_fields)]
1424#[serde(default)]
1425pub(crate) struct Homepage {
1426 pub(crate) enabled: bool,
1428 pub(crate) graph_ref: Option<String>,
1431}
1432
1433fn default_homepage() -> bool {
1434 true
1435}
1436
1437#[buildstructor::buildstructor]
1438impl Homepage {
1439 #[builder]
1440 pub(crate) fn new(enabled: Option<bool>) -> Self {
1441 Self {
1442 enabled: enabled.unwrap_or_else(default_homepage),
1443 graph_ref: None,
1444 }
1445 }
1446}
1447
1448#[cfg(test)]
1449#[buildstructor::buildstructor]
1450impl Homepage {
1451 #[builder]
1452 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1453 Self {
1454 enabled: enabled.unwrap_or_else(default_homepage),
1455 graph_ref: None,
1456 }
1457 }
1458}
1459
1460impl Default for Homepage {
1461 fn default() -> Self {
1462 Self::builder().enabled(default_homepage()).build()
1463 }
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1468#[serde(untagged)]
1469pub enum ListenAddr {
1470 SocketAddr(SocketAddr),
1472 #[cfg(unix)]
1474 UnixSocket(std::path::PathBuf),
1475}
1476
1477impl ListenAddr {
1478 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1479 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1480 if let Self::SocketAddr(addr) = self {
1481 Some((addr.ip(), addr.port()))
1482 } else {
1483 None
1484 }
1485 }
1486}
1487
1488impl From<SocketAddr> for ListenAddr {
1489 fn from(addr: SocketAddr) -> Self {
1490 Self::SocketAddr(addr)
1491 }
1492}
1493
1494#[allow(clippy::from_over_into)]
1495impl Into<serde_json::Value> for ListenAddr {
1496 fn into(self) -> serde_json::Value {
1497 match self {
1498 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1501 #[cfg(unix)]
1502 Self::UnixSocket(path) => serde_json::Value::String(
1503 path.as_os_str()
1504 .to_str()
1505 .expect("unsupported non-UTF-8 path")
1506 .to_string(),
1507 ),
1508 }
1509 }
1510}
1511
1512#[cfg(unix)]
1513impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1514 for ListenAddr
1515{
1516 fn from(
1517 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1518 ) -> Self {
1519 match addr {
1520 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1521 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1522 addr.as_pathname()
1523 .map(ToOwned::to_owned)
1524 .unwrap_or_default(),
1525 ),
1526 }
1527 }
1528}
1529
1530impl fmt::Display for ListenAddr {
1531 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1532 match self {
1533 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1534 #[cfg(unix)]
1535 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1536 }
1537 }
1538}
1539
1540fn default_graphql_path() -> String {
1541 String::from("/")
1542}
1543
1544fn default_graphql_introspection() -> bool {
1545 false
1546}
1547
1548fn default_connection_shutdown_timeout() -> Duration {
1549 Duration::from_secs(60)
1550}
1551
1552fn default_strict_variable_validation() -> Mode {
1553 Mode::Enforce
1554}
1555
1556#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1557#[serde(deny_unknown_fields, rename_all = "snake_case")]
1558pub(crate) enum BatchingMode {
1559 #[default]
1561 BatchHttpLink,
1562}
1563
1564#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1566#[serde(deny_unknown_fields)]
1567pub(crate) struct Batching {
1568 #[serde(default)]
1570 pub(crate) enabled: bool,
1571
1572 pub(crate) mode: BatchingMode,
1574
1575 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1577
1578 #[serde(default)]
1580 pub(crate) maximum_size: Option<usize>,
1581}
1582
1583#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1585pub(crate) struct CommonBatchingConfig {
1586 pub(crate) enabled: bool,
1588}
1589
1590impl Batching {
1591 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1593 match &self.subgraph {
1594 Some(subgraph_batching_config) => {
1595 if subgraph_batching_config.all.enabled {
1597 subgraph_batching_config
1601 .subgraphs
1602 .get(service_name)
1603 .is_none_or(|x| x.enabled)
1604 } else {
1605 subgraph_batching_config
1608 .subgraphs
1609 .get(service_name)
1610 .is_some_and(|x| x.enabled)
1611 }
1612 }
1613 None => false,
1614 }
1615 }
1616
1617 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1618 match self.maximum_size {
1619 Some(maximum_size) => batch.len() > maximum_size,
1620 None => false,
1621 }
1622 }
1623}
1624
1625#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1632#[serde(deny_unknown_fields)]
1633pub(crate) struct HoistOrphanErrors {
1634 #[serde(default)]
1636 pub(crate) enabled: bool,
1637}