Skip to main content

rna/megu/
namespace.rs

1/// Namespace consist of 'prefix' and 'suffix'. 
2/// where 'prefix' is any string before `:` and 'suffix' is any string after that
3/// 
4/// # Example Namespaces
5/// - `minecraft:test` -> prefix: `minecraft`, suffix: `test`
6/// - `megumin:explosion` -> prefix: `megumin`, suffix: `explosion`
7/// - `boomber:hello_world` -> prefix: `boomber`, suffix: `hello_world`
8/// 
9/// # Examples
10/// ## Creating Namespace
11/// This method will create new namespace without any check for invalid character in namespace.
12/// ```
13/// # use rna::Namespace;
14/// let namespace = Namespace::new("megumin", "explosion");
15/// assert_eq!(namespace.prefix, "megumin");
16/// assert_eq!(namespace.suffix, "explosion");
17/// ```
18/// 
19/// 
20/// ## Decode Namespace
21/// This method will create new namespace while also check of any invalid syntax in namespace and will return `DecodeError` if that happened.
22/// ```
23/// # use rna::Namespace;
24/// let namespace = Namespace::decode("megumin:explosion").unwrap();
25/// assert_eq!(namespace.prefix, "megumin");
26/// assert_eq!(namespace.suffix, "explosion");
27/// ```
28#[derive(Clone, PartialEq, Eq, Hash)]
29pub struct Namespace {
30	/// String that come before `:`
31	pub prefix: String,
32	/// String that come after `:`
33	pub suffix: String
34}
35
36const NAMESPACE_RULE: &str = r#"^[a-z:._\-/\d]+$"#;
37
38use regex::Regex;
39impl Namespace {
40	/// Manually create new Namespace
41	pub fn new(prefix: impl Into<String>, suffix: impl Into<String>) -> Namespace {
42		let prefix = prefix.into();
43		let suffix = suffix.into();
44		Namespace { prefix, suffix }
45	}
46
47	/// Create Namespace from a given string.  
48	/// 
49	/// # Errors
50	/// This method can error when:
51	/// - Input contain invalid characters for namespace
52	/// - Input contain too many colons (`:`)
53	/// - There's an error inside `regex` crate
54	/// 
55	/// # Examples
56	/// ```
57	/// # use rna::Namespace;
58	/// assert_eq!(
59	///    Namespace::decode("boomber:hello_world").unwrap(),
60	///    Namespace::new("boomber", "hello_world")
61	/// );
62	/// ```
63	/// 
64	/// If no colon is provided, 'minecraft' prefix will be used.
65	/// ```
66	/// # use rna::Namespace;
67	/// assert_eq!(
68	///    Namespace::decode("without_prefix").unwrap(),
69	///    Namespace::new("minecraft", "without_prefix")
70	/// );
71	/// ```
72	pub fn decode(value: impl Into<String>) -> Result<Namespace, DecodeError> {
73		let value = value.into();
74		let namespace_validation = Regex::new(&NAMESPACE_RULE)?;
75
76		if !namespace_validation.is_match(&value) {
77			return Err(DecodeError::InvalidNamespace(value));
78		}
79
80		let semicolon_counts = value.clone().chars().filter(|&c| c == ':').count();
81		if semicolon_counts > 1 {
82			return Err(DecodeError::TooManyColons(value));
83		}
84
85		let (prefix, suffix) = {
86			if semicolon_counts == 1 {
87				let result: Vec<&str> = value.split(':').take(2).collect();
88
89				(result[0], result[1])
90			}
91			else {
92				let result: &str = &value;
93				("minecraft", result)
94			}
95		};
96
97		let result = Namespace::new(prefix, suffix);
98		Ok(result)
99	}
100}
101
102/// Error handling for Namespace::decode() method
103#[derive(Debug, PartialEq)]
104pub enum DecodeError {
105	/// Error cause from 'regex' crate
106	RegexError(regex::Error),
107	/// Cause when there is invalid character inside namespace.
108	/// The original string is attached to this error.
109	InvalidNamespace(String),
110	/// Cause when there are too many colons inside namespace.
111	/// The original string is attached to this error.
112	TooManyColons(String)
113}
114
115use colored::*;
116/// Create Namespace from &str.  
117/// This will `unwrap()` error emit from `Namespace::decode()` function.
118impl From<&str> for Namespace {
119	fn from(value: &str) -> Namespace {
120		Namespace::decode(value).unwrap()
121	}
122}
123impl From<regex::Error> for DecodeError {
124	fn from(error: regex::Error) -> DecodeError {
125		DecodeError::RegexError(error)
126	}
127}
128impl fmt::Display for DecodeError {
129	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
130		match self {
131			DecodeError::RegexError(error) => write!(f, "{}", error),
132			DecodeError::InvalidNamespace(original) => write!(f, "'{}' contain invalid character ({})", original.red(), format!("/{}/", NAMESPACE_RULE.yellow()).red()),
133			DecodeError::TooManyColons(original) => write!(f, "'{}' can only contain at most 1 colon.", original.cyan()),
134		}
135	}
136}
137
138use std::fmt;
139impl fmt::Debug for Namespace {
140	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141		write!(f, "{}:{}", self.prefix, self.suffix)
142	}
143}
144
145#[cfg(test)]
146mod tests {
147	use super::*;
148
149	#[test]
150	fn decode_namespace_with_prefix() {
151		assert_eq!(
152			Namespace::from("boomber:test"),
153			Namespace {
154				prefix: String::from("boomber"),
155				suffix: String::from("test")
156			}
157		);
158	}
159
160	#[test]
161	#[should_panic]
162	fn panic_on_invalid_namespace() {
163		Namespace::from("this:namespace:IS invalid");
164	}
165
166	#[test]
167	fn decode_namespace_without_prefix() {
168		assert_eq!(
169			Namespace::from("no_prefix"),
170			Namespace {
171				prefix: String::from("minecraft"),
172				suffix: String::from("no_prefix")
173			}
174		);
175	}
176
177	#[test]
178	#[should_panic]
179	fn panic_on_invalid_namespace_without_prefix() {
180		Namespace::from("This Namespace Is Not Valid");
181	}
182}