acp/bridge/
config.rs

1//! @acp:module "Bridge Configuration"
2//! @acp:summary "RFC-0006: Configuration types for documentation bridging"
3//! @acp:domain cli
4//! @acp:layer model
5
6use serde::{Deserialize, Serialize};
7
8/// @acp:summary "Precedence mode for merging native docs with ACP"
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum Precedence {
12    /// ACP annotations take precedence; native docs fill gaps
13    #[default]
14    AcpFirst,
15    /// Native docs are authoritative; ACP adds directives only
16    NativeFirst,
17    /// Intelligently combine both sources
18    Merge,
19}
20
21impl std::fmt::Display for Precedence {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Precedence::AcpFirst => write!(f, "acp-first"),
25            Precedence::NativeFirst => write!(f, "native-first"),
26            Precedence::Merge => write!(f, "merge"),
27        }
28    }
29}
30
31/// @acp:summary "Strictness mode for parsing native documentation"
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum Strictness {
35    /// Best-effort extraction; skip malformed documentation
36    #[default]
37    Permissive,
38    /// Reject and warn on malformed documentation
39    Strict,
40}
41
42/// @acp:summary "Python docstring style"
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum DocstringStyle {
46    /// Auto-detect from content
47    #[default]
48    Auto,
49    /// Google-style docstrings
50    Google,
51    /// NumPy-style docstrings
52    Numpy,
53    /// Sphinx/reST-style docstrings
54    Sphinx,
55}
56
57/// @acp:summary "JSDoc/TSDoc configuration"
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct JsDocConfig {
61    /// Whether JSDoc bridging is enabled
62    #[serde(default = "default_true")]
63    pub enabled: bool,
64    /// Extract types from @param {Type} annotations
65    #[serde(default = "default_true")]
66    pub extract_types: bool,
67    /// Tags to convert to ACP annotations
68    #[serde(default = "default_jsdoc_tags")]
69    pub convert_tags: Vec<String>,
70}
71
72impl Default for JsDocConfig {
73    fn default() -> Self {
74        Self {
75            enabled: true,
76            extract_types: true,
77            convert_tags: default_jsdoc_tags(),
78        }
79    }
80}
81
82fn default_jsdoc_tags() -> Vec<String> {
83    vec![
84        "param".to_string(),
85        "returns".to_string(),
86        "throws".to_string(),
87        "deprecated".to_string(),
88        "example".to_string(),
89        "see".to_string(),
90    ]
91}
92
93/// @acp:summary "Python docstring configuration"
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct PythonConfig {
97    /// Whether Python docstring bridging is enabled
98    #[serde(default = "default_true")]
99    pub enabled: bool,
100    /// Docstring style to use (or auto-detect)
101    #[serde(default)]
102    pub docstring_style: DocstringStyle,
103    /// Extract types from Python type hints
104    #[serde(default = "default_true")]
105    pub extract_type_hints: bool,
106    /// Sections to convert to ACP annotations
107    #[serde(default = "default_python_sections")]
108    pub convert_sections: Vec<String>,
109}
110
111impl Default for PythonConfig {
112    fn default() -> Self {
113        Self {
114            enabled: true,
115            docstring_style: DocstringStyle::Auto,
116            extract_type_hints: true,
117            convert_sections: default_python_sections(),
118        }
119    }
120}
121
122fn default_python_sections() -> Vec<String> {
123    vec![
124        "Args".to_string(),
125        "Parameters".to_string(),
126        "Returns".to_string(),
127        "Raises".to_string(),
128        "Example".to_string(),
129        "Yields".to_string(),
130    ]
131}
132
133/// @acp:summary "Rust doc comment configuration"
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct RustConfig {
137    /// Whether Rust doc bridging is enabled
138    #[serde(default = "default_true")]
139    pub enabled: bool,
140    /// Sections to convert to ACP annotations
141    #[serde(default = "default_rust_sections")]
142    pub convert_sections: Vec<String>,
143}
144
145impl Default for RustConfig {
146    fn default() -> Self {
147        Self {
148            enabled: true,
149            convert_sections: default_rust_sections(),
150        }
151    }
152}
153
154fn default_rust_sections() -> Vec<String> {
155    vec![
156        "Arguments".to_string(),
157        "Returns".to_string(),
158        "Panics".to_string(),
159        "Errors".to_string(),
160        "Examples".to_string(),
161        "Safety".to_string(),
162    ]
163}
164
165/// @acp:summary "Provenance tracking configuration"
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct ProvenanceConfig {
169    /// Mark converted annotations with source information
170    #[serde(default = "default_true")]
171    pub mark_converted: bool,
172    /// Include source format in provenance
173    #[serde(default = "default_true")]
174    pub include_source_format: bool,
175}
176
177impl Default for ProvenanceConfig {
178    fn default() -> Self {
179        Self {
180            mark_converted: true,
181            include_source_format: true,
182        }
183    }
184}
185
186fn default_true() -> bool {
187    true
188}
189
190/// @acp:summary "RFC-0006: Documentation bridging configuration"
191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct BridgeConfig {
194    /// Enable documentation bridging (default: false)
195    #[serde(default)]
196    pub enabled: bool,
197    /// Precedence mode when both native and ACP exist
198    #[serde(default)]
199    pub precedence: Precedence,
200    /// How to handle malformed documentation
201    #[serde(default)]
202    pub strictness: Strictness,
203    /// JSDoc/TSDoc settings
204    #[serde(default)]
205    pub jsdoc: JsDocConfig,
206    /// Python docstring settings
207    #[serde(default)]
208    pub python: PythonConfig,
209    /// Rust doc comment settings
210    #[serde(default)]
211    pub rust: RustConfig,
212    /// Provenance tracking settings
213    #[serde(default)]
214    pub provenance: ProvenanceConfig,
215}
216
217impl BridgeConfig {
218    /// @acp:summary "Create a new default configuration (bridging disabled)"
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    /// @acp:summary "Create configuration with bridging enabled"
224    pub fn enabled() -> Self {
225        Self {
226            enabled: true,
227            ..Default::default()
228        }
229    }
230
231    /// @acp:summary "Check if bridging is enabled for a specific language"
232    pub fn is_enabled_for(&self, language: &str) -> bool {
233        if !self.enabled {
234            return false;
235        }
236        match language.to_lowercase().as_str() {
237            "javascript" | "typescript" | "js" | "ts" => self.jsdoc.enabled,
238            "python" | "py" => self.python.enabled,
239            "rust" | "rs" => self.rust.enabled,
240            "java" | "kotlin" => true, // Javadoc always enabled if bridging is on
241            "go" => true,              // Godoc always enabled if bridging is on
242            _ => false,
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_bridge_config_defaults() {
253        let config = BridgeConfig::new();
254        assert!(!config.enabled);
255        assert_eq!(config.precedence, Precedence::AcpFirst);
256        assert_eq!(config.strictness, Strictness::Permissive);
257        assert!(config.jsdoc.enabled);
258        assert!(config.python.enabled);
259        assert!(config.rust.enabled);
260    }
261
262    #[test]
263    fn test_bridge_config_enabled() {
264        let config = BridgeConfig::enabled();
265        assert!(config.enabled);
266        assert!(config.is_enabled_for("typescript"));
267        assert!(config.is_enabled_for("python"));
268        assert!(config.is_enabled_for("rust"));
269    }
270
271    #[test]
272    fn test_is_enabled_for_disabled_global() {
273        let config = BridgeConfig::new();
274        assert!(!config.is_enabled_for("typescript"));
275        assert!(!config.is_enabled_for("python"));
276    }
277
278    #[test]
279    fn test_is_enabled_for_specific_disabled() {
280        let mut config = BridgeConfig::enabled();
281        config.python.enabled = false;
282
283        assert!(config.is_enabled_for("typescript"));
284        assert!(!config.is_enabled_for("python"));
285    }
286
287    #[test]
288    fn test_precedence_display() {
289        assert_eq!(Precedence::AcpFirst.to_string(), "acp-first");
290        assert_eq!(Precedence::NativeFirst.to_string(), "native-first");
291        assert_eq!(Precedence::Merge.to_string(), "merge");
292    }
293
294    #[test]
295    fn test_config_serialization() {
296        let config = BridgeConfig::enabled();
297        let json = serde_json::to_string_pretty(&config).unwrap();
298        let parsed: BridgeConfig = serde_json::from_str(&json).unwrap();
299
300        assert!(parsed.enabled);
301        assert_eq!(parsed.precedence, Precedence::AcpFirst);
302    }
303}