1use oversync_core::error::OversyncError;
2
3use crate::TransformStep;
4
5pub struct Rename {
7 pub from: String,
8 pub to: String,
9}
10
11impl TransformStep for Rename {
12 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
13 if let Some(obj) = data.as_object_mut()
14 && let Some(val) = obj.remove(&self.from)
15 {
16 obj.insert(self.to.clone(), val);
17 }
18 Ok(true)
19 }
20
21 fn step_name(&self) -> &str {
22 "rename"
23 }
24}
25
26pub struct Set {
28 pub field: String,
29 pub value: serde_json::Value,
30}
31
32impl TransformStep for Set {
33 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
34 if let Some(obj) = data.as_object_mut() {
35 obj.insert(self.field.clone(), self.value.clone());
36 }
37 Ok(true)
38 }
39
40 fn step_name(&self) -> &str {
41 "set"
42 }
43}
44
45pub struct Upper {
47 pub field: String,
48}
49
50impl TransformStep for Upper {
51 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
52 if let Some(obj) = data.as_object_mut()
53 && let Some(val) = obj.get_mut(&self.field)
54 && let Some(s) = val.as_str()
55 {
56 *val = serde_json::Value::String(s.to_uppercase());
57 }
58 Ok(true)
59 }
60
61 fn step_name(&self) -> &str {
62 "upper"
63 }
64}
65
66pub struct Lower {
68 pub field: String,
69}
70
71impl TransformStep for Lower {
72 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
73 if let Some(obj) = data.as_object_mut()
74 && let Some(val) = obj.get_mut(&self.field)
75 && let Some(s) = val.as_str()
76 {
77 *val = serde_json::Value::String(s.to_lowercase());
78 }
79 Ok(true)
80 }
81
82 fn step_name(&self) -> &str {
83 "lower"
84 }
85}
86
87pub struct Remove {
89 pub field: String,
90}
91
92impl TransformStep for Remove {
93 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
94 if let Some(obj) = data.as_object_mut() {
95 obj.remove(&self.field);
96 }
97 Ok(true)
98 }
99
100 fn step_name(&self) -> &str {
101 "remove"
102 }
103}
104
105pub struct Copy {
107 pub from: String,
108 pub to: String,
109}
110
111impl TransformStep for Copy {
112 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
113 if let Some(obj) = data.as_object_mut()
114 && let Some(val) = obj.get(&self.from).cloned()
115 {
116 obj.insert(self.to.clone(), val);
117 }
118 Ok(true)
119 }
120
121 fn step_name(&self) -> &str {
122 "copy"
123 }
124}
125
126pub struct Default {
128 pub field: String,
129 pub value: serde_json::Value,
130}
131
132impl TransformStep for Default {
133 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
134 if let Some(obj) = data.as_object_mut() {
135 let needs_default = match obj.get(&self.field) {
136 None => true,
137 Some(v) => v.is_null(),
138 };
139 if needs_default {
140 obj.insert(self.field.clone(), self.value.clone());
141 }
142 }
143 Ok(true)
144 }
145
146 fn step_name(&self) -> &str {
147 "default"
148 }
149}
150
151pub struct Filter {
153 pub field: String,
154 pub op: FilterOp,
155 pub value: serde_json::Value,
156}
157
158#[derive(Debug, Clone)]
159pub enum FilterOp {
160 Eq,
161 Ne,
162 Gt,
163 Gte,
164 Lt,
165 Lte,
166 Contains,
167 Exists,
168}
169
170impl TransformStep for Filter {
171 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
172 let obj = match data.as_object() {
173 Some(o) => o,
174 None => return Ok(true),
175 };
176
177 if matches!(self.op, FilterOp::Exists) {
178 return Ok(obj.contains_key(&self.field));
179 }
180
181 let field_val = match obj.get(&self.field) {
182 Some(v) => v,
183 None => return Ok(false),
184 };
185
186 let keep = match self.op {
187 FilterOp::Eq => field_val == &self.value,
188 FilterOp::Ne => field_val != &self.value,
189 FilterOp::Gt => json_cmp(field_val, &self.value).is_some_and(|o| o.is_gt()),
190 FilterOp::Gte => json_cmp(field_val, &self.value).is_some_and(|o| o.is_ge()),
191 FilterOp::Lt => json_cmp(field_val, &self.value).is_some_and(|o| o.is_lt()),
192 FilterOp::Lte => json_cmp(field_val, &self.value).is_some_and(|o| o.is_le()),
193 FilterOp::Contains => {
194 if let (Some(haystack), Some(needle)) = (field_val.as_str(), self.value.as_str()) {
195 haystack.contains(needle)
196 } else {
197 false
198 }
199 }
200 FilterOp::Exists => unreachable!(),
201 };
202 Ok(keep)
203 }
204
205 fn step_name(&self) -> &str {
206 "filter"
207 }
208}
209
210pub struct MapValue {
212 pub field: String,
213 pub mapping: serde_json::Map<String, serde_json::Value>,
214}
215
216impl TransformStep for MapValue {
217 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
218 if let Some(obj) = data.as_object_mut()
219 && let Some(val) = obj.get(&self.field)
220 {
221 let key = match val {
222 serde_json::Value::String(s) => s.clone(),
223 other => other.to_string(),
224 };
225 if let Some(replacement) = self.mapping.get(&key) {
226 obj.insert(self.field.clone(), replacement.clone());
227 }
228 }
229 Ok(true)
230 }
231
232 fn step_name(&self) -> &str {
233 "map_value"
234 }
235}
236
237pub struct Truncate {
239 pub field: String,
240 pub max_len: usize,
241}
242
243impl TransformStep for Truncate {
244 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
245 if let Some(obj) = data.as_object_mut()
246 && let Some(val) = obj.get_mut(&self.field)
247 && let Some(s) = val.as_str()
248 && s.chars().count() > self.max_len
249 {
250 let truncated: String = s.chars().take(self.max_len).collect();
251 *val = serde_json::Value::String(truncated);
252 }
253 Ok(true)
254 }
255
256 fn step_name(&self) -> &str {
257 "truncate"
258 }
259}
260
261pub struct Nest {
263 pub fields: Vec<String>,
264 pub into: String,
265}
266
267impl TransformStep for Nest {
268 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
269 if let Some(obj) = data.as_object_mut() {
270 let mut nested = serde_json::Map::new();
271 for field in &self.fields {
272 if let Some(val) = obj.remove(field) {
273 nested.insert(field.clone(), val);
274 }
275 }
276 if !nested.is_empty() {
277 obj.insert(self.into.clone(), serde_json::Value::Object(nested));
278 }
279 }
280 Ok(true)
281 }
282
283 fn step_name(&self) -> &str {
284 "nest"
285 }
286}
287
288pub struct Flatten {
290 pub field: String,
291}
292
293impl TransformStep for Flatten {
294 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
295 if let Some(obj) = data.as_object_mut()
296 && let Some(serde_json::Value::Object(nested)) = obj.remove(&self.field)
297 {
298 for (k, v) in nested {
299 obj.insert(k, v);
300 }
301 }
302 Ok(true)
303 }
304
305 fn step_name(&self) -> &str {
306 "flatten"
307 }
308}
309
310pub struct Hash {
312 pub field: String,
313}
314
315impl TransformStep for Hash {
316 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
317 if let Some(obj) = data.as_object_mut()
318 && let Some(val) = obj.get(&self.field)
319 {
320 let input = match val {
321 serde_json::Value::String(s) => s.clone(),
322 other => other.to_string(),
323 };
324 use sha2::{Digest, Sha256};
325 let hash = hex::encode(Sha256::digest(input.as_bytes()));
326 obj.insert(self.field.clone(), serde_json::Value::String(hash));
327 }
328 Ok(true)
329 }
330
331 fn step_name(&self) -> &str {
332 "hash"
333 }
334}
335
336pub struct Coalesce {
338 pub fields: Vec<String>,
339 pub into: String,
340}
341
342impl TransformStep for Coalesce {
343 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
344 if let Some(obj) = data.as_object_mut() {
345 for field in &self.fields {
346 if let Some(val) = obj.get(field)
347 && !val.is_null()
348 {
349 let v = val.clone();
350 obj.insert(self.into.clone(), v);
351 return Ok(true);
352 }
353 }
354 }
355 Ok(true)
356 }
357
358 fn step_name(&self) -> &str {
359 "coalesce"
360 }
361}
362
363pub struct SchemaFilter {
370 pub field: String,
371 pub allow: Vec<regex::Regex>,
372 pub deny: Vec<regex::Regex>,
373}
374
375impl TransformStep for SchemaFilter {
376 fn apply(&self, data: &mut serde_json::Value) -> Result<bool, OversyncError> {
377 let val_str = match data.as_object().and_then(|o| o.get(&self.field)) {
378 Some(serde_json::Value::String(s)) => s.clone(),
379 Some(v) => v.to_string(),
380 None => return Ok(self.allow.is_empty()),
381 };
382
383 for deny in &self.deny {
384 if deny.is_match(&val_str) {
385 return Ok(false);
386 }
387 }
388
389 if !self.allow.is_empty() {
390 let allowed = self.allow.iter().any(|r| r.is_match(&val_str));
391 if !allowed {
392 return Ok(false);
393 }
394 }
395
396 Ok(true)
397 }
398
399 fn step_name(&self) -> &str {
400 "schema_filter"
401 }
402}
403
404fn json_cmp(a: &serde_json::Value, b: &serde_json::Value) -> Option<std::cmp::Ordering> {
405 match (a, b) {
406 (serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
407 a.as_f64()?.partial_cmp(&b.as_f64()?)
408 }
409 (serde_json::Value::String(a), serde_json::Value::String(b)) => Some(a.cmp(b)),
410 _ => None,
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn rename_moves_field() {
420 let mut data = serde_json::json!({"old_name": "alice"});
421 let step = Rename {
422 from: "old_name".into(),
423 to: "new_name".into(),
424 };
425 assert!(step.apply(&mut data).unwrap());
426 assert_eq!(data, serde_json::json!({"new_name": "alice"}));
427 }
428
429 #[test]
430 fn rename_missing_field_is_noop() {
431 let mut data = serde_json::json!({"x": 1});
432 let step = Rename {
433 from: "missing".into(),
434 to: "y".into(),
435 };
436 assert!(step.apply(&mut data).unwrap());
437 assert_eq!(data, serde_json::json!({"x": 1}));
438 }
439
440 #[test]
441 fn set_adds_field() {
442 let mut data = serde_json::json!({"x": 1});
443 let step = Set {
444 field: "version".into(),
445 value: serde_json::json!(2),
446 };
447 step.apply(&mut data).unwrap();
448 assert_eq!(data["version"], 2);
449 }
450
451 #[test]
452 fn set_overwrites_existing() {
453 let mut data = serde_json::json!({"x": 1});
454 let step = Set {
455 field: "x".into(),
456 value: serde_json::json!(99),
457 };
458 step.apply(&mut data).unwrap();
459 assert_eq!(data["x"], 99);
460 }
461
462 #[test]
463 fn upper_converts_string() {
464 let mut data = serde_json::json!({"name": "alice"});
465 Upper {
466 field: "name".into(),
467 }
468 .apply(&mut data)
469 .unwrap();
470 assert_eq!(data["name"], "ALICE");
471 }
472
473 #[test]
474 fn upper_ignores_non_string() {
475 let mut data = serde_json::json!({"count": 42});
476 Upper {
477 field: "count".into(),
478 }
479 .apply(&mut data)
480 .unwrap();
481 assert_eq!(data["count"], 42);
482 }
483
484 #[test]
485 fn lower_converts_string() {
486 let mut data = serde_json::json!({"name": "ALICE"});
487 Lower {
488 field: "name".into(),
489 }
490 .apply(&mut data)
491 .unwrap();
492 assert_eq!(data["name"], "alice");
493 }
494
495 #[test]
496 fn remove_deletes_field() {
497 let mut data = serde_json::json!({"x": 1, "secret": "password"});
498 Remove {
499 field: "secret".into(),
500 }
501 .apply(&mut data)
502 .unwrap();
503 assert_eq!(data, serde_json::json!({"x": 1}));
504 }
505
506 #[test]
507 fn remove_missing_is_noop() {
508 let mut data = serde_json::json!({"x": 1});
509 Remove {
510 field: "missing".into(),
511 }
512 .apply(&mut data)
513 .unwrap();
514 assert_eq!(data, serde_json::json!({"x": 1}));
515 }
516
517 #[test]
518 fn copy_duplicates_value() {
519 let mut data = serde_json::json!({"src": "hello"});
520 Copy {
521 from: "src".into(),
522 to: "dst".into(),
523 }
524 .apply(&mut data)
525 .unwrap();
526 assert_eq!(data["src"], "hello");
527 assert_eq!(data["dst"], "hello");
528 }
529
530 #[test]
531 fn copy_missing_source_is_noop() {
532 let mut data = serde_json::json!({"x": 1});
533 Copy {
534 from: "missing".into(),
535 to: "dst".into(),
536 }
537 .apply(&mut data)
538 .unwrap();
539 assert!(!data.as_object().unwrap().contains_key("dst"));
540 }
541
542 #[test]
543 fn default_sets_when_absent() {
544 let mut data = serde_json::json!({"x": 1});
545 Default {
546 field: "y".into(),
547 value: serde_json::json!(42),
548 }
549 .apply(&mut data)
550 .unwrap();
551 assert_eq!(data["y"], 42);
552 }
553
554 #[test]
555 fn default_sets_when_null() {
556 let mut data = serde_json::json!({"x": null});
557 Default {
558 field: "x".into(),
559 value: serde_json::json!(0),
560 }
561 .apply(&mut data)
562 .unwrap();
563 assert_eq!(data["x"], 0);
564 }
565
566 #[test]
567 fn default_skips_when_present() {
568 let mut data = serde_json::json!({"x": 99});
569 Default {
570 field: "x".into(),
571 value: serde_json::json!(0),
572 }
573 .apply(&mut data)
574 .unwrap();
575 assert_eq!(data["x"], 99);
576 }
577
578 #[test]
579 fn filter_eq_keeps() {
580 let mut data = serde_json::json!({"status": "active"});
581 let step = Filter {
582 field: "status".into(),
583 op: FilterOp::Eq,
584 value: serde_json::json!("active"),
585 };
586 assert!(step.apply(&mut data).unwrap());
587 }
588
589 #[test]
590 fn filter_eq_drops() {
591 let mut data = serde_json::json!({"status": "inactive"});
592 let step = Filter {
593 field: "status".into(),
594 op: FilterOp::Eq,
595 value: serde_json::json!("active"),
596 };
597 assert!(!step.apply(&mut data).unwrap());
598 }
599
600 #[test]
601 fn filter_ne() {
602 let mut data = serde_json::json!({"status": "active"});
603 let step = Filter {
604 field: "status".into(),
605 op: FilterOp::Ne,
606 value: serde_json::json!("deleted"),
607 };
608 assert!(step.apply(&mut data).unwrap());
609 }
610
611 #[test]
612 fn filter_gt_numeric() {
613 let mut data = serde_json::json!({"score": 80});
614 let step = Filter {
615 field: "score".into(),
616 op: FilterOp::Gt,
617 value: serde_json::json!(50),
618 };
619 assert!(step.apply(&mut data).unwrap());
620
621 let mut data2 = serde_json::json!({"score": 30});
622 assert!(!step.apply(&mut data2).unwrap());
623 }
624
625 #[test]
626 fn filter_contains_string() {
627 let mut data = serde_json::json!({"email": "alice@example.com"});
628 let step = Filter {
629 field: "email".into(),
630 op: FilterOp::Contains,
631 value: serde_json::json!("@example"),
632 };
633 assert!(step.apply(&mut data).unwrap());
634 }
635
636 #[test]
637 fn filter_exists() {
638 let mut data = serde_json::json!({"name": "alice"});
639 let step = Filter {
640 field: "name".into(),
641 op: FilterOp::Exists,
642 value: serde_json::json!(null),
643 };
644 assert!(step.apply(&mut data).unwrap());
645
646 let mut data2 = serde_json::json!({"other": 1});
647 assert!(!step.apply(&mut data2).unwrap());
648 }
649
650 #[test]
651 fn filter_missing_field_drops() {
652 let mut data = serde_json::json!({"x": 1});
653 let step = Filter {
654 field: "y".into(),
655 op: FilterOp::Eq,
656 value: serde_json::json!(1),
657 };
658 assert!(!step.apply(&mut data).unwrap());
659 }
660
661 #[test]
662 fn map_value_replaces() {
663 let mut data = serde_json::json!({"op_type": "D"});
664 let mut mapping = serde_json::Map::new();
665 mapping.insert("D".into(), serde_json::json!("deleted"));
666 mapping.insert("U".into(), serde_json::json!("updated"));
667 mapping.insert("I".into(), serde_json::json!("inserted"));
668 let step = MapValue {
669 field: "op_type".into(),
670 mapping,
671 };
672 step.apply(&mut data).unwrap();
673 assert_eq!(data["op_type"], "deleted");
674 }
675
676 #[test]
677 fn map_value_unmapped_unchanged() {
678 let mut data = serde_json::json!({"op_type": "X"});
679 let mut mapping = serde_json::Map::new();
680 mapping.insert("D".into(), serde_json::json!("deleted"));
681 let step = MapValue {
682 field: "op_type".into(),
683 mapping,
684 };
685 step.apply(&mut data).unwrap();
686 assert_eq!(data["op_type"], "X");
687 }
688
689 #[test]
690 fn truncate_shortens() {
691 let mut data = serde_json::json!({"desc": "a very long description text here"});
692 Truncate {
693 field: "desc".into(),
694 max_len: 10,
695 }
696 .apply(&mut data)
697 .unwrap();
698 assert_eq!(data["desc"], "a very lon");
699 }
700
701 #[test]
702 fn truncate_short_string_unchanged() {
703 let mut data = serde_json::json!({"desc": "short"});
704 Truncate {
705 field: "desc".into(),
706 max_len: 100,
707 }
708 .apply(&mut data)
709 .unwrap();
710 assert_eq!(data["desc"], "short");
711 }
712
713 #[test]
714 fn nest_groups_fields() {
715 let mut data = serde_json::json!({"city": "NYC", "zip": "10001", "name": "alice"});
716 Nest {
717 fields: vec!["city".into(), "zip".into()],
718 into: "address".into(),
719 }
720 .apply(&mut data)
721 .unwrap();
722 assert_eq!(data["address"]["city"], "NYC");
723 assert_eq!(data["address"]["zip"], "10001");
724 assert_eq!(data["name"], "alice");
725 assert!(!data.as_object().unwrap().contains_key("city"));
726 }
727
728 #[test]
729 fn nest_partial_fields() {
730 let mut data = serde_json::json!({"city": "NYC"});
731 Nest {
732 fields: vec!["city".into(), "missing".into()],
733 into: "addr".into(),
734 }
735 .apply(&mut data)
736 .unwrap();
737 assert_eq!(data["addr"]["city"], "NYC");
738 assert!(!data["addr"].as_object().unwrap().contains_key("missing"));
739 }
740
741 #[test]
742 fn flatten_inlines_nested() {
743 let mut data = serde_json::json!({"meta": {"source": "pg", "version": 2}, "id": 1});
744 Flatten {
745 field: "meta".into(),
746 }
747 .apply(&mut data)
748 .unwrap();
749 assert_eq!(data["source"], "pg");
750 assert_eq!(data["version"], 2);
751 assert_eq!(data["id"], 1);
752 assert!(!data.as_object().unwrap().contains_key("meta"));
753 }
754
755 #[test]
756 fn flatten_missing_is_noop() {
757 let mut data = serde_json::json!({"id": 1});
758 Flatten {
759 field: "meta".into(),
760 }
761 .apply(&mut data)
762 .unwrap();
763 assert_eq!(data, serde_json::json!({"id": 1}));
764 }
765
766 #[test]
767 fn hash_sha256() {
768 let mut data = serde_json::json!({"email": "alice@example.com"});
769 Hash {
770 field: "email".into(),
771 }
772 .apply(&mut data)
773 .unwrap();
774 let hashed = data["email"].as_str().unwrap();
775 assert_eq!(hashed.len(), 64);
776 assert!(hashed.chars().all(|c| c.is_ascii_hexdigit()));
777 assert_ne!(hashed, "alice@example.com");
778 }
779
780 #[test]
781 fn hash_missing_is_noop() {
782 let mut data = serde_json::json!({"x": 1});
783 Hash {
784 field: "missing".into(),
785 }
786 .apply(&mut data)
787 .unwrap();
788 assert_eq!(data, serde_json::json!({"x": 1}));
789 }
790
791 #[test]
792 fn coalesce_takes_first_non_null() {
793 let mut data = serde_json::json!({"a": null, "b": "found", "c": "also"});
794 Coalesce {
795 fields: vec!["a".into(), "b".into(), "c".into()],
796 into: "result".into(),
797 }
798 .apply(&mut data)
799 .unwrap();
800 assert_eq!(data["result"], "found");
801 }
802
803 #[test]
804 fn coalesce_all_null_no_write() {
805 let mut data = serde_json::json!({"a": null, "b": null});
806 Coalesce {
807 fields: vec!["a".into(), "b".into()],
808 into: "result".into(),
809 }
810 .apply(&mut data)
811 .unwrap();
812 assert!(!data.as_object().unwrap().contains_key("result"));
813 }
814
815 #[test]
816 fn coalesce_missing_field_skipped() {
817 let mut data = serde_json::json!({"b": 42});
818 Coalesce {
819 fields: vec!["missing".into(), "b".into()],
820 into: "out".into(),
821 }
822 .apply(&mut data)
823 .unwrap();
824 assert_eq!(data["out"], 42);
825 }
826
827 fn re(pattern: &str) -> regex::Regex {
830 regex::Regex::new(pattern).unwrap()
831 }
832
833 #[test]
834 fn schema_filter_allow_keeps_matching() {
835 let mut data = serde_json::json!({"table": "public.users"});
836 let step = SchemaFilter {
837 field: "table".into(),
838 allow: vec![re("^public\\.")],
839 deny: vec![],
840 };
841 assert!(step.apply(&mut data).unwrap());
842 }
843
844 #[test]
845 fn schema_filter_allow_drops_non_matching() {
846 let mut data = serde_json::json!({"table": "internal.secrets"});
847 let step = SchemaFilter {
848 field: "table".into(),
849 allow: vec![re("^public\\.")],
850 deny: vec![],
851 };
852 assert!(!step.apply(&mut data).unwrap());
853 }
854
855 #[test]
856 fn schema_filter_deny_drops_matching() {
857 let mut data = serde_json::json!({"table": "pg_catalog.pg_class"});
858 let step = SchemaFilter {
859 field: "table".into(),
860 allow: vec![],
861 deny: vec![re("^pg_catalog"), re("^information_schema")],
862 };
863 assert!(!step.apply(&mut data).unwrap());
864 }
865
866 #[test]
867 fn schema_filter_deny_keeps_non_matching() {
868 let mut data = serde_json::json!({"table": "public.users"});
869 let step = SchemaFilter {
870 field: "table".into(),
871 allow: vec![],
872 deny: vec![re("^pg_catalog")],
873 };
874 assert!(step.apply(&mut data).unwrap());
875 }
876
877 #[test]
878 fn schema_filter_deny_takes_precedence_over_allow() {
879 let mut data = serde_json::json!({"table": "public.pg_temp"});
880 let step = SchemaFilter {
881 field: "table".into(),
882 allow: vec![re("^public\\.")],
883 deny: vec![re("pg_temp$")],
884 };
885 assert!(!step.apply(&mut data).unwrap());
886 }
887
888 #[test]
889 fn schema_filter_multiple_allow_any_matches() {
890 let step = SchemaFilter {
891 field: "schema".into(),
892 allow: vec![re("^public$"), re("^analytics$")],
893 deny: vec![],
894 };
895 let mut d1 = serde_json::json!({"schema": "public"});
896 assert!(step.apply(&mut d1).unwrap());
897 let mut d2 = serde_json::json!({"schema": "analytics"});
898 assert!(step.apply(&mut d2).unwrap());
899 let mut d3 = serde_json::json!({"schema": "internal"});
900 assert!(!step.apply(&mut d3).unwrap());
901 }
902
903 #[test]
904 fn schema_filter_missing_field_dropped_when_allow_set() {
905 let mut data = serde_json::json!({"other": "value"});
906 let step = SchemaFilter {
907 field: "table".into(),
908 allow: vec![re(".*")],
909 deny: vec![],
910 };
911 assert!(!step.apply(&mut data).unwrap());
912 }
913
914 #[test]
915 fn schema_filter_missing_field_kept_when_deny_only() {
916 let mut data = serde_json::json!({"other": "value"});
917 let step = SchemaFilter {
918 field: "table".into(),
919 allow: vec![],
920 deny: vec![re("secret")],
921 };
922 assert!(step.apply(&mut data).unwrap());
923 }
924
925 #[test]
928 fn rename_overwrites_existing_target() {
929 let mut data = serde_json::json!({"old": "moved", "new": "existing"});
930 Rename {
931 from: "old".into(),
932 to: "new".into(),
933 }
934 .apply(&mut data)
935 .unwrap();
936 assert_eq!(data["new"], "moved");
937 assert!(!data.as_object().unwrap().contains_key("old"));
938 }
939
940 #[test]
941 fn flatten_overwrites_parent_on_collision() {
942 let mut data = serde_json::json!({"id": 1, "meta": {"id": "nested", "extra": "val"}});
943 Flatten {
944 field: "meta".into(),
945 }
946 .apply(&mut data)
947 .unwrap();
948 assert_eq!(data["id"], "nested"); assert_eq!(data["extra"], "val");
950 }
951
952 #[test]
953 fn nest_overwrites_existing_target() {
954 let mut data = serde_json::json!({"a": 1, "b": 2, "target": "old"});
955 Nest {
956 fields: vec!["a".into(), "b".into()],
957 into: "target".into(),
958 }
959 .apply(&mut data)
960 .unwrap();
961 assert!(data["target"].is_object()); assert_eq!(data["target"]["a"], 1);
963 }
964}