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