pylon_plugin/builtin/
computed.rs1use std::collections::HashMap;
2
3use crate::Plugin;
4use serde_json::Value;
5
6pub type ComputeFn = Box<dyn Fn(&Value) -> Value + Send + Sync>;
8
9pub struct ComputedFieldsPlugin {
12 fields: HashMap<String, Vec<(String, ComputeFn)>>,
14}
15
16impl ComputedFieldsPlugin {
17 pub fn new() -> Self {
18 Self {
19 fields: HashMap::new(),
20 }
21 }
22
23 pub fn add<F>(&mut self, entity: &str, field_name: &str, compute: F)
25 where
26 F: Fn(&Value) -> Value + Send + Sync + 'static,
27 {
28 self.fields
29 .entry(entity.to_string())
30 .or_default()
31 .push((field_name.to_string(), Box::new(compute)));
32 }
33
34 pub fn apply(&self, entity: &str, row: &mut Value) {
36 if let Some(fields) = self.fields.get(entity) {
37 if let Some(obj) = row.as_object_mut() {
38 for (name, compute) in fields {
39 let value = compute(&Value::Object(obj.clone()));
40 obj.insert(name.clone(), value);
41 }
42 }
43 }
44 }
45
46 pub fn apply_all(&self, entity: &str, rows: &mut [Value]) {
48 for row in rows {
49 self.apply(entity, row);
50 }
51 }
52}
53
54impl Plugin for ComputedFieldsPlugin {
55 fn name(&self) -> &str {
56 "computed-fields"
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn basic_computed_field() {
66 let mut plugin = ComputedFieldsPlugin::new();
67 plugin.add("User", "fullName", |row| {
68 let first = row.get("firstName").and_then(|v| v.as_str()).unwrap_or("");
69 let last = row.get("lastName").and_then(|v| v.as_str()).unwrap_or("");
70 Value::String(format!("{first} {last}").trim().to_string())
71 });
72
73 let mut row = serde_json::json!({"firstName": "Alice", "lastName": "Smith"});
74 plugin.apply("User", &mut row);
75 assert_eq!(row["fullName"], "Alice Smith");
76 }
77
78 #[test]
79 fn computed_field_from_numeric() {
80 let mut plugin = ComputedFieldsPlugin::new();
81 plugin.add("Product", "priceFormatted", |row| {
82 let price = row.get("price").and_then(|v| v.as_f64()).unwrap_or(0.0);
83 Value::String(format!("${:.2}", price))
84 });
85
86 let mut row = serde_json::json!({"price": 29.99});
87 plugin.apply("Product", &mut row);
88 assert_eq!(row["priceFormatted"], "$29.99");
89 }
90
91 #[test]
92 fn no_config_no_change() {
93 let plugin = ComputedFieldsPlugin::new();
94 let mut row = serde_json::json!({"name": "Alice"});
95 plugin.apply("User", &mut row);
96 assert!(row.get("fullName").is_none());
97 }
98
99 #[test]
100 fn apply_all_rows() {
101 let mut plugin = ComputedFieldsPlugin::new();
102 plugin.add("User", "upper", |row| {
103 let name = row.get("name").and_then(|v| v.as_str()).unwrap_or("");
104 Value::String(name.to_uppercase())
105 });
106
107 let mut rows = vec![
108 serde_json::json!({"name": "alice"}),
109 serde_json::json!({"name": "bob"}),
110 ];
111 plugin.apply_all("User", &mut rows);
112 assert_eq!(rows[0]["upper"], "ALICE");
113 assert_eq!(rows[1]["upper"], "BOB");
114 }
115
116 #[test]
117 fn multiple_computed_fields() {
118 let mut plugin = ComputedFieldsPlugin::new();
119 plugin.add("User", "initials", |row| {
120 let first = row.get("firstName").and_then(|v| v.as_str()).unwrap_or("");
121 let last = row.get("lastName").and_then(|v| v.as_str()).unwrap_or("");
122 let i = format!(
123 "{}{}",
124 first.chars().next().unwrap_or(' '),
125 last.chars().next().unwrap_or(' ')
126 );
127 Value::String(i.trim().to_string())
128 });
129 plugin.add("User", "emailDomain", |row| {
130 let email = row.get("email").and_then(|v| v.as_str()).unwrap_or("");
131 let domain = email.split('@').nth(1).unwrap_or("");
132 Value::String(domain.to_string())
133 });
134
135 let mut row = serde_json::json!({"firstName": "Alice", "lastName": "Smith", "email": "alice@example.com"});
136 plugin.apply("User", &mut row);
137 assert_eq!(row["initials"], "AS");
138 assert_eq!(row["emailDomain"], "example.com");
139 }
140}