xui-rs 2.0.0

A modern, typed async Rust SDK for the 3x-ui panel API
Documentation
use std::collections::BTreeSet;

use serde_json::Value;

const SDK_ROUTES: &[(&str, &str, Option<&str>)] = &[
    (
        "get",
        "/panel/api/hosts/list",
        Some("get_panel_api_hosts_list"),
    ),
    (
        "get",
        "/panel/api/hosts/get/{groupId}",
        Some("get_panel_api_hosts_get_groupId"),
    ),
    (
        "get",
        "/panel/api/hosts/byInbound/{inboundId}",
        Some("get_panel_api_hosts_byInbound_inboundId"),
    ),
    (
        "get",
        "/panel/api/hosts/tags",
        Some("get_panel_api_hosts_tags"),
    ),
    (
        "post",
        "/panel/api/hosts/add",
        Some("post_panel_api_hosts_add"),
    ),
    (
        "post",
        "/panel/api/hosts/update/{groupId}",
        Some("post_panel_api_hosts_update_groupId"),
    ),
    (
        "post",
        "/panel/api/hosts/del/{groupId}",
        Some("post_panel_api_hosts_del_groupId"),
    ),
    (
        "post",
        "/panel/api/hosts/setEnable/{groupId}",
        Some("post_panel_api_hosts_setEnable_groupId"),
    ),
    (
        "post",
        "/panel/api/hosts/reorder",
        Some("post_panel_api_hosts_reorder"),
    ),
    (
        "post",
        "/panel/api/hosts/bulk/add",
        Some("post_panel_api_hosts_bulk_add"),
    ),
    (
        "post",
        "/panel/api/hosts/bulk/setEnable",
        Some("post_panel_api_hosts_bulk_setEnable"),
    ),
    (
        "post",
        "/panel/api/hosts/bulk/del",
        Some("post_panel_api_hosts_bulk_del"),
    ),
];

/// Compares the SDK route inventory with both tagged `OpenAPI` operations and Go-router snapshots.
/// Source-only routes are checked separately so omissions in `OpenAPI` cannot hide SDK gaps.
#[test]
fn sdk_covers_openapi_and_source_routes() {
    let openapi: Value =
        serde_json::from_str(include_str!("../../spec/3x-ui-v3.8.5.openapi.json")).unwrap();
    let documented = openapi["paths"]
        .as_object()
        .unwrap()
        .iter()
        .flat_map(|(path, item)| {
            item.as_object().into_iter().flat_map(move |operations| {
                operations
                    .iter()
                    .filter(|(_, operation)| {
                        operation["tags"]
                            .as_array()
                            .is_some_and(|tags| tags.iter().any(|tag| tag == "Hosts"))
                    })
                    .map(move |(method, operation)| {
                        (
                            method.as_str(),
                            canonical_path(path),
                            operation["operationId"].as_str().unwrap(),
                        )
                    })
            })
        })
        .collect::<BTreeSet<_>>();
    let implemented_openapi = SDK_ROUTES
        .iter()
        .filter_map(|(method, path, operation)| {
            operation.map(|operation| (*method, *path, operation))
        })
        .collect::<BTreeSet<_>>();
    assert_eq!(documented.len(), 12);
    assert_eq!(documented, implemented_openapi);

    let snapshot: Value =
        serde_json::from_str(include_str!("../../spec/3x-ui-v3.8.5.hosts-routes.json")).unwrap();
    let source = snapshot["routes"]
        .as_array()
        .unwrap()
        .iter()
        .map(|route| {
            (
                route["method"].as_str().unwrap(),
                route["path"].as_str().unwrap(),
            )
        })
        .collect::<BTreeSet<_>>();
    let implemented = SDK_ROUTES
        .iter()
        .map(|(method, path, _)| (*method, *path))
        .collect::<BTreeSet<_>>();
    assert_eq!(source.len(), 12);
    assert_eq!(source, implemented);
}

fn canonical_path(path: &str) -> &str {
    match path {
        "/panel/api/hosts/get/{id}" => "/panel/api/hosts/get/{groupId}",
        "/panel/api/hosts/update/{id}" => "/panel/api/hosts/update/{groupId}",
        "/panel/api/hosts/del/{id}" => "/panel/api/hosts/del/{groupId}",
        "/panel/api/hosts/setEnable/{id}" => "/panel/api/hosts/setEnable/{groupId}",
        _ => path,
    }
}