1use crate::{GeneratorError, generator::GeneratorConfig};
141use serde::{Deserialize, Serialize};
142use std::collections::BTreeMap;
143use std::path::{Path, PathBuf};
144use validator::Validate;
145
146#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
148pub struct ConfigFile {
149 #[validate(nested)]
150 pub generator: GeneratorSection,
151 #[validate(nested)]
152 pub features: FeaturesSection,
153 #[serde(default)]
154 #[validate(nested)]
155 pub http_client: Option<HttpClientSection>,
156 #[serde(default)]
157 #[validate(nested)]
158 pub streaming: Option<StreamingSection>,
159 #[serde(default)]
162 #[validate(nested)]
163 pub server: Option<ServerSection>,
164 #[serde(default)]
165 pub nullable_overrides: BTreeMap<String, bool>,
166 #[serde(default)]
171 pub extensible_enums: BTreeMap<String, bool>,
172 #[serde(default)]
173 pub type_mappings: BTreeMap<String, String>,
174 #[serde(default)]
177 pub types: crate::type_mapping::TypeMappingConfig,
178}
179
180#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
181pub struct GeneratorSection {
182 #[validate(custom(function = "validate_spec_path_exists"))]
183 pub spec_path: PathBuf,
184 pub output_dir: PathBuf,
185 #[validate(length(min = 1, message = "module_name cannot be empty"))]
192 pub module_name: String,
193 #[serde(default)]
196 pub schema_extensions: Vec<PathBuf>,
197}
198
199#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
200pub struct FeaturesSection {
201 #[serde(default)]
202 pub enable_sse_client: bool,
203 #[serde(default)]
204 pub enable_async_client: bool,
205 #[serde(default)]
206 pub enable_specta: bool,
207 #[serde(default)]
209 pub enable_registry: bool,
210 #[serde(default)]
212 pub registry_only: bool,
213}
214
215#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
223pub struct ServerSection {
224 #[validate(custom(function = "validate_server_framework"))]
226 pub framework: String,
227 #[serde(default)]
230 pub operations: Vec<String>,
231 #[serde(default)]
238 pub prune_models: bool,
239}
240
241impl ServerSection {
242 pub fn parsed_selectors(
245 &self,
246 ) -> Result<Vec<crate::server::Selector>, crate::server::SelectorParseError> {
247 self.operations
248 .iter()
249 .map(|s| crate::server::Selector::parse(s))
250 .collect()
251 }
252}
253
254#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
255pub struct HttpClientSection {
256 #[validate(url(message = "base_url must be a valid URL"))]
257 pub base_url: Option<String>,
258 #[validate(custom(function = "validate_timeout_seconds"))]
259 pub timeout_seconds: Option<u64>,
260 #[validate(nested)]
261 pub auth: Option<AuthConfigSection>,
262 #[serde(default)]
263 #[validate(nested)]
264 pub headers: Vec<HeaderEntry>,
265 #[validate(nested)]
266 pub retry: Option<RetryConfigSection>,
267 #[validate(nested)]
268 pub tracing: Option<TracingConfigSection>,
269}
270
271#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
272pub struct TracingConfigSection {
273 #[serde(default = "default_tracing_enabled")]
274 pub enabled: bool,
275}
276
277fn default_tracing_enabled() -> bool {
278 true
279}
280
281#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
282pub struct RetryConfigSection {
283 #[serde(default = "default_max_retries")]
284 #[validate(custom(function = "validate_max_retries"))]
285 pub max_retries: u32,
286 #[serde(default = "default_initial_delay_ms")]
287 #[validate(custom(function = "validate_initial_delay_ms"))]
288 pub initial_delay_ms: u64,
289 #[serde(default = "default_max_delay_ms")]
290 #[validate(custom(function = "validate_max_delay_ms"))]
291 pub max_delay_ms: u64,
292}
293
294fn default_max_retries() -> u32 {
295 3
296}
297fn default_initial_delay_ms() -> u64 {
298 500
299}
300fn default_max_delay_ms() -> u64 {
301 16000
302}
303
304#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
305pub struct AuthConfigSection {
306 #[serde(rename = "type")]
307 #[validate(custom(function = "validate_auth_type"))]
308 pub auth_type: String,
309 #[validate(length(min = 1, message = "header_name cannot be empty"))]
310 pub header_name: String,
311}
312
313#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
314pub struct HeaderEntry {
315 #[validate(length(min = 1, message = "header name cannot be empty"))]
316 pub name: String,
317 pub value: String,
318}
319
320#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
321pub struct StreamingSection {
322 #[validate(nested)]
323 pub endpoints: Vec<StreamingEndpointSection>,
324}
325
326#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
327pub struct StreamingEndpointSection {
328 #[validate(length(min = 1))]
329 pub operation_id: String,
330 #[validate(length(min = 1))]
331 pub path: String,
332 #[serde(default)]
334 pub http_method: Option<String>,
335 #[serde(default)]
337 pub stream_parameter: String,
338 #[serde(default)]
340 pub query_parameters: Vec<QueryParameterSection>,
341 #[validate(length(min = 1))]
342 pub event_union_type: String,
343 pub content_type: Option<String>,
344 #[validate(nested)]
345 pub event_flow: Option<EventFlowSection>,
346}
347
348#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
349pub struct QueryParameterSection {
350 #[validate(length(min = 1))]
351 pub name: String,
352 #[serde(default)]
353 pub required: bool,
354}
355
356#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
357pub struct EventFlowSection {
358 #[serde(rename = "type")]
359 #[validate(custom(function = "validate_event_flow_type"))]
360 pub flow_type: String,
361 pub start_events: Option<Vec<String>>,
362 pub delta_events: Option<Vec<String>>,
363 pub stop_events: Option<Vec<String>>,
364}
365
366fn validate_spec_path_exists(path: &Path) -> Result<(), validator::ValidationError> {
368 if !path.exists() {
369 let mut error = validator::ValidationError::new("file_not_found");
370 error.message = Some(
371 format!(
372 "OpenAPI spec file not found: {}. Ensure spec_path points to a valid OpenAPI JSON or YAML file.",
373 path.display()
374 )
375 .into(),
376 );
377 return Err(error);
378 }
379 Ok(())
380}
381
382fn validate_server_framework(framework: &str) -> Result<(), validator::ValidationError> {
383 if framework == "axum" {
384 Ok(())
385 } else {
386 let mut err = validator::ValidationError::new("invalid_framework");
387 err.message = Some(
388 format!("framework must be \"axum\" (got \"{framework}\"); other frameworks are not supported yet").into(),
389 );
390 Err(err)
391 }
392}
393
394fn validate_auth_type(auth_type: &str) -> Result<(), validator::ValidationError> {
395 match auth_type {
396 "Bearer" | "ApiKey" | "Custom" => Ok(()),
397 _ => {
398 let mut error = validator::ValidationError::new("invalid_auth_type");
399 error.message = Some(
400 format!(
401 "Invalid auth type '{}'. Must be one of: Bearer, ApiKey, Custom",
402 auth_type
403 )
404 .into(),
405 );
406 Err(error)
407 }
408 }
409}
410
411fn validate_event_flow_type(flow_type: &str) -> Result<(), validator::ValidationError> {
412 match flow_type {
413 "StartDeltaStop" | "Continuous" => Ok(()),
414 _ => {
415 let mut error = validator::ValidationError::new("invalid_event_flow_type");
416 error.message = Some(
417 format!(
418 "Invalid event flow type '{}'. Must be one of: StartDeltaStop, Continuous",
419 flow_type
420 )
421 .into(),
422 );
423 Err(error)
424 }
425 }
426}
427
428fn validate_timeout_seconds(timeout: u64) -> Result<(), validator::ValidationError> {
429 if !(1..=3600).contains(&timeout) {
430 let mut error = validator::ValidationError::new("out_of_range");
431 error.message = Some("timeout_seconds must be between 1 and 3600".into());
432 return Err(error);
433 }
434 Ok(())
435}
436
437fn validate_max_retries(retries: u32) -> Result<(), validator::ValidationError> {
438 if retries > 10 {
439 let mut error = validator::ValidationError::new("out_of_range");
440 error.message = Some("max_retries must be between 0 and 10".into());
441 return Err(error);
442 }
443 Ok(())
444}
445
446fn validate_initial_delay_ms(delay: u64) -> Result<(), validator::ValidationError> {
447 if !(100..=10000).contains(&delay) {
448 let mut error = validator::ValidationError::new("out_of_range");
449 error.message = Some("initial_delay_ms must be between 100 and 10000".into());
450 return Err(error);
451 }
452 Ok(())
453}
454
455fn validate_max_delay_ms(delay: u64) -> Result<(), validator::ValidationError> {
456 if !(1000..=300000).contains(&delay) {
457 let mut error = validator::ValidationError::new("out_of_range");
458 error.message = Some("max_delay_ms must be between 1000 and 300000".into());
459 return Err(error);
460 }
461 Ok(())
462}
463
464impl ConfigFile {
465 pub fn load(path: &Path) -> Result<Self, GeneratorError> {
467 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
468 message: format!("Failed to read config file '{}': {}", path.display(), e),
469 })?;
470
471 let config: ConfigFile =
472 toml::from_str(&content).map_err(|e| GeneratorError::FileError {
473 message: format!(
474 "Failed to parse TOML config: {}\n\nExample config:\n{}",
475 e, EXAMPLE_CONFIG
476 ),
477 })?;
478
479 config.validate().map_err(|e| {
481 GeneratorError::ValidationError(format!(
482 "Configuration validation failed:\n{}",
483 format_validation_errors(&e)
484 ))
485 })?;
486
487 Ok(config)
488 }
489
490 pub fn into_generator_config(self) -> GeneratorConfig {
492 use crate::http_config::{AuthConfig, HttpClientConfig, RetryConfig};
493
494 let http_client_config = self.http_client.as_ref().map(|http| HttpClientConfig {
496 base_url: http.base_url.clone(),
497 timeout_seconds: http.timeout_seconds,
498 default_headers: http
499 .headers
500 .iter()
501 .map(|h| (h.name.clone(), h.value.clone()))
502 .collect(),
503 });
504
505 let retry_config = self
507 .http_client
508 .as_ref()
509 .and_then(|http| http.retry.as_ref())
510 .map(|retry| RetryConfig {
511 max_retries: retry.max_retries,
512 initial_delay_ms: retry.initial_delay_ms,
513 max_delay_ms: retry.max_delay_ms,
514 });
515
516 let tracing_enabled = self
518 .http_client
519 .as_ref()
520 .and_then(|http| http.tracing.as_ref())
521 .map(|tracing| tracing.enabled)
522 .unwrap_or(true);
523
524 let auth_config = self
526 .http_client
527 .as_ref()
528 .and_then(|http| http.auth.as_ref())
529 .map(|auth| match auth.auth_type.as_str() {
530 "Bearer" => AuthConfig::Bearer {
531 header_name: auth.header_name.clone(),
532 },
533 "ApiKey" => AuthConfig::ApiKey {
534 header_name: auth.header_name.clone(),
535 },
536 "Custom" => AuthConfig::Custom {
537 header_name: auth.header_name.clone(),
538 header_value_prefix: None,
539 },
540 _ => AuthConfig::Bearer {
541 header_name: "Authorization".to_string(),
542 },
543 });
544
545 let streaming_config = self.streaming.map(|section| {
547 use crate::streaming::{
548 EventFlow, HttpMethod, QueryParameter, StreamingConfig, StreamingEndpoint,
549 };
550
551 let endpoints = section
552 .endpoints
553 .into_iter()
554 .map(|e| {
555 let event_flow = e
556 .event_flow
557 .map(|ef| match ef.flow_type.as_str() {
558 "start_delta_stop" => EventFlow::StartDeltaStop {
559 start_events: ef.start_events.unwrap_or_default(),
560 delta_events: ef.delta_events.unwrap_or_default(),
561 stop_events: ef.stop_events.unwrap_or_default(),
562 },
563 _ => EventFlow::Simple,
564 })
565 .unwrap_or(EventFlow::Simple);
566
567 let http_method = e
568 .http_method
569 .map(|m| match m.to_uppercase().as_str() {
570 "GET" => HttpMethod::Get,
571 _ => HttpMethod::Post,
572 })
573 .unwrap_or(HttpMethod::Post);
574
575 let query_parameters = e
576 .query_parameters
577 .into_iter()
578 .map(|qp| QueryParameter {
579 name: qp.name,
580 required: qp.required,
581 })
582 .collect();
583
584 StreamingEndpoint {
585 operation_id: e.operation_id,
586 path: e.path,
587 http_method,
588 stream_parameter: e.stream_parameter,
589 query_parameters,
590 event_union_type: e.event_union_type,
591 content_type: e.content_type,
592 event_flow,
593 ..Default::default()
594 }
595 })
596 .collect();
597
598 StreamingConfig {
599 endpoints,
600 ..Default::default()
601 }
602 });
603
604 GeneratorConfig {
605 spec_path: self.generator.spec_path,
606 output_dir: self.generator.output_dir,
607 module_name: self.generator.module_name,
608 enable_sse_client: self.features.enable_sse_client,
609 enable_async_client: self.features.enable_async_client,
610 enable_specta: self.features.enable_specta,
611 type_mappings: if self.type_mappings.is_empty() {
612 super::generator::default_type_mappings()
613 } else {
614 self.type_mappings
615 },
616 streaming_config,
617 nullable_field_overrides: self.nullable_overrides,
618 extensible_enum_overrides: self.extensible_enums,
619 schema_extensions: self.generator.schema_extensions,
620 http_client_config,
621 retry_config,
622 tracing_enabled,
623 auth_config,
624 enable_registry: self.features.enable_registry,
625 registry_only: self.features.registry_only,
626 types: self.types,
627 server: self.server,
628 }
629 }
630}
631
632fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
634 let mut messages = Vec::new();
635
636 for (field, field_errors) in errors.field_errors() {
638 for error in field_errors {
639 let msg = if let Some(message) = &error.message {
640 format!(" - {}: {}", field, message)
641 } else {
642 format!(" - {}: validation failed (code: {})", field, error.code)
643 };
644 messages.push(msg);
645 }
646 }
647
648 for (field, nested_errors) in errors.errors() {
650 if let validator::ValidationErrorsKind::Struct(struct_errors) = nested_errors {
651 let nested_msgs = format_validation_errors(struct_errors);
652 if !nested_msgs.is_empty() {
653 messages.push(format!(" - {} (nested):\n{}", field, nested_msgs));
654 }
655 }
656 }
657
658 messages.join("\n")
659}
660
661const EXAMPLE_CONFIG: &str = r#"[generator]
662spec_path = "openapi.json"
663output_dir = "src/generated"
664module_name = "types"
665
666[features]
667enable_async_client = true
668
669[http_client]
670base_url = "https://api.example.com"
671timeout_seconds = 30
672
673[http_client.retry]
674max_retries = 3
675
676[http_client.auth]
677type = "Bearer"
678header_name = "Authorization""#;