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