1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::path::RelPath;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum ResourceKind {
16 Instructions,
18 Skills,
21}
22
23impl ResourceKind {
24 pub const ALL: &'static [ResourceKind] = &[Self::Instructions, Self::Skills];
26
27 pub const fn as_str(self) -> &'static str {
28 match self {
29 Self::Instructions => "instructions",
30 Self::Skills => "skills",
31 }
32 }
33
34 pub const fn node(self) -> NodeKind {
39 match self {
40 Self::Instructions => NodeKind::File,
41 Self::Skills => NodeKind::Dir,
42 }
43 }
44}
45
46impl fmt::Display for ResourceKind {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(self.as_str())
49 }
50}
51
52impl std::str::FromStr for ResourceKind {
53 type Err = String;
54
55 fn from_str(s: &str) -> Result<Self, Self::Err> {
56 Self::ALL
57 .iter()
58 .copied()
59 .find(|kind| kind.as_str() == s)
60 .ok_or_else(|| format!("unknown resource `{s}`"))
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "kebab-case")]
67pub enum NodeKind {
68 File,
69 Dir,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "kebab-case")]
78pub enum Strategy {
79 Native,
84 Link,
87 Import,
90}
91
92impl Strategy {
93 pub const fn as_str(self) -> &'static str {
94 match self {
95 Self::Native => "native",
96 Self::Link => "link",
97 Self::Import => "import",
98 }
99 }
100}
101
102impl fmt::Display for Strategy {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(self.as_str())
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "kebab-case")]
116pub enum Via {
117 Symlink,
119 Junction,
122 Import,
124}
125
126impl Via {
127 pub const fn as_str(self) -> &'static str {
128 match self {
129 Self::Symlink => "symlink",
130 Self::Junction => "junction",
131 Self::Import => "import",
132 }
133 }
134
135 pub const fn is_link(self) -> bool {
138 matches!(self, Self::Symlink | Self::Junction)
139 }
140}
141
142impl fmt::Display for Via {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(self.as_str())
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct LinkSupport {
155 pub symlink_file: bool,
156 pub symlink_dir: bool,
157 pub junction: bool,
158}
159
160impl LinkSupport {
161 pub const FULL: Self = Self {
163 symlink_file: true,
164 symlink_dir: true,
165 junction: false,
166 };
167
168 pub const NONE: Self = Self {
170 symlink_file: false,
171 symlink_dir: false,
172 junction: false,
173 };
174
175 pub const JUNCTION_ONLY: Self = Self {
177 symlink_file: false,
178 symlink_dir: false,
179 junction: true,
180 };
181
182 pub const fn best_for(self, node: NodeKind) -> Option<Via> {
187 match node {
188 NodeKind::File if self.symlink_file => Some(Via::Symlink),
189 NodeKind::Dir if self.symlink_dir => Some(Via::Symlink),
190 NodeKind::Dir if self.junction => Some(Via::Junction),
193 _ => None,
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct Entry {
201 pub node: NodeKind,
202 pub link: Option<LinkTarget>,
204}
205
206impl Entry {
207 pub fn file() -> Self {
208 Self {
209 node: NodeKind::File,
210 link: None,
211 }
212 }
213
214 pub fn dir() -> Self {
215 Self {
216 node: NodeKind::Dir,
217 link: None,
218 }
219 }
220
221 pub fn link(node: NodeKind, target: LinkTarget) -> Self {
222 Self {
223 node,
224 link: Some(target),
225 }
226 }
227
228 pub fn is_concrete(&self) -> bool {
230 self.link.is_none()
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum LinkTarget {
237 Inside(RelPath),
239 Outside(String),
242}
243
244impl fmt::Display for LinkTarget {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::Inside(path) => write!(f, "{path}"),
248 Self::Outside(raw) => f.write_str(raw),
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn resource_node_kinds_drive_link_choice() {
259 assert_eq!(ResourceKind::Instructions.node(), NodeKind::File);
260 assert_eq!(ResourceKind::Skills.node(), NodeKind::Dir);
261 }
262
263 #[test]
264 fn symlinks_are_preferred_when_available() {
265 assert_eq!(
266 LinkSupport::FULL.best_for(NodeKind::Dir),
267 Some(Via::Symlink)
268 );
269 assert_eq!(
270 LinkSupport::FULL.best_for(NodeKind::File),
271 Some(Via::Symlink)
272 );
273 }
274
275 #[test]
276 fn windows_without_privileges_still_links_directories() {
277 assert_eq!(
281 LinkSupport::JUNCTION_ONLY.best_for(NodeKind::Dir),
282 Some(Via::Junction)
283 );
284 assert_eq!(LinkSupport::JUNCTION_ONLY.best_for(NodeKind::File), None);
286 }
287
288 #[test]
289 fn no_support_yields_no_mechanism() {
290 assert_eq!(LinkSupport::NONE.best_for(NodeKind::Dir), None);
291 assert_eq!(LinkSupport::NONE.best_for(NodeKind::File), None);
292 }
293
294 #[test]
295 fn only_real_links_propagate_edits() {
296 assert!(Via::Symlink.is_link());
297 assert!(Via::Junction.is_link());
298 assert!(!Via::Import.is_link());
299 }
300}