use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::bootstrap::MergeMethod;
use crate::event::ProtectionGap;
use crate::resource::Resource;
use crate::rights::ForgeRole;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ForgeKind {
GitHub,
Forgejo,
}
impl ForgeKind {
pub fn as_str(self) -> &'static str {
match self {
ForgeKind::GitHub => "github",
ForgeKind::Forgejo => "forgejo",
}
}
}
impl fmt::Display for ForgeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum NamespaceKind {
Organization,
User,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Namespace {
pub resource: Resource,
pub owner_id: Option<u64>,
pub kind: NamespaceKind,
pub installation_id: Option<u64>,
}
impl Namespace {
pub fn new(resource: Resource, kind: NamespaceKind) -> Self {
Namespace {
resource,
owner_id: None,
kind,
installation_id: None,
}
}
pub fn with_owner_id(mut self, id: u64) -> Self {
self.owner_id = Some(id);
self
}
pub fn with_installation(mut self, id: u64) -> Self {
self.installation_id = Some(id);
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum RequiredCheckKind {
#[default]
None,
Ruleset,
BranchProtection,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum LinkMethod {
#[default]
None,
DeviceFlow,
AuthorizationCodePkce,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Capabilities {
pub automation: bool,
pub bot_can_create_repos: bool,
pub role_levels: Vec<ForgeRole>,
pub required_checks: RequiredCheckKind,
pub webhooks: bool,
pub account_link: LinkMethod,
pub per_repo_tokens: bool,
#[serde(default)]
pub required_workflow: bool,
#[serde(default)]
pub single_owner_repos_unreviewed: bool,
#[serde(default)]
pub bridge_posted_check: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
#[non_exhaustive]
pub enum CheckSourceGuard {
#[default]
Unknown,
RequiredWorkflow,
OwnerReview {
reviewers: Vec<ForgeAccount>,
issues: Vec<String>,
},
Unreviewed,
BridgePosted,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForgeAccount {
pub id: u64,
pub login: String,
}
impl ForgeAccount {
pub fn new(id: u64, login: impl Into<String>) -> Self {
ForgeAccount {
id,
login: login.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoleAssignment {
pub account: ForgeAccount,
pub role: ForgeRole,
}
impl RoleAssignment {
pub fn new(account: ForgeAccount, role: ForgeRole) -> Self {
RoleAssignment { account, role }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum Unlisted {
#[default]
Keep,
Remove,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Visibility {
#[default]
Public,
Private,
Internal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RepoSpec {
pub resource: Resource,
pub visibility: Visibility,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub owners: Vec<ForgeAccount>,
}
impl RepoSpec {
pub fn new(resource: Resource) -> Self {
RepoSpec {
resource,
visibility: Visibility::Public,
description: None,
owners: Vec::new(),
}
}
pub fn with_owner(mut self, owner: ForgeAccount) -> Self {
self.owners.push(owner);
self
}
pub fn with_visibility(mut self, visibility: Visibility) -> Self {
self.visibility = visibility;
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct IndirectAccess {
pub role: ForgeRole,
pub via: Vec<AccessSource>,
}
impl IndirectAccess {
pub fn new(role: ForgeRole, via: Vec<AccessSource>) -> Self {
IndirectAccess { role, via }
}
}
impl fmt::Display for IndirectAccess {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`{}` access", self.role)?;
if self.via.is_empty() {
return f.write_str(" from something other than a direct role");
}
for (i, v) in self.via.iter().enumerate() {
f.write_str(match i {
0 => " ",
_ if i + 1 == self.via.len() => " and ",
_ => ", ",
})?;
write!(f, "{v}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind", content = "name")]
#[non_exhaustive]
pub enum AccessSource {
Team(String),
OrgOwner(String),
OrgMember(String),
}
impl fmt::Display for AccessSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AccessSource::Team(t) => write!(f, "through team `{t}`"),
AccessSource::OrgOwner(o) => write!(f, "as an owner of `{o}`"),
AccessSource::OrgMember(o) => write!(f, "as a member of `{o}` (its base permission)"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Collaborator {
pub account: ForgeAccount,
pub role: ForgeRole,
pub pending: bool,
}
impl Collaborator {
pub fn new(account: ForgeAccount, role: ForgeRole) -> Self {
Collaborator {
account,
role,
pending: false,
}
}
pub fn invited(account: ForgeAccount, role: ForgeRole) -> Self {
Collaborator {
account,
role,
pending: true,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProtectionState {
pub present: bool,
pub enforced: bool,
pub covers_default_branch: bool,
pub requires_pull_request: bool,
pub required_checks: Vec<String>,
pub blocks_force_push: bool,
pub blocks_deletion: bool,
pub bypass_actors: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub other_gaps: Vec<ProtectionGap>,
#[serde(default)]
pub check_source_guard: CheckSourceGuard,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub protected_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merge_methods: Option<Vec<MergeMethod>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ci_enabled: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RepoState {
pub resource: Resource,
pub forge_id: u64,
pub visibility: Visibility,
pub archived: bool,
pub default_branch: Option<String>,
pub collaborators: Vec<Collaborator>,
pub protection: ProtectionState,
}
impl RepoState {
pub fn new(resource: Resource, forge_id: u64) -> Self {
RepoState {
resource,
forge_id,
visibility: Visibility::Public,
archived: false,
default_branch: None,
collaborators: Vec::new(),
protection: ProtectionState::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Projection {
pub resource: Resource,
pub forge_id: Option<u64>,
pub roles: Vec<RoleAssignment>,
pub required_check: Option<String>,
pub archived: bool,
pub visibility: Option<Visibility>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub owners: Vec<ForgeAccount>,
}
impl Projection {
pub fn new(resource: Resource) -> Self {
Projection {
resource,
forge_id: None,
roles: Vec::new(),
required_check: None,
archived: false,
visibility: None,
owners: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RoleChange {
pub account: ForgeAccount,
pub from: ForgeRole,
pub to: ForgeRole,
pub outcome: RoleOutcome,
}
impl RoleChange {
pub fn new(
account: ForgeAccount,
from: ForgeRole,
to: ForgeRole,
outcome: RoleOutcome,
) -> Self {
RoleChange {
account,
from,
to,
outcome,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "status", content = "detail")]
#[non_exhaustive]
pub enum RoleOutcome {
Applied,
Invited,
Failed(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ApplyReport {
pub changes: Vec<RoleChange>,
pub unchanged: Vec<ForgeAccount>,
pub kept_unlisted: Vec<Collaborator>,
}
impl ApplyReport {
pub fn is_complete(&self) -> bool {
!self
.changes
.iter()
.any(|c| matches!(c.outcome, RoleOutcome::Failed(_)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BindRequest {
pub namespace: Resource,
pub state: String,
}
impl BindRequest {
pub fn new(namespace: Resource, state: impl Into<String>) -> Self {
BindRequest {
namespace,
state: state.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
#[non_exhaustive]
pub enum BindStep {
Redirect {
url: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BindCallback {
pub params: BTreeMap<String, String>,
pub expected_state: String,
pub expected_namespace: Resource,
}
impl BindCallback {
pub fn new(
params: BTreeMap<String, String>,
expected_state: impl Into<String>,
expected_namespace: Resource,
) -> Self {
BindCallback {
params,
expected_state: expected_state.into(),
expected_namespace,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct NamespaceBinding {
pub namespace: Namespace,
pub missing_permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Capabilities>,
}
impl NamespaceBinding {
pub fn new(namespace: Namespace, missing_permissions: Vec<String>) -> Self {
NamespaceBinding {
namespace,
missing_permissions,
capabilities: None,
}
}
pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
self.capabilities = Some(capabilities);
self
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
#[non_exhaustive]
pub enum LinkStep {
DeviceCode {
device_code: String,
user_code: String,
verification_uri: String,
expires_in: u64,
interval: u64,
},
Redirect {
url: String,
},
}
impl fmt::Debug for LinkStep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LinkStep::DeviceCode {
user_code,
verification_uri,
expires_in,
interval,
..
} => f
.debug_struct("DeviceCode")
.field("device_code", &"<redacted>")
.field("user_code", user_code)
.field("verification_uri", verification_uri)
.field("expires_in", expires_in)
.field("interval", interval)
.finish(),
LinkStep::Redirect { url } => f.debug_struct("Redirect").field("url", url).finish(),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
#[non_exhaustive]
pub enum LinkCallback {
DeviceCode {
device_code: String,
interval: u64,
expires_in: u64,
},
#[non_exhaustive]
Redirect {
params: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
member: Option<String>,
},
}
impl fmt::Debug for LinkCallback {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LinkCallback::DeviceCode {
interval,
expires_in,
..
} => f
.debug_struct("DeviceCode")
.field("device_code", &"<redacted>")
.field("interval", interval)
.field("expires_in", expires_in)
.finish(),
LinkCallback::Redirect { params, member } => f
.debug_struct("Redirect")
.field("params", ¶ms.keys().collect::<Vec<_>>())
.field("member", member)
.finish(),
}
}
}
impl LinkCallback {
pub fn redirect(params: BTreeMap<String, String>, member: impl Into<String>) -> LinkCallback {
LinkCallback::Redirect {
params,
member: Some(member.into()),
}
}
pub fn from_device_step(step: &LinkStep) -> Option<LinkCallback> {
match step {
LinkStep::DeviceCode {
device_code,
interval,
expires_in,
..
} => Some(LinkCallback::DeviceCode {
device_code: device_code.clone(),
interval: *interval,
expires_in: *expires_in,
}),
LinkStep::Redirect { .. } => None,
}
}
}