|
77 | 77 | use once_cell::sync::Lazy; |
78 | 78 | use std::str::FromStr; |
79 | 79 | use validator::ValidationError; |
| 80 | +use serde::{Deserialize, Serialize}; |
| 81 | + |
| 82 | +use crate::slug::Slug; |
80 | 83 |
|
81 | 84 | pub const ETCD_ROOT_PATH: &str = "dynamo://"; |
82 | 85 | pub const COMPONENT_KEYWORD: &str = "_component_"; |
@@ -308,7 +311,7 @@ impl DynamoPath { |
308 | 311 |
|
309 | 312 | /// Pure data descriptor for component identification |
310 | 313 | /// Owns the canonical path format and validation logic |
311 | | -#[derive(Debug, Clone, PartialEq, Eq)] |
| 314 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
312 | 315 | pub struct Identifier { |
313 | 316 | namespace: String, |
314 | 317 | component: Option<String>, |
@@ -383,6 +386,29 @@ impl Identifier { |
383 | 386 |
|
384 | 387 | Ok(()) |
385 | 388 | } |
| 389 | + |
| 390 | + /// Generate a slugified subject string for event publishing |
| 391 | + pub fn slug(&self) -> Slug { |
| 392 | + Slug::slugify_unique(&self.to_string()) |
| 393 | + } |
| 394 | + |
| 395 | + /// Create a namespace-only identifier from this identifier |
| 396 | + pub fn to_namespace(&self) -> Identifier { |
| 397 | + Identifier { |
| 398 | + namespace: self.namespace.clone(), |
| 399 | + component: None, |
| 400 | + endpoint: None, |
| 401 | + } |
| 402 | + } |
| 403 | + |
| 404 | + /// Create a component identifier from this identifier (requires component to be present) |
| 405 | + pub fn to_component(&self) -> Option<Identifier> { |
| 406 | + self.component.as_ref().map(|comp| Identifier { |
| 407 | + namespace: self.namespace.clone(), |
| 408 | + component: Some(comp.clone()), |
| 409 | + endpoint: None, |
| 410 | + }) |
| 411 | + } |
386 | 412 | } |
387 | 413 |
|
388 | 414 | impl std::fmt::Display for Identifier { |
@@ -421,7 +447,7 @@ impl TryFrom<&str> for Identifier { |
421 | 447 |
|
422 | 448 | /// Identifier extended with instance_id (lease_id) |
423 | 449 | /// Immutable - identifier cannot be changed after construction |
424 | | -#[derive(Debug, Clone, PartialEq, Eq)] |
| 450 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
425 | 451 | pub struct Instance { |
426 | 452 | identifier: Identifier, // Private to enforce immutability |
427 | 453 | instance_id: Option<i64>, |
@@ -481,6 +507,15 @@ impl Instance { |
481 | 507 | pub fn instance_id(&self) -> Option<i64> { |
482 | 508 | self.instance_id |
483 | 509 | } |
| 510 | + |
| 511 | + pub fn is_static(&self) -> bool { |
| 512 | + self.is_static |
| 513 | + } |
| 514 | + |
| 515 | + /// Generate a slugified subject string for event publishing |
| 516 | + pub fn slug(&self) -> Slug { |
| 517 | + Slug::slugify_unique(&self.to_string()) |
| 518 | + } |
484 | 519 | } |
485 | 520 |
|
486 | 521 | impl std::fmt::Display for Instance { |
@@ -525,14 +560,14 @@ impl TryFrom<&str> for Instance { |
525 | 560 |
|
526 | 561 | /// Descriptor with additional path segments for extended paths |
527 | 562 | /// Always inserts _path_ before the segments |
528 | | -#[derive(Debug, Clone, PartialEq, Eq)] |
| 563 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
529 | 564 | pub struct Keys { |
530 | 565 | base: KeysBase, // Either Identifier or Instance |
531 | 566 | keys: Vec<String>, |
532 | 567 | } |
533 | 568 |
|
534 | 569 | /// Base can be either Identifier or Instance |
535 | | -#[derive(Debug, Clone, PartialEq, Eq)] |
| 570 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
536 | 571 | pub enum KeysBase { |
537 | 572 | Identifier(Identifier), |
538 | 573 | Instance(Instance), |
@@ -579,6 +614,11 @@ impl Keys { |
579 | 614 | &self.keys |
580 | 615 | } |
581 | 616 |
|
| 617 | + /// Generate a slugified subject string for event publishing |
| 618 | + pub fn slug(&self) -> Slug { |
| 619 | + Slug::slugify_unique(&self.to_string()) |
| 620 | + } |
| 621 | + |
582 | 622 | } |
583 | 623 |
|
584 | 624 | impl std::fmt::Display for Keys { |
@@ -774,6 +814,47 @@ mod tests { |
774 | 814 | assert_eq!(id.endpoint_name(), Some("http")); |
775 | 815 | } |
776 | 816 |
|
| 817 | + #[test] |
| 818 | + fn test_identifier_conversions() { |
| 819 | + // Create a full endpoint identifier |
| 820 | + let full_id = Identifier::new_endpoint("production.api", "gateway", "http").unwrap(); |
| 821 | + assert_eq!(full_id.to_string(), "dynamo://production.api/_component_/gateway/_endpoint_/http"); |
| 822 | + |
| 823 | + // Convert to namespace-only |
| 824 | + let ns_id = full_id.to_namespace(); |
| 825 | + assert_eq!(ns_id.to_string(), "dynamo://production.api"); |
| 826 | + assert_eq!(ns_id.namespace_name(), "production.api"); |
| 827 | + assert_eq!(ns_id.component_name(), None); |
| 828 | + assert_eq!(ns_id.endpoint_name(), None); |
| 829 | + |
| 830 | + // Convert to component-only |
| 831 | + let comp_id = full_id.to_component().unwrap(); |
| 832 | + assert_eq!(comp_id.to_string(), "dynamo://production.api/_component_/gateway"); |
| 833 | + assert_eq!(comp_id.namespace_name(), "production.api"); |
| 834 | + assert_eq!(comp_id.component_name(), Some("gateway")); |
| 835 | + assert_eq!(comp_id.endpoint_name(), None); |
| 836 | + |
| 837 | + // Test with component-only identifier |
| 838 | + let comp_only = Identifier::new_component("ns", "comp").unwrap(); |
| 839 | + |
| 840 | + let ns_from_comp = comp_only.to_namespace(); |
| 841 | + assert_eq!(ns_from_comp.to_string(), "dynamo://ns"); |
| 842 | + |
| 843 | + let comp_from_comp = comp_only.to_component().unwrap(); |
| 844 | + assert_eq!(comp_from_comp.to_string(), "dynamo://ns/_component_/comp"); |
| 845 | + assert_eq!(comp_from_comp, comp_only); |
| 846 | + |
| 847 | + // Test with namespace-only identifier |
| 848 | + let ns_only = Identifier::new_namespace("ns").unwrap(); |
| 849 | + |
| 850 | + let ns_from_ns = ns_only.to_namespace(); |
| 851 | + assert_eq!(ns_from_ns.to_string(), "dynamo://ns"); |
| 852 | + assert_eq!(ns_from_ns, ns_only); |
| 853 | + |
| 854 | + // Should return None when trying to get component from namespace-only |
| 855 | + assert!(ns_only.to_component().is_none()); |
| 856 | + } |
| 857 | + |
777 | 858 | #[test] |
778 | 859 | fn test_instance_dynamic_and_static_creation_and_parsing() { |
779 | 860 | // Dynamic instance creation |
|
0 commit comments