1use calamine::Reader;
2use rust_xlsxwriter::{
3 workbook::Workbook, Color, DataValidation, DataValidationErrorStyle, DataValidationRule,
4 Format, FormatPattern, Formula,
5};
6use crate::DriverError;
8use serde::{Deserialize, Serialize};
9use serde_json::Value as Json;
10use std::{
11 collections::BTreeMap,
12 fmt::{self, Display, Formatter},
13 io::{Read as IoRead, Seek as IoSeek, Write as IoWrite},
14 path::Path,
15};
16
17#[macro_export]
67macro_rules! ui_text {
68 ({ $($key:expr => $val:expr),+ $(,)? }) => {{
70 let mut m = ::std::collections::BTreeMap::new();
71 $( m.insert($key.to_string(), $val.to_string()); )+
72 $crate::UiText::Localized { locales: m }
73 }};
74
75 ( en = $en:expr $(, zh = $zh:expr )? $(, zh_cn = $zhcn:expr )? ) => {{
77 let mut m = ::std::collections::BTreeMap::new();
78 m.insert("en-US".to_string(), $en.to_string());
79 $( m.insert("zh-CN".to_string(), $zh.to_string()); )?
80 $( m.insert("zh-CN".to_string(), $zhcn.to_string()); )?
81 $crate::UiText::Localized { locales: m }
82 }};
83
84 ($s:expr) => {{
86 $crate::UiText::Simple { value: $s.to_string() }
87 }};
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct DriverSchemas {
92 pub channel: Vec<Node>,
93 pub device: Vec<Node>,
94 pub point: Vec<Node>,
95 pub action: Vec<Node>,
96}
97
98pub type PluginConfigSchemas = Vec<Node>;
99
100impl DriverSchemas {
101 pub fn build_template(&self, entity: FlattenEntity, locale: &str) -> DriverEntityTemplate {
102 let mut columns = Vec::new();
103 let mut discriminator_keys = Vec::new();
104
105 match entity {
106 FlattenEntity::DevicePoints => {
107 Self::push_base_columns(FlattenEntity::Device, locale, &mut columns);
110
111 Self::flatten_nodes(
113 &self.device,
114 "device_driver_config.",
115 locale,
116 &mut columns,
117 &mut discriminator_keys,
118 None,
119 None,
120 );
121
122 Self::push_base_columns(FlattenEntity::Point, locale, &mut columns);
124
125 Self::flatten_nodes(
127 &self.point,
128 "driver_config.",
129 locale,
130 &mut columns,
131 &mut discriminator_keys,
132 None,
133 None,
134 );
135 }
136 _ => {
137 let (nodes, prefix) = match entity {
138 FlattenEntity::Device => (&self.device, "driver_config."),
139 FlattenEntity::Point => (&self.point, "driver_config."),
140 FlattenEntity::Action => (&self.action, "driver_config."),
141 FlattenEntity::DevicePoints => unreachable!(),
142 };
143
144 Self::push_base_columns(entity, locale, &mut columns);
146 Self::flatten_nodes(
147 nodes,
148 prefix,
149 locale,
150 &mut columns,
151 &mut discriminator_keys,
152 None,
153 None,
154 );
155 }
156 }
157
158 DriverEntityTemplate {
159 columns,
160 discriminator_keys,
161 }
162 }
163
164 fn flatten_nodes(
165 nodes: &[Node],
166 prefix: &str,
167 locale: &str,
168 out: &mut Vec<FlattenColumn>,
169 discriminators: &mut Vec<String>,
170 union_discriminator: Option<&str>,
171 union_case_value: Option<&serde_json::Value>,
172 ) {
173 for n in nodes {
174 match n {
175 Node::Field(field) => {
176 let label = field.label.resolve(locale);
177 let key = format!("{}{}", prefix, field.path);
178
179 let enum_items_localized = match &field.data_type {
180 UiDataType::Enum { items } => {
181 Some(UiDataType::localize_enum_items(items, locale))
182 }
183 _ => None,
184 };
185
186 out.push(FlattenColumn {
187 key,
188 label,
189 data_type: field.data_type.clone(),
190 rules: field.rules.clone(),
191 when: field.when.clone(),
192 union_discriminator: union_discriminator.map(|s| s.to_string()),
193 union_case_value: union_case_value.cloned(),
194 enum_items_localized,
195 });
196 }
197 Node::Group(g) => {
198 Self::flatten_nodes(
199 &g.children,
200 prefix,
201 locale,
202 out,
203 discriminators,
204 union_discriminator,
205 union_case_value,
206 );
207 }
208 Node::Union(u) => {
209 let discr_key = format!("{}{}", prefix, u.discriminator);
210 discriminators.push(discr_key.clone());
211
212 out.push(FlattenColumn {
215 key: discr_key.clone(),
216 label: u.discriminator.clone(),
217 data_type: UiDataType::String,
218 rules: None,
219 when: None,
220 union_discriminator: None,
221 union_case_value: None,
222 enum_items_localized: None,
223 });
224
225 for case in u.mapping.iter() {
226 Self::flatten_nodes(
227 &case.children,
228 prefix,
229 locale,
230 out,
231 discriminators,
232 Some(&discr_key),
233 Some(&case.case_value),
234 );
235 }
236 }
237 }
238 }
239 }
240
241 fn push_base_columns(entity: FlattenEntity, locale: &str, out: &mut Vec<FlattenColumn>) {
244 match entity {
245 FlattenEntity::Device => {
246 out.push(FlattenColumn {
248 key: "device_name".to_string(),
249 label: Self::loc(locale, "设备名称", "Device Name"),
250 data_type: UiDataType::String,
251 rules: Some(Rules {
252 required: Some(RuleValue::Value(true)),
253 ..Default::default()
254 }),
255 when: None,
256 union_discriminator: None,
257 union_case_value: None,
258 enum_items_localized: None,
259 });
260 out.push(FlattenColumn {
262 key: "device_type".to_string(),
263 label: Self::loc(locale, "设备类型", "Device Type"),
264 data_type: UiDataType::String,
265 rules: Some(Rules {
266 required: Some(RuleValue::Value(true)),
267 ..Default::default()
268 }),
269 when: None,
270 union_discriminator: None,
271 union_case_value: None,
272 enum_items_localized: None,
273 });
274 }
275 FlattenEntity::Point => {
276 out.push(FlattenColumn {
278 key: "name".to_string(),
279 label: Self::loc(locale, "名称", "Name"),
280 data_type: UiDataType::String,
281 rules: Some(Rules {
282 required: Some(RuleValue::Value(true)),
283 ..Default::default()
284 }),
285 when: None,
286 union_discriminator: None,
287 union_case_value: None,
288 enum_items_localized: None,
289 });
290 out.push(FlattenColumn {
292 key: "key".to_string(),
293 label: Self::loc(locale, "键名", "Key"),
294 data_type: UiDataType::String,
295 rules: Some(Rules {
296 required: Some(RuleValue::Value(true)),
297 ..Default::default()
298 }),
299 when: None,
300 union_discriminator: None,
301 union_case_value: None,
302 enum_items_localized: None,
303 });
304 out.push(FlattenColumn {
306 key: "type".to_string(),
307 label: Self::loc(locale, "类型", "Type"),
308 data_type: UiDataType::Enum {
309 items: Self::enum_items_datapoint_type(locale),
310 },
311 rules: Some(Rules {
312 required: Some(RuleValue::Value(true)),
313 ..Default::default()
314 }),
315 when: None,
316 union_discriminator: None,
317 union_case_value: None,
318 enum_items_localized: None,
319 });
320 out.push(FlattenColumn {
322 key: "data_type".to_string(),
323 label: Self::loc(locale, "数据类型", "Data Type"),
324 data_type: UiDataType::Enum {
325 items: Self::enum_items_data_type(locale),
326 },
327 rules: Some(Rules {
328 required: Some(RuleValue::Value(true)),
329 ..Default::default()
330 }),
331 when: None,
332 union_discriminator: None,
333 union_case_value: None,
334 enum_items_localized: None,
335 });
336 out.push(FlattenColumn {
338 key: "access_mode".to_string(),
339 label: Self::loc(locale, "访问模式", "Access Mode"),
340 data_type: UiDataType::Enum {
341 items: Self::enum_items_access_mode(locale),
342 },
343 rules: Some(Rules {
344 required: Some(RuleValue::Value(true)),
345 ..Default::default()
346 }),
347 when: None,
348 union_discriminator: None,
349 union_case_value: None,
350 enum_items_localized: None,
351 });
352 out.push(FlattenColumn {
354 key: "unit".to_string(),
355 label: Self::loc(locale, "单位", "Unit"),
356 data_type: UiDataType::String,
357 rules: None,
358 when: None,
359 union_discriminator: None,
360 union_case_value: None,
361 enum_items_localized: None,
362 });
363 for (k, l_en, l_zh) in [
365 ("min_value", "Min Value", "最小值"),
366 ("max_value", "Max Value", "最大值"),
367 ] {
368 out.push(FlattenColumn {
369 key: k.to_string(),
370 label: Self::loc(locale, l_zh, l_en),
371 data_type: UiDataType::Float,
372 rules: None,
373 when: None,
374 union_discriminator: None,
375 union_case_value: None,
376 enum_items_localized: None,
377 });
378 }
379
380 out.push(FlattenColumn {
382 key: "transform_data_type".to_string(),
383 label: Self::loc(locale, "逻辑数据类型", "Logical Data Type"),
384 data_type: UiDataType::Enum {
385 items: Self::enum_items_data_type(locale),
386 },
387 rules: None,
388 when: None,
389 union_discriminator: None,
390 union_case_value: None,
391 enum_items_localized: None,
392 });
393 for (k, l_en, l_zh, dt) in [
394 (
395 "transform_scale",
396 "Transform Scale",
397 "缩放比例",
398 UiDataType::Float,
399 ),
400 (
401 "transform_offset",
402 "Transform Offset",
403 "偏移量",
404 UiDataType::Float,
405 ),
406 (
407 "transform_negate",
408 "Transform Negate",
409 "取反",
410 UiDataType::Boolean,
411 ),
412 ] {
413 out.push(FlattenColumn {
414 key: k.to_string(),
415 label: Self::loc(locale, l_zh, l_en),
416 data_type: dt,
417 rules: None,
418 when: None,
419 union_discriminator: None,
420 union_case_value: None,
421 enum_items_localized: None,
422 });
423 }
424 }
425 FlattenEntity::Action => {
426 out.push(FlattenColumn {
428 key: "action_name".to_string(),
429 label: Self::loc(locale, "动作名称", "Action Name"),
430 data_type: UiDataType::String,
431 rules: Some(Rules {
432 required: Some(RuleValue::Value(true)),
433 ..Default::default()
434 }),
435 when: None,
436 union_discriminator: None,
437 union_case_value: None,
438 enum_items_localized: None,
439 });
440 out.push(FlattenColumn {
441 key: "command".to_string(),
442 label: Self::loc(locale, "命令", "Command"),
443 data_type: UiDataType::String,
444 rules: Some(Rules {
445 required: Some(RuleValue::Value(true)),
446 ..Default::default()
447 }),
448 when: None,
449 union_discriminator: None,
450 union_case_value: None,
451 enum_items_localized: None,
452 });
453 out.push(FlattenColumn {
455 key: "param_name".to_string(),
456 label: Self::loc(locale, "参数名称", "Param Name"),
457 data_type: UiDataType::String,
458 rules: Some(Rules {
459 required: Some(RuleValue::Value(true)),
460 ..Default::default()
461 }),
462 when: None,
463 union_discriminator: None,
464 union_case_value: None,
465 enum_items_localized: None,
466 });
467 out.push(FlattenColumn {
468 key: "param_key".to_string(),
469 label: Self::loc(locale, "参数键名", "Param Key"),
470 data_type: UiDataType::String,
471 rules: Some(Rules {
472 required: Some(RuleValue::Value(true)),
473 ..Default::default()
474 }),
475 when: None,
476 union_discriminator: None,
477 union_case_value: None,
478 enum_items_localized: None,
479 });
480 out.push(FlattenColumn {
481 key: "param_data_type".to_string(),
482 label: Self::loc(locale, "数据类型", "Param Data Type"),
483 data_type: UiDataType::Enum {
484 items: Self::enum_items_data_type(locale),
485 },
486 rules: Some(Rules {
487 required: Some(RuleValue::Value(true)),
488 ..Default::default()
489 }),
490 when: None,
491 union_discriminator: None,
492 union_case_value: None,
493 enum_items_localized: None,
494 });
495 out.push(FlattenColumn {
496 key: "param_required".to_string(),
497 label: Self::loc(locale, "是否必填", "Required"),
498 data_type: UiDataType::Boolean,
499 rules: Some(Rules {
500 required: Some(RuleValue::Value(true)),
501 ..Default::default()
502 }),
503 when: None,
504 union_discriminator: None,
505 union_case_value: None,
506 enum_items_localized: None,
507 });
508 out.push(FlattenColumn {
509 key: "param_default_value".to_string(),
510 label: Self::loc(locale, "默认值", "Default Value"),
511 data_type: UiDataType::Any,
512 rules: None,
513 when: None,
514 union_discriminator: None,
515 union_case_value: None,
516 enum_items_localized: None,
517 });
518 out.push(FlattenColumn {
519 key: "param_min_value".to_string(),
520 label: Self::loc(locale, "最小值", "Min Value"),
521 data_type: UiDataType::Float,
522 rules: None,
523 when: None,
524 union_discriminator: None,
525 union_case_value: None,
526 enum_items_localized: None,
527 });
528 out.push(FlattenColumn {
529 key: "param_max_value".to_string(),
530 label: Self::loc(locale, "最大值", "Max Value"),
531 data_type: UiDataType::Float,
532 rules: None,
533 when: None,
534 union_discriminator: None,
535 union_case_value: None,
536 enum_items_localized: None,
537 });
538
539 out.push(FlattenColumn {
541 key: "param_transform_data_type".to_string(),
542 label: Self::loc(locale, "逻辑数据类型", "Logical Data Type"),
543 data_type: UiDataType::Enum {
544 items: Self::enum_items_data_type(locale),
545 },
546 rules: None,
547 when: None,
548 union_discriminator: None,
549 union_case_value: None,
550 enum_items_localized: None,
551 });
552 for (k, l_en, l_zh, dt) in [
553 (
554 "param_transform_scale",
555 "Transform Scale",
556 "缩放比例",
557 UiDataType::Float,
558 ),
559 (
560 "param_transform_offset",
561 "Transform Offset",
562 "偏移量",
563 UiDataType::Float,
564 ),
565 (
566 "param_transform_negate",
567 "Transform Negate",
568 "取反",
569 UiDataType::Boolean,
570 ),
571 ] {
572 out.push(FlattenColumn {
573 key: k.to_string(),
574 label: Self::loc(locale, l_zh, l_en),
575 data_type: dt,
576 rules: None,
577 when: None,
578 union_discriminator: None,
579 union_case_value: None,
580 enum_items_localized: None,
581 });
582 }
583 }
584 FlattenEntity::DevicePoints => {
585 unreachable!("DevicePoints should not call push_base_columns directly")
589 }
590 }
591 }
592
593 #[inline]
595 fn loc(locale: &str, zh_cn: &str, en: &str) -> String {
596 if locale.eq_ignore_ascii_case("zh-CN") || locale.eq_ignore_ascii_case("zh") {
597 zh_cn.to_string()
598 } else {
599 en.to_string()
600 }
601 }
602
603 fn enum_items_datapoint_type(_locale: &str) -> Vec<EnumItem> {
605 vec![
606 EnumItem {
607 key: serde_json::Value::from(0),
608 label: UiText::Localized {
609 locales: BTreeMap::from([
610 ("zh-CN".to_string(), "属性".to_string()),
611 ("en".to_string(), "Attribute".to_string()),
612 ]),
613 },
614 },
615 EnumItem {
616 key: serde_json::Value::from(1),
617 label: UiText::Localized {
618 locales: BTreeMap::from([
619 ("zh-CN".to_string(), "遥测".to_string()),
620 ("en".to_string(), "Telemetry".to_string()),
621 ]),
622 },
623 },
624 ]
625 }
626
627 fn enum_items_access_mode(_locale: &str) -> Vec<EnumItem> {
629 vec![
630 EnumItem {
631 key: serde_json::Value::from(0),
632 label: UiText::Localized {
633 locales: BTreeMap::from([
634 ("zh-CN".to_string(), "只读".to_string()),
635 ("en".to_string(), "Read".to_string()),
636 ]),
637 },
638 },
639 EnumItem {
640 key: serde_json::Value::from(1),
641 label: UiText::Localized {
642 locales: BTreeMap::from([
643 ("zh-CN".to_string(), "只写".to_string()),
644 ("en".to_string(), "Write".to_string()),
645 ]),
646 },
647 },
648 EnumItem {
649 key: serde_json::Value::from(2),
650 label: UiText::Localized {
651 locales: BTreeMap::from([
652 ("zh-CN".to_string(), "读写".to_string()),
653 ("en".to_string(), "Read/Write".to_string()),
654 ]),
655 },
656 },
657 ]
658 }
659
660 fn enum_items_data_type(_locale: &str) -> Vec<EnumItem> {
662 vec![
663 (0, "Boolean", "Boolean"),
664 (1, "Int8", "Int8"),
665 (2, "UInt8", "UInt8"),
666 (3, "Int16", "Int16"),
667 (4, "UInt16", "UInt16"),
668 (5, "Int32", "Int32"),
669 (6, "UInt32", "UInt32"),
670 (7, "Int64", "Int64"),
671 (8, "UInt64", "UInt64"),
672 (9, "Float32", "Float32"),
673 (10, "Float64", "Float64"),
674 (11, "String", "String"),
675 (12, "Binary", "Binary"),
676 (13, "Timestamp", "Timestamp"),
677 ]
678 .into_iter()
679 .map(|(k, zh, en)| EnumItem {
680 key: serde_json::Value::from(k),
681 label: UiText::Localized {
682 locales: BTreeMap::from([
683 ("zh-CN".to_string(), zh.to_string()),
684 ("en".to_string(), en.to_string()),
685 ]),
686 },
687 })
688 .collect()
689 }
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct TemplateMetadata {
699 pub driver_type: String,
701 pub driver_version: Option<String>,
703 pub api_version: Option<String>,
705 pub entity: String,
707 pub locale: String,
709 pub schema_version: String,
711}
712
713impl TemplateMetadata {
714 pub fn validate(
715 &self,
716 expected_driver_type: &str,
717 expected_entity: FlattenEntity,
718 ) -> Result<(), DriverError> {
719 if self.driver_type != expected_driver_type {
720 return Err(DriverError::ExecutionError(format!(
721 "Driver type mismatch: expected {}, got {}",
722 expected_driver_type, self.driver_type
723 )));
724 }
725
726 let entity_str = expected_entity.to_string().to_ascii_lowercase();
727 if self.entity != entity_str {
728 return Err(DriverError::ExecutionError(format!(
729 "Entity mismatch: expected {}, got {}",
730 entity_str, self.entity
731 )));
732 }
733
734 Ok(())
735 }
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
739#[serde(tag = "kind")]
740pub enum Node {
741 Field(Box<Field>),
742 Group(Group),
743 Union(Union),
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct Group {
748 pub id: String,
749 pub label: UiText,
750 pub description: Option<UiText>,
751 pub collapsible: bool,
752 pub order: Option<i32>,
753 pub children: Vec<Node>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub struct Union {
758 pub order: Option<i32>,
759 pub discriminator: String,
760 pub mapping: Vec<UnionCase>,
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
764pub struct UnionCase {
765 pub case_value: serde_json::Value,
766 pub children: Vec<Node>,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
770pub struct Field {
771 pub path: String,
772 pub label: UiText,
773 pub data_type: UiDataType,
774 pub default_value: Option<serde_json::Value>,
775 pub order: Option<i32>,
776 pub ui: Option<UiProps>,
777 pub rules: Option<Rules>,
778 pub when: Option<Vec<When>>,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(tag = "kind")]
783pub enum UiDataType {
784 String,
785 Integer,
786 Float,
787 Boolean,
788 Enum { items: Vec<EnumItem> },
789 Any,
790}
791
792impl UiDataType {
793 #[inline]
795 pub fn localize_enum_items(
796 items: &[EnumItem],
797 locale: &str,
798 ) -> Vec<(serde_json::Value, String)> {
799 items.iter().map(|i| i.localize(locale)).collect()
800 }
801
802 #[inline]
804 pub fn find_enum_key_by_label(
805 items: &[EnumItem],
806 locale: &str,
807 label: &str,
808 ) -> Option<serde_json::Value> {
809 items
810 .iter()
811 .find(|i| i.label.resolve(locale) == label)
812 .map(|i| i.key.clone())
813 }
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize)]
817pub struct EnumItem {
818 pub key: serde_json::Value,
819 pub label: UiText,
820}
821
822impl EnumItem {
823 #[inline]
825 pub fn localize(&self, locale: &str) -> (serde_json::Value, String) {
826 (self.key.clone(), self.label.resolve(locale))
827 }
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize, Default)]
831pub struct UiProps {
832 pub placeholder: Option<UiText>,
833 pub help: Option<UiText>,
834 pub prefix: Option<String>,
835 pub suffix: Option<String>,
836 pub col_span: Option<u8>,
837 pub read_only: Option<bool>,
838 pub disabled: Option<bool>,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize, Default)]
842pub struct Rules {
843 pub required: Option<RuleValue<bool>>,
845 pub min: Option<RuleValue<f64>>,
847 pub max: Option<RuleValue<f64>>,
848 pub min_length: Option<RuleValue<u32>>,
850 pub max_length: Option<RuleValue<u32>>,
851 pub pattern: Option<RuleValue<String>>,
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize)]
858#[serde(untagged)]
859pub enum RuleValue<T> {
860 Value(T),
862 WithMessage { value: T, message: Option<UiText> },
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
867pub struct When {
868 pub target: String,
869 pub operator: Operator,
870 pub value: serde_json::Value,
871 pub effect: WhenEffect,
872}
873
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
875pub enum Operator {
876 Eq,
877 Neq,
878 Gt,
879 Gte,
880 Lt,
881 Lte,
882 Contains,
883 Prefix,
884 Suffix,
885 Regex,
886 In,
887 NotIn,
888 Between,
889 NotBetween,
890 NotNull,
891}
892
893#[derive(Debug, Clone, Serialize, Deserialize)]
894pub enum WhenEffect {
895 If,
902 IfNot,
904 Visible,
905 Invisible,
906 Enable,
907 Disable,
908 Require,
909 Optional,
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize)]
913#[serde(tag = "kind", rename_all = "camelCase")]
914pub enum UiText {
915 Simple { value: String },
916 Localized { locales: BTreeMap<String, String> },
917}
918
919impl From<String> for UiText {
920 fn from(value: String) -> Self {
921 UiText::Simple { value }
922 }
923}
924
925impl From<&str> for UiText {
926 fn from(value: &str) -> Self {
927 UiText::Simple {
928 value: value.to_string(),
929 }
930 }
931}
932
933impl UiText {
934 pub fn resolve(&self, locale: &str) -> String {
935 match self {
936 UiText::Simple { value } => value.clone(),
937 UiText::Localized { locales } => locales.get(locale).cloned().unwrap_or_default(),
938 }
939 }
940}
941
942#[derive(Debug, Clone, Copy, PartialEq, Eq)]
944pub enum FlattenEntity {
945 Device,
946 Point,
947 Action,
948 DevicePoints,
949}
950
951impl Display for FlattenEntity {
952 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
953 match self {
954 FlattenEntity::Device => write!(f, "device"),
955 FlattenEntity::Point => write!(f, "point"),
956 FlattenEntity::Action => write!(f, "action"),
957 FlattenEntity::DevicePoints => write!(f, "device-points"),
958 }
959 }
960}
961
962impl TryFrom<&str> for FlattenEntity {
963 type Error = DriverError;
964 fn try_from(value: &str) -> Result<Self, Self::Error> {
965 match value.to_ascii_lowercase().as_str() {
966 "device" => Ok(FlattenEntity::Device),
967 "point" => Ok(FlattenEntity::Point),
968 "action" => Ok(FlattenEntity::Action),
969 "device-points" => Ok(FlattenEntity::DevicePoints),
970 _ => Err(DriverError::InvalidEntity(value.to_string())),
971 }
972 }
973}
974
975#[derive(Debug)]
977pub struct ValidateRowContext<'a> {
978 pub row_index: usize,
980 pub locale: &'a str,
982 pub values: &'a serde_json::Map<String, serde_json::Value>,
984}
985
986#[derive(Debug, Clone)]
988pub struct FlattenColumn {
989 pub key: String,
991 pub label: String,
993 pub data_type: UiDataType,
995 pub rules: Option<Rules>,
997 pub when: Option<Vec<When>>,
999 pub union_discriminator: Option<String>,
1001 pub union_case_value: Option<serde_json::Value>,
1003 pub enum_items_localized: Option<Vec<(serde_json::Value, String)>>,
1005}
1006
1007impl FlattenColumn {
1008 #[inline]
1009 fn evaluate_when(
1010 op: &Operator,
1011 lhs: Option<&serde_json::Value>,
1012 rhs: &serde_json::Value,
1013 ) -> bool {
1014 match op {
1015 Operator::NotNull => lhs.is_some() && !matches!(lhs, Some(serde_json::Value::Null)),
1016 Operator::Eq => Self::compare(lhs, rhs, |o| o == 0),
1017 Operator::Neq => Self::compare(lhs, rhs, |o| o != 0),
1018 Operator::Gt => Self::compare(lhs, rhs, |o| o > 0),
1019 Operator::Gte => Self::compare(lhs, rhs, |o| o >= 0),
1020 Operator::Lt => Self::compare(lhs, rhs, |o| o < 0),
1021 Operator::Lte => Self::compare(lhs, rhs, |o| o <= 0),
1022 Operator::Contains => Self::contains(lhs, rhs),
1023 Operator::Prefix => Self::prefix(lhs, rhs),
1024 Operator::Suffix => Self::suffix(lhs, rhs),
1025 Operator::Regex => Self::regex(lhs, rhs),
1026 Operator::In => Self::in_list(lhs, rhs, true),
1027 Operator::NotIn => Self::in_list(lhs, rhs, false),
1028 Operator::Between => Self::between(lhs, rhs, true),
1029 Operator::NotBetween => Self::between(lhs, rhs, false),
1030 }
1031 }
1032
1033 #[inline]
1034 fn loosely_equal(a: &serde_json::Value, b: &serde_json::Value) -> bool {
1035 match (a, b) {
1036 (serde_json::Value::String(x), serde_json::Value::String(y)) => x == y,
1037 (serde_json::Value::Bool(x), serde_json::Value::Bool(y)) => x == y,
1038 (serde_json::Value::Number(x), serde_json::Value::Number(y)) => {
1039 x.to_string() == y.to_string()
1040 }
1041 _ => a == b,
1042 }
1043 }
1044
1045 #[inline]
1046 fn compare<F>(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value, f: F) -> bool
1047 where
1048 F: FnOnce(i8) -> bool,
1049 {
1050 if let Some(l) = lhs {
1051 if let (Some(a), Some(b)) = (Self::to_f64(l), Self::to_f64(rhs)) {
1052 return f(a
1053 .partial_cmp(&b)
1054 .map(|o| match o {
1055 std::cmp::Ordering::Less => -1,
1056 std::cmp::Ordering::Equal => 0,
1057 std::cmp::Ordering::Greater => 1,
1058 })
1059 .unwrap_or(1));
1060 }
1061 if let (Some(a), Some(b)) = (Self::to_string(l), Self::to_string(rhs)) {
1062 return f(a.cmp(&b) as i8);
1063 }
1064 if let (Some(a), Some(b)) = (Self::to_bool(l), Self::to_bool(rhs)) {
1065 return f((a as i8) - (b as i8));
1066 }
1067 }
1068 false
1069 }
1070
1071 #[inline]
1072 fn to_f64(v: &serde_json::Value) -> Option<f64> {
1073 match v {
1074 serde_json::Value::Number(n) => n.as_f64(),
1075 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1076 serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
1077 _ => None,
1078 }
1079 }
1080
1081 #[inline]
1082 fn to_string(v: &serde_json::Value) -> Option<String> {
1083 match v {
1084 serde_json::Value::String(s) => Some(s.clone()),
1085 serde_json::Value::Number(n) => Some(n.to_string()),
1086 serde_json::Value::Bool(b) => Some(b.to_string()),
1087 _ => None,
1088 }
1089 }
1090
1091 #[inline]
1092 fn to_bool(v: &serde_json::Value) -> Option<bool> {
1093 match v {
1094 serde_json::Value::Bool(b) => Some(*b),
1095 serde_json::Value::String(s) => match s.as_str() {
1096 "true" | "1" | "yes" | "on" => Some(true),
1097 "false" | "0" | "no" | "off" => Some(false),
1098 _ => None,
1099 },
1100 serde_json::Value::Number(n) => Some(n.as_f64().unwrap_or(0.0) != 0.0),
1101 _ => None,
1102 }
1103 }
1104
1105 #[inline]
1106 fn contains(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value) -> bool {
1107 match (lhs, rhs) {
1108 (Some(serde_json::Value::String(a)), serde_json::Value::String(b)) => a.contains(b),
1109 (Some(serde_json::Value::Array(arr)), _) => {
1110 arr.iter().any(|v| Self::loosely_equal(v, rhs))
1111 }
1112 _ => false,
1113 }
1114 }
1115
1116 #[inline]
1117 fn prefix(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value) -> bool {
1118 match (lhs, rhs) {
1119 (Some(serde_json::Value::String(a)), serde_json::Value::String(b)) => a.starts_with(b),
1120 _ => false,
1121 }
1122 }
1123
1124 #[inline]
1125 fn suffix(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value) -> bool {
1126 match (lhs, rhs) {
1127 (Some(serde_json::Value::String(a)), serde_json::Value::String(b)) => a.ends_with(b),
1128 _ => false,
1129 }
1130 }
1131
1132 #[inline]
1133 fn regex(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value) -> bool {
1134 let (target, pattern_spec) = match (lhs, rhs) {
1136 (Some(serde_json::Value::String(s)), serde_json::Value::String(p)) => (s, p),
1137 _ => return false,
1138 };
1139
1140 #[inline]
1144 fn build_regex(spec: &str) -> Option<regex::Regex> {
1145 if spec.starts_with('/') {
1146 if let Some(end) = spec.rfind('/') {
1148 if end > 0 {
1149 let pat = &spec[1..end];
1150 let flags = &spec[end + 1..];
1151 let mut mods = String::new();
1152 for ch in flags.chars() {
1153 match ch {
1154 'i' | 'm' | 's' | 'x' => mods.push(ch),
1156 _ => {}
1158 }
1159 }
1160 let final_pat = if mods.is_empty() {
1161 pat.to_string()
1162 } else {
1163 format!("(?{}){}", mods, pat)
1164 };
1165 return regex::Regex::new(&final_pat).ok();
1166 }
1167 }
1168 }
1169 regex::Regex::new(spec).ok()
1171 }
1172
1173 if let Some(re) = build_regex(pattern_spec) {
1174 re.is_match(target)
1175 } else {
1176 false
1177 }
1178 }
1179
1180 #[inline]
1181 fn in_list(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value, positive: bool) -> bool {
1182 let mut found = false;
1183 match (lhs, rhs) {
1184 (Some(lv), serde_json::Value::Array(arr)) => {
1185 for v in arr {
1186 if Self::loosely_equal(lv, v) {
1187 found = true;
1188 break;
1189 }
1190 }
1191 }
1192 (Some(lv), serde_json::Value::String(s)) => {
1193 for part in s.split(',') {
1195 if Self::to_string(lv).as_deref() == Some(part.trim()) {
1196 found = true;
1197 break;
1198 }
1199 }
1200 }
1201 _ => {}
1202 }
1203 if positive {
1204 found
1205 } else {
1206 !found
1207 }
1208 }
1209
1210 #[inline]
1211 fn between(lhs: Option<&serde_json::Value>, rhs: &serde_json::Value, positive: bool) -> bool {
1212 if let (Some(a), serde_json::Value::Array(arr)) = (lhs, rhs) {
1213 if arr.len() == 2 {
1214 if let (Some(v), Some(min), Some(max)) = (
1215 Self::to_f64(a),
1216 Self::to_f64(&arr[0]),
1217 Self::to_f64(&arr[1]),
1218 ) {
1219 let ok = v >= min && v <= max;
1220 return if positive { ok } else { !ok };
1221 }
1222 }
1223 }
1224 false
1225 }
1226
1227 #[inline]
1236 pub fn validate(
1237 &self,
1238 ctx: &ValidateRowContext<'_>,
1239 ) -> Result<Option<serde_json::Value>, FieldError> {
1240 let required = self.resolve_required(ctx);
1242
1243 if !self.union_applicable(ctx) {
1245 return Ok(None);
1246 }
1247
1248 let raw_opt = self.read_and_trim_value(ctx, required)?;
1250 let raw = match raw_opt {
1251 Some(v) => v,
1252 None => return Ok(None),
1253 };
1254
1255 let normalized = self.normalize_value_for_type(&raw, ctx)?;
1257
1258 self.validate_rules_on(&normalized, ctx)?;
1260
1261 Ok(Some(normalized))
1262 }
1263
1264 #[inline]
1265 fn resolve_required(&self, ctx: &ValidateRowContext<'_>) -> bool {
1266 let mut required = match &self.rules.as_ref().and_then(|r| r.required.as_ref()) {
1267 Some(RuleValue::Value(v)) => *v,
1268 Some(RuleValue::WithMessage { value, .. }) => *value,
1269 None => false,
1270 };
1271 if let Some(list) = &self.when {
1272 for w in list.iter() {
1273 if Self::evaluate_when(
1274 &w.operator,
1275 DriverEntityTemplate::get_value_by_path(ctx.values, &w.target),
1276 &w.value,
1277 ) {
1278 match w.effect {
1279 WhenEffect::Require => required = true,
1280 WhenEffect::Optional => required = false,
1281 _ => {}
1282 }
1283 }
1284 }
1285 }
1286 required
1287 }
1288
1289 #[inline]
1290 fn union_applicable(&self, ctx: &ValidateRowContext<'_>) -> bool {
1291 if let (Some(discr), Some(case_val)) = (&self.union_discriminator, &self.union_case_value) {
1292 matches!(
1293 DriverEntityTemplate::get_value_by_path(ctx.values, discr),
1294 Some(v) if Self::loosely_equal(v, case_val)
1295 )
1296 } else {
1297 true
1298 }
1299 }
1300
1301 #[inline]
1302 fn read_and_trim_value(
1303 &self,
1304 ctx: &ValidateRowContext<'_>,
1305 required: bool,
1306 ) -> Result<Option<serde_json::Value>, FieldError> {
1307 use serde_json::Value as Json;
1308 match DriverEntityTemplate::get_value_by_path(ctx.values, &self.key) {
1309 Some(v) => {
1310 if let Json::String(s) = v {
1311 let trimmed = s.trim();
1312 if trimmed.is_empty() {
1313 if required {
1314 return Err(FieldError {
1315 row: ctx.row_index + 2,
1316 field: self.key.clone(),
1317 code: ValidationCode::Required,
1318 message: format!("{} is required", self.label),
1319 });
1320 } else {
1321 return Ok(None);
1322 }
1323 }
1324 Ok(Some(Json::String(trimmed.to_string())))
1326 } else {
1327 Ok(Some(v.clone()))
1328 }
1329 }
1330 None => {
1331 if required {
1332 Err(FieldError {
1333 row: ctx.row_index + 2,
1334 field: self.key.clone(),
1335 code: ValidationCode::Required,
1336 message: format!("{} is required", self.label),
1337 })
1338 } else {
1339 Ok(None)
1340 }
1341 }
1342 }
1343 }
1344
1345 #[inline]
1346 fn normalize_value_for_type(
1347 &self,
1348 value: &serde_json::Value,
1349 ctx: &ValidateRowContext<'_>,
1350 ) -> Result<serde_json::Value, FieldError> {
1351 use serde_json::Value as Json;
1352 let normalized = match &self.data_type {
1353 UiDataType::String => match value {
1354 Json::String(s) => Json::String(s.clone()),
1355 _ => match Self::to_string(value) {
1356 Some(s) => Json::String(s),
1357 None => {
1358 return Err(FieldError {
1359 row: ctx.row_index + 2,
1360 field: self.key.clone(),
1361 code: ValidationCode::TypeMismatch,
1362 message: format!("{} type mismatch (string)", self.label),
1363 })
1364 }
1365 },
1366 },
1367 UiDataType::Integer => match value {
1368 Json::Number(n) if n.as_i64().is_some() => value.clone(),
1369 Json::String(s) => match s.parse::<i64>() {
1370 Ok(n) => Json::from(n),
1371 Err(_) => {
1372 return Err(FieldError {
1373 row: ctx.row_index + 2,
1374 field: self.key.clone(),
1375 code: ValidationCode::TypeMismatch,
1376 message: format!("{} type mismatch (integer)", self.label),
1377 })
1378 }
1379 },
1380 _ => {
1381 return Err(FieldError {
1382 row: ctx.row_index + 2,
1383 field: self.key.clone(),
1384 code: ValidationCode::TypeMismatch,
1385 message: format!("{} type mismatch (integer)", self.label),
1386 })
1387 }
1388 },
1389 UiDataType::Float => match value {
1390 Json::Number(n) if n.as_f64().is_some() => value.clone(),
1391 Json::String(s) => match s.parse::<f64>() {
1392 Ok(n) => Json::from(n),
1393 Err(_) => {
1394 return Err(FieldError {
1395 row: ctx.row_index + 2,
1396 field: self.key.clone(),
1397 code: ValidationCode::TypeMismatch,
1398 message: format!("{} type mismatch (float)", self.label),
1399 })
1400 }
1401 },
1402 _ => {
1403 return Err(FieldError {
1404 row: ctx.row_index + 2,
1405 field: self.key.clone(),
1406 code: ValidationCode::TypeMismatch,
1407 message: format!("{} type mismatch (float)", self.label),
1408 })
1409 }
1410 },
1411 UiDataType::Boolean => match value {
1412 Json::Bool(_) => value.clone(),
1413 Json::String(s) => match s.to_ascii_lowercase().as_str() {
1414 "true" | "1" | "yes" | "on" | "y" | "t" | "是" => Json::Bool(true),
1415 "false" | "0" | "no" | "off" | "n" | "f" | "否" => Json::Bool(false),
1416 _ => {
1417 return Err(FieldError {
1418 row: ctx.row_index + 2,
1419 field: self.key.clone(),
1420 code: ValidationCode::TypeMismatch,
1421 message: format!("{} type mismatch (boolean)", self.label),
1422 })
1423 }
1424 },
1425 _ => {
1426 return Err(FieldError {
1427 row: ctx.row_index + 2,
1428 field: self.key.clone(),
1429 code: ValidationCode::TypeMismatch,
1430 message: format!("{} type mismatch (boolean)", self.label),
1431 })
1432 }
1433 },
1434 UiDataType::Enum { items } => {
1435 if items.iter().any(|it| it.key == *value) {
1436 value.clone()
1437 } else {
1438 match value {
1439 Json::String(s) => {
1440 let s_lower = s.to_ascii_lowercase();
1441 if let Some((k, _)) =
1442 self.enum_items_localized.as_ref().and_then(|pairs| {
1443 pairs
1444 .iter()
1445 .find(|(_, l)| l.to_ascii_lowercase() == s_lower)
1446 })
1447 {
1448 k.clone()
1449 } else {
1450 let hit = items.iter().find(|it| match &it.key {
1451 Json::String(ks) => ks.eq_ignore_ascii_case(s),
1452 Json::Number(n) => n.to_string().eq_ignore_ascii_case(s),
1453 Json::Bool(b) => b.to_string().eq_ignore_ascii_case(s),
1454 _ => false,
1455 });
1456 if let Some(h) = hit {
1457 h.key.clone()
1458 } else {
1459 return Err(FieldError {
1460 row: ctx.row_index + 2,
1461 field: self.key.clone(),
1462 code: ValidationCode::TypeMismatch,
1463 message: format!("{} not in enum", self.label),
1464 });
1465 }
1466 }
1467 }
1468 _ => {
1469 return Err(FieldError {
1470 row: ctx.row_index + 2,
1471 field: self.key.clone(),
1472 code: ValidationCode::TypeMismatch,
1473 message: format!("{} not in enum", self.label),
1474 })
1475 }
1476 }
1477 }
1478 }
1479 UiDataType::Any => value.clone(),
1480 };
1481 Ok(normalized)
1482 }
1483
1484 #[inline]
1485 fn validate_rules_on(
1486 &self,
1487 normalized: &serde_json::Value,
1488 ctx: &ValidateRowContext<'_>,
1489 ) -> Result<(), FieldError> {
1490 if let Some(rules) = &self.rules {
1491 if matches!(self.data_type, UiDataType::Integer | UiDataType::Float) {
1492 if let Some(v) = FlattenColumn::to_f64(normalized) {
1493 if let Some(rule) = &rules.min {
1494 let minv = match rule {
1495 RuleValue::Value(x) => *x,
1496 RuleValue::WithMessage { value, .. } => *value,
1497 };
1498 if v < minv {
1499 return Err(FieldError {
1500 row: ctx.row_index + 2,
1501 field: self.key.clone(),
1502 code: ValidationCode::Range,
1503 message: format!("{} < min {}", self.label, minv),
1504 });
1505 }
1506 }
1507 if let Some(rule) = &rules.max {
1508 let maxv = match rule {
1509 RuleValue::Value(x) => *x,
1510 RuleValue::WithMessage { value, .. } => *value,
1511 };
1512 if v > maxv {
1513 return Err(FieldError {
1514 row: ctx.row_index + 2,
1515 field: self.key.clone(),
1516 code: ValidationCode::Range,
1517 message: format!("{} > max {}", self.label, maxv),
1518 });
1519 }
1520 }
1521 }
1522 }
1523
1524 if matches!(self.data_type, UiDataType::String | UiDataType::Enum { .. }) {
1525 if let Some(s) = FlattenColumn::to_string(normalized) {
1526 if let Some(rule) = &rules.min_length {
1527 let minl = match rule {
1528 RuleValue::Value(x) => *x as usize,
1529 RuleValue::WithMessage { value, .. } => *value as usize,
1530 };
1531 if s.chars().count() < minl {
1532 return Err(FieldError {
1533 row: ctx.row_index + 2,
1534 field: self.key.clone(),
1535 code: ValidationCode::Length,
1536 message: format!("{} length < {}", self.label, minl),
1537 });
1538 }
1539 }
1540 if let Some(rule) = &rules.max_length {
1541 let maxl = match rule {
1542 RuleValue::Value(x) => *x as usize,
1543 RuleValue::WithMessage { value, .. } => *value as usize,
1544 };
1545 if s.chars().count() > maxl {
1546 return Err(FieldError {
1547 row: ctx.row_index + 2,
1548 field: self.key.clone(),
1549 code: ValidationCode::Length,
1550 message: format!("{} length > {}", self.label, maxl),
1551 });
1552 }
1553 }
1554 }
1555 }
1556
1557 if let Some(rule) = &rules.pattern {
1558 if let Some(s) = FlattenColumn::to_string(normalized) {
1559 let pat = match rule {
1560 RuleValue::Value(p) => p,
1561 RuleValue::WithMessage { value, .. } => value,
1562 };
1563 if let Ok(re) = regex::Regex::new(pat) {
1564 if !re.is_match(&s) {
1565 return Err(FieldError {
1566 row: ctx.row_index + 2,
1567 field: self.key.clone(),
1568 code: ValidationCode::Pattern,
1569 message: format!("{} does not match pattern", self.label),
1570 });
1571 }
1572 }
1573 }
1574 }
1575
1576 if matches!(self.data_type, UiDataType::Any) {
1578 if let Some(v) = FlattenColumn::to_f64(normalized) {
1579 if let Some(rule) = &rules.min {
1580 let minv = match rule {
1581 RuleValue::Value(x) => *x,
1582 RuleValue::WithMessage { value, .. } => *value,
1583 };
1584 if v < minv {
1585 return Err(FieldError {
1586 row: ctx.row_index + 2,
1587 field: self.key.clone(),
1588 code: ValidationCode::Range,
1589 message: format!("{} < min {}", self.label, minv),
1590 });
1591 }
1592 }
1593 if let Some(rule) = &rules.max {
1594 let maxv = match rule {
1595 RuleValue::Value(x) => *x,
1596 RuleValue::WithMessage { value, .. } => *value,
1597 };
1598 if v > maxv {
1599 return Err(FieldError {
1600 row: ctx.row_index + 2,
1601 field: self.key.clone(),
1602 code: ValidationCode::Range,
1603 message: format!("{} > max {}", self.label, maxv),
1604 });
1605 }
1606 }
1607 }
1608
1609 if let Some(s) = FlattenColumn::to_string(normalized) {
1610 if let Some(rule) = &rules.min_length {
1611 let minl = match rule {
1612 RuleValue::Value(x) => *x as usize,
1613 RuleValue::WithMessage { value, .. } => *value as usize,
1614 };
1615 if s.chars().count() < minl {
1616 return Err(FieldError {
1617 row: ctx.row_index + 2,
1618 field: self.key.clone(),
1619 code: ValidationCode::Length,
1620 message: format!("{} length < {}", self.label, minl),
1621 });
1622 }
1623 }
1624 if let Some(rule) = &rules.max_length {
1625 let maxl = match rule {
1626 RuleValue::Value(x) => *x as usize,
1627 RuleValue::WithMessage { value, .. } => *value as usize,
1628 };
1629 if s.chars().count() > maxl {
1630 return Err(FieldError {
1631 row: ctx.row_index + 2,
1632 field: self.key.clone(),
1633 code: ValidationCode::Length,
1634 message: format!("{} length > {}", self.label, maxl),
1635 });
1636 }
1637 }
1638
1639 if let Some(rule) = &rules.pattern {
1640 let pat = match rule {
1641 RuleValue::Value(p) => p,
1642 RuleValue::WithMessage { value, .. } => value,
1643 };
1644 if let Ok(re) = regex::Regex::new(pat) {
1645 if !re.is_match(&s) {
1646 return Err(FieldError {
1647 row: ctx.row_index + 2,
1648 field: self.key.clone(),
1649 code: ValidationCode::Pattern,
1650 message: format!("{} does not match pattern", self.label),
1651 });
1652 }
1653 }
1654 }
1655 }
1656 }
1657 }
1658 Ok(())
1659 }
1660}
1661
1662#[derive(Debug, Clone)]
1664pub struct DriverEntityTemplate {
1665 pub columns: Vec<FlattenColumn>,
1667 pub discriminator_keys: Vec<String>,
1669}
1670
1671impl DriverEntityTemplate {
1672 pub fn read_template_metadata<R>(reader: R) -> Result<TemplateMetadata, DriverError>
1675 where
1676 R: IoRead + IoSeek,
1677 {
1678 let mut workbook = calamine::Xlsx::new(reader)
1679 .map_err(|e| DriverError::ExecutionError(format!("xlsx open: {e}")))?;
1680 let meta_range = workbook
1681 .worksheet_range("__meta__")
1682 .map_err(|e| DriverError::ExecutionError(format!("xlsx read: {e}")))?;
1683
1684 let mut meta_map = BTreeMap::new();
1685 for (ri, row) in meta_range.rows().enumerate() {
1686 if ri == 0 {
1687 continue;
1688 }
1689 if row.len() >= 2 {
1690 let key = row[0].to_string();
1691 let val = row[1].to_string();
1692 if !key.trim().is_empty() {
1693 meta_map.insert(key, val);
1694 }
1695 }
1696 }
1697
1698 Ok(TemplateMetadata {
1699 driver_type: meta_map.get("driver_type").cloned().unwrap_or_default(),
1700 driver_version: meta_map
1701 .get("driver_version")
1702 .cloned()
1703 .filter(|s| !s.is_empty()),
1704 api_version: meta_map
1705 .get("api_version")
1706 .cloned()
1707 .filter(|s| !s.is_empty()),
1708 entity: meta_map.get("entity").cloned().unwrap_or_default(),
1709 locale: meta_map
1710 .get("locale")
1711 .cloned()
1712 .unwrap_or("zh-CN".to_string()),
1713 schema_version: meta_map
1714 .get("schema_version")
1715 .cloned()
1716 .unwrap_or("1.0".to_string()),
1717 })
1718 }
1719
1720 pub fn validate_and_normalize_rows(
1723 &self,
1724 rows: Vec<serde_json::Map<String, Json>>,
1725 locale: &str,
1726 ) -> (Vec<ValidatedRow>, Vec<FieldError>, usize) {
1727 let mut valids = Vec::with_capacity(rows.len());
1728 let mut errors = Vec::new();
1729 let warn_count = 0usize;
1730
1731 for (idx, row) in rows.into_iter().enumerate() {
1732 let mut normalized_row = row;
1734 let mut row_has_error = false;
1735
1736 for discr in &self.discriminator_keys {
1738 if normalized_row.get(discr).is_none() {
1739 errors.push(FieldError {
1740 row: idx + 2,
1741 field: discr.clone(),
1742 code: ValidationCode::DiscriminatorMissing,
1743 message: format!("missing discriminator: {discr}"),
1744 });
1745 row_has_error = true;
1746 }
1747 }
1748
1749 for col in &self.columns {
1750 let present_value_ref =
1753 DriverEntityTemplate::get_value_by_path(&normalized_row, &col.key);
1754 let ctx_row = ValidateRowContext {
1755 row_index: idx,
1756 locale,
1757 values: &normalized_row,
1758 };
1759
1760 match col.validate(&ctx_row) {
1761 Ok(Some(new_value)) => {
1762 let needs_update = match present_value_ref {
1763 Some(v) => v != &new_value,
1764 None => true,
1765 };
1766 if needs_update {
1767 Self::insert_nested(&mut normalized_row, &col.key, new_value);
1768 }
1769 }
1770 Ok(None) => {
1771 if present_value_ref.is_some() {
1772 let _ = Self::remove_by_path(&mut normalized_row, &col.key);
1773 }
1774 }
1775 Err(err) => {
1776 errors.push(err);
1777 row_has_error = true;
1778 }
1779 }
1780 }
1781
1782 if !row_has_error {
1783 valids.push(ValidatedRow {
1785 row_index: idx + 2,
1786 values: normalized_row,
1787 });
1788 }
1789 }
1790
1791 (valids, errors, warn_count)
1792 }
1793
1794 pub fn get_column(&self, key: &str) -> Option<&FlattenColumn> {
1796 self.columns.iter().find(|c| c.key == key)
1797 }
1798
1799 #[inline]
1801 fn read_rows_from_range(
1802 &self,
1803 locale: &str,
1804 range: calamine::Range<calamine::Data>,
1805 ) -> Result<Vec<serde_json::Map<String, Json>>, DriverError> {
1806 let mut rows_iter = range.rows();
1807 let header = match rows_iter.next() {
1809 Some(r) => r,
1810 None => return Ok(Vec::new()),
1811 };
1812
1813 let mut header_to_idx = Vec::with_capacity(header.len());
1815 for cell in header {
1816 let h = cell.to_string();
1817 let idx = self.columns.iter().position(|c| c.label == h);
1818 header_to_idx.push(idx);
1819 }
1820
1821 let mut out = Vec::new();
1822 for row in rows_iter {
1823 let mut obj = serde_json::Map::new();
1824 let mut has_any = false;
1825 for (ci, cell) in row.iter().enumerate() {
1826 if let Some(Some(pi)) = header_to_idx.get(ci) {
1827 let col = &self.columns[*pi];
1828 if let Some(v) = Self::map_cell_to_json(cell, col, locale) {
1829 obj.insert(col.key.clone(), v);
1830 has_any = true;
1831 }
1832 }
1833 }
1834 if has_any {
1835 Self::fold_special_prefixes(&mut obj);
1837 out.push(obj);
1838 }
1839 }
1840 Ok(out)
1841 }
1842
1843 #[inline]
1853 fn map_cell_to_json(cell: &calamine::Data, col: &FlattenColumn, locale: &str) -> Option<Json> {
1854 use calamine::Data as CData;
1855 use serde_json::Value as Json;
1856
1857 match cell {
1859 CData::Empty => return None,
1860 CData::String(s) if s.trim().is_empty() => return None,
1861 _ => {}
1862 }
1863
1864 match &col.data_type {
1865 UiDataType::String => Some(match cell {
1866 CData::String(s) => Json::String(s.clone()),
1867 CData::Int(n) => Json::String(n.to_string()),
1868 CData::Float(f) => Json::String(f.to_string()),
1869 CData::Bool(b) => Json::String(b.to_string()),
1870 CData::DateTimeIso(s) => Json::String(s.clone()),
1871 CData::DateTime(dt) => Json::String(dt.to_string()),
1872 CData::DurationIso(s) => Json::String(s.clone()),
1873 CData::Error(_) => Json::String(cell.to_string()),
1874 CData::Empty => return None,
1875 }),
1876
1877 UiDataType::Integer => match cell {
1878 CData::Int(n) => Some(Json::from(*n)),
1879 CData::Float(f) => Some(Json::from(*f as i64)),
1880 CData::Bool(b) => Some(Json::from(if *b { 1i64 } else { 0i64 })),
1881 CData::String(s) => {
1882 let st = s.trim();
1883 if let Ok(n) = st.parse::<i64>() {
1884 Some(Json::from(n))
1885 } else {
1886 None
1887 }
1888 }
1889 _ => None,
1890 },
1891
1892 UiDataType::Float => match cell {
1893 CData::Float(f) => Some(Json::from(*f)),
1894 CData::Int(n) => Some(Json::from(*n as f64)),
1895 CData::Bool(b) => Some(Json::from(if *b { 1.0 } else { 0.0 })),
1896 CData::String(s) => {
1897 let st = s.trim();
1898 if let Ok(n) = st.parse::<f64>() {
1899 Some(Json::from(n))
1900 } else {
1901 None
1902 }
1903 }
1904 _ => None,
1905 },
1906
1907 UiDataType::Boolean => match cell {
1908 CData::Bool(b) => Some(Json::Bool(*b)),
1909 CData::Int(n) => Some(Json::Bool(*n != 0)),
1910 CData::Float(f) => Some(Json::Bool(*f != 0.0)),
1911 CData::String(s) => match s.trim().to_ascii_lowercase().as_str() {
1912 "true" | "1" | "yes" | "on" | "y" | "t" | "是" => Some(Json::Bool(true)),
1913 "false" | "0" | "no" | "off" | "n" | "f" | "否" => Some(Json::Bool(false)),
1914 _ => None,
1915 },
1916 _ => None,
1917 },
1918
1919 UiDataType::Enum { items } => {
1920 if let CData::String(s) = cell {
1922 if let Some(k) = UiDataType::find_enum_key_by_label(items.as_slice(), locale, s)
1923 {
1924 return Some(k);
1925 }
1926 }
1927 let candidate = match cell {
1929 CData::Int(n) => Json::from(*n),
1930 CData::Float(f) => Json::from(*f),
1931 CData::Bool(b) => Json::from(*b),
1932 CData::String(s) => Json::String(s.clone()),
1933 _ => Json::String(cell.to_string()),
1934 };
1935 if items.iter().any(|it| it.key == candidate) {
1936 Some(candidate)
1937 } else {
1938 match cell {
1940 CData::String(s) => Some(Json::String(s.clone())),
1941 _ => None,
1942 }
1943 }
1944 }
1945
1946 UiDataType::Any => Some(match cell {
1947 CData::Int(n) => Json::from(*n),
1948 CData::Float(f) => Json::from(*f),
1949 CData::Bool(b) => Json::from(*b),
1950 CData::String(s) => {
1951 if let Some(v) = Self::parse_any_str(s) {
1952 v
1953 } else {
1954 Json::String(s.clone())
1955 }
1956 }
1957 CData::DateTimeIso(s) => Json::String(s.clone()),
1958 CData::DateTime(dt) => Json::String(dt.to_string()),
1959 CData::DurationIso(s) => Json::String(s.clone()),
1960 CData::Error(_) => Json::String(cell.to_string()),
1961 CData::Empty => return None,
1962 }),
1963 }
1964 }
1965
1966 #[inline]
1968 fn build_template_workbook(&self, locale: &str) -> Result<Workbook, DriverError> {
1969 let mut workbook = Workbook::new();
1970
1971 let mut enum_list_formulas = vec![None; self.columns.len()];
1973 for (i, col) in self.columns.iter().enumerate() {
1974 if let UiDataType::Enum { items } = &col.data_type {
1975 let localized = UiDataType::localize_enum_items(items, locale);
1976 let list = localized.into_iter().map(|(_, l)| l).collect::<Vec<_>>();
1977 if !list.is_empty() {
1978 let col_letter = Self::excel_col_letter(i);
1979 let start_row = 1; let end_row = list.len();
1981 let formula = format!(
1982 "'__lists__'!${}${}:${}${}",
1983 col_letter, start_row, col_letter, end_row
1984 );
1985 enum_list_formulas[i] = Some(formula);
1986 }
1987 }
1988 }
1989
1990 {
1992 let worksheet = workbook.add_worksheet();
1993 let header_format = Format::new()
1995 .set_bold()
1996 .set_font_size(12.0)
1997 .set_background_color(Color::RGB(0xE6F7FF))
1998 .set_pattern(FormatPattern::Solid);
1999 for (i, col) in self.columns.iter().enumerate() {
2000 worksheet
2001 .write_string_with_format(0, i as u16, &col.label, &header_format)
2002 .map_err(|e| DriverError::ExecutionError(format!("xlsx write header: {e}")))?;
2003 }
2004
2005 for (i, col) in self.columns.iter().enumerate() {
2006 match &col.data_type {
2007 UiDataType::Enum { items: _ } => {
2008 if let Some(formula_range) = &enum_list_formulas[i] {
2009 let dv = DataValidation::new()
2010 .allow_list_formula(Formula::new(formula_range))
2011 .set_error_title("Invalid value".to_string())
2012 .map_err(|e| {
2013 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2014 })?
2015 .set_error_message("Please select a valid value".to_string())
2016 .map_err(|e| {
2017 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2018 })?
2019 .set_error_style(DataValidationErrorStyle::Warning);
2020 worksheet
2021 .add_data_validation(1, i as u16, 50000, i as u16, &dv)
2022 .map_err(|e| {
2023 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2024 })?;
2025 }
2026 }
2027 UiDataType::Integer => {
2028 let (min_opt, max_opt) = col
2029 .rules
2030 .as_ref()
2031 .map(|r| {
2032 let minf = r.min.as_ref().map(|rv| match rv {
2033 RuleValue::Value(v) => *v,
2034 RuleValue::WithMessage { value, .. } => *value,
2035 });
2036 let maxf = r.max.as_ref().map(|rv| match rv {
2037 RuleValue::Value(v) => *v,
2038 RuleValue::WithMessage { value, .. } => *value,
2039 });
2040 (minf, maxf)
2041 })
2042 .unwrap_or((None, None));
2043
2044 let to_i32 = |v: f64| -> i32 {
2045 if v.is_finite() {
2046 let rounded = v.round();
2047 if rounded > i32::MAX as f64 {
2048 i32::MAX
2049 } else if rounded < i32::MIN as f64 {
2050 i32::MIN
2051 } else {
2052 rounded as i32
2053 }
2054 } else if v.is_sign_positive() {
2055 i32::MAX
2056 } else {
2057 i32::MIN
2058 }
2059 };
2060
2061 let rule = match (min_opt, max_opt) {
2062 (Some(minf), Some(maxf)) => {
2063 DataValidationRule::Between(to_i32(minf), to_i32(maxf))
2064 }
2065 (Some(minf), None) => {
2066 DataValidationRule::GreaterThanOrEqualTo(to_i32(minf))
2067 }
2068 (None, Some(maxf)) => {
2069 DataValidationRule::LessThanOrEqualTo(to_i32(maxf))
2070 }
2071 _ => DataValidationRule::Between(i32::MIN, i32::MAX),
2072 };
2073
2074 let dv = DataValidation::new()
2075 .allow_whole_number(rule)
2076 .set_error_title("Invalid integer".to_string())
2077 .map_err(|e| {
2078 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2079 })?
2080 .set_error_message("Please enter a valid integer".to_string())
2081 .map_err(|e| {
2082 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2083 })?
2084 .set_error_style(DataValidationErrorStyle::Warning);
2085 worksheet
2086 .add_data_validation(1, i as u16, 50000, i as u16, &dv)
2087 .map_err(|e| {
2088 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2089 })?;
2090 }
2091 UiDataType::Float => {
2092 let (min_opt, max_opt) = col
2094 .rules
2095 .as_ref()
2096 .map(|r| {
2097 let minv = r.min.as_ref().map(|rv| match rv {
2098 RuleValue::Value(v) => *v,
2099 RuleValue::WithMessage { value, .. } => *value,
2100 });
2101 let maxv = r.max.as_ref().map(|rv| match rv {
2102 RuleValue::Value(v) => *v,
2103 RuleValue::WithMessage { value, .. } => *value,
2104 });
2105 (minv, maxv)
2106 })
2107 .unwrap_or((None, None));
2108
2109 let rule = match (min_opt, max_opt) {
2110 (Some(minv), Some(maxv)) if minv.is_finite() && maxv.is_finite() => {
2111 DataValidationRule::Between(minv, maxv)
2112 }
2113 (Some(minv), None) if minv.is_finite() => {
2114 DataValidationRule::GreaterThanOrEqualTo(minv)
2115 }
2116 (None, Some(maxv)) if maxv.is_finite() => {
2117 DataValidationRule::LessThanOrEqualTo(maxv)
2118 }
2119 _ => DataValidationRule::Between(f64::MIN, f64::MAX),
2120 };
2121
2122 let dv = DataValidation::new()
2123 .allow_decimal_number(rule)
2124 .set_error_title("Invalid number".to_string())
2125 .map_err(|e| {
2126 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2127 })?
2128 .set_error_message("Please enter a valid number".to_string())
2129 .map_err(|e| {
2130 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2131 })?
2132 .set_error_style(DataValidationErrorStyle::Warning);
2133 worksheet
2134 .add_data_validation(1, i as u16, 50000, i as u16, &dv)
2135 .map_err(|e| {
2136 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2137 })?;
2138 }
2139 UiDataType::Boolean => {
2140 let dv = DataValidation::new()
2141 .allow_list_strings(&["true", "false"])
2142 .map_err(|e| {
2143 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2144 })?
2145 .set_error_title("Invalid boolean".to_string())
2146 .map_err(|e| {
2147 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2148 })?
2149 .set_error_message("Please enter a valid boolean".to_string())
2150 .map_err(|e| {
2151 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2152 })?
2153 .set_error_style(DataValidationErrorStyle::Warning);
2154 worksheet
2155 .add_data_validation(1, i as u16, 50000, i as u16, &dv)
2156 .map_err(|e| {
2157 DriverError::ExecutionError(format!("xlsx validation: {e}"))
2158 })?;
2159 }
2160 _ => {}
2161 }
2162 }
2163 }
2164
2165 {
2167 let lists_ws = workbook.add_worksheet();
2168 let _ = lists_ws.set_hidden(true).set_name("__lists__");
2169 for (i, col) in self.columns.iter().enumerate() {
2171 if let UiDataType::Enum { items } = &col.data_type {
2172 let localized = UiDataType::localize_enum_items(items, locale);
2173 if localized.is_empty() {
2174 continue;
2175 }
2176 for (ri, (_, label)) in localized.into_iter().enumerate() {
2177 lists_ws
2178 .write_string(ri as u32, i as u16, &label)
2179 .map_err(|e| {
2180 DriverError::ExecutionError(format!("xlsx list write: {e}"))
2181 })?;
2182 }
2183 }
2184 }
2185 }
2186
2187 Ok(workbook)
2188 }
2189
2190 #[inline]
2193 fn excel_col_letter(mut idx: usize) -> String {
2194 let mut letters: Vec<char> = Vec::with_capacity(3);
2196 idx += 1;
2197 while idx > 0 {
2198 let rem = (idx - 1) % 26;
2200 letters.push((b'A' + rem as u8) as char);
2201 idx = (idx - 1) / 26;
2202 }
2203 letters.iter().rev().collect()
2204 }
2205
2206 #[inline]
2208 fn append_meta_sheet(
2209 workbook: &mut Workbook,
2210 metadata: &TemplateMetadata,
2211 ) -> Result<(), DriverError> {
2212 let meta_ws = workbook.add_worksheet();
2213 let _ = meta_ws.set_hidden(true).set_name("__meta__");
2214
2215 meta_ws
2217 .write_string(0u32, 0u16, "key")
2218 .map_err(|e| DriverError::ExecutionError(format!("xlsx meta header: {e}")))?;
2219 meta_ws
2220 .write_string(0u32, 1u16, "value")
2221 .map_err(|e| DriverError::ExecutionError(format!("xlsx meta header: {e}")))?;
2222
2223 let items: [(&str, Option<String>); 6] = [
2224 ("driver_type", Some(metadata.driver_type.clone())),
2225 ("driver_version", metadata.driver_version.clone()),
2226 ("api_version", metadata.api_version.clone()),
2227 ("entity", Some(metadata.entity.clone())),
2228 ("locale", Some(metadata.locale.clone())),
2229 ("schema_version", Some(metadata.schema_version.clone())),
2230 ];
2231
2232 for (idx, (k, v)) in items.iter().enumerate() {
2233 let r = (idx as u32) + 1;
2234 let key_str = k.to_string();
2235 meta_ws
2236 .write_string(r, 0u16, &key_str)
2237 .map_err(|e| DriverError::ExecutionError(format!("xlsx meta key: {e}")))?;
2238 let v_str = v.clone().unwrap_or_default();
2239 meta_ws
2240 .write_string(r, 1u16, v_str.as_str())
2241 .map_err(|e| DriverError::ExecutionError(format!("xlsx meta value: {e}")))?;
2242 }
2243
2244 Ok(())
2245 }
2246
2247 #[inline]
2249 fn extract_metadata_from_workbook<R>(
2250 workbook: &mut calamine::Xlsx<R>,
2251 ) -> Result<TemplateMetadata, DriverError>
2252 where
2253 R: IoRead + IoSeek,
2254 {
2255 let meta_range = workbook
2256 .worksheet_range("__meta__")
2257 .map_err(|e| DriverError::ExecutionError(format!("xlsx read: {e}")))?;
2258
2259 let mut meta_map: BTreeMap<String, String> = BTreeMap::new();
2260 for (ri, row) in meta_range.rows().enumerate() {
2261 if ri == 0 {
2262 continue;
2263 }
2264 if row.len() >= 2 {
2265 let key = row[0].to_string();
2266 let val = row[1].to_string();
2267 if !key.trim().is_empty() {
2268 meta_map.insert(key, val);
2269 }
2270 }
2271 }
2272
2273 Ok(TemplateMetadata {
2274 driver_type: meta_map.get("driver_type").cloned().unwrap_or_default(),
2275 driver_version: meta_map
2276 .get("driver_version")
2277 .cloned()
2278 .filter(|s| !s.is_empty()),
2279 api_version: meta_map
2280 .get("api_version")
2281 .cloned()
2282 .filter(|s| !s.is_empty()),
2283 entity: meta_map.get("entity").cloned().unwrap_or_default(),
2284 locale: meta_map
2285 .get("locale")
2286 .cloned()
2287 .unwrap_or("zh-CN".to_string()),
2288 schema_version: meta_map
2289 .get("schema_version")
2290 .cloned()
2291 .unwrap_or("1.0".to_string()),
2292 })
2293 }
2294
2295 #[inline]
2297 fn read_first_data_range<R>(
2298 workbook: &mut calamine::Xlsx<R>,
2299 ) -> Result<calamine::Range<calamine::Data>, DriverError>
2300 where
2301 R: IoRead + IoSeek,
2302 {
2303 workbook
2304 .worksheet_range_at(0)
2305 .ok_or(DriverError::ExecutionError(
2306 "missing first worksheet".to_string(),
2307 ))
2308 .and_then(|r| r.map_err(|e| DriverError::ExecutionError(format!("xlsx read: {e}"))))
2309 }
2310
2311 #[inline]
2314 pub fn write_with_meta_to_buffer(
2315 &self,
2316 metadata: &TemplateMetadata,
2317 ) -> Result<Vec<u8>, DriverError> {
2318 let mut workbook = self.build_template_workbook(&metadata.locale)?;
2319 Self::append_meta_sheet(&mut workbook, metadata)?;
2320
2321 workbook
2322 .save_to_buffer()
2323 .map_err(|e| DriverError::ExecutionError(format!("xlsx save buffer: {e}")))
2324 }
2325
2326 #[inline]
2329 pub fn write_with_meta_to_file<P: AsRef<Path>>(
2330 &self,
2331 metadata: &TemplateMetadata,
2332 path: P,
2333 ) -> Result<(), DriverError> {
2334 let mut workbook = self.build_template_workbook(&metadata.locale)?;
2335 Self::append_meta_sheet(&mut workbook, metadata)?;
2336
2337 workbook
2338 .save(path)
2339 .map_err(|e| DriverError::ExecutionError(format!("xlsx save: {e}")))
2340 }
2341
2342 #[inline]
2345 pub fn write_with_meta_to_writer<W>(
2346 &self,
2347 metadata: &TemplateMetadata,
2348 writer: W,
2349 ) -> Result<(), DriverError>
2350 where
2351 W: IoWrite + IoSeek + Send,
2352 {
2353 let mut workbook = self.build_template_workbook(&metadata.locale)?;
2354 Self::append_meta_sheet(&mut workbook, metadata)?;
2355
2356 workbook
2357 .save_to_writer(writer)
2358 .map_err(|e| DriverError::ExecutionError(format!("xlsx save writer: {e}")))
2359 }
2360
2361 #[inline]
2364 pub fn read_with_meta_from_reader<R>(
2365 &self,
2366 reader: R,
2367 ) -> Result<(TemplateMetadata, Vec<serde_json::Map<String, Json>>), DriverError>
2368 where
2369 R: IoRead + IoSeek,
2370 {
2371 let mut workbook = calamine::Xlsx::new(reader)
2372 .map_err(|e| DriverError::ExecutionError(format!("xlsx open: {e}")))?;
2373
2374 let metadata = Self::extract_metadata_from_workbook(&mut workbook)?;
2375 let data_range = Self::read_first_data_range(&mut workbook)?;
2376 let rows = self.read_rows_from_range(&metadata.locale, data_range)?;
2377 Ok((metadata, rows))
2378 }
2379
2380 #[inline]
2383 pub fn read_with_meta_from_file(
2384 &self,
2385 path: &Path,
2386 ) -> Result<(TemplateMetadata, Vec<serde_json::Map<String, Json>>), DriverError> {
2387 let mut workbook: calamine::Xlsx<_> = calamine::open_workbook(path)
2388 .map_err(|e| DriverError::ExecutionError(format!("xlsx open: {e}")))?;
2389
2390 let metadata = Self::extract_metadata_from_workbook(&mut workbook)?;
2391 let data_range = Self::read_first_data_range(&mut workbook)?;
2392 let rows = self.read_rows_from_range(&metadata.locale, data_range)?;
2393 Ok((metadata, rows))
2394 }
2395
2396 #[inline]
2407 fn parse_any_str(s: &str) -> Option<Json> {
2408 let st = s.trim();
2409 if st.is_empty() {
2410 return None;
2411 }
2412 if st.eq_ignore_ascii_case("null") {
2413 return Some(Json::Null);
2414 }
2415 match st.to_ascii_lowercase().as_str() {
2416 "true" | "1" | "yes" | "on" | "y" | "t" | "是" => return Some(Json::Bool(true)),
2417 "false" | "0" | "no" | "off" | "n" | "f" | "否" => return Some(Json::Bool(false)),
2418 _ => {}
2419 }
2420 if (st.starts_with('{') && st.ends_with('}')) || (st.starts_with('[') && st.ends_with(']'))
2421 {
2422 if let Ok(v) = serde_json::from_str::<Json>(st) {
2423 return Some(v);
2424 }
2425 }
2426 if ((st.starts_with('"') && st.ends_with('"'))
2427 || (st.starts_with('\'') && st.ends_with('\'')))
2428 && st.len() >= 2
2429 {
2430 let unquoted = &st[1..st.len() - 1];
2431 return Some(Json::String(unquoted.to_string()));
2432 }
2433 if let Ok(n) = st.parse::<i64>() {
2434 return Some(Json::from(n));
2435 }
2436 if let Ok(n) = st.parse::<f64>() {
2437 return Some(Json::from(n));
2438 }
2439 Some(Json::String(st.to_string()))
2440 }
2441
2442 fn fold_special_prefixes(obj: &mut serde_json::Map<String, Json>) {
2447 for prefix in ["driver_config.", "device_driver_config."] {
2448 let mut taken: Vec<(String, Json)> = Vec::new();
2449 let keys: Vec<String> = obj
2451 .keys()
2452 .filter(|k| k.starts_with(prefix))
2453 .cloned()
2454 .collect();
2455 for k in keys {
2456 if let Some(v) = obj.remove(&k) {
2457 taken.push((k, v));
2458 }
2459 }
2460
2461 if taken.is_empty() {
2462 continue;
2463 }
2464
2465 let root_name = &prefix[..prefix.len() - 1]; let mut root = obj
2468 .remove(root_name)
2469 .and_then(|v| match v {
2470 Json::Object(m) => Some(m),
2471 _ => None,
2472 })
2473 .unwrap_or_default();
2474
2475 for (full_key, value) in taken.into_iter() {
2476 let remainder = &full_key[prefix.len()..];
2478 Self::insert_nested(&mut root, remainder, value);
2479 }
2480
2481 obj.insert(root_name.to_string(), Json::Object(root));
2482 }
2483 }
2484
2485 fn insert_nested(root: &mut serde_json::Map<String, Json>, path: &str, value: Json) {
2487 let mut current = root;
2488 let parts: Vec<&str> = path.split('.').collect();
2489 let mut to_insert = Some(value);
2490 for (i, part) in parts.iter().enumerate() {
2491 if i == parts.len() - 1 {
2492 if let Some(v) = to_insert.take() {
2493 current.insert((*part).to_string(), v);
2494 }
2495 } else {
2496 current = Self::ensure_child_map(current, part);
2497 }
2498 }
2499 }
2500
2501 fn ensure_child_map<'a>(
2503 parent: &'a mut serde_json::Map<String, Json>,
2504 key: &str,
2505 ) -> &'a mut serde_json::Map<String, Json> {
2506 use serde_json::map::Entry;
2507 match parent.entry(key.to_string()) {
2509 Entry::Occupied(mut occ) => {
2510 if !matches!(occ.get(), Json::Object(_)) {
2511 occ.insert(Json::Object(serde_json::Map::new()));
2512 }
2513 }
2514 Entry::Vacant(vac) => {
2515 vac.insert(Json::Object(serde_json::Map::new()));
2516 }
2517 }
2518 match parent.get_mut(key) {
2520 Some(Json::Object(m)) => m,
2521 _ => unreachable!(),
2522 }
2523 }
2524
2525 #[inline]
2527 fn get_value_by_path<'a>(
2528 obj: &'a serde_json::Map<String, Json>,
2529 path: &str,
2530 ) -> Option<&'a Json> {
2531 let mut current_map = obj;
2532 let mut iter = path.split('.').peekable();
2533 while let Some(seg) = iter.next() {
2534 let v = current_map.get(seg)?;
2535 if iter.peek().is_none() {
2536 return Some(v);
2537 }
2538 match v {
2539 Json::Object(m) => {
2540 current_map = m;
2541 }
2542 _ => return None,
2543 }
2544 }
2545 None
2546 }
2547
2548 #[inline]
2550 fn remove_by_path(obj: &mut serde_json::Map<String, Json>, path: &str) -> Option<Json> {
2551 let mut current_map = obj;
2552 let mut iter = path.split('.').peekable();
2553 while let Some(seg) = iter.next() {
2554 if iter.peek().is_none() {
2555 return current_map.remove(seg);
2556 }
2557 let next = current_map.get_mut(seg)?;
2558 match next {
2559 Json::Object(m) => {
2560 current_map = m;
2561 }
2562 _ => return None,
2563 }
2564 }
2565 None
2566 }
2567}
2568
2569#[derive(Debug, Clone, Serialize, Deserialize)]
2571#[serde(rename_all = "camelCase")]
2572pub enum ValidationCode {
2573 Required,
2574 TypeMismatch,
2575 Range,
2576 Length,
2577 Pattern,
2578 DiscriminatorMissing,
2579 DiscriminatorMismatch,
2580 InvisibleFilled,
2581 Unknown,
2582}
2583
2584#[derive(Debug, Clone, Serialize, Deserialize)]
2586#[serde(rename_all = "camelCase")]
2587pub struct FieldError {
2588 pub row: usize,
2589 pub field: String,
2590 pub code: ValidationCode,
2591 pub message: String,
2592}
2593
2594#[derive(Debug, Clone, Serialize, Deserialize)]
2596#[serde(rename_all = "camelCase")]
2597pub struct ValidationSummary {
2598 pub total: usize,
2599 pub valid: usize,
2600 pub invalid: usize,
2601 pub warn: usize,
2602}
2603
2604#[derive(Debug, Clone, Serialize, Deserialize)]
2606pub struct ImportValidationPreview {
2607 pub summary: ValidationSummary,
2608 pub error_preview: Vec<FieldError>,
2609 pub preview: Vec<serde_json::Map<String, Json>>, }
2611
2612#[derive(Debug, Clone)]
2614pub struct ValidatedRow {
2615 pub row_index: usize,
2616 pub values: serde_json::Map<String, Json>,
2617}
2618
2619pub trait FromValidatedRow: Sized {
2624 fn from_validated_row(
2626 row: &ValidatedRow,
2627 context: &RowMappingContext,
2628 ) -> Result<Self, DriverError>;
2629}
2630
2631#[derive(Debug, Clone)]
2633pub struct RowMappingContext {
2634 pub entity_id: i32,
2636 pub driver_type: String,
2638 pub locale: String,
2640}
2641
2642impl DriverEntityTemplate {
2643 pub fn map_to_domain<T>(
2645 &self,
2646 validated_rows: Vec<ValidatedRow>,
2647 context: RowMappingContext,
2648 ) -> Result<Vec<T>, DriverError>
2649 where
2650 T: FromValidatedRow,
2651 {
2652 validated_rows
2653 .iter()
2654 .map(|row| T::from_validated_row(row, &context))
2655 .collect()
2656 }
2657}