use std::{borrow::Cow, collections::HashMap, fmt::Display};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum Permissions {
Base(BasePermission),
Explicit(ExplicitPermissions),
}
impl Default for Permissions {
fn default() -> Self {
Self::Base(BasePermission::Default)
}
}
#[derive(Deserialize, Default, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum BasePermission {
#[default]
Default,
ReadAll,
WriteAll,
}
#[derive(Deserialize, Default, Debug, PartialEq)]
#[serde(rename_all = "kebab-case", default)]
pub struct ExplicitPermissions {
pub actions: Permission,
pub attestations: Permission,
pub checks: Permission,
pub contents: Permission,
pub deployments: Permission,
pub id_token: Permission,
pub issues: Permission,
pub discussions: Permission,
pub packages: Permission,
pub pages: Permission,
pub pull_requests: Permission,
pub repository_projects: Permission,
pub security_events: Permission,
pub statuses: Permission,
}
#[derive(Deserialize, Default, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum Permission {
Read,
Write,
#[default]
None,
}
pub type Env = HashMap<String, EnvValue>;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
pub enum EnvValue {
String(String),
Number(f64),
Boolean(bool),
}
impl Display for EnvValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(s) => write!(f, "{s}"),
Self::Number(n) => write!(f, "{n}"),
Self::Boolean(b) => write!(f, "{b}"),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct Expression(String);
impl Expression {
fn trimmed(&self) -> &str {
self.0.trim()
}
fn is_curly(&self) -> bool {
self.trimmed().starts_with("${{") && self.trimmed().ends_with("}}")
}
pub fn from_curly(value: String) -> Option<Self> {
let expr = Self(value);
if !expr.is_curly() {
return None;
}
Some(expr)
}
pub fn from_bare(value: String) -> Option<Self> {
let expr = Self(value);
if expr.is_curly() {
return None;
}
Some(expr)
}
pub fn as_curly(&self) -> Cow<'_, str> {
if self.is_curly() {
Cow::Borrowed(self.trimmed())
} else {
Cow::Owned(format!("${{{{ {expr} }}}}", expr = self.trimmed()))
}
}
pub fn as_bare(&self) -> &str {
if self.is_curly() {
self.trimmed()
.strip_prefix("${{")
.unwrap()
.strip_suffix("}}")
.unwrap()
.trim()
} else {
self.trimmed()
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum LoE<T> {
Literal(T),
Expr(Expression),
}
impl<T> Default for LoE<T>
where
T: Default,
{
fn default() -> Self {
Self::Literal(T::default())
}
}
pub type BoE = LoE<bool>;
#[derive(Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub(crate) enum SoV<T> {
One(T),
Many(Vec<T>),
}
impl<T> From<SoV<T>> for Vec<T> {
fn from(val: SoV<T>) -> Vec<T> {
match val {
SoV::One(v) => vec![v],
SoV::Many(vs) => vs,
}
}
}
pub(crate) fn scalar_or_vector<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
SoV::deserialize(de).map(Into::into)
}
#[cfg(test)]
mod tests {
use crate::common::{BasePermission, ExplicitPermissions};
use super::{Expression, Permissions};
#[test]
fn test_permissions() {
assert_eq!(
serde_yaml::from_str::<Permissions>("read-all").unwrap(),
Permissions::Base(BasePermission::ReadAll)
);
let perm = "security-events: write";
assert!(serde_yaml::from_str::<ExplicitPermissions>(perm).is_ok());
}
#[test]
fn test_expression() {
let expr = Expression("${{ foo }}".to_string());
assert_eq!(expr.as_curly(), "${{ foo }}");
assert_eq!(expr.as_bare(), "foo");
let expr = Expression("foo".to_string());
assert_eq!(expr.as_curly(), "${{ foo }}");
assert_eq!(expr.as_bare(), "foo");
let expr = Expression(" \t ${{ foo }} \t\n".to_string());
assert_eq!(expr.as_curly(), "${{ foo }}");
assert_eq!(expr.as_bare(), "foo");
let expr = Expression(" foo \t\n".to_string());
assert_eq!(expr.as_curly(), "${{ foo }}");
assert_eq!(expr.as_bare(), "foo");
}
#[test]
fn test_expression_from_curly() {
assert!(Expression::from_curly("${{ foo }}".into()).is_some());
assert!(Expression::from_curly("foo".into()).is_none());
}
#[test]
fn test_expression_from_bare() {
assert!(Expression::from_bare("${{ foo }}".into()).is_none());
assert!(Expression::from_bare("foo".into()).is_some());
}
}