adaptive_pipeline_domain/value_objects/
processing_step_descriptor.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Processing Step Descriptor Value Object
9//!
10//! This module defines the processing step descriptor value object for the
11//! adaptive pipeline system. It provides a comprehensive description of
12//! processing steps with validation, parameters, and metadata.
13//!
14//! ## Overview
15//!
16//! The processing step descriptor provides:
17//!
18//! - **Step Definition**: Complete definition of processing steps
19//! - **Algorithm Validation**: Validated algorithm names and parameters
20//! - **Parameter Management**: Type-safe parameter handling
21//! - **Metadata Tracking**: Comprehensive metadata for each step
22//! - **Serialization**: Support for persistence and transmission
23//!
24//! ## Architecture
25//!
26//! The descriptor follows Domain-Driven Design principles:
27//!
28//! - **Value Object**: Immutable value object with equality semantics
29//! - **Rich Domain Model**: Encapsulates processing step business logic
30//! - **Validation**: Comprehensive validation of step definitions
31//! - **Type Safety**: Type-safe parameter and algorithm handling
32//!
33//! ## Key Features
34//!
35//! ### Step Definition
36//!
37//! - **Step Types**: Support for different processing step types
38//! - **Algorithm Specification**: Validated algorithm names and versions
39//! - **Parameter Configuration**: Type-safe parameter configuration
40//! - **Dependency Management**: Step dependency tracking
41//!
42//! ### Algorithm Management
43//!
44//! - **Name Validation**: Comprehensive algorithm name validation
45//! - **Version Control**: Algorithm version tracking and compatibility
46//! - **Parameter Schema**: Algorithm-specific parameter schemas
47//! - **Capability Detection**: Detect algorithm capabilities and features
48//!
49//! ### Parameter Handling
50//!
51//! - **Type Safety**: Type-safe parameter values
52//! - **Validation**: Parameter validation against schemas
53//! - **Default Values**: Support for default parameter values
54//! - **Documentation**: Parameter documentation and help text
55//!
56//! ## Usage Examples
57//!
58//! ### Basic Step Creation
59
60//!
61//! ### Algorithm Validation
62
63//!
64//! ### Parameter Management
65
66//!
67//! ### Step Composition and Chaining
68
69//!
70//! ### Serialization and Configuration
71
72//!
73//! ## Processing Step Types
74//!
75//! ### Built-in Step Types
76//!
77//! - **Validation**: Input validation and integrity checking
78//!   - Algorithms: checksum, signature, format validation
79//!   - Use case: Validate input files before processing
80//!
81//! - **Compression**: Data compression and decompression
82//!   - Algorithms: brotli, gzip, zstd, lz4, deflate
83//!   - Use case: Reduce file size for storage or transmission
84//!
85//! - **Encryption**: Data encryption and decryption
86//!   - Algorithms: aes-256-gcm, chacha20-poly1305, aes-128-gcm
87//!   - Use case: Secure data storage and transmission
88//!
89//! - **Transformation**: Data format transformation
90//!   - Algorithms: json-to-binary, xml-to-json, custom transforms
91//!   - Use case: Convert between different data formats
92//!
93//! - **Analysis**: Data analysis and metrics collection
94//!   - Algorithms: statistics, pattern detection, anomaly detection
95//!   - Use case: Analyze data patterns and collect metrics
96//!
97//! ### Custom Step Types
98//!
99//! Create custom step types by extending the ProcessingStepType enum:
100//!
101//! - **Domain-Specific**: Steps specific to your application domain
102//! - **Integration**: Steps for integrating with external systems
103//! - **Monitoring**: Steps for monitoring and alerting
104//!
105//! ## Algorithm Validation
106//!
107//! ### Name Format Rules
108//!
109//! - **Characters**: Alphanumeric, hyphens, and underscores only
110//! - **Case**: Converted to lowercase for consistency
111//! - **Length**: Must be non-empty after trimming
112//! - **Format**: Must match regex pattern `^[a-zA-Z0-9_-]+$`
113//!
114//! ### Validation Process
115//!
116//! 1. **Trim Whitespace**: Remove leading/trailing whitespace
117//! 2. **Empty Check**: Ensure name is not empty
118//! 3. **Character Validation**: Check allowed characters
119//! 4. **Normalization**: Convert to lowercase
120//! 5. **Registration**: Check against algorithm registry
121//!
122//! ## Parameter Management
123//!
124//! ### Parameter Types
125//!
126//! - **String Parameters**: Text values and identifiers
127//! - **Numeric Parameters**: Integer and floating-point values
128//! - **Boolean Parameters**: True/false flags
129//! - **Array Parameters**: Lists of values
130//! - **Object Parameters**: Nested parameter structures
131//!
132//! ### Parameter Validation
133//!
134//! - **Type Checking**: Validate parameter types
135//! - **Range Validation**: Check numeric ranges
136//! - **Format Validation**: Validate string formats
137//! - **Dependency Validation**: Check parameter dependencies
138//!
139//! ### Default Values
140//!
141//! - **Algorithm Defaults**: Default values for each algorithm
142//! - **Override Support**: Allow overriding default values
143//! - **Validation**: Validate default values
144//!
145//! ## Error Handling
146//!
147//! ### Validation Errors
148//!
149//! - **Invalid Algorithm**: Algorithm name is invalid
150//! - **Invalid Parameters**: Parameter values are invalid
151//! - **Missing Parameters**: Required parameters are missing
152//! - **Type Mismatch**: Parameter type doesn't match expected type
153//!
154//! ### Configuration Errors
155//!
156//! - **Invalid Step Type**: Step type is not supported
157//! - **Incompatible Parameters**: Parameters are incompatible
158//! - **Circular Dependencies**: Circular step dependencies detected
159//!
160//! ## Performance Considerations
161//!
162//! ### Memory Usage
163//!
164//! - **Efficient Storage**: Compact storage of step definitions
165//! - **String Interning**: Intern common algorithm names
166//! - **Parameter Optimization**: Optimize parameter storage
167//!
168//! ### Validation Performance
169//!
170//! - **Lazy Validation**: Validate only when necessary
171//! - **Caching**: Cache validation results
172//! - **Batch Validation**: Validate multiple steps together
173//!
174//! ## Integration
175//!
176//! The processing step descriptor integrates with:
177//!
178//! - **Processing Pipeline**: Define pipeline processing steps
179//! - **Configuration System**: Store and load step configurations
180//! - **Validation Framework**: Validate step definitions
181//! - **Execution Engine**: Execute defined processing steps
182//!
183//! ## Thread Safety
184//!
185//! The processing step descriptor is thread-safe:
186//!
187//! - **Immutable**: Descriptors are immutable after creation
188//! - **Safe Sharing**: Safe to share between threads
189//! - **Concurrent Access**: Safe concurrent access to descriptor data
190//!
191//! ## Future Enhancements
192//!
193//! Planned enhancements include:
194//!
195//! - **Dynamic Parameters**: Runtime parameter modification
196//! - **Parameter Templates**: Template-based parameter generation
197//! - **Advanced Validation**: More sophisticated validation rules
198//! - **Performance Optimization**: Further performance improvements
199
200use super::binary_file_format::ProcessingStepType;
201use crate::PipelineError;
202use serde::{Deserialize, Serialize};
203use std::collections::HashMap;
204
205/// Value object representing a validated algorithm name
206///
207/// This value object encapsulates algorithm names with comprehensive validation
208/// to ensure they meet the required format and naming conventions.
209///
210/// # Key Features
211///
212/// - **Name Validation**: Comprehensive validation of algorithm names
213/// - **Format Normalization**: Normalize names to lowercase
214/// - **Character Restrictions**: Only alphanumeric, hyphens, and underscores
215/// - **Immutability**: Algorithm names cannot be changed after creation
216///
217/// # Validation Rules
218///
219/// - Must be non-empty after trimming whitespace
220/// - Must contain only alphanumeric characters, hyphens, and underscores
221/// - Is normalized to lowercase for consistency
222/// - Must match the pattern: `^[a-zA-Z0-9_-]+$`
223///
224/// # Examples
225#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
226pub struct Algorithm(String);
227
228impl Algorithm {
229    /// Create a new Algorithm with validation
230    ///
231    /// # Purpose
232    /// Creates a validated algorithm name with format normalization.
233    /// Ensures algorithm names follow consistent naming conventions.
234    ///
235    /// # Why
236    /// Validated algorithm names provide:
237    /// - Prevention of configuration errors
238    /// - Consistent naming across systems
239    /// - Type-safe algorithm specification
240    /// - Cross-platform compatibility
241    ///
242    /// # Arguments
243    /// * `value` - Algorithm name (alphanumeric, hyphens, underscores)
244    ///
245    /// # Returns
246    /// * `Ok(Algorithm)` - Validated algorithm (normalized to lowercase)
247    /// * `Err(PipelineError)` - Invalid format
248    ///
249    /// # Errors
250    /// - Empty name after trimming
251    /// - Invalid characters (only alphanumeric, `-`, `_` allowed)
252    ///
253    /// # Examples
254    pub fn new(value: &str) -> Result<Self, PipelineError> {
255        let trimmed = value.trim();
256        if trimmed.is_empty() {
257            return Err(PipelineError::InvalidConfiguration(
258                "Algorithm cannot be empty".to_string(),
259            ));
260        }
261
262        // Validate algorithm name format (alphanumeric, hyphens, underscores)
263        if !trimmed.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
264            return Err(PipelineError::InvalidConfiguration(format!(
265                "Invalid algorithm name '{}': only alphanumeric, hyphens, and underscores allowed",
266                trimmed
267            )));
268        }
269
270        Ok(Algorithm(trimmed.to_lowercase()))
271    }
272
273    /// Get the algorithm name as a string
274    pub fn as_str(&self) -> &str {
275        &self.0
276    }
277
278    /// Convert to owned String
279    pub fn into_string(self) -> String {
280        self.0
281    }
282}
283
284impl std::fmt::Display for Algorithm {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "{}", self.0)
287    }
288}
289
290/// Value object representing validated stage parameters
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct StageParameters(HashMap<String, String>);
293
294impl StageParameters {
295    /// Create new empty parameters
296    pub fn new() -> Self {
297        Self(HashMap::new())
298    }
299
300    /// Create from existing HashMap with validation
301    pub fn from_map(map: HashMap<String, String>) -> Result<Self, PipelineError> {
302        // Validate parameter keys and values
303        for (key, value) in &map {
304            if key.trim().is_empty() {
305                return Err(PipelineError::InvalidConfiguration(
306                    "Parameter key cannot be empty".to_string(),
307                ));
308            }
309            if value.len() > 1024 {
310                return Err(PipelineError::InvalidConfiguration(format!(
311                    "Parameter value for '{}' exceeds maximum length of 1024 characters",
312                    key
313                )));
314            }
315        }
316        Ok(Self(map))
317    }
318
319    /// Add a parameter with validation
320    pub fn add_parameter(&mut self, key: &str, value: &str) -> Result<(), PipelineError> {
321        let trimmed_key = key.trim();
322        if trimmed_key.is_empty() {
323            return Err(PipelineError::InvalidConfiguration(
324                "Parameter key cannot be empty".to_string(),
325            ));
326        }
327        if value.len() > 1024 {
328            return Err(PipelineError::InvalidConfiguration(format!(
329                "Parameter value for '{}' exceeds maximum length of 1024 characters",
330                trimmed_key
331            )));
332        }
333
334        self.0.insert(trimmed_key.to_string(), value.to_string());
335        Ok(())
336    }
337
338    /// Get parameter value
339    pub fn get(&self, key: &str) -> Option<&String> {
340        self.0.get(key)
341    }
342
343    /// Get all parameters as HashMap reference
344    pub fn as_map(&self) -> &HashMap<String, String> {
345        &self.0
346    }
347
348    /// Convert to owned HashMap
349    pub fn into_map(self) -> HashMap<String, String> {
350        self.0
351    }
352
353    /// Check if parameters are empty
354    pub fn is_empty(&self) -> bool {
355        self.0.is_empty()
356    }
357}
358
359impl Default for StageParameters {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365/// Value object representing the order of a processing step
366#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
367pub struct StepOrder(u32);
368
369impl StepOrder {
370    /// Create a new StepOrder
371    pub fn new(order: u32) -> Self {
372        Self(order)
373    }
374
375    /// Get the order value
376    pub fn value(&self) -> u32 {
377        self.0
378    }
379
380    /// Get the next order
381    pub fn next(&self) -> Self {
382        Self(self.0 + 1)
383    }
384}
385
386impl std::fmt::Display for StepOrder {
387    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388        write!(f, "{}", self.0)
389    }
390}
391
392/// Value object describing a complete processing step
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct ProcessingStepDescriptor {
395    step_type: ProcessingStepType,
396    algorithm: Algorithm,
397    parameters: StageParameters,
398    order: StepOrder,
399}
400
401impl ProcessingStepDescriptor {
402    /// Create a new ProcessingStepDescriptor
403    pub fn new(
404        step_type: ProcessingStepType,
405        algorithm: Algorithm,
406        parameters: StageParameters,
407        order: StepOrder,
408    ) -> Self {
409        Self {
410            step_type,
411            algorithm,
412            parameters,
413            order,
414        }
415    }
416
417    /// Create a compression step descriptor
418    pub fn compression(algorithm: Algorithm, order: StepOrder) -> Self {
419        Self::new(
420            ProcessingStepType::Compression,
421            algorithm,
422            StageParameters::new(),
423            order,
424        )
425    }
426
427    /// Create an encryption step descriptor
428    pub fn encryption(algorithm: Algorithm, order: StepOrder) -> Self {
429        Self::new(ProcessingStepType::Encryption, algorithm, StageParameters::new(), order)
430    }
431
432    /// Create a checksum step descriptor
433    pub fn checksum(algorithm: Algorithm, order: StepOrder) -> Self {
434        Self::new(ProcessingStepType::Checksum, algorithm, StageParameters::new(), order)
435    }
436
437    /// Create a pass-through step descriptor
438    pub fn pass_through(algorithm: Algorithm, order: StepOrder) -> Self {
439        Self::new(
440            ProcessingStepType::PassThrough,
441            algorithm,
442            StageParameters::new(),
443            order,
444        )
445    }
446
447    /// Get the step type
448    pub fn step_type(&self) -> &ProcessingStepType {
449        &self.step_type
450    }
451
452    /// Get the algorithm
453    pub fn algorithm(&self) -> &Algorithm {
454        &self.algorithm
455    }
456
457    /// Get the parameters
458    pub fn parameters(&self) -> &StageParameters {
459        &self.parameters
460    }
461
462    /// Get the order
463    pub fn order(&self) -> StepOrder {
464        self.order
465    }
466
467    /// Add a parameter to this descriptor
468    pub fn with_parameter(mut self, key: &str, value: &str) -> Result<Self, PipelineError> {
469        self.parameters.add_parameter(key, value)?;
470        Ok(self)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    /// Tests algorithm validation rules and constraint enforcement.
479    ///
480    /// This test validates that algorithm names are properly validated
481    /// according to business rules and that invalid algorithm names
482    /// are rejected with appropriate error handling.
483    ///
484    /// # Test Coverage
485    ///
486    /// - Valid algorithm name acceptance
487    /// - Standard algorithm validation (brotli, aes256-gcm, sha256)
488    /// - Custom algorithm name support
489    /// - Empty string rejection
490    /// - Whitespace-only string rejection
491    /// - Algorithm names with spaces rejection
492    /// - Algorithm names with special characters rejection
493    ///
494    /// # Test Scenario
495    ///
496    /// Tests various algorithm name formats including valid standard
497    /// algorithms, custom algorithms, and invalid formats.
498    ///
499    /// # Domain Concerns
500    ///
501    /// - Algorithm name validation
502    /// - Processing step configuration
503    /// - Input validation and sanitization
504    /// - Business rule enforcement
505    ///
506    /// # Assertions
507    ///
508    /// - Standard algorithms are accepted
509    /// - Custom algorithms are accepted
510    /// - Empty strings are rejected
511    /// - Whitespace-only strings are rejected
512    /// - Names with spaces are rejected
513    /// - Names with special characters are rejected
514    #[test]
515    fn test_algorithm_validation() {
516        // Valid algorithms
517        assert!(Algorithm::new("brotli").is_ok());
518        assert!(Algorithm::new("aes256-gcm").is_ok());
519        assert!(Algorithm::new("sha256").is_ok());
520        assert!(Algorithm::new("custom_algo").is_ok());
521
522        // Invalid algorithms
523        assert!(Algorithm::new("").is_err());
524        assert!(Algorithm::new("   ").is_err());
525        assert!(Algorithm::new("algo with spaces").is_err());
526        assert!(Algorithm::new("algo@special").is_err());
527    }
528
529    /// Tests stage parameters management and validation.
530    ///
531    /// This test validates that stage parameters can be properly
532    /// managed including parameter addition, validation, and
533    /// retrieval for processing step configuration.
534    ///
535    /// # Test Coverage
536    ///
537    /// - Parameter addition with valid key-value pairs
538    /// - Parameter validation for empty keys
539    /// - Parameter retrieval functionality
540    /// - Parameter storage and access
541    /// - Validation error handling
542    ///
543    /// # Test Scenario
544    ///
545    /// Creates stage parameters, adds valid and invalid parameters,
546    /// and verifies parameter management functionality.
547    ///
548    /// # Domain Concerns
549    ///
550    /// - Processing step configuration
551    /// - Parameter validation and management
552    /// - Configuration storage and retrieval
553    /// - Input validation
554    ///
555    /// # Assertions
556    ///
557    /// - Valid parameters are added successfully
558    /// - Empty key parameters are rejected
559    /// - Parameter values can be retrieved
560    /// - Parameter storage works correctly
561    #[test]
562    fn test_stage_parameters() {
563        let mut params = StageParameters::new();
564        assert!(params.add_parameter("level", "6").is_ok());
565        assert!(params.add_parameter("", "value").is_err());
566        assert_eq!(params.get("level"), Some(&"6".to_string()));
567    }
568
569    /// Tests step order creation and navigation functionality.
570    ///
571    /// This test validates that step orders can be created with
572    /// proper value storage and that navigation to next orders
573    /// works correctly for processing sequence management.
574    ///
575    /// # Test Coverage
576    ///
577    /// - Step order creation with value
578    /// - Value storage and retrieval
579    /// - Next order calculation
580    /// - Order navigation functionality
581    /// - Sequential order management
582    ///
583    /// # Test Scenario
584    ///
585    /// Creates a step order and tests value access and navigation
586    /// to the next order in the sequence.
587    ///
588    /// # Domain Concerns
589    ///
590    /// - Processing step sequencing
591    /// - Order management and navigation
592    /// - Sequential processing logic
593    /// - Step execution order
594    ///
595    /// # Assertions
596    ///
597    /// - Order value is stored correctly
598    /// - Next order is calculated correctly
599    /// - Navigation functionality works
600    /// - Sequential ordering is maintained
601    #[test]
602    fn test_step_order() {
603        let order = StepOrder::new(5);
604        assert_eq!(order.value(), 5);
605        assert_eq!(order.next().value(), 6);
606    }
607
608    /// Tests processing step descriptor creation and properties.
609    ///
610    /// This test validates that processing step descriptors can be
611    /// created with proper algorithm, order, and step type
612    /// configuration for pipeline processing steps.
613    ///
614    /// # Test Coverage
615    ///
616    /// - Compression step descriptor creation
617    /// - Step type property validation
618    /// - Algorithm property access
619    /// - Order property access
620    /// - Descriptor configuration integrity
621    ///
622    /// # Test Scenario
623    ///
624    /// Creates a compression processing step descriptor and verifies
625    /// all properties are correctly configured and accessible.
626    ///
627    /// # Domain Concerns
628    ///
629    /// - Processing step configuration
630    /// - Step type classification
631    /// - Algorithm and order management
632    /// - Pipeline step definition
633    ///
634    /// # Assertions
635    ///
636    /// - Step type is compression
637    /// - Algorithm is stored correctly
638    /// - Order value is preserved
639    /// - Descriptor properties are accessible
640    #[test]
641    fn test_processing_step_descriptor() {
642        let algorithm = Algorithm::new("brotli").unwrap();
643        let order = StepOrder::new(1);
644
645        let descriptor = ProcessingStepDescriptor::compression(algorithm, order);
646        assert_eq!(descriptor.step_type(), &ProcessingStepType::Compression);
647        assert_eq!(descriptor.algorithm().as_str(), "brotli");
648        assert_eq!(descriptor.order().value(), 1);
649    }
650}