reputation_types/
builder.rs

1use crate::AgentData;
2use chrono::{DateTime, Utc};
3
4/// Builder for constructing `AgentData` instances with a fluent API.
5/// 
6/// The builder provides a convenient way to create `AgentData` instances with
7/// validation and sensible defaults. All fields except the DID are optional.
8/// 
9/// # Examples
10/// 
11/// ## Basic Usage
12/// 
13/// ```
14/// use reputation_types::AgentDataBuilder;
15/// 
16/// // Simple agent with minimal data
17/// let agent = AgentDataBuilder::new("did:example:123")
18///     .build()
19///     .unwrap();
20/// 
21/// // Agent with reviews
22/// let agent = AgentDataBuilder::new("did:example:123")
23///     .total_interactions(150)
24///     .with_reviews(100, 4.5)
25///     .mcp_level(2)
26///     .identity_verified(true)
27///     .build()
28///     .unwrap();
29/// ```
30/// 
31/// ## Using the Convenience Constructor
32/// 
33/// ```
34/// use reputation_types::AgentData;
35/// 
36/// let agent = AgentData::builder("did:example:456")
37///     .total_interactions(300)
38///     .with_reviews(250, 4.8)
39///     .mcp_level(3)
40///     .identity_verified(true)
41///     .security_audit_passed(true)
42///     .open_source(true)
43///     .build()
44///     .unwrap();
45/// ```
46/// 
47/// ## Setting Individual Review Counts
48/// 
49/// ```
50/// use reputation_types::AgentDataBuilder;
51/// 
52/// let agent = AgentDataBuilder::new("did:example:789")
53///     .positive_reviews(180)
54///     .negative_reviews(20)
55///     .total_interactions(500)
56///     .build()
57///     .unwrap();
58/// 
59/// // The builder will automatically calculate:
60/// // - total_reviews: 200 (180 + 20)
61/// // - average_rating: 4.6 (based on positive/negative ratio)
62/// ```
63/// 
64/// ## Error Handling
65/// 
66/// ```
67/// use reputation_types::{AgentDataBuilder, BuilderError};
68/// 
69/// // Empty DID returns an error
70/// let result = AgentDataBuilder::new("")
71///     .build();
72/// assert!(matches!(result, Err(BuilderError::InvalidField(_))));
73/// 
74/// // Invalid rating returns an error
75/// let result = AgentDataBuilder::new("did:example:123")
76///     .average_rating(6.0)
77///     .build();
78/// assert!(matches!(result, Err(BuilderError::InvalidField(_))));
79/// ```
80#[derive(Debug, Clone)]
81pub struct AgentDataBuilder {
82    did: String,
83    created_at: DateTime<Utc>,
84    mcp_level: Option<u8>,
85    identity_verified: bool,
86    security_audit_passed: bool,
87    open_source: bool,
88    total_interactions: u32,
89    total_reviews: u32,
90    average_rating: Option<f64>,
91    positive_reviews: u32,
92    negative_reviews: u32,
93}
94
95impl AgentDataBuilder {
96    /// Creates a new builder with the required DID field.
97    /// 
98    /// All other fields are initialized with sensible defaults:
99    /// - `created_at`: Current UTC time
100    /// - `mcp_level`: None
101    /// - `identity_verified`: false
102    /// - `security_audit_passed`: false
103    /// - `open_source`: false
104    /// - All numeric fields: 0
105    pub fn new(did: impl Into<String>) -> Self {
106        Self {
107            did: did.into(),
108            created_at: Utc::now(),
109            mcp_level: None,
110            identity_verified: false,
111            security_audit_passed: false,
112            open_source: false,
113            total_interactions: 0,
114            total_reviews: 0,
115            average_rating: None,
116            positive_reviews: 0,
117            negative_reviews: 0,
118        }
119    }
120
121    /// Sets the creation timestamp.
122    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
123        self.created_at = created_at;
124        self
125    }
126
127    /// Sets the MCP level.
128    pub fn mcp_level(mut self, level: u8) -> Self {
129        self.mcp_level = Some(level);
130        self
131    }
132
133    /// Sets whether the agent's identity is verified.
134    pub fn identity_verified(mut self, verified: bool) -> Self {
135        self.identity_verified = verified;
136        self
137    }
138
139    /// Sets whether the agent has passed security audit.
140    pub fn security_audit_passed(mut self, passed: bool) -> Self {
141        self.security_audit_passed = passed;
142        self
143    }
144
145    /// Sets whether the agent is open source.
146    pub fn open_source(mut self, is_open_source: bool) -> Self {
147        self.open_source = is_open_source;
148        self
149    }
150
151    /// Sets the total number of interactions.
152    pub fn total_interactions(mut self, count: u32) -> Self {
153        self.total_interactions = count;
154        self
155    }
156
157    /// Sets the total number of reviews.
158    pub fn total_reviews(mut self, count: u32) -> Self {
159        self.total_reviews = count;
160        self
161    }
162
163    /// Sets the average rating.
164    pub fn average_rating(mut self, rating: f64) -> Self {
165        self.average_rating = Some(rating);
166        self
167    }
168
169    /// Sets the number of positive reviews.
170    pub fn positive_reviews(mut self, count: u32) -> Self {
171        self.positive_reviews = count;
172        self
173    }
174
175    /// Sets the number of negative reviews.
176    pub fn negative_reviews(mut self, count: u32) -> Self {
177        self.negative_reviews = count;
178        self
179    }
180
181    /// Convenience method to set review data based on total reviews and average rating.
182    /// 
183    /// This method automatically calculates:
184    /// - Positive and negative review counts based on the average rating
185    /// - The average rating is clamped between 1.0 and 5.0
186    /// 
187    /// # Examples
188    /// 
189    /// ```
190    /// use reputation_types::AgentDataBuilder;
191    /// 
192    /// let agent = AgentDataBuilder::new("did:example:123")
193    ///     .total_interactions(150)
194    ///     .with_reviews(100, 4.5)  // 87 positive, 13 negative
195    ///     .build()
196    ///     .unwrap();
197    /// ```
198    pub fn with_reviews(mut self, total: u32, average_rating: f64) -> Self {
199        self.total_reviews = total;
200        
201        // Store the rating as-is, validation happens in build()
202        self.average_rating = Some(average_rating);
203        
204        if total > 0 {
205            // Calculate positive/negative based on average
206            // A 5.0 rating = 100% positive, 1.0 rating = 0% positive
207            let positive_ratio = (average_rating.max(1.0).min(5.0) - 1.0) / 4.0;
208            self.positive_reviews = (total as f64 * positive_ratio).round() as u32;
209            self.negative_reviews = total.saturating_sub(self.positive_reviews);
210        } else {
211            self.positive_reviews = 0;
212            self.negative_reviews = 0;
213        }
214        
215        self
216    }
217
218    /// Builds the `AgentData` instance with validation.
219    /// 
220    /// # Errors
221    /// 
222    /// Returns an error if:
223    /// - The DID is empty
224    /// - The total reviews don't match positive + negative reviews (if all are set)
225    /// - The average rating is outside the valid range (1.0-5.0)
226    /// - The creation date is in the future
227    pub fn build(self) -> Result<AgentData, BuilderError> {
228        // Validate DID
229        if self.did.is_empty() {
230            return Err(BuilderError::InvalidField("DID cannot be empty".to_string()));
231        }
232        if !self.did.starts_with("did:") {
233            return Err(BuilderError::InvalidField("DID must start with 'did:' prefix".to_string()));
234        }
235        if self.did.contains("..") || self.did.contains("//") || self.did.contains('\'') || 
236           self.did.contains('"') || self.did.contains(';') || self.did.contains("--") ||
237           self.did.contains('\n') || self.did.contains('\t') || self.did.contains('<') ||
238           self.did.contains('>') || self.did.contains('\0') || self.did.contains('\r') ||
239           self.did.contains("javascript:") {
240            return Err(BuilderError::InvalidField("DID contains invalid format".to_string()));
241        }
242        if self.did.len() > 1000 {
243            return Err(BuilderError::InvalidField("DID exceeds maximum length".to_string()));
244        }
245        let parts: Vec<&str> = self.did.split(':').collect();
246        if parts.len() < 3 || parts[1].is_empty() || parts[2].is_empty() {
247            return Err(BuilderError::InvalidField("DID must have format 'did:method:id'".to_string()));
248        }
249
250        // Validate creation date
251        if self.created_at > Utc::now() {
252            return Err(BuilderError::InvalidField("Creation date cannot be in the future".to_string()));
253        }
254
255        // Update total_reviews if not explicitly set but positive/negative are set
256        let total_reviews = if self.total_reviews == 0 && (self.positive_reviews > 0 || self.negative_reviews > 0) {
257            self.positive_reviews + self.negative_reviews
258        } else {
259            self.total_reviews
260        };
261
262        // Validate review counts if all are set
263        if total_reviews > 0 && self.positive_reviews + self.negative_reviews > 0 {
264            if total_reviews != self.positive_reviews + self.negative_reviews {
265                return Err(BuilderError::InvalidField(
266                    format!(
267                        "Total reviews ({}) must equal positive ({}) + negative ({}) reviews",
268                        total_reviews, self.positive_reviews, self.negative_reviews
269                    )
270                ));
271            }
272        }
273        
274        // Validate reviews don't exceed interactions
275        if total_reviews > self.total_interactions {
276            return Err(BuilderError::InvalidField(
277                format!(
278                    "Total reviews ({}) cannot exceed total interactions ({})",
279                    total_reviews, self.total_interactions
280                )
281            ));
282        }
283
284        // Calculate average rating if not set but we have review data
285        let average_rating = match self.average_rating {
286            Some(rating) => {
287                // Validate rating range and special values
288                if rating.is_nan() || rating.is_infinite() {
289                    return Err(BuilderError::InvalidField(
290                        format!("Average rating must be a valid number, got {}", rating)
291                    ));
292                }
293                if rating < 1.0 || rating > 5.0 {
294                    return Err(BuilderError::InvalidField(
295                        format!("Average rating must be between 1.0 and 5.0, got {}", rating)
296                    ));
297                }
298                Some(rating)
299            }
300            None => {
301                if total_reviews > 0 && (self.positive_reviews > 0 || self.negative_reviews > 0) {
302                    // Calculate from positive/negative ratio
303                    let positive_ratio = self.positive_reviews as f64 / total_reviews as f64;
304                    Some(1.0 + (positive_ratio * 4.0))
305                } else {
306                    None
307                }
308            }
309        };
310
311        // Validate MCP level if set
312        if let Some(level) = self.mcp_level {
313            if level > 3 {
314                return Err(BuilderError::InvalidField(
315                    format!("MCP level must be between 0 and 3, got {}", level)
316                ));
317            }
318        }
319
320        Ok(AgentData {
321            did: self.did,
322            created_at: self.created_at,
323            mcp_level: self.mcp_level,
324            identity_verified: self.identity_verified,
325            security_audit_passed: self.security_audit_passed,
326            open_source: self.open_source,
327            total_interactions: self.total_interactions,
328            total_reviews,
329            average_rating,
330            positive_reviews: self.positive_reviews,
331            negative_reviews: self.negative_reviews,
332        })
333    }
334}
335
336/// Errors that can occur when building AgentData
337#[derive(Debug, Clone, PartialEq)]
338pub enum BuilderError {
339    /// A field has an invalid value
340    InvalidField(String),
341}
342
343impl std::fmt::Display for BuilderError {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        match self {
346            BuilderError::InvalidField(msg) => write!(f, "Invalid field: {}", msg),
347        }
348    }
349}
350
351impl std::error::Error for BuilderError {}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use chrono::Duration;
357
358    #[test]
359    fn test_builder_minimal() {
360        let agent = AgentDataBuilder::new("did:example:123")
361            .build()
362            .unwrap();
363
364        assert_eq!(agent.did, "did:example:123");
365        assert_eq!(agent.mcp_level, None);
366        assert!(!agent.identity_verified);
367        assert!(!agent.security_audit_passed);
368        assert!(!agent.open_source);
369        assert_eq!(agent.total_interactions, 0);
370        assert_eq!(agent.total_reviews, 0);
371        assert_eq!(agent.average_rating, None);
372        assert_eq!(agent.positive_reviews, 0);
373        assert_eq!(agent.negative_reviews, 0);
374    }
375
376    #[test]
377    fn test_builder_with_all_fields() {
378        let created_at = Utc::now() - Duration::days(30);
379        let agent = AgentDataBuilder::new("did:example:456")
380            .created_at(created_at)
381            .mcp_level(3)
382            .identity_verified(true)
383            .security_audit_passed(true)
384            .open_source(true)
385            .total_interactions(1000)
386            .total_reviews(100)
387            .average_rating(4.2)
388            .positive_reviews(80)
389            .negative_reviews(20)
390            .build()
391            .unwrap();
392
393        assert_eq!(agent.did, "did:example:456");
394        assert_eq!(agent.created_at, created_at);
395        assert_eq!(agent.mcp_level, Some(3));
396        assert!(agent.identity_verified);
397        assert!(agent.security_audit_passed);
398        assert!(agent.open_source);
399        assert_eq!(agent.total_interactions, 1000);
400        assert_eq!(agent.total_reviews, 100);
401        assert_eq!(agent.average_rating, Some(4.2));
402        assert_eq!(agent.positive_reviews, 80);
403        assert_eq!(agent.negative_reviews, 20);
404    }
405
406    #[test]
407    fn test_with_reviews_helper() {
408        let agent = AgentDataBuilder::new("did:example:789")
409            .total_interactions(150)
410            .with_reviews(100, 4.5)
411            .build()
412            .unwrap();
413
414        assert_eq!(agent.total_reviews, 100);
415        assert_eq!(agent.average_rating, Some(4.5));
416        assert_eq!(agent.positive_reviews, 88); // (4.5 - 1.0) / 4.0 * 100 = 87.5, rounded to 88
417        assert_eq!(agent.negative_reviews, 12);
418    }
419
420    #[test]
421    fn test_with_reviews_edge_cases() {
422        // Test with perfect rating
423        let agent = AgentDataBuilder::new("did:example:1")
424            .total_interactions(60)
425            .with_reviews(50, 5.0)
426            .build()
427            .unwrap();
428        assert_eq!(agent.positive_reviews, 50);
429        assert_eq!(agent.negative_reviews, 0);
430
431        // Test with worst rating
432        let agent = AgentDataBuilder::new("did:example:2")
433            .total_interactions(60)
434            .with_reviews(50, 1.0)
435            .build()
436            .unwrap();
437        assert_eq!(agent.positive_reviews, 0);
438        assert_eq!(agent.negative_reviews, 50);
439
440        // Test with rating above 5.0 (should fail)
441        let result = AgentDataBuilder::new("did:example:3")
442            .with_reviews(100, 6.0)
443            .build();
444        assert!(result.is_err());
445
446        // Test with rating below 1.0 (should fail)
447        let result = AgentDataBuilder::new("did:example:4")
448            .with_reviews(100, 0.5)
449            .build();
450        assert!(result.is_err());
451    }
452
453    #[test]
454    fn test_validation_empty_did() {
455        let result = AgentDataBuilder::new("")
456            .build();
457
458        assert!(result.is_err());
459        match result.unwrap_err() {
460            BuilderError::InvalidField(msg) => assert!(msg.contains("DID cannot be empty")),
461        }
462    }
463
464    #[test]
465    fn test_validation_future_date() {
466        let result = AgentDataBuilder::new("did:example:123")
467            .created_at(Utc::now() + Duration::days(1))
468            .build();
469
470        assert!(result.is_err());
471        match result.unwrap_err() {
472            BuilderError::InvalidField(msg) => assert!(msg.contains("future")),
473        }
474    }
475
476    #[test]
477    fn test_validation_review_mismatch() {
478        let result = AgentDataBuilder::new("did:example:123")
479            .total_reviews(100)
480            .positive_reviews(60)
481            .negative_reviews(50) // 60 + 50 = 110 != 100
482            .build();
483
484        assert!(result.is_err());
485        match result.unwrap_err() {
486            BuilderError::InvalidField(msg) => assert!(msg.contains("must equal")),
487        }
488    }
489
490    #[test]
491    fn test_validation_invalid_rating() {
492        let result = AgentDataBuilder::new("did:example:123")
493            .average_rating(5.5)
494            .build();
495
496        assert!(result.is_err());
497        match result.unwrap_err() {
498            BuilderError::InvalidField(msg) => assert!(msg.contains("between 1.0 and 5.0")),
499        }
500    }
501
502    #[test]
503    fn test_validation_invalid_mcp_level() {
504        let result = AgentDataBuilder::new("did:example:123")
505            .mcp_level(10)
506            .build();
507
508        assert!(result.is_err());
509        match result.unwrap_err() {
510            BuilderError::InvalidField(msg) => assert!(msg.contains("MCP level")),
511        }
512    }
513
514    #[test]
515    fn test_auto_calculate_total_reviews() {
516        let agent = AgentDataBuilder::new("did:example:123")
517            .total_interactions(120)
518            .positive_reviews(75)
519            .negative_reviews(25)
520            .build()
521            .unwrap();
522
523        assert_eq!(agent.total_reviews, 100);
524    }
525
526    #[test]
527    fn test_auto_calculate_average_rating() {
528        let agent = AgentDataBuilder::new("did:example:123")
529            .total_interactions(120)
530            .positive_reviews(80)
531            .negative_reviews(20)
532            .build()
533            .unwrap();
534
535        // 80/100 = 0.8 positive ratio, 1.0 + (0.8 * 4.0) = 4.2
536        assert_eq!(agent.average_rating, Some(4.2));
537    }
538
539    #[test]
540    fn test_builder_is_cloneable() {
541        let builder = AgentDataBuilder::new("did:example:123")
542            .mcp_level(2)
543            .identity_verified(true);
544
545        let builder2 = builder.clone();
546        let agent1 = builder.build().unwrap();
547        let agent2 = builder2.build().unwrap();
548
549        assert_eq!(agent1.did, agent2.did);
550        assert_eq!(agent1.mcp_level, agent2.mcp_level);
551        assert_eq!(agent1.identity_verified, agent2.identity_verified);
552    }
553
554    #[test]
555    fn test_method_chaining() {
556        // This test ensures all methods return Self for proper chaining
557        let _agent = AgentDataBuilder::new("did:example:123")
558            .created_at(Utc::now())
559            .mcp_level(2)
560            .identity_verified(true)
561            .security_audit_passed(true)
562            .open_source(true)
563            .total_interactions(1000)
564            .with_reviews(100, 4.5)
565            .build()
566            .unwrap();
567    }
568
569    #[test]
570    fn test_builder_error_display() {
571        let error = BuilderError::InvalidField("test error".to_string());
572        assert_eq!(error.to_string(), "Invalid field: test error");
573        
574        // Test that it implements std::error::Error
575        let _: &dyn std::error::Error = &error;
576    }
577}