use std::fmt;
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use wsb_prj::settings::ProjectSettings;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BoardRef {
pub owner: String,
pub number: i64,
}
impl fmt::Display for BoardRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.owner, self.number)
}
}
impl FromStr for BoardRef {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let (owner, number) = text
.split_once('/')
.ok_or_else(|| format!("{text:?} is not <owner>/<number>"))?;
let owner = owner.trim();
if owner.is_empty()
|| !owner
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(format!("{owner:?} is not a GitHub owner"));
}
let number: i64 = number
.trim()
.trim_start_matches('#')
.parse()
.map_err(|_| format!("{number:?} is not a board number"))?;
if number < 1 {
return Err(format!("{number} is not a board number"));
}
Ok(Self {
owner: owner.to_string(),
number,
})
}
}
impl Serialize for BoardRef {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for BoardRef {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
pub fn read(checkout: &Path) -> Result<Vec<BoardRef>> {
ProjectSettings::load(checkout)?
.boards
.iter()
.map(|board| board.parse::<BoardRef>().map_err(anyhow::Error::msg))
.collect()
}
pub fn write(checkout: &Path, boards: &[BoardRef]) -> Result<()> {
let mut settings = ProjectSettings::load(checkout)?;
settings.boards = boards.iter().map(BoardRef::to_string).collect();
settings.save(checkout)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_board_reference_is_owner_and_number() {
let board: BoardRef = "machinefabric/7".parse().expect("a reference");
assert_eq!(board.owner, "machinefabric");
assert_eq!(board.number, 7);
assert_eq!(board.to_string(), "machinefabric/7");
assert_eq!("machinefabric/#7".parse::<BoardRef>(), Ok(board));
for refused in ["machinefabric", "machinefabric/x", "/7", "a b/7", "machinefabric/0"] {
assert!(refused.parse::<BoardRef>().is_err(), "{refused} was read as a board");
}
}
#[test]
fn a_projects_boards_round_trip_through_its_settings() {
let dir = tempfile::tempdir().expect("a checkout");
assert!(read(dir.path()).expect("read").is_empty());
let boards: Vec<BoardRef> = vec!["machinefabric/7".parse().unwrap(), "someone/2".parse().unwrap()];
write(dir.path(), &boards).expect("written");
assert_eq!(read(dir.path()).expect("read"), boards);
let text = std::fs::read_to_string(ProjectSettings::path(dir.path())).expect("on disk");
assert!(text.contains("\"machinefabric/7\""), "boards are written as people name them: {text}");
let mut settings = ProjectSettings::load(dir.path()).unwrap();
settings.ignored_submodules = vec!["vendor".to_string()];
settings.save(dir.path()).unwrap();
write(dir.path(), &boards[..1]).unwrap();
assert_eq!(ProjectSettings::load(dir.path()).unwrap().ignored_submodules, vec!["vendor".to_string()]);
}
}