signal_gateway_code_tool/
config.rs

1//! Configuration types for repository code browsing.
2
3use serde::Deserialize;
4use std::{path::PathBuf, str::FromStr};
5
6/// A GitHub repository identifier (owner/repo).
7#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
8#[serde(try_from = "String")]
9pub struct GitHubRepo {
10    /// The repository owner (user or organization).
11    pub owner: String,
12    /// The repository name.
13    pub repo: String,
14}
15
16impl FromStr for GitHubRepo {
17    type Err = String;
18
19    fn from_str(s: &str) -> Result<Self, Self::Err> {
20        let parts: Vec<&str> = s.split('/').collect();
21        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
22            return Err(format!(
23                "invalid GitHub repo '{}': expected 'owner/repo' format",
24                s
25            ));
26        }
27        Ok(Self {
28            owner: parts[0].to_string(),
29            repo: parts[1].to_string(),
30        })
31    }
32}
33
34impl TryFrom<String> for GitHubRepo {
35    type Error = String;
36
37    fn try_from(s: String) -> Result<Self, Self::Error> {
38        s.parse()
39    }
40}
41
42/// Source of repository code - either a GitHub repo or a local tarball file.
43#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
44pub enum Source {
45    /// GitHub repository source. Downloads tarballs from GitHub API.
46    #[serde(rename = "github")]
47    GitHub {
48        /// GitHub repository in "owner/repo" format.
49        repo: GitHubRepo,
50        /// Path to file containing the GitHub personal access token.
51        /// Optional for public repositories (unauthenticated access has lower rate limits).
52        token_file: Option<PathBuf>,
53    },
54    /// Local tarball file source. Reads a .tar.gz file from disk.
55    #[serde(rename = "file")]
56    File {
57        /// Path to the tarball file (.tar.gz).
58        path: PathBuf,
59    },
60}
61
62/// Configuration for a repository's source code access.
63#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
64pub struct CodeToolConfig {
65    /// Name of the repository (used to identify it in tool calls).
66    pub name: String,
67    /// Source of the repository code.
68    #[serde(flatten)]
69    pub source: Source,
70    /// Glob patterns to filter which files are included from the tarball.
71    /// If non-empty, only files matching at least one pattern are kept.
72    /// Uses gitignore-style glob syntax (e.g., "*.rs", "src/**/*.rs").
73    #[serde(default)]
74    pub glob: Vec<String>,
75    /// Include files that aren't valid UTF-8 (using lossy conversion).
76    /// By default (false), non-UTF-8 files are skipped entirely.
77    #[serde(default)]
78    pub include_non_utf8: bool,
79    /// Path to a file containing a summary/description of the codebase.
80    /// If present, the agent can request a summary of the repository.
81    #[serde(default)]
82    pub summary_file: Option<String>,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_parse_github_source() {
91        let json = r#"{
92            "name": "my-app",
93            "github": {
94                "repo": "owner/repo-name",
95                "token_file": "/path/to/token"
96            }
97        }"#;
98
99        let config: CodeToolConfig = serde_json::from_str(json).unwrap();
100        assert_eq!(config.name, "my-app");
101        assert_eq!(
102            config.source,
103            Source::GitHub {
104                repo: GitHubRepo {
105                    owner: "owner".to_string(),
106                    repo: "repo-name".to_string(),
107                },
108                token_file: Some(PathBuf::from("/path/to/token")),
109            }
110        );
111        assert!(config.glob.is_empty());
112        assert!(!config.include_non_utf8);
113    }
114
115    #[test]
116    fn test_parse_github_source_no_token() {
117        let json = r#"{
118            "name": "public-app",
119            "github": {
120                "repo": "org/public-repo"
121            }
122        }"#;
123
124        let config: CodeToolConfig = serde_json::from_str(json).unwrap();
125        assert_eq!(config.name, "public-app");
126        assert_eq!(
127            config.source,
128            Source::GitHub {
129                repo: GitHubRepo {
130                    owner: "org".to_string(),
131                    repo: "public-repo".to_string(),
132                },
133                token_file: None,
134            }
135        );
136    }
137
138    #[test]
139    fn test_parse_file_source() {
140        let json = r#"{
141            "name": "local-app",
142            "file": {
143                "path": "/tmp/source.tar.gz"
144            }
145        }"#;
146
147        let config: CodeToolConfig = serde_json::from_str(json).unwrap();
148        assert_eq!(config.name, "local-app");
149        assert_eq!(
150            config.source,
151            Source::File {
152                path: PathBuf::from("/tmp/source.tar.gz"),
153            }
154        );
155    }
156
157    #[test]
158    fn test_parse_with_glob_and_options() {
159        let json = r#"{
160            "name": "filtered-app",
161            "github": {
162                "repo": "owner/repo"
163            },
164            "glob": ["**/*.rs", "Cargo.toml"],
165            "include_non_utf8": true
166        }"#;
167
168        let config: CodeToolConfig = serde_json::from_str(json).unwrap();
169        assert_eq!(config.name, "filtered-app");
170        assert_eq!(config.glob, vec!["**/*.rs", "Cargo.toml"]);
171        assert!(config.include_non_utf8);
172    }
173
174    #[test]
175    fn test_parse_github_repo_string() {
176        let repo: GitHubRepo = "owner/repo".parse().unwrap();
177        assert_eq!(repo.owner, "owner");
178        assert_eq!(repo.repo, "repo");
179    }
180
181    #[test]
182    fn test_parse_github_repo_invalid() {
183        assert!("invalid".parse::<GitHubRepo>().is_err());
184        assert!("".parse::<GitHubRepo>().is_err());
185        assert!("/repo".parse::<GitHubRepo>().is_err());
186        assert!("owner/".parse::<GitHubRepo>().is_err());
187        assert!("a/b/c".parse::<GitHubRepo>().is_err());
188    }
189}