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::graphql;
53use crate::plugin::plugins;
54use crate::plugins::chaos;
55use crate::plugins::chaos::Config;
56use crate::plugins::healthcheck::Config as HealthCheck;
57#[cfg(test)]
58use crate::plugins::healthcheck::test_listen;
59use crate::plugins::limits;
60use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
61use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
62use crate::plugins::subscription::SubscriptionConfig;
63use crate::plugins::subscription::notification::Notify;
64use crate::uplink::UplinkConfig;
65
66pub(crate) mod connector;
67pub(crate) mod cooperative_cancellation;
68pub(crate) mod cors;
69pub(crate) mod expansion;
70mod experimental;
71pub(crate) mod metrics;
72pub(crate) mod mode;
73mod persisted_queries;
74pub(crate) mod schema;
75pub(crate) mod server;
76pub(crate) mod shared;
77pub(crate) mod subgraph;
78#[cfg(test)]
79mod tests;
80mod upgrade;
81mod yaml;
82
83static HEARTBEAT_TIMEOUT_DURATION_SECONDS: u64 = 15;
85
86static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
87 Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
88 .expect("this regex to check the path is valid")
89});
90
91#[derive(Debug, Error, Display)]
93#[non_exhaustive]
94pub enum ConfigurationError {
95 CannotExpandVariable { key: String, cause: String },
97 UnknownExpansionMode {
99 key: String,
100 supported_modes: String,
101 },
102 PluginUnknown(String),
104 PluginConfiguration { plugin: String, error: String },
106 InvalidConfiguration {
108 message: &'static str,
109 error: String,
110 },
111 DeserializeConfigError(serde_json::Error),
113
114 InvalidExpansionModeConfig,
116
117 MigrationFailure { error: String },
119
120 CertificateAuthorities { error: String },
122}
123
124impl From<proteus::Error> for ConfigurationError {
125 fn from(error: proteus::Error) -> Self {
126 Self::MigrationFailure {
127 error: error.to_string(),
128 }
129 }
130}
131
132impl From<proteus::parser::Error> for ConfigurationError {
133 fn from(error: proteus::parser::Error) -> Self {
134 Self::MigrationFailure {
135 error: error.to_string(),
136 }
137 }
138}
139
140#[derive(Clone, Derivative, Serialize, JsonSchema)]
145#[derivative(Debug)]
146pub struct Configuration {
148 #[serde(skip)]
150 pub(crate) validated_yaml: Option<Value>,
151
152 #[serde(skip)]
154 pub(crate) raw_yaml: Option<Arc<str>>,
155
156 #[serde(default)]
158 pub(crate) health_check: HealthCheck,
159
160 #[serde(default)]
162 pub(crate) sandbox: Sandbox,
163
164 #[serde(default)]
166 pub(crate) homepage: Homepage,
167
168 #[serde(default)]
170 pub(crate) server: Server,
171
172 #[serde(default)]
174 pub(crate) supergraph: Supergraph,
175
176 #[serde(default)]
178 pub(crate) cors: Cors,
179
180 #[serde(default)]
181 pub(crate) tls: Tls,
182
183 #[serde(default)]
185 pub(crate) apq: Apq,
186
187 #[serde(default)]
189 pub persisted_queries: PersistedQueries,
190
191 #[serde(default)]
193 pub(crate) limits: limits::Config,
194
195 #[serde(default)]
198 pub(crate) experimental_chaos: Config,
199
200 #[serde(default)]
202 pub(crate) plugins: UserPlugins,
203
204 #[serde(default)]
206 #[serde(flatten)]
207 pub(crate) apollo_plugins: ApolloPlugins,
208
209 #[serde(skip)]
211 pub uplink: Option<UplinkConfig>,
212
213 #[serde(default, skip_serializing, skip_deserializing)]
216 pub(crate) notify: Notify<String, graphql::Response>,
217
218 #[serde(default)]
220 pub(crate) batching: Batching,
221
222 #[serde(default)]
224 pub(crate) experimental_type_conditioned_fetching: bool,
225
226 #[serde(default)]
231 pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
232}
233
234impl PartialEq for Configuration {
235 fn eq(&self, other: &Self) -> bool {
236 self.validated_yaml == other.validated_yaml
237 }
238}
239
240impl<'de> serde::Deserialize<'de> for Configuration {
241 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
242 where
243 D: serde::Deserializer<'de>,
244 {
245 #[derive(Deserialize, Default)]
248 #[serde(default)]
249 struct AdHocConfiguration {
250 health_check: HealthCheck,
251 sandbox: Sandbox,
252 homepage: Homepage,
253 server: Server,
254 supergraph: Supergraph,
255 cors: Cors,
256 plugins: UserPlugins,
257 #[serde(flatten)]
258 apollo_plugins: ApolloPlugins,
259 tls: Tls,
260 apq: Apq,
261 persisted_queries: PersistedQueries,
262 limits: limits::Config,
263 experimental_chaos: chaos::Config,
264 batching: Batching,
265 experimental_type_conditioned_fetching: bool,
266 experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
267 }
268 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
269
270 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
271 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
272
273 ad_hoc.apollo_plugins.plugins.insert(
276 "limits".to_string(),
277 serde_json::to_value(&ad_hoc.limits).unwrap(),
278 );
279 ad_hoc.apollo_plugins.plugins.insert(
280 "health_check".to_string(),
281 serde_json::to_value(&ad_hoc.health_check).unwrap(),
282 );
283
284 Configuration {
286 health_check: ad_hoc.health_check,
287 sandbox: ad_hoc.sandbox,
288 homepage: ad_hoc.homepage,
289 server: ad_hoc.server,
290 supergraph: ad_hoc.supergraph,
291 cors: ad_hoc.cors,
292 tls: ad_hoc.tls,
293 apq: ad_hoc.apq,
294 persisted_queries: ad_hoc.persisted_queries,
295 limits: ad_hoc.limits,
296 experimental_chaos: ad_hoc.experimental_chaos,
297 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
298 experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
299 plugins: ad_hoc.plugins,
300 apollo_plugins: ad_hoc.apollo_plugins,
301 batching: ad_hoc.batching,
302
303 notify,
305 uplink: None,
306 validated_yaml: None,
307 raw_yaml: None,
308 }
309 .validate()
310 .map_err(|e| serde::de::Error::custom(e.to_string()))
311 }
312}
313
314pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
315
316fn default_graphql_listen() -> ListenAddr {
317 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
318}
319
320#[cfg(test)]
321#[buildstructor::buildstructor]
322impl Configuration {
323 #[builder]
324 pub(crate) fn new(
325 supergraph: Option<Supergraph>,
326 health_check: Option<HealthCheck>,
327 sandbox: Option<Sandbox>,
328 homepage: Option<Homepage>,
329 cors: Option<Cors>,
330 plugins: Map<String, Value>,
331 apollo_plugins: Map<String, Value>,
332 tls: Option<Tls>,
333 apq: Option<Apq>,
334 persisted_query: Option<PersistedQueries>,
335 operation_limits: Option<limits::Config>,
336 chaos: Option<chaos::Config>,
337 uplink: Option<UplinkConfig>,
338 experimental_type_conditioned_fetching: Option<bool>,
339 experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
340 batching: Option<Batching>,
341 server: Option<Server>,
342 ) -> Result<Self, ConfigurationError> {
343 let notify = Self::notify(&apollo_plugins)?;
344
345 let conf = Self {
346 validated_yaml: Default::default(),
347 raw_yaml: None,
348 supergraph: supergraph.unwrap_or_default(),
349 server: server.unwrap_or_default(),
350 health_check: health_check.unwrap_or_default(),
351 sandbox: sandbox.unwrap_or_default(),
352 homepage: homepage.unwrap_or_default(),
353 cors: cors.unwrap_or_default(),
354 apq: apq.unwrap_or_default(),
355 persisted_queries: persisted_query.unwrap_or_default(),
356 limits: operation_limits.unwrap_or_default(),
357 experimental_chaos: chaos.unwrap_or_default(),
358 plugins: UserPlugins {
359 plugins: Some(plugins),
360 },
361 apollo_plugins: ApolloPlugins {
362 plugins: apollo_plugins,
363 },
364 tls: tls.unwrap_or_default(),
365 uplink,
366 batching: batching.unwrap_or_default(),
367 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
368 .unwrap_or_default(),
369 experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
370 notify,
371 };
372
373 conf.validate()
374 }
375}
376
377impl Configuration {
378 pub(crate) fn hash(&self) -> String {
379 let mut hasher = sha2::Sha256::new();
380 let defaulted_raw = self
381 .validated_yaml
382 .as_ref()
383 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
384 .unwrap_or_default();
385 hasher.update(defaulted_raw);
386 let hash: String = format!("{:x}", hasher.finalize());
387 hash
388 }
389
390 fn notify(
391 apollo_plugins: &Map<String, Value>,
392 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
393 if cfg!(test) {
394 return Ok(Notify::for_tests());
395 }
396 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
397 Some(plugin_conf) => {
398 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
399 .map_err(|err| ConfigurationError::PluginConfiguration {
400 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
401 error: format!("{err:?}"),
402 })?;
403 conf.queue_capacity
404 }
405 None => None,
406 };
407 Ok(Notify::builder()
408 .and_queue_size(notify_queue_cap)
409 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
410 .heartbeat_error_message(
411 graphql::Response::builder()
412 .errors(vec![
413 graphql::Error::builder()
414 .message("the connection has been closed because it hasn't heartbeat for a while")
415 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
416 .build()
417 ])
418 .build()
419 ).build())
420 }
421
422 pub(crate) fn rust_query_planner_config(
423 &self,
424 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
425 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
426 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
427 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
428
429 let max_evaluated_plans = self
430 .supergraph
431 .query_planning
432 .experimental_plans_limit
433 .and_then(NonZeroU32::new)
435 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
436
437 QueryPlannerConfig {
438 subgraph_graphql_validation: false,
439 generate_query_fragments: self.supergraph.generate_query_fragments,
440 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
441 enable_defer: self.supergraph.defer_support,
442 },
443 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
444 debug: QueryPlannerDebugConfig {
445 max_evaluated_plans,
446 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
447 },
448 }
449 }
450
451 fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
452 self.apollo_plugins
453 .plugins
454 .get(plugin_name)
455 .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
456 .and_then(|enabled| enabled.as_bool())
457 .unwrap_or(false)
458 }
459}
460
461impl Default for Configuration {
462 fn default() -> Self {
463 Configuration::from_str("").expect("default configuration must be valid")
465 }
466}
467
468#[cfg(test)]
469#[buildstructor::buildstructor]
470impl Configuration {
471 #[builder]
472 pub(crate) fn fake_new(
473 supergraph: Option<Supergraph>,
474 health_check: Option<HealthCheck>,
475 sandbox: Option<Sandbox>,
476 homepage: Option<Homepage>,
477 cors: Option<Cors>,
478 plugins: Map<String, Value>,
479 apollo_plugins: Map<String, Value>,
480 tls: Option<Tls>,
481 notify: Option<Notify<String, graphql::Response>>,
482 apq: Option<Apq>,
483 persisted_query: Option<PersistedQueries>,
484 operation_limits: Option<limits::Config>,
485 chaos: Option<chaos::Config>,
486 uplink: Option<UplinkConfig>,
487 batching: Option<Batching>,
488 experimental_type_conditioned_fetching: Option<bool>,
489 server: Option<Server>,
490 ) -> Result<Self, ConfigurationError> {
491 let configuration = Self {
492 validated_yaml: Default::default(),
493 server: server.unwrap_or_default(),
494 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
495 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
496 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
497 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
498 cors: cors.unwrap_or_default(),
499 limits: operation_limits.unwrap_or_default(),
500 experimental_chaos: chaos.unwrap_or_default(),
501 plugins: UserPlugins {
502 plugins: Some(plugins),
503 },
504 apollo_plugins: ApolloPlugins {
505 plugins: apollo_plugins,
506 },
507 tls: tls.unwrap_or_default(),
508 notify: notify.unwrap_or_default(),
509 apq: apq.unwrap_or_default(),
510 persisted_queries: persisted_query.unwrap_or_default(),
511 uplink,
512 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
513 .unwrap_or_default(),
514 experimental_hoist_orphan_errors: Default::default(),
515 batching: batching.unwrap_or_default(),
516 raw_yaml: None,
517 };
518
519 configuration.validate()
520 }
521}
522
523impl Configuration {
524 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
525 if self.sandbox.enabled && self.homepage.enabled {
527 return Err(ConfigurationError::InvalidConfiguration {
528 message: "sandbox and homepage cannot be enabled at the same time",
529 error: "disable the homepage if you want to enable sandbox".to_string(),
530 });
531 }
532 if self.sandbox.enabled && !self.supergraph.introspection {
534 return Err(ConfigurationError::InvalidConfiguration {
535 message: "sandbox requires introspection",
536 error: "sandbox needs introspection to be enabled".to_string(),
537 });
538 }
539 if !self.supergraph.path.starts_with('/') {
540 return Err(ConfigurationError::InvalidConfiguration {
541 message: "invalid 'server.graphql_path' configuration",
542 error: format!(
543 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
544 self.supergraph.path, self.supergraph.path
545 ),
546 });
547 }
548 if self.supergraph.path.ends_with('*')
549 && !self.supergraph.path.ends_with("/*")
550 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
551 {
552 return Err(ConfigurationError::InvalidConfiguration {
553 message: "invalid 'server.graphql_path' configuration",
554 error: format!(
555 "'{}' is invalid, you can only set a wildcard after a '/'",
556 self.supergraph.path
557 ),
558 });
559 }
560 if self.supergraph.path.contains("/*/") {
561 return Err(ConfigurationError::InvalidConfiguration {
562 message: "invalid 'server.graphql_path' configuration",
563 error: format!(
564 "'{}' 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'",
565 self.supergraph.path
566 ),
567 });
568 }
569
570 if self.persisted_queries.enabled {
572 if self.persisted_queries.safelist.enabled && self.apq.enabled {
573 return Err(ConfigurationError::InvalidConfiguration {
574 message: "apqs must be disabled to enable safelisting",
575 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
576 });
577 } else if !self.persisted_queries.safelist.enabled
578 && self.persisted_queries.safelist.require_id
579 {
580 return Err(ConfigurationError::InvalidConfiguration {
581 message: "safelist must be enabled to require IDs",
582 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
583 });
584 }
585 } else {
586 if self.persisted_queries.safelist.enabled {
588 return Err(ConfigurationError::InvalidConfiguration {
589 message: "persisted queries must be enabled to enable safelisting",
590 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
591 });
592 } else if self.persisted_queries.log_unknown {
593 return Err(ConfigurationError::InvalidConfiguration {
594 message: "persisted queries must be enabled to enable logging unknown operations",
595 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
596 });
597 }
598 }
599
600 if self.apollo_plugin_enabled("response_cache")
602 && self.apollo_plugin_enabled("preview_entity_cache")
603 {
604 return Err(ConfigurationError::InvalidConfiguration {
605 message: "entity cache and response cache features are mutually exclusive",
606 error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
607 });
608 }
609
610 Ok(self)
611 }
612}
613
614impl FromStr for Configuration {
616 type Err = ConfigurationError;
617
618 fn from_str(s: &str) -> Result<Self, Self::Err> {
619 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
620 .validate()
621 }
622}
623
624fn gen_schema(
625 plugins: BTreeMap<String, Schema>,
626 hidden_plugins: Option<BTreeMap<String, Schema>>,
627) -> Schema {
628 schemars::json_schema!({
629 "type": "object",
630 "properties": plugins,
631 "additionalProperties": false,
632 "patternProperties": hidden_plugins
633 .unwrap_or_default()
634 .into_iter()
635 .map(|(k, v)| (format!("^{}$", k), v))
637 .collect::<BTreeMap<_, _>>()
638 })
639}
640
641#[derive(Clone, Debug, Default, Deserialize, Serialize)]
647#[serde(transparent)]
648pub(crate) struct ApolloPlugins {
649 pub(crate) plugins: Map<String, Value>,
650}
651
652impl JsonSchema for ApolloPlugins {
653 fn schema_name() -> std::borrow::Cow<'static, str> {
654 stringify!(Plugins).into()
655 }
656
657 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
658 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
662 .sorted_by_key(|factory| factory.name.clone())
663 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
664 .partition_map(|factory| {
665 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
666 let schema = factory.create_schema(generator);
667 if factory.hidden_from_config_json_schema {
669 Either::Right((key, schema))
670 } else {
671 Either::Left((key, schema))
672 }
673 });
674 gen_schema(
675 plugin_entries.into_iter().collect(),
676 Some(hidden_plugin_entries.into_iter().collect()),
677 )
678 }
679}
680
681#[derive(Clone, Debug, Default, Deserialize, Serialize)]
686#[serde(transparent)]
687pub(crate) struct UserPlugins {
688 pub(crate) plugins: Option<Map<String, Value>>,
689}
690
691impl JsonSchema for UserPlugins {
692 fn schema_name() -> std::borrow::Cow<'static, str> {
693 stringify!(Plugins).into()
694 }
695
696 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
697 let plugins = crate::plugin::plugins()
701 .sorted_by_key(|factory| factory.name.clone())
702 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
703 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
704 .collect();
705 gen_schema(plugins, None)
706 }
707}
708
709#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
711#[serde(deny_unknown_fields)]
712#[serde(default)]
713pub(crate) struct Supergraph {
714 pub(crate) listen: ListenAddr,
717
718 #[serde(deserialize_with = "humantime_serde::deserialize")]
720 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
721 pub(crate) connection_shutdown_timeout: Duration,
722
723 pub(crate) path: String,
726
727 pub(crate) introspection: bool,
730
731 pub(crate) generate_query_fragments: bool,
734
735 pub(crate) defer_support: bool,
737
738 pub(crate) query_planning: QueryPlanning,
740
741 pub(crate) early_cancel: bool,
746
747 pub(crate) enable_result_coercion_errors: bool,
754
755 pub(crate) experimental_log_on_broken_pipe: bool,
758}
759
760const fn default_generate_query_fragments() -> bool {
761 true
762}
763
764fn default_defer_support() -> bool {
765 true
766}
767
768#[buildstructor::buildstructor]
769impl Supergraph {
770 #[builder]
771 pub(crate) fn new(
772 listen: Option<ListenAddr>,
773 path: Option<String>,
774 connection_shutdown_timeout: Option<Duration>,
775 introspection: Option<bool>,
776 defer_support: Option<bool>,
777 query_planning: Option<QueryPlanning>,
778 generate_query_fragments: Option<bool>,
779 early_cancel: Option<bool>,
780 experimental_log_on_broken_pipe: Option<bool>,
781 insert_result_coercion_errors: Option<bool>,
782 ) -> Self {
783 Self {
784 listen: listen.unwrap_or_else(default_graphql_listen),
785 path: path.unwrap_or_else(default_graphql_path),
786 connection_shutdown_timeout: connection_shutdown_timeout
787 .unwrap_or_else(default_connection_shutdown_timeout),
788 introspection: introspection.unwrap_or_else(default_graphql_introspection),
789 defer_support: defer_support.unwrap_or_else(default_defer_support),
790 query_planning: query_planning.unwrap_or_default(),
791 generate_query_fragments: generate_query_fragments
792 .unwrap_or_else(default_generate_query_fragments),
793 early_cancel: early_cancel.unwrap_or_default(),
794 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
795 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
796 }
797 }
798}
799
800#[cfg(test)]
801#[buildstructor::buildstructor]
802impl Supergraph {
803 #[builder]
804 pub(crate) fn fake_new(
805 listen: Option<ListenAddr>,
806 path: Option<String>,
807 connection_shutdown_timeout: Option<Duration>,
808 introspection: Option<bool>,
809 defer_support: Option<bool>,
810 query_planning: Option<QueryPlanning>,
811 generate_query_fragments: Option<bool>,
812 early_cancel: Option<bool>,
813 experimental_log_on_broken_pipe: Option<bool>,
814 insert_result_coercion_errors: Option<bool>,
815 ) -> Self {
816 Self {
817 listen: listen.unwrap_or_else(test_listen),
818 path: path.unwrap_or_else(default_graphql_path),
819 connection_shutdown_timeout: connection_shutdown_timeout
820 .unwrap_or_else(default_connection_shutdown_timeout),
821 introspection: introspection.unwrap_or_else(default_graphql_introspection),
822 defer_support: defer_support.unwrap_or_else(default_defer_support),
823 query_planning: query_planning.unwrap_or_default(),
824 generate_query_fragments: generate_query_fragments
825 .unwrap_or_else(default_generate_query_fragments),
826 early_cancel: early_cancel.unwrap_or_default(),
827 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
828 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
829 }
830 }
831}
832
833impl Default for Supergraph {
834 fn default() -> Self {
835 Self::builder().build()
836 }
837}
838
839impl Supergraph {
840 pub(crate) fn sanitized_path(&self) -> String {
842 let mut path = self.path.clone();
843 if self.path.ends_with("/*") {
844 path = format!("{}router_extra_path", self.path);
846 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
847 let new_path = SUPERGRAPH_ENDPOINT_REGEX
848 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
849 path = new_path.to_string();
850 }
851
852 path
853 }
854}
855
856#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
858#[serde(deny_unknown_fields)]
859pub(crate) struct Router {
860 #[serde(default)]
861 pub(crate) cache: Cache,
862}
863
864#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
866#[serde(deny_unknown_fields, default)]
867pub(crate) struct Apq {
868 pub(crate) enabled: bool,
870
871 pub(crate) router: Router,
872
873 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
874}
875
876#[cfg(test)]
877#[buildstructor::buildstructor]
878impl Apq {
879 #[builder]
880 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
881 Self {
882 enabled: enabled.unwrap_or_else(default_apq),
883 ..Default::default()
884 }
885 }
886}
887
888#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
890#[serde(deny_unknown_fields, default)]
891pub(crate) struct SubgraphApq {
892 pub(crate) enabled: bool,
894}
895
896fn default_apq() -> bool {
897 true
898}
899
900impl Default for Apq {
901 fn default() -> Self {
902 Self {
903 enabled: default_apq(),
904 router: Default::default(),
905 subgraph: Default::default(),
906 }
907 }
908}
909
910#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
912#[serde(deny_unknown_fields, default)]
913pub(crate) struct QueryPlanning {
914 pub(crate) cache: QueryPlanCache,
916 pub(crate) warmed_up_queries: Option<usize>,
921
922 pub(crate) experimental_plans_limit: Option<u32>,
930
931 pub(crate) experimental_paths_limit: Option<u32>,
944
945 pub(crate) experimental_reuse_query_plans: bool,
948
949 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
953}
954
955#[buildstructor::buildstructor]
956impl QueryPlanning {
957 #[builder]
958 #[allow(dead_code)]
959 pub(crate) fn new(
960 cache: Option<QueryPlanCache>,
961 warmed_up_queries: Option<usize>,
962 experimental_plans_limit: Option<u32>,
963 experimental_paths_limit: Option<u32>,
964 experimental_reuse_query_plans: Option<bool>,
965 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
966 ) -> Self {
967 Self {
968 cache: cache.unwrap_or_default(),
969 warmed_up_queries,
970 experimental_plans_limit,
971 experimental_paths_limit,
972 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
973 experimental_cooperative_cancellation: experimental_cooperative_cancellation
974 .unwrap_or_default(),
975 }
976 }
977}
978
979#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
981#[serde(deny_unknown_fields, default)]
982pub(crate) struct QueryPlanCache {
983 pub(crate) in_memory: InMemoryCache,
985 pub(crate) redis: Option<QueryPlanRedisCache>,
987}
988
989#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
990#[serde(deny_unknown_fields)]
991pub(crate) struct QueryPlanRedisCache {
993 pub(crate) urls: Vec<url::Url>,
995
996 pub(crate) username: Option<String>,
998 pub(crate) password: Option<String>,
1000
1001 #[serde(
1002 deserialize_with = "humantime_serde::deserialize",
1003 default = "default_timeout"
1004 )]
1005 #[schemars(with = "Option<String>", default)]
1006 pub(crate) timeout: Duration,
1008
1009 #[serde(
1010 deserialize_with = "humantime_serde::deserialize",
1011 default = "default_query_plan_cache_ttl"
1012 )]
1013 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1014 pub(crate) ttl: Duration,
1016
1017 pub(crate) namespace: Option<String>,
1019
1020 #[serde(default)]
1021 pub(crate) tls: Option<TlsClient>,
1023
1024 #[serde(default = "default_required_to_start")]
1025 pub(crate) required_to_start: bool,
1027
1028 #[serde(default = "default_reset_ttl")]
1029 pub(crate) reset_ttl: bool,
1031
1032 #[serde(default = "default_query_planner_cache_pool_size")]
1033 pub(crate) pool_size: u32,
1035}
1036
1037fn default_query_plan_cache_ttl() -> Duration {
1038 Duration::from_secs(86400 * 30)
1040}
1041
1042fn default_query_planner_cache_pool_size() -> u32 {
1043 1
1044}
1045
1046#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1048#[serde(deny_unknown_fields, default)]
1049pub(crate) struct Cache {
1050 pub(crate) in_memory: InMemoryCache,
1052 pub(crate) redis: Option<RedisCache>,
1054}
1055
1056impl From<QueryPlanCache> for Cache {
1057 fn from(value: QueryPlanCache) -> Self {
1058 Cache {
1059 in_memory: value.in_memory,
1060 redis: value.redis.map(Into::into),
1061 }
1062 }
1063}
1064
1065#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1066#[serde(deny_unknown_fields)]
1067pub(crate) struct InMemoryCache {
1069 pub(crate) limit: NonZeroUsize,
1071}
1072
1073impl Default for InMemoryCache {
1074 fn default() -> Self {
1075 Self {
1076 limit: DEFAULT_CACHE_CAPACITY,
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1082#[serde(deny_unknown_fields)]
1083pub(crate) struct RedisCache {
1085 pub(crate) urls: Vec<url::Url>,
1087
1088 pub(crate) username: Option<String>,
1090 pub(crate) password: Option<String>,
1092
1093 #[serde(
1094 deserialize_with = "humantime_serde::deserialize",
1095 default = "default_timeout"
1096 )]
1097 #[schemars(with = "Option<String>", default)]
1098 pub(crate) timeout: Duration,
1100
1101 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1102 #[schemars(with = "Option<String>", default)]
1103 pub(crate) ttl: Option<Duration>,
1105
1106 pub(crate) namespace: Option<String>,
1108
1109 #[serde(default)]
1110 pub(crate) tls: Option<TlsClient>,
1112
1113 #[serde(default = "default_required_to_start")]
1114 pub(crate) required_to_start: bool,
1116
1117 #[serde(default = "default_reset_ttl")]
1118 pub(crate) reset_ttl: bool,
1120
1121 #[serde(default = "default_pool_size")]
1122 pub(crate) pool_size: u32,
1124 #[serde(
1125 deserialize_with = "humantime_serde::deserialize",
1126 default = "default_metrics_interval"
1127 )]
1128 #[schemars(with = "Option<String>", default)]
1129 pub(crate) metrics_interval: Duration,
1131}
1132
1133fn default_timeout() -> Duration {
1134 Duration::from_millis(500)
1135}
1136
1137pub(crate) fn default_required_to_start() -> bool {
1138 false
1139}
1140
1141pub(crate) fn default_pool_size() -> u32 {
1142 1
1143}
1144
1145pub(crate) fn default_metrics_interval() -> Duration {
1146 Duration::from_secs(1)
1147}
1148
1149impl From<QueryPlanRedisCache> for RedisCache {
1150 fn from(value: QueryPlanRedisCache) -> Self {
1151 RedisCache {
1152 urls: value.urls,
1153 username: value.username,
1154 password: value.password,
1155 timeout: value.timeout,
1156 ttl: Some(value.ttl),
1157 namespace: value.namespace,
1158 tls: value.tls,
1159 required_to_start: value.required_to_start,
1160 reset_ttl: value.reset_ttl,
1161 pool_size: value.pool_size,
1162 metrics_interval: default_metrics_interval(),
1163 }
1164 }
1165}
1166
1167fn default_reset_ttl() -> bool {
1168 true
1169}
1170
1171#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1173#[serde(deny_unknown_fields)]
1174#[serde(default)]
1175pub(crate) struct Tls {
1176 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1180 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1182 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1184}
1185
1186#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1188#[serde(deny_unknown_fields)]
1189pub(crate) struct TlsSupergraph {
1190 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1192 #[schemars(with = "String")]
1193 pub(crate) certificate: CertificateDer<'static>,
1194 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1196 #[schemars(with = "String")]
1197 pub(crate) key: PrivateKeyDer<'static>,
1198 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1200 #[schemars(with = "String")]
1201 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1202}
1203
1204impl TlsSupergraph {
1205 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1206 let mut certificates = vec![self.certificate.clone()];
1207 certificates.extend(self.certificate_chain.iter().cloned());
1208
1209 let mut config = ServerConfig::builder()
1210 .with_no_client_auth()
1211 .with_single_cert(certificates, self.key.clone_key())
1212 .map_err(ApolloRouterError::Rustls)?;
1213 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1214
1215 Ok(Arc::new(config))
1216 }
1217}
1218
1219fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1220where
1221 D: Deserializer<'de>,
1222{
1223 let data = String::deserialize(deserializer)?;
1224
1225 load_certs(&data)
1226 .map_err(serde::de::Error::custom)
1227 .and_then(|mut certs| {
1228 if certs.len() > 1 {
1229 Err(serde::de::Error::custom("expected exactly one certificate"))
1230 } else {
1231 certs
1232 .pop()
1233 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1234 }
1235 })
1236}
1237
1238fn deserialize_certificate_chain<'de, D>(
1239 deserializer: D,
1240) -> Result<Vec<CertificateDer<'static>>, D::Error>
1241where
1242 D: Deserializer<'de>,
1243{
1244 let data = String::deserialize(deserializer)?;
1245
1246 load_certs(&data).map_err(serde::de::Error::custom)
1247}
1248
1249fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1250where
1251 D: Deserializer<'de>,
1252{
1253 let data = String::deserialize(deserializer)?;
1254
1255 load_key(&data).map_err(serde::de::Error::custom)
1256}
1257
1258#[derive(thiserror::Error, Debug)]
1259#[error("could not load TLS certificate: {0}")]
1260struct LoadCertError(std::io::Error);
1261
1262pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1263 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1264 .collect::<Result<Vec<_>, _>>()
1265 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1266}
1267
1268pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1269 let mut reader = BufReader::new(data.as_bytes());
1270 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1271
1272 let private_key = match key_iterator.next() {
1273 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1274 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1275 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1276 Some(Err(e)) => {
1277 return Err(io::Error::new(
1278 io::ErrorKind::InvalidInput,
1279 format!("could not parse the key: {e}"),
1280 ));
1281 }
1282 Some(_) => {
1283 return Err(io::Error::new(
1284 io::ErrorKind::InvalidInput,
1285 "expected a private key",
1286 ));
1287 }
1288 None => {
1289 return Err(io::Error::new(
1290 io::ErrorKind::InvalidInput,
1291 "could not find a private key",
1292 ));
1293 }
1294 };
1295
1296 if key_iterator.next().is_some() {
1297 return Err(io::Error::new(
1298 io::ErrorKind::InvalidInput,
1299 "expected exactly one private key",
1300 ));
1301 }
1302 Ok(private_key)
1303}
1304
1305#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1307#[serde(deny_unknown_fields)]
1308#[serde(default)]
1309pub(crate) struct TlsClient {
1310 pub(crate) certificate_authorities: Option<String>,
1312 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1314}
1315
1316#[buildstructor::buildstructor]
1317impl TlsClient {
1318 #[builder]
1319 pub(crate) fn new(
1320 certificate_authorities: Option<String>,
1321 client_authentication: Option<Arc<TlsClientAuth>>,
1322 ) -> Self {
1323 Self {
1324 certificate_authorities,
1325 client_authentication,
1326 }
1327 }
1328}
1329
1330impl Default for TlsClient {
1331 fn default() -> Self {
1332 Self::builder().build()
1333 }
1334}
1335
1336#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1338#[serde(deny_unknown_fields)]
1339pub(crate) struct TlsClientAuth {
1340 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1342 #[schemars(with = "String")]
1343 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1344 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1346 #[schemars(with = "String")]
1347 pub(crate) key: PrivateKeyDer<'static>,
1348}
1349
1350#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1352#[serde(deny_unknown_fields)]
1353#[serde(default)]
1354pub(crate) struct Sandbox {
1355 pub(crate) enabled: bool,
1357}
1358
1359fn default_sandbox() -> bool {
1360 false
1361}
1362
1363#[buildstructor::buildstructor]
1364impl Sandbox {
1365 #[builder]
1366 pub(crate) fn new(enabled: Option<bool>) -> Self {
1367 Self {
1368 enabled: enabled.unwrap_or_else(default_sandbox),
1369 }
1370 }
1371}
1372
1373#[cfg(test)]
1374#[buildstructor::buildstructor]
1375impl Sandbox {
1376 #[builder]
1377 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1378 Self {
1379 enabled: enabled.unwrap_or_else(default_sandbox),
1380 }
1381 }
1382}
1383
1384impl Default for Sandbox {
1385 fn default() -> Self {
1386 Self::builder().build()
1387 }
1388}
1389
1390#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1392#[serde(deny_unknown_fields)]
1393#[serde(default)]
1394pub(crate) struct Homepage {
1395 pub(crate) enabled: bool,
1397 pub(crate) graph_ref: Option<String>,
1400}
1401
1402fn default_homepage() -> bool {
1403 true
1404}
1405
1406#[buildstructor::buildstructor]
1407impl Homepage {
1408 #[builder]
1409 pub(crate) fn new(enabled: Option<bool>) -> Self {
1410 Self {
1411 enabled: enabled.unwrap_or_else(default_homepage),
1412 graph_ref: None,
1413 }
1414 }
1415}
1416
1417#[cfg(test)]
1418#[buildstructor::buildstructor]
1419impl Homepage {
1420 #[builder]
1421 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1422 Self {
1423 enabled: enabled.unwrap_or_else(default_homepage),
1424 graph_ref: None,
1425 }
1426 }
1427}
1428
1429impl Default for Homepage {
1430 fn default() -> Self {
1431 Self::builder().enabled(default_homepage()).build()
1432 }
1433}
1434
1435#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1437#[serde(untagged)]
1438pub enum ListenAddr {
1439 SocketAddr(SocketAddr),
1441 #[cfg(unix)]
1443 UnixSocket(std::path::PathBuf),
1444}
1445
1446impl ListenAddr {
1447 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1448 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1449 if let Self::SocketAddr(addr) = self {
1450 Some((addr.ip(), addr.port()))
1451 } else {
1452 None
1453 }
1454 }
1455}
1456
1457impl From<SocketAddr> for ListenAddr {
1458 fn from(addr: SocketAddr) -> Self {
1459 Self::SocketAddr(addr)
1460 }
1461}
1462
1463#[allow(clippy::from_over_into)]
1464impl Into<serde_json::Value> for ListenAddr {
1465 fn into(self) -> serde_json::Value {
1466 match self {
1467 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1470 #[cfg(unix)]
1471 Self::UnixSocket(path) => serde_json::Value::String(
1472 path.as_os_str()
1473 .to_str()
1474 .expect("unsupported non-UTF-8 path")
1475 .to_string(),
1476 ),
1477 }
1478 }
1479}
1480
1481#[cfg(unix)]
1482impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1483 for ListenAddr
1484{
1485 fn from(
1486 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1487 ) -> Self {
1488 match addr {
1489 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1490 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1491 addr.as_pathname()
1492 .map(ToOwned::to_owned)
1493 .unwrap_or_default(),
1494 ),
1495 }
1496 }
1497}
1498
1499impl fmt::Display for ListenAddr {
1500 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1501 match self {
1502 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1503 #[cfg(unix)]
1504 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1505 }
1506 }
1507}
1508
1509fn default_graphql_path() -> String {
1510 String::from("/")
1511}
1512
1513fn default_graphql_introspection() -> bool {
1514 false
1515}
1516
1517fn default_connection_shutdown_timeout() -> Duration {
1518 Duration::from_secs(60)
1519}
1520
1521#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1522#[serde(deny_unknown_fields, rename_all = "snake_case")]
1523pub(crate) enum BatchingMode {
1524 #[default]
1526 BatchHttpLink,
1527}
1528
1529#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1531#[serde(deny_unknown_fields)]
1532pub(crate) struct Batching {
1533 #[serde(default)]
1535 pub(crate) enabled: bool,
1536
1537 pub(crate) mode: BatchingMode,
1539
1540 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1542
1543 #[serde(default)]
1545 pub(crate) maximum_size: Option<usize>,
1546}
1547
1548#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1550pub(crate) struct CommonBatchingConfig {
1551 pub(crate) enabled: bool,
1553}
1554
1555impl Batching {
1556 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1558 match &self.subgraph {
1559 Some(subgraph_batching_config) => {
1560 if subgraph_batching_config.all.enabled {
1562 subgraph_batching_config
1566 .subgraphs
1567 .get(service_name)
1568 .is_none_or(|x| x.enabled)
1569 } else {
1570 subgraph_batching_config
1573 .subgraphs
1574 .get(service_name)
1575 .is_some_and(|x| x.enabled)
1576 }
1577 }
1578 None => false,
1579 }
1580 }
1581
1582 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1583 match self.maximum_size {
1584 Some(maximum_size) => batch.len() > maximum_size,
1585 None => false,
1586 }
1587 }
1588}
1589
1590#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1597#[serde(deny_unknown_fields)]
1598pub(crate) struct HoistOrphanErrors {
1599 #[serde(default)]
1601 pub(crate) enabled: bool,
1602}