use std::collections::HashMap;
use drasi_lib::bootstrap::BootstrapProvider;
use drasi_plugin_sdk::prelude::*;
use utoipa::OpenApi;
use crate::config::{
ApiKeyLocation, AuthConfig, ContentTypeOverride, ElementMappingConfig, ElementTemplate,
ElementType, EndpointConfig, HttpBootstrapConfig, HttpMethod, PaginationConfig, ResponseConfig,
};
use crate::provider::HttpBootstrapProvider;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::HttpBootstrapConfig)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HttpBootstrapConfigDto {
#[schema(value_type = Vec<bootstrap::http::EndpointConfig>)]
pub endpoints: Vec<EndpointConfigDto>,
#[serde(default = "default_timeout")]
pub timeout_seconds: ConfigValue<u64>,
#[serde(default = "default_retries")]
pub max_retries: ConfigValue<u32>,
#[serde(default = "default_retry_delay")]
pub retry_delay_ms: ConfigValue<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_pages: Option<ConfigValue<u64>>,
}
fn default_timeout() -> ConfigValue<u64> {
ConfigValue::Static(30)
}
fn default_retries() -> ConfigValue<u32> {
ConfigValue::Static(3)
}
fn default_retry_delay() -> ConfigValue<u64> {
ConfigValue::Static(1000)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::EndpointConfig)]
#[serde(rename_all = "camelCase")]
pub struct EndpointConfigDto {
pub url: ConfigValue<String>,
#[serde(default = "default_method")]
#[schema(value_type = bootstrap::http::HttpMethod)]
pub method: HttpMethodDto,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<bootstrap::http::AuthConfig>)]
pub auth: Option<AuthConfigDto>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<bootstrap::http::PaginationConfig>)]
pub pagination: Option<PaginationConfigDto>,
#[schema(value_type = bootstrap::http::ResponseConfig)]
pub response: ResponseConfigDto,
}
fn default_method() -> HttpMethodDto {
HttpMethodDto::Get
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::HttpMethod)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethodDto {
Get,
Post,
Put,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::AuthConfig)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum AuthConfigDto {
Bearer {
token_env: ConfigValue<String>,
},
ApiKey {
#[schema(value_type = bootstrap::http::ApiKeyLocation)]
location: ApiKeyLocationDto,
name: ConfigValue<String>,
value_env: ConfigValue<String>,
},
Basic {
username_env: ConfigValue<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
password_env: Option<ConfigValue<String>>,
},
#[serde(rename = "oauth2-client-credentials")]
OAuth2ClientCredentials {
token_url: ConfigValue<String>,
client_id_env: ConfigValue<String>,
client_secret_env: ConfigValue<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
scopes: Vec<ConfigValue<String>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ApiKeyLocation)]
#[serde(rename_all = "kebab-case")]
pub enum ApiKeyLocationDto {
Header,
Query,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::PaginationConfig)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum PaginationConfigDto {
OffsetLimit {
#[serde(default = "default_offset_param")]
offset_param: ConfigValue<String>,
#[serde(default = "default_limit_param")]
limit_param: ConfigValue<String>,
page_size: ConfigValue<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
total_path: Option<ConfigValue<String>>,
},
PageNumber {
#[serde(default = "default_page_param")]
page_param: ConfigValue<String>,
#[serde(default = "default_per_page_param")]
page_size_param: ConfigValue<String>,
page_size: ConfigValue<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
total_pages_path: Option<ConfigValue<String>>,
},
Cursor {
cursor_param: ConfigValue<String>,
cursor_path: ConfigValue<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
has_more_path: Option<ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
page_size_param: Option<ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
page_size: Option<ConfigValue<u64>>,
},
LinkHeader {
#[serde(default, skip_serializing_if = "Option::is_none")]
page_size_param: Option<ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
page_size: Option<ConfigValue<u64>>,
},
NextUrl {
next_url_path: ConfigValue<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
base_url: Option<ConfigValue<String>>,
},
}
fn default_offset_param() -> ConfigValue<String> {
ConfigValue::Static("offset".to_string())
}
fn default_limit_param() -> ConfigValue<String> {
ConfigValue::Static("limit".to_string())
}
fn default_page_param() -> ConfigValue<String> {
ConfigValue::Static("page".to_string())
}
fn default_per_page_param() -> ConfigValue<String> {
ConfigValue::Static("per_page".to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ResponseConfig)]
#[serde(rename_all = "camelCase")]
pub struct ResponseConfigDto {
#[serde(default = "default_items_path")]
pub items_path: ConfigValue<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<bootstrap::http::ContentTypeOverride>)]
pub content_type: Option<ContentTypeOverrideDto>,
#[schema(value_type = Vec<bootstrap::http::ElementMappingConfig>)]
pub mappings: Vec<ElementMappingConfigDto>,
}
fn default_items_path() -> ConfigValue<String> {
ConfigValue::Static("$".to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ContentTypeOverride)]
#[serde(rename_all = "lowercase")]
pub enum ContentTypeOverrideDto {
Json,
Xml,
Yaml,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ElementMappingConfig)]
#[serde(rename_all = "camelCase")]
pub struct ElementMappingConfigDto {
#[schema(value_type = bootstrap::http::ElementType)]
pub element_type: ElementTypeDto,
#[schema(value_type = bootstrap::http::ElementTemplate)]
pub template: ElementTemplateDto,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ElementType)]
#[serde(rename_all = "lowercase")]
pub enum ElementTypeDto {
Node,
Relation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
#[schema(as = bootstrap::http::ElementTemplate)]
#[serde(rename_all = "camelCase")]
pub struct ElementTemplateDto {
pub id: ConfigValue<String>,
pub labels: Vec<ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub properties: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<ConfigValue<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<ConfigValue<String>>,
}
fn map_http_method(dto: &HttpMethodDto) -> HttpMethod {
match dto {
HttpMethodDto::Get => HttpMethod::Get,
HttpMethodDto::Post => HttpMethod::Post,
HttpMethodDto::Put => HttpMethod::Put,
}
}
fn map_api_key_location(dto: &ApiKeyLocationDto) -> ApiKeyLocation {
match dto {
ApiKeyLocationDto::Header => ApiKeyLocation::Header,
ApiKeyLocationDto::Query => ApiKeyLocation::Query,
}
}
fn map_element_type(dto: &ElementTypeDto) -> ElementType {
match dto {
ElementTypeDto::Node => ElementType::Node,
ElementTypeDto::Relation => ElementType::Relation,
}
}
fn map_content_type_override(dto: &ContentTypeOverrideDto) -> ContentTypeOverride {
match dto {
ContentTypeOverrideDto::Json => ContentTypeOverride::Json,
ContentTypeOverrideDto::Xml => ContentTypeOverride::Xml,
ContentTypeOverrideDto::Yaml => ContentTypeOverride::Yaml,
}
}
async fn map_auth_config(
dto: &AuthConfigDto,
resolver: &DtoMapper,
) -> Result<AuthConfig, MappingError> {
match dto {
AuthConfigDto::Bearer { token_env } => Ok(AuthConfig::Bearer {
token_env: resolver.resolve_string(token_env).await?,
}),
AuthConfigDto::ApiKey {
location,
name,
value_env,
} => Ok(AuthConfig::ApiKey {
location: map_api_key_location(location),
name: resolver.resolve_string(name).await?,
value_env: resolver.resolve_string(value_env).await?,
}),
AuthConfigDto::Basic {
username_env,
password_env,
} => Ok(AuthConfig::Basic {
username_env: resolver.resolve_string(username_env).await?,
password_env: resolver.resolve_optional_string(password_env).await?,
}),
AuthConfigDto::OAuth2ClientCredentials {
token_url,
client_id_env,
client_secret_env,
scopes,
} => Ok(AuthConfig::OAuth2ClientCredentials {
token_url: resolver.resolve_string(token_url).await?,
client_id_env: resolver.resolve_string(client_id_env).await?,
client_secret_env: resolver.resolve_string(client_secret_env).await?,
scopes: resolver.resolve_string_vec(scopes).await?,
}),
}
}
async fn map_pagination_config(
dto: &PaginationConfigDto,
resolver: &DtoMapper,
) -> Result<PaginationConfig, MappingError> {
match dto {
PaginationConfigDto::OffsetLimit {
offset_param,
limit_param,
page_size,
total_path,
} => Ok(PaginationConfig::OffsetLimit {
offset_param: resolver.resolve_string(offset_param).await?,
limit_param: resolver.resolve_string(limit_param).await?,
page_size: resolver.resolve_typed(page_size).await?,
total_path: resolver.resolve_optional_string(total_path).await?,
}),
PaginationConfigDto::PageNumber {
page_param,
page_size_param,
page_size,
total_pages_path,
} => Ok(PaginationConfig::PageNumber {
page_param: resolver.resolve_string(page_param).await?,
page_size_param: resolver.resolve_string(page_size_param).await?,
page_size: resolver.resolve_typed(page_size).await?,
total_pages_path: resolver.resolve_optional_string(total_pages_path).await?,
}),
PaginationConfigDto::Cursor {
cursor_param,
cursor_path,
has_more_path,
page_size_param,
page_size,
} => Ok(PaginationConfig::Cursor {
cursor_param: resolver.resolve_string(cursor_param).await?,
cursor_path: resolver.resolve_string(cursor_path).await?,
has_more_path: resolver.resolve_optional_string(has_more_path).await?,
page_size_param: resolver.resolve_optional_string(page_size_param).await?,
page_size: resolver.resolve_optional(page_size).await?,
}),
PaginationConfigDto::LinkHeader {
page_size_param,
page_size,
} => Ok(PaginationConfig::LinkHeader {
page_size_param: resolver.resolve_optional_string(page_size_param).await?,
page_size: resolver.resolve_optional(page_size).await?,
}),
PaginationConfigDto::NextUrl {
next_url_path,
base_url,
} => Ok(PaginationConfig::NextUrl {
next_url_path: resolver.resolve_string(next_url_path).await?,
base_url: resolver.resolve_optional_string(base_url).await?,
}),
}
}
async fn map_element_template(
dto: &ElementTemplateDto,
resolver: &DtoMapper,
) -> Result<ElementTemplate, MappingError> {
Ok(ElementTemplate {
id: resolver.resolve_string(&dto.id).await?,
labels: resolver.resolve_string_vec(&dto.labels).await?,
properties: dto.properties.clone(),
from: resolver.resolve_optional_string(&dto.from).await?,
to: resolver.resolve_optional_string(&dto.to).await?,
})
}
async fn map_element_mapping(
dto: &ElementMappingConfigDto,
resolver: &DtoMapper,
) -> Result<ElementMappingConfig, MappingError> {
Ok(ElementMappingConfig {
element_type: map_element_type(&dto.element_type),
template: map_element_template(&dto.template, resolver).await?,
})
}
async fn map_response_config(
dto: &ResponseConfigDto,
resolver: &DtoMapper,
) -> Result<ResponseConfig, MappingError> {
let mut mappings = Vec::with_capacity(dto.mappings.len());
for mapping in &dto.mappings {
mappings.push(map_element_mapping(mapping, resolver).await?);
}
Ok(ResponseConfig {
items_path: resolver.resolve_string(&dto.items_path).await?,
content_type: dto.content_type.as_ref().map(map_content_type_override),
mappings,
})
}
async fn map_endpoint_config(
dto: &EndpointConfigDto,
resolver: &DtoMapper,
) -> Result<EndpointConfig, MappingError> {
let mut headers = HashMap::with_capacity(dto.headers.len());
for (key, value) in &dto.headers {
headers.insert(key.clone(), resolver.resolve_string(value).await?);
}
let auth = match &dto.auth {
Some(auth) => Some(map_auth_config(auth, resolver).await?),
None => None,
};
let pagination = match &dto.pagination {
Some(pagination) => Some(map_pagination_config(pagination, resolver).await?),
None => None,
};
Ok(EndpointConfig {
url: resolver.resolve_string(&dto.url).await?,
method: map_http_method(&dto.method),
headers,
body: dto.body.clone(),
auth,
pagination,
response: map_response_config(&dto.response, resolver).await?,
})
}
async fn map_config(
dto: &HttpBootstrapConfigDto,
resolver: &DtoMapper,
) -> Result<HttpBootstrapConfig, MappingError> {
let mut endpoints = Vec::with_capacity(dto.endpoints.len());
for endpoint in &dto.endpoints {
endpoints.push(map_endpoint_config(endpoint, resolver).await?);
}
let max_pages = match &dto.max_pages {
Some(value) => Some(resolver.resolve_typed(value).await?),
None => None,
};
Ok(HttpBootstrapConfig {
endpoints,
timeout_seconds: resolver.resolve_typed(&dto.timeout_seconds).await?,
max_retries: resolver.resolve_typed(&dto.max_retries).await?,
retry_delay_ms: resolver.resolve_typed(&dto.retry_delay_ms).await?,
max_pages,
})
}
#[derive(OpenApi)]
#[openapi(components(schemas(
HttpBootstrapConfigDto,
EndpointConfigDto,
HttpMethodDto,
AuthConfigDto,
ApiKeyLocationDto,
PaginationConfigDto,
ResponseConfigDto,
ContentTypeOverrideDto,
ElementMappingConfigDto,
ElementTypeDto,
ElementTemplateDto,
)))]
struct HttpBootstrapSchemas;
pub struct HttpBootstrapDescriptor;
#[async_trait]
impl BootstrapPluginDescriptor for HttpBootstrapDescriptor {
fn kind(&self) -> &str {
"http"
}
fn config_version(&self) -> &str {
"1.0.0"
}
fn config_schema_name(&self) -> &str {
"bootstrap.http.HttpBootstrapConfig"
}
fn config_schema_json(&self) -> String {
let api = HttpBootstrapSchemas::openapi();
serde_json::to_string(
&api.components
.as_ref()
.expect("OpenAPI components missing")
.schemas,
)
.expect("Failed to serialize config schema")
}
async fn create_bootstrap_provider(
&self,
config_json: &serde_json::Value,
_source_config_json: &serde_json::Value,
) -> anyhow::Result<Box<dyn BootstrapProvider>> {
let dto: HttpBootstrapConfigDto = serde_json::from_value(config_json.clone())
.map_err(|e| anyhow::anyhow!("Failed to parse HTTP bootstrap config: {e}"))?;
let mapper = DtoMapper::new();
let config = map_config(&dto, &mapper)
.await
.map_err(|e| anyhow::anyhow!("Failed to resolve HTTP bootstrap config: {e}"))?;
config.validate()?;
let provider = HttpBootstrapProvider::new(config)?;
Ok(Box::new(provider))
}
}