feat: add soap core domain model
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod observability;
|
||||
pub mod operation;
|
||||
pub mod protocol;
|
||||
pub mod secret;
|
||||
pub mod soap;
|
||||
pub mod stream_session;
|
||||
pub mod streaming;
|
||||
pub mod workspace;
|
||||
@@ -30,11 +31,14 @@ pub use observability::{
|
||||
pub use operation::{
|
||||
ConfigExport, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlTarget,
|
||||
GrpcProtocolOptions, GrpcTarget, Operation, OperationStatus, ProtocolOptions, RestTarget,
|
||||
RetryPolicy, Samples, Target, ToolDescription, ToolExample, WebsocketProtocolOptions,
|
||||
WebsocketTarget,
|
||||
RetryPolicy, Samples, SoapProtocolOptions, SoapTarget, Target, ToolDescription, ToolExample,
|
||||
WebsocketProtocolOptions, WebsocketTarget,
|
||||
};
|
||||
pub use protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol};
|
||||
pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
||||
pub use soap::{
|
||||
SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata, SoapVersion,
|
||||
};
|
||||
pub use stream_session::{AsyncJobHandle, JobStatus, StreamSession, StreamStatus};
|
||||
pub use streaming::{
|
||||
AggregationMode, ExecutionMode, StreamingConfig, StreamingConfigError, ToolFamilyConfig,
|
||||
|
||||
@@ -6,6 +6,9 @@ use serde_json::Value;
|
||||
use crate::{
|
||||
ids::{AuthProfileId, DescriptorId, OperationId, SampleId},
|
||||
protocol::{ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
soap::{
|
||||
SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata, SoapVersion,
|
||||
},
|
||||
streaming::StreamingConfig,
|
||||
};
|
||||
|
||||
@@ -63,6 +66,26 @@ pub struct WebsocketTarget {
|
||||
pub static_headers: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SoapTarget {
|
||||
pub wsdl_ref: SampleId,
|
||||
pub service_name: String,
|
||||
pub port_name: String,
|
||||
pub operation_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint_override: Option<String>,
|
||||
pub soap_version: SoapVersion,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub soap_action: Option<String>,
|
||||
pub binding_style: SoapBindingStyle,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub headers: Vec<SoapHeaderConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fault_contract: Option<SoapFaultContract>,
|
||||
#[serde(default)]
|
||||
pub metadata: SoapOperationMetadata,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Target {
|
||||
@@ -70,6 +93,7 @@ pub enum Target {
|
||||
Graphql(GraphqlTarget),
|
||||
Grpc(GrpcTarget),
|
||||
Websocket(WebsocketTarget),
|
||||
Soap(SoapTarget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
@@ -92,12 +116,24 @@ pub struct WebsocketProtocolOptions {
|
||||
pub reconnect_backoff_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct SoapProtocolOptions {
|
||||
#[serde(default)]
|
||||
pub use_tls: bool,
|
||||
#[serde(default)]
|
||||
pub validate_certificate: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ws_security_profile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct ProtocolOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grpc: Option<GrpcProtocolOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub websocket: Option<WebsocketProtocolOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub soap: Option<SoapProtocolOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -231,13 +267,17 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
auth::{AuthConfig, AuthProfile, BearerAuthConfig, SecretRef},
|
||||
ids::{AuthProfileId, OperationId},
|
||||
ids::{AuthProfileId, OperationId, SampleId},
|
||||
operation::{
|
||||
ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus,
|
||||
ProtocolOptions, RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
WebsocketProtocolOptions, WebsocketTarget,
|
||||
ProtocolOptions, RestTarget, Samples, SoapProtocolOptions, SoapTarget, Target,
|
||||
ToolDescription, ToolExample, WebsocketProtocolOptions, WebsocketTarget,
|
||||
},
|
||||
protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
soap::{
|
||||
SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata,
|
||||
SoapVersion,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -288,6 +328,43 @@ mod tests {
|
||||
assert_eq!(value["subscribe_message_template"]["type"], "subscribe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soap_target_serializes_binding_metadata() {
|
||||
let target = Target::Soap(SoapTarget {
|
||||
wsdl_ref: SampleId::new("sample_wsdl_01"),
|
||||
service_name: "LeadService".to_owned(),
|
||||
port_name: "LeadPort".to_owned(),
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
endpoint_override: Some("https://soap.example.com/lead".to_owned()),
|
||||
soap_version: SoapVersion::Soap12,
|
||||
soap_action: Some("urn:createLead".to_owned()),
|
||||
binding_style: SoapBindingStyle::DocumentLiteral,
|
||||
headers: vec![SoapHeaderConfig {
|
||||
name: "CorrelationId".to_owned(),
|
||||
namespace_uri: Some("urn:crm".to_owned()),
|
||||
required: true,
|
||||
value_path: Some("$.mcp.correlation_id".to_owned()),
|
||||
}],
|
||||
fault_contract: Some(SoapFaultContract {
|
||||
code_path: Some("$.Envelope.Body.Fault.Code.Value".to_owned()),
|
||||
message_path: Some("$.Envelope.Body.Fault.Reason.Text".to_owned()),
|
||||
detail_path: Some("$.Envelope.Body.Fault.Detail".to_owned()),
|
||||
}),
|
||||
metadata: SoapOperationMetadata {
|
||||
input_part_names: vec!["LeadRequest".to_owned()],
|
||||
output_part_names: vec!["LeadResponse".to_owned()],
|
||||
namespaces: vec!["urn:crm".to_owned()],
|
||||
},
|
||||
});
|
||||
|
||||
let value = serde_json::to_value(target).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "soap");
|
||||
assert_eq!(value["soap_version"], "soap_12");
|
||||
assert_eq!(value["binding_style"], "document_literal");
|
||||
assert_eq!(value["metadata"]["input_part_names"][0], "LeadRequest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_exposes_local_domain_helpers() {
|
||||
let operation = Operation {
|
||||
@@ -320,6 +397,11 @@ mod tests {
|
||||
reconnect_max_attempts: Some(3),
|
||||
reconnect_backoff_ms: Some(250),
|
||||
}),
|
||||
soap: Some(SoapProtocolOptions {
|
||||
use_tls: true,
|
||||
validate_certificate: true,
|
||||
ws_security_profile: Some("username_token".to_owned()),
|
||||
}),
|
||||
}),
|
||||
streaming: None,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ pub enum Protocol {
|
||||
Graphql,
|
||||
Grpc,
|
||||
Websocket,
|
||||
Soap,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -51,6 +52,7 @@ impl Protocol {
|
||||
Self::Graphql => matches!(mode, ExecutionMode::Unary),
|
||||
Self::Grpc => true,
|
||||
Self::Websocket => !matches!(mode, ExecutionMode::Unary),
|
||||
Self::Soap => matches!(mode, ExecutionMode::Unary | ExecutionMode::AsyncJob),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ impl Protocol {
|
||||
Self::Graphql => matches!(behavior, TransportBehavior::RequestResponse),
|
||||
Self::Grpc => !matches!(behavior, TransportBehavior::DeferredResult),
|
||||
Self::Websocket => !matches!(behavior, TransportBehavior::RequestResponse),
|
||||
Self::Soap => !matches!(behavior, TransportBehavior::StatefulSession),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,4 +101,16 @@ mod tests {
|
||||
);
|
||||
assert!(Protocol::Websocket.supports_transport_behavior(TransportBehavior::DeferredResult));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soap_is_request_response_first_with_async_job_support() {
|
||||
assert!(Protocol::Soap.supports_execution_mode(ExecutionMode::Unary));
|
||||
assert!(!Protocol::Soap.supports_execution_mode(ExecutionMode::Window));
|
||||
assert!(!Protocol::Soap.supports_execution_mode(ExecutionMode::Session));
|
||||
assert!(Protocol::Soap.supports_execution_mode(ExecutionMode::AsyncJob));
|
||||
assert!(Protocol::Soap.supports_transport_behavior(TransportBehavior::RequestResponse));
|
||||
assert!(Protocol::Soap.supports_transport_behavior(TransportBehavior::ServerStream));
|
||||
assert!(!Protocol::Soap.supports_transport_behavior(TransportBehavior::StatefulSession));
|
||||
assert!(Protocol::Soap.supports_transport_behavior(TransportBehavior::DeferredResult));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SoapVersion {
|
||||
#[serde(rename = "soap_11")]
|
||||
Soap11,
|
||||
#[serde(rename = "soap_12")]
|
||||
Soap12,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoapBindingStyle {
|
||||
DocumentLiteral,
|
||||
RpcLiteral,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SoapHeaderConfig {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub namespace_uri: Option<String>,
|
||||
pub required: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SoapFaultContract {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct SoapOperationMetadata {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub input_part_names: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub output_part_names: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub namespaces: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
SoapBindingStyle, SoapFaultContract, SoapHeaderConfig, SoapOperationMetadata, SoapVersion,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn soap_metadata_serializes_compactly() {
|
||||
let value = serde_json::to_value(SoapOperationMetadata {
|
||||
input_part_names: vec!["LeadRequest".to_owned()],
|
||||
output_part_names: vec!["LeadResponse".to_owned()],
|
||||
namespaces: vec!["urn:crm".to_owned()],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(value["input_part_names"][0], "LeadRequest");
|
||||
assert_eq!(value["output_part_names"][0], "LeadResponse");
|
||||
assert_eq!(value["namespaces"][0], "urn:crm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soap_supporting_types_roundtrip() {
|
||||
let value = json!({
|
||||
"version": "soap_12",
|
||||
"style": "document_literal",
|
||||
"header": {
|
||||
"name": "CorrelationId",
|
||||
"namespace_uri": "urn:crm",
|
||||
"required": true,
|
||||
"value_path": "$.mcp.correlation_id"
|
||||
},
|
||||
"fault": {
|
||||
"code_path": "$.Envelope.Body.Fault.Code.Value",
|
||||
"message_path": "$.Envelope.Body.Fault.Reason.Text",
|
||||
"detail_path": "$.Envelope.Body.Fault.Detail"
|
||||
}
|
||||
});
|
||||
|
||||
let version: SoapVersion = serde_json::from_value(value["version"].clone()).unwrap();
|
||||
let style: SoapBindingStyle = serde_json::from_value(value["style"].clone()).unwrap();
|
||||
let header: SoapHeaderConfig = serde_json::from_value(value["header"].clone()).unwrap();
|
||||
let fault: SoapFaultContract = serde_json::from_value(value["fault"].clone()).unwrap();
|
||||
|
||||
assert_eq!(version, SoapVersion::Soap12);
|
||||
assert_eq!(style, SoapBindingStyle::DocumentLiteral);
|
||||
assert_eq!(header.name, "CorrelationId");
|
||||
assert_eq!(
|
||||
fault.detail_path.as_deref(),
|
||||
Some("$.Envelope.Body.Fault.Detail")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3959,6 +3959,12 @@ fn target_summary(target: &Target) -> (String, String) {
|
||||
),
|
||||
Target::Grpc(grpc) => (grpc.server_addr.clone(), grpc.method.clone()),
|
||||
Target::Websocket(websocket) => (websocket.url.clone(), "SUBSCRIBE".to_owned()),
|
||||
Target::Soap(soap) => (
|
||||
soap.endpoint_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("wsdl://{}", soap.service_name)),
|
||||
soap.operation_name.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,9 @@ impl RuntimeExecutor {
|
||||
data: Value::Null,
|
||||
})
|
||||
}
|
||||
Target::Soap(_) => Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: crank_core::Protocol::Soap,
|
||||
}),
|
||||
Target::Websocket(_) => Err(RuntimeError::UnsupportedExecutionMode {
|
||||
operation_id: operation.operation_id.as_str().to_owned(),
|
||||
mode: ExecutionMode::Unary,
|
||||
@@ -323,6 +326,9 @@ impl RuntimeExecutor {
|
||||
data: Value::Null,
|
||||
})
|
||||
}
|
||||
Target::Soap(_) => Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: crank_core::Protocol::Soap,
|
||||
}),
|
||||
_ => self.execute_adapter(operation, prepared_request).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ mod normalize;
|
||||
mod validate;
|
||||
|
||||
pub use error::SchemaError;
|
||||
pub use model::{Schema, SchemaKind};
|
||||
pub use model::{Schema, SchemaKind, XmlNodeKind, XmlQualifiedName, XmlSchemaBinding};
|
||||
|
||||
@@ -3,6 +3,33 @@ use std::collections::BTreeMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum XmlNodeKind {
|
||||
Element,
|
||||
Attribute,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct XmlQualifiedName {
|
||||
pub local_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub namespace_uri: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct XmlSchemaBinding {
|
||||
pub node_kind: XmlNodeKind,
|
||||
pub name: XmlQualifiedName,
|
||||
#[serde(default)]
|
||||
pub repeated: bool,
|
||||
#[serde(default)]
|
||||
pub nillable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SchemaKind {
|
||||
@@ -72,7 +99,7 @@ impl Schema {
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{Schema, SchemaKind};
|
||||
use super::{Schema, SchemaKind, XmlNodeKind, XmlQualifiedName, XmlSchemaBinding};
|
||||
|
||||
#[test]
|
||||
fn object_schema_exposes_fields() {
|
||||
@@ -175,4 +202,25 @@ mod tests {
|
||||
assert!(yaml.contains("type: object"));
|
||||
assert_eq!(restored, schema);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xml_schema_binding_roundtrips() {
|
||||
let binding = XmlSchemaBinding {
|
||||
node_kind: XmlNodeKind::Element,
|
||||
name: XmlQualifiedName {
|
||||
local_name: "CreateLeadRequest".to_owned(),
|
||||
namespace_uri: Some("urn:crm".to_owned()),
|
||||
prefix: Some("crm".to_owned()),
|
||||
},
|
||||
repeated: false,
|
||||
nillable: true,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&binding).unwrap();
|
||||
let restored: XmlSchemaBinding = serde_json::from_value(value.clone()).unwrap();
|
||||
|
||||
assert_eq!(value["node_kind"], "element");
|
||||
assert_eq!(value["name"]["local_name"], "CreateLeadRequest");
|
||||
assert_eq!(restored, binding);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user