1use crate::config::{
18 AuthConfig, BearerConfig, CorsConfig, EffectiveFromConfig, ElementTemplate, ElementType,
19 ErrorBehavior, HttpMethod, MappingCondition, OperationType, SignatureAlgorithm,
20 SignatureConfig, SignatureEncoding, TimestampFormat, WebhookConfig, WebhookMapping,
21 WebhookRoute,
22};
23use crate::{HttpSourceBuilder, HttpSourceConfig};
24use drasi_plugin_sdk::prelude::*;
25use std::collections::HashMap;
26use utoipa::OpenApi;
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
30#[schema(as = source::http::HttpSourceConfig)]
31#[serde(rename_all = "camelCase", deny_unknown_fields)]
32pub struct HttpSourceConfigDto {
33 pub host: ConfigValue<String>,
34 pub port: ConfigValue<u16>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub endpoint: Option<ConfigValue<String>>,
37 #[serde(default = "default_http_timeout_ms")]
38 pub timeout_ms: ConfigValue<u64>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub adaptive_max_batch_size: Option<ConfigValue<usize>>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub adaptive_min_batch_size: Option<ConfigValue<usize>>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub adaptive_max_wait_ms: Option<ConfigValue<u64>>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub adaptive_min_wait_ms: Option<ConfigValue<u64>>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub adaptive_window_secs: Option<ConfigValue<u64>>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub adaptive_enabled: Option<ConfigValue<bool>>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 #[schema(value_type = Option<source::http::WebhookConfig>)]
53 pub webhooks: Option<WebhookConfigDto>,
54}
55
56fn default_http_timeout_ms() -> ConfigValue<u64> {
57 ConfigValue::Static(10000)
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
61#[schema(as = source::http::WebhookConfig)]
62#[serde(rename_all = "camelCase")]
63pub struct WebhookConfigDto {
64 #[serde(default)]
65 #[schema(value_type = source::http::ErrorBehavior)]
66 pub error_behavior: ErrorBehaviorDto,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 #[schema(value_type = Option<source::http::CorsConfig>)]
69 pub cors: Option<CorsConfigDto>,
70 #[schema(value_type = Vec<source::http::WebhookRoute>)]
71 pub routes: Vec<WebhookRouteDto>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
75#[schema(as = source::http::CorsConfig)]
76#[serde(rename_all = "camelCase")]
77pub struct CorsConfigDto {
78 #[serde(default = "default_cors_enabled")]
79 pub enabled: bool,
80 #[serde(default = "default_cors_origins")]
81 pub allow_origins: Vec<ConfigValue<String>>,
82 #[serde(default = "default_cors_methods")]
83 pub allow_methods: Vec<ConfigValue<String>>,
84 #[serde(default = "default_cors_headers")]
85 pub allow_headers: Vec<ConfigValue<String>>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 pub expose_headers: Vec<ConfigValue<String>>,
88 #[serde(default)]
89 pub allow_credentials: bool,
90 #[serde(default = "default_cors_max_age")]
91 pub max_age: u64,
92}
93
94fn default_cors_enabled() -> bool {
95 true
96}
97
98fn default_cors_origins() -> Vec<ConfigValue<String>> {
99 vec![ConfigValue::Static("*".to_string())]
100}
101
102fn default_cors_methods() -> Vec<ConfigValue<String>> {
103 vec![
104 ConfigValue::Static("GET".to_string()),
105 ConfigValue::Static("POST".to_string()),
106 ConfigValue::Static("PUT".to_string()),
107 ConfigValue::Static("PATCH".to_string()),
108 ConfigValue::Static("DELETE".to_string()),
109 ConfigValue::Static("OPTIONS".to_string()),
110 ]
111}
112
113fn default_cors_headers() -> Vec<ConfigValue<String>> {
114 vec![
115 ConfigValue::Static("Content-Type".to_string()),
116 ConfigValue::Static("Authorization".to_string()),
117 ConfigValue::Static("X-Requested-With".to_string()),
118 ]
119}
120
121fn default_cors_max_age() -> u64 {
122 3600
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, utoipa::ToSchema)]
126#[schema(as = source::http::ErrorBehavior)]
127#[serde(rename_all = "snake_case")]
128pub enum ErrorBehaviorDto {
129 #[default]
130 AcceptAndLog,
131 AcceptAndSkip,
132 Reject,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
136#[schema(as = source::http::WebhookRoute)]
137#[serde(rename_all = "camelCase")]
138pub struct WebhookRouteDto {
139 pub path: ConfigValue<String>,
140 #[serde(default = "default_methods")]
141 #[schema(value_type = Vec<source::http::HttpMethod>)]
142 pub methods: Vec<HttpMethodDto>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 #[schema(value_type = Option<source::http::AuthConfig>)]
145 pub auth: Option<AuthConfigDto>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 #[schema(value_type = Option<source::http::ErrorBehavior>)]
148 pub error_behavior: Option<ErrorBehaviorDto>,
149 #[schema(value_type = Vec<source::http::WebhookMapping>)]
150 pub mappings: Vec<WebhookMappingDto>,
151}
152
153fn default_methods() -> Vec<HttpMethodDto> {
154 vec![HttpMethodDto::Post]
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, utoipa::ToSchema)]
158#[schema(as = source::http::HttpMethod)]
159#[serde(rename_all = "UPPERCASE")]
160pub enum HttpMethodDto {
161 Get,
162 Post,
163 Put,
164 Patch,
165 Delete,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
169#[schema(as = source::http::AuthConfig)]
170#[serde(rename_all = "camelCase")]
171pub struct AuthConfigDto {
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 #[schema(value_type = Option<source::http::SignatureConfig>)]
174 pub signature: Option<SignatureConfigDto>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 #[schema(value_type = Option<source::http::BearerConfig>)]
177 pub bearer: Option<BearerConfigDto>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
181#[schema(as = source::http::SignatureConfig)]
182#[serde(rename_all = "camelCase")]
183pub struct SignatureConfigDto {
184 #[serde(rename = "type")]
185 #[schema(value_type = source::http::SignatureAlgorithm)]
186 pub algorithm: SignatureAlgorithmDto,
187 pub secret_env: ConfigValue<String>,
188 pub header: ConfigValue<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub prefix: Option<ConfigValue<String>>,
191 #[serde(default)]
192 #[schema(value_type = source::http::SignatureEncoding)]
193 pub encoding: SignatureEncodingDto,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
197#[schema(as = source::http::SignatureAlgorithm)]
198#[serde(rename_all = "kebab-case")]
199pub enum SignatureAlgorithmDto {
200 HmacSha1,
201 HmacSha256,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, utoipa::ToSchema)]
205#[schema(as = source::http::SignatureEncoding)]
206#[serde(rename_all = "lowercase")]
207pub enum SignatureEncodingDto {
208 #[default]
209 Hex,
210 Base64,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
214#[schema(as = source::http::BearerConfig)]
215#[serde(rename_all = "camelCase")]
216pub struct BearerConfigDto {
217 pub token_env: ConfigValue<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
221#[schema(as = source::http::WebhookMapping)]
222#[serde(rename_all = "camelCase")]
223pub struct WebhookMappingDto {
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 #[schema(value_type = Option<source::http::MappingCondition>)]
226 pub when: Option<MappingConditionDto>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 #[schema(value_type = Option<source::http::OperationType>)]
229 pub operation: Option<OperationTypeDto>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub operation_from: Option<ConfigValue<String>>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 #[schema(value_type = Option<HashMap<String, source::http::OperationType>>)]
234 pub operation_map: Option<HashMap<String, OperationTypeDto>>,
235 #[schema(value_type = source::http::ElementType)]
236 pub element_type: ElementTypeDto,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 #[schema(value_type = Option<source::http::EffectiveFromConfig>)]
239 pub effective_from: Option<EffectiveFromConfigDto>,
240 #[schema(value_type = source::http::ElementTemplate)]
241 pub template: ElementTemplateDto,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
245#[schema(as = source::http::MappingCondition)]
246#[serde(rename_all = "camelCase")]
247pub struct MappingConditionDto {
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub header: Option<ConfigValue<String>>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub field: Option<ConfigValue<String>>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub equals: Option<ConfigValue<String>>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub contains: Option<ConfigValue<String>>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub regex: Option<ConfigValue<String>>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
261#[schema(as = source::http::OperationType)]
262#[serde(rename_all = "lowercase")]
263pub enum OperationTypeDto {
264 Insert,
265 Update,
266 Delete,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
270#[schema(as = source::http::ElementType)]
271#[serde(rename_all = "lowercase")]
272pub enum ElementTypeDto {
273 Node,
274 Relation,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
278#[schema(as = source::http::EffectiveFromConfig)]
279#[serde(untagged)]
280pub enum EffectiveFromConfigDto {
281 Simple(ConfigValue<String>),
282 Explicit {
283 value: ConfigValue<String>,
284 format: TimestampFormatDto,
285 },
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
289#[schema(as = source::http::TimestampFormat)]
290#[serde(rename_all = "snake_case")]
291pub enum TimestampFormatDto {
292 Iso8601,
293 UnixSeconds,
294 UnixMillis,
295 UnixNanos,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
299#[schema(as = source::http::ElementTemplate)]
300#[serde(rename_all = "camelCase")]
301pub struct ElementTemplateDto {
302 pub id: ConfigValue<String>,
303 pub labels: Vec<ConfigValue<String>>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub properties: Option<serde_json::Value>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub from: Option<ConfigValue<String>>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub to: Option<ConfigValue<String>>,
310}
311
312fn map_error_behavior(dto: &ErrorBehaviorDto) -> ErrorBehavior {
315 match dto {
316 ErrorBehaviorDto::AcceptAndLog => ErrorBehavior::AcceptAndLog,
317 ErrorBehaviorDto::AcceptAndSkip => ErrorBehavior::AcceptAndSkip,
318 ErrorBehaviorDto::Reject => ErrorBehavior::Reject,
319 }
320}
321
322fn map_http_method(dto: &HttpMethodDto) -> HttpMethod {
323 match dto {
324 HttpMethodDto::Get => HttpMethod::Get,
325 HttpMethodDto::Post => HttpMethod::Post,
326 HttpMethodDto::Put => HttpMethod::Put,
327 HttpMethodDto::Patch => HttpMethod::Patch,
328 HttpMethodDto::Delete => HttpMethod::Delete,
329 }
330}
331
332fn map_signature_algorithm(dto: &SignatureAlgorithmDto) -> SignatureAlgorithm {
333 match dto {
334 SignatureAlgorithmDto::HmacSha1 => SignatureAlgorithm::HmacSha1,
335 SignatureAlgorithmDto::HmacSha256 => SignatureAlgorithm::HmacSha256,
336 }
337}
338
339fn map_signature_encoding(dto: &SignatureEncodingDto) -> SignatureEncoding {
340 match dto {
341 SignatureEncodingDto::Hex => SignatureEncoding::Hex,
342 SignatureEncodingDto::Base64 => SignatureEncoding::Base64,
343 }
344}
345
346fn map_operation_type(dto: &OperationTypeDto) -> OperationType {
347 match dto {
348 OperationTypeDto::Insert => OperationType::Insert,
349 OperationTypeDto::Update => OperationType::Update,
350 OperationTypeDto::Delete => OperationType::Delete,
351 }
352}
353
354fn map_element_type(dto: &ElementTypeDto) -> ElementType {
355 match dto {
356 ElementTypeDto::Node => ElementType::Node,
357 ElementTypeDto::Relation => ElementType::Relation,
358 }
359}
360
361fn map_timestamp_format(dto: &TimestampFormatDto) -> TimestampFormat {
362 match dto {
363 TimestampFormatDto::Iso8601 => TimestampFormat::Iso8601,
364 TimestampFormatDto::UnixSeconds => TimestampFormat::UnixSeconds,
365 TimestampFormatDto::UnixMillis => TimestampFormat::UnixMillis,
366 TimestampFormatDto::UnixNanos => TimestampFormat::UnixNanos,
367 }
368}
369
370async fn map_webhook_config(
371 dto: &WebhookConfigDto,
372 resolver: &DtoMapper,
373) -> Result<WebhookConfig, MappingError> {
374 let cors = if let Some(cors) = dto.cors.as_ref() {
375 Some(map_cors_config(cors, resolver).await?)
376 } else {
377 None
378 };
379
380 let mut routes = Vec::with_capacity(dto.routes.len());
381 for route in &dto.routes {
382 routes.push(map_webhook_route(route, resolver).await?);
383 }
384
385 Ok(WebhookConfig {
386 error_behavior: map_error_behavior(&dto.error_behavior),
387 cors,
388 routes,
389 })
390}
391
392async fn map_cors_config(
393 dto: &CorsConfigDto,
394 resolver: &DtoMapper,
395) -> Result<CorsConfig, MappingError> {
396 Ok(CorsConfig {
397 enabled: dto.enabled,
398 allow_origins: resolver.resolve_string_vec(&dto.allow_origins).await?,
399 allow_methods: resolver.resolve_string_vec(&dto.allow_methods).await?,
400 allow_headers: resolver.resolve_string_vec(&dto.allow_headers).await?,
401 expose_headers: resolver.resolve_string_vec(&dto.expose_headers).await?,
402 allow_credentials: dto.allow_credentials,
403 max_age: dto.max_age,
404 })
405}
406
407async fn map_webhook_route(
408 dto: &WebhookRouteDto,
409 resolver: &DtoMapper,
410) -> Result<WebhookRoute, MappingError> {
411 let auth = if let Some(auth) = dto.auth.as_ref() {
412 Some(map_auth_config(auth, resolver).await?)
413 } else {
414 None
415 };
416
417 let mut mappings = Vec::with_capacity(dto.mappings.len());
418 for mapping in &dto.mappings {
419 mappings.push(map_webhook_mapping(mapping, resolver).await?);
420 }
421
422 Ok(WebhookRoute {
423 path: resolver.resolve_string(&dto.path).await?,
424 methods: dto.methods.iter().map(map_http_method).collect(),
425 auth,
426 error_behavior: dto.error_behavior.as_ref().map(map_error_behavior),
427 mappings,
428 })
429}
430
431async fn map_auth_config(
432 dto: &AuthConfigDto,
433 resolver: &DtoMapper,
434) -> Result<AuthConfig, MappingError> {
435 let signature = if let Some(signature) = dto.signature.as_ref() {
436 Some(map_signature_config(signature, resolver).await?)
437 } else {
438 None
439 };
440
441 let bearer = if let Some(bearer) = dto.bearer.as_ref() {
442 Some(map_bearer_config(bearer, resolver).await?)
443 } else {
444 None
445 };
446
447 Ok(AuthConfig { signature, bearer })
448}
449
450async fn map_signature_config(
451 dto: &SignatureConfigDto,
452 resolver: &DtoMapper,
453) -> Result<SignatureConfig, MappingError> {
454 Ok(SignatureConfig {
455 algorithm: map_signature_algorithm(&dto.algorithm),
456 secret_env: resolver.resolve_string(&dto.secret_env).await?,
457 header: resolver.resolve_string(&dto.header).await?,
458 prefix: resolver.resolve_optional_string(&dto.prefix).await?,
459 encoding: map_signature_encoding(&dto.encoding),
460 })
461}
462
463async fn map_bearer_config(
464 dto: &BearerConfigDto,
465 resolver: &DtoMapper,
466) -> Result<BearerConfig, MappingError> {
467 Ok(BearerConfig {
468 token_env: resolver.resolve_string(&dto.token_env).await?,
469 })
470}
471
472async fn map_webhook_mapping(
473 dto: &WebhookMappingDto,
474 resolver: &DtoMapper,
475) -> Result<WebhookMapping, MappingError> {
476 let when = if let Some(condition) = dto.when.as_ref() {
477 Some(map_mapping_condition(condition, resolver).await?)
478 } else {
479 None
480 };
481
482 let effective_from = if let Some(effective_from) = dto.effective_from.as_ref() {
483 Some(map_effective_from(effective_from, resolver).await?)
484 } else {
485 None
486 };
487
488 Ok(WebhookMapping {
489 when,
490 operation: dto.operation.as_ref().map(map_operation_type),
491 operation_from: resolver
492 .resolve_optional_string(&dto.operation_from)
493 .await?,
494 operation_map: dto.operation_map.as_ref().map(|m| {
495 m.iter()
496 .map(|(k, v)| (k.clone(), map_operation_type(v)))
497 .collect()
498 }),
499 element_type: map_element_type(&dto.element_type),
500 effective_from,
501 template: map_element_template(&dto.template, resolver).await?,
502 })
503}
504
505async fn map_mapping_condition(
506 dto: &MappingConditionDto,
507 resolver: &DtoMapper,
508) -> Result<MappingCondition, MappingError> {
509 Ok(MappingCondition {
510 header: resolver.resolve_optional_string(&dto.header).await?,
511 field: resolver.resolve_optional_string(&dto.field).await?,
512 equals: resolver.resolve_optional_string(&dto.equals).await?,
513 contains: resolver.resolve_optional_string(&dto.contains).await?,
514 regex: resolver.resolve_optional_string(&dto.regex).await?,
515 })
516}
517
518async fn map_effective_from(
519 dto: &EffectiveFromConfigDto,
520 resolver: &DtoMapper,
521) -> Result<EffectiveFromConfig, MappingError> {
522 match dto {
523 EffectiveFromConfigDto::Simple(v) => Ok(EffectiveFromConfig::Simple(
524 resolver.resolve_string(v).await?,
525 )),
526 EffectiveFromConfigDto::Explicit { value, format } => Ok(EffectiveFromConfig::Explicit {
527 value: resolver.resolve_string(value).await?,
528 format: map_timestamp_format(format),
529 }),
530 }
531}
532
533async fn map_element_template(
534 dto: &ElementTemplateDto,
535 resolver: &DtoMapper,
536) -> Result<ElementTemplate, MappingError> {
537 Ok(ElementTemplate {
538 id: resolver.resolve_string(&dto.id).await?,
539 labels: resolver.resolve_string_vec(&dto.labels).await?,
540 properties: dto.properties.clone(),
541 from: resolver.resolve_optional_string(&dto.from).await?,
542 to: resolver.resolve_optional_string(&dto.to).await?,
543 })
544}
545
546impl From<&ErrorBehavior> for ErrorBehaviorDto {
549 fn from(eb: &ErrorBehavior) -> Self {
550 match eb {
551 ErrorBehavior::AcceptAndLog => ErrorBehaviorDto::AcceptAndLog,
552 ErrorBehavior::AcceptAndSkip => ErrorBehaviorDto::AcceptAndSkip,
553 ErrorBehavior::Reject => ErrorBehaviorDto::Reject,
554 }
555 }
556}
557
558impl From<&HttpMethod> for HttpMethodDto {
559 fn from(m: &HttpMethod) -> Self {
560 match m {
561 HttpMethod::Get => HttpMethodDto::Get,
562 HttpMethod::Post => HttpMethodDto::Post,
563 HttpMethod::Put => HttpMethodDto::Put,
564 HttpMethod::Patch => HttpMethodDto::Patch,
565 HttpMethod::Delete => HttpMethodDto::Delete,
566 }
567 }
568}
569
570impl From<&SignatureAlgorithm> for SignatureAlgorithmDto {
571 fn from(a: &SignatureAlgorithm) -> Self {
572 match a {
573 SignatureAlgorithm::HmacSha1 => SignatureAlgorithmDto::HmacSha1,
574 SignatureAlgorithm::HmacSha256 => SignatureAlgorithmDto::HmacSha256,
575 }
576 }
577}
578
579impl From<&SignatureEncoding> for SignatureEncodingDto {
580 fn from(e: &SignatureEncoding) -> Self {
581 match e {
582 SignatureEncoding::Hex => SignatureEncodingDto::Hex,
583 SignatureEncoding::Base64 => SignatureEncodingDto::Base64,
584 }
585 }
586}
587
588impl From<&OperationType> for OperationTypeDto {
589 fn from(op: &OperationType) -> Self {
590 match op {
591 OperationType::Insert => OperationTypeDto::Insert,
592 OperationType::Update => OperationTypeDto::Update,
593 OperationType::Delete => OperationTypeDto::Delete,
594 }
595 }
596}
597
598impl From<&ElementType> for ElementTypeDto {
599 fn from(et: &ElementType) -> Self {
600 match et {
601 ElementType::Node => ElementTypeDto::Node,
602 ElementType::Relation => ElementTypeDto::Relation,
603 }
604 }
605}
606
607impl From<&TimestampFormat> for TimestampFormatDto {
608 fn from(f: &TimestampFormat) -> Self {
609 match f {
610 TimestampFormat::Iso8601 => TimestampFormatDto::Iso8601,
611 TimestampFormat::UnixSeconds => TimestampFormatDto::UnixSeconds,
612 TimestampFormat::UnixMillis => TimestampFormatDto::UnixMillis,
613 TimestampFormat::UnixNanos => TimestampFormatDto::UnixNanos,
614 }
615 }
616}
617
618impl From<&MappingCondition> for MappingConditionDto {
619 fn from(c: &MappingCondition) -> Self {
620 Self {
621 header: c.header.as_ref().map(|v| ConfigValue::Static(v.clone())),
622 field: c.field.as_ref().map(|v| ConfigValue::Static(v.clone())),
623 equals: c.equals.as_ref().map(|v| ConfigValue::Static(v.clone())),
624 contains: c.contains.as_ref().map(|v| ConfigValue::Static(v.clone())),
625 regex: c.regex.as_ref().map(|v| ConfigValue::Static(v.clone())),
626 }
627 }
628}
629
630impl From<&EffectiveFromConfig> for EffectiveFromConfigDto {
631 fn from(e: &EffectiveFromConfig) -> Self {
632 match e {
633 EffectiveFromConfig::Simple(v) => {
634 EffectiveFromConfigDto::Simple(ConfigValue::Static(v.clone()))
635 }
636 EffectiveFromConfig::Explicit { value, format } => EffectiveFromConfigDto::Explicit {
637 value: ConfigValue::Static(value.clone()),
638 format: TimestampFormatDto::from(format),
639 },
640 }
641 }
642}
643
644impl From<&ElementTemplate> for ElementTemplateDto {
645 fn from(t: &ElementTemplate) -> Self {
646 Self {
647 id: ConfigValue::Static(t.id.clone()),
648 labels: t
649 .labels
650 .iter()
651 .map(|l| ConfigValue::Static(l.clone()))
652 .collect(),
653 properties: t.properties.clone(),
654 from: t.from.as_ref().map(|v| ConfigValue::Static(v.clone())),
655 to: t.to.as_ref().map(|v| ConfigValue::Static(v.clone())),
656 }
657 }
658}
659
660impl From<&WebhookMapping> for WebhookMappingDto {
661 fn from(m: &WebhookMapping) -> Self {
662 Self {
663 when: m.when.as_ref().map(MappingConditionDto::from),
664 operation: m.operation.as_ref().map(OperationTypeDto::from),
665 operation_from: m
666 .operation_from
667 .as_ref()
668 .map(|v| ConfigValue::Static(v.clone())),
669 operation_map: m.operation_map.as_ref().map(|om| {
670 om.iter()
671 .map(|(k, v)| (k.clone(), OperationTypeDto::from(v)))
672 .collect()
673 }),
674 element_type: ElementTypeDto::from(&m.element_type),
675 effective_from: m.effective_from.as_ref().map(EffectiveFromConfigDto::from),
676 template: ElementTemplateDto::from(&m.template),
677 }
678 }
679}
680
681impl From<&SignatureConfig> for SignatureConfigDto {
682 fn from(s: &SignatureConfig) -> Self {
683 Self {
684 algorithm: SignatureAlgorithmDto::from(&s.algorithm),
685 secret_env: ConfigValue::Static(s.secret_env.clone()),
686 header: ConfigValue::Static(s.header.clone()),
687 prefix: s.prefix.as_ref().map(|v| ConfigValue::Static(v.clone())),
688 encoding: SignatureEncodingDto::from(&s.encoding),
689 }
690 }
691}
692
693impl From<&BearerConfig> for BearerConfigDto {
694 fn from(b: &BearerConfig) -> Self {
695 Self {
696 token_env: ConfigValue::Static(b.token_env.clone()),
697 }
698 }
699}
700
701impl From<&AuthConfig> for AuthConfigDto {
702 fn from(a: &AuthConfig) -> Self {
703 Self {
704 signature: a.signature.as_ref().map(SignatureConfigDto::from),
705 bearer: a.bearer.as_ref().map(BearerConfigDto::from),
706 }
707 }
708}
709
710impl From<&WebhookRoute> for WebhookRouteDto {
711 fn from(r: &WebhookRoute) -> Self {
712 Self {
713 path: ConfigValue::Static(r.path.clone()),
714 methods: r.methods.iter().map(HttpMethodDto::from).collect(),
715 auth: r.auth.as_ref().map(AuthConfigDto::from),
716 error_behavior: r.error_behavior.as_ref().map(ErrorBehaviorDto::from),
717 mappings: r.mappings.iter().map(WebhookMappingDto::from).collect(),
718 }
719 }
720}
721
722impl From<&CorsConfig> for CorsConfigDto {
723 fn from(c: &CorsConfig) -> Self {
724 Self {
725 enabled: c.enabled,
726 allow_origins: c
727 .allow_origins
728 .iter()
729 .map(|v| ConfigValue::Static(v.clone()))
730 .collect(),
731 allow_methods: c
732 .allow_methods
733 .iter()
734 .map(|v| ConfigValue::Static(v.clone()))
735 .collect(),
736 allow_headers: c
737 .allow_headers
738 .iter()
739 .map(|v| ConfigValue::Static(v.clone()))
740 .collect(),
741 expose_headers: c
742 .expose_headers
743 .iter()
744 .map(|v| ConfigValue::Static(v.clone()))
745 .collect(),
746 allow_credentials: c.allow_credentials,
747 max_age: c.max_age,
748 }
749 }
750}
751
752impl From<&WebhookConfig> for WebhookConfigDto {
753 fn from(w: &WebhookConfig) -> Self {
754 Self {
755 error_behavior: ErrorBehaviorDto::from(&w.error_behavior),
756 cors: w.cors.as_ref().map(CorsConfigDto::from),
757 routes: w.routes.iter().map(WebhookRouteDto::from).collect(),
758 }
759 }
760}
761
762impl From<&HttpSourceConfig> for HttpSourceConfigDto {
763 fn from(config: &HttpSourceConfig) -> Self {
764 Self {
765 host: ConfigValue::Static(config.host.clone()),
766 port: ConfigValue::Static(config.port),
767 endpoint: config
768 .endpoint
769 .as_ref()
770 .map(|v| ConfigValue::Static(v.clone())),
771 timeout_ms: ConfigValue::Static(config.timeout_ms),
772 adaptive_max_batch_size: config.adaptive_max_batch_size.map(ConfigValue::Static),
773 adaptive_min_batch_size: config.adaptive_min_batch_size.map(ConfigValue::Static),
774 adaptive_max_wait_ms: config.adaptive_max_wait_ms.map(ConfigValue::Static),
775 adaptive_min_wait_ms: config.adaptive_min_wait_ms.map(ConfigValue::Static),
776 adaptive_window_secs: config.adaptive_window_secs.map(ConfigValue::Static),
777 adaptive_enabled: config.adaptive_enabled.map(ConfigValue::Static),
778 webhooks: config.webhooks.as_ref().map(WebhookConfigDto::from),
779 }
780 }
781}
782
783#[derive(OpenApi)]
784#[openapi(components(schemas(
785 HttpSourceConfigDto,
786 WebhookConfigDto,
787 CorsConfigDto,
788 ErrorBehaviorDto,
789 WebhookRouteDto,
790 HttpMethodDto,
791 AuthConfigDto,
792 SignatureConfigDto,
793 SignatureAlgorithmDto,
794 SignatureEncodingDto,
795 BearerConfigDto,
796 WebhookMappingDto,
797 MappingConditionDto,
798 OperationTypeDto,
799 ElementTypeDto,
800 EffectiveFromConfigDto,
801 TimestampFormatDto,
802 ElementTemplateDto,
803)))]
804struct HttpSourceSchemas;
805
806pub struct HttpSourceDescriptor;
808
809#[async_trait]
810impl SourcePluginDescriptor for HttpSourceDescriptor {
811 fn kind(&self) -> &str {
812 "http"
813 }
814
815 fn config_version(&self) -> &str {
816 "1.0.0"
817 }
818
819 fn config_schema_name(&self) -> &str {
820 "source.http.HttpSourceConfig"
821 }
822
823 fn config_schema_json(&self) -> String {
824 use drasi_plugin_sdk::schema_ui::SchemaUiAnnotator;
825 let api = HttpSourceSchemas::openapi();
826 let schemas = serde_json::to_value(
827 &api.components
828 .as_ref()
829 .expect("OpenAPI components missing")
830 .schemas,
831 )
832 .expect("Failed to serialize config schema");
833
834 SchemaUiAnnotator::new(schemas, "source.http.HttpSourceConfig")
835 .expect("root schema not found")
836 .field("host", |f| {
837 f.group("Connection").order(1).placeholder("0.0.0.0")
838 })
839 .field("port", |f| {
840 f.group("Connection").order(2).placeholder("8081")
841 })
842 .field("endpoint", |f| {
843 f.group("Connection").order(3).placeholder("/api/data")
844 })
845 .field("timeoutMs", |f| {
846 f.group("Connection").order(4).placeholder("10000")
847 })
848 .field("webhooks", |f| f.group("Webhooks").order(1))
849 .field("adaptiveEnabled", |f| {
850 f.group("Adaptive Batching").order(1).collapsed(true)
851 })
852 .field("adaptiveMaxBatchSize", |f| {
853 f.group("Adaptive Batching").order(2)
854 })
855 .field("adaptiveMinBatchSize", |f| {
856 f.group("Adaptive Batching").order(3)
857 })
858 .field("adaptiveMaxWaitMs", |f| {
859 f.group("Adaptive Batching").order(4)
860 })
861 .field("adaptiveMinWaitMs", |f| {
862 f.group("Adaptive Batching").order(5)
863 })
864 .field("adaptiveWindowSecs", |f| {
865 f.group("Adaptive Batching").order(6)
866 })
867 .annotate()
868 }
869
870 async fn create_source(
871 &self,
872 id: &str,
873 config_json: &serde_json::Value,
874 auto_start: bool,
875 ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
876 let dto: HttpSourceConfigDto = serde_json::from_value(config_json.clone())?;
877 let mapper = DtoMapper::new();
878
879 let config = HttpSourceConfig {
880 host: mapper.resolve_string(&dto.host).await?,
881 port: mapper.resolve_typed(&dto.port).await?,
882 endpoint: mapper.resolve_optional(&dto.endpoint).await?,
883 timeout_ms: mapper.resolve_typed(&dto.timeout_ms).await?,
884 adaptive_max_batch_size: mapper
885 .resolve_optional(&dto.adaptive_max_batch_size)
886 .await?,
887 adaptive_min_batch_size: mapper
888 .resolve_optional(&dto.adaptive_min_batch_size)
889 .await?,
890 adaptive_max_wait_ms: mapper.resolve_optional(&dto.adaptive_max_wait_ms).await?,
891 adaptive_min_wait_ms: mapper.resolve_optional(&dto.adaptive_min_wait_ms).await?,
892 adaptive_window_secs: mapper.resolve_optional(&dto.adaptive_window_secs).await?,
893 adaptive_enabled: mapper.resolve_optional(&dto.adaptive_enabled).await?,
894 webhooks: if let Some(webhooks) = dto.webhooks.as_ref() {
895 Some(map_webhook_config(webhooks, &mapper).await?)
896 } else {
897 None
898 },
899 };
900
901 let mut source = HttpSourceBuilder::new(id)
902 .with_config(config)
903 .with_auto_start(auto_start)
904 .build()?;
905
906 source.base.set_raw_config(config_json.clone());
907
908 Ok(Box::new(source))
909 }
910}