1use crate::{GeneratorError, generator::GeneratorConfig};
155use serde::{Deserialize, Serialize};
156use std::collections::BTreeMap;
157use std::path::{Path, PathBuf};
158
159#[derive(Debug, Clone)]
161pub struct ConfigFile {
162 pub generator: GeneratorSection,
163 pub features: FeaturesSection,
164 pub http_client: Option<HttpClientSection>,
165 pub streaming: Option<StreamingSection>,
166 pub server: Option<ServerSection>,
169 pub client: Option<ClientSection>,
171 pub nullable_overrides: BTreeMap<String, bool>,
172 pub extensible_enums: BTreeMap<String, bool>,
177 pub type_mappings: BTreeMap<String, String>,
178 pub types: crate::type_mapping::TypeMappingConfig,
184}
185
186#[derive(Debug, Clone, Deserialize, Serialize)]
187#[serde(deny_unknown_fields)]
188pub struct GeneratorSection {
189 pub spec_path: PathBuf,
192 pub output_dir: PathBuf,
195 pub module_name: String,
202 #[serde(default)]
205 pub schema_extensions: Vec<PathBuf>,
206 #[serde(default)]
208 pub builders: BuildersSection,
209}
210
211#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
213#[serde(default, deny_unknown_fields)]
214pub struct BuildersSection {
215 pub enabled: bool,
217 pub threshold: usize,
220}
221
222impl Default for BuildersSection {
223 fn default() -> Self {
224 Self {
225 enabled: false,
226 threshold: 3,
227 }
228 }
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
232#[serde(deny_unknown_fields)]
233pub struct FeaturesSection {
234 #[serde(default)]
235 pub enable_sse_client: bool,
236 #[serde(default)]
237 pub enable_async_client: bool,
238 #[serde(default)]
239 pub enable_specta: bool,
240 #[serde(default)]
242 pub enable_registry: bool,
243 #[serde(default)]
245 pub registry_only: bool,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize)]
256#[serde(deny_unknown_fields)]
257pub struct ServerSection {
258 pub framework: String,
260 #[serde(default)]
263 pub operations: Vec<String>,
264 #[serde(default)]
268 pub prune_models: bool,
269}
270
271#[derive(Debug, Clone, Deserialize, Serialize)]
280#[serde(deny_unknown_fields)]
281pub struct ClientSection {
282 #[serde(default)]
285 pub operations: Vec<String>,
286 #[serde(default)]
289 pub prune_models: bool,
290}
291
292impl ClientSection {
293 pub fn parsed_selectors(
295 &self,
296 ) -> Result<Vec<crate::server::Selector>, crate::server::SelectorParseError> {
297 self.operations
298 .iter()
299 .map(|s| crate::server::Selector::parse(s))
300 .collect()
301 }
302}
303
304impl ServerSection {
305 pub fn parsed_selectors(
308 &self,
309 ) -> Result<Vec<crate::server::Selector>, crate::server::SelectorParseError> {
310 self.operations
311 .iter()
312 .map(|s| crate::server::Selector::parse(s))
313 .collect()
314 }
315}
316
317#[derive(Debug, Clone, Deserialize, Serialize)]
318#[serde(deny_unknown_fields)]
319pub struct HttpClientSection {
320 pub base_url: Option<String>,
321 pub timeout_seconds: Option<u64>,
322 pub auth: Option<AuthConfigSection>,
323 #[serde(default)]
324 pub headers: Vec<HeaderEntry>,
325 pub retry: Option<RetryConfigSection>,
326 pub tracing: Option<TracingConfigSection>,
327}
328
329#[derive(Debug, Clone, Deserialize, Serialize)]
330#[serde(deny_unknown_fields)]
331pub struct TracingConfigSection {
332 #[serde(default = "default_tracing_enabled")]
333 pub enabled: bool,
334}
335
336fn default_tracing_enabled() -> bool {
337 true
338}
339
340#[derive(Debug, Clone, Deserialize, Serialize)]
341#[serde(deny_unknown_fields)]
342pub struct RetryConfigSection {
343 #[serde(default = "default_max_retries")]
344 pub max_retries: u32,
345 #[serde(default = "default_initial_delay_ms")]
346 pub initial_delay_ms: u64,
347 #[serde(default = "default_max_delay_ms")]
348 pub max_delay_ms: u64,
349}
350
351fn default_max_retries() -> u32 {
352 3
353}
354fn default_initial_delay_ms() -> u64 {
355 500
356}
357fn default_max_delay_ms() -> u64 {
358 16000
359}
360
361#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(deny_unknown_fields)]
363pub struct AuthConfigSection {
364 #[serde(rename = "type")]
365 pub auth_type: String,
366 pub header_name: String,
367}
368
369#[derive(Debug, Clone, Deserialize, Serialize)]
370#[serde(deny_unknown_fields)]
371pub struct HeaderEntry {
372 pub name: String,
373 pub value: String,
374}
375
376#[derive(Debug, Clone, Deserialize, Serialize)]
377#[serde(deny_unknown_fields)]
378pub struct StreamingSection {
379 pub endpoints: Vec<StreamingEndpointSection>,
380}
381
382#[derive(Debug, Clone, Deserialize, Serialize)]
383#[serde(deny_unknown_fields)]
384pub struct StreamingEndpointSection {
385 pub operation_id: String,
386 pub path: String,
387 #[serde(default)]
389 pub http_method: Option<String>,
390 #[serde(default)]
392 pub stream_parameter: String,
393 #[serde(default)]
395 pub query_parameters: Vec<QueryParameterSection>,
396 pub event_union_type: String,
397 pub content_type: Option<String>,
398 pub event_flow: Option<EventFlowSection>,
399}
400
401#[derive(Debug, Clone, Deserialize, Serialize)]
402#[serde(deny_unknown_fields)]
403pub struct QueryParameterSection {
404 pub name: String,
405 #[serde(default)]
406 pub required: bool,
407}
408
409#[derive(Debug, Clone, Deserialize, Serialize)]
410#[serde(deny_unknown_fields)]
411pub struct EventFlowSection {
412 #[serde(rename = "type")]
413 pub flow_type: String,
414 pub start_events: Option<Vec<String>>,
415 pub delta_events: Option<Vec<String>>,
416 pub stop_events: Option<Vec<String>>,
417}
418
419#[derive(Deserialize)]
423#[serde(deny_unknown_fields)]
424struct ConfigFileWire {
425 generator: GeneratorSectionWire,
426 features: FeaturesSection,
427 #[serde(default)]
428 http_client: Option<HttpClientSection>,
429 #[serde(default)]
430 streaming: Option<StreamingSection>,
431 #[serde(default)]
432 server: Option<ServerSection>,
433 #[serde(default)]
434 client: Option<ClientSection>,
435 #[serde(default)]
436 nullable_overrides: BTreeMap<String, bool>,
437 #[serde(default)]
438 extensible_enums: BTreeMap<String, bool>,
439 #[serde(default)]
440 type_mappings: BTreeMap<String, String>,
441 #[serde(default)]
442 types: Option<crate::type_mapping::TypeMappingConfig>,
443}
444
445#[derive(Deserialize)]
446#[serde(deny_unknown_fields)]
447struct GeneratorSectionWire {
448 spec_path: PathBuf,
449 output_dir: PathBuf,
450 module_name: String,
451 #[serde(default)]
452 schema_extensions: Vec<PathBuf>,
453 #[serde(default)]
454 builders: BuildersSection,
455 #[serde(default)]
456 types: Option<crate::type_mapping::TypeMappingConfig>,
457}
458
459impl TryFrom<ConfigFileWire> for ConfigFile {
460 type Error = String;
461
462 fn try_from(wire: ConfigFileWire) -> Result<Self, Self::Error> {
463 let types = match (wire.generator.types, wire.types) {
464 (Some(_), Some(_)) => {
465 return Err(
466 "Configuration contains both legacy [types] and canonical [generator.types]. Remove [types] and keep [generator.types]."
467 .to_string(),
468 );
469 }
470 (Some(types), None) | (None, Some(types)) => types,
471 (None, None) => crate::type_mapping::TypeMappingConfig::default(),
472 };
473
474 Ok(Self {
475 generator: GeneratorSection {
476 spec_path: wire.generator.spec_path,
477 output_dir: wire.generator.output_dir,
478 module_name: wire.generator.module_name,
479 schema_extensions: wire.generator.schema_extensions,
480 builders: wire.generator.builders,
481 },
482 features: wire.features,
483 http_client: wire.http_client,
484 streaming: wire.streaming,
485 server: wire.server,
486 client: wire.client,
487 nullable_overrides: wire.nullable_overrides,
488 extensible_enums: wire.extensible_enums,
489 type_mappings: wire.type_mappings,
490 types,
491 })
492 }
493}
494
495impl<'de> Deserialize<'de> for ConfigFile {
496 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497 where
498 D: serde::Deserializer<'de>,
499 {
500 ConfigFileWire::deserialize(deserializer)?
501 .try_into()
502 .map_err(serde::de::Error::custom)
503 }
504}
505
506#[derive(Serialize)]
507struct ConfigFileRef<'a> {
508 generator: GeneratorSectionRef<'a>,
509 features: &'a FeaturesSection,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 http_client: Option<&'a HttpClientSection>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 streaming: Option<&'a StreamingSection>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 server: Option<&'a ServerSection>,
516 #[serde(skip_serializing_if = "Option::is_none")]
517 client: Option<&'a ClientSection>,
518 nullable_overrides: &'a BTreeMap<String, bool>,
519 extensible_enums: &'a BTreeMap<String, bool>,
520 type_mappings: &'a BTreeMap<String, String>,
521}
522
523#[derive(Serialize)]
524struct GeneratorSectionRef<'a> {
525 spec_path: &'a Path,
526 output_dir: &'a Path,
527 module_name: &'a str,
528 schema_extensions: &'a [PathBuf],
529 builders: &'a BuildersSection,
530 types: &'a crate::type_mapping::TypeMappingConfig,
531}
532
533impl Serialize for ConfigFile {
534 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
535 where
536 S: serde::Serializer,
537 {
538 ConfigFileRef {
539 generator: GeneratorSectionRef {
540 spec_path: &self.generator.spec_path,
541 output_dir: &self.generator.output_dir,
542 module_name: &self.generator.module_name,
543 schema_extensions: &self.generator.schema_extensions,
544 builders: &self.generator.builders,
545 types: &self.types,
546 },
547 features: &self.features,
548 http_client: self.http_client.as_ref(),
549 streaming: self.streaming.as_ref(),
550 server: self.server.as_ref(),
551 client: self.client.as_ref(),
552 nullable_overrides: &self.nullable_overrides,
553 extensible_enums: &self.extensible_enums,
554 type_mappings: &self.type_mappings,
555 }
556 .serialize(serializer)
557 }
558}
559
560fn resolve_relative_path(config_dir: &Path, path: &mut PathBuf) {
561 if path.is_relative() && !path.to_string_lossy().contains("://") {
562 *path = config_dir.join(&*path);
563 }
564}
565
566fn inspect_type_config_layout(value: &toml::Value) -> Result<(), GeneratorError> {
567 let legacy_types = value.get("types").is_some();
568 let canonical_types = value
569 .get("generator")
570 .and_then(|generator| generator.get("types"));
571
572 if legacy_types && canonical_types.is_some() {
573 return Err(GeneratorError::ValidationError(
574 "Configuration contains both legacy [types] and canonical [generator.types]. Remove [types] and keep [generator.types]."
575 .to_string(),
576 ));
577 }
578
579 if canonical_types
580 .and_then(|types| types.get("strategies"))
581 .is_some()
582 {
583 return Err(GeneratorError::ValidationError(
584 "[generator.types.strategies] is obsolete. Move its fields directly under [generator.types]. Use snake_case keys such as date_time (not date-time); valid byte values are string, base64, base64_url_unpadded, and vec_u8 (for example: byte = \"base64\")."
585 .to_string(),
586 ));
587 }
588
589 Ok(())
590}
591
592impl ConfigFile {
593 pub fn load(path: &Path) -> Result<Self, GeneratorError> {
599 let config_path = path.canonicalize().map_err(|e| GeneratorError::FileError {
600 message: format!("Failed to resolve config file '{}': {}", path.display(), e),
601 })?;
602 let config_dir = config_path
603 .parent()
604 .ok_or_else(|| GeneratorError::FileError {
605 message: format!(
606 "Config file '{}' has no parent directory",
607 config_path.display()
608 ),
609 })?;
610 let content =
611 std::fs::read_to_string(&config_path).map_err(|e| GeneratorError::FileError {
612 message: format!(
613 "Failed to read config file '{}': {}",
614 config_path.display(),
615 e
616 ),
617 })?;
618
619 let value: toml::Value =
620 toml::from_str(&content).map_err(|e| GeneratorError::FileError {
621 message: format!(
622 "Failed to parse TOML config: {}\n\nExample config:\n{}",
623 e, EXAMPLE_CONFIG
624 ),
625 })?;
626 inspect_type_config_layout(&value)?;
627
628 let mut config: ConfigFile =
629 toml::from_str(&content).map_err(|e| GeneratorError::FileError {
630 message: format!(
631 "Failed to parse TOML config: {}\n\nExample config:\n{}",
632 e, EXAMPLE_CONFIG
633 ),
634 })?;
635
636 resolve_relative_path(config_dir, &mut config.generator.spec_path);
637 resolve_relative_path(config_dir, &mut config.generator.output_dir);
638 for extension in &mut config.generator.schema_extensions {
639 resolve_relative_path(config_dir, extension);
640 }
641
642 config.validate()?;
643
644 Ok(config)
645 }
646
647 fn validate(&self) -> Result<(), GeneratorError> {
648 let mut errors = Vec::new();
649
650 let spec_source = self.generator.spec_path.to_string_lossy();
651 if crate::spec_source::is_remote_spec(&spec_source) {
652 if let Err(error) = crate::spec_source::validate_remote_spec_url(&spec_source) {
653 errors.push(format!("generator.spec_path: {error}"));
654 }
655 } else if spec_source.contains("://") {
656 let error = crate::spec_source::validate_remote_spec_url(&spec_source)
657 .err()
658 .unwrap_or_else(|| "unsupported remote OpenAPI URL".to_string());
659 errors.push(format!("generator.spec_path: {error}"));
660 } else if !self.generator.spec_path.exists() {
661 errors.push(format!(
662 "generator.spec_path: OpenAPI spec file not found: {}. Ensure spec_path points to a valid OpenAPI JSON or YAML file.",
663 self.generator.spec_path.display()
664 ));
665 }
666 if self.generator.module_name.is_empty() {
667 errors.push("generator.module_name: module_name cannot be empty".to_string());
668 }
669
670 if let Some(server) = &self.server
671 && server.framework != "axum"
672 {
673 errors.push(format!(
674 "server.framework: framework must be \"axum\" (got \"{}\"); other frameworks are not supported yet",
675 server.framework
676 ));
677 }
678
679 if let Some(client) = &self.client {
680 for (index, selector) in client.operations.iter().enumerate() {
681 if let Err(error) = crate::server::Selector::parse(selector) {
682 errors.push(format!("client.operations[{index}]: {error}"));
683 }
684 }
685 }
686 if let Some(server) = &self.server {
687 for (index, selector) in server.operations.iter().enumerate() {
688 if let Err(error) = crate::server::Selector::parse(selector) {
689 errors.push(format!("server.operations[{index}]: {error}"));
690 }
691 }
692 }
693
694 if let Some(http) = &self.http_client {
695 if let Some(base_url) = &http.base_url
696 && url::Url::parse(base_url).is_err()
697 {
698 errors.push("http_client.base_url: base_url must be a valid URL".to_string());
699 }
700 if let Some(timeout) = http.timeout_seconds
701 && !(1..=3600).contains(&timeout)
702 {
703 errors.push(
704 "http_client.timeout_seconds: timeout_seconds must be between 1 and 3600"
705 .to_string(),
706 );
707 }
708 if let Some(auth) = &http.auth {
709 if !matches!(auth.auth_type.as_str(), "Bearer" | "ApiKey" | "Custom") {
710 errors.push(format!(
711 "http_client.auth.type: Invalid auth type '{}'. Must be one of: Bearer, ApiKey, Custom",
712 auth.auth_type
713 ));
714 }
715 if auth.header_name.is_empty() {
716 errors.push(
717 "http_client.auth.header_name: header_name cannot be empty".to_string(),
718 );
719 }
720 }
721 for (index, header) in http.headers.iter().enumerate() {
722 if header.name.is_empty() {
723 errors.push(format!(
724 "http_client.headers[{index}].name: header name cannot be empty"
725 ));
726 }
727 }
728 if let Some(retry) = &http.retry {
729 if retry.max_retries > 10 {
730 errors.push(
731 "http_client.retry.max_retries: max_retries must be between 0 and 10"
732 .to_string(),
733 );
734 }
735 if !(100..=10000).contains(&retry.initial_delay_ms) {
736 errors.push(
737 "http_client.retry.initial_delay_ms: initial_delay_ms must be between 100 and 10000"
738 .to_string(),
739 );
740 }
741 if !(1000..=300000).contains(&retry.max_delay_ms) {
742 errors.push(
743 "http_client.retry.max_delay_ms: max_delay_ms must be between 1000 and 300000"
744 .to_string(),
745 );
746 }
747 }
748 }
749
750 if let Some(streaming) = &self.streaming {
751 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
752 let prefix = format!("streaming.endpoints[{index}]");
753 if endpoint.operation_id.is_empty() {
754 errors.push(format!("{prefix}.operation_id: must not be empty"));
755 }
756 if endpoint.path.is_empty() {
757 errors.push(format!("{prefix}.path: must not be empty"));
758 }
759 if endpoint.event_union_type.is_empty() {
760 errors.push(format!("{prefix}.event_union_type: must not be empty"));
761 }
762 for (query_index, query) in endpoint.query_parameters.iter().enumerate() {
763 if query.name.is_empty() {
764 errors.push(format!(
765 "{prefix}.query_parameters[{query_index}].name: must not be empty"
766 ));
767 }
768 }
769 if let Some(flow) = &endpoint.event_flow
770 && !matches!(
771 flow.flow_type.as_str(),
772 "StartDeltaStop" | "start_delta_stop" | "Continuous"
773 )
774 {
775 errors.push(format!(
776 "{prefix}.event_flow.type: Invalid event flow type '{}'. Must be one of: StartDeltaStop, Continuous",
777 flow.flow_type
778 ));
779 }
780 }
781 }
782
783 if errors.is_empty() {
784 Ok(())
785 } else {
786 Err(GeneratorError::ValidationError(format!(
787 "Configuration validation failed:\n - {}",
788 errors.join("\n - ")
789 )))
790 }
791 }
792
793 pub fn into_generator_config(self) -> GeneratorConfig {
795 use crate::http_config::{AuthConfig, HttpClientConfig, RetryConfig};
796
797 let types = self.types;
798
799 let http_client_config = self.http_client.as_ref().map(|http| HttpClientConfig {
801 base_url: http.base_url.clone(),
802 timeout_seconds: http.timeout_seconds,
803 default_headers: http
804 .headers
805 .iter()
806 .map(|h| (h.name.clone(), h.value.clone()))
807 .collect(),
808 });
809
810 let retry_config = self
812 .http_client
813 .as_ref()
814 .and_then(|http| http.retry.as_ref())
815 .map(|retry| RetryConfig {
816 max_retries: retry.max_retries,
817 initial_delay_ms: retry.initial_delay_ms,
818 max_delay_ms: retry.max_delay_ms,
819 });
820
821 let tracing_enabled = self
823 .http_client
824 .as_ref()
825 .and_then(|http| http.tracing.as_ref())
826 .map(|tracing| tracing.enabled)
827 .unwrap_or(true);
828
829 let auth_config = self
831 .http_client
832 .as_ref()
833 .and_then(|http| http.auth.as_ref())
834 .map(|auth| match auth.auth_type.as_str() {
835 "Bearer" => AuthConfig::Bearer {
836 header_name: auth.header_name.clone(),
837 },
838 "ApiKey" => AuthConfig::ApiKey {
839 header_name: auth.header_name.clone(),
840 },
841 "Custom" => AuthConfig::Custom {
842 header_name: auth.header_name.clone(),
843 header_value_prefix: None,
844 },
845 _ => AuthConfig::Bearer {
846 header_name: "Authorization".to_string(),
847 },
848 });
849
850 let streaming_config = self.streaming.map(|section| {
852 use crate::streaming::{
853 EventFlow, HttpMethod, QueryParameter, StreamingConfig, StreamingEndpoint,
854 };
855
856 let endpoints = section
857 .endpoints
858 .into_iter()
859 .map(|e| {
860 let event_flow = e
861 .event_flow
862 .map(|ef| match ef.flow_type.as_str() {
863 "StartDeltaStop" | "start_delta_stop" => EventFlow::StartDeltaStop {
864 start_events: ef.start_events.unwrap_or_default(),
865 delta_events: ef.delta_events.unwrap_or_default(),
866 stop_events: ef.stop_events.unwrap_or_default(),
867 },
868 _ => EventFlow::Simple,
869 })
870 .unwrap_or(EventFlow::Simple);
871
872 let http_method = e
873 .http_method
874 .map(|m| match m.to_uppercase().as_str() {
875 "GET" => HttpMethod::Get,
876 _ => HttpMethod::Post,
877 })
878 .unwrap_or(HttpMethod::Post);
879
880 let query_parameters = e
881 .query_parameters
882 .into_iter()
883 .map(|qp| QueryParameter {
884 name: qp.name,
885 required: qp.required,
886 })
887 .collect();
888
889 StreamingEndpoint {
890 operation_id: e.operation_id,
891 path: e.path,
892 http_method,
893 stream_parameter: e.stream_parameter,
894 query_parameters,
895 event_union_type: e.event_union_type,
896 content_type: e.content_type,
897 event_flow,
898 ..Default::default()
899 }
900 })
901 .collect();
902
903 StreamingConfig {
904 endpoints,
905 ..Default::default()
906 }
907 });
908
909 GeneratorConfig {
910 spec_path: self.generator.spec_path,
911 output_dir: self.generator.output_dir,
912 module_name: self.generator.module_name,
913 enable_sse_client: self.features.enable_sse_client,
914 enable_async_client: self.features.enable_async_client,
915 enable_specta: self.features.enable_specta,
916 type_mappings: if self.type_mappings.is_empty() {
917 super::generator::default_type_mappings()
918 } else {
919 self.type_mappings
920 },
921 streaming_config,
922 nullable_field_overrides: self.nullable_overrides,
923 extensible_enum_overrides: self.extensible_enums,
924 schema_extensions: self.generator.schema_extensions,
925 http_client_config,
926 retry_config,
927 tracing_enabled,
928 auth_config,
929 enable_registry: self.features.enable_registry,
930 registry_only: self.features.registry_only,
931 types,
932 builders: self.generator.builders,
933 server: self.server,
934 client: self.client,
935 }
936 }
937}
938
939const EXAMPLE_CONFIG: &str = r#"[generator]
940spec_path = "openapi.json"
941output_dir = "src/generated"
942module_name = "types"
943
944[generator.builders]
945enabled = true
946threshold = 3
947
948[features]
949enable_async_client = true
950
951[http_client]
952base_url = "https://api.example.com"
953timeout_seconds = 30
954
955[http_client.retry]
956max_retries = 3
957
958[http_client.auth]
959type = "Bearer"
960header_name = "Authorization""#;