use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::api::client::{ApiError, GitHub};
use crate::api::model::RepoId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PropValue {
One(String),
Many(Vec<String>),
}
impl PropValue {
pub fn text(&self) -> String {
match self {
PropValue::One(s) => s.clone(),
PropValue::Many(v) => v.join(", "),
}
}
}
words! {
#[derive(Serialize, Deserialize)]
pub enum PropKind { pub fn word -> pub fn from_word, pub fn label }
Text "string" "Text";
Select "single_select" "Single select";
Multi "multi_select" "Multi select";
Bool "true_false" "True/false";
Url "url" "URL";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Property {
pub name: String,
pub kind: PropKind,
pub required: bool,
pub description: Option<String>,
pub allowed: Vec<String>,
pub repo_may: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Properties {
pub values: Vec<(String, Option<PropValue>)>,
pub schema: Result<Vec<Property>, String>,
}
impl Properties {
pub fn value(&self, name: &str) -> Option<&PropValue> {
(self.values.iter())
.find(|(n, _)| n == name)
.and_then(|(_, v)| v.as_ref())
}
}
pub type PropertyValues = Vec<(String, Option<PropValue>)>;
mod wire {
use super::PropValue;
use serde::Deserialize;
#[derive(Deserialize)]
pub(super) struct Value {
pub property_name: String,
pub value: Option<PropValue>,
}
#[derive(Deserialize)]
pub(super) struct Property {
pub property_name: String,
#[serde(default)]
pub value_type: String,
#[serde(default)]
pub required: bool,
pub description: Option<String>,
pub allowed_values: Option<Vec<String>>,
pub values_editable_by: Option<String>,
}
}
impl GitHub {
pub(super) async fn properties(&self, repo: &RepoId) -> Result<Properties, ApiError> {
let values = format!("/repos/{repo}/properties/values");
let schema = format!("/orgs/{}/properties/schema", repo.owner);
let schema = self.rest_refusable(&schema, "its properties", |status, _, _| {
matches!(status, 403 | 404).then(|| {
format!(
"GitHub says {}'s properties only to its members, and only an organization has them",
repo.owner
)
})
});
let values = self.rest_refusable(&values, "its custom properties", |status, _, _| {
(status == 404).then_some(())
});
let (values, schema) = tokio::join!(values, schema);
let Ok(values) = values? else {
let none = format!(
"Only an organization's repositories have custom properties, and {repo} isn't one"
);
return Ok(Properties {
values: Vec::new(),
schema: Err(none),
});
};
let values: Vec<wire::Value> = serde_json::from_str(values.body())?;
let values = (values.into_iter())
.map(|v| (v.property_name, v.value))
.collect();
let schema = match schema? {
Err(why) => Err(why),
Ok(answer) => {
let found: Vec<wire::Property> = serde_json::from_str(answer.body())?;
Ok(found
.into_iter()
.map(|p| Property {
name: p.property_name,
kind: PropKind::from_word(&p.value_type).unwrap_or(PropKind::Text),
required: p.required,
description: p.description.filter(|d| !d.is_empty()),
allowed: p.allowed_values.unwrap_or_default(),
repo_may: p.values_editable_by.as_deref() == Some("org_and_repo_actors"),
})
.collect())
}
};
Ok(Properties { values, schema })
}
pub(super) async fn set_properties(
&self,
repo: &RepoId,
values: &PropertyValues,
) -> Result<(), ApiError> {
let properties: Vec<_> = (values.iter())
.map(|(name, value)| json!({ "property_name": name, "value": value }))
.collect();
let body = json!({ "properties": properties });
let path = format!("/repos/{repo}/properties/values");
self.rest_write(http::Method::PATCH, &path, Some(&body))
.await
}
}