use std::fmt;
use serde::{Deserialize, Serialize};
use crate::path::RelPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ResourceKind {
Instructions,
Skills,
}
impl ResourceKind {
pub const ALL: &'static [ResourceKind] = &[Self::Instructions, Self::Skills];
pub const fn as_str(self) -> &'static str {
match self {
Self::Instructions => "instructions",
Self::Skills => "skills",
}
}
pub const fn node(self) -> NodeKind {
match self {
Self::Instructions => NodeKind::File,
Self::Skills => NodeKind::Dir,
}
}
}
impl fmt::Display for ResourceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ResourceKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.iter()
.copied()
.find(|kind| kind.as_str() == s)
.ok_or_else(|| format!("unknown resource `{s}`"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeKind {
File,
Dir,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Strategy {
Native,
Link,
Import,
}
impl Strategy {
pub const fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::Link => "link",
Self::Import => "import",
}
}
}
impl fmt::Display for Strategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Via {
Symlink,
Junction,
Import,
}
impl Via {
pub const fn as_str(self) -> &'static str {
match self {
Self::Symlink => "symlink",
Self::Junction => "junction",
Self::Import => "import",
}
}
pub const fn is_link(self) -> bool {
matches!(self, Self::Symlink | Self::Junction)
}
}
impl fmt::Display for Via {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LinkSupport {
pub symlink_file: bool,
pub symlink_dir: bool,
pub junction: bool,
}
impl LinkSupport {
pub const FULL: Self = Self {
symlink_file: true,
symlink_dir: true,
junction: false,
};
pub const NONE: Self = Self {
symlink_file: false,
symlink_dir: false,
junction: false,
};
pub const JUNCTION_ONLY: Self = Self {
symlink_file: false,
symlink_dir: false,
junction: true,
};
pub const fn best_for(self, node: NodeKind) -> Option<Via> {
match node {
NodeKind::File if self.symlink_file => Some(Via::Symlink),
NodeKind::Dir if self.symlink_dir => Some(Via::Symlink),
NodeKind::Dir if self.junction => Some(Via::Junction),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub node: NodeKind,
pub link: Option<LinkTarget>,
}
impl Entry {
pub fn file() -> Self {
Self {
node: NodeKind::File,
link: None,
}
}
pub fn dir() -> Self {
Self {
node: NodeKind::Dir,
link: None,
}
}
pub fn link(node: NodeKind, target: LinkTarget) -> Self {
Self {
node,
link: Some(target),
}
}
pub fn is_concrete(&self) -> bool {
self.link.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkTarget {
Inside(RelPath),
Outside(String),
}
impl fmt::Display for LinkTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Inside(path) => write!(f, "{path}"),
Self::Outside(raw) => f.write_str(raw),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_node_kinds_drive_link_choice() {
assert_eq!(ResourceKind::Instructions.node(), NodeKind::File);
assert_eq!(ResourceKind::Skills.node(), NodeKind::Dir);
}
#[test]
fn symlinks_are_preferred_when_available() {
assert_eq!(
LinkSupport::FULL.best_for(NodeKind::Dir),
Some(Via::Symlink)
);
assert_eq!(
LinkSupport::FULL.best_for(NodeKind::File),
Some(Via::Symlink)
);
}
#[test]
fn windows_without_privileges_still_links_directories() {
assert_eq!(
LinkSupport::JUNCTION_ONLY.best_for(NodeKind::Dir),
Some(Via::Junction)
);
assert_eq!(LinkSupport::JUNCTION_ONLY.best_for(NodeKind::File), None);
}
#[test]
fn no_support_yields_no_mechanism() {
assert_eq!(LinkSupport::NONE.best_for(NodeKind::Dir), None);
assert_eq!(LinkSupport::NONE.best_for(NodeKind::File), None);
}
#[test]
fn only_real_links_propagate_edits() {
assert!(Via::Symlink.is_link());
assert!(Via::Junction.is_link());
assert!(!Via::Import.is_link());
}
}