eui-core 0.10.0

Exponential UI core: the A2UI reducer (core + basic catalog, macros, extensions), runtime themes with recipe resolution, taffy layout with batched measurement, overlay layers and windowed lists — the engine behind the SwiftUI, Compose and gpui painters.
Documentation
//! The host router. Server messages in, ops out (`catalog/host.json` ops);
//! the platform host performs them on its surfaces. Pure and synchronous, so
//! `fixtures/host-router.json` locks the same behaviour on every platform.

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};

use super::contract::{error_message, INVALID_MESSAGE, MESSAGE_KINDS, SOURCE_DENIED, SURFACE_NOT_FOUND, TEMPLATE_NOT_FOUND, UNSUPPORTED_CATALOG};
use super::package::{package_sources, source_allowed, supported_catalog_ids, template_messages, validate_package_with, AvailableExtension, PackageIssue};
use super::sources::parse_source;
use crate::catalog::A2UI_VERSION;
use crate::limits;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SurfaceInfo {
    pub surface_id: String,
    pub catalog_id: String,
    /// The package whose template created it (its function policy).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub package_id: Option<String>,
}

/// One router per connection: the surfaces it created, the packages
/// installed, the extension catalogs negotiated.
#[derive(Debug, Clone, Default)]
pub struct HostRouter {
    extension_ids: Vec<String>,
    /// VAPP-161: their versions and components (a package's `extensions`
    /// are checked against them; `None` = only that the id is registered).
    extensions: Option<Vec<AvailableExtension>>,
    surfaces: IndexMap<String, SurfaceInfo>,
    packages: IndexMap<String, Value>,
}

fn send(message: Value) -> Value {
    json!({"op": "send", "message": message})
}

fn invalid(surface_id: &str, message: &str) -> Value {
    send(error_message(INVALID_MESSAGE, surface_id, message, None))
}

fn missing(surface_id: &str) -> Value {
    send(error_message(SURFACE_NOT_FOUND, surface_id, &format!("no surface {surface_id}; send createSurface first"), None))
}

/// Is the message's JSON past `maxMessageBytes`? (Counted while
/// serializing; the count stops at the limit.)
fn oversized(message: &Value) -> bool {
    struct Count(usize);
    impl std::io::Write for Count {
        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
            self.0 += buf.len();
            if self.0 > limits::MAX_MESSAGE_BYTES {
                return Err(std::io::Error::other("over the limit"));
            }
            Ok(buf.len())
        }
        fn flush(&mut self) -> std::io::Result<()> {
            Ok(())
        }
    }
    let mut count = Count(0);
    serde_json::to_writer(&mut count, message).is_err()
}

fn surface_id_of(message: &Map<String, Value>) -> String {
    message.values().find_map(|v| v.as_object().and_then(|o| o.get("surfaceId")).and_then(Value::as_str)).unwrap_or("").to_string()
}

/// A2UI: an omitted path or `/` is the whole data model (`""` in JSON pointer
/// terms, which every surface's setPointer takes).
pub fn normalize_path(path: Option<&str>) -> String {
    match path {
        None | Some("/") => String::new(),
        Some(p) => p.to_string(),
    }
}

impl HostRouter {
    /// `extension_ids` = the extension catalog ids the host registered.
    pub fn new<S: AsRef<str>>(extension_ids: &[S]) -> Self {
        HostRouter { extension_ids: extension_ids.iter().map(|s| s.as_ref().to_string()).collect(), ..Default::default() }
    }

    /// [`Self::new`] with the registered extensions' versions and components
    /// (the TS `HostRouterOptions.extensions`).
    pub fn with_extensions<S: AsRef<str>>(extension_ids: &[S], extensions: Option<Vec<AvailableExtension>>) -> Self {
        HostRouter { extensions, ..Self::new(extension_ids) }
    }

    pub fn supported_catalog_ids(&self) -> Vec<String> {
        supported_catalog_ids(&self.extension_ids)
    }

    pub fn register_extension(&mut self, id: &str) {
        self.register_extension_with(id, None);
    }

    /// VAPP-161: with its version and components (`None` = `{id}`), kept
    /// when the router checks extensions at all.
    pub fn register_extension_with(&mut self, id: &str, available: Option<AvailableExtension>) {
        if !self.extension_ids.iter().any(|x| x == id) {
            self.extension_ids.push(id.to_string());
        }
        if let Some(extensions) = &mut self.extensions {
            if !extensions.iter().any(|e| e.id == id) {
                extensions.push(available.unwrap_or_else(|| AvailableExtension { id: id.to_string(), ..Default::default() }));
            }
        }
    }

    /// Install a declarative package: its templates become `applyTemplate`
    /// targets. Returns the validation issues (installed only when none).
    pub fn install_package(&mut self, pkg: &Value) -> Vec<PackageIssue> {
        let issues = validate_package_with(pkg, &self.supported_catalog_ids(), self.extensions.as_deref());
        if issues.is_empty() {
            let id = pkg["id"].as_str().unwrap_or("").to_string();
            self.packages.insert(id, pkg.clone());
        }
        issues
    }

    pub fn package(&self, id: &str) -> Option<&Value> {
        self.packages.get(id)
    }

    pub fn surface(&self, id: &str) -> Option<&SurfaceInfo> {
        self.surfaces.get(id)
    }

    /// In creation order.
    pub fn surface_ids(&self) -> Vec<String> {
        self.surfaces.keys().cloned().collect()
    }

    /// The package whose template created the surface.
    pub fn package_id_of(&self, surface_id: &str) -> Option<String> {
        self.surfaces.get(surface_id).and_then(|s| s.package_id.clone())
    }

    /// One server message → the ops to perform, in order. Never fails.
    /// VAPP-103: a message past `maxMessageBytes` (as UTF-8 JSON) is
    /// refused: INVALID_MESSAGE.
    pub fn route(&mut self, message: &Value) -> Vec<Value> {
        if oversized(message) {
            let sid = message.as_object().map(surface_id_of).unwrap_or_default();
            return vec![invalid(&sid, &limits::message_bytes_issue())];
        }
        let Some(message) = message.as_object() else { return vec![invalid("", "a message is a JSON object")] };
        if let Some(version) = message.get("version") {
            if version.as_str() != Some(A2UI_VERSION) {
                return vec![invalid(&surface_id_of(message), &format!("unsupported version {}", crate::json::to_js_string(version)))];
            }
        }
        let kinds: Vec<&String> = message.keys().filter(|k| *k != "version").collect();
        if kinds.len() != 1 || !MESSAGE_KINDS.contains(&kinds[0].as_str()) {
            return vec![invalid(&surface_id_of(message), &format!("expected exactly one of {}", MESSAGE_KINDS.join(", ")))];
        }
        let kind = kinds[0].as_str();
        let Some(body) = message[kind].as_object().filter(|b| b.get("surfaceId").and_then(Value::as_str).is_some_and(|s| !s.is_empty())) else {
            return vec![invalid("", &format!("{kind}.surfaceId is required"))];
        };
        let surface_id = body["surfaceId"].as_str().unwrap_or("").to_string();
        let sid = surface_id.as_str();
        match kind {
            "createSurface" => {
                let Some(catalog_id) = body.get("catalogId").and_then(Value::as_str) else {
                    return vec![invalid(sid, "createSurface.catalogId is required")];
                };
                if !self.supported_catalog_ids().iter().any(|c| c == catalog_id) {
                    return vec![send(error_message(UNSUPPORTED_CATALOG, sid, &format!("catalog {catalog_id} is not supported"), None))];
                }
                self.surfaces.insert(surface_id.clone(), SurfaceInfo { surface_id: surface_id.clone(), catalog_id: catalog_id.to_string(), package_id: None });
                let mut op = Map::new();
                op.insert("op".into(), json!("create"));
                op.insert("surfaceId".into(), json!(sid));
                op.insert("catalogId".into(), json!(catalog_id));
                if let Some(theme) = body.get("theme") {
                    op.insert("theme".into(), theme.clone());
                }
                if body.get("sendDataModel") == Some(&Value::Bool(true)) {
                    op.insert("sendDataModel".into(), Value::Bool(true));
                }
                vec![Value::Object(op)]
            }
            "updateComponents" => {
                let Some(components) = body.get("components").filter(|c| c.is_array()) else {
                    return vec![invalid(sid, "updateComponents.components is required")];
                };
                if !self.surfaces.contains_key(sid) {
                    return vec![missing(sid)];
                }
                vec![json!({"op": "components", "surfaceId": sid, "components": components})]
            }
            "updateDataModel" => {
                if !self.surfaces.contains_key(sid) {
                    return vec![missing(sid)];
                }
                let path = match body.get("path") {
                    None => None,
                    Some(Value::String(p)) => Some(p.as_str()),
                    Some(_) => return vec![invalid(sid, "updateDataModel.path is a string")],
                };
                let path = normalize_path(path);
                match body.get("value") {
                    Some(value) => vec![json!({"op": "data", "surfaceId": sid, "path": path, "value": value})],
                    None => vec![json!({"op": "data", "surfaceId": sid, "path": path})],
                }
            }
            "deleteSurface" => {
                if self.surfaces.shift_remove(sid).is_none() {
                    return vec![missing(sid)];
                }
                vec![json!({"op": "delete", "surfaceId": sid})]
            }
            "bindDataModel" => {
                let Some(info) = self.surfaces.get(sid) else {
                    return vec![missing(sid)];
                };
                match (body.get("path").and_then(Value::as_str), body.get("source").and_then(Value::as_str)) {
                    (Some(path), Some(source)) if parse_source(source).is_some() => {
                        // VAPP-107: a package surface follows only the sources its package lists.
                        if let Some(pkg) = info.package_id.as_deref().and_then(|id| self.packages.get(id)) {
                            if !source_allowed(package_sources(pkg).as_deref(), source) {
                                let pkg_id = pkg.get("id").and_then(Value::as_str).unwrap_or("");
                                let at = normalize_path(Some(path));
                                let at = if at.is_empty() { "/".to_string() } else { at };
                                return vec![send(error_message(SOURCE_DENIED, sid, &format!("source {source} is not in package {pkg_id}'s sources"), Some(&at)))];
                            }
                        }
                        // VAPP-190: `?query=<name>` follows the package's named query.
                        let source = match info.package_id.as_deref().and_then(|id| self.packages.get(id)) {
                            Some(pkg) => super::query::expand_query_source(source, pkg.get("queries")),
                            None => source.to_string(),
                        };
                        vec![json!({"op": "bind", "surfaceId": sid, "path": normalize_path(Some(path)), "source": source})]
                    }
                    _ => vec![invalid(sid, "bindDataModel needs a path and a scheme:name source")],
                }
            }
            "applyTemplate" => {
                let Some(template_id) = body.get("templateId").and_then(Value::as_str) else {
                    return vec![invalid(sid, "applyTemplate.templateId is required")];
                };
                let Some(pkg) = self.find_package(template_id, body.get("packageId").and_then(Value::as_str)).cloned() else {
                    return vec![send(error_message(TEMPLATE_NOT_FOUND, sid, &format!("no installed package has the template {template_id}"), None))];
                };
                let mut ops = Vec::new();
                let pkg_id = pkg.get("id").and_then(Value::as_str).map(str::to_string);
                for m in template_messages(&pkg, template_id, sid, body.get("data")).unwrap_or_default() {
                    ops.extend(self.route(&m));
                    // The surface belongs to the package from its creation on (its
                    // function policy and source allowlist apply to the template's own binds).
                    if let Some(info) = self.surfaces.get_mut(sid) {
                        info.package_id = pkg_id.clone();
                    }
                }
                ops
            }
            _ => vec![invalid(sid, "unknown message")],
        }
    }

    fn find_package(&self, template_id: &str, package_id: Option<&str>) -> Option<&Value> {
        let has = |pkg: &Value| pkg.get("templates").and_then(|t| t.get(template_id)).is_some_and(super::js_truthy);
        match package_id {
            Some(id) => self.packages.get(id).filter(|p| has(p)),
            None => self.packages.values().find(|p| has(p)),
        }
    }
}