use serde::{Deserialize, Serialize};
use crate::identifier::Identifier;
use crate::ir::difference::Difference;
use crate::ir::eq::{Equiv, field_difference, prefix_differences};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Role {
pub name: Identifier,
pub attributes: RoleAttributes,
pub member_of: Vec<Identifier>,
pub comment: Option<String>,
}
impl Equiv for Role {
fn differences(&self, other: &Self) -> Vec<Difference> {
let Self {
name: _,
attributes: _,
member_of: _,
comment: _,
} = self;
let mut out = Vec::new();
out.extend(field_difference("name", &self.name, &other.name));
out.extend(prefix_differences(
"attributes",
Equiv::differences(&self.attributes, &other.attributes),
));
out.extend(field_difference(
"member_of",
&format!("{:?}", self.member_of),
&format!("{:?}", other.member_of),
));
out.extend(field_difference(
"comment",
&format!("{:?}", self.comment),
&format!("{:?}", other.comment),
));
out
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoleAttributes {
pub superuser: bool,
pub createdb: bool,
pub createrole: bool,
pub inherit: bool,
pub login: bool,
pub replication: bool,
pub bypass_rls: bool,
pub connection_limit: Option<i64>,
pub valid_until: Option<String>,
}
impl Equiv for RoleAttributes {
fn differences(&self, other: &Self) -> Vec<Difference> {
let Self {
superuser: _,
createdb: _,
createrole: _,
inherit: _,
login: _,
replication: _,
bypass_rls: _,
connection_limit: _,
valid_until: _,
} = self;
let mut out = Vec::new();
out.extend(field_difference(
"superuser",
&self.superuser,
&other.superuser,
));
out.extend(field_difference(
"createdb",
&self.createdb,
&other.createdb,
));
out.extend(field_difference(
"createrole",
&self.createrole,
&other.createrole,
));
out.extend(field_difference("inherit", &self.inherit, &other.inherit));
out.extend(field_difference("login", &self.login, &other.login));
out.extend(field_difference(
"replication",
&self.replication,
&other.replication,
));
out.extend(field_difference(
"bypass_rls",
&self.bypass_rls,
&other.bypass_rls,
));
out.extend(field_difference(
"connection_limit",
&format!("{:?}", self.connection_limit),
&format!("{:?}", other.connection_limit),
));
out.extend(field_difference(
"valid_until",
&format!("{:?}", self.valid_until),
&format!("{:?}", other.valid_until),
));
out
}
}
impl Default for RoleAttributes {
fn default() -> Self {
Self {
superuser: false,
createdb: false,
createrole: false,
inherit: true, login: false,
replication: false,
bypass_rls: false,
connection_limit: None,
valid_until: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::eq::Equiv;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn base() -> Role {
Role {
name: id("app_user"),
attributes: RoleAttributes::default(),
member_of: vec![],
comment: None,
}
}
#[test]
fn equal_roles_have_no_diff() {
assert!(base().canonical_eq(&base()));
}
#[test]
fn login_change_diffs() {
let mut b = base();
b.attributes.login = true;
assert!(
base()
.differences(&b)
.iter()
.any(|x| x.path == "attributes.login")
);
}
#[test]
fn connection_limit_change_diffs() {
let mut b = base();
b.attributes.connection_limit = Some(10);
assert!(
base()
.differences(&b)
.iter()
.any(|x| x.path == "attributes.connection_limit")
);
}
#[test]
fn valid_until_change_diffs() {
let mut b = base();
b.attributes.valid_until = Some("2030-01-01T00:00:00Z".into());
assert!(
base()
.differences(&b)
.iter()
.any(|x| x.path == "attributes.valid_until")
);
}
#[test]
fn attributes_diff_does_not_emit_coarse_path() {
let mut b = base();
b.attributes.superuser = true;
assert!(
!base()
.differences(&b)
.iter()
.any(|x| x.path == "attributes"),
"coarse 'attributes' path must not appear; use per-field paths"
);
}
#[test]
fn membership_change_diffs() {
let mut b = base();
b.member_of.push(id("readers"));
assert!(base().differences(&b).iter().any(|x| x.path == "member_of"));
}
#[test]
fn comment_change_diffs() {
let mut b = base();
b.comment = Some("the app".into());
assert!(base().differences(&b).iter().any(|x| x.path == "comment"));
}
#[test]
fn default_attributes_match_postgres_defaults() {
let a = RoleAttributes::default();
assert!(a.inherit, "PG default for INHERIT is true");
assert!(!a.superuser);
assert!(!a.login);
assert_eq!(a.connection_limit, None);
}
}