Skip to main content

drasi_source_http/
descriptor.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! HTTP source plugin descriptor and configuration DTOs.
16
17use 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/// HTTP source configuration DTO.
29#[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
314// --- Mapping functions ---
315
316fn 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
548// --- Reverse mapping: internal config → DTO ---
549
550impl 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
809/// Descriptor for the HTTP source plugin.
810pub 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}