155 lines
4.8 KiB
Rust
155 lines
4.8 KiB
Rust
//! JSON Transform — WASM guest module for ZCLAW skill system.
|
|
//!
|
|
//! Reads JSON from stdin, applies transforms, writes result to stdout.
|
|
//! Target: `wasm32-wasi`
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use std::io::{self, Read, Write};
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(tag = "type", rename_all = "lowercase")]
|
|
enum Transform {
|
|
Pick { fields: Vec<String> },
|
|
Rename { from: String, to: String },
|
|
Filter { field: String, op: String, value: Value },
|
|
Sort { field: String, order: String },
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Input {
|
|
data: Value,
|
|
#[serde(default)]
|
|
transforms: Vec<Transform>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct Output {
|
|
success: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
data: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
error: Option<String>,
|
|
}
|
|
|
|
impl Output {
|
|
fn ok(data: Value) -> Self {
|
|
Self {
|
|
success: true,
|
|
data: Some(data),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
fn err(msg: impl Into<String>) -> Self {
|
|
Self {
|
|
success: false,
|
|
data: None,
|
|
error: Some(msg.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn _start() {
|
|
let mut input_str = String::new();
|
|
if let Err(e) = io::stdin().read_to_string(&mut input_str) {
|
|
write_output(&Output::err(format!("Failed to read stdin: {}", e)));
|
|
return;
|
|
}
|
|
|
|
let input: Input = match serde_json::from_str(&input_str) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
write_output(&Output::err(format!("Invalid JSON input: {}", e)));
|
|
return;
|
|
}
|
|
};
|
|
|
|
let mut data = input.data;
|
|
for transform in input.transforms {
|
|
data = match apply_transform(data, &transform) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
write_output(&Output::err(e));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
write_output(&Output::ok(data));
|
|
}
|
|
|
|
fn apply_transform(data: Value, transform: &Transform) -> Result<Value, String> {
|
|
match transform {
|
|
Transform::Pick { fields } => {
|
|
let obj = data.as_object().ok_or("Pick requires object input")?;
|
|
let mut result = serde_json::Map::new();
|
|
for field in fields {
|
|
if let Some(v) = obj.get(field) {
|
|
result.insert(field.clone(), v.clone());
|
|
}
|
|
}
|
|
Ok(Value::Object(result))
|
|
}
|
|
Transform::Rename { from, to } => {
|
|
let mut obj = data.as_object().ok_or("Rename requires object input")?.clone();
|
|
if let Some(v) = obj.remove(from) {
|
|
obj.insert(to.clone(), v);
|
|
}
|
|
Ok(Value::Object(obj))
|
|
}
|
|
Transform::Filter { field, op, value } => {
|
|
let arr = data.as_array().ok_or("Filter requires array input")?;
|
|
let filtered: Vec<Value> = arr
|
|
.iter()
|
|
.filter(|item| {
|
|
let item_val = item.get(field);
|
|
match op.as_str() {
|
|
"eq" => item_val == Some(value),
|
|
"ne" => item_val != Some(value),
|
|
"gt" => compare_values(item_val, value).map(|c| c.is_gt()).unwrap_or(false),
|
|
"lt" => compare_values(item_val, value).map(|c| c.is_lt()).unwrap_or(false),
|
|
_ => false,
|
|
}
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
Ok(Value::Array(filtered))
|
|
}
|
|
Transform::Sort { field, order } => {
|
|
let mut arr = data.as_array().ok_or("Sort requires array input")?.clone();
|
|
let field = field.clone();
|
|
let ascending = order != "desc";
|
|
arr.sort_by(|a, b| {
|
|
let va = a.get(&field).unwrap_or(&Value::Null);
|
|
let vb = b.get(&field).unwrap_or(&Value::Null);
|
|
let cmp = compare_values(Some(va), vb).unwrap_or(std::cmp::Ordering::Equal);
|
|
if ascending { cmp } else { cmp.reverse() }
|
|
});
|
|
Ok(Value::Array(arr))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compare_values(a: Option<&Value>, b: &Value) -> Option<std::cmp::Ordering> {
|
|
let a = a?;
|
|
match (a, b) {
|
|
(Value::Number(an), Value::Number(bn)) => {
|
|
if let (Some(af), Some(bf)) = (an.as_f64(), bn.as_f64()) {
|
|
Some(af.partial_cmp(&bf).unwrap_or(std::cmp::Ordering::Equal))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
(Value::String(as_), Value::String(bs)) => Some(as_.cmp(bs)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn write_output(output: &Output) {
|
|
let json = serde_json::to_string(output).unwrap_or_else(|_| r#"{"success":false}"#.to_string());
|
|
let _ = io::stdout().write_all(json.as_bytes());
|
|
let _ = io::stdout().flush();
|
|
}
|