1use ahash::{HashMap, HashMapExt};
4use ron2::schema::TypeKind;
5
6use crate::discovery::DiscoveredSchema;
7
8pub struct LinkResolver<'a> {
10 type_ref_to_path: HashMap<&'a str, &'a str>,
12 short_name_to_path: HashMap<&'a str, &'a str>,
14 short_name_counts: HashMap<&'a str, usize>,
16 base_url: Option<&'a str>,
18}
19
20impl<'a> LinkResolver<'a> {
21 pub fn new(schemas: &'a [DiscoveredSchema], base_url: Option<&'a str>) -> Self {
23 let mut type_ref_to_path = HashMap::new();
24 let mut short_name_to_path = HashMap::new();
25 let mut short_name_counts: HashMap<&str, usize> = HashMap::new();
26
27 for schema in schemas {
28 type_ref_to_path.insert(schema.type_path.as_str(), schema.type_path.as_str());
30
31 let short_name = type_path_short_name(&schema.type_path);
33 short_name_to_path.insert(short_name, schema.type_path.as_str());
34 *short_name_counts.entry(short_name).or_insert(0) += 1;
35
36 collect_type_refs(&schema.schema.kind, &mut |type_ref| {
39 let ref_short_name = type_path_short_name(type_ref);
41 for s in schemas {
42 if type_path_short_name(&s.type_path) == ref_short_name {
43 type_ref_to_path.insert(type_ref, s.type_path.as_str());
44 break;
45 }
46 }
47 });
48 }
49
50 Self {
51 type_ref_to_path,
52 short_name_to_path,
53 short_name_counts,
54 base_url,
55 }
56 }
57
58 pub fn has_collision(&self, short_name: &str) -> bool {
60 self.short_name_counts
61 .get(short_name)
62 .is_some_and(|c| *c > 1)
63 }
64
65 pub fn display_name(&self, type_path: &str) -> String {
68 let short_name = type_path_short_name(type_path);
69 if self.has_collision(short_name) {
70 type_path.to_string()
71 } else {
72 short_name.to_string()
73 }
74 }
75
76 pub fn type_path_to_anchor(type_path: &str) -> String {
79 type_path.replace("::", "-").to_lowercase()
80 }
81
82 fn resolve_type_ref(&self, type_ref: &str) -> String {
84 if let Some(resolved_path) = self.type_ref_to_path.get(type_ref) {
86 return type_path_to_md_path(resolved_path);
87 }
88
89 let short_name = type_path_short_name(type_ref);
91 if let Some(resolved_path) = self.short_name_to_path.get(short_name) {
92 return type_path_to_md_path(resolved_path);
93 }
94
95 type_path_to_md_path(type_ref)
97 }
98
99 pub fn type_to_markdown(&self, type_kind: &TypeKind) -> String {
101 match type_kind {
102 TypeKind::Bool => "`bool`".to_string(),
104 TypeKind::I8 => "`i8`".to_string(),
105 TypeKind::I16 => "`i16`".to_string(),
106 TypeKind::I32 => "`i32`".to_string(),
107 TypeKind::I64 => "`i64`".to_string(),
108 TypeKind::I128 => "`i128`".to_string(),
109 TypeKind::U8 => "`u8`".to_string(),
110 TypeKind::U16 => "`u16`".to_string(),
111 TypeKind::U32 => "`u32`".to_string(),
112 TypeKind::U64 => "`u64`".to_string(),
113 TypeKind::U128 => "`u128`".to_string(),
114 TypeKind::F32 => "`f32`".to_string(),
115 TypeKind::F64 => "`f64`".to_string(),
116 TypeKind::Char => "`char`".to_string(),
117 TypeKind::String => "`String`".to_string(),
118 TypeKind::Unit => "`()`".to_string(),
119
120 TypeKind::Option(inner) => {
122 format!("Option<{}>", self.type_to_markdown(inner))
123 }
124 TypeKind::List(inner) => {
125 format!("List<{}>", self.type_to_markdown(inner))
126 }
127 TypeKind::Map { key, value } => {
128 format!(
129 "Map<{}, {}>",
130 self.type_to_markdown(key),
131 self.type_to_markdown(value)
132 )
133 }
134 TypeKind::Tuple(types) => {
135 let items: Vec<_> = types.iter().map(|t| self.type_to_markdown(t)).collect();
136 format!("({})", items.join(", "))
137 }
138
139 TypeKind::TypeRef(path) => {
141 let display_name = type_path_short_name(path);
142 let md_path = self.resolve_type_ref(path);
143
144 if let Some(base) = self.base_url {
145 format!(
146 "[`{}`]({}/{})",
147 display_name,
148 base.trim_end_matches('/'),
149 md_path
150 )
151 } else {
152 format!("[`{}`](./{})", display_name, md_path)
153 }
154 }
155
156 TypeKind::Struct { .. } => "`struct`".to_string(),
158 TypeKind::Enum { .. } => "`enum`".to_string(),
159 }
160 }
161
162 pub fn type_to_markdown_anchor(&self, type_kind: &TypeKind) -> String {
164 match type_kind {
165 TypeKind::Bool => "`bool`".to_string(),
167 TypeKind::I8 => "`i8`".to_string(),
168 TypeKind::I16 => "`i16`".to_string(),
169 TypeKind::I32 => "`i32`".to_string(),
170 TypeKind::I64 => "`i64`".to_string(),
171 TypeKind::I128 => "`i128`".to_string(),
172 TypeKind::U8 => "`u8`".to_string(),
173 TypeKind::U16 => "`u16`".to_string(),
174 TypeKind::U32 => "`u32`".to_string(),
175 TypeKind::U64 => "`u64`".to_string(),
176 TypeKind::U128 => "`u128`".to_string(),
177 TypeKind::F32 => "`f32`".to_string(),
178 TypeKind::F64 => "`f64`".to_string(),
179 TypeKind::Char => "`char`".to_string(),
180 TypeKind::String => "`String`".to_string(),
181 TypeKind::Unit => "`()`".to_string(),
182
183 TypeKind::Option(inner) => {
185 format!("Option<{}>", self.type_to_markdown_anchor(inner))
186 }
187 TypeKind::List(inner) => {
188 format!("List<{}>", self.type_to_markdown_anchor(inner))
189 }
190 TypeKind::Map { key, value } => {
191 format!(
192 "Map<{}, {}>",
193 self.type_to_markdown_anchor(key),
194 self.type_to_markdown_anchor(value)
195 )
196 }
197 TypeKind::Tuple(types) => {
198 let items: Vec<_> = types
199 .iter()
200 .map(|t| self.type_to_markdown_anchor(t))
201 .collect();
202 format!("({})", items.join(", "))
203 }
204
205 TypeKind::TypeRef(path) => {
207 let resolved = self.resolve_type_ref_path(path);
208 let display = self.display_name(&resolved);
209 let anchor = Self::type_path_to_anchor(&resolved);
210 format!("[`{}`](#{})", display, anchor)
211 }
212
213 TypeKind::Struct { .. } => "`struct`".to_string(),
215 TypeKind::Enum { .. } => "`enum`".to_string(),
216 }
217 }
218
219 fn resolve_type_ref_path(&self, type_ref: &str) -> String {
221 if let Some(resolved_path) = self.type_ref_to_path.get(type_ref) {
223 return (*resolved_path).to_string();
224 }
225
226 let short_name = type_path_short_name(type_ref);
228 if let Some(resolved_path) = self.short_name_to_path.get(short_name) {
229 return (*resolved_path).to_string();
230 }
231
232 type_ref.to_string()
234 }
235}
236
237fn collect_type_refs<'a>(kind: &'a TypeKind, collector: &mut impl FnMut(&'a str)) {
239 match kind {
240 TypeKind::TypeRef(path) => collector(path),
241 TypeKind::Option(inner) | TypeKind::List(inner) => collect_type_refs(inner, collector),
242 TypeKind::Map { key, value } => {
243 collect_type_refs(key, collector);
244 collect_type_refs(value, collector);
245 }
246 TypeKind::Tuple(types) => {
247 for t in types {
248 collect_type_refs(t, collector);
249 }
250 }
251 TypeKind::Struct { fields } => {
252 for f in fields {
253 collect_type_refs(&f.ty, collector);
254 }
255 }
256 TypeKind::Enum { variants } => {
257 for v in variants {
258 match &v.kind {
259 ron2::schema::VariantKind::Tuple(types) => {
260 for t in types {
261 collect_type_refs(t, collector);
262 }
263 }
264 ron2::schema::VariantKind::Struct(fields) => {
265 for f in fields {
266 collect_type_refs(&f.ty, collector);
267 }
268 }
269 ron2::schema::VariantKind::Unit => {}
270 }
271 }
272 }
273 _ => {}
274 }
275}
276
277pub fn type_path_to_md_path(type_path: &str) -> String {
281 type_path.replace("::", "/") + ".md"
282}
283
284pub fn type_path_short_name(type_path: &str) -> &str {
288 type_path.split("::").last().unwrap_or(type_path)
289}
290
291#[cfg(test)]
292mod tests {
293 use std::path::PathBuf;
294
295 use ron2::schema::Schema;
296
297 use super::*;
298
299 fn make_schema(type_path: &str) -> DiscoveredSchema {
300 DiscoveredSchema {
301 path: PathBuf::from("test.schema.ron"),
302 type_path: type_path.to_string(),
303 schema: Schema::new(TypeKind::Unit),
304 }
305 }
306
307 #[test]
308 fn test_link_resolver_primitives() {
309 let schemas = vec![make_schema("Config")];
310 let resolver = LinkResolver::new(&schemas, None);
311
312 assert_eq!(resolver.type_to_markdown(&TypeKind::Bool), "`bool`");
313 assert_eq!(resolver.type_to_markdown(&TypeKind::String), "`String`");
314 assert_eq!(resolver.type_to_markdown(&TypeKind::I32), "`i32`");
315 }
316
317 #[test]
318 fn test_link_resolver_compound() {
319 let schemas = vec![make_schema("Config")];
320 let resolver = LinkResolver::new(&schemas, None);
321
322 let option_string = TypeKind::Option(Box::new(TypeKind::String));
323 assert_eq!(
324 resolver.type_to_markdown(&option_string),
325 "Option<`String`>"
326 );
327
328 let list_i32 = TypeKind::List(Box::new(TypeKind::I32));
329 assert_eq!(resolver.type_to_markdown(&list_i32), "List<`i32`>");
330 }
331
332 #[test]
333 fn test_link_resolver_typeref_resolves_to_discovered() {
334 let schemas = vec![make_schema("Config")];
337 let resolver = LinkResolver::new(&schemas, None);
338
339 let type_ref = TypeKind::TypeRef("my_crate::Config".to_string());
340 assert_eq!(
342 resolver.type_to_markdown(&type_ref),
343 "[`Config`](./Config.md)"
344 );
345 }
346
347 #[test]
348 fn test_link_resolver_with_base_url() {
349 let schemas = vec![make_schema("Config")];
350 let resolver = LinkResolver::new(&schemas, Some("/docs/types"));
351
352 let type_ref = TypeKind::TypeRef("my_crate::Config".to_string());
353 assert_eq!(
354 resolver.type_to_markdown(&type_ref),
355 "[`Config`](/docs/types/Config.md)"
356 );
357 }
358
359 #[test]
360 fn test_type_path_to_md_path() {
361 assert_eq!(
362 type_path_to_md_path("my_crate::config::AppConfig"),
363 "my_crate/config/AppConfig.md"
364 );
365 }
366
367 #[test]
368 fn test_type_path_short_name() {
369 assert_eq!(
370 type_path_short_name("my_crate::config::AppConfig"),
371 "AppConfig"
372 );
373 assert_eq!(type_path_short_name("Config"), "Config");
374 }
375
376 #[test]
377 fn test_collision_detection_no_collision() {
378 let schemas = vec![
379 make_schema("crate_a::Config"),
380 make_schema("crate_b::Server"),
381 ];
382 let resolver = LinkResolver::new(&schemas, None);
383
384 assert!(!resolver.has_collision("Config"));
385 assert!(!resolver.has_collision("Server"));
386 assert_eq!(resolver.display_name("crate_a::Config"), "Config");
387 assert_eq!(resolver.display_name("crate_b::Server"), "Server");
388 }
389
390 #[test]
391 fn test_collision_detection_with_collision() {
392 let schemas = vec![
393 make_schema("crate_a::Config"),
394 make_schema("crate_b::Config"),
395 ];
396 let resolver = LinkResolver::new(&schemas, None);
397
398 assert!(resolver.has_collision("Config"));
399 assert_eq!(resolver.display_name("crate_a::Config"), "crate_a::Config");
401 assert_eq!(resolver.display_name("crate_b::Config"), "crate_b::Config");
402 }
403
404 #[test]
405 fn test_type_path_to_anchor() {
406 assert_eq!(
407 LinkResolver::type_path_to_anchor("my_crate::config::AppConfig"),
408 "my_crate-config-appconfig"
409 );
410 assert_eq!(LinkResolver::type_path_to_anchor("Config"), "config");
411 }
412
413 #[test]
414 fn test_type_to_markdown_anchor() {
415 let schemas = vec![make_schema("Config")];
416 let resolver = LinkResolver::new(&schemas, None);
417
418 let type_ref = TypeKind::TypeRef("Config".to_string());
419 assert_eq!(
420 resolver.type_to_markdown_anchor(&type_ref),
421 "[`Config`](#config)"
422 );
423 }
424
425 #[test]
426 fn test_type_to_markdown_anchor_with_collision() {
427 let schemas = vec![
428 make_schema("crate_a::Config"),
429 make_schema("crate_b::Config"),
430 ];
431 let resolver = LinkResolver::new(&schemas, None);
432
433 let type_ref = TypeKind::TypeRef("crate_a::Config".to_string());
434 assert_eq!(
435 resolver.type_to_markdown_anchor(&type_ref),
436 "[`crate_a::Config`](#crate_a-config)"
437 );
438 }
439}