@@ -11,11 +11,8 @@ use crate::{
11
11
12
12
#[ cfg( feature = "webhooks" ) ]
13
13
use 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 ,
19
16
} ;
20
17
21
18
#[ cfg( feature = "alerting" ) ]
@@ -27,8 +24,9 @@ use crate::metrics::MetricsConfig;
27
24
use chrono:: Duration ;
28
25
use serde:: { Deserialize , Serialize } ;
29
26
use std:: { collections:: HashMap , path:: PathBuf , time:: Duration as StdDuration } ;
27
+ use crate :: streaming:: StreamConfig ;
30
28
31
- /// Module for serializing std::time::Duration as seconds
29
+ /// Module for serializing std::time::Duration as human-readable strings
32
30
mod duration_secs {
33
31
use serde:: { Deserialize , Deserializer , Serializer } ;
34
32
use std:: time:: Duration ;
@@ -37,17 +35,78 @@ mod duration_secs {
37
35
where
38
36
S : Serializer ,
39
37
{
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
+ }
41
48
}
42
49
43
50
pub fn deserialize < ' de , D > ( deserializer : D ) -> Result < Duration , D :: Error >
44
51
where
45
52
D : Deserializer < ' de > ,
46
53
{
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
+ }
49
85
}
50
86
}
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
+
51
110
use uuid:: Uuid ;
52
111
53
112
/// Main configuration for the Hammerwork job queue system.
@@ -283,62 +342,6 @@ pub struct WebhookConfigs {
283
342
pub global_settings : WebhookGlobalSettings ,
284
343
}
285
344
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
- }
342
345
343
346
/// Global webhook settings
344
347
#[ cfg( feature = "webhooks" ) ]
@@ -379,64 +382,6 @@ pub struct StreamingConfigs {
379
382
pub global_settings : StreamingGlobalSettings ,
380
383
}
381
384
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
- }
440
385
441
386
/// Simple event filter for when webhooks feature is disabled
442
387
#[ cfg( not( feature = "webhooks" ) ) ]
@@ -695,7 +640,6 @@ mod tests {
695
640
}
696
641
697
642
#[ test]
698
- #[ ignore] // TODO: Fix Duration serialization in TOML
699
643
fn test_config_file_operations ( ) {
700
644
let dir = tempdir ( ) . unwrap ( ) ;
701
645
let config_path = dir. path ( ) . join ( "hammerwork.toml" ) ;
@@ -736,6 +680,98 @@ mod tests {
736
680
}
737
681
}
738
682
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
+
739
775
#[ cfg( feature = "webhooks" ) ]
740
776
#[ test]
741
777
fn test_webhook_config ( ) {
0 commit comments