Skip to main content

ng_gateway_sdk/
ui_schema.rs

1use calamine::Reader;
2use rust_xlsxwriter::{
3    workbook::Workbook, Color, DataValidation, DataValidationErrorStyle, DataValidationRule,
4    Format, FormatPattern, Formula,
5};
6// Dedicated UI condition/operator types; decoupled from legacy metadata
7use 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/// Inline i18n helper for driver metadata (UiSchema)
18///
19/// This macro builds `UiText` values embedded in `DriverSchemas` to provide
20/// localized strings without requiring a separate translation bundle. It is
21/// intended for labels, descriptions, placeholders, help text, and enum item
22/// labels used by the dynamic forms in the UI.
23///
24/// What it returns
25/// - `UiText::Localized` when you pass a map or language aliases
26/// - `UiText::Simple` when you pass a plain string
27/// - `UiText::Key` when you pass a dictionary key
28///
29/// When to use what
30/// - Prefer `Localized` for authoring-time strings that ship with the schema.
31///   Always include an English fallback (e.g. `en-US`).
32/// - Use `Key` only if you plan to resolve from a site-level dictionary (stage 2).
33///   Keep keys namespaced, e.g. `driver.modbus.connection.port`.
34/// - Use `Simple` for quick prototypes or when localization is not needed.
35///
36/// Supported forms
37/// - Map style (explicit locales):
38///   `ui_text!({ "en-US" => "Port", "zh-CN" => "端口" })`
39/// - Alias style (convenience for common locales):
40///   `ui_text!(en = "Port", zh = "端口")`
41///   Aliases: `en` -> `en-US`, `zh`/`zh_cn` -> `zh-CN`.
42/// - Key style (dictionary indirection):
43///   `ui_text!(key = "driver.modbus.connection.port")`
44/// - Plain string:
45///   `ui_text!("Port")`
46///
47/// JSON shape (serde tagged enum with `kind`):
48/// - Localized:
49///   `{ "kind": "Localized", "locales": { "en-US": "Port", "zh-CN": "端口" } }`
50/// - Simple:
51///   `{ "kind": "Simple", "value": "Port" }`
52/// - Key:
53///   `{ "kind": "Key", "key": "driver.modbus.connection.port" }`
54///
55/// UI behavior and fallbacks
56/// - The UI resolves `UiText` using the active locale with a fallback chain:
57///   current-locale -> base-language -> `en-US` -> `zh-CN` -> any available.
58/// - `Key` currently resolves to its key string; site-level dictionaries may
59///   override it in a future phase.
60///
61/// Best practices
62/// - Use BCP-47 language tags (e.g., `en-US`, `zh-CN`).
63/// - Always provide `en-US`; add other locales as needed.
64/// - Keep dictionary keys stable and namespaced.
65/// - Use this only for driver form metadata; do not reuse for logs/errors.
66#[macro_export]
67macro_rules! ui_text {
68    // Map style: ui_text!({ "en-US" => "Port", "zh-CN" => "端口" })
69    ({ $($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    // Aliases: ui_text!(en = "Port", zh = "端口")
76    ( 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    // Plain string: ui_text!("Port")
85    ($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                // Build combined template: device base + device driver + point base + point driver
108                // 1) Device base columns
109                Self::push_base_columns(FlattenEntity::Device, locale, &mut columns);
110
111                // 2) Device driver config columns (with prefix device_driver_config.)
112                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                // 3) Point base columns
123                Self::push_base_columns(FlattenEntity::Point, locale, &mut columns);
124
125                // 4) Point driver config columns (with prefix driver_config.)
126                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                // 1) Prepend base columns per entity (localized)
145                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                    // Discriminator column itself (string/numeric depending on downstream usage).
213                    // We keep it as String type for template header label equal to discriminator key.
214                    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    /// Push base columns for each entity before driver schema columns.
242    /// This ensures template headers place common fields first and support localization.
243    fn push_base_columns(entity: FlattenEntity, locale: &str, out: &mut Vec<FlattenColumn>) {
244        match entity {
245            FlattenEntity::Device => {
246                // device_name
247                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                // device_type (readonly in many flows, but exported for clarity)
261                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                // name
277                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                // key
291                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                // type (DataPointType)
305                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                // data_type (DataType)
321                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                // access_mode (AccessMode)
337                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                // unit
353                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                // min_value, max_value
364                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                // transform_* (logical datatype + affine transform)
381                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                // One row per parameter. Include action-level fields first.
427                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                // Parameter-level base fields
454                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                // param_transform_* (logical datatype + affine transform)
540                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                // DevicePoints is handled specially in build_template() by calling
586                // push_base_columns separately for Device and Point entities.
587                // This branch should never be reached.
588                unreachable!("DevicePoints should not call push_base_columns directly")
589            }
590        }
591    }
592
593    /// Localize helper for column labels
594    #[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    /// Build localized enum items for DataPointType
604    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    /// Build localized enum items for AccessMode
628    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    /// Build localized enum items for DataType
661    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/// Metadata written into hidden `__meta__` sheet alongside the data sheet.
693///
694/// This metadata is used to drive locale resolution and basic compatibility checks
695/// when importing templates generated by the gateway. Only the listed fields are
696/// persisted for minimalism and stability.
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct TemplateMetadata {
699    /// Driver type identifier, e.g. "modbus", "s7"
700    pub driver_type: String,
701    /// Optional driver version string
702    pub driver_version: Option<String>,
703    /// Optional API version string/number encoded as string
704    pub api_version: Option<String>,
705    /// Entity kind: "device" | "point" | "action"
706    pub entity: String,
707    /// Locale for header labels, e.g. "zh-CN" | "en-US"
708    pub locale: String,
709    /// Schema version for future compatibility, e.g. "1.0"
710    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    /// Localize the enum items into (key,label) pairs using the same locale strategy
794    #[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    /// Find enum key by its localized label. Falls back to raw key string match.
803    #[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    /// Localize the enum item into (key,label) pairs using the same locale strategy
824    #[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    /// Whether the field is required. Prefer set here over Field.required
844    pub required: Option<RuleValue<bool>>,
845    /// Numeric bounds for Integer/Float
846    pub min: Option<RuleValue<f64>>,
847    pub max: Option<RuleValue<f64>>,
848    /// String length bounds
849    pub min_length: Option<RuleValue<u32>>,
850    pub max_length: Option<RuleValue<u32>>,
851    /// Regex pattern for strings
852    pub pattern: Option<RuleValue<String>>,
853}
854
855/// RuleValue allows a raw value or an object with value and message.
856/// This maximizes wire compatibility with frontend needs (custom error messages).
857#[derive(Debug, Clone, Serialize, Deserialize)]
858#[serde(untagged)]
859pub enum RuleValue<T> {
860    /// Primitive value, e.g., 10
861    Value(T),
862    /// Object form with error message, e.g., { value: 10, message: UiText }
863    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    /// Control whether the node is rendered (mounted) in the UI (removes DOM when false).
896    ///
897    /// Best practice:
898    /// - Use `If`/`IfNot` for structural gating (e.g. union cases), so hidden fields do not
899    ///   participate in validation and do not submit values.
900    /// - Use `Visible`/`Invisible` for CSS visibility only (keeps DOM and preserves values).
901    If,
902    /// The inverse of `If` (a convenience effect for readability).
903    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/// Entity kinds for building flattened plans
943#[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/// Context of a single row used for field validation.
976#[derive(Debug)]
977pub struct ValidateRowContext<'a> {
978    /// 0-based row index in the sheet/template
979    pub row_index: usize,
980    /// Current locale (e.g., "zh-CN")
981    pub locale: &'a str,
982    /// Map of fully-qualified keys to values (e.g., "driver_config.host")
983    pub values: &'a serde_json::Map<String, serde_json::Value>,
984}
985
986/// A single flattened column derived from DriverSchemas
987#[derive(Debug, Clone)]
988pub struct FlattenColumn {
989    /// Fully-qualified machine key, e.g. "driver_config.host" or "inputs.ioa"
990    pub key: String,
991    /// Localized human-readable header text resolved with locale
992    pub label: String,
993    /// UI data type as declared by schema
994    pub data_type: UiDataType,
995    /// Validation rules attached to this field (if any)
996    pub rules: Option<Rules>,
997    /// When conditions (raw) that affect visibility/requirement
998    pub when: Option<Vec<When>>,
999    /// If this column belongs to a union case, the discriminator key
1000    pub union_discriminator: Option<String>,
1001    /// If this column belongs to a union case, the case value
1002    pub union_case_value: Option<serde_json::Value>,
1003    /// Localized enum items for UI/Excel dropdowns (key,label)
1004    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        // Only support string target and string pattern for performance and clarity
1135        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        // Support two styles:
1141        // 1) Raw pattern or inline flags:    "^abc$", "(?i)^abc$"
1142        // 2) JS-like delimiter with flags:  "/^abc$/i", "/foo.bar/msx"
1143        #[inline]
1144        fn build_regex(spec: &str) -> Option<regex::Regex> {
1145            if spec.starts_with('/') {
1146                // Find last '/' as delimiter end; allow pattern to contain '/'
1147                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                                // Supported flags mapped to Rust regex inline flags
1155                                'i' | 'm' | 's' | 'x' => mods.push(ch),
1156                                // Silently ignore unsupported flags to stay lenient
1157                                _ => {}
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            // Fallback: accept inline-flag patterns or plain patterns
1170            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                // comma separated
1194                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    /// Validate and normalize a single cell for this column with full row context.
1228    ///
1229    /// Behavior per requirements:
1230    /// - Locale uses '.' as decimal separator strictly
1231    /// - Strings are trimmed; empty string => None if not required, error if required
1232    /// - Enum matching is case-insensitive on both labels and string keys
1233    /// - Arrays are not supported
1234    /// - `when` effects are ignored except `Require`/`Optional`, which override required
1235    #[inline]
1236    pub fn validate(
1237        &self,
1238        ctx: &ValidateRowContext<'_>,
1239    ) -> Result<Option<serde_json::Value>, FieldError> {
1240        // Required after applying `when` overrides
1241        let required = self.resolve_required(ctx);
1242
1243        // Union case gating
1244        if !self.union_applicable(ctx) {
1245            return Ok(None);
1246        }
1247
1248        // Read and trim raw value (Option)
1249        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        // Normalize to target type
1256        let normalized = self.normalize_value_for_type(&raw, ctx)?;
1257
1258        // Apply rule-based validations
1259        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                    // For Any, we keep trimming but not parsing here; parsing happens in normalize
1325                    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            // For Any: apply numeric or string rules based on the runtime value type
1577            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/// A flattened view for a specific entity, ready for template generation or import validation
1663#[derive(Debug, Clone)]
1664pub struct DriverEntityTemplate {
1665    /// All columns (including union discriminator and case fields)
1666    pub columns: Vec<FlattenColumn>,
1667    /// All discriminator keys that appear in the plan (e.g. ["driver_config.kind"])
1668    pub discriminator_keys: Vec<String>,
1669}
1670
1671impl DriverEntityTemplate {
1672    /// Read only TemplateMetadata from `__meta__` sheet. Useful to build a correct
1673    /// template with the appropriate locale before parsing the data sheet.
1674    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    /// Validate and normalize rows; currently implements required checks and basic visibility logic.
1721    /// Further rules (range/length/pattern/union) can extend this method without breaking API.
1722    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            // Initialize normalized row first
1733            let mut normalized_row = row;
1734            let mut row_has_error = false;
1735
1736            // Discriminator presence (coarse check)
1737            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                // Avoid cloning `serde_json::Value` unless we actually need to update/remove it.
1751                // On large imports (e.g. 100k rows), repeated clones here become a major hotspot.
1752                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                // Note: at this stage `row` already had prefixes folded in read phase
1784                valids.push(ValidatedRow {
1785                    row_index: idx + 2,
1786                    values: normalized_row,
1787                });
1788            }
1789        }
1790
1791        (valids, errors, warn_count)
1792    }
1793
1794    /// Find a column by its machine key
1795    pub fn get_column(&self, key: &str) -> Option<&FlattenColumn> {
1796        self.columns.iter().find(|c| c.key == key)
1797    }
1798
1799    /// Read rows from a range
1800    #[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        // read header row
1808        let header = match rows_iter.next() {
1809            Some(r) => r,
1810            None => return Ok(Vec::new()),
1811        };
1812
1813        // Build column index mapping: header label -> plan index
1814        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                // Fold special prefixes into nested objects (driver_config.*, inputs.*)
1836                Self::fold_special_prefixes(&mut obj);
1837                out.push(obj);
1838            }
1839        }
1840        Ok(out)
1841    }
1842
1843    /// Map a single Excel cell (calamine::Data) to a serde_json::Value according to UiDataType.
1844    ///
1845    /// Behavior:
1846    /// - Empty -> None (skip)
1847    /// - String: trim and skip if empty
1848    /// - Enum: accept localized label (String); or raw key when types match (String/Number/Bool)
1849    /// - Boolean: prefer Data::Bool; numeric non-zero => true; string aliases supported
1850    /// - Integer/Float: prefer numeric cells; fallback parse from string; bool -> 1/0
1851    /// - Any: preserve native cell types; for String use parse_any_str for rich parsing
1852    #[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        // Normalize Empty and whitespace-only strings to None
1858        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                // 1) label match for String
1921                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                // 2) direct key match for non-string cells or string-as-key
1928                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                    // Keep original string (if any) for later normalize to decide
1939                    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    /// Build a workbook for the template
1967    #[inline]
1968    fn build_template_workbook(&self, locale: &str) -> Result<Workbook, DriverError> {
1969        let mut workbook = Workbook::new();
1970
1971        // Pre-compute enum list labels and formulas without creating worksheets yet.
1972        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; // A1-based
1980                    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        // Create the main worksheet first to ensure there is a visible sheet.
1991        {
1992            let worksheet = workbook.add_worksheet();
1993            // Header format: bold, larger font, with a distinctive background fill
1994            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                        // 从规则中提取 min/max(若存在),直接使用 f64 规则
2093                        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        // After main sheet exists, create and hide the __lists__ sheet and write labels.
2166        {
2167            let lists_ws = workbook.add_worksheet();
2168            let _ = lists_ws.set_hidden(true).set_name("__lists__");
2169            // Compute and write labels on the fly to avoid storing them upfront.
2170            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    /// Convert a zero-based column index to Excel's A1 column letters.
2191    /// For example: 0 -> "A", 25 -> "Z", 26 -> "AA".
2192    #[inline]
2193    fn excel_col_letter(mut idx: usize) -> String {
2194        // Excel columns are 1-based in their letter representation
2195        let mut letters: Vec<char> = Vec::with_capacity(3);
2196        idx += 1;
2197        while idx > 0 {
2198            // Convert to 0-based for the current digit then map to letter
2199            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    /// Append a hidden `__meta__` worksheet containing the given TemplateMetadata.
2207    #[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        // headers
2216        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    /// Extract TemplateMetadata from a calamine workbook's hidden `__meta__` sheet.
2248    #[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    /// Read the first worksheet as data with header row at Row(1).
2296    #[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    /// Build a workbook and append a hidden `__meta__` sheet containing TemplateMetadata,
2312    /// then return the serialized buffer.
2313    #[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    /// Build a workbook and append a hidden `__meta__` sheet containing TemplateMetadata,
2327    /// then save to a file path.
2328    #[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    /// Build a workbook and append a hidden `__meta__` sheet containing TemplateMetadata,
2343    /// then save to a writer.
2344    #[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    /// Read `__meta__` sheet first to resolve locale, then parse the first data worksheet
2362    /// using localized headers. Returns the metadata and the parsed rows.
2363    #[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    /// Read `__meta__` sheet first from a file path, then parse the first data worksheet
2381    /// using localized headers. Returns the metadata and the parsed rows.
2382    #[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    /// Parse a string into a JSON value for UiDataType::Any.
2397    ///
2398    /// Order of attempts:
2399    /// - null literal ("null")
2400    /// - boolean aliases (true/false, 1/0, yes/no, on/off, y/n, t/f, 是/否)
2401    /// - integer (i64)
2402    /// - float (f64)
2403    /// - JSON object/array (starts with '{' or '[')
2404    /// - quoted string (single or double quotes)
2405    /// - fallback to plain string
2406    #[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    /// Fold keys with well-known prefixes into nested JSON objects. Unlimited depth is supported,
2443    /// e.g., "driver_config.connect.policy.timeout" becomes
2444    /// { "driver_config": { "connect": { "policy": { "timeout": v }}}}
2445    /// Also supports "device_driver_config." prefix for device+points combined templates.
2446    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            // Collect and remove prefixed entries first to avoid borrow issues
2450            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            // Ensure root object exists
2466            let root_name = &prefix[..prefix.len() - 1]; // strip trailing dot
2467            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                // split after the prefix
2477                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    /// Insert a value into a nested object using dotted path (unlimited depth).
2486    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    /// Ensure a child key exists as an object and return its mutable map reference.
2502    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        // Ensure the entry is initialized as an object
2508        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        // Now safely get the mutable object reference
2519        match parent.get_mut(key) {
2520            Some(Json::Object(m)) => m,
2521            _ => unreachable!(),
2522        }
2523    }
2524
2525    /// Get a value from a nested object using dotted path, e.g., "driver_config.ca".
2526    #[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    /// Remove a value from a nested object using dotted path. Returns removed value if any.
2549    #[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/// Validation status code for a single field in import flows.
2570#[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/// A single field-level error captured during validation.
2585#[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/// Overall summary stats for an import validation.
2595#[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/// Public response shape for import preview.
2605#[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>>, // normalized rows (first N)
2610}
2611
2612/// A row that passed validation (or only with warnings)
2613#[derive(Debug, Clone)]
2614pub struct ValidatedRow {
2615    pub row_index: usize,
2616    pub values: serde_json::Map<String, Json>,
2617}
2618
2619/// Domain mapping trait for converting a normalized `ValidatedRow` into a domain model.
2620///
2621/// Implementors should extract required fields from `row.values` and apply any
2622/// driver- or entity-specific defaults using the provided `context`.
2623pub trait FromValidatedRow: Sized {
2624    /// Create a domain object from a validated row and mapping context.
2625    fn from_validated_row(
2626        row: &ValidatedRow,
2627        context: &RowMappingContext,
2628    ) -> Result<Self, DriverError>;
2629}
2630
2631/// Mapping context that provides ambient information for domain conversion.
2632#[derive(Debug, Clone)]
2633pub struct RowMappingContext {
2634    /// Optional entity id for association
2635    pub entity_id: i32,
2636    /// Driver type to propagate to domain models
2637    pub driver_type: String,
2638    /// Locale used during import (from template metadata)
2639    pub locale: String,
2640}
2641
2642impl DriverEntityTemplate {
2643    /// Map validated rows to a domain vector using the provided mapping trait.
2644    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}