use serde_json::{Map, Number, Value}; use crate::{ JsonPath, JsonPathSegment, MappingError, MappingSet, MappingTargetContext, TransformKind, }; impl MappingSet { pub fn apply(&self, source: &Value) -> Result { self.validate_paths()?; let mut target = empty_target_context(self.target_context()); for rule in &self.rules { if let Some(condition) = &rule.condition { let condition_path = JsonPath::parse(&condition.source)?; let condition_value = read_path(source, &condition_path); if condition_value != Some(&condition.equals) { continue; } } let source_path = JsonPath::parse(&rule.source)?; let target_path = JsonPath::parse(&rule.target)?; let Some(source_value) = read_path(source, &source_path) .cloned() .or(rule.default_value.clone()) else { if rule.required { return Err(MappingError::MissingRequiredValue { path: source_path.to_string_path(), }); } continue; }; let value = apply_transform( source_value, rule.transform.as_ref().map(|value| &value.kind), )?; write_path(&mut target, &target_path, value)?; } Ok(target) } } fn empty_target_context(context: MappingTargetContext) -> Value { match context { MappingTargetContext::Input => Value::Object(Map::from_iter([( "request".to_owned(), Value::Object(Map::from_iter([ ("path".to_owned(), Value::Object(Map::new())), ("query".to_owned(), Value::Object(Map::new())), ("headers".to_owned(), Value::Object(Map::new())), ("body".to_owned(), Value::Object(Map::new())), ("variables".to_owned(), Value::Object(Map::new())), ("grpc".to_owned(), Value::Object(Map::new())), ])), )])), MappingTargetContext::Output => Value::Object(Map::from_iter([( "output".to_owned(), Value::Object(Map::new()), )])), _ => Value::Object(Map::new()), } } fn read_path<'a>(value: &'a Value, path: &JsonPath) -> Option<&'a Value> { path.read(value) } fn write_path(target: &mut Value, path: &JsonPath, value: Value) -> Result<(), MappingError> { let mut segments = path .root .root_fields() .iter() .map(|field| JsonPathSegment::Field((*field).to_owned())) .collect::>(); segments.extend(path.segments.clone()); write_segments(target, &segments, value) } fn write_segments( target: &mut Value, segments: &[JsonPathSegment], value: Value, ) -> Result<(), MappingError> { if segments.is_empty() { *target = value; return Ok(()); } match &segments[0] { JsonPathSegment::Field(field) => { if !target.is_object() { *target = Value::Object(Map::new()); } let Some(object) = target.as_object_mut() else { return Err(MappingError::InvalidJsonPath { path: field.clone(), }); }; let entry = object.entry(field.clone()).or_insert(Value::Null); write_segments(entry, &segments[1..], value) } JsonPathSegment::Index(index) => { if !target.is_array() { *target = Value::Array(Vec::new()); } let Some(array) = target.as_array_mut() else { return Err(MappingError::InvalidJsonPath { path: index.to_string(), }); }; while array.len() <= *index { array.push(Value::Null); } write_segments(&mut array[*index], &segments[1..], value) } } } fn apply_transform(value: Value, transform: Option<&TransformKind>) -> Result { let Some(transform) = transform else { return Ok(value); }; match transform { TransformKind::Identity => Ok(value), TransformKind::ToString => Ok(Value::String(to_string_value(&value))), TransformKind::ToNumber => to_number(value), TransformKind::ToBoolean => to_boolean(value), TransformKind::Join => join_values(value), TransformKind::Split => split_value(value), TransformKind::WrapArray => Ok(match value { Value::Array(values) => Value::Array(values), other => Value::Array(vec![other]), }), TransformKind::UnwrapSingleton => unwrap_singleton(value), } } fn to_string_value(value: &Value) -> String { match value { Value::Null => "null".to_owned(), Value::Bool(boolean) => boolean.to_string(), Value::Number(number) => number.to_string(), Value::String(text) => text.clone(), Value::Array(_) | Value::Object(_) => serde_json::to_string(value).unwrap_or_default(), } } fn to_number(value: Value) -> Result { let number = match value { Value::Number(number) => return Ok(Value::Number(number)), Value::String(text) => { if let Ok(integer) = text.parse::() { Number::from(integer) } else { Number::from_f64(text.parse::().map_err(|_| { MappingError::InvalidTransformInput { transform: "to_number", actual: "string", } })?) .ok_or(MappingError::InvalidTransformInput { transform: "to_number", actual: "string", })? } } Value::Bool(boolean) => Number::from(if boolean { 1 } else { 0 }), other => { return Err(MappingError::InvalidTransformInput { transform: "to_number", actual: type_name(&other), }); } }; Ok(Value::Number(number)) } fn to_boolean(value: Value) -> Result { let boolean = match value { Value::Bool(boolean) => boolean, Value::Number(number) => { number.as_i64().unwrap_or_default() != 0 || number.as_f64().unwrap_or_default() != 0.0 } Value::String(text) => match text.to_ascii_lowercase().as_str() { "true" | "1" | "yes" => true, "false" | "0" | "no" => false, _ => { return Err(MappingError::InvalidTransformInput { transform: "to_boolean", actual: "string", }); } }, other => { return Err(MappingError::InvalidTransformInput { transform: "to_boolean", actual: type_name(&other), }); } }; Ok(Value::Bool(boolean)) } fn join_values(value: Value) -> Result { let Value::Array(values) = value else { return Err(MappingError::InvalidTransformInput { transform: "join", actual: type_name(&value), }); }; Ok(Value::String( values .iter() .map(to_string_value) .collect::>() .join(","), )) } fn split_value(value: Value) -> Result { let Value::String(text) = value else { return Err(MappingError::InvalidTransformInput { transform: "split", actual: type_name(&value), }); }; Ok(Value::Array( text.split(',') .map(|part| Value::String(part.trim().to_owned())) .collect(), )) } fn unwrap_singleton(value: Value) -> Result { match value { Value::Array(mut values) if values.len() == 1 => Ok(values.remove(0)), Value::Array(_) => Err(MappingError::InvalidTransformInput { transform: "unwrap_singleton", actual: "array", }), other => Err(MappingError::InvalidTransformInput { transform: "unwrap_singleton", actual: type_name(&other), }), } } fn type_name(value: &Value) -> &'static str { match value { Value::Null => "null", Value::Bool(_) => "boolean", Value::Number(_) => "number", Value::String(_) => "string", Value::Array(_) => "array", Value::Object(_) => "object", } } #[cfg(test)] mod tests { use serde_json::json; use crate::{MappingRule, MappingSet, Transform, TransformKind}; #[test] fn applies_input_mapping_into_request_context() { let mapping = MappingSet { rules: vec![ MappingRule { source: "$.mcp.email".to_owned(), target: "$.request.body.contact.email".to_owned(), required: true, default_value: None, transform: None, condition: None, notes: None, }, MappingRule { source: "$.mcp.tags".to_owned(), target: "$.request.query.tags".to_owned(), required: false, default_value: None, transform: Some(Transform { kind: TransformKind::Join, }), condition: None, notes: None, }, ], }; let result = mapping .apply(&json!({ "mcp": { "email": "user@example.com", "tags": ["warm", "priority"] } })) .unwrap(); assert_eq!( result["request"]["body"]["contact"]["email"], "user@example.com" ); assert_eq!(result["request"]["query"]["tags"], "warm,priority"); } #[test] fn applies_output_mapping_into_output_context() { let mapping = MappingSet { rules: vec![MappingRule { source: "$.response.body.lead_id".to_owned(), target: "$.output.id".to_owned(), required: true, default_value: None, transform: Some(Transform { kind: TransformKind::ToString, }), condition: None, notes: None, }], }; let result = mapping .apply(&json!({ "response": { "body": { "lead_id": 42 } } })) .unwrap(); assert_eq!(result["output"]["id"], "42"); } #[test] fn uses_default_value_for_missing_optional_source() { let mapping = MappingSet { rules: vec![MappingRule { source: "$.mcp.region".to_owned(), target: "$.request.headers.X-Region".to_owned(), required: false, default_value: Some(json!("eu-central")), transform: None, condition: None, notes: None, }], }; let result = mapping.apply(&json!({ "mcp": {} })).unwrap(); assert_eq!(result["request"]["headers"]["X-Region"], "eu-central"); } }