lingxia_surface/
content.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(
12 tag = "kind",
13 rename_all = "lowercase",
14 rename_all_fields = "camelCase"
15)]
16pub enum SurfaceContent {
17 Lxapp {
18 app_id: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 path: Option<String>,
22 },
23 Page {
24 app_id: String,
25 path: String,
26 },
27 Browser {
28 initial_url: String,
29 #[serde(
32 default = "default_reuse_by_url",
33 skip_serializing_if = "is_reuse_by_url"
34 )]
35 reuse_by_url: bool,
36 },
37 Native {
38 capability: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
42 instance_key: Option<String>,
43 },
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub enum SlotKind {
50 Lxapp,
51 Browser,
52 Native,
53}
54
55impl SurfaceContent {
56 pub fn slot_kind(&self) -> SlotKind {
57 match self {
58 Self::Lxapp { .. } | Self::Page { .. } => SlotKind::Lxapp,
59 Self::Browser { .. } => SlotKind::Browser,
60 Self::Native { .. } => SlotKind::Native,
61 }
62 }
63
64 pub fn native_identity(&self) -> Option<(&str, Option<&str>)> {
67 match self {
68 Self::Native {
69 capability,
70 instance_key,
71 } => Some((capability, instance_key.as_deref())),
72 _ => None,
73 }
74 }
75}
76
77const fn default_reuse_by_url() -> bool {
78 true
79}
80
81fn is_reuse_by_url(value: &bool) -> bool {
82 *value
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn serialization_keeps_content_kinds_explicit() {
91 let cases = [
92 (
93 SurfaceContent::Lxapp {
94 app_id: "home".into(),
95 path: None,
96 },
97 "lxapp",
98 ),
99 (
100 SurfaceContent::Page {
101 app_id: "home".into(),
102 path: "/settings".into(),
103 },
104 "page",
105 ),
106 (
107 SurfaceContent::Browser {
108 initial_url: "https://example.com".into(),
109 reuse_by_url: true,
110 },
111 "browser",
112 ),
113 (
114 SurfaceContent::Native {
115 capability: "terminal".into(),
116 instance_key: None,
117 },
118 "native",
119 ),
120 ];
121
122 for (content, expected_kind) in cases {
123 let value = serde_json::to_value(content).unwrap();
124 assert_eq!(value["kind"], expected_kind);
125 }
126 }
127
128 #[test]
129 fn content_kind_selects_engine_slot_without_identity_special_cases() {
130 let terminal = SurfaceContent::Native {
131 capability: "terminal".into(),
132 instance_key: None,
133 };
134 let editor = SurfaceContent::Native {
135 capability: "editor".into(),
136 instance_key: None,
137 };
138
139 assert_eq!(terminal.slot_kind(), SlotKind::Native);
140 assert_eq!(editor.slot_kind(), SlotKind::Native);
141 }
142
143 #[test]
144 fn native_instance_key_is_part_of_content_identity() {
145 let first = SurfaceContent::Native {
146 capability: "terminal".into(),
147 instance_key: Some("project-a".into()),
148 };
149 let second = SurfaceContent::Native {
150 capability: "terminal".into(),
151 instance_key: Some("project-b".into()),
152 };
153
154 assert_ne!(first, second);
155 assert_eq!(
156 first.native_identity(),
157 Some(("terminal", Some("project-a")))
158 );
159 assert_eq!(
160 serde_json::to_value(first).unwrap()["instanceKey"],
161 "project-a"
162 );
163 }
164}