vv-agent 0.2.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::path::Path;

use serde_json::Value;

use crate::tools::base::ToolContext;
use crate::tools::common::{matches_file_type, workspace_relative_path_or_absolute};

use super::decode::decode_rg_field;
use super::paths::normalize_rg_relative_path;

pub(super) enum RgJsonEvent {
    Begin {
        path: String,
    },
    Match {
        path: String,
        line_number: u64,
        matched_lines: String,
        submatches: Vec<RgSubmatch>,
    },
    Context {
        path: String,
        line_number: u64,
        lines: String,
    },
}

pub(super) struct RgSubmatch {
    pub start: Option<usize>,
    pub end: Option<usize>,
    pub matched_text: String,
}

impl RgJsonEvent {
    pub fn from_value(
        context: &ToolContext,
        base_path: &Path,
        file_type: Option<&str>,
        event: &Value,
    ) -> Option<Self> {
        let event_type = event
            .get("type")
            .and_then(Value::as_str)
            .unwrap_or_default();
        if event_type == "summary" {
            return None;
        }
        let data = event.get("data").and_then(Value::as_object)?;
        let path = event_workspace_path(context, base_path, file_type, data.get("path"))?;
        match event_type {
            "begin" => Some(Self::Begin { path }),
            "match" => Some(Self::Match {
                path,
                line_number: data.get("line_number").and_then(Value::as_u64).unwrap_or(1),
                matched_lines: decode_rg_field(data.get("lines")),
                submatches: parse_submatches(data.get("submatches")),
            }),
            "context" => {
                let line_number = data.get("line_number").and_then(Value::as_u64)?;
                Some(Self::Context {
                    path,
                    line_number,
                    lines: decode_rg_field(data.get("lines")),
                })
            }
            _ => None,
        }
    }
}

fn event_workspace_path(
    context: &ToolContext,
    base_path: &Path,
    file_type: Option<&str>,
    path_field: Option<&Value>,
) -> Option<String> {
    let rel_from_base = decode_rg_field(path_field);
    if rel_from_base.is_empty() {
        return None;
    }
    let normalized = normalize_rg_relative_path(&rel_from_base);
    let rel_workspace =
        workspace_relative_path_or_absolute(&context.workspace, &base_path.join(normalized));
    if let Some(file_type) = file_type {
        if !matches_file_type(&rel_workspace, Some(file_type)) {
            return None;
        }
    }
    Some(rel_workspace)
}

fn parse_submatches(value: Option<&Value>) -> Vec<RgSubmatch> {
    value
        .and_then(Value::as_array)
        .map(|items| items.iter().filter_map(parse_submatch).collect())
        .unwrap_or_default()
}

fn parse_submatch(value: &Value) -> Option<RgSubmatch> {
    let object = value.as_object()?;
    Some(RgSubmatch {
        start: object
            .get("start")
            .and_then(Value::as_u64)
            .map(|value| value as usize),
        end: object
            .get("end")
            .and_then(Value::as_u64)
            .map(|value| value as usize),
        matched_text: decode_rg_field(object.get("match")),
    })
}