diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index 440c00d..8bba890 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -3230,12 +3230,16 @@ fn build_request_preview( mapping: &MappingSet, input: &Value, ) -> Result { - let mapped = mapping.apply(&json!({ "mcp": input }))?; - let prepared = PreparedRequest::from_mapping_output(&mapped).map_err(|error| { - crank_mapping::MappingError::InvalidJsonPath { - path: error.to_string(), - } - })?; + let prepared = if mapping.is_empty() { + PreparedRequest::default() + } else { + let mapped = mapping.apply(&json!({ "mcp": input }))?; + PreparedRequest::from_mapping_output(&mapped).map_err(|error| { + crank_mapping::MappingError::InvalidJsonPath { + path: error.to_string(), + } + })? + }; Ok(json!({ "path": prepared.path_params, diff --git a/crates/crank-runtime/src/executor.rs b/crates/crank-runtime/src/executor.rs index dd19992..4400eda 100644 --- a/crates/crank-runtime/src/executor.rs +++ b/crates/crank-runtime/src/executor.rs @@ -123,6 +123,10 @@ impl RuntimeExecutor { ) -> Result { operation.input_schema.validate_shape(input)?; + if operation.input_mapping.is_empty() { + return Ok(PreparedRequest::default()); + } + let mapped_input = operation.input_mapping.apply(&json!({ "mcp": input }))?; PreparedRequest::from_mapping_output(&mapped_input) } diff --git a/crates/crank-runtime/tests/integration.rs b/crates/crank-runtime/tests/integration.rs new file mode 100644 index 0000000..9270287 --- /dev/null +++ b/crates/crank-runtime/tests/integration.rs @@ -0,0 +1,3 @@ +mod integration { + mod no_input_get; +} diff --git a/crates/crank-runtime/tests/integration/no_input_get.rs b/crates/crank-runtime/tests/integration/no_input_get.rs new file mode 100644 index 0000000..ac29679 --- /dev/null +++ b/crates/crank-runtime/tests/integration/no_input_get.rs @@ -0,0 +1,89 @@ +use std::collections::BTreeMap; + +use crank_core::{ + ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus, + Protocol, RestTarget, Target, ToolDescription, +}; +use crank_mapping::MappingSet; +use crank_runtime::RuntimeExecutor; +use crank_schema::{Schema, SchemaKind}; +use serde_json::json; +use time::OffsetDateTime; + +#[test] +fn prepares_empty_request_for_no_input_get_with_empty_mapping() { + let executor = RuntimeExecutor::new(); + let operation = no_input_get_operation(); + + let request = executor + .prepare_request(&operation.into(), &json!({})) + .unwrap(); + + assert!(request.path_params.is_empty()); + assert!(request.query_params.is_empty()); + assert!(request.headers.is_empty()); + assert!(request.body.is_none()); +} + +fn no_input_get_operation() -> Operation { + Operation { + id: OperationId::new("op_no_input_get"), + name: "internal_health".to_owned(), + display_name: "Internal Health".to_owned(), + category: "smoke".to_owned(), + protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, + status: OperationStatus::Draft, + version: 1, + target: Target::Rest(RestTarget { + base_url: "http://admin-api:3001".to_owned(), + method: HttpMethod::Get, + path_template: "/health".to_owned(), + static_headers: BTreeMap::new(), + }), + input_schema: Schema { + kind: SchemaKind::Object, + description: None, + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::new(), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + output_schema: Schema { + kind: SchemaKind::Object, + description: None, + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::new(), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + input_mapping: MappingSet { rules: Vec::new() }, + output_mapping: MappingSet { rules: Vec::new() }, + execution_config: ExecutionConfig { + timeout_ms: 1_000, + retry_policy: None, + response_cache: None, + auth_profile_ref: None, + headers: BTreeMap::new(), + }, + tool_description: ToolDescription { + title: "Internal Health".to_owned(), + description: "Checks internal health.".to_owned(), + tags: Vec::new(), + examples: Vec::new(), + }, + samples: None, + generated_draft: None, + config_export: None, + wizard_state: None, + created_at: OffsetDateTime::UNIX_EPOCH, + updated_at: OffsetDateTime::UNIX_EPOCH, + published_at: None, + } +}