Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/rights.rs

//! **What THIS person may do on one repository** — gunnar's answer, asked by
//! the server (`GET /api/repos/{ns}/{name}/rights`, owner 2026-10-18).
//!
//! The console role (viewer / operator / admin) is the SERVER-wide ladder.
//! Every ordinary person is a viewer, and the owner of a repository is still a
//! viewer — so the role cannot say whether they may open an issue on their own
//! repository. gunnar can: its per-repository grants (owner, admin, write of
//! that repository or its namespace) decide, and the server asks it as the
//! person. The pages draw their write controls from this and nothing else.
//!
//! Host-side, so `cargo test` covers the decisions the wasm pages make.

use serde::Deserialize;

/// The server's answer for one repository.
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
pub struct Rights {
    /// May read it.
    #[serde(default)]
    pub read: bool,
    /// May push, and open, comment on and merge issues and pull requests.
    #[serde(default)]
    pub write: bool,
    /// May change its settings, including branch protection.
    #[serde(default)]
    pub admin: bool,
}

impl Rights {
    /// Issues and pull requests: open, comment, close, merge, approve.
    pub fn may_work(&self) -> bool {
        self.write || self.admin
    }

    /// Branch protection and the repository's settings.
    pub fn may_administer(&self) -> bool {
        self.admin
    }
}

/// The address of the answer for `namespace/name`.
pub fn url(namespace: &str, name: &str) -> String {
    let seg = |s: &str| {
        s.bytes()
            .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
            .collect::<String>()
    };
    format!("/api/repos/{}/{}/rights", seg(namespace), seg(name))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_owner_works_and_administers_a_stranger_does_neither() {
        let owner: Rights = serde_json::from_str(r#"{"read":true,"write":true,"admin":true}"#).unwrap();
        assert!(owner.may_work() && owner.may_administer());
        let writer: Rights = serde_json::from_str(r#"{"read":true,"write":true,"admin":false}"#).unwrap();
        assert!(writer.may_work() && !writer.may_administer());
        let stranger: Rights = serde_json::from_str(r#"{"read":true}"#).unwrap();
        assert!(!stranger.may_work() && !stranger.may_administer());
        // No answer (a visitor, a refused read) is no right at all.
        assert_eq!(Rights::default(), Rights { read: false, write: false, admin: false });
    }

    #[test]
    fn the_address_is_the_repositorys_own() {
        assert_eq!(url("alice", "tool"), "/api/repos/alice/tool/rights");
        assert_eq!(url("a b", "x/y"), "/api/repos/a%20b/x%2Fy/rights");
    }
}