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");
}
}