Skip to main content

_diffctx/edges/semantic/
openapi.rs

1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::edge_weights::OPENAPI_SEMANTIC;
8use crate::config::weights::EDGE_WEIGHTS;
9use crate::types::Fragment;
10
11use super::super::EdgeDict;
12use super::super::base::{self, EdgeBuilder, add_edge, discover_files_by_refs};
13
14fn is_openapi_candidate(path: &Path) -> bool {
15    let ext = base::file_ext(path);
16    matches!(ext.as_str(), ".yaml" | ".yml" | ".json")
17}
18
19fn is_openapi_file(content: &str) -> bool {
20    content
21        .lines()
22        .take(OPENAPI_SEMANTIC.marker_scan_lines)
23        .any(|l| l.contains("openapi:") || l.contains("swagger:"))
24}
25
26static INTERNAL_REF_RE: Lazy<Regex> =
27    Lazy::new(|| Regex::new(r#"\$ref:\s*['"]?#/components/(\w+)/(\w+)"#).unwrap());
28static EXTERNAL_REF_RE: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r#"\$ref:\s*['"]?([^#'"]+)#"#).unwrap());
30fn extract_internal_refs(content: &str) -> FxHashSet<String> {
31    INTERNAL_REF_RE
32        .captures_iter(content)
33        .map(|c| c[2].to_string())
34        .collect()
35}
36
37fn extract_external_refs(content: &str) -> FxHashSet<String> {
38    EXTERNAL_REF_RE
39        .captures_iter(content)
40        .map(|c| c[1].trim().to_string())
41        .collect()
42}
43
44fn extract_schema_defs(content: &str) -> FxHashSet<String> {
45    let mut defs = FxHashSet::default();
46    let mut in_components = false;
47    let mut in_schemas = false;
48    for line in content.lines() {
49        let trimmed = line.trim_start();
50        let indent = line.len() - trimmed.len();
51        if indent == 0 && trimmed.starts_with("components:") {
52            in_components = true;
53            in_schemas = false;
54            continue;
55        }
56        if indent == 0 && !trimmed.is_empty() {
57            in_components = false;
58            in_schemas = false;
59            continue;
60        }
61        if in_components && indent == 2 && trimmed.starts_with("schemas:") {
62            in_schemas = true;
63            continue;
64        }
65        if in_components && indent == 2 && !trimmed.is_empty() {
66            in_schemas = false;
67            continue;
68        }
69        if in_schemas && indent == 4 {
70            if let Some(name) = trimmed.strip_suffix(':') {
71                let name = name.trim();
72                if !name.is_empty() {
73                    defs.insert(name.to_string());
74                }
75            }
76        }
77    }
78    defs
79}
80
81pub struct OpenapiEdgeBuilder;
82
83impl EdgeBuilder for OpenapiEdgeBuilder {
84    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
85        let frags: Vec<&Fragment> = fragments
86            .iter()
87            .filter(|f| is_openapi_candidate(Path::new(f.path())) && is_openapi_file(&f.content))
88            .collect();
89        if frags.is_empty() {
90            return FxHashMap::default();
91        }
92
93        let internal_w = EDGE_WEIGHTS["openapi_internal_ref"].forward;
94        let external_w = EDGE_WEIGHTS["openapi_external_ref"].forward;
95        let reverse_factor = EDGE_WEIGHTS["openapi_internal_ref"].reverse_factor;
96
97        let idx = base::FragmentIndex::new(fragments, repo_root);
98        let mut schema_to_frags: FxHashMap<String, Vec<_>> = FxHashMap::default();
99        for f in &frags {
100            for name in extract_schema_defs(&f.content) {
101                schema_to_frags
102                    .entry(name.to_lowercase())
103                    .or_default()
104                    .push(f.id.clone());
105            }
106        }
107
108        let mut edges: EdgeDict = FxHashMap::default();
109
110        for f in &frags {
111            for iref in extract_internal_refs(&f.content) {
112                if let Some(targets) = schema_to_frags.get(&iref.to_lowercase()) {
113                    for t in targets {
114                        if t != &f.id {
115                            add_edge(&mut edges, &f.id, t, internal_w, reverse_factor);
116                        }
117                    }
118                }
119            }
120            for eref in extract_external_refs(&f.content) {
121                base::link_by_name(&f.id, &eref, &idx, &mut edges, external_w, reverse_factor);
122            }
123        }
124        edges
125    }
126
127    fn discover_related_files(
128        &self,
129        changed: &[PathBuf],
130        candidates: &[PathBuf],
131        repo_root: Option<&Path>,
132        file_cache: Option<&FxHashMap<PathBuf, String>>,
133    ) -> Vec<PathBuf> {
134        let mut refs = FxHashSet::default();
135        for f in changed {
136            if !is_openapi_candidate(f) {
137                continue;
138            }
139            if let Some(content) = base::read_file_cached(f, file_cache) {
140                if !is_openapi_file(&content) {
141                    continue;
142                }
143                refs.extend(extract_external_refs(&content));
144            }
145        }
146        if refs.is_empty() {
147            return vec![];
148        }
149        discover_files_by_refs(&refs, changed, candidates, repo_root)
150    }
151}