zeyn 0.0.8

Rust web framework for data heavy applications, written by AI agents and reviewed by people
Documentation
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use http_body_util::BodyExt;
use tower::ServiceExt;
use zeyn::prelude::*;

#[screen(route = "/", public)]
pub fn home() -> Screen {
    Screen::new("Welcome")
}

#[screen(route = "/shop/tea", public)]
pub fn tea() -> Screen {
    Screen::new("Tea")
}

#[screen(route = "/shop/coffee", public)]
pub fn coffee() -> Screen {
    Screen::new("Coffee")
}

#[screen(route = "/staff", permission = "staff.read")]
pub fn staff() -> Screen {
    Screen::new("Staff only")
}

#[screen(route = "/panel", public)]
pub fn panel() -> Screen {
    Screen::new("Panel")
}

fn site() -> Feature {
    Feature::new("site")
        .screen(home)
        .screen(tea)
        .screen(coffee)
        .screen(staff)
        .menu(|m| {
            m.group("Start", Icon::Home)
                .item("Home", home)
                .group("Shop", Icon::Cart)
                .item("Tea", tea)
                .item("Coffee", coffee)
                .item("Staff", staff)
        })
        .shell(
            Shell::site()
                .header(|h| h.sign_in().language().theme())
                .footer(|f| {
                    f.column("Shop", |c| {
                        c.link("Tea", tea)
                            .link("Staff", staff)
                            .url("Source", "https://example.com/src")
                    })
                    .text("Made with care")
                }),
        )
}

fn app() -> App {
    App::new("Corner Shop")
        .login_url("/sign-in")
        .feature(site())
        .feature(
            Feature::new("admin")
                .screen(panel)
                .menu(|m| {
                    m.group("Admin", Icon::Table)
                        .item("Panel", panel)
                }),
        )
}

async fn get(
    router: &axum::Router,
    path: &str,
    partial: bool,
) -> (StatusCode, axum::http::HeaderMap, String) {
    let mut b = Request::get(path);
    if partial {
        b = b.header("x-zeyn-partial", "1");
    }
    let res = router
        .clone()
        .oneshot(b.body(Body::empty()).unwrap())
        .await
        .unwrap();
    let (status, headers) = (res.status(), res.headers().clone());
    (
        status,
        headers,
        String::from_utf8(
            res.into_body()
                .collect()
                .await
                .unwrap()
                .to_bytes()
                .to_vec(),
        )
        .unwrap(),
    )
}

#[tokio::test]
async fn site_screens_open_in_the_site_shell_and_panel_screens_keep_the_app_shell() {
    let router = app().router().unwrap();
    let (status, _, page) = get(&router, "/shop/tea", false).await;
    assert_eq!(status, StatusCode::OK);
    assert!(page.contains(r#"<div class="z-app z-site" data-zeyn-shell="site">"#));
    assert!(
        page.contains(r#"href="/_zeyn/site.css?v="#) && page.contains(r#"src="/_zeyn/site.js?v="#)
    );
    assert!(page.contains(r#"<a class="z-site-brand" href="/">Corner Shop</a>"#));
    assert!(
        !page.contains(r#"<nav class="z-nav""#),
        "no side menu in the site shell"
    );
    assert!(
        !page.contains("/panel"),
        "menus of app shell features stay out of the site menu"
    );
    let (_, _, admin) = get(&router, "/panel", false).await;
    assert!(admin.contains(r#"<div class="z-app" data-zeyn-shell="app"><nav class="z-nav""#));
    assert!(
        !admin.contains("site.js"),
        "app shell pages do not load the site shell's assets"
    );
    assert!(admin.contains(r#"href="/panel""#) && !admin.contains(r#"href="/shop/tea""#));
}

#[tokio::test]
async fn the_site_menu_is_a_disclosure_navigation_with_one_link_groups_inline() {
    let (_, _, page) = get(&app().router().unwrap(), "/shop/tea", false).await;
    assert!(
        page.contains(r#"<ul class="z-site-menu"><li><a href="/">Home</a></li>"#),
        "a group of one is a link"
    );
    assert!(page.contains(
        r#"<button type="button" class="z-site-group-button" aria-expanded="false" aria-controls="z-site-group-1" data-current>Shop"#
    ));
    assert!(page.contains(r#"<ul class="z-site-sub" id="z-site-group-1" hidden>"#));
    assert!(page.contains(r#"<a href="/shop/tea" aria-current="page">Tea</a>"#));
    assert!(
        !page.contains(r#"href="/staff""#),
        "screens the visitor may not open are left out"
    );
    assert!(
        page.contains(r#"aria-controls="z-site-nav z-site-actions" data-zeyn-site-toggle hidden"#)
    );
}

#[tokio::test]
async fn the_header_shows_the_actions_asked_for_and_the_footer_its_columns() {
    let (_, _, page) = get(&app().router().unwrap(), "/", false).await;
    assert!(page.contains(
        r#"<a class="z-button z-site-sign-in" href="/sign-in" data-zeyn-reload>Sign in</a>"#
    ));
    assert!(page.contains(r#"data-zeyn-theme-picker"#));
    assert!(page.contains(r#"<footer class="z-site-footer">"#));
    assert!(page.contains(r#"<h2 id="z-site-footer-0">Shop</h2><ul><li><a href="/shop/tea">Tea</a></li><li><a href="https://example.com/src">Source</a></li></ul>"#));
    assert!(page.contains(r#"<p class="z-site-note">Made with care</p>"#));
}

#[tokio::test]
async fn partial_screens_tell_the_runtime_which_shell_they_belong_to() {
    let router = app().router().unwrap();
    let (_, site, _) = get(&router, "/shop/coffee", true).await;
    assert_eq!(site.get("x-zeyn-shell").unwrap(), "site");
    let (_, other, _) = get(&router, "/panel", true).await;
    assert_eq!(other.get("x-zeyn-shell").unwrap(), "app");
}

#[tokio::test]
async fn the_site_assets_are_served_without_sign_in() {
    let router = app().router().unwrap();
    let (status, headers, js) = get(&router, "/_zeyn/site.js", false).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(
        headers
            .get(header::CONTENT_TYPE)
            .unwrap(),
        "text/javascript; charset=utf-8"
    );
    assert!(js.contains("aria-expanded"));
    let (status, _, css) = get(&router, "/_zeyn/site.css", false).await;
    assert_eq!(status, StatusCode::OK);
    assert!(css.contains(".z-site-header"));
}

#[test]
fn footer_links_must_point_to_registered_screens_and_safe_addresses() {
    let bad = Feature::new("bad")
        .screen(tea)
        .shell(Shell::site().footer(|f| {
            f.column("Links", |c| {
                c.link("Coffee", coffee)
                    .url("Run", "javascript:alert(1)")
            })
        }));
    let problems = App::new("Bad")
        .feature(bad)
        .check()
        .unwrap_err();
    assert!(
        problems
            .iter()
            .any(|p| p.contains("screen `coffee`, which is not registered")),
        "{problems:?}"
    );
    assert!(
        problems
            .iter()
            .any(|p| p.contains("`javascript:alert(1)` is not a path")),
        "{problems:?}"
    );
}