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_VALIDATE_SQL_SOURCES", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
79 pub validate_sql_sources: Option<bool>,
80
81 #[arg(long, env = "FRAISEQL_METRICS_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
84 pub metrics_enabled: Option<bool>,
85
86 #[arg(long, env = "FRAISEQL_METRICS_TOKEN")]
88 pub metrics_token: Option<String>,
89
90 #[arg(long, env = "FRAISEQL_ADMIN_API_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
93 pub admin_api_enabled: Option<bool>,
94
95 #[arg(long, env = "FRAISEQL_ADMIN_TOKEN")]
97 pub admin_token: Option<String>,
98
99 #[arg(long, env = "FRAISEQL_INTROSPECTION_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
102 pub introspection_enabled: Option<bool>,
103
104 #[arg(long, env = "FRAISEQL_INTROSPECTION_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
106 pub introspection_require_auth: Option<bool>,
107
108 #[arg(long, env = "FRAISEQL_METADATA_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
111 pub metadata_require_auth: Option<bool>,
112
113 #[arg(long, env = "FRAISEQL_SCHEMA_EXPORT_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
116 pub schema_export_require_auth: Option<bool>,
117
118 #[arg(long, env = "FRAISEQL_PLAYGROUND_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
121 pub playground_require_auth: Option<bool>,
122
123 #[arg(long, env = "FRAISEQL_SUBSCRIPTION_REQUIRE_AUTH", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
126 pub subscription_require_auth: Option<bool>,
127
128 #[arg(long, env = "FRAISEQL_RATE_LIMITING_ENABLED", value_parser = BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
131 pub rate_limiting_enabled: Option<bool>,
132
133 #[arg(long, env = "FRAISEQL_RATE_LIMIT_RPS_PER_IP")]
135 pub rate_limit_rps_per_ip: Option<u32>,
136
137 #[arg(long, env = "FRAISEQL_RATE_LIMIT_RPS_PER_USER")]
139 pub rate_limit_rps_per_user: Option<u32>,
140
141 #[arg(long, env = "FRAISEQL_RATE_LIMIT_BURST_SIZE")]
143 pub rate_limit_burst_size: Option<u32>,
144
145 #[arg(long, env = "FRAISEQL_LOG_FORMAT")]
149 pub log_format: Option<String>,
150}
151
152impl ServerArgs {
153 #[must_use]
162 pub fn from_env() -> Self {
163 Self {
164 config: std::env::var("FRAISEQL_CONFIG").ok(),
165 database_url: std::env::var("DATABASE_URL").ok(),
166 bind_addr: std::env::var("FRAISEQL_BIND_ADDR")
167 .ok()
168 .and_then(|v| v.parse().ok()),
169 schema_path: std::env::var("FRAISEQL_SCHEMA_PATH").ok(),
170 validate_sql_sources: parse_bool_env_opt("FRAISEQL_VALIDATE_SQL_SOURCES"),
171 metrics_enabled: parse_bool_env_opt("FRAISEQL_METRICS_ENABLED"),
172 metrics_token: std::env::var("FRAISEQL_METRICS_TOKEN").ok(),
173 admin_api_enabled: parse_bool_env_opt("FRAISEQL_ADMIN_API_ENABLED"),
174 admin_token: std::env::var("FRAISEQL_ADMIN_TOKEN").ok(),
175 introspection_enabled: parse_bool_env_opt("FRAISEQL_INTROSPECTION_ENABLED"),
176 introspection_require_auth: parse_bool_env_opt("FRAISEQL_INTROSPECTION_REQUIRE_AUTH"),
177 metadata_require_auth: parse_bool_env_opt("FRAISEQL_METADATA_REQUIRE_AUTH"),
178 schema_export_require_auth: parse_bool_env_opt("FRAISEQL_SCHEMA_EXPORT_REQUIRE_AUTH"),
179 playground_require_auth: parse_bool_env_opt("FRAISEQL_PLAYGROUND_REQUIRE_AUTH"),
180 subscription_require_auth: parse_bool_env_opt("FRAISEQL_SUBSCRIPTION_REQUIRE_AUTH"),
181 rate_limiting_enabled: parse_bool_env_opt("FRAISEQL_RATE_LIMITING_ENABLED"),
182 rate_limit_rps_per_ip: std::env::var("FRAISEQL_RATE_LIMIT_RPS_PER_IP")
183 .ok()
184 .and_then(|v| v.parse().ok()),
185 rate_limit_rps_per_user: std::env::var("FRAISEQL_RATE_LIMIT_RPS_PER_USER")
186 .ok()
187 .and_then(|v| v.parse().ok()),
188 rate_limit_burst_size: std::env::var("FRAISEQL_RATE_LIMIT_BURST_SIZE")
189 .ok()
190 .and_then(|v| v.parse().ok()),
191 log_format: std::env::var("FRAISEQL_LOG_FORMAT").ok(),
192 }
193 }
194
195 pub fn apply_to_config(&self, config: &mut ServerConfig) {
201 if let Some(ref db_url) = self.database_url {
203 config.database_url.clone_from(db_url);
204 }
205 if let Some(addr) = self.bind_addr {
206 config.bind_addr = addr;
207 }
208 if let Some(ref path) = self.schema_path {
209 config.schema_path = path.into();
210 }
211 if let Some(enabled) = self.validate_sql_sources {
214 config.validate_sql_sources = enabled;
215 }
216
217 if let Some(enabled) = self.metrics_enabled {
219 config.metrics_enabled = enabled;
220 }
221 if self.metrics_token.is_some() {
222 config.metrics_token.clone_from(&self.metrics_token);
223 }
224
225 if let Some(enabled) = self.admin_api_enabled {
227 config.admin_api_enabled = enabled;
228 }
229 if self.admin_token.is_some() {
230 config.admin_token.clone_from(&self.admin_token);
231 }
232
233 if let Some(enabled) = self.introspection_enabled {
235 config.introspection_enabled = enabled;
236 }
237 if let Some(require_auth) = self.introspection_require_auth {
238 config.introspection_require_auth = require_auth;
239 }
240 if let Some(require_auth) = self.metadata_require_auth {
241 config.metadata_require_auth = Some(require_auth);
242 }
243 if let Some(require_auth) = self.schema_export_require_auth {
244 config.schema_export_require_auth = Some(require_auth);
245 }
246 if let Some(require_auth) = self.playground_require_auth {
247 config.playground_require_auth = Some(require_auth);
248 }
249 if let Some(require_auth) = self.subscription_require_auth {
250 config.subscription_require_auth = Some(require_auth);
251 }
252
253 self.apply_rate_limit_overrides(config);
255 }
256
257 fn apply_rate_limit_overrides(&self, config: &mut ServerConfig) {
259 if self.rate_limiting_enabled.is_none()
260 && self.rate_limit_rps_per_ip.is_none()
261 && self.rate_limit_rps_per_user.is_none()
262 && self.rate_limit_burst_size.is_none()
263 {
264 return;
265 }
266
267 let mut rate_config = config.rate_limiting.take().unwrap_or_default();
268
269 if let Some(enabled) = self.rate_limiting_enabled {
270 rate_config.enabled = enabled;
271 }
272 if let Some(v) = self.rate_limit_rps_per_ip {
273 rate_config.rps_per_ip = v;
274 }
275 if let Some(v) = self.rate_limit_rps_per_user {
276 rate_config.rps_per_user = v;
277 }
278 if let Some(v) = self.rate_limit_burst_size {
279 rate_config.burst_size = v;
280 }
281
282 config.rate_limiting = Some(rate_config);
283 }
284
285 #[must_use]
287 pub fn is_json_log_format(&self) -> bool {
288 self.log_format.as_deref().is_some_and(|v| v.eq_ignore_ascii_case("json"))
289 }
290}