chore: publish clean community baseline
CI / Rust Checks (push) Successful in 4m56s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m55s
Deploy / deploy (push) Failing after 3m52s

This commit is contained in:
github-ops
2026-06-17 06:15:46 +00:00
commit 5c9d87c413
317 changed files with 72908 additions and 0 deletions
+372
View File
@@ -0,0 +1,372 @@
use serde_json::{Map, Number, Value};
use crate::{
JsonPath, JsonPathSegment, MappingError, MappingSet, MappingTargetContext, TransformKind,
};
impl MappingSet {
pub fn apply(&self, source: &Value) -> Result<Value, MappingError> {
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::<Vec<_>>();
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<Value, MappingError> {
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<Value, MappingError> {
let number = match value {
Value::Number(number) => return Ok(Value::Number(number)),
Value::String(text) => {
if let Ok(integer) = text.parse::<i64>() {
Number::from(integer)
} else {
Number::from_f64(text.parse::<f64>().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<Value, MappingError> {
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<Value, MappingError> {
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::<Vec<_>>()
.join(","),
))
}
fn split_value(value: Value) -> Result<Value, MappingError> {
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<Value, MappingError> {
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");
}
}