Skip to main content

fraiseql_server/config/
mod.rs

1//! Runtime configuration types for the FraiseQL server.
2//!
3//! Structs in this module are deserialized from `fraiseql.toml` (via the
4//! `loader` sub-module) or assembled from environment variables (via the
5//! `env` sub-module).
6//! Sub-modules contain configuration for specific subsystems such as CORS,
7//! metrics, rate limiting, and TLS.
8
9use std::{collections::HashMap, path::PathBuf};
10
11use serde::Deserialize;
12
13pub mod cors;
14pub mod env;
15pub mod error_sanitization;
16pub mod loader;
17pub mod metrics;
18pub mod pool_tuning;
19pub mod rate_limiting;
20#[cfg(test)]
21mod tests;
22pub mod tracing;
23pub mod validation;
24
25// Re-export config types
26pub use cors::CorsConfig;
27pub use error_sanitization::{ErrorSanitizationConfig, ErrorSanitizer};
28pub use metrics::{LatencyTargets, MetricsConfig, SloConfig};
29#[allow(deprecated)] // Reason: re-export deprecated alias for backwards compatibility
30pub use pool_tuning::{PoolPressureMonitorConfig, PoolTuningConfig};
31pub use rate_limiting::{BackpressureConfig, RateLimitRule, RateLimitingConfig};
32pub use tracing::TracingConfig;
33
34/// Configuration for durable usage counter persistence.
35///
36/// Add a `[usage]` section to `fraiseql.toml` (or `ServerConfig`) to enable:
37///
38/// ```toml
39/// [usage]
40/// flush_interval_secs = 60
41/// ```
42///
43/// When absent (default), the [`NoopBackend`] is used and counters are
44/// in-memory only (reset on process restart).
45///
46/// [`NoopBackend`]: crate::usage::aggregator::NoopBackend
47#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
48pub struct UsagePersistenceConfig {
49    /// How often (in seconds) to flush in-memory counters to PostgreSQL.
50    ///
51    /// Defaults to `60` seconds.
52    #[serde(default = "default_flush_interval_secs")]
53    pub flush_interval_secs: u64,
54}
55
56const fn default_flush_interval_secs() -> u64 {
57    60
58}
59
60/// Root configuration structure loaded from `fraiseql.toml`.
61#[derive(Debug, Clone, Deserialize)]
62pub struct RuntimeConfig {
63    /// HTTP server binding, TLS, and connection-limit settings.
64    pub server:   HttpServerConfig,
65    /// Primary database connection and pool settings.
66    pub database: DatabaseConfig,
67
68    /// Named webhook route configurations, keyed by route name.
69    #[serde(default)]
70    pub webhooks: HashMap<String, WebhookRouteConfig>,
71
72    /// Named file-upload route configurations, keyed by route name.
73    #[serde(default)]
74    pub files: HashMap<String, FileConfig>,
75
76    /// Optional JWT authentication and OAuth provider configuration.
77    #[serde(default)]
78    pub auth: Option<AuthConfig>,
79
80    /// Reserved: placeholder for future notification system configuration.
81    #[serde(default)]
82    pub notifications: Option<NotificationsConfig>,
83
84    /// Event observer configurations, keyed by observer name.
85    #[serde(default)]
86    pub observers: HashMap<String, ObserverConfig>,
87
88    /// Request interceptor chains, keyed by interceptor name.
89    #[serde(default)]
90    pub interceptors: HashMap<String, Vec<String>>,
91
92    /// Optional rate-limiting rules and backpressure thresholds.
93    #[serde(default)]
94    pub rate_limiting: Option<RateLimitingConfig>,
95
96    /// Optional CORS origin and header policy.
97    #[serde(default)]
98    pub cors: Option<CorsConfig>,
99
100    /// Optional Prometheus metrics and SLO tracking configuration.
101    #[serde(default)]
102    pub metrics: Option<MetricsConfig>,
103
104    /// Optional distributed-tracing (OTLP/Jaeger) configuration.
105    #[serde(default)]
106    pub tracing: Option<TracingConfig>,
107
108    /// Optional structured-logging configuration.
109    #[serde(default)]
110    pub logging: Option<LoggingConfig>,
111
112    /// Named object-storage backend configurations, keyed by storage name.
113    #[serde(default)]
114    pub storage: HashMap<String, StorageConfig>,
115
116    /// Reserved: placeholder for future search-indexing configuration.
117    #[serde(default)]
118    pub search: Option<SearchConfig>,
119
120    /// Reserved: placeholder for future advanced caching strategy configuration.
121    #[serde(default)]
122    pub cache: Option<CacheConfig>,
123
124    /// Reserved: placeholder for future job-queue configuration.
125    #[serde(default)]
126    pub queues: Option<QueueConfig>,
127
128    /// Reserved: placeholder for future real-time update configuration.
129    #[serde(default)]
130    pub realtime: Option<RealtimeConfig>,
131
132    /// Reserved: placeholder for future custom-endpoint configuration.
133    #[serde(default)]
134    pub custom_endpoints: Option<CustomEndpointsConfig>,
135
136    /// Graceful-shutdown timing and health-check endpoint paths.
137    #[serde(default)]
138    pub lifecycle: Option<LifecycleConfig>,
139}
140
141/// HTTP server binding configuration.
142#[derive(Debug, Clone, Deserialize)]
143pub struct HttpServerConfig {
144    /// TCP port to listen on.  Default: `4000`.
145    #[serde(default = "default_port")]
146    pub port: u16,
147
148    /// Network interface to bind.  Default: `"127.0.0.1"`.
149    #[serde(default = "default_host")]
150    pub host: String,
151
152    /// Number of async worker threads.  `None` uses the Tokio default (number of CPU cores).
153    #[serde(default)]
154    pub workers: Option<usize>,
155
156    /// Optional TLS certificate and private key paths.
157    #[serde(default)]
158    pub tls: Option<TlsConfig>,
159
160    /// Optional per-request and concurrency limits.
161    #[serde(default)]
162    pub limits: Option<ServerLimitsConfig>,
163}
164
165const fn default_port() -> u16 {
166    4000
167}
168fn default_host() -> String {
169    "127.0.0.1".to_string()
170}
171
172/// TLS certificate and private key paths for HTTPS listeners.
173#[derive(Debug, Clone, Deserialize)]
174pub struct TlsConfig {
175    /// Path to the PEM-encoded TLS certificate (or certificate chain).
176    pub cert_file: PathBuf,
177    /// Path to the PEM-encoded private key corresponding to `cert_file`.
178    pub key_file:  PathBuf,
179}
180
181/// Per-request body size and concurrency limits for the HTTP server.
182#[derive(Debug, Clone, Deserialize)]
183pub struct ServerLimitsConfig {
184    /// Maximum allowed request body size as a human-readable string (e.g. `"10MB"`).  Default:
185    /// `"10MB"`.
186    #[serde(default = "default_max_request_size")]
187    pub max_request_size: String,
188
189    /// Maximum time to process a single request (e.g. `"30s"`).  Default: `"30s"`.
190    #[serde(default = "default_request_timeout")]
191    pub request_timeout: String,
192
193    /// Maximum number of requests being processed simultaneously.  Default: `1000`.
194    #[serde(default = "default_max_concurrent")]
195    pub max_concurrent_requests: usize,
196
197    /// Maximum number of requests waiting in the accept queue.  Default: `5000`.
198    #[serde(default = "default_max_queue_depth")]
199    pub max_queue_depth: usize,
200}
201
202fn default_max_request_size() -> String {
203    "10MB".to_string()
204}
205fn default_request_timeout() -> String {
206    "30s".to_string()
207}
208const fn default_max_concurrent() -> usize {
209    1000
210}
211const fn default_max_queue_depth() -> usize {
212    5000
213}
214
215/// Primary database connection and connection-pool configuration.
216#[derive(Debug, Clone, Deserialize)]
217pub struct DatabaseConfig {
218    /// Name of the environment variable that holds the database connection URL.
219    pub url_env: String,
220
221    /// Maximum number of connections in the pool.  Default: `10`.
222    #[serde(default = "default_pool_size")]
223    pub pool_size: u32,
224
225    /// How long to wait for an available connection before returning an error (e.g. `"5s"`).
226    #[serde(default)]
227    pub pool_timeout: Option<String>,
228
229    /// Per-query statement timeout sent to the database (e.g. `"30s"`).
230    #[serde(default)]
231    pub statement_timeout: Option<String>,
232
233    /// Optional read-replica pools used for load balancing SELECT queries.
234    #[serde(default)]
235    pub replicas: Vec<ReplicaConfig>,
236
237    /// How often to ping the database to verify liveness (e.g. `"60s"`).
238    #[serde(default)]
239    pub health_check_interval: Option<String>,
240}
241
242const fn default_pool_size() -> u32 {
243    10
244}
245
246/// Connection configuration for a single read replica.
247#[derive(Debug, Clone, Deserialize)]
248pub struct ReplicaConfig {
249    /// Name of the environment variable that holds this replica's connection URL.
250    pub url_env: String,
251
252    /// Relative weight for load-balancing SELECT queries across replicas.  Default: `1`.
253    #[serde(default = "default_weight")]
254    pub weight: u32,
255}
256
257const fn default_weight() -> u32 {
258    1
259}
260
261/// Lifecycle configuration for graceful shutdown
262#[derive(Debug, Clone, Deserialize)]
263pub struct LifecycleConfig {
264    /// Time to wait for in-flight requests to complete
265    #[serde(default = "default_shutdown_timeout")]
266    pub shutdown_timeout: String,
267
268    /// Time to wait before starting shutdown (for load balancer deregistration)
269    #[serde(default = "default_shutdown_delay")]
270    pub shutdown_delay: String,
271
272    /// Health check endpoint path
273    #[serde(default = "default_health_path")]
274    pub health_path: String,
275
276    /// Readiness check endpoint path
277    #[serde(default = "default_ready_path")]
278    pub ready_path: String,
279}
280
281impl Default for LifecycleConfig {
282    fn default() -> Self {
283        Self {
284            shutdown_timeout: default_shutdown_timeout(),
285            shutdown_delay:   default_shutdown_delay(),
286            health_path:      default_health_path(),
287            ready_path:       default_ready_path(),
288        }
289    }
290}
291
292fn default_shutdown_timeout() -> String {
293    "30s".to_string()
294}
295fn default_shutdown_delay() -> String {
296    "5s".to_string()
297}
298fn default_health_path() -> String {
299    "/health".to_string()
300}
301fn default_ready_path() -> String {
302    "/ready".to_string()
303}
304
305/// Configuration for a single incoming webhook route.
306#[derive(Debug, Clone, Deserialize)]
307pub struct WebhookRouteConfig {
308    /// Name of the environment variable that holds the webhook signing secret.
309    pub secret_env: String,
310    /// Webhook provider identifier (e.g. `"github"`, `"stripe"`).
311    pub provider:   String,
312    /// URL path override; if absent, the route name is used as the path segment.
313    #[serde(default)]
314    pub path:       Option<String>,
315}
316
317/// Configuration for a file-upload route.
318#[derive(Debug, Clone, Deserialize)]
319pub struct FileConfig {
320    /// Named storage backend (must match a key in `storage`).
321    pub storage:  String,
322    /// Maximum upload size as a human-readable string (e.g. `"50MB"`).
323    pub max_size: String,
324    /// URL path prefix for upload and download endpoints.
325    #[serde(default)]
326    pub path:     Option<String>,
327}
328
329/// JWT authentication and OAuth provider configuration.
330#[derive(Debug, Clone, Deserialize)]
331pub struct AuthConfig {
332    /// JWT signing secret configuration.
333    pub jwt:               JwtConfig,
334    /// Named OAuth2/OIDC provider configurations.
335    #[serde(default)]
336    pub providers:         HashMap<String, OAuthProviderConfig>,
337    /// Base URL for OAuth callback endpoints (e.g. `"https://api.example.com"`).
338    #[serde(default)]
339    pub callback_base_url: Option<String>,
340}
341
342/// JWT signing-secret configuration.
343#[derive(Debug, Clone, Deserialize)]
344pub struct JwtConfig {
345    /// Name of the environment variable that holds the JWT signing secret.
346    pub secret_env: String,
347}
348
349/// Configuration for a single OAuth2/OIDC provider.
350#[derive(Debug, Clone, Deserialize)]
351pub struct OAuthProviderConfig {
352    /// Well-known provider type identifier (e.g. `"auth0"`, `"github"`, `"google"`).
353    pub provider_type:     String,
354    /// Name of the environment variable that holds the OAuth client ID.
355    pub client_id_env:     String,
356    /// Name of the environment variable that holds the OAuth client secret.
357    pub client_secret_env: String,
358    /// OIDC issuer URL (required for providers that support OIDC discovery).
359    #[serde(default)]
360    pub issuer_url:        Option<String>,
361}
362
363/// Reserved: placeholder for future notification system configuration.
364#[derive(Debug, Clone, Deserialize)]
365pub struct NotificationsConfig {}
366
367/// Configuration for a single event observer (entity-event → action).
368#[derive(Debug, Clone, Deserialize)]
369pub struct ObserverConfig {
370    /// GraphQL entity type name to watch (e.g. `"User"`).
371    pub entity:  String,
372    /// List of mutation operation names that trigger this observer.
373    pub events:  Vec<String>,
374    /// Ordered list of actions to execute when an observed event fires.
375    pub actions: Vec<ActionConfig>,
376}
377
378/// A single action within an observer pipeline.
379#[derive(Debug, Clone, Deserialize)]
380pub struct ActionConfig {
381    /// Action type identifier (e.g. `"webhook"`, `"email"`, `"queue"`).
382    #[serde(rename = "type")]
383    pub action_type: String,
384    /// Optional Jinja2-style template used to render the action payload.
385    #[serde(default)]
386    pub template:    Option<String>,
387}
388
389// These types are now defined in their own modules and re-exported above
390
391/// Reserved: placeholder for future structured-logging configuration.
392#[derive(Debug, Clone, Deserialize)]
393pub struct LoggingConfig {}
394
395/// Configuration for a single object-storage backend.
396#[derive(Debug, Clone, Deserialize)]
397pub struct StorageConfig {
398    /// Storage backend identifier (e.g. `"s3"`, `"gcs"`, `"local"`).
399    pub backend:          String,
400    /// Bucket or container name (required for cloud backends).
401    #[serde(default)]
402    pub bucket:           Option<String>,
403    /// Local filesystem path (used by the `"local"` backend).
404    #[serde(default)]
405    pub path:             Option<String>,
406    /// Cloud region (e.g. `"eu-west-1"` for AWS, `"fr-par"` for Scaleway).
407    #[serde(default)]
408    pub region:           Option<String>,
409    /// Custom endpoint URL (for S3-compatible providers, Azurite, and
410    /// fake-gcs-server local-development emulators).
411    #[serde(default)]
412    pub endpoint:         Option<String>,
413    /// GCP project ID (used by the `"gcs"` backend).
414    #[serde(default)]
415    pub project_id:       Option<String>,
416    /// Azure storage account name (used by the `"azure"` backend).
417    #[serde(default)]
418    pub account_name:     Option<String>,
419    /// Maximum upload size in bytes for this storage backend.
420    ///
421    /// Defaults to `104_857_600` (100 `MiB`). Uploads exceeding this size are
422    /// rejected with HTTP 413 before touching the backend.
423    #[serde(default = "default_max_upload_bytes")]
424    pub max_upload_bytes: usize,
425}
426
427const fn default_max_upload_bytes() -> usize {
428    100 * 1024 * 1024 // 100 MiB
429}
430
431/// Reserved: placeholder for future full-text search indexing configuration.
432#[derive(Debug, Clone, Deserialize)]
433pub struct SearchConfig {}
434
435/// Reserved: placeholder for future advanced query-result caching configuration.
436#[derive(Debug, Clone, Deserialize)]
437pub struct CacheConfig {}
438
439/// Reserved: placeholder for future background job-queue configuration.
440#[derive(Debug, Clone, Deserialize)]
441pub struct QueueConfig {}
442
443/// Reserved: placeholder for future real-time subscription update configuration.
444#[derive(Debug, Clone, Deserialize)]
445pub struct RealtimeConfig {}
446
447/// Reserved: placeholder for future custom HTTP endpoint configuration.
448#[derive(Debug, Clone, Deserialize)]
449pub struct CustomEndpointsConfig {}
450
451// ---------------------------------------------------------------------------
452// Builders
453// ---------------------------------------------------------------------------
454
455impl HttpServerConfig {
456    /// Returns a builder for `HttpServerConfig`.
457    #[must_use = "builder does nothing until .build() is called"]
458    pub fn builder() -> HttpServerConfigBuilder {
459        HttpServerConfigBuilder::default()
460    }
461}
462
463/// Builder for [`HttpServerConfig`].
464#[derive(Debug)]
465pub struct HttpServerConfigBuilder {
466    port:    u16,
467    host:    String,
468    workers: Option<usize>,
469    tls:     Option<TlsConfig>,
470    limits:  Option<ServerLimitsConfig>,
471}
472
473impl Default for HttpServerConfigBuilder {
474    fn default() -> Self {
475        Self {
476            port:    default_port(),
477            host:    default_host(),
478            workers: None,
479            tls:     None,
480            limits:  None,
481        }
482    }
483}
484
485impl HttpServerConfigBuilder {
486    /// Sets the TCP port to listen on.
487    #[must_use = "builder method returns modified builder"]
488    pub const fn port(mut self, port: u16) -> Self {
489        self.port = port;
490        self
491    }
492
493    /// Sets the network interface to bind.
494    pub fn host(mut self, host: impl Into<String>) -> Self {
495        self.host = host.into();
496        self
497    }
498
499    /// Sets the number of async worker threads.
500    #[must_use = "builder method returns modified builder"]
501    pub const fn workers(mut self, workers: usize) -> Self {
502        self.workers = Some(workers);
503        self
504    }
505
506    /// Sets the TLS configuration.
507    #[must_use = "builder method returns modified builder"]
508    pub fn tls(mut self, tls: TlsConfig) -> Self {
509        self.tls = Some(tls);
510        self
511    }
512
513    /// Sets the per-request and concurrency limits.
514    #[must_use = "builder method returns modified builder"]
515    pub fn limits(mut self, limits: ServerLimitsConfig) -> Self {
516        self.limits = Some(limits);
517        self
518    }
519
520    /// Builds the [`HttpServerConfig`].
521    #[must_use = "building a config that is not used has no effect"]
522    pub fn build(self) -> HttpServerConfig {
523        HttpServerConfig {
524            port:    self.port,
525            host:    self.host,
526            workers: self.workers,
527            tls:     self.tls,
528            limits:  self.limits,
529        }
530    }
531}
532
533impl ServerLimitsConfig {
534    /// Returns a builder for `ServerLimitsConfig`.
535    #[must_use = "builder does nothing until .build() is called"]
536    pub fn builder() -> ServerLimitsConfigBuilder {
537        ServerLimitsConfigBuilder::default()
538    }
539}
540
541/// Builder for [`ServerLimitsConfig`].
542#[derive(Debug)]
543pub struct ServerLimitsConfigBuilder {
544    max_request_size:        String,
545    request_timeout:         String,
546    max_concurrent_requests: usize,
547    max_queue_depth:         usize,
548}
549
550impl Default for ServerLimitsConfigBuilder {
551    fn default() -> Self {
552        Self {
553            max_request_size:        default_max_request_size(),
554            request_timeout:         default_request_timeout(),
555            max_concurrent_requests: default_max_concurrent(),
556            max_queue_depth:         default_max_queue_depth(),
557        }
558    }
559}
560
561impl ServerLimitsConfigBuilder {
562    /// Sets the maximum request body size (e.g. `"10MB"`).
563    pub fn max_request_size(mut self, max_request_size: impl Into<String>) -> Self {
564        self.max_request_size = max_request_size.into();
565        self
566    }
567
568    /// Sets the maximum request processing time (e.g. `"30s"`).
569    pub fn request_timeout(mut self, request_timeout: impl Into<String>) -> Self {
570        self.request_timeout = request_timeout.into();
571        self
572    }
573
574    /// Sets the maximum number of concurrent requests.
575    #[must_use = "builder method returns modified builder"]
576    pub const fn max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
577        self.max_concurrent_requests = max_concurrent_requests;
578        self
579    }
580
581    /// Sets the maximum request queue depth.
582    #[must_use = "builder method returns modified builder"]
583    pub const fn max_queue_depth(mut self, max_queue_depth: usize) -> Self {
584        self.max_queue_depth = max_queue_depth;
585        self
586    }
587
588    /// Builds the [`ServerLimitsConfig`].
589    #[must_use = "building a config that is not used has no effect"]
590    pub fn build(self) -> ServerLimitsConfig {
591        ServerLimitsConfig {
592            max_request_size:        self.max_request_size,
593            request_timeout:         self.request_timeout,
594            max_concurrent_requests: self.max_concurrent_requests,
595            max_queue_depth:         self.max_queue_depth,
596        }
597    }
598}
599
600impl DatabaseConfig {
601    /// Returns a builder for `DatabaseConfig`.
602    #[must_use = "builder does nothing until .build() is called"]
603    pub fn builder() -> DatabaseConfigBuilder {
604        DatabaseConfigBuilder::default()
605    }
606}
607
608/// Builder for [`DatabaseConfig`].
609#[derive(Debug, Default)]
610pub struct DatabaseConfigBuilder {
611    url_env:               Option<String>,
612    pool_size:             u32,
613    pool_timeout:          Option<String>,
614    statement_timeout:     Option<String>,
615    replicas:              Vec<ReplicaConfig>,
616    health_check_interval: Option<String>,
617}
618
619impl DatabaseConfigBuilder {
620    /// Sets the environment variable name that holds the database URL.
621    pub fn url_env(mut self, url_env: impl Into<String>) -> Self {
622        self.url_env = Some(url_env.into());
623        self
624    }
625
626    /// Sets the maximum number of connections in the pool.
627    #[must_use = "builder method returns modified builder"]
628    pub const fn pool_size(mut self, pool_size: u32) -> Self {
629        self.pool_size = pool_size;
630        self
631    }
632
633    /// Sets how long to wait for a connection before returning an error.
634    pub fn pool_timeout(mut self, pool_timeout: impl Into<String>) -> Self {
635        self.pool_timeout = Some(pool_timeout.into());
636        self
637    }
638
639    /// Sets the per-query statement timeout.
640    pub fn statement_timeout(mut self, statement_timeout: impl Into<String>) -> Self {
641        self.statement_timeout = Some(statement_timeout.into());
642        self
643    }
644
645    /// Adds a read replica.
646    #[must_use = "builder method returns modified builder"]
647    pub fn replica(mut self, replica: ReplicaConfig) -> Self {
648        self.replicas.push(replica);
649        self
650    }
651
652    /// Sets the health-check ping interval.
653    pub fn health_check_interval(mut self, health_check_interval: impl Into<String>) -> Self {
654        self.health_check_interval = Some(health_check_interval.into());
655        self
656    }
657
658    /// Builds the [`DatabaseConfig`].
659    ///
660    /// # Errors
661    ///
662    /// Returns an error string if `url_env` was not set.
663    pub fn build(self) -> Result<DatabaseConfig, String> {
664        let url_env =
665            self.url_env.ok_or_else(|| "DatabaseConfig: url_env is required".to_string())?;
666        Ok(DatabaseConfig {
667            url_env,
668            pool_size: if self.pool_size == 0 {
669                default_pool_size()
670            } else {
671                self.pool_size
672            },
673            pool_timeout: self.pool_timeout,
674            statement_timeout: self.statement_timeout,
675            replicas: self.replicas,
676            health_check_interval: self.health_check_interval,
677        })
678    }
679}
680
681impl LifecycleConfig {
682    /// Returns a builder for `LifecycleConfig`.
683    #[must_use = "builder does nothing until .build() is called"]
684    pub fn builder() -> LifecycleConfigBuilder {
685        LifecycleConfigBuilder::default()
686    }
687}
688
689/// Builder for [`LifecycleConfig`].
690#[derive(Debug)]
691pub struct LifecycleConfigBuilder {
692    shutdown_timeout: String,
693    shutdown_delay:   String,
694    health_path:      String,
695    ready_path:       String,
696}
697
698impl Default for LifecycleConfigBuilder {
699    fn default() -> Self {
700        Self {
701            shutdown_timeout: default_shutdown_timeout(),
702            shutdown_delay:   default_shutdown_delay(),
703            health_path:      default_health_path(),
704            ready_path:       default_ready_path(),
705        }
706    }
707}
708
709impl LifecycleConfigBuilder {
710    /// Sets the graceful-shutdown timeout (e.g. `"30s"`).
711    pub fn shutdown_timeout(mut self, shutdown_timeout: impl Into<String>) -> Self {
712        self.shutdown_timeout = shutdown_timeout.into();
713        self
714    }
715
716    /// Sets the pre-shutdown delay for load balancer deregistration (e.g. `"5s"`).
717    pub fn shutdown_delay(mut self, shutdown_delay: impl Into<String>) -> Self {
718        self.shutdown_delay = shutdown_delay.into();
719        self
720    }
721
722    /// Sets the health-check endpoint path.
723    pub fn health_path(mut self, health_path: impl Into<String>) -> Self {
724        self.health_path = health_path.into();
725        self
726    }
727
728    /// Sets the readiness-check endpoint path.
729    pub fn ready_path(mut self, ready_path: impl Into<String>) -> Self {
730        self.ready_path = ready_path.into();
731        self
732    }
733
734    /// Builds the [`LifecycleConfig`].
735    #[must_use = "building a config that is not used has no effect"]
736    pub fn build(self) -> LifecycleConfig {
737        LifecycleConfig {
738            shutdown_timeout: self.shutdown_timeout,
739            shutdown_delay:   self.shutdown_delay,
740            health_path:      self.health_path,
741            ready_path:       self.ready_path,
742        }
743    }
744}