Skip to main content

lingxia_surface/
content.rs

1//! Resolved content identities carried by the surface graph.
2//!
3//! Authoring lookup keys are resolved before entering this crate. Keeping the
4//! variants explicit prevents platform renderers from guessing whether an
5//! opaque entry names an lxapp, a browser document, or a native capability.
6
7use serde::{Deserialize, Serialize};
8
9/// Content hosted by a surface after declaration/runtime request resolution.
10#[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        /// Initial route only; later navigation belongs to the lxapp.
20        #[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        /// Ordinary browser asides reuse an existing matching initial URL.
30        /// Callback surfaces opt out to keep navigation and data isolated.
31        #[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        /// Stable caller-selected identity within one native capability.
40        /// `None` is the declaration's default instance.
41        #[serde(default, skip_serializing_if = "Option::is_none")]
42        instance_key: Option<String>,
43    },
44}
45
46/// Rendering-engine grouping used by adaptive aside slots.
47#[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    /// Identity of a native provider instance. The declaration itself owns the
65    /// default instance, represented by a missing key.
66    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}