Skip to main content

apimock_server/
control.rs

1//! Stage-1 control/introspection API for the server.
2//!
3//! # Scope (5.0.0)
4//!
5//! Per the brief (§4.3, §5.3, §7), the server crate exposes:
6//!
7//! - A minimal handle the embedder can hold.
8//! - A "state" enum the embedder can poll.
9//! - A reload-hint type.
10//!
11//! Critically, **the server does not restart itself**. When a config
12//! edit would need the listener rebuilt, the server-side code only
13//! emits a hint; an external control layer (GUI / supervisor) decides
14//! whether to restart. That's the brief's §7 rule and the shape here
15//! reflects it.
16//!
17//! Implementation of actual shutdown / reload wiring is stage-2 work.
18//! In 5.0.0 these types exist with placeholder methods so downstream
19//! code can start coding against them.
20
21use serde::Serialize;
22
23/// Handle an embedder holds to interact with a running server.
24///
25/// # Why it doesn't expose a `.restart()`
26///
27/// The brief is specific: "restart は server crate の内部責務にしない"
28/// (restart is not a server-crate responsibility). A `ServerHandle`
29/// carries read-only introspection and a shutdown signal — nothing
30/// more. If a change requires the listener to rebind a new port, the
31/// embedder tears the server down and constructs a fresh one.
32#[derive(Clone, Debug)]
33#[non_exhaustive]
34pub struct ServerHandle {
35    /// Address the HTTP listener is bound to, if any.
36    pub http_addr: Option<std::net::SocketAddr>,
37    /// Address the HTTPS listener is bound to, if any.
38    pub https_addr: Option<std::net::SocketAddr>,
39    /// Hot-reload handle for TLS certificates (RFC 020).
40    ///
41    /// `Some` when an HTTPS listener is active with a reloadable cert.
42    /// `None` for HTTP-only servers.
43    pub cert_reloader: Option<std::sync::Arc<crate::tls::ReloadableCertResolver>>,
44}
45
46impl ServerHandle {
47    /// Reload the TLS certificate and private key from the given PEM files.
48    ///
49    /// Returns `Ok(())` if the swap succeeded; the new cert is used for all
50    /// TLS handshakes started after this call. On error, the old cert remains
51    /// active.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error string if:
56    /// - No HTTPS listener is active (`cert_reloader` is `None`).
57    /// - The new cert or key fails to parse.
58    pub fn reload_tls_certs(&self, cert_path: &str, key_path: &str) -> Result<(), String> {
59        match &self.cert_reloader {
60            Some(reloader) => reloader
61                .reload_from_paths(cert_path, key_path)
62                .map_err(|e| e.to_string()),
63            None => Err("no HTTPS listener active; cert reload not available".to_owned()),
64        }
65    }
66}
67
68/// Small control surface for the embedder.
69///
70/// 5.0.0 ships this as a placeholder; stage-2 adds the actual
71/// shutdown-signal channel + reload trigger implementation.
72#[derive(Clone, Debug, Default)]
73#[non_exhaustive]
74pub struct ServerControl {}
75
76impl ServerControl {
77    pub fn new() -> Self {
78        Self {}
79    }
80}
81
82/// What the server is doing right now.
83#[derive(Clone, Copy, Debug, Serialize)]
84#[non_exhaustive]
85pub enum ServerState {
86    /// The listener is being brought up.
87    Starting,
88    /// Requests are being served normally.
89    Running,
90    /// Shutdown has been requested; drains are in flight.
91    ShuttingDown,
92    /// The listener is no longer accepting connections.
93    Stopped,
94}
95
96/// How much of the server needs to restart after a config change.
97///
98/// # Why this lives here alongside `apimock_config::ReloadHint`
99///
100/// The config crate carries the same concept as a `view::ReloadHint`
101/// struct because it's what an `ApplyResult` / `SaveResult` carries;
102/// GUIs consume it from the config layer without pulling server.
103/// The server-side mirror is an enum for more ergonomic pattern
104/// matching on the server's own code paths. The `From` impls below
105/// bridge the two.
106#[derive(Clone, Copy, Debug, Serialize)]
107#[non_exhaustive]
108pub enum ReloadHint {
109    /// No reload required.
110    None,
111    /// Rule sets / middlewares need to reload.
112    Reload,
113    /// Listener configuration changed; need a full restart.
114    Restart,
115}
116
117impl From<apimock_config::ReloadHint> for ReloadHint {
118    fn from(value: apimock_config::ReloadHint) -> Self {
119        // `Restart` implies `Reload`, so restart wins if both flags are set.
120        if value.requires_restart {
121            ReloadHint::Restart
122        } else if value.requires_reload {
123            ReloadHint::Reload
124        } else {
125            ReloadHint::None
126        }
127    }
128}
129
130impl From<ReloadHint> for apimock_config::ReloadHint {
131    fn from(value: ReloadHint) -> Self {
132        match value {
133            ReloadHint::None => apimock_config::ReloadHint::none(),
134            ReloadHint::Reload => apimock_config::ReloadHint::reload(),
135            ReloadHint::Restart => apimock_config::ReloadHint::restart(),
136        }
137    }
138}