1use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct RecipeId {
20 pub namespace: String,
22 pub path: String,
24}
25
26impl RecipeId {
27 pub fn new(namespace: impl Into<String>, path: impl Into<String>) -> Self {
32 Self {
33 namespace: namespace.into(),
34 path: path.into(),
35 }
36 }
37
38 pub fn vanilla(path: impl Into<String>) -> Self {
42 Self::new("minecraft", path)
43 }
44
45 pub fn parse(input: &str) -> Option<Self> {
52 let (ns, path) = input.split_once(':')?;
53 if ns.is_empty() || path.is_empty() {
54 return None;
55 }
56 Some(Self::new(ns, path))
57 }
58}
59
60impl fmt::Display for RecipeId {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(f, "{}:{}", self.namespace, self.path)
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn new_owns_strings() {
72 let id = RecipeId::new("plugin", "magic_sword");
73 assert_eq!(id.namespace, "plugin");
74 assert_eq!(id.path, "magic_sword");
75 }
76
77 #[test]
78 fn vanilla_uses_minecraft_namespace() {
79 let id = RecipeId::vanilla("oak_planks");
80 assert_eq!(id.namespace, "minecraft");
81 assert_eq!(id.path, "oak_planks");
82 }
83
84 #[test]
85 fn display_round_trips_with_parse() {
86 let id = RecipeId::vanilla("crafting_table");
87 let s = id.to_string();
88 assert_eq!(s, "minecraft:crafting_table");
89 assert_eq!(RecipeId::parse(&s), Some(id));
90 }
91
92 #[test]
93 fn parse_rejects_missing_colon() {
94 assert_eq!(RecipeId::parse("oak_planks"), None);
95 }
96
97 #[test]
98 fn parse_rejects_empty_namespace() {
99 assert_eq!(RecipeId::parse(":oak_planks"), None);
100 }
101
102 #[test]
103 fn parse_rejects_empty_path() {
104 assert_eq!(RecipeId::parse("minecraft:"), None);
105 }
106
107 #[test]
108 fn parse_takes_first_colon() {
109 let id = RecipeId::parse("ns:a:b").unwrap();
113 assert_eq!(id.namespace, "ns");
114 assert_eq!(id.path, "a:b");
115 }
116
117 #[test]
118 fn equality_is_exact() {
119 assert_eq!(
120 RecipeId::vanilla("oak_planks"),
121 RecipeId::new("minecraft", "oak_planks")
122 );
123 assert_ne!(
124 RecipeId::vanilla("oak_planks"),
125 RecipeId::vanilla("birch_planks")
126 );
127 }
128
129 #[test]
130 fn hashable_in_collections() {
131 use std::collections::HashSet;
132 let mut set = HashSet::new();
133 set.insert(RecipeId::vanilla("oak_planks"));
134 assert!(set.contains(&RecipeId::new("minecraft", "oak_planks")));
135 }
136}