lingxia-devtool 0.7.0

Devtool runtime bridge and protocol helpers for LingXia apps
use lingxia_devtool_protocol::handlers;
use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::Duration;

const DEFAULT_EVAL_TIMEOUT: Duration = Duration::from_secs(5);

pub(crate) fn handle_lxapp_command(
    handler: &str,
    args: Option<Value>,
) -> Option<Result<Option<Value>, String>> {
    if !handler.starts_with("lxapp.") {
        return None;
    }

    Some(handle_lxapp_command_impl(handler, args))
}

fn handle_lxapp_command_impl(handler: &str, args: Option<Value>) -> Result<Option<Value>, String> {
    match handler {
        handlers::lxapp::LIST => {
            let args: ListArgs = parse_args(handler, args)?;
            let (current_appid, _, current_session_id) = lxapp::get_current_lxapp();
            let mut apps: Vec<Value> = lxapp::list_lxapps()
                .into_iter()
                .filter(|app| args.all || app.status == "opened" || app.status == "opening")
                .map(|app| {
                    let current =
                        app.appid == current_appid && app.session_id == current_session_id;
                    json!({
                        "appid": app.appid,
                        "name": app.app_name,
                        "status": app.status,
                        "current": current,
                        "page": app.current_page,
                        "pages_count": app.pages_count,
                    })
                })
                .collect();
            apps.sort_by(|a, b| {
                let a_current = a.get("current").and_then(Value::as_bool).unwrap_or(false);
                let b_current = b.get("current").and_then(Value::as_bool).unwrap_or(false);
                b_current.cmp(&a_current).then_with(|| {
                    a.get("appid")
                        .and_then(Value::as_str)
                        .unwrap_or("")
                        .cmp(b.get("appid").and_then(Value::as_str).unwrap_or(""))
                })
            });
            Ok(Some(Value::Array(apps)))
        }
        handlers::lxapp::CURRENT => {
            let (appid, path, _) = lxapp::get_current_lxapp();
            Ok(Some(json!({
                "appid": appid,
                "path": path,
            })))
        }
        handlers::lxapp::INFO => {
            let args: AppArgs = parse_args(handler, args)?;
            let app = resolve_app(&args.appid)?;
            lxapp_runtime_info_value(&app).map(Some)
        }
        handlers::lxapp::PAGES => {
            let args: AppArgs = parse_args(handler, args)?;
            let app = resolve_app(&args.appid)?;
            let info = app.runtime_info();
            let pages = info
                .page_entries
                .iter()
                .map(|page| {
                    json!({
                        "name": page.name,
                        "path": page.path,
                        "current": info.current_page.as_deref() == Some(page.path.as_str()),
                        "in_stack": info.page_stack.iter().any(|stack_page| stack_page == &page.path),
                    })
                })
                .collect::<Vec<_>>();
            Ok(Some(json!({
                "appid": info.appid,
                "pages_count": info.pages_count,
                "pages": pages,
            })))
        }
        handlers::lxapp::EVAL => {
            let args: EvalArgs = parse_args(handler, args)?;
            let app = resolve_app(&args.appid)?;
            let timeout = Duration::from_millis(args.timeout_ms.unwrap_or_else(|| {
                u64::try_from(DEFAULT_EVAL_TIMEOUT.as_millis()).unwrap_or(5000)
            }));
            let value = run_async(async move {
                tokio::time::timeout(timeout, app.eval_logic(args.script))
                    .await
                    .map_err(|_| format!("lxapp eval timed out after {}ms", timeout.as_millis()))?
                    .map_err(|err| err.to_string())
            })?;
            Ok(Some(json!({ "value": value })))
        }
        handlers::lxapp::OPEN => {
            let args: OpenArgs = parse_args(handler, args)?;
            let release_type = release_type(args.release_type.as_deref())?;
            ensure_lxapp_available(&args.appid, release_type)?;
            let app = lxapp::open_lxapp(
                &args.appid,
                lxapp::LxAppStartupOptions::new(args.path.as_deref().unwrap_or(""))
                    .set_release_type(release_type),
            )
            .map_err(|err| err.to_string())?;
            Ok(Some(json!({
                "appid": app.appid,
                "path": app.initial_route(),
            })))
        }
        handlers::lxapp::CLOSE => {
            let args: AppArgs = parse_args(handler, args)?;
            let appid = resolve_appid(&args.appid)?;
            lxapp::close_lxapp(&appid).map_err(|err| err.to_string())?;
            Ok(None)
        }
        handlers::lxapp::RESTART => {
            let args: AppArgs = parse_args(handler, args)?;
            let appid = resolve_appid(&args.appid)?;
            lxapp::restart_lxapp(&appid).map_err(|err| err.to_string())?;
            Ok(None)
        }
        handlers::lxapp::UNINSTALL => {
            let args: AppArgs = parse_args(handler, args)?;
            let appid = resolve_appid(&args.appid)?;
            lxapp::uninstall_lxapp(&appid).map_err(|err| err.to_string())?;
            Ok(None)
        }
        _ => Err(format!("unknown lxapp handler: {}", handler)),
    }
}

fn lxapp_runtime_info_value(app: &Arc<lxapp::LxApp>) -> Result<Value, String> {
    let mut value = serde_json::to_value(app.runtime_info()).map_err(|err| err.to_string())?;
    if let Value::Object(map) = &mut value {
        map.remove("session_id");
        map.remove("current_page");
        map.remove("initial_route");
        map.remove("pages_count");
        map.remove("page_entries");
        map.remove("page_stack");
    }
    Ok(value)
}

fn resolve_app(raw: &str) -> Result<Arc<lxapp::LxApp>, String> {
    let appid = resolve_appid(raw)?;
    if let Some(app) = lxapp::try_get(&appid) {
        return Ok(app);
    }
    ensure_lxapp_available(&appid, lxapp::ReleaseType::Release)
}

fn ensure_lxapp_available(
    appid: &str,
    release_type: lxapp::ReleaseType,
) -> Result<Arc<lxapp::LxApp>, String> {
    if let Some(app) = lxapp::try_get(appid) {
        return Ok(app);
    }
    if lxapp::installed_lxapp_path(appid, release_type).is_some() {
        return lxapp::ensure_lxapp(appid, release_type).map_err(|err| err.to_string());
    }
    lxapp::register_builtin_asset_bundle(appid.to_string(), appid.to_string());
    lxapp::ensure_builtin_lxapp(appid).map_err(|err| err.to_string())
}

fn resolve_appid(raw: &str) -> Result<String, String> {
    let trimmed = raw.trim();
    if trimmed.eq_ignore_ascii_case("current") {
        let (appid, _, _) = lxapp::get_current_lxapp();
        if appid.is_empty() {
            Err("no current lxapp".to_string())
        } else {
            Ok(appid)
        }
    } else if trimmed.is_empty() {
        Err("appid is required".to_string())
    } else {
        Ok(trimmed.to_string())
    }
}

fn release_type(value: Option<&str>) -> Result<lxapp::ReleaseType, String> {
    match value
        .unwrap_or("release")
        .trim()
        .to_ascii_lowercase()
        .as_str()
    {
        "release" => Ok(lxapp::ReleaseType::Release),
        "preview" | "trial" => Ok(lxapp::ReleaseType::Preview),
        "developer" | "develop" | "dev" => Ok(lxapp::ReleaseType::Developer),
        other => Err(format!(
            "unsupported release_type {other:?}; expected release, preview, or developer"
        )),
    }
}

fn parse_args<T>(handler: &str, args: Option<Value>) -> Result<T, String>
where
    T: for<'de> Deserialize<'de>,
{
    serde_json::from_value(args.unwrap_or_else(|| json!({})))
        .map_err(|err| format!("invalid args for {}: {}", handler, err))
}

fn run_async<T, E>(future: impl std::future::Future<Output = Result<T, E>>) -> Result<T, String>
where
    E: std::fmt::Display,
{
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|err| err.to_string())?
        .block_on(future)
        .map_err(|err| err.to_string())
}

#[derive(Deserialize)]
struct ListArgs {
    #[serde(default)]
    all: bool,
}

#[derive(Deserialize)]
struct AppArgs {
    appid: String,
}

#[derive(Deserialize)]
struct EvalArgs {
    appid: String,
    script: String,
    #[serde(default)]
    timeout_ms: Option<u64>,
}

#[derive(Deserialize)]
struct OpenArgs {
    appid: String,
    #[serde(default)]
    path: Option<String>,
    #[serde(default)]
    release_type: Option<String>,
}