Skip to main content

tauri_plugin_widgets/
codegen.rs

1//! Emit TypeScript IR types from an explicit Rust `IR_ELEMENTS` spec (SoT with models).
2//!
3//! Static preamble / enums / supporting / footer live in `src/codegen/*.ts` via
4//! `include_str!`. The remaining `push_str` calls are intentional: per-element
5//! interfaces and the `WidgetElement` union are generated from `IR_ELEMENTS`
6//! so exhaustiveness tests stay coupled to [`crate::models::WidgetElement`].
7
8/// Emit `guest-js/generated/widget-types.ts`.
9pub fn emit_widget_types_ts() -> String {
10    let mut out = String::new();
11    out.push_str(include_str!("codegen/preamble.ts"));
12    out.push('\n');
13    out.push_str(include_str!("codegen/enums.ts"));
14    out.push_str(include_str!("codegen/supporting.ts"));
15
16    // Element interfaces — IR_SPEC mapping (must stay in sync with WidgetElement)
17    for spec in IR_ELEMENTS {
18        out.push_str(&format!(
19            "export interface {} extends ElementStyle {{\n  type: \"{}\";\n",
20            spec.ts_name, spec.wire
21        ));
22        for field in spec.fields {
23            out.push_str(&format!("  {};\n", field));
24        }
25        out.push_str("}\n\n");
26    }
27
28    out.push_str("export type WidgetElement =\n");
29    let names: Vec<&str> = IR_ELEMENTS
30        .iter()
31        .map(|e| e.ts_name)
32        .chain(std::iter::once("SpacerElement"))
33        .collect();
34    for (i, n) in names.iter().enumerate() {
35        let sep = if i + 1 == names.len() { ";" } else { "" };
36        out.push_str(&format!("  | {}{}\n", n, sep));
37    }
38    out.push('\n');
39
40    out.push_str(include_str!("codegen/footer.ts"));
41
42    out
43}
44
45struct ElementSpec {
46    wire: &'static str,
47    ts_name: &'static str,
48    fields: &'static [&'static str],
49}
50
51/// Explicit IR → TS mapping. When adding a [`WidgetElement`] variant, extend this list
52/// and the exhaustive match in tests.
53const IR_ELEMENTS: &[ElementSpec] = &[
54    ElementSpec {
55        wire: "vstack",
56        ts_name: "VStackElement",
57        fields: &[
58            "children: WidgetElement[]",
59            "spacing?: number",
60            "alignment?: HorizontalAlignment",
61            "justify?: Justify",
62        ],
63    },
64    ElementSpec {
65        wire: "hstack",
66        ts_name: "HStackElement",
67        fields: &[
68            "children: WidgetElement[]",
69            "spacing?: number",
70            "alignment?: VerticalAlignment",
71            "justify?: Justify",
72        ],
73    },
74    ElementSpec {
75        wire: "zstack",
76        ts_name: "ZStackElement",
77        fields: &["children: WidgetElement[]", "alignment?: string"],
78    },
79    ElementSpec {
80        wire: "grid",
81        ts_name: "GridElement",
82        fields: &[
83            "children: WidgetElement[]",
84            "columns?: number",
85            "spacing?: number",
86            "rowSpacing?: number",
87        ],
88    },
89    ElementSpec {
90        wire: "container",
91        ts_name: "ContainerElement",
92        fields: &["children?: WidgetElement[]", "contentAlignment?: string"],
93    },
94    ElementSpec {
95        wire: "text",
96        ts_name: "TextElement",
97        fields: &[
98            "content: string",
99            "fontSize?: number",
100            "fontWeight?: FontWeight",
101            "fontDesign?: FontDesign",
102            "textStyle?: TextStyle",
103            "color?: ColorValue",
104            "alignment?: TextAlignment",
105            "lineLimit?: number",
106            "truncationMode?: TruncationMode",
107            "minimumScaleFactor?: number",
108            "lineHeight?: number",
109            "letterSpacing?: number",
110            "underline?: boolean",
111            "strikethrough?: boolean",
112            "italic?: boolean",
113            "monospacedDigit?: boolean",
114        ],
115    },
116    ElementSpec {
117        wire: "image",
118        ts_name: "ImageElement",
119        fields: &[
120            "systemName?: string",
121            "data?: string",
122            "url?: string",
123            "size?: number",
124            "color?: ColorValue",
125            "contentMode?: ContentMode",
126            "fallbackSystemName?: string",
127            "symbolRendering?: SymbolRendering",
128            "symbolWeight?: FontWeight",
129            "paletteColors?: ColorValue[]",
130        ],
131    },
132    ElementSpec {
133        wire: "progress",
134        ts_name: "ProgressElement",
135        fields: &[
136            "value: number",
137            "total?: number",
138            "label?: string",
139            "tint?: ColorValue",
140            "color?: ColorValue",
141            "barStyle?: ProgressStyle",
142            "startAt?: number",
143            "endAt?: number",
144            "trackColor?: ColorValue",
145            "thickness?: number",
146        ],
147    },
148    ElementSpec {
149        wire: "gauge",
150        ts_name: "GaugeElement",
151        fields: &[
152            "value: number",
153            "min?: number",
154            "max?: number",
155            "label?: string",
156            "currentValueLabel?: string",
157            "tint?: ColorValue",
158            "color?: ColorValue",
159            "gaugeStyle?: GaugeStyle",
160            "startAt?: number",
161            "endAt?: number",
162            "minLabel?: string",
163            "maxLabel?: string",
164        ],
165    },
166    ElementSpec {
167        wire: "button",
168        ts_name: "ButtonElement",
169        fields: &[
170            "label: string",
171            "url?: string",
172            "action?: string",
173            "color?: ColorValue",
174            "backgroundColor?: ColorValue",
175            "fontSize?: number",
176            "textAlignment?: TextAlignment",
177            "variant?: ButtonVariant",
178            "systemName?: string",
179            "disabled?: boolean",
180            "set?: StatePatch",
181        ],
182    },
183    ElementSpec {
184        wire: "toggle",
185        ts_name: "ToggleElement",
186        fields: &[
187            "isOn: boolean",
188            "label?: string",
189            "tint?: ColorValue",
190            "action?: string",
191            "variant?: ToggleVariant",
192            "uncheckedTint?: ColorValue",
193            "set?: StatePatch",
194        ],
195    },
196    ElementSpec {
197        wire: "divider",
198        ts_name: "DividerElement",
199        fields: &["color?: ColorValue", "thickness?: number"],
200    },
201    ElementSpec {
202        wire: "date",
203        ts_name: "DateElement",
204        fields: &[
205            "date: string",
206            "dateStyle?: DateStyle",
207            "fontSize?: number",
208            "color?: ColorValue",
209        ],
210    },
211    ElementSpec {
212        wire: "chart",
213        ts_name: "ChartElement",
214        fields: &[
215            "chartType: ChartType",
216            "chartData: ChartDataPoint[]",
217            "tint?: ColorValue",
218            "showXAxis?: boolean",
219            "showYAxis?: boolean",
220            "showLegend?: boolean",
221            "yMin?: number",
222            "yMax?: number",
223            "interpolation?: ChartInterpolation",
224            "innerRadius?: number",
225        ],
226    },
227    ElementSpec {
228        wire: "list",
229        ts_name: "ListElement",
230        fields: &[
231            "items: ListItem[]",
232            "children?: WidgetElement[]",
233            "spacing?: number",
234            "fontSize?: number",
235            "color?: ColorValue",
236        ],
237    },
238    ElementSpec {
239        wire: "link",
240        ts_name: "LinkElement",
241        fields: &[
242            "children: WidgetElement[]",
243            "url?: string",
244            "action?: string",
245            "set?: StatePatch",
246        ],
247    },
248    ElementSpec {
249        wire: "shape",
250        ts_name: "ShapeElement",
251        fields: &[
252            "shapeType: ShapeType",
253            "fill?: ColorValue",
254            "stroke?: ColorValue",
255            "strokeWidth?: number",
256            "size?: number",
257        ],
258    },
259    ElementSpec {
260        wire: "timer",
261        ts_name: "TimerElement",
262        fields: &[
263            "targetDate: string",
264            "counting?: TimerCounting",
265            "fontSize?: number",
266            "fontWeight?: FontWeight",
267            "color?: ColorValue",
268            "startDate?: string",
269            "showHours?: boolean",
270            "completedText?: string",
271        ],
272    },
273    ElementSpec {
274        wire: "canvas",
275        ts_name: "CanvasElement",
276        fields: &[
277            "width: number",
278            "height: number",
279            "elements: CanvasDrawCommand[]",
280        ],
281    },
282    ElementSpec {
283        wire: "label",
284        ts_name: "LabelElement",
285        fields: &[
286            "text: string",
287            "systemName: string",
288            "iconColor?: ColorValue",
289            "fontSize?: number",
290            "fontWeight?: FontWeight",
291            "color?: ColorValue",
292            "spacing?: number",
293        ],
294    },
295    ElementSpec {
296        wire: "repeat",
297        ts_name: "RepeatElement",
298        fields: &[
299            "each: string",
300            "as?: string",
301            "limit?: number",
302            "template: WidgetElement",
303        ],
304    },
305    ElementSpec {
306        wire: "use",
307        ts_name: "UseElement",
308        fields: &["template: string", "args?: Record<string, unknown>"],
309    },
310];
311
312/// Wire type strings + field hints for Swift (flat Codable stays in Models.swift).
313pub fn emit_wire_catalog_swift() -> String {
314    let mut out = String::from(
315        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
316         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
317         // Decoding uses the generated `WidgetElement` (Generated/WidgetElement.swift).\n\n\
318         import Foundation\n\n\
319         /// Known `type` wire values for widget IR elements.\n\
320         public enum WidgetWireType: String, CaseIterable, Sendable {\n",
321    );
322    for spec in IR_ELEMENTS {
323        let case = swift_case_name(spec.wire);
324        out.push_str(&format!("    case {case} = \"{}\"\n", spec.wire));
325    }
326    out.push_str("    case spacer = \"spacer\"\n}\n\n");
327    out.push_str("/// Per-element field hints (documentation / drift checks; not Codable).\n");
328    out.push_str("public enum WidgetWireFields {\n");
329    for spec in IR_ELEMENTS {
330        let case = swift_case_name(spec.wire);
331        let fields: Vec<String> = spec
332            .fields
333            .iter()
334            .map(|f| format!("\"{}\"", f.replace('\\', "\\\\").replace('"', "\\\"")))
335            .collect();
336        out.push_str(&format!(
337            "    public static let {case}: [String] = [{}]\n",
338            fields.join(", ")
339        ));
340    }
341    out.push_str("    public static let spacer: [String] = [\"minLength?: number\"]\n");
342    out.push_str("}\n");
343    out
344}
345
346/// Wire type constants for Android Glance renderer (JSONObject path stays dynamic).
347pub fn emit_wire_catalog_kotlin() -> String {
348    let mut out = String::from(
349        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
350         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
351         // Runtime still uses org.json.JSONObject via render.El.\n\n\
352         package git.s00d.widgets\n\n\
353         /** Known `type` wire values for widget IR elements. */\n\
354         object WireTypes {\n",
355    );
356    for spec in IR_ELEMENTS {
357        let const_name = kotlin_const_name(spec.wire);
358        out.push_str(&format!("    const val {const_name} = \"{}\"\n", spec.wire));
359    }
360    out.push_str("    const val SPACER = \"spacer\"\n\n");
361    out.push_str("    val ALL: Set<String> = setOf(\n");
362    for spec in IR_ELEMENTS {
363        out.push_str(&format!("        {},\n", kotlin_const_name(spec.wire)));
364    }
365    out.push_str("        SPACER,\n    )\n}\n");
366    out
367}
368
369// ─── Native decoders from the Rust IR (schemars schema of `WidgetConfig`) ────────────────
370
371/// Element fields resolved away before any native renderer sees them.
372#[cfg(feature = "schema")]
373const HOST_ONLY_FIELDS: &[&str] = &["bind", "when"];
374
375/// Element types resolved away before any native renderer sees them.
376#[cfg(feature = "schema")]
377const HOST_ONLY_TYPES: &[&str] = &["repeat", "use"];
378
379#[cfg(feature = "schema")]
380fn variant_wire(v: &serde_json::Value) -> &str {
381    v["properties"]["type"]["enum"][0]
382        .as_str()
383        .unwrap_or_default()
384}
385
386/// Numeric fields Swift keeps as `Double` (values, ratios); other numbers are `CGFloat`.
387#[cfg(feature = "schema")]
388const SWIFT_DOUBLE_FIELDS: &[&str] = &[
389    "value",
390    "total",
391    "min",
392    "max",
393    "opacity",
394    "minimumScaleFactor",
395    "yMin",
396    "yMax",
397    "innerRadius",
398    "aspectRatio",
399    "layoutPriority",
400    "scale",
401    "rotation",
402];
403
404/// Union of every element's properties (style included) → wire type, from the schema
405/// the host validates with. Panics on a name used with two different types.
406#[cfg(feature = "schema")]
407pub fn element_fields() -> std::collections::BTreeMap<String, WireType> {
408    use serde_json::Value;
409    let schema = serde_json::to_value(schemars::schema_for!(crate::models::WidgetConfig))
410        .expect("schema serializes");
411    let defs = schema["definitions"].as_object().expect("definitions");
412    let variants = defs["WidgetElement"]["oneOf"].as_array().expect("oneOf");
413    let mut out = std::collections::BTreeMap::new();
414    for v in variants
415        .iter()
416        .filter(|v| !HOST_ONLY_TYPES.contains(&variant_wire(v)))
417    {
418        for (name, prop) in v["properties"].as_object().expect("properties") {
419            if name == "type" || HOST_ONLY_FIELDS.contains(&name.as_str()) {
420                continue;
421            }
422            // `set` (contract v3) is free-form JSON; Swift decodes it with the generated
423            // `StatePatch` (end of WidgetElement.swift).
424            let ty = if name == "set" {
425                WireType::Named("StatePatch".into())
426            } else {
427                wire_type(prop, defs)
428            };
429            if let Some(prev) = out.insert(name.clone(), ty.clone()) {
430                assert_eq!(prev, ty, "field `{name}` has two wire types");
431            }
432        }
433    }
434    let _: Option<Value> = None;
435    out
436}
437
438/// Wire type of one schema property (nullability dropped: every field is optional).
439#[cfg(feature = "schema")]
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum WireType {
442    /// JSON number.
443    Number,
444    /// JSON integer.
445    Integer,
446    /// JSON boolean.
447    Bool,
448    /// JSON string, string enum included.
449    String,
450    /// A named object / union type shared with the hand-written support code.
451    Named(String),
452    /// Array of a wire type.
453    Array(Box<WireType>),
454}
455
456#[cfg(feature = "schema")]
457fn wire_type(
458    prop: &serde_json::Value,
459    defs: &serde_json::Map<String, serde_json::Value>,
460) -> WireType {
461    use serde_json::Value;
462    if let Some(r) = prop.get("$ref").and_then(Value::as_str) {
463        let name = r.rsplit('/').next().unwrap_or(r);
464        return if is_string_enum(&defs[name]) {
465            WireType::String
466        } else {
467            WireType::Named(name.to_string())
468        };
469    }
470    if let Some(all) = prop.get("allOf").and_then(Value::as_array) {
471        assert_eq!(all.len(), 1, "unsupported allOf {prop}");
472        return wire_type(&all[0], defs);
473    }
474    if let Some(any) = prop.get("anyOf").and_then(Value::as_array) {
475        let non_null: Vec<&Value> = any.iter().filter(|a| a["type"] != "null").collect();
476        assert_eq!(non_null.len(), 1, "unsupported union {prop}");
477        return wire_type(non_null[0], defs);
478    }
479    let types: Vec<&str> = match &prop["type"] {
480        Value::String(t) => vec![t.as_str()],
481        Value::Array(a) => a
482            .iter()
483            .filter_map(Value::as_str)
484            .filter(|t| *t != "null")
485            .collect(),
486        _ => panic!("no type in {prop}"),
487    };
488    match types.as_slice() {
489        ["number"] => WireType::Number,
490        ["integer"] => WireType::Integer,
491        ["boolean"] => WireType::Bool,
492        ["string"] => WireType::String,
493        ["array"] => WireType::Array(Box::new(wire_type(&prop["items"], defs))),
494        other => panic!("unsupported type {other:?} in {prop}"),
495    }
496}
497
498#[cfg(feature = "schema")]
499fn is_string_enum(def: &serde_json::Value) -> bool {
500    if def.get("enum").is_some() {
501        return true;
502    }
503    def.get("oneOf")
504        .and_then(serde_json::Value::as_array)
505        .is_some_and(|items| items.iter().all(|i| i["type"] == "string"))
506}
507
508#[cfg(feature = "schema")]
509fn swift_type(name: &str, ty: &WireType) -> String {
510    match ty {
511        WireType::Number if SWIFT_DOUBLE_FIELDS.contains(&name) => "Double".into(),
512        WireType::Number => "CGFloat".into(),
513        WireType::Integer => "Int".into(),
514        WireType::Bool => "Bool".into(),
515        WireType::String => "String".into(),
516        WireType::Named(n) => n.clone(),
517        WireType::Array(inner) => format!("[{}]", swift_type(name, inner)),
518    }
519}
520
521/// `swift/Sources/TauriWidgets/Generated/WidgetElement.swift`: the flat `Codable` element
522/// every SwiftUI renderer decodes, generated from the Rust IR so no field can drift.
523#[cfg(feature = "schema")]
524pub fn emit_widget_element_swift() -> String {
525    let mut out = String::from(
526        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
527         // Do not edit. Source: the Rust IR (`src/models`), via its JSON Schema.\n\
528         // Supporting types (ColorValue, PaddingValue, …) are hand-written in Models.swift.\n\n\
529         import CoreGraphics\n\
530         import Foundation\n\n\
531         /// One widget element: the union of every element's fields, all optional but `type`.\n\
532         public struct WidgetElement: Codable {\n\
533         \x20   public let type: String\n",
534    );
535    let fields = element_fields();
536    for (name, ty) in &fields {
537        out.push_str(&format!(
538            "    public let {name}: {}?\n",
539            swift_type(name, ty)
540        ));
541    }
542    // Lossy: a field of the wrong type reads as absent and a broken child is dropped, so
543    // one bad value never blanks the whole widget.
544    out.push_str(
545        "\n    public init(from decoder: Decoder) throws {\n\
546         \x20       let c = try decoder.container(keyedBy: CodingKeys.self)\n\
547         \x20       type = try c.decode(String.self, forKey: .type)\n",
548    );
549    for (name, ty) in &fields {
550        let t = swift_type(name, ty);
551        if t == "[WidgetElement]" {
552            out.push_str(&format!(
553                "        {name} = (try? c.decodeIfPresent([LossyWidgetElement].self, forKey: .{name}))?.compactMap(\\.value)\n"
554            ));
555        } else {
556            out.push_str(&format!(
557                "        {name} = try? c.decodeIfPresent({t}.self, forKey: .{name})\n"
558            ));
559        }
560    }
561    out.push_str(
562        "    }\n}\n\n\
563         /// Decodes to `nil` instead of failing the enclosing array.\n\
564         struct LossyWidgetElement: Decodable {\n\
565         \x20   let value: WidgetElement?\n\
566         \x20   init(from decoder: Decoder) throws { value = try? WidgetElement(from: decoder) }\n\
567         }\n",
568    );
569    out.push_str(SWIFT_STATE_PATCH);
570    out
571}
572
573/// Swift support type for the free-form `set` object (contract v3).
574#[cfg(feature = "schema")]
575const SWIFT_STATE_PATCH: &str = r#"
576/// `set` on a button / toggle / link (contract v3): key → JSON value, as
577/// `JSONSerialization` objects (`NSNull` for `null`), ready for `WidgetStateIntent`.
578public struct StatePatch: Codable {
579    public let values: [String: Any]
580
581    public init(_ values: [String: Any]) { self.values = values }
582
583    public init(from decoder: Decoder) throws {
584        values = try decoder.singleValueContainer().decode([String: StatePatchJSON].self).mapValues(\.value)
585    }
586
587    public func encode(to encoder: Encoder) throws {
588        var c = encoder.singleValueContainer()
589        try c.encode(values.mapValues(StatePatchJSON.init))
590    }
591}
592
593/// Any JSON value (the values of a `StatePatch`).
594struct StatePatchJSON: Codable {
595    let value: Any
596
597    init(_ value: Any) { self.value = value }
598
599    init(from decoder: Decoder) throws {
600        let c = try decoder.singleValueContainer()
601        if c.decodeNil() { value = NSNull() }
602        else if let b = try? c.decode(Bool.self) { value = b }
603        else if let i = try? c.decode(Int.self) { value = i }
604        else if let d = try? c.decode(Double.self) { value = d }
605        else if let s = try? c.decode(String.self) { value = s }
606        else if let a = try? c.decode([StatePatchJSON].self) { value = a.map(\.value) }
607        else { value = try c.decode([String: StatePatchJSON].self).mapValues(\.value) }
608    }
609
610    func encode(to encoder: Encoder) throws {
611        var c = encoder.singleValueContainer()
612        switch value {
613        case let b as Bool: try c.encode(b)
614        case let i as Int: try c.encode(i)
615        case let d as Double: try c.encode(d)
616        case let s as String: try c.encode(s)
617        case let a as [Any]: try c.encode(a.map(StatePatchJSON.init))
618        case let o as [String: Any]: try c.encode(o.mapValues(StatePatchJSON.init))
619        default: try c.encodeNil()
620        }
621    }
622}
623"#;
624
625/// `android/src/main/java/render/WireFields.kt`: every field name as a constant, plus the
626/// fields each element type accepts (renderers read JSONObject keys through these).
627#[cfg(feature = "schema")]
628pub fn emit_wire_fields_kotlin() -> String {
629    use serde_json::Value;
630    let schema = serde_json::to_value(schemars::schema_for!(crate::models::WidgetConfig))
631        .expect("schema serializes");
632    let mut out = String::from(
633        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
634         // Do not edit. Source: the Rust IR (`src/models`), via its JSON Schema.\n\n\
635         package git.s00d.widgets\n\n\
636         /** JSON keys of widget elements (`El.str(WireField.CONTENT)`). */\n\
637         object WireField {\n",
638    );
639    for name in element_fields().keys() {
640        out.push_str(&format!(
641            "    const val {} = \"{name}\"\n",
642            kotlin_field_const(name)
643        ));
644    }
645    out.push_str("}\n\n/** Fields each element `type` accepts (style fields included). */\nval ELEMENT_FIELDS: Map<String, Set<String>> = mapOf(\n");
646    let variants = schema["definitions"]["WidgetElement"]["oneOf"]
647        .as_array()
648        .expect("oneOf");
649    let mut rows: Vec<(String, Vec<String>)> = variants
650        .iter()
651        .filter(|v| !HOST_ONLY_TYPES.contains(&variant_wire(v)))
652        .map(|v| {
653            let wire = v["properties"]["type"]["enum"][0]
654                .as_str()
655                .expect("type enum")
656                .to_string();
657            let mut fields: Vec<String> = v["properties"]
658                .as_object()
659                .expect("properties")
660                .keys()
661                .filter(|k| *k != "type" && !HOST_ONLY_FIELDS.contains(&k.as_str()))
662                .map(|k| format!("WireField.{}", kotlin_field_const(k)))
663                .collect();
664            fields.sort();
665            (wire, fields)
666        })
667        .collect();
668    rows.sort();
669    for (wire, fields) in rows {
670        out.push_str(&format!(
671            "    \"{wire}\" to setOf({}),\n",
672            fields.join(", ")
673        ));
674    }
675    let _: Option<Value> = None;
676    out.push_str(")\n");
677    out
678}
679
680/// `minimumScaleFactor` → `MINIMUM_SCALE_FACTOR`.
681fn kotlin_field_const(name: &str) -> String {
682    let mut out = String::new();
683    for (i, c) in name.chars().enumerate() {
684        if c.is_ascii_uppercase() && i > 0 {
685            out.push('_');
686        }
687        out.push(c.to_ascii_uppercase());
688    }
689    out
690}
691
692fn swift_case_name(wire: &str) -> String {
693    // Wire names are already lower camelCase; Swift keywords need backticks.
694    match wire {
695        "repeat" | "switch" | "case" | "default" | "in" | "is" => format!("`{wire}`"),
696        _ => wire.to_string(),
697    }
698}
699
700fn kotlin_const_name(wire: &str) -> String {
701    wire.to_uppercase()
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use crate::capabilities::ELEMENT_TYPES;
708
709    #[cfg(feature = "schema")]
710    #[test]
711    fn native_decoders_are_generated_and_current() {
712        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
713        let swift = std::fs::read_to_string(
714            root.join("swift/Sources/TauriWidgets/Generated/WidgetElement.swift"),
715        )
716        .unwrap_or_default();
717        assert_eq!(swift, emit_widget_element_swift(), "run `pnpm codegen`");
718        let kt = std::fs::read_to_string(root.join("android/src/main/java/render/WireFields.kt"))
719            .unwrap_or_default();
720        assert_eq!(kt, emit_wire_fields_kotlin(), "run `pnpm codegen`");
721    }
722
723    /// Every IR element type reaches every emitter: IR_ELEMENTS (+ spacer) → generated TS
724    /// (and, with `schema`, every schema field is in the TS spec) → Swift and Kotlin wire
725    /// catalogs → Kotlin field constant naming → `type_name`.
726    #[test]
727    fn every_element_type_reaches_every_emitter() {
728        // 1. The IR spec lists exactly the element types.
729        let wires: Vec<&str> = IR_ELEMENTS
730            .iter()
731            .map(|e| e.wire)
732            .chain(["spacer"])
733            .collect();
734        for ty in ELEMENT_TYPES {
735            assert!(wires.contains(ty), "IR_ELEMENTS missing wire type `{ty}`");
736        }
737        assert_eq!(wires.len(), ELEMENT_TYPES.len());
738
739        // 2. Generated TS: one discriminated member per type, emitted from IR_SPEC.
740        let ts = emit_widget_types_ts();
741        for ty in ELEMENT_TYPES {
742            let needle = format!("type: \"{ty}\"");
743            assert!(
744                ts.contains(&needle),
745                "generated TS missing element type `{ty}`"
746            );
747        }
748        assert!(ts.contains("export interface WidgetConfig"));
749        assert!(ts.contains("export type WidgetElement"));
750        assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
751        // The hand-written TS spec (IR_ELEMENTS + ElementStyle in supporting.ts) must name
752        // every field the Rust IR accepts.
753        #[cfg(feature = "schema")]
754        {
755            let missing: Vec<String> = element_fields()
756                .keys()
757                .filter(|f| !ts.contains(&format!("  {f}?:")) && !ts.contains(&format!("  {f}:")))
758                .cloned()
759                .collect();
760            assert!(
761                missing.is_empty(),
762                "add to IR_ELEMENTS / supporting.ts: {missing:?}"
763            );
764        }
765
766        // 3. Native wire catalogs.
767        let swift = emit_wire_catalog_swift();
768        let kt = emit_wire_catalog_kotlin();
769        for ty in ELEMENT_TYPES {
770            let needle = format!("= \"{ty}\"");
771            assert!(swift.contains(&needle), "Swift catalog missing `{ty}`");
772            assert!(kt.contains(&needle), "Kotlin catalog missing `{ty}`");
773        }
774        assert_eq!(
775            kotlin_field_const("minimumScaleFactor"),
776            "MINIMUM_SCALE_FACTOR"
777        );
778        assert_eq!(kotlin_field_const("x"), "X");
779
780        // 4. The Rust side names the same type.
781        use crate::models::{SpacerElement, WidgetElement};
782        assert_eq!(
783            WidgetElement::Spacer(SpacerElement { min_length: None }).type_name(),
784            "spacer"
785        );
786    }
787}