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 or local development).
410    #[serde(default)]
411    pub endpoint:         Option<String>,
412    /// GCP project ID (used by the `"gcs"` backend).
413    #[serde(default)]
414    pub project_id:       Option<String>,
415    /// Azure storage account name (used by the `"azure"` backend).
416    #[serde(default)]
417    pub account_name:     Option<String>,
418    /// Maximum upload size in bytes for this storage backend.
419    ///
420    /// Defaults to `104_857_600` (100 `MiB`). Uploads exceeding this size are
421    /// rejected with HTTP 413 before touching the backend.
422    #[serde(default = "default_max_upload_bytes")]
423    pub max_upload_bytes: usize,
424}
425
426const fn default_max_upload_bytes() -> usize {
427    100 * 1024 * 1024 // 100 MiB
428}
429
430/// Reserved: placeholder for future full-text search indexing configuration.
431#[derive(Debug, Clone, Deserialize)]
432pub struct SearchConfig {}
433
434/// Reserved: placeholder for future advanced query-result caching configuration.
435#[derive(Debug, Clone, Deserialize)]
436pub struct CacheConfig {}
437
438/// Reserved: placeholder for future background job-queue configuration.
439#[derive(Debug, Clone, Deserialize)]
440pub struct QueueConfig {}
441
442/// Reserved: placeholder for future real-time subscription update configuration.
443#[derive(Debug, Clone, Deserialize)]
444pub struct RealtimeConfig {}
445
446/// Reserved: placeholder for future custom HTTP endpoint configuration.
447#[derive(Debug, Clone, Deserialize)]
448pub struct CustomEndpointsConfig {}
449
450// ---------------------------------------------------------------------------
451// Builders
452// ---------------------------------------------------------------------------
453
454impl HttpServerConfig {
455    /// Returns a builder for `HttpServerConfig`.
456    #[must_use = "builder does nothing until .build() is called"]
457    pub fn builder() -> HttpServerConfigBuilder {
458        HttpServerConfigBuilder::default()
459    }
460}
461
462/// Builder for [`HttpServerConfig`].
463#[derive(Debug)]
464pub struct HttpServerConfigBuilder {
465    port:    u16,
466    host:    String,
467    workers: Option<usize>,
468    tls:     Option<TlsConfig>,
469    limits:  Option<ServerLimitsConfig>,
470}
471
472impl Default for HttpServerConfigBuilder {
473    fn default() -> Self {
474        Self {
475            port:    default_port(),
476            host:    default_host(),
477            workers: None,
478            tls:     None,
479            limits:  None,
480        }
481    }
482}
483
484impl HttpServerConfigBuilder {
485    /// Sets the TCP port to listen on.
486    #[must_use = "builder method returns modified builder"]
487    pub const fn port(mut self, port: u16) -> Self {
488        self.port = port;
489        self
490    }
491
492    /// Sets the network interface to bind.
493    pub fn host(mut self, host: impl Into<String>) -> Self {
494        self.host = host.into();
495        self
496    }
497
498    /// Sets the number of async worker threads.
499    #[must_use = "builder method returns modified builder"]
500    pub const fn workers(mut self, workers: usize) -> Self {
501        self.workers = Some(workers);
502        self
503    }
504
505    /// Sets the TLS configuration.
506    #[must_use = "builder method returns modified builder"]
507    pub fn tls(mut self, tls: TlsConfig) -> Self {
508        self.tls = Some(tls);
509        self
510    }
511
512    /// Sets the per-request and concurrency limits.
513    #[must_use = "builder method returns modified builder"]
514    pub fn limits(mut self, limits: ServerLimitsConfig) -> Self {
515        self.limits = Some(limits);
516        self
517    }
518
519    /// Builds the [`HttpServerConfig`].
520    #[must_use = "building a config that is not used has no effect"]
521    pub fn build(self) -> HttpServerConfig {
522        HttpServerConfig {
523            port:    self.port,
524            host:    self.host,
525            workers: self.workers,
526            tls:     self.tls,
527            limits:  self.limits,
528        }
529    }
530}
531
532impl ServerLimitsConfig {
533    /// Returns a builder for `ServerLimitsConfig`.
534    #[must_use = "builder does nothing until .build() is called"]
535    pub fn builder() -> ServerLimitsConfigBuilder {
536        ServerLimitsConfigBuilder::default()
537    }
538}
539
540/// Builder for [`ServerLimitsConfig`].
541#[derive(Debug)]
542pub struct ServerLimitsConfigBuilder {
543    max_request_size:        String,
544    request_timeout:         String,
545    max_concurrent_requests: usize,
546    max_queue_depth:         usize,
547}
548
549impl Default for ServerLimitsConfigBuilder {
550    fn default() -> Self {
551        Self {
552            max_request_size:        default_max_request_size(),
553            request_timeout:         default_request_timeout(),
554            max_concurrent_requests: default_max_concurrent(),
555            max_queue_depth:         default_max_queue_depth(),
556        }
557    }
558}
559
560impl ServerLimitsConfigBuilder {
561    /// Sets the maximum request body size (e.g. `"10MB"`).
562    pub fn max_request_size(mut self, max_request_size: impl Into<String>) -> Self {
563        self.max_request_size = max_request_size.into();
564        self
565    }
566
567    /// Sets the maximum request processing time (e.g. `"30s"`).
568    pub fn request_timeout(mut self, request_timeout: impl Into<String>) -> Self {
569        self.request_timeout = request_timeout.into();
570        self
571    }
572
573    /// Sets the maximum number of concurrent requests.
574    #[must_use = "builder method returns modified builder"]
575    pub const fn max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
576        self.max_concurrent_requests = max_concurrent_requests;
577        self
578    }
579
580    /// Sets the maximum request queue depth.
581    #[must_use = "builder method returns modified builder"]
582    pub const fn max_queue_depth(mut self, max_queue_depth: usize) -> Self {
583        self.max_queue_depth = max_queue_depth;
584        self
585    }
586
587    /// Builds the [`ServerLimitsConfig`].
588    #[must_use = "building a config that is not used has no effect"]
589    pub fn build(self) -> ServerLimitsConfig {
590        ServerLimitsConfig {
591            max_request_size:        self.max_request_size,
592            request_timeout:         self.request_timeout,
593            max_concurrent_requests: self.max_concurrent_requests,
594            max_queue_depth:         self.max_queue_depth,
595        }
596    }
597}
598
599impl DatabaseConfig {
600    /// Returns a builder for `DatabaseConfig`.
601    #[must_use = "builder does nothing until .build() is called"]
602    pub fn builder() -> DatabaseConfigBuilder {
603        DatabaseConfigBuilder::default()
604    }
605}
606
607/// Builder for [`DatabaseConfig`].
608#[derive(Debug, Default)]
609pub struct DatabaseConfigBuilder {
610    url_env:               Option<String>,
611    pool_size:             u32,
612    pool_timeout:          Option<String>,
613    statement_timeout:     Option<String>,
614    replicas:              Vec<ReplicaConfig>,
615    health_check_interval: Option<String>,
616}
617
618impl DatabaseConfigBuilder {
619    /// Sets the environment variable name that holds the database URL.
620    pub fn url_env(mut self, url_env: impl Into<String>) -> Self {
621        self.url_env = Some(url_env.into());
622        self
623    }
624
625    /// Sets the maximum number of connections in the pool.
626    #[must_use = "builder method returns modified builder"]
627    pub const fn pool_size(mut self, pool_size: u32) -> Self {
628        self.pool_size = pool_size;
629        self
630    }
631
632    /// Sets how long to wait for a connection before returning an error.
633    pub fn pool_timeout(mut self, pool_timeout: impl Into<String>) -> Self {
634        self.pool_timeout = Some(pool_timeout.into());
635        self
636    }
637
638    /// Sets the per-query statement timeout.
639    pub fn statement_timeout(mut self, statement_timeout: impl Into<String>) -> Self {
640        self.statement_timeout = Some(statement_timeout.into());
641        self
642    }
643
644    /// Adds a read replica.
645    #[must_use = "builder method returns modified builder"]
646    pub fn replica(mut self, replica: ReplicaConfig) -> Self {
647        self.replicas.push(replica);
648        self
649    }
650
651    /// Sets the health-check ping interval.
652    pub fn health_check_interval(mut self, health_check_interval: impl Into<String>) -> Self {
653        self.health_check_interval = Some(health_check_interval.into());
654        self
655    }
656
657    /// Builds the [`DatabaseConfig`].
658    ///
659    /// # Errors
660    ///
661    /// Returns an error string if `url_env` was not set.
662    pub fn build(self) -> Result<DatabaseConfig, String> {
663        let url_env =
664            self.url_env.ok_or_else(|| "DatabaseConfig: url_env is required".to_string())?;
665        Ok(DatabaseConfig {
666            url_env,
667            pool_size: if self.pool_size == 0 {
668                default_pool_size()
669            } else {
670                self.pool_size
671            },
672            pool_timeout: self.pool_timeout,
673            statement_timeout: self.statement_timeout,
674            replicas: self.replicas,
675            health_check_interval: self.health_check_interval,
676        })
677    }
678}
679
680impl LifecycleConfig {
681    /// Returns a builder for `LifecycleConfig`.
682    #[must_use = "builder does nothing until .build() is called"]
683    pub fn builder() -> LifecycleConfigBuilder {
684        LifecycleConfigBuilder::default()
685    }
686}
687
688/// Builder for [`LifecycleConfig`].
689#[derive(Debug)]
690pub struct LifecycleConfigBuilder {
691    shutdown_timeout: String,
692    shutdown_delay:   String,
693    health_path:      String,
694    ready_path:       String,
695}
696
697impl Default for LifecycleConfigBuilder {
698    fn default() -> Self {
699        Self {
700            shutdown_timeout: default_shutdown_timeout(),
701            shutdown_delay:   default_shutdown_delay(),
702            health_path:      default_health_path(),
703            ready_path:       default_ready_path(),
704        }
705    }
706}
707
708impl LifecycleConfigBuilder {
709    /// Sets the graceful-shutdown timeout (e.g. `"30s"`).
710    pub fn shutdown_timeout(mut self, shutdown_timeout: impl Into<String>) -> Self {
711        self.shutdown_timeout = shutdown_timeout.into();
712        self
713    }
714
715    /// Sets the pre-shutdown delay for load balancer deregistration (e.g. `"5s"`).
716    pub fn shutdown_delay(mut self, shutdown_delay: impl Into<String>) -> Self {
717        self.shutdown_delay = shutdown_delay.into();
718        self
719    }
720
721    /// Sets the health-check endpoint path.
722    pub fn health_path(mut self, health_path: impl Into<String>) -> Self {
723        self.health_path = health_path.into();
724        self
725    }
726
727    /// Sets the readiness-check endpoint path.
728    pub fn ready_path(mut self, ready_path: impl Into<String>) -> Self {
729        self.ready_path = ready_path.into();
730        self
731    }
732
733    /// Builds the [`LifecycleConfig`].
734    #[must_use = "building a config that is not used has no effect"]
735    pub fn build(self) -> LifecycleConfig {
736        LifecycleConfig {
737            shutdown_timeout: self.shutdown_timeout,
738            shutdown_delay:   self.shutdown_delay,
739            health_path:      self.health_path,
740            ready_path:       self.ready_path,
741        }
742    }
743}