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}
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
312// --- Mapping functions ---
313
314fn 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
546// --- Reverse mapping: internal config → DTO ---
547
548impl 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
806/// Descriptor for the HTTP source plugin.
807pub 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}