1use std::net::SocketAddr;
14
15use clap::{Args, Parser, builder::BoolishValueParser};
16
17use crate::ServerConfig;
18
19fn parse_bool_env_opt(var: &str) -> Option<bool> {
24 std::env::var(var)
25 .ok()
26 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
27}
28
29#[derive(Parser, Debug, Clone)]
33#[command(name = "fraiseql-server", version, about)]
34pub struct Cli {
35 #[command(flatten)]
37 pub server: ServerArgs,
38
39 #[cfg(feature = "mcp")]
44 #[arg(long, env = "FRAISEQL_MCP_STDIO", hide = true)]
45 pub mcp_stdio: Option<String>,
46}
47
48#[derive(Args, Debug, Clone, Default)]
57pub struct ServerArgs {
58 #[arg(long, env = "FRAISEQL_CONFIG")]
61 pub config: Option<String>,
62
63 #[arg(long, env = "DATABASE_URL")]
65 pub database_url: Option<String>,
66
67 #[arg(long, env = "FRAISEQL_BIND_ADDR")]
69 pub bind_addr: Option<SocketAddr>,
70
71 #[arg(long, env = "FRAISEQL_SCHEMA_PATH")]
73 pub schema_path: Option<String>,
74
75 #[arg(long, env = "FRAISEQL_METRICS_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
78 pub metrics_enabled: Option<bool>,
79
80 #[arg(long, env = "FRAISEQL_METRICS_TOKEN")]
82 pub metrics_token: Option<String>,
83
84 #[arg(long, env = "FRAISEQL_ADMIN_API_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
87 pub admin_api_enabled: Option<bool>,
88
89 #[arg(long, env = "FRAISEQL_ADMIN_TOKEN")]
91 pub admin_token: Option<String>,
92
93 #[arg(long, env = "FRAISEQL_INTROSPECTION_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
96 pub introspection_enabled: Option<bool>,
97
98 #[arg(long, env = "FRAISEQL_INTROSPECTION_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
100 pub introspection_require_auth: Option<bool>,
101
102 #[arg(long, env = "FRAISEQL_METADATA_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
105 pub metadata_require_auth: Option<bool>,
106
107 #[arg(long, env = "FRAISEQL_SCHEMA_EXPORT_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
110 pub schema_export_require_auth: Option<bool>,
111
112 #[arg(long, env = "FRAISEQL_PLAYGROUND_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
115 pub playground_require_auth: Option<bool>,
116
117 #[arg(long, env = "FRAISEQL_SUBSCRIPTION_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
120 pub subscription_require_auth: Option<bool>,
121
122 #[arg(long, env = "FRAISEQL_RATE_LIMITING_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
125 pub rate_limiting_enabled: Option<bool>,
126
127 #[arg(long, env = "FRAISEQL_RATE_LIMIT_RPS_PER_IP")]
129 pub rate_limit_rps_per_ip: Option<u32>,
130
131 #[arg(long, env = "FRAISEQL_RATE_LIMIT_RPS_PER_USER")]
133 pub rate_limit_rps_per_user: Option<u32>,
134
135 #[arg(long, env = "FRAISEQL_RATE_LIMIT_BURST_SIZE")]
137 pub rate_limit_burst_size: Option<u32>,
138
139 #[arg(long, env = "FRAISEQL_LOG_FORMAT")]
143 pub log_format: Option<String>,
144}
145
146impl ServerArgs {
147 #[must_use]
156 pub fn from_env() -> Self {
157 Self {
158 config: std::env::var("FRAISEQL_CONFIG").ok(),
159 database_url: std::env::var("DATABASE_URL").ok(),
160 bind_addr: std::env::var("FRAISEQL_BIND_ADDR")
161 .ok()
162 .and_then(|v| v.parse().ok()),
163 schema_path: std::env::var("FRAISEQL_SCHEMA_PATH").ok(),
164 metrics_enabled: parse_bool_env_opt("FRAISEQL_METRICS_ENABLED"),
165 metrics_token: std::env::var("FRAISEQL_METRICS_TOKEN").ok(),
166 admin_api_enabled: parse_bool_env_opt("FRAISEQL_ADMIN_API_ENABLED"),
167 admin_token: std::env::var("FRAISEQL_ADMIN_TOKEN").ok(),
168 introspection_enabled: parse_bool_env_opt("FRAISEQL_INTROSPECTION_ENABLED"),
169 introspection_require_auth: parse_bool_env_opt("FRAISEQL_INTROSPECTION_REQUIRE_AUTH"),
170 metadata_require_auth: parse_bool_env_opt("FRAISEQL_METADATA_REQUIRE_AUTH"),
171 schema_export_require_auth: parse_bool_env_opt("FRAISEQL_SCHEMA_EXPORT_REQUIRE_AUTH"),
172 playground_require_auth: parse_bool_env_opt("FRAISEQL_PLAYGROUND_REQUIRE_AUTH"),
173 subscription_require_auth: parse_bool_env_opt("FRAISEQL_SUBSCRIPTION_REQUIRE_AUTH"),
174 rate_limiting_enabled: parse_bool_env_opt("FRAISEQL_RATE_LIMITING_ENABLED"),
175 rate_limit_rps_per_ip: std::env::var("FRAISEQL_RATE_LIMIT_RPS_PER_IP")
176 .ok()
177 .and_then(|v| v.parse().ok()),
178 rate_limit_rps_per_user: std::env::var("FRAISEQL_RATE_LIMIT_RPS_PER_USER")
179 .ok()
180 .and_then(|v| v.parse().ok()),
181 rate_limit_burst_size: std::env::var("FRAISEQL_RATE_LIMIT_BURST_SIZE")
182 .ok()
183 .and_then(|v| v.parse().ok()),
184 log_format: std::env::var("FRAISEQL_LOG_FORMAT").ok(),
185 }
186 }
187
188 pub fn apply_to_config(&self, config: &mut ServerConfig) {
194 if let Some(ref db_url) = self.database_url {
196 config.database_url.clone_from(db_url);
197 }
198 if let Some(addr) = self.bind_addr {
199 config.bind_addr = addr;
200 }
201 if let Some(ref path) = self.schema_path {
202 config.schema_path = path.into();
203 }
204
205 if let Some(enabled) = self.metrics_enabled {
207 config.metrics_enabled = enabled;
208 }
209 if self.metrics_token.is_some() {
210 config.metrics_token.clone_from(&self.metrics_token);
211 }
212
213 if let Some(enabled) = self.admin_api_enabled {
215 config.admin_api_enabled = enabled;
216 }
217 if self.admin_token.is_some() {
218 config.admin_token.clone_from(&self.admin_token);
219 }
220
221 if let Some(enabled) = self.introspection_enabled {
223 config.introspection_enabled = enabled;
224 }
225 if let Some(require_auth) = self.introspection_require_auth {
226 config.introspection_require_auth = require_auth;
227 }
228 if let Some(require_auth) = self.metadata_require_auth {
229 config.metadata_require_auth = Some(require_auth);
230 }
231 if let Some(require_auth) = self.schema_export_require_auth {
232 config.schema_export_require_auth = Some(require_auth);
233 }
234 if let Some(require_auth) = self.playground_require_auth {
235 config.playground_require_auth = Some(require_auth);
236 }
237 if let Some(require_auth) = self.subscription_require_auth {
238 config.subscription_require_auth = Some(require_auth);
239 }
240
241 self.apply_rate_limit_overrides(config);
243 }
244
245 fn apply_rate_limit_overrides(&self, config: &mut ServerConfig) {
247 if self.rate_limiting_enabled.is_none()
248 && self.rate_limit_rps_per_ip.is_none()
249 && self.rate_limit_rps_per_user.is_none()
250 && self.rate_limit_burst_size.is_none()
251 {
252 return;
253 }
254
255 let mut rate_config = config.rate_limiting.take().unwrap_or_default();
256
257 if let Some(enabled) = self.rate_limiting_enabled {
258 rate_config.enabled = enabled;
259 }
260 if let Some(v) = self.rate_limit_rps_per_ip {
261 rate_config.rps_per_ip = v;
262 }
263 if let Some(v) = self.rate_limit_rps_per_user {
264 rate_config.rps_per_user = v;
265 }
266 if let Some(v) = self.rate_limit_burst_size {
267 rate_config.burst_size = v;
268 }
269
270 config.rate_limiting = Some(rate_config);
271 }
272
273 #[must_use]
275 pub fn is_json_log_format(&self) -> bool {
276 self.log_format.as_deref().is_some_and(|v| v.eq_ignore_ascii_case("json"))
277 }
278}