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