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