1pub 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 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
51const 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
312pub 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
346pub 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#[cfg(feature = "schema")]
373const HOST_ONLY_FIELDS: &[&str] = &["bind", "when"];
374
375#[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#[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#[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 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#[cfg(feature = "schema")]
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum WireType {
442 Number,
444 Integer,
446 Bool,
448 String,
450 Named(String),
452 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#[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 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#[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#[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
680fn 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 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 #[test]
727 fn every_element_type_reaches_every_emitter() {
728 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 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 #[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 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 use crate::models::{SpacerElement, WidgetElement};
782 assert_eq!(
783 WidgetElement::Spacer(SpacerElement { min_length: None }).type_name(),
784 "spacer"
785 );
786 }
787}