sarif_rust 0.3.0

A comprehensive Rust library for parsing, generating, and manipulating SARIF (Static Analysis Results Interchange Format) v2.1.0 files
Documentation
//! Builder for SARIF log objects
//!
//! This module provides a fluent interface for constructing SARIF log objects.
//! SarifLogBuilder is the main entry point for creating complete SARIF documents.

use crate::builder::{ResultBuilder, RunBuilder};
use crate::parser::{SarifValidator, ValidationResult, validator::Validate};
use crate::types::{ExternalProperties, Run, SarifLog, Tool, ToolComponent};

/// Main builder for creating SARIF log objects
///
/// This is the primary entry point for building SARIF documents. It provides
/// a fluent interface for constructing complete SARIF logs with validation.
#[derive(Debug, Clone)]
pub struct SarifLogBuilder {
    version: String,
    runs: Vec<Run>,
    schema: Option<String>,
    inline_external_properties: Vec<ExternalProperties>,
}

impl SarifLogBuilder {
    /// Create a new SARIF log builder with default version 2.1.0
    pub fn new() -> Self {
        Self {
            version: "2.1.0".to_string(),
            runs: Vec::new(),
            schema: None,
            inline_external_properties: Vec::new(),
        }
    }

    /// Create a SARIF log builder for version 2.1.0 (convenience method)
    pub fn v2_1_0() -> Self {
        Self::new().with_version("2.1.0")
    }

    /// Create a SARIF log builder with the standard JSON schema
    pub fn with_standard_schema() -> Self {
        Self::new().with_schema("https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json")
    }

    /// Set the SARIF version
    pub fn with_version(mut self, version: impl Into<String>) -> Self {
        self.version = version.into();
        self
    }

    /// Set the JSON schema URI
    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
        self.schema = Some(schema.into());
        self
    }

    /// Add a run to the log
    pub fn add_run(mut self, run: Run) -> Self {
        self.runs.push(run);
        self
    }

    /// Add multiple runs to the log
    pub fn add_runs(mut self, runs: impl IntoIterator<Item = Run>) -> Self {
        self.runs.extend(runs);
        self
    }

    /// Add a run using a builder function
    pub fn add_run_with<F>(mut self, f: F) -> Self
    where
        F: FnOnce(RunBuilder) -> RunBuilder,
    {
        let default_tool = Tool {
            driver: ToolComponent::new("unknown"),
            extensions: None,
            properties: None,
        };
        let builder = RunBuilder::new(default_tool);
        let run = f(builder).build();
        self.runs.push(run);
        self
    }

    /// Add a simple run with tool name and version
    pub fn add_simple_run(
        mut self,
        tool_name: impl Into<String>,
        tool_version: Option<impl Into<String>>,
    ) -> Self {
        let run = RunBuilder::with_tool(tool_name, tool_version).build();
        self.runs.push(run);
        self
    }

    /// Add external properties
    pub fn add_external_properties(mut self, properties: ExternalProperties) -> Self {
        self.inline_external_properties.push(properties);
        self
    }

    /// Create a quick SARIF log with a single tool and result
    pub fn quick_sarif(
        tool_name: impl Into<String>,
        tool_version: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        line: i32,
        column: i32,
    ) -> Self {
        let result = ResultBuilder::with_text_message(message)
            .add_file_location(file_path, line, column)
            .build();

        let run = RunBuilder::with_tool(tool_name, Some(tool_version))
            .add_result(result)
            .build();

        Self::new().add_run(run)
    }

    /// Create a SARIF log with error-level finding
    #[allow(clippy::too_many_arguments)]
    pub fn error_finding(
        tool_name: impl Into<String>,
        rule_id: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        start_line: i32,
        start_column: i32,
        end_line: i32,
        end_column: i32,
    ) -> Self {
        use crate::types::Level;

        let result = ResultBuilder::with_text_message(message)
            .with_rule_id(rule_id)
            .with_level(Level::Error)
            .add_file_region(file_path, start_line, start_column, end_line, end_column)
            .build();

        let run = RunBuilder::with_tool(tool_name, None::<String>)
            .add_result(result)
            .build();

        Self::new().add_run(run)
    }

    /// Create a SARIF log with warning-level finding
    #[allow(clippy::too_many_arguments)]
    pub fn warning_finding(
        tool_name: impl Into<String>,
        rule_id: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        start_line: i32,
        start_column: i32,
        end_line: i32,
        end_column: i32,
    ) -> Self {
        use crate::types::Level;

        let result = ResultBuilder::with_text_message(message)
            .with_rule_id(rule_id)
            .with_level(Level::Warning)
            .add_file_region(file_path, start_line, start_column, end_line, end_column)
            .build();

        let run = RunBuilder::with_tool(tool_name, None::<String>)
            .add_result(result)
            .build();

        Self::new().add_run(run)
    }

    /// Create a SARIF log with error-level finding using a Region
    pub fn error_finding_with_region(
        tool_name: impl Into<String>,
        rule_id: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        region: crate::types::Region,
    ) -> Self {
        use crate::types::Level;

        let result = ResultBuilder::with_text_message(message)
            .with_rule_id(rule_id)
            .with_level(Level::Error)
            .add_file_location_with_region(file_path, region)
            .build();

        let run = RunBuilder::with_tool(tool_name, None::<String>)
            .add_result(result)
            .build();

        Self::new().add_run(run)
    }

    /// Create a SARIF log with warning-level finding using a Region
    pub fn warning_finding_with_region(
        tool_name: impl Into<String>,
        rule_id: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        region: crate::types::Region,
    ) -> Self {
        use crate::types::Level;

        let result = ResultBuilder::with_text_message(message)
            .with_rule_id(rule_id)
            .with_level(Level::Warning)
            .add_file_location_with_region(file_path, region)
            .build();

        let run = RunBuilder::with_tool(tool_name, None::<String>)
            .add_result(result)
            .build();

        Self::new().add_run(run)
    }

    /// Validate the builder state
    pub fn validate(&self, validator: &SarifValidator) -> ValidationResult<()> {
        // Validate version
        validator.validate_version(&self.version)?;

        // Validate schema URI if present
        if let Some(ref schema) = self.schema {
            validator.validate_uri(schema)?;
        }

        // Validate that we have at least one run
        if self.runs.is_empty() {
            return Err(crate::parser::ValidationError::missing_field(
                "At least one run is required in a SARIF log",
            ));
        }

        // Validate each run
        for run in &self.runs {
            validator.validate_run(run)?;
        }

        Ok(())
    }

    /// Build the SARIF log with validation
    pub fn build(self) -> ValidationResult<SarifLog> {
        let log = SarifLog {
            schema: self.schema,
            version: self.version,
            runs: self.runs,
            inline_external_properties: if self.inline_external_properties.is_empty() {
                None
            } else {
                Some(self.inline_external_properties)
            },
            properties: None,
        };

        // Validate the constructed log
        log.validate()?;
        Ok(log)
    }

    /// Build and validate the SARIF log with a specific validator
    pub fn build_validated(self, validator: &SarifValidator) -> ValidationResult<SarifLog> {
        self.validate(validator)?;
        Ok(self.build_unchecked())
    }

    /// Build the SARIF log without validation
    ///
    /// Use this method only when you're confident the SARIF log is valid,
    /// or when you want to handle validation separately.
    pub fn build_unchecked(self) -> SarifLog {
        SarifLog {
            schema: self.schema,
            version: self.version,
            runs: self.runs,
            inline_external_properties: if self.inline_external_properties.is_empty() {
                None
            } else {
                Some(self.inline_external_properties)
            },
            properties: None,
        }
    }

    /// Count total number of results across all runs
    pub fn result_count(&self) -> usize {
        self.runs
            .iter()
            .map(|run| run.results.as_ref().map_or(0, |results| results.len()))
            .sum()
    }

    /// Count total number of artifacts across all runs
    pub fn artifact_count(&self) -> usize {
        self.runs
            .iter()
            .map(|run| {
                run.artifacts
                    .as_ref()
                    .map_or(0, |artifacts| artifacts.len())
            })
            .sum()
    }

    /// Get the number of runs
    pub fn run_count(&self) -> usize {
        self.runs.len()
    }

    /// Check if the log is empty (no runs)
    pub fn is_empty(&self) -> bool {
        self.runs.is_empty()
    }
}

impl Default for SarifLogBuilder {
    fn default() -> Self {
        Self::new()
    }
}

// Convenience functions for quick SARIF creation
impl SarifLogBuilder {
    /// Create a minimal valid SARIF log with an empty run
    pub fn minimal() -> Self {
        let tool = Tool {
            driver: ToolComponent::new("minimal-tool"),
            extensions: None,
            properties: None,
        };
        let run = Run::new(tool);
        Self::new().add_run(run)
    }

    /// Create a SARIF log from a single error message
    pub fn single_error(
        tool_name: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        line: i32,
    ) -> Self {
        Self::error_finding(tool_name, "ERROR", message, file_path, line, 1, line, 80)
    }

    /// Create a SARIF log from a single warning message  
    pub fn single_warning(
        tool_name: impl Into<String>,
        message: impl Into<String>,
        file_path: impl Into<String>,
        line: i32,
    ) -> Self {
        Self::warning_finding(tool_name, "WARNING", message, file_path, line, 1, line, 80)
    }
}