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