1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! Generate a single character for use in a key.
//!
//! This is the same module used internally to generate keys.
//! The other modules are recommended, but this is
//! for when/if you need more options than what's
//! here by default.
//!
//! Public constants are the character sets
//! typically used to generate keys.

use crate::uuid::UUID;
use rand::Rng;

/// 123456789
pub const NUMBERS: &str = "0123456789";
/// abcdefghijklmnopqrstuvwxyz
pub const LETTERS: &str = "abcdefghijklmnopqrstuvwxyz";
/// abcdef
pub const HEX_LETTERS: &str = "abcdef";
/// -_.()~@
pub const SAFE_SP_CHARS: &str = "-_.()~@";
/// #%&*+={}\/<>?!$:'"`|
pub const UNSAFE_SP_CHARS: &str = r#"#%&*+={}\/<>?!$:'"`|"#;

// See this https://stackoverflow.com/a/40415059 for special chars
// Might want to look more into this later

/// Options for char()
#[derive(Debug, Copy, Clone)]
pub struct GenCharOpts {
	/// Generate numbers?
	pub nums: bool,
	/// Generate letters?
	pub letters: bool,
	/// Generate uppercase letters?
	pub upper: bool,
	/// Generate safe special characters?
	pub safe_sp_chars: bool,
	/// Generate unsafe special characters? (false is recommended)
	pub unsafe_sp_chars: bool,
}

/// Generate numbers and letters (no uppercase)
fn gen_uuid_nonstandard_char() -> char {
	let opts = GenCharOpts {
		nums: true,
		letters: true,
		upper: false,
		safe_sp_chars: false,
		unsafe_sp_chars: false,
	};

	char(opts)
}

/// Generate numbers or letters a-f
fn gen_uuid_v4_char() -> char {
	let mut charset = String::from("");

	charset.push_str(NUMBERS);
	charset.push_str(HEX_LETTERS);

	get_char_from_set(&charset)
}

/// Returns a random character from the given character set
fn get_char_from_set(charset: &str) -> char {
	let mut rng = rand::thread_rng();

	// get a rand index from chars
	let idx = rng.gen_range(0..charset.len());

	// get the value of the index
	let c = charset
		.chars()
		.nth(idx)
		.expect("Could not get value of char.");

	c
}

// Public API

/// Use a GenCharOpts to create a character set.
///
/// Ex:
/// ```
/// let opts = GenCharOpts {
///     nums: true,
///     letters: true,
///     upper: true,
///     safe_sp_chars: false,
///     unsafe_sp_chars: false,
/// };
/// ```
///
/// Will return -> `0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`
pub fn get_charset_from_opts(opts: GenCharOpts) -> String {
	let mut charset = String::from("");

	// Set allowed characters
	if opts.nums {
		charset.push_str(NUMBERS);
	}

	if opts.letters {
		charset.push_str(LETTERS);

		// This is nested because if letters aren't allowed,
		// uppercase letters obviously aren't allowed either.
		if opts.upper {
			charset.push_str(&LETTERS.to_uppercase());
		}
	}

	if opts.safe_sp_chars {
		charset.push_str(SAFE_SP_CHARS);
	}

	if opts.unsafe_sp_chars {
		charset.push_str(UNSAFE_SP_CHARS);
	}

	charset
}

/// Generates a char for a key. Use GenCharOpts for options.
pub fn char(opts: GenCharOpts) -> char {
	let charset = get_charset_from_opts(opts);

	get_char_from_set(&charset)
}

/// Generates a UUID char for the specified version.
///
/// Version input should be either '4' or 'n'.
///
/// Returns '0' if the input is invalid.
pub fn char_uuid(version: UUID) -> char {
	match version {
		UUID::V4 => gen_uuid_v4_char(),
		UUID::Nonstandard => gen_uuid_nonstandard_char(),
	}
	// Returns result of match version
}

/// Returns a random character from a custom character set
pub fn char_custom(charset: &str) -> char {
	get_char_from_set(charset)
}

// Tests

#[cfg(test)]
mod tests {
	use super::*;
	use crate::test_utils::{test_correct_chars, test_correct_uuid_chars};

	#[test]
	fn test_get_char_from_set() {
		let charset = "abc123";

		let mut success = true;

		for _i in 1..=10 {
			// Stop checking if the test has failed
			if !success {
				break;
			}

			let char = get_char_from_set(charset);
			success = test_correct_chars(String::from(char), charset)
		}

		if !success {
			panic!("get_char_from_set() generated an invalid character.");
		}
	}

	#[test]
	fn test_char() {
		let opts = GenCharOpts {
			nums: true,
			letters: true,
			upper: true,
			safe_sp_chars: false,
			unsafe_sp_chars: false,
		};

		let charset = get_charset_from_opts(opts);

		let mut success = true;

		for _i in 1..=10 {
			// Stop checking if the test has failed
			if !success {
				break;
			}

			let char = char(opts);
			success = test_correct_chars(String::from(char), &charset)
		}

		if !success {
			panic!("char() generated an invalid character.");
		}
	}

	fn test_uuid_char(version: UUID) -> bool {
		let mut success = true;

		for _i in 1..=10 {
			// Stop checking if the test has failed
			if !success {
				break;
			}

			let chunk = match version {
				UUID::V4 => String::from(gen_uuid_v4_char()),
				UUID::Nonstandard => String::from(gen_uuid_nonstandard_char()),
			};
			success = test_correct_uuid_chars(version, chunk);
		}

		success
	}

	#[test]
	fn test_uuid4_char() {
		let version = UUID::V4;
		let success = test_uuid_char(version);

		if !success {
			panic!("gen_uuid_v4_char() generated an invalid character.");
		}
	}

	#[test]
	fn test_uuidn_char() {
		let version = UUID::Nonstandard;
		let success = test_uuid_char(version);

		if !success {
			panic!(
				"gen_uuid_nonstandard_char() generated an invalid character."
			);
		}
	}
}