1use clasp_core::Value;
12use evalexpr::{eval_with_context_mut, ContextWithMutableVariables, HashMapContext};
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, VecDeque};
15use std::f64::consts::PI;
16use tracing::warn;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum Transform {
22 Identity,
24
25 Expression { expr: String },
28
29 Scale {
31 from_min: f64,
32 from_max: f64,
33 to_min: f64,
34 to_max: f64,
35 },
36
37 Clamp { min: f64, max: f64 },
39
40 Invert,
42
43 ToInt,
45
46 ToFloat,
48
49 Lookup {
51 table: HashMap<String, Value>,
52 default: Option<Value>,
53 },
54
55 Curve { curve_type: CurveType },
57
58 Quantize { steps: u32 },
60
61 DeadZone { threshold: f64 },
63
64 Smooth { factor: f64 },
66
67 RateLimit { max_delta: f64 },
69
70 Threshold { value: f64, mode: ThresholdMode },
72
73 Modulo { divisor: f64 },
75
76 Abs,
78
79 Negate,
81
82 Power { exponent: f64 },
84
85 Log { base: Option<f64> },
87
88 Round { decimals: u32 },
90
91 Chain { transforms: Vec<Transform> },
93
94 Conditional {
96 condition: Condition,
97 if_true: Box<Transform>,
98 if_false: Box<Transform>,
99 },
100
101 JsonPath { path: String },
103
104 MapType {
106 from_type: ValueType,
107 to_type: ValueType,
108 },
109
110 Bitwise {
112 operation: BitwiseOp,
113 operand: Option<i64>,
114 },
115}
116
117#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum CurveType {
121 Linear,
123 EaseIn,
125 EaseOut,
127 EaseInOut,
129 QuadIn,
131 QuadOut,
133 QuadInOut,
135 CubicIn,
137 CubicOut,
139 CubicInOut,
141 ExpoIn,
143 ExpoOut,
145 ExpoInOut,
147 SineIn,
149 SineOut,
151 SineInOut,
153 CircIn,
155 CircOut,
157 CircInOut,
159 ElasticIn,
161 ElasticOut,
163 BounceOut,
165 Bezier { x1: f64, y1: f64, x2: f64, y2: f64 },
167}
168
169#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ThresholdMode {
173 Above,
175 Below,
177 Equal,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(tag = "type", rename_all = "snake_case")]
184pub enum Condition {
185 GreaterThan { value: f64 },
187 LessThan { value: f64 },
189 Equals { value: f64, tolerance: Option<f64> },
191 InRange { min: f64, max: f64 },
193 Expression { expr: String },
195 And { conditions: Vec<Condition> },
197 Or { conditions: Vec<Condition> },
199 Not { condition: Box<Condition> },
201}
202
203#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum ValueType {
207 Int,
208 Float,
209 Bool,
210 String,
211}
212
213#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub enum BitwiseOp {
217 And,
218 Or,
219 Xor,
220 Not,
221 ShiftLeft,
222 ShiftRight,
223 GetBit,
224 SetBit,
225 ClearBit,
226}
227
228#[derive(Debug, Clone, Default)]
230pub struct TransformState {
231 pub last_value: Option<f64>,
233 pub window: VecDeque<f64>,
235 pub last_time: Option<std::time::Instant>,
237}
238
239impl Transform {
240 pub fn apply(&self, value: &Value, state: &mut TransformState) -> Value {
242 match self {
243 Transform::Identity => value.clone(),
244
245 Transform::Expression { expr } => self.eval_expression(expr, value),
246
247 Transform::Scale {
248 from_min,
249 from_max,
250 to_min,
251 to_max,
252 } => {
253 if let Some(v) = value.as_f64() {
254 let normalized = (v - from_min) / (from_max - from_min);
255 let scaled = to_min + normalized * (to_max - to_min);
256 Value::Float(scaled)
257 } else {
258 value.clone()
259 }
260 }
261
262 Transform::Clamp { min, max } => {
263 if let Some(v) = value.as_f64() {
264 Value::Float(v.clamp(*min, *max))
265 } else {
266 value.clone()
267 }
268 }
269
270 Transform::Invert => {
271 if let Some(v) = value.as_f64() {
272 Value::Float(1.0 - v)
273 } else {
274 value.clone()
275 }
276 }
277
278 Transform::ToInt => {
279 if let Some(v) = value.as_f64() {
280 Value::Int(v as i64)
281 } else {
282 value.clone()
283 }
284 }
285
286 Transform::ToFloat => {
287 if let Some(v) = value.as_i64() {
288 Value::Float(v as f64)
289 } else {
290 value.clone()
291 }
292 }
293
294 Transform::Lookup { table, default } => {
295 let key = match value {
296 Value::Int(i) => i.to_string(),
297 Value::Float(f) => f.to_string(),
298 Value::String(s) => s.clone(),
299 Value::Bool(b) => b.to_string(),
300 _ => return default.clone().unwrap_or_else(|| value.clone()),
301 };
302 table
303 .get(&key)
304 .cloned()
305 .unwrap_or_else(|| default.clone().unwrap_or_else(|| value.clone()))
306 }
307
308 Transform::Curve { curve_type } => {
309 if let Some(t) = value.as_f64() {
310 let t = t.clamp(0.0, 1.0);
311 Value::Float(curve_type.apply(t))
312 } else {
313 value.clone()
314 }
315 }
316
317 Transform::Quantize { steps } => {
318 if let Some(v) = value.as_f64() {
319 let steps = *steps as f64;
320 let quantized = (v * steps).round() / steps;
321 Value::Float(quantized)
322 } else {
323 value.clone()
324 }
325 }
326
327 Transform::DeadZone { threshold } => {
328 if let Some(v) = value.as_f64() {
329 if v.abs() < *threshold {
330 Value::Float(0.0)
331 } else {
332 value.clone()
333 }
334 } else {
335 value.clone()
336 }
337 }
338
339 Transform::Smooth { factor } => {
340 if let Some(v) = value.as_f64() {
341 let smoothed = if let Some(last) = state.last_value {
342 last + factor * (v - last)
343 } else {
344 v
345 };
346 state.last_value = Some(smoothed);
347 Value::Float(smoothed)
348 } else {
349 value.clone()
350 }
351 }
352
353 Transform::RateLimit { max_delta } => {
354 if let Some(v) = value.as_f64() {
355 let limited = if let Some(last) = state.last_value {
356 let delta = v - last;
357 if delta.abs() > *max_delta {
358 last + max_delta * delta.signum()
359 } else {
360 v
361 }
362 } else {
363 v
364 };
365 state.last_value = Some(limited);
366 Value::Float(limited)
367 } else {
368 value.clone()
369 }
370 }
371
372 Transform::Threshold {
373 value: thresh,
374 mode,
375 } => {
376 if let Some(v) = value.as_f64() {
377 let result = match mode {
378 ThresholdMode::Above => {
379 if v > *thresh {
380 1.0
381 } else {
382 0.0
383 }
384 }
385 ThresholdMode::Below => {
386 if v < *thresh {
387 1.0
388 } else {
389 0.0
390 }
391 }
392 ThresholdMode::Equal => {
393 if (v - thresh).abs() < 0.001 {
394 1.0
395 } else {
396 0.0
397 }
398 }
399 };
400 Value::Float(result)
401 } else {
402 value.clone()
403 }
404 }
405
406 Transform::Modulo { divisor } => {
407 if let Some(v) = value.as_f64() {
408 Value::Float(v % divisor)
409 } else {
410 value.clone()
411 }
412 }
413
414 Transform::Abs => {
415 if let Some(v) = value.as_f64() {
416 Value::Float(v.abs())
417 } else if let Some(v) = value.as_i64() {
418 Value::Int(v.abs())
419 } else {
420 value.clone()
421 }
422 }
423
424 Transform::Negate => {
425 if let Some(v) = value.as_f64() {
426 Value::Float(-v)
427 } else if let Some(v) = value.as_i64() {
428 Value::Int(-v)
429 } else {
430 value.clone()
431 }
432 }
433
434 Transform::Power { exponent } => {
435 if let Some(v) = value.as_f64() {
436 Value::Float(v.powf(*exponent))
437 } else {
438 value.clone()
439 }
440 }
441
442 Transform::Log { base } => {
443 if let Some(v) = value.as_f64() {
444 if v > 0.0 {
445 let result = match base {
446 Some(b) => v.log(*b),
447 None => v.ln(),
448 };
449 Value::Float(result)
450 } else {
451 value.clone()
452 }
453 } else {
454 value.clone()
455 }
456 }
457
458 Transform::Round { decimals } => {
459 if let Some(v) = value.as_f64() {
460 let factor = 10_f64.powi(*decimals as i32);
461 Value::Float((v * factor).round() / factor)
462 } else {
463 value.clone()
464 }
465 }
466
467 Transform::Chain { transforms } => {
468 let mut result = value.clone();
469 for transform in transforms {
470 result = transform.apply(&result, state);
471 }
472 result
473 }
474
475 Transform::Conditional {
476 condition,
477 if_true,
478 if_false,
479 } => {
480 if condition.evaluate(value) {
481 if_true.apply(value, state)
482 } else {
483 if_false.apply(value, state)
484 }
485 }
486
487 Transform::JsonPath { path } => self.extract_json_path(value, path),
488
489 Transform::MapType { to_type, .. } => match to_type {
490 ValueType::Int => {
491 if let Some(v) = value.as_f64() {
492 Value::Int(v as i64)
493 } else if let Some(s) = value.as_str().map(|s| s.to_string()) {
494 s.parse::<i64>()
495 .map(Value::Int)
496 .unwrap_or_else(|_| value.clone())
497 } else {
498 value.clone()
499 }
500 }
501 ValueType::Float => {
502 if let Some(v) = value.as_i64() {
503 Value::Float(v as f64)
504 } else if let Some(s) = value.as_str().map(|s| s.to_string()) {
505 s.parse::<f64>()
506 .map(Value::Float)
507 .unwrap_or_else(|_| value.clone())
508 } else {
509 value.clone()
510 }
511 }
512 ValueType::Bool => {
513 if let Some(v) = value.as_f64() {
514 Value::Bool(v != 0.0)
515 } else if let Some(v) = value.as_i64() {
516 Value::Bool(v != 0)
517 } else {
518 value.clone()
519 }
520 }
521 ValueType::String => match value {
522 Value::Int(i) => Value::String(i.to_string()),
523 Value::Float(f) => Value::String(f.to_string()),
524 Value::Bool(b) => Value::String(b.to_string()),
525 _ => value.clone(),
526 },
527 },
528
529 Transform::Bitwise { operation, operand } => {
530 if let Some(v) = value.as_i64() {
531 let result = match operation {
532 BitwiseOp::And => v & operand.unwrap_or(0),
533 BitwiseOp::Or => v | operand.unwrap_or(0),
534 BitwiseOp::Xor => v ^ operand.unwrap_or(0),
535 BitwiseOp::Not => !v,
536 BitwiseOp::ShiftLeft => v << operand.unwrap_or(1) as u32,
537 BitwiseOp::ShiftRight => v >> operand.unwrap_or(1) as u32,
538 BitwiseOp::GetBit => (v >> operand.unwrap_or(0) as u32) & 1,
539 BitwiseOp::SetBit => v | (1 << operand.unwrap_or(0) as u32),
540 BitwiseOp::ClearBit => v & !(1 << operand.unwrap_or(0) as u32),
541 };
542 Value::Int(result)
543 } else {
544 value.clone()
545 }
546 }
547 }
548 }
549
550 fn eval_expression(&self, expr: &str, value: &Value) -> Value {
551 let mut context = HashMapContext::new();
552
553 if let Some(v) = value.as_f64() {
555 let _ = context.set_value("value".to_string(), evalexpr::Value::Float(v));
556 let _ = context.set_value("x".to_string(), evalexpr::Value::Float(v));
557 } else if let Some(v) = value.as_i64() {
558 let _ = context.set_value("value".to_string(), evalexpr::Value::Int(v));
559 let _ = context.set_value("x".to_string(), evalexpr::Value::Int(v));
560 }
561
562 let _ = context.set_value("PI".to_string(), evalexpr::Value::Float(PI));
564 let _ = context.set_value("E".to_string(), evalexpr::Value::Float(std::f64::consts::E));
565
566 match eval_with_context_mut(expr, &mut context) {
567 Ok(evalexpr::Value::Float(f)) => Value::Float(f),
568 Ok(evalexpr::Value::Int(i)) => Value::Int(i),
569 Ok(evalexpr::Value::Boolean(b)) => Value::Bool(b),
570 Ok(evalexpr::Value::String(s)) => Value::String(s),
571 Ok(_) => value.clone(),
572 Err(e) => {
573 warn!("Expression evaluation failed: {}", e);
574 value.clone()
575 }
576 }
577 }
578
579 fn extract_json_path(&self, value: &Value, path: &str) -> Value {
580 let json_value = Self::value_to_json(value);
582
583 match jsonpath_lib::select(&json_value, path) {
584 Ok(results) => {
585 if results.len() == 1 {
586 Self::json_to_value(results[0])
587 } else if !results.is_empty() {
588 let arr: Vec<Value> = results.iter().map(|v| Self::json_to_value(v)).collect();
589 Value::Array(arr)
590 } else {
591 Value::Null
592 }
593 }
594 Err(e) => {
595 warn!("JSON path extraction failed: {}", e);
596 value.clone()
597 }
598 }
599 }
600
601 fn value_to_json(value: &Value) -> serde_json::Value {
602 match value {
603 Value::Int(i) => serde_json::Value::Number((*i).into()),
604 Value::Float(f) => serde_json::Number::from_f64(*f)
605 .map(serde_json::Value::Number)
606 .unwrap_or(serde_json::Value::Null),
607 Value::String(s) => serde_json::Value::String(s.clone()),
608 Value::Bool(b) => serde_json::Value::Bool(*b),
609 Value::Bytes(b) => serde_json::Value::String(base64::encode(b)),
610 Value::Array(arr) => {
611 serde_json::Value::Array(arr.iter().map(Self::value_to_json).collect())
612 }
613 Value::Map(m) => {
614 let obj: serde_json::Map<String, serde_json::Value> = m
615 .iter()
616 .map(|(k, v)| (k.clone(), Self::value_to_json(v)))
617 .collect();
618 serde_json::Value::Object(obj)
619 }
620 Value::Null => serde_json::Value::Null,
621 }
622 }
623
624 fn json_to_value(json: &serde_json::Value) -> Value {
625 match json {
626 serde_json::Value::Null => Value::Null,
627 serde_json::Value::Bool(b) => Value::Bool(*b),
628 serde_json::Value::Number(n) => {
629 if let Some(i) = n.as_i64() {
630 Value::Int(i)
631 } else if let Some(f) = n.as_f64() {
632 Value::Float(f)
633 } else {
634 Value::Null
635 }
636 }
637 serde_json::Value::String(s) => Value::String(s.clone()),
638 serde_json::Value::Array(arr) => {
639 Value::Array(arr.iter().map(Self::json_to_value).collect())
640 }
641 serde_json::Value::Object(_) => {
642 Value::String(json.to_string())
644 }
645 }
646 }
647}
648
649impl CurveType {
650 pub fn apply(&self, t: f64) -> f64 {
652 match self {
653 CurveType::Linear => t,
654 CurveType::EaseIn => t * t,
655 CurveType::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
656 CurveType::EaseInOut => {
657 if t < 0.5 {
658 2.0 * t * t
659 } else {
660 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
661 }
662 }
663 CurveType::QuadIn => t * t,
664 CurveType::QuadOut => 1.0 - (1.0 - t).powi(2),
665 CurveType::QuadInOut => {
666 if t < 0.5 {
667 2.0 * t * t
668 } else {
669 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
670 }
671 }
672 CurveType::CubicIn => t * t * t,
673 CurveType::CubicOut => 1.0 - (1.0 - t).powi(3),
674 CurveType::CubicInOut => {
675 if t < 0.5 {
676 4.0 * t * t * t
677 } else {
678 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
679 }
680 }
681 CurveType::ExpoIn => {
682 if t == 0.0 {
683 0.0
684 } else {
685 2_f64.powf(10.0 * t - 10.0)
686 }
687 }
688 CurveType::ExpoOut => {
689 if t == 1.0 {
690 1.0
691 } else {
692 1.0 - 2_f64.powf(-10.0 * t)
693 }
694 }
695 CurveType::ExpoInOut => {
696 if t == 0.0 {
697 0.0
698 } else if t == 1.0 {
699 1.0
700 } else if t < 0.5 {
701 2_f64.powf(20.0 * t - 10.0) / 2.0
702 } else {
703 (2.0 - 2_f64.powf(-20.0 * t + 10.0)) / 2.0
704 }
705 }
706 CurveType::SineIn => 1.0 - (t * PI / 2.0).cos(),
707 CurveType::SineOut => (t * PI / 2.0).sin(),
708 CurveType::SineInOut => -((PI * t).cos() - 1.0) / 2.0,
709 CurveType::CircIn => 1.0 - (1.0 - t * t).sqrt(),
710 CurveType::CircOut => (1.0 - (t - 1.0).powi(2)).sqrt(),
711 CurveType::CircInOut => {
712 if t < 0.5 {
713 (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0
714 } else {
715 ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0
716 }
717 }
718 CurveType::ElasticIn => {
719 if t == 0.0 {
720 0.0
721 } else if t == 1.0 {
722 1.0
723 } else {
724 let c4 = (2.0 * PI) / 3.0;
725 -2_f64.powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * c4).sin()
726 }
727 }
728 CurveType::ElasticOut => {
729 if t == 0.0 {
730 0.0
731 } else if t == 1.0 {
732 1.0
733 } else {
734 let c4 = (2.0 * PI) / 3.0;
735 2_f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
736 }
737 }
738 CurveType::BounceOut => {
739 let n1 = 7.5625;
740 let d1 = 2.75;
741 if t < 1.0 / d1 {
742 n1 * t * t
743 } else if t < 2.0 / d1 {
744 let t = t - 1.5 / d1;
745 n1 * t * t + 0.75
746 } else if t < 2.5 / d1 {
747 let t = t - 2.25 / d1;
748 n1 * t * t + 0.9375
749 } else {
750 let t = t - 2.625 / d1;
751 n1 * t * t + 0.984375
752 }
753 }
754 CurveType::Bezier { x1, y1, x2, y2 } => {
755 Self::cubic_bezier(t, *x1, *y1, *x2, *y2)
757 }
758 }
759 }
760
761 fn cubic_bezier(t: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
762 let mut guess = t;
764 for _ in 0..8 {
765 let x = Self::bezier_x(guess, x1, x2) - t;
766 if x.abs() < 1e-6 {
767 break;
768 }
769 let dx = Self::bezier_dx(guess, x1, x2);
770 if dx.abs() < 1e-6 {
771 break;
772 }
773 guess -= x / dx;
774 }
775 Self::bezier_y(guess, y1, y2)
776 }
777
778 fn bezier_x(t: f64, x1: f64, x2: f64) -> f64 {
779 let t2 = t * t;
780 let t3 = t2 * t;
781 let mt = 1.0 - t;
782 let mt2 = mt * mt;
783 3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3
784 }
785
786 fn bezier_y(t: f64, y1: f64, y2: f64) -> f64 {
787 let t2 = t * t;
788 let t3 = t2 * t;
789 let mt = 1.0 - t;
790 let mt2 = mt * mt;
791 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
792 }
793
794 fn bezier_dx(t: f64, x1: f64, x2: f64) -> f64 {
795 let t2 = t * t;
796 let mt = 1.0 - t;
797 3.0 * mt * mt * x1 + 6.0 * mt * t * (x2 - x1) + 3.0 * t2 * (1.0 - x2)
798 }
799}
800
801impl Condition {
802 pub fn evaluate(&self, value: &Value) -> bool {
804 match self {
805 Condition::GreaterThan { value: threshold } => {
806 value.as_f64().map(|v| v > *threshold).unwrap_or(false)
807 }
808 Condition::LessThan { value: threshold } => {
809 value.as_f64().map(|v| v < *threshold).unwrap_or(false)
810 }
811 Condition::Equals {
812 value: target,
813 tolerance,
814 } => {
815 if let Some(v) = value.as_f64() {
816 let tol = tolerance.unwrap_or(0.001);
817 (v - target).abs() < tol
818 } else {
819 false
820 }
821 }
822 Condition::InRange { min, max } => value
823 .as_f64()
824 .map(|v| v >= *min && v <= *max)
825 .unwrap_or(false),
826 Condition::Expression { expr } => {
827 let mut context = HashMapContext::new();
828 if let Some(v) = value.as_f64() {
829 let _ = context.set_value("value".to_string(), evalexpr::Value::Float(v));
830 }
831 eval_with_context_mut(expr, &mut context)
832 .map(|v| v.as_boolean().unwrap_or(false))
833 .unwrap_or(false)
834 }
835 Condition::And { conditions } => conditions.iter().all(|c| c.evaluate(value)),
836 Condition::Or { conditions } => conditions.iter().any(|c| c.evaluate(value)),
837 Condition::Not { condition } => !condition.evaluate(value),
838 }
839 }
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(tag = "type", rename_all = "snake_case")]
845pub enum Aggregator {
846 Average,
848 Sum,
850 Min,
852 Max,
854 Latest,
856 First,
858 Count,
860 MovingAverage { window_size: usize },
862 RateOfChange,
864 StdDev,
866}
867
868#[derive(Debug, Clone, Default)]
870pub struct AggregatorState {
871 values: VecDeque<f64>,
872 last_value: Option<f64>,
873 last_time: Option<std::time::Instant>,
874 window_size: usize,
875}
876
877impl Aggregator {
878 pub fn new_state(&self) -> AggregatorState {
880 let window_size = match self {
881 Aggregator::MovingAverage { window_size } => *window_size,
882 _ => 100,
883 };
884 AggregatorState {
885 window_size,
886 ..Default::default()
887 }
888 }
889
890 pub fn add(&self, value: f64, state: &mut AggregatorState) -> f64 {
892 state.values.push_back(value);
894 while state.values.len() > state.window_size {
895 state.values.pop_front();
896 }
897
898 let result = match self {
899 Aggregator::Average => state.values.iter().sum::<f64>() / state.values.len() as f64,
900 Aggregator::Sum => state.values.iter().sum(),
901 Aggregator::Min => state.values.iter().cloned().fold(f64::INFINITY, f64::min),
902 Aggregator::Max => state
903 .values
904 .iter()
905 .cloned()
906 .fold(f64::NEG_INFINITY, f64::max),
907 Aggregator::Latest => value,
908 Aggregator::First => state.values.front().copied().unwrap_or(value),
909 Aggregator::Count => state.values.len() as f64,
910 Aggregator::MovingAverage { .. } => {
911 state.values.iter().sum::<f64>() / state.values.len() as f64
912 }
913 Aggregator::RateOfChange => {
914 let now = std::time::Instant::now();
915 let rate = if let (Some(last), Some(time)) = (state.last_value, state.last_time) {
916 let dt = now.duration_since(time).as_secs_f64();
917 if dt > 0.0 {
918 (value - last) / dt
919 } else {
920 0.0
921 }
922 } else {
923 0.0
924 };
925 state.last_time = Some(now);
926 rate
927 }
928 Aggregator::StdDev => {
929 if state.values.is_empty() {
930 0.0
931 } else {
932 let mean = state.values.iter().sum::<f64>() / state.values.len() as f64;
933 let variance = state.values.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
934 / state.values.len() as f64;
935 variance.sqrt()
936 }
937 }
938 };
939
940 state.last_value = Some(value);
941 result
942 }
943}
944
945mod base64 {
947 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
948
949 pub fn encode(data: &[u8]) -> String {
950 let mut result = String::new();
951 for chunk in data.chunks(3) {
952 let mut n = (chunk[0] as u32) << 16;
953 if chunk.len() > 1 {
954 n |= (chunk[1] as u32) << 8;
955 }
956 if chunk.len() > 2 {
957 n |= chunk[2] as u32;
958 }
959
960 result.push(CHARS[(n >> 18 & 0x3F) as usize] as char);
961 result.push(CHARS[(n >> 12 & 0x3F) as usize] as char);
962 if chunk.len() > 1 {
963 result.push(CHARS[(n >> 6 & 0x3F) as usize] as char);
964 } else {
965 result.push('=');
966 }
967 if chunk.len() > 2 {
968 result.push(CHARS[(n & 0x3F) as usize] as char);
969 } else {
970 result.push('=');
971 }
972 }
973 result
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn test_expression_transform() {
983 let transform = Transform::Expression {
984 expr: "value * 2 + 10".to_string(),
985 };
986 let mut state = TransformState::default();
987 let result = transform.apply(&Value::Float(5.0), &mut state);
988 assert_eq!(result.as_f64(), Some(20.0));
989 }
990
991 #[test]
992 fn test_lookup_transform() {
993 let mut table = HashMap::new();
994 table.insert("0".to_string(), Value::String("off".to_string()));
995 table.insert("1".to_string(), Value::String("on".to_string()));
996
997 let transform = Transform::Lookup {
998 table,
999 default: Some(Value::String("unknown".to_string())),
1000 };
1001 let mut state = TransformState::default();
1002
1003 assert_eq!(
1004 transform.apply(&Value::Int(0), &mut state).as_str(),
1005 Some("off")
1006 );
1007 assert_eq!(
1008 transform.apply(&Value::Int(1), &mut state).as_str(),
1009 Some("on")
1010 );
1011 assert_eq!(
1012 transform.apply(&Value::Int(99), &mut state).as_str(),
1013 Some("unknown")
1014 );
1015 }
1016
1017 #[test]
1018 fn test_curve_ease_in() {
1019 let curve = CurveType::EaseIn;
1020 assert!((curve.apply(0.0) - 0.0).abs() < 0.001);
1021 assert!((curve.apply(0.5) - 0.25).abs() < 0.001);
1022 assert!((curve.apply(1.0) - 1.0).abs() < 0.001);
1023 }
1024
1025 #[test]
1026 fn test_curve_ease_out() {
1027 let curve = CurveType::EaseOut;
1028 assert!((curve.apply(0.0) - 0.0).abs() < 0.001);
1029 assert!((curve.apply(0.5) - 0.75).abs() < 0.001);
1030 assert!((curve.apply(1.0) - 1.0).abs() < 0.001);
1031 }
1032
1033 #[test]
1034 fn test_smooth_transform() {
1035 let transform = Transform::Smooth { factor: 0.5 };
1036 let mut state = TransformState::default();
1037
1038 let r1 = transform.apply(&Value::Float(1.0), &mut state);
1040 assert_eq!(r1.as_f64(), Some(1.0));
1041
1042 let r2 = transform.apply(&Value::Float(0.0), &mut state);
1044 assert!((r2.as_f64().unwrap() - 0.5).abs() < 0.001);
1045 }
1046
1047 #[test]
1048 fn test_chain_transform() {
1049 let transform = Transform::Chain {
1050 transforms: vec![
1051 Transform::Scale {
1052 from_min: 0.0,
1053 from_max: 127.0,
1054 to_min: 0.0,
1055 to_max: 1.0,
1056 },
1057 Transform::Curve {
1058 curve_type: CurveType::EaseIn,
1059 },
1060 ],
1061 };
1062 let mut state = TransformState::default();
1063
1064 let result = transform.apply(&Value::Float(63.5), &mut state);
1065 assert!((result.as_f64().unwrap() - 0.25).abs() < 0.01);
1067 }
1068
1069 #[test]
1070 fn test_conditional_transform() {
1071 let transform = Transform::Conditional {
1072 condition: Condition::GreaterThan { value: 0.5 },
1073 if_true: Box::new(Transform::Expression {
1074 expr: "1.0".to_string(),
1075 }),
1076 if_false: Box::new(Transform::Expression {
1077 expr: "0.0".to_string(),
1078 }),
1079 };
1080 let mut state = TransformState::default();
1081
1082 assert_eq!(
1083 transform.apply(&Value::Float(0.7), &mut state).as_f64(),
1084 Some(1.0)
1085 );
1086 assert_eq!(
1087 transform.apply(&Value::Float(0.3), &mut state).as_f64(),
1088 Some(0.0)
1089 );
1090 }
1091
1092 #[test]
1093 fn test_aggregator_average() {
1094 let agg = Aggregator::Average;
1095 let mut state = agg.new_state();
1096
1097 assert_eq!(agg.add(1.0, &mut state), 1.0);
1098 assert_eq!(agg.add(2.0, &mut state), 1.5);
1099 assert_eq!(agg.add(3.0, &mut state), 2.0);
1100 }
1101
1102 #[test]
1103 fn test_aggregator_moving_average() {
1104 let agg = Aggregator::MovingAverage { window_size: 3 };
1105 let mut state = agg.new_state();
1106
1107 agg.add(1.0, &mut state);
1108 agg.add(2.0, &mut state);
1109 agg.add(3.0, &mut state);
1110 let result = agg.add(4.0, &mut state);
1111 assert_eq!(result, 3.0);
1113 }
1114}