@@ -11,11 +11,8 @@ use crate::{
1111
1212#[ cfg( feature = "webhooks" ) ]
1313use crate :: {
14- events:: EventFilter ,
15- streaming:: {
16- BufferConfig , PartitioningStrategy , SerializationFormat , StreamBackend , StreamRetryPolicy ,
17- } ,
18- webhooks:: { HttpMethod , RetryPolicy as WebhookRetryPolicy , WebhookAuth } ,
14+ streaming:: StreamBackend ,
15+ webhooks:: WebhookConfig ,
1916} ;
2017
2118#[ cfg( feature = "alerting" ) ]
@@ -27,8 +24,9 @@ use crate::metrics::MetricsConfig;
2724use chrono:: Duration ;
2825use serde:: { Deserialize , Serialize } ;
2926use std:: { collections:: HashMap , path:: PathBuf , time:: Duration as StdDuration } ;
27+ use crate :: streaming:: StreamConfig ;
3028
31- /// Module for serializing std::time::Duration as seconds
29+ /// Module for serializing std::time::Duration as human-readable strings
3230mod duration_secs {
3331 use serde:: { Deserialize , Deserializer , Serializer } ;
3432 use std:: time:: Duration ;
@@ -37,17 +35,78 @@ mod duration_secs {
3735 where
3836 S : Serializer ,
3937 {
40- serializer. serialize_u64 ( duration. as_secs ( ) )
38+ let secs = duration. as_secs ( ) ;
39+ if secs == 0 {
40+ serializer. serialize_str ( "0s" )
41+ } else if secs % 3600 == 0 {
42+ serializer. serialize_str ( & format ! ( "{}h" , secs / 3600 ) )
43+ } else if secs % 60 == 0 {
44+ serializer. serialize_str ( & format ! ( "{}m" , secs / 60 ) )
45+ } else {
46+ serializer. serialize_str ( & format ! ( "{}s" , secs) )
47+ }
4148 }
4249
4350 pub fn deserialize < ' de , D > ( deserializer : D ) -> Result < Duration , D :: Error >
4451 where
4552 D : Deserializer < ' de > ,
4653 {
47- let secs = u64:: deserialize ( deserializer) ?;
48- Ok ( Duration :: from_secs ( secs) )
54+ use serde:: de:: Error ;
55+
56+ let s = String :: deserialize ( deserializer) ?;
57+ parse_duration ( & s) . map_err ( D :: Error :: custom)
58+ }
59+
60+ /// Parse a duration string like "30s", "5m", "1h", "90", etc.
61+ fn parse_duration ( s : & str ) -> Result < Duration , String > {
62+ let s = s. trim ( ) ;
63+
64+ // Handle just numbers (assume seconds)
65+ if let Ok ( secs) = s. parse :: < u64 > ( ) {
66+ return Ok ( Duration :: from_secs ( secs) ) ;
67+ }
68+
69+ // Handle suffixed durations
70+ if s. len ( ) < 2 {
71+ return Err ( format ! ( "Invalid duration format: {}" , s) ) ;
72+ }
73+
74+ let ( num_str, suffix) = s. split_at ( s. len ( ) - 1 ) ;
75+ let num: u64 = num_str. parse ( )
76+ . map_err ( |_| format ! ( "Invalid number in duration: {}" , num_str) ) ?;
77+
78+ match suffix {
79+ "s" => Ok ( Duration :: from_secs ( num) ) ,
80+ "m" => Ok ( Duration :: from_secs ( num * 60 ) ) ,
81+ "h" => Ok ( Duration :: from_secs ( num * 3600 ) ) ,
82+ "d" => Ok ( Duration :: from_secs ( num * 86400 ) ) ,
83+ _ => Err ( format ! ( "Invalid duration suffix: {}. Use s, m, h, or d" , suffix) ) ,
84+ }
4985 }
5086}
87+
88+ /// Module for serializing UUID as string for TOML compatibility
89+ mod uuid_string {
90+ use serde:: { Deserialize , Deserializer , Serializer } ;
91+ use uuid:: Uuid ;
92+
93+ pub fn serialize < S > ( uuid : & Uuid , serializer : S ) -> Result < S :: Ok , S :: Error >
94+ where
95+ S : Serializer ,
96+ {
97+ serializer. serialize_str ( & uuid. to_string ( ) )
98+ }
99+
100+ pub fn deserialize < ' de , D > ( deserializer : D ) -> Result < Uuid , D :: Error >
101+ where
102+ D : Deserializer < ' de > ,
103+ {
104+ use serde:: de:: Error ;
105+ let s = String :: deserialize ( deserializer) ?;
106+ Uuid :: parse_str ( & s) . map_err ( D :: Error :: custom)
107+ }
108+ }
109+
51110use uuid:: Uuid ;
52111
53112/// Main configuration for the Hammerwork job queue system.
@@ -283,62 +342,6 @@ pub struct WebhookConfigs {
283342 pub global_settings : WebhookGlobalSettings ,
284343}
285344
286- /// Individual webhook configuration
287- #[ cfg( feature = "webhooks" ) ]
288- #[ derive( Debug , Clone , Serialize , Deserialize ) ]
289- pub struct WebhookConfig {
290- /// Unique identifier
291- pub id : Uuid ,
292-
293- /// Human-readable name
294- pub name : String ,
295-
296- /// Webhook URL
297- pub url : String ,
298-
299- /// HTTP method
300- pub method : HttpMethod ,
301-
302- /// Custom headers
303- pub headers : HashMap < String , String > ,
304-
305- /// Event filter
306- pub filter : EventFilter ,
307-
308- /// Retry policy
309- pub retry_policy : WebhookRetryPolicy ,
310-
311- /// Authentication
312- pub auth : Option < WebhookAuth > ,
313-
314- /// Request timeout in seconds
315- pub timeout_secs : u64 ,
316-
317- /// Whether enabled
318- pub enabled : bool ,
319-
320- /// Secret for HMAC signatures
321- pub secret : Option < String > ,
322- }
323-
324- #[ cfg( feature = "webhooks" ) ]
325- impl Default for WebhookConfig {
326- fn default ( ) -> Self {
327- Self {
328- id : Uuid :: new_v4 ( ) ,
329- name : "Default Webhook" . to_string ( ) ,
330- url : "https://example.com/webhook" . to_string ( ) ,
331- method : HttpMethod :: Post ,
332- headers : HashMap :: new ( ) ,
333- filter : EventFilter :: default ( ) ,
334- retry_policy : WebhookRetryPolicy :: default ( ) ,
335- auth : None ,
336- timeout_secs : 30 ,
337- enabled : true ,
338- secret : None ,
339- }
340- }
341- }
342345
343346/// Global webhook settings
344347#[ cfg( feature = "webhooks" ) ]
@@ -379,64 +382,6 @@ pub struct StreamingConfigs {
379382 pub global_settings : StreamingGlobalSettings ,
380383}
381384
382- /// Individual stream configuration
383- #[ derive( Debug , Clone , Serialize , Deserialize ) ]
384- pub struct StreamConfig {
385- /// Unique identifier
386- pub id : Uuid ,
387-
388- /// Human-readable name
389- pub name : String ,
390-
391- /// Streaming backend
392- pub backend : StreamBackend ,
393-
394- /// Event filter
395- #[ cfg( feature = "webhooks" ) ]
396- pub filter : EventFilter ,
397-
398- #[ cfg( not( feature = "webhooks" ) ) ]
399- /// Simple event filter when webhooks feature is disabled
400- pub filter : SimpleEventFilter ,
401-
402- /// Partitioning strategy
403- pub partitioning : PartitioningStrategy ,
404-
405- /// Serialization format
406- pub serialization : SerializationFormat ,
407-
408- /// Retry policy
409- pub retry_policy : StreamRetryPolicy ,
410-
411- /// Whether enabled
412- pub enabled : bool ,
413-
414- /// Buffer configuration
415- pub buffer_config : BufferConfig ,
416- }
417-
418- impl Default for StreamConfig {
419- fn default ( ) -> Self {
420- Self {
421- id : Uuid :: new_v4 ( ) ,
422- name : "Default Stream" . to_string ( ) ,
423- backend : StreamBackend :: Kafka {
424- brokers : vec ! [ "localhost:9092" . to_string( ) ] ,
425- topic : "hammerwork-events" . to_string ( ) ,
426- config : HashMap :: new ( ) ,
427- } ,
428- #[ cfg( feature = "webhooks" ) ]
429- filter : EventFilter :: default ( ) ,
430- #[ cfg( not( feature = "webhooks" ) ) ]
431- filter : SimpleEventFilter :: default ( ) ,
432- partitioning : PartitioningStrategy :: QueueName ,
433- serialization : SerializationFormat :: Json ,
434- retry_policy : StreamRetryPolicy :: default ( ) ,
435- enabled : true ,
436- buffer_config : BufferConfig :: default ( ) ,
437- }
438- }
439- }
440385
441386/// Simple event filter for when webhooks feature is disabled
442387#[ cfg( not( feature = "webhooks" ) ) ]
@@ -695,7 +640,6 @@ mod tests {
695640 }
696641
697642 #[ test]
698- #[ ignore] // TODO: Fix Duration serialization in TOML
699643 fn test_config_file_operations ( ) {
700644 let dir = tempdir ( ) . unwrap ( ) ;
701645 let config_path = dir. path ( ) . join ( "hammerwork.toml" ) ;
@@ -736,6 +680,98 @@ mod tests {
736680 }
737681 }
738682
683+ #[ test]
684+ fn test_duration_serialization ( ) {
685+ use tempfile:: tempdir;
686+
687+ let dir = tempdir ( ) . unwrap ( ) ;
688+ let config_path = dir. path ( ) . join ( "duration_test.toml" ) ;
689+
690+ // Create config with various durations
691+ let mut config = HammerworkConfig :: new ( ) ;
692+ config. worker . polling_interval = StdDuration :: from_secs ( 30 ) ; // Should serialize as "30s"
693+ config. worker . job_timeout = StdDuration :: from_secs ( 300 ) ; // Should serialize as "5m"
694+
695+ // Save to TOML
696+ config. save_to_file ( config_path. to_str ( ) . unwrap ( ) ) . unwrap ( ) ;
697+
698+ // Read the TOML content to verify human-readable format
699+ let toml_content = std:: fs:: read_to_string ( & config_path) . unwrap ( ) ;
700+ assert ! ( toml_content. contains( "polling_interval = \" 30s\" " ) ) ;
701+ assert ! ( toml_content. contains( "job_timeout = \" 5m\" " ) ) ;
702+
703+ // Load back and verify values
704+ let loaded_config = HammerworkConfig :: from_file ( config_path. to_str ( ) . unwrap ( ) ) . unwrap ( ) ;
705+ assert_eq ! ( loaded_config. worker. polling_interval, StdDuration :: from_secs( 30 ) ) ;
706+ assert_eq ! ( loaded_config. worker. job_timeout, StdDuration :: from_secs( 300 ) ) ;
707+
708+ // Test parsing various duration formats
709+ let test_durations = [
710+ ( "30" , StdDuration :: from_secs ( 30 ) ) ,
711+ ( "30s" , StdDuration :: from_secs ( 30 ) ) ,
712+ ( "5m" , StdDuration :: from_secs ( 300 ) ) ,
713+ ( "2h" , StdDuration :: from_secs ( 7200 ) ) ,
714+ ( "1d" , StdDuration :: from_secs ( 86400 ) ) ,
715+ ] ;
716+
717+ for ( duration_str, expected) in test_durations. iter ( ) {
718+ let toml_content = format ! (
719+ r#"
720+ [database]
721+ url = "postgresql://localhost/test"
722+
723+ [worker]
724+ pool_size = 4
725+ polling_interval = "{}"
726+ job_timeout = "5m"
727+ autoscaling_enabled = false
728+ min_workers = 1
729+ max_workers = 10
730+ scale_up_threshold = 0.8
731+ scale_down_threshold = 0.2
732+ scale_check_interval = "30s"
733+
734+ [worker.priority_weights]
735+ background = 1
736+ low = 2
737+ normal = 5
738+ high = 10
739+ critical = 20
740+
741+ [worker.retry_strategy]
742+ max_attempts = 3
743+ initial_delay = "1s"
744+ max_delay = "60s"
745+ backoff_multiplier = 2.0
746+
747+ [events]
748+ enabled = true
749+ buffer_size = 1000
750+
751+ [streaming]
752+
753+ [archive]
754+ enabled = false
755+ retention_days = 30
756+ compression_enabled = false
757+
758+ [rate_limiting]
759+ enabled = false
760+ requests_per_second = 100
761+ burst_size = 200
762+
763+ [logging]
764+ level = "info"
765+ json_format = false
766+ "# ,
767+ duration_str
768+ ) ;
769+
770+ let config: HammerworkConfig = toml:: from_str ( & toml_content) . unwrap ( ) ;
771+ assert_eq ! ( config. worker. polling_interval, * expected, "Failed to parse duration: {}" , duration_str) ;
772+ }
773+ }
774+
739775 #[ cfg( feature = "webhooks" ) ]
740776 #[ test]
741777 fn test_webhook_config ( ) {
0 commit comments