1use super::config::{BridgeConfig, Precedence};
7use super::BridgeResult;
8use crate::annotate::converters::ParsedDocumentation;
9use crate::cache::{BridgeSource, ParamEntry, ReturnsEntry, SourceFormat, ThrowsEntry};
10
11#[derive(Debug, Clone, Default)]
13pub struct AcpAnnotations {
14 pub summary: Option<String>,
16 pub directive: Option<String>,
18 pub params: Vec<(String, String)>,
20 pub returns: Option<String>,
22 pub throws: Vec<(String, String)>,
24}
25
26pub struct BridgeMerger {
28 config: BridgeConfig,
29}
30
31impl BridgeMerger {
32 pub fn new(config: &BridgeConfig) -> Self {
34 Self {
35 config: config.clone(),
36 }
37 }
38
39 pub fn merge(
41 &self,
42 native: Option<&ParsedDocumentation>,
43 native_format: SourceFormat,
44 acp: &AcpAnnotations,
45 ) -> BridgeResult {
46 if native.is_none() || !self.config.enabled {
48 return self.build_acp_only(acp);
49 }
50
51 let native = native.unwrap();
52
53 if native.is_empty() {
55 return self.build_acp_only(acp);
56 }
57
58 if self.is_acp_empty(acp) {
60 return BridgeResult::from_native(native, native_format);
61 }
62
63 match self.config.precedence {
65 Precedence::AcpFirst => self.merge_acp_first(native, native_format, acp),
66 Precedence::NativeFirst => self.merge_native_first(native, native_format, acp),
67 Precedence::Merge => self.merge_combined(native, native_format, acp),
68 }
69 }
70
71 fn build_acp_only(&self, acp: &AcpAnnotations) -> BridgeResult {
73 let mut result = BridgeResult {
74 summary: acp.summary.clone(),
75 directive: acp.directive.clone(),
76 source: BridgeSource::Explicit,
77 source_formats: vec![SourceFormat::Acp],
78 ..Default::default()
79 };
80
81 for (name, directive) in &acp.params {
83 result.params.push(ParamEntry {
84 name: name.clone(),
85 r#type: None,
86 type_source: None,
87 description: None,
88 directive: Some(directive.clone()),
89 optional: false,
90 default: None,
91 source: BridgeSource::Explicit,
92 source_format: Some(SourceFormat::Acp),
93 source_formats: vec![],
94 });
95 }
96
97 if let Some(directive) = &acp.returns {
99 result.returns = Some(ReturnsEntry {
100 r#type: None,
101 type_source: None,
102 description: None,
103 directive: Some(directive.clone()),
104 source: BridgeSource::Explicit,
105 source_format: Some(SourceFormat::Acp),
106 source_formats: vec![],
107 });
108 }
109
110 for (exception, directive) in &acp.throws {
112 result.throws.push(ThrowsEntry {
113 exception: exception.clone(),
114 description: None,
115 directive: Some(directive.clone()),
116 source: BridgeSource::Explicit,
117 source_format: Some(SourceFormat::Acp),
118 });
119 }
120
121 result
122 }
123
124 fn is_acp_empty(&self, acp: &AcpAnnotations) -> bool {
126 acp.summary.is_none()
127 && acp.directive.is_none()
128 && acp.params.is_empty()
129 && acp.returns.is_none()
130 && acp.throws.is_empty()
131 }
132
133 fn merge_acp_first(
136 &self,
137 native: &ParsedDocumentation,
138 native_format: SourceFormat,
139 acp: &AcpAnnotations,
140 ) -> BridgeResult {
141 let mut result = BridgeResult {
142 summary: native.summary.clone().or_else(|| acp.summary.clone()),
144 directive: acp.directive.clone(),
146 source: BridgeSource::Merged,
147 source_formats: vec![native_format, SourceFormat::Acp],
148 examples: native.examples.clone(),
149 ..Default::default()
150 };
151
152 result.params = self.merge_params(native, native_format, acp);
154
155 result.returns = self.merge_returns(native, native_format, acp);
157
158 result.throws = self.merge_throws(native, native_format, acp);
160
161 result
162 }
163
164 fn merge_native_first(
167 &self,
168 native: &ParsedDocumentation,
169 native_format: SourceFormat,
170 acp: &AcpAnnotations,
171 ) -> BridgeResult {
172 let mut result = BridgeResult::from_native(native, native_format);
173
174 result.directive = acp.directive.clone();
176 result.source = BridgeSource::Merged;
177 result.source_formats = vec![native_format, SourceFormat::Acp];
178
179 for param in &mut result.params {
181 if let Some((_, directive)) = acp.params.iter().find(|(n, _)| n == ¶m.name) {
182 param.directive = Some(directive.clone());
183 param.source = BridgeSource::Merged;
184 param.source_formats = vec![native_format, SourceFormat::Acp];
185 }
186 }
187
188 if let Some(returns) = &mut result.returns {
190 if let Some(directive) = &acp.returns {
191 returns.directive = Some(directive.clone());
192 returns.source = BridgeSource::Merged;
193 returns.source_formats = vec![native_format, SourceFormat::Acp];
194 }
195 }
196
197 result
198 }
199
200 fn merge_combined(
202 &self,
203 native: &ParsedDocumentation,
204 native_format: SourceFormat,
205 acp: &AcpAnnotations,
206 ) -> BridgeResult {
207 let summary = match (&native.summary, &acp.summary) {
209 (Some(n), Some(a)) if n != a => Some(format!("{} {}", n, a)),
210 (Some(n), _) => Some(n.clone()),
211 (_, Some(a)) => Some(a.clone()),
212 _ => None,
213 };
214
215 let mut result = BridgeResult {
216 summary,
217 directive: acp.directive.clone(),
218 source: BridgeSource::Merged,
219 source_formats: vec![native_format, SourceFormat::Acp],
220 examples: native.examples.clone(),
221 ..Default::default()
222 };
223
224 result.params = self.merge_params(native, native_format, acp);
225 result.returns = self.merge_returns(native, native_format, acp);
226 result.throws = self.merge_throws(native, native_format, acp);
227
228 result
229 }
230
231 fn merge_params(
233 &self,
234 native: &ParsedDocumentation,
235 native_format: SourceFormat,
236 acp: &AcpAnnotations,
237 ) -> Vec<ParamEntry> {
238 let mut params = Vec::new();
239
240 for (name, type_str, desc) in &native.params {
242 let directive = acp
243 .params
244 .iter()
245 .find(|(n, _)| n == name)
246 .map(|(_, d)| d.clone());
247
248 let source = if directive.is_some() {
249 BridgeSource::Merged
250 } else {
251 BridgeSource::Converted
252 };
253
254 let source_formats = if directive.is_some() {
255 vec![native_format, SourceFormat::Acp]
256 } else {
257 vec![]
258 };
259
260 params.push(ParamEntry {
261 name: name.clone(),
262 r#type: type_str.clone(),
263 type_source: type_source_from_format(native_format),
264 description: desc.clone(),
265 directive,
266 optional: false,
267 default: None,
268 source,
269 source_format: if source_formats.is_empty() {
270 Some(native_format)
271 } else {
272 None
273 },
274 source_formats,
275 });
276 }
277
278 for (name, directive) in &acp.params {
280 if !params.iter().any(|p| &p.name == name) {
281 params.push(ParamEntry {
282 name: name.clone(),
283 r#type: None,
284 type_source: None,
285 description: None,
286 directive: Some(directive.clone()),
287 optional: false,
288 default: None,
289 source: BridgeSource::Explicit,
290 source_format: Some(SourceFormat::Acp),
291 source_formats: vec![],
292 });
293 }
294 }
295
296 params
297 }
298
299 fn merge_returns(
301 &self,
302 native: &ParsedDocumentation,
303 native_format: SourceFormat,
304 acp: &AcpAnnotations,
305 ) -> Option<ReturnsEntry> {
306 match (&native.returns, &acp.returns) {
307 (Some((type_str, desc)), Some(directive)) => {
308 Some(ReturnsEntry {
310 r#type: type_str.clone(),
311 type_source: type_source_from_format(native_format),
312 description: desc.clone(),
313 directive: Some(directive.clone()),
314 source: BridgeSource::Merged,
315 source_format: None,
316 source_formats: vec![native_format, SourceFormat::Acp],
317 })
318 }
319 (Some((type_str, desc)), None) => {
320 Some(ReturnsEntry {
322 r#type: type_str.clone(),
323 type_source: type_source_from_format(native_format),
324 description: desc.clone(),
325 directive: None,
326 source: BridgeSource::Converted,
327 source_format: Some(native_format),
328 source_formats: vec![],
329 })
330 }
331 (None, Some(directive)) => {
332 Some(ReturnsEntry {
334 r#type: None,
335 type_source: None,
336 description: None,
337 directive: Some(directive.clone()),
338 source: BridgeSource::Explicit,
339 source_format: Some(SourceFormat::Acp),
340 source_formats: vec![],
341 })
342 }
343 (None, None) => None,
344 }
345 }
346
347 fn merge_throws(
349 &self,
350 native: &ParsedDocumentation,
351 native_format: SourceFormat,
352 acp: &AcpAnnotations,
353 ) -> Vec<ThrowsEntry> {
354 let mut throws = Vec::new();
355
356 for (exc_type, desc) in &native.throws {
358 let directive = acp
359 .throws
360 .iter()
361 .find(|(e, _)| e == exc_type)
362 .map(|(_, d)| d.clone());
363
364 let source = if directive.is_some() {
365 BridgeSource::Merged
366 } else {
367 BridgeSource::Converted
368 };
369
370 throws.push(ThrowsEntry {
371 exception: exc_type.clone(),
372 description: desc.clone(),
373 directive,
374 source,
375 source_format: Some(native_format),
376 });
377 }
378
379 for (exception, directive) in &acp.throws {
381 if !throws.iter().any(|t| &t.exception == exception) {
382 throws.push(ThrowsEntry {
383 exception: exception.clone(),
384 description: None,
385 directive: Some(directive.clone()),
386 source: BridgeSource::Explicit,
387 source_format: Some(SourceFormat::Acp),
388 });
389 }
390 }
391
392 throws
393 }
394}
395
396fn type_source_from_format(format: SourceFormat) -> Option<crate::cache::TypeSource> {
398 use crate::cache::TypeSource;
399 match format {
400 SourceFormat::Jsdoc => Some(TypeSource::Jsdoc),
401 SourceFormat::DocstringGoogle
402 | SourceFormat::DocstringNumpy
403 | SourceFormat::DocstringSphinx => Some(TypeSource::Docstring),
404 SourceFormat::Rustdoc => Some(TypeSource::Rustdoc),
405 SourceFormat::Javadoc => Some(TypeSource::Javadoc),
406 SourceFormat::TypeHint => Some(TypeSource::TypeHint),
407 _ => None,
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 fn test_config() -> BridgeConfig {
416 BridgeConfig::enabled()
417 }
418
419 #[test]
420 fn test_merge_acp_only() {
421 let merger = BridgeMerger::new(&test_config());
422 let acp = AcpAnnotations {
423 summary: Some("Test function".to_string()),
424 directive: Some("MUST validate input".to_string()),
425 params: vec![("userId".to_string(), "MUST be UUID".to_string())],
426 returns: Some("MAY be null".to_string()),
427 throws: vec![],
428 };
429
430 let result = merger.merge(None, SourceFormat::Acp, &acp);
431
432 assert_eq!(result.summary, Some("Test function".to_string()));
433 assert_eq!(result.directive, Some("MUST validate input".to_string()));
434 assert_eq!(result.source, BridgeSource::Explicit);
435 assert_eq!(result.params.len(), 1);
436 assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
437 }
438
439 #[test]
440 fn test_merge_native_only() {
441 let merger = BridgeMerger::new(&test_config());
442 let mut native = ParsedDocumentation::new();
443 native.summary = Some("Native summary".to_string());
444 native.params.push((
445 "userId".to_string(),
446 Some("string".to_string()),
447 Some("User ID".to_string()),
448 ));
449
450 let acp = AcpAnnotations::default();
451 let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
452
453 assert_eq!(result.summary, Some("Native summary".to_string()));
454 assert_eq!(result.source, BridgeSource::Converted);
455 assert_eq!(result.params.len(), 1);
456 assert!(result.params[0].directive.is_none());
457 }
458
459 #[test]
460 fn test_merge_acp_first() {
461 let config = BridgeConfig::enabled();
462 let merger = BridgeMerger::new(&config);
463
464 let mut native = ParsedDocumentation::new();
465 native.summary = Some("Native summary".to_string());
466 native.params.push((
467 "userId".to_string(),
468 Some("string".to_string()),
469 Some("User ID".to_string()),
470 ));
471 native.returns = Some((
472 Some("User".to_string()),
473 Some("The user object".to_string()),
474 ));
475
476 let acp = AcpAnnotations {
477 summary: Some("ACP summary".to_string()),
478 directive: Some("MUST authenticate".to_string()),
479 params: vec![("userId".to_string(), "MUST be UUID".to_string())],
480 returns: Some("MAY be cached".to_string()),
481 throws: vec![],
482 };
483
484 let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
485
486 assert_eq!(result.summary, Some("Native summary".to_string()));
488 assert_eq!(result.directive, Some("MUST authenticate".to_string()));
490 assert_eq!(result.source, BridgeSource::Merged);
491
492 assert_eq!(result.params.len(), 1);
494 assert_eq!(result.params[0].description, Some("User ID".to_string()));
495 assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
496 assert_eq!(result.params[0].source, BridgeSource::Merged);
497
498 let returns = result.returns.unwrap();
500 assert_eq!(returns.description, Some("The user object".to_string()));
501 assert_eq!(returns.directive, Some("MAY be cached".to_string()));
502 }
503
504 #[test]
505 fn test_merge_native_first() {
506 let mut config = BridgeConfig::enabled();
507 config.precedence = Precedence::NativeFirst;
508 let merger = BridgeMerger::new(&config);
509
510 let mut native = ParsedDocumentation::new();
511 native.summary = Some("Native summary".to_string());
512 native.params.push((
513 "userId".to_string(),
514 Some("string".to_string()),
515 Some("User ID".to_string()),
516 ));
517
518 let acp = AcpAnnotations {
519 directive: Some("MUST authenticate".to_string()),
520 params: vec![("userId".to_string(), "MUST be UUID".to_string())],
521 ..Default::default()
522 };
523
524 let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
525
526 assert_eq!(result.summary, Some("Native summary".to_string()));
528 assert_eq!(result.directive, Some("MUST authenticate".to_string()));
530 assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
532 }
533
534 #[test]
535 fn test_merge_disabled() {
536 let config = BridgeConfig::new(); let merger = BridgeMerger::new(&config);
538
539 let mut native = ParsedDocumentation::new();
540 native.summary = Some("Native".to_string());
541
542 let acp = AcpAnnotations {
543 summary: Some("ACP".to_string()),
544 ..Default::default()
545 };
546
547 let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
548
549 assert_eq!(result.summary, Some("ACP".to_string()));
551 assert_eq!(result.source, BridgeSource::Explicit);
552 }
553}