1use crate::Value;
11use indexmap::IndexMap;
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PropertySourceType {
22 CommandLine,
25
26 SystemEnvironment,
29
30 SystemProperties,
33
34 ApplicationProperties,
37
38 ApplicationYaml,
41
42 ApplicationToml,
45
46 External,
49
50 Custom,
53}
54
55impl PropertySourceType {
56 pub fn default_order(&self) -> u32 {
59 match self {
60 PropertySourceType::CommandLine => 100,
61 PropertySourceType::SystemEnvironment => 200,
62 PropertySourceType::SystemProperties => 300,
63 PropertySourceType::ApplicationProperties => 400,
64 PropertySourceType::ApplicationYaml => 500,
65 PropertySourceType::ApplicationToml => 600,
66 PropertySourceType::External => 700,
67 PropertySourceType::Custom => 800,
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
81pub struct PropertySource {
82 name: String,
85
86 properties: IndexMap<String, Value>,
89
90 source_type: PropertySourceType,
93
94 order: u32,
97
98 file_path: Option<PathBuf>,
101}
102
103impl PropertySource {
104 pub fn new(name: impl Into<String>) -> Self {
107 let name = name.into();
108 let source_type = Self::infer_source_type(&name);
109
110 Self {
111 name,
112 properties: IndexMap::new(),
113 source_type,
114 order: source_type.default_order(),
115 file_path: None,
116 }
117 }
118
119 pub fn with_map(name: impl Into<String>, map: HashMap<String, Value>) -> Self {
122 let mut source = Self::new(name);
123 source.properties = map.into_iter().collect();
124 source
125 }
126
127 fn infer_source_type(name: &str) -> PropertySourceType {
130 let lower = name.to_lowercase();
131 if lower.contains("command") || lower.contains("argv") {
132 PropertySourceType::CommandLine
133 } else if lower.contains("env") {
134 PropertySourceType::SystemEnvironment
135 } else if lower.contains("yaml") || lower.contains("yml") {
136 PropertySourceType::ApplicationYaml
137 } else if lower.contains("toml") {
138 PropertySourceType::ApplicationToml
139 } else if lower.contains("properties") || lower.contains("props") {
140 PropertySourceType::ApplicationProperties
141 } else if lower.contains("external") {
142 PropertySourceType::External
143 } else {
144 PropertySourceType::Custom
145 }
146 }
147
148 pub fn name(&self) -> &str {
151 &self.name
152 }
153
154 pub fn properties(&self) -> &IndexMap<String, Value> {
157 &self.properties
158 }
159
160 pub fn source_type(&self) -> PropertySourceType {
163 self.source_type
164 }
165
166 pub fn order(&self) -> u32 {
169 self.order
170 }
171
172 pub fn file_path(&self) -> Option<&PathBuf> {
175 self.file_path.as_ref()
176 }
177
178 pub fn set_file_path(&mut self, path: PathBuf) {
181 self.file_path = Some(path);
182 }
183
184 pub fn set_order(&mut self, order: u32) {
187 self.order = order;
188 }
189
190 pub fn put(&mut self, key: impl Into<String>, value: impl Into<Value>) {
193 self.properties.insert(key.into(), value.into());
194 }
195
196 pub fn get(&self, key: &str) -> Option<Value> {
199 self.properties.get(key).cloned()
200 }
201
202 pub fn contains_key(&self, key: &str) -> bool {
205 self.properties.contains_key(key)
206 }
207
208 pub fn remove(&mut self, key: &str) -> Option<Value> {
211 self.properties.shift_remove(key)
212 }
213
214 pub fn keys(&self) -> impl Iterator<Item = &String> {
217 self.properties.keys()
218 }
219
220 pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
223 self.properties.iter()
224 }
225
226 pub fn len(&self) -> usize {
229 self.properties.len()
230 }
231
232 pub fn is_empty(&self) -> bool {
235 self.properties.is_empty()
236 }
237
238 pub fn merge(&mut self, other: PropertySource) {
241 for (key, value) in other.properties {
242 self.properties.insert(key, value);
243 }
244 }
245}
246
247pub struct PropertySourceBuilder {
253 source: PropertySource,
254}
255
256impl PropertySourceBuilder {
257 pub fn new(name: impl Into<String>) -> Self {
260 Self {
261 source: PropertySource::new(name),
262 }
263 }
264
265 pub fn source_type(mut self, source_type: PropertySourceType) -> Self {
268 self.source.source_type = source_type;
269 self
270 }
271
272 pub fn order(mut self, order: u32) -> Self {
275 self.source.order = order;
276 self
277 }
278
279 pub fn file_path(mut self, path: PathBuf) -> Self {
282 self.source.file_path = Some(path);
283 self
284 }
285
286 pub fn put(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
289 self.source.put(key, value);
290 self
291 }
292
293 pub fn put_all(&mut self, map: HashMap<String, Value>) -> &mut Self {
296 for (key, value) in map {
297 self.source.put(key, value);
298 }
299 self
300 }
301
302 pub fn build(self) -> PropertySource {
305 self.source
306 }
307}
308
309impl Default for PropertySource {
310 fn default() -> Self {
311 Self::new("default")
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
326 fn test_source_type_default_order() {
327 assert_eq!(PropertySourceType::CommandLine.default_order(), 100);
328 assert_eq!(PropertySourceType::SystemEnvironment.default_order(), 200);
329 assert_eq!(PropertySourceType::SystemProperties.default_order(), 300);
330 assert_eq!(PropertySourceType::ApplicationProperties.default_order(), 400);
331 assert_eq!(PropertySourceType::ApplicationYaml.default_order(), 500);
332 assert_eq!(PropertySourceType::ApplicationToml.default_order(), 600);
333 assert_eq!(PropertySourceType::External.default_order(), 700);
334 assert_eq!(PropertySourceType::Custom.default_order(), 800);
335 }
336
337 #[test]
340 fn test_source_type_priority_ordering() {
341 assert!(
342 PropertySourceType::CommandLine.default_order()
343 < PropertySourceType::SystemEnvironment.default_order()
344 );
345 assert!(
346 PropertySourceType::SystemEnvironment.default_order()
347 < PropertySourceType::ApplicationProperties.default_order()
348 );
349 assert!(
350 PropertySourceType::ApplicationProperties.default_order()
351 < PropertySourceType::Custom.default_order()
352 );
353 }
354
355 #[test]
362 fn test_new_property_source() {
363 let source = PropertySource::new("test-source");
364 assert_eq!(source.name(), "test-source");
365 assert_eq!(source.source_type(), PropertySourceType::Custom);
366 assert!(source.is_empty());
367 assert_eq!(source.len(), 0);
368 assert!(source.file_path().is_none());
369 }
370
371 #[test]
374 fn test_infer_source_type_command_line() {
375 let source = PropertySource::new("commandArgs");
376 assert_eq!(source.source_type(), PropertySourceType::CommandLine);
377 }
378
379 #[test]
382 fn test_infer_source_type_environment() {
383 let source = PropertySource::new("envVars");
384 assert_eq!(source.source_type(), PropertySourceType::SystemEnvironment);
385 }
386
387 #[test]
390 fn test_infer_source_type_yaml() {
391 let source = PropertySource::new("application-yaml");
392 assert_eq!(source.source_type(), PropertySourceType::ApplicationYaml);
393 }
394
395 #[test]
398 fn test_infer_source_type_toml() {
399 let source = PropertySource::new("config.toml");
400 assert_eq!(source.source_type(), PropertySourceType::ApplicationToml);
401 }
402
403 #[test]
406 fn test_infer_source_type_properties() {
407 let source = PropertySource::new("app-properties");
408 assert_eq!(source.source_type(), PropertySourceType::ApplicationProperties);
409 }
410
411 #[test]
414 fn test_infer_source_type_external() {
415 let source = PropertySource::new("external-config");
416 assert_eq!(source.source_type(), PropertySourceType::External);
417 }
418
419 #[test]
422 fn test_with_map() {
423 let mut map = HashMap::new();
424 map.insert("key1".to_string(), Value::string("value1"));
425 map.insert("key2".to_string(), Value::integer(42));
426
427 let source = PropertySource::with_map("test-map", map);
428 assert_eq!(source.len(), 2);
429 assert!(source.contains_key("key1"));
430 assert!(source.contains_key("key2"));
431 assert_eq!(source.get("key1").unwrap().as_str(), Some("value1"));
432 assert_eq!(source.get("key2").unwrap().as_i64(), Some(42));
433 }
434
435 #[test]
438 fn test_put_get_remove() {
439 let mut source = PropertySource::new("test");
440 assert!(!source.contains_key("name"));
441
442 source.put("name", "hiver");
443 assert!(source.contains_key("name"));
444 assert_eq!(source.get("name").unwrap().as_str(), Some("hiver"));
445
446 source.put("name", "updated");
448 assert_eq!(source.get("name").unwrap().as_str(), Some("updated"));
449
450 let removed = source.remove("name");
452 assert_eq!(removed.unwrap().as_str(), Some("updated"));
453 assert!(!source.contains_key("name"));
454 }
455
456 #[test]
459 fn test_keys_and_iter() {
460 let mut source = PropertySource::new("test");
461 source.put("a", 1);
462 source.put("b", 2);
463 source.put("c", 3);
464
465 let keys: Vec<&String> = source.keys().collect();
466 assert_eq!(keys.len(), 3);
467
468 let entries: Vec<_> = source.iter().collect();
469 assert_eq!(entries.len(), 3);
470 }
471
472 #[test]
475 fn test_order_and_file_path() {
476 let mut source = PropertySource::new("test");
477 assert_eq!(source.order(), PropertySourceType::Custom.default_order());
478
479 source.set_order(50);
480 assert_eq!(source.order(), 50);
481
482 source.set_file_path(PathBuf::from("/etc/hiver/app.yaml"));
483 assert_eq!(source.file_path(), Some(&PathBuf::from("/etc/hiver/app.yaml")));
484 }
485
486 #[test]
489 fn test_merge() {
490 let mut source1 = PropertySource::new("source1");
491 source1.put("shared", "from_source1");
492 source1.put("only_in_1", "value1");
493
494 let mut source2 = PropertySource::new("source2");
495 source2.put("shared", "from_source2");
496 source2.put("only_in_2", "value2");
497
498 source1.merge(source2);
499 assert_eq!(source1.len(), 3);
501 assert_eq!(source1.get("shared").unwrap().as_str(), Some("from_source2"));
503 assert_eq!(source1.get("only_in_1").unwrap().as_str(), Some("value1"));
504 assert_eq!(source1.get("only_in_2").unwrap().as_str(), Some("value2"));
505 }
506
507 #[test]
510 fn test_default() {
511 let source = PropertySource::default();
512 assert_eq!(source.name(), "default");
513 assert!(source.is_empty());
514 }
515
516 #[test]
523 fn test_builder_basic() {
524 let source = PropertySourceBuilder::new("built-source")
525 .source_type(PropertySourceType::CommandLine)
526 .order(50)
527 .file_path(PathBuf::from("/tmp/config"))
528 .build();
529
530 assert_eq!(source.name(), "built-source");
531 assert_eq!(source.source_type(), PropertySourceType::CommandLine);
532 assert_eq!(source.order(), 50);
533 assert_eq!(source.file_path(), Some(&PathBuf::from("/tmp/config")));
534 }
535
536 #[test]
539 fn test_builder_put_properties() {
540 let mut builder = PropertySourceBuilder::new("props");
541 builder.put("key1", "value1");
542 builder.put("key2", 42);
543
544 let mut extra = HashMap::new();
545 extra.insert("key3".to_string(), Value::bool(true));
546 builder.put_all(extra);
547
548 let source = builder.build();
549 assert_eq!(source.len(), 3);
550 assert_eq!(source.get("key1").unwrap().as_str(), Some("value1"));
551 assert_eq!(source.get("key2").unwrap().as_i64(), Some(42));
552 assert_eq!(source.get("key3").unwrap().as_bool(), Some(true));
553 }
554}