feat: add soap adapter foundation
This commit is contained in:
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
|
||||
use crank_adapter_graphql::{GraphqlAdapter, GraphqlRequest};
|
||||
use crank_adapter_grpc::{GrpcAdapter, GrpcRequest, GrpcWindowRequest};
|
||||
use crank_adapter_rest::{RestAdapter, RestRequest, RestWindowRequest};
|
||||
use crank_adapter_soap::{SoapAdapter, SoapRequest};
|
||||
use crank_adapter_websocket::{WebsocketAdapter, WebsocketWindowRequest};
|
||||
use crank_core::{ExecutionMode, Target, TransportBehavior};
|
||||
use serde_json::{Map, Value, json};
|
||||
@@ -16,6 +17,7 @@ pub struct RuntimeExecutor {
|
||||
graphql_adapter: GraphqlAdapter,
|
||||
grpc_adapter: GrpcAdapter,
|
||||
rest_adapter: RestAdapter,
|
||||
soap_adapter: SoapAdapter,
|
||||
websocket_adapter: WebsocketAdapter,
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ impl RuntimeExecutor {
|
||||
graphql_adapter: GraphqlAdapter::new(),
|
||||
grpc_adapter: GrpcAdapter::new(),
|
||||
rest_adapter: RestAdapter::new(),
|
||||
soap_adapter: SoapAdapter::new(),
|
||||
websocket_adapter: WebsocketAdapter::new(),
|
||||
}
|
||||
}
|
||||
@@ -205,9 +208,28 @@ impl RuntimeExecutor {
|
||||
data: Value::Null,
|
||||
})
|
||||
}
|
||||
Target::Soap(_) => Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: crank_core::Protocol::Soap,
|
||||
}),
|
||||
Target::Soap(target) => {
|
||||
let request = SoapRequest {
|
||||
headers: merge_headers(
|
||||
&BTreeMap::new(),
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
body: prepared_request
|
||||
.body
|
||||
.clone()
|
||||
.unwrap_or(Value::Object(Map::new())),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.soap_adapter.execute(target, &request).await?;
|
||||
|
||||
Ok(AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
data: Value::Null,
|
||||
})
|
||||
}
|
||||
Target::Websocket(_) => Err(RuntimeError::UnsupportedExecutionMode {
|
||||
operation_id: operation.operation_id.as_str().to_owned(),
|
||||
mode: ExecutionMode::Unary,
|
||||
@@ -326,8 +348,9 @@ impl RuntimeExecutor {
|
||||
data: Value::Null,
|
||||
})
|
||||
}
|
||||
Target::Soap(_) => Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: crank_core::Protocol::Soap,
|
||||
Target::Soap(_) => Err(RuntimeError::UnsupportedExecutionMode {
|
||||
operation_id: operation.operation_id.as_str().to_owned(),
|
||||
mode: ExecutionMode::Window,
|
||||
}),
|
||||
_ => self.execute_adapter(operation, prepared_request).await,
|
||||
}
|
||||
@@ -447,7 +470,8 @@ mod tests {
|
||||
AggregationMode, DescriptorId, ExecutionConfig, ExecutionMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod,
|
||||
Operation, OperationId, OperationStatus, Protocol, ProtocolOptions, RestTarget, Samples,
|
||||
StreamingConfig, Target, ToolDescription, ToolExample, ToolFamilyConfig, TransportBehavior,
|
||||
SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, StreamingConfig, Target,
|
||||
ToolDescription, ToolExample, ToolFamilyConfig, TransportBehavior,
|
||||
WebsocketProtocolOptions, WebsocketTarget,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
@@ -501,6 +525,20 @@ mod tests {
|
||||
assert_eq!(output, json!({ "message": "hello" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_soap_operation_end_to_end() {
|
||||
let endpoint = spawn_soap_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_soap_operation(&endpoint);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_grpc_window_mode_with_server_stream() {
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
@@ -729,6 +767,18 @@ mod tests {
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn spawn_soap_server() -> String {
|
||||
let app = Router::new().route("/", post(soap_handler));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn spawn_websocket_server() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
@@ -853,6 +903,23 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn soap_handler(body: String) -> (axum::http::StatusCode, String) {
|
||||
assert!(body.contains("<email>user@example.com</email>"));
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<CreateLeadResponse>
|
||||
<id>lead_123</id>
|
||||
<status>created</status>
|
||||
</CreateLeadResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"#
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_rest_operation(
|
||||
base_url: &str,
|
||||
should_fail: bool,
|
||||
@@ -1181,6 +1248,90 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn test_soap_operation(endpoint: &str) -> RuntimeOperation {
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_soap_runtime"),
|
||||
name: "crm_create_lead_soap".to_owned(),
|
||||
display_name: "Create Lead SOAP".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Soap,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Soap(SoapTarget {
|
||||
wsdl_ref: "sample_wsdl".into(),
|
||||
service_name: "LeadService".to_owned(),
|
||||
port_name: "LeadPort".to_owned(),
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
endpoint_override: Some(endpoint.to_owned()),
|
||||
soap_version: SoapVersion::Soap11,
|
||||
soap_action: Some("urn:createLead".to_owned()),
|
||||
binding_style: SoapBindingStyle::DocumentLiteral,
|
||||
headers: Vec::new(),
|
||||
fault_contract: None,
|
||||
metadata: SoapOperationMetadata {
|
||||
input_part_names: vec!["CreateLeadRequest".to_owned()],
|
||||
output_part_names: vec!["CreateLeadResponse".to_owned()],
|
||||
namespaces: vec!["urn:crm".to_owned()],
|
||||
},
|
||||
}),
|
||||
input_schema: object_schema("email", SchemaKind::String),
|
||||
output_schema: object_schema("id", SchemaKind::String),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead SOAP".to_owned(),
|
||||
description: "Creates a CRM lead through SOAP".to_owned(),
|
||||
tags: vec!["crm".to_owned(), "soap".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "email": "user@example.com" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["wsdl".to_owned()],
|
||||
generated_at: Some("2026-04-06T12:00:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: None,
|
||||
created_at: "2026-04-06T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-04-06T12:00:00Z".to_owned(),
|
||||
published_at: Some("2026-04-06T12:00:00Z".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn test_window_snapshot_operation(
|
||||
base_url: &str,
|
||||
aggregation_mode: AggregationMode,
|
||||
@@ -1340,6 +1491,7 @@ mod tests {
|
||||
reconnect_max_attempts: Some(1),
|
||||
reconnect_backoff_ms: Some(10),
|
||||
}),
|
||||
soap: None,
|
||||
}),
|
||||
streaming: Some(StreamingConfig {
|
||||
mode: ExecutionMode::Window,
|
||||
|
||||
Reference in New Issue
Block a user