1use alloc::collections::BTreeMap;
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use core::fmt;
12
13use crate::entity::EntityKind;
14
15pub fn to_snake_case(s: &str) -> String {
26 let mut out = String::with_capacity(s.len());
27 let mut prev_sep = true; for ch in s.chars() {
29 if ch == ' ' || ch == '-' || ch == '_' {
30 if !prev_sep && !out.is_empty() {
31 out.push('_');
32 prev_sep = true;
33 }
34 } else {
35 out.push(ch.to_ascii_lowercase());
36 prev_sep = false;
37 }
38 }
39 if out.ends_with('_') {
40 out.pop();
41 }
42 out
43}
44
45#[derive(Clone, Debug)]
48pub struct EntityTypeDef {
49 pub kind: EntityKind,
51 pub type_name: &'static str,
53 pub aliases: &'static [&'static str],
56}
57
58static BUILTIN_DEFS: &[EntityTypeDef] = &[
60 EntityTypeDef {
62 kind: EntityKind::Document,
63 type_name: "paper",
64 aliases: &["preprint", "article"],
65 },
66 EntityTypeDef {
67 kind: EntityKind::Document,
68 type_name: "report",
69 aliases: &[],
70 },
71 EntityTypeDef {
72 kind: EntityKind::Document,
73 type_name: "blog_post",
74 aliases: &["blog"],
75 },
76 EntityTypeDef {
77 kind: EntityKind::Document,
78 type_name: "book",
79 aliases: &[],
80 },
81 EntityTypeDef {
82 kind: EntityKind::Document,
83 type_name: "specification",
84 aliases: &["spec"],
85 },
86 EntityTypeDef {
87 kind: EntityKind::Document,
88 type_name: "documentation",
89 aliases: &["docs"],
90 },
91 EntityTypeDef {
92 kind: EntityKind::Document,
93 type_name: "thesis",
94 aliases: &[],
95 },
96 EntityTypeDef {
97 kind: EntityKind::Document,
98 type_name: "page",
99 aliases: &["web_page"],
100 },
101 EntityTypeDef {
105 kind: EntityKind::Document,
106 type_name: "resource",
107 aliases: &[],
108 },
109 EntityTypeDef {
110 kind: EntityKind::Document,
111 type_name: "research_report",
112 aliases: &[],
113 },
114 EntityTypeDef {
116 kind: EntityKind::Concept,
117 type_name: "algorithm",
118 aliases: &["algo"],
119 },
120 EntityTypeDef {
122 kind: EntityKind::Concept,
123 type_name: "theorem",
124 aliases: &["lemma", "proposition", "corollary"],
125 },
126 EntityTypeDef {
127 kind: EntityKind::Concept,
128 type_name: "definition",
129 aliases: &["def"],
130 },
131 EntityTypeDef {
132 kind: EntityKind::Concept,
133 type_name: "structure",
134 aliases: &["inductive", "struct", "class"],
135 },
136 EntityTypeDef {
137 kind: EntityKind::Concept,
138 type_name: "instance",
139 aliases: &[],
140 },
141 EntityTypeDef {
142 kind: EntityKind::Concept,
143 type_name: "axiom",
144 aliases: &[],
145 },
146 EntityTypeDef {
147 kind: EntityKind::Concept,
148 type_name: "goal",
149 aliases: &["proof_goal"],
150 },
151 EntityTypeDef {
152 kind: EntityKind::Concept,
153 type_name: "technique",
154 aliases: &[],
155 },
156 EntityTypeDef {
157 kind: EntityKind::Concept,
158 type_name: "architecture",
159 aliases: &["arch"],
160 },
161 EntityTypeDef {
162 kind: EntityKind::Concept,
163 type_name: "model_family",
166 aliases: &["model"],
167 },
168 EntityTypeDef {
169 kind: EntityKind::Concept,
170 type_name: "theory",
171 aliases: &[],
172 },
173 EntityTypeDef {
174 kind: EntityKind::Concept,
175 type_name: "research_gap",
176 aliases: &["gap"],
177 },
178 EntityTypeDef {
179 kind: EntityKind::Concept,
180 type_name: "design_pattern",
181 aliases: &["pattern"],
182 },
183 EntityTypeDef {
184 kind: EntityKind::Concept,
185 type_name: "mathematical_operation",
186 aliases: &["math_op"],
187 },
188 EntityTypeDef {
189 kind: EntityKind::Concept,
190 type_name: "metric",
191 aliases: &[],
192 },
193 EntityTypeDef {
194 kind: EntityKind::Concept,
195 type_name: "objective",
196 aliases: &["loss"],
197 },
198 EntityTypeDef {
203 kind: EntityKind::Concept,
204 type_name: "module",
205 aliases: &["mod", "namespace"],
206 },
207 EntityTypeDef {
208 kind: EntityKind::Concept,
209 type_name: "function",
210 aliases: &["fn", "func", "method"],
211 },
212 EntityTypeDef {
213 kind: EntityKind::Concept,
214 type_name: "datatype",
215 aliases: &["enum", "record", "type_alias"],
216 },
217 EntityTypeDef {
218 kind: EntityKind::Concept,
219 type_name: "interface",
220 aliases: &["trait", "protocol"],
221 },
222 EntityTypeDef {
225 kind: EntityKind::Dataset,
226 type_name: "benchmark",
227 aliases: &[],
228 },
229 EntityTypeDef {
230 kind: EntityKind::Dataset,
231 type_name: "corpus",
232 aliases: &[],
233 },
234 EntityTypeDef {
235 kind: EntityKind::Dataset,
236 type_name: "training_set",
237 aliases: &["train_set"],
238 },
239 EntityTypeDef {
240 kind: EntityKind::Dataset,
241 type_name: "evaluation_set",
242 aliases: &["eval_set"],
243 },
244 EntityTypeDef {
245 kind: EntityKind::Dataset,
246 type_name: "test_set",
247 aliases: &[],
248 },
249 EntityTypeDef {
250 kind: EntityKind::Dataset,
251 type_name: "synthetic_dataset",
252 aliases: &["synthetic"],
253 },
254 EntityTypeDef {
259 kind: EntityKind::Project,
260 type_name: "library",
261 aliases: &["lib"],
262 },
263 EntityTypeDef {
264 kind: EntityKind::Project,
265 type_name: "framework",
266 aliases: &[],
267 },
268 EntityTypeDef {
269 kind: EntityKind::Project,
270 type_name: "tool",
271 aliases: &[],
272 },
273 EntityTypeDef {
274 kind: EntityKind::Project,
275 type_name: "application",
276 aliases: &["app"],
277 },
278 EntityTypeDef {
279 kind: EntityKind::Project,
280 type_name: "repository",
281 aliases: &["repo"],
282 },
283 EntityTypeDef {
285 kind: EntityKind::Org,
286 type_name: "academic_institution",
287 aliases: &["university", "uni"],
288 },
289 EntityTypeDef {
290 kind: EntityKind::Org,
291 type_name: "company",
292 aliases: &[],
293 },
294 EntityTypeDef {
295 kind: EntityKind::Org,
296 type_name: "research_lab",
297 aliases: &["lab"],
298 },
299 EntityTypeDef {
300 kind: EntityKind::Org,
301 type_name: "nonprofit",
302 aliases: &[],
303 },
304 EntityTypeDef {
305 kind: EntityKind::Org,
306 type_name: "government_agency",
307 aliases: &["gov_agency"],
308 },
309 EntityTypeDef {
310 kind: EntityKind::Org,
311 type_name: "consortium",
312 aliases: &[],
313 },
314 EntityTypeDef {
315 kind: EntityKind::Org,
316 type_name: "standards_body",
317 aliases: &[],
318 },
319 EntityTypeDef {
321 kind: EntityKind::Artifact,
322 type_name: "checkpoint",
323 aliases: &["ckpt"],
324 },
325 EntityTypeDef {
326 kind: EntityKind::Artifact,
327 type_name: "snapshot",
328 aliases: &[],
329 },
330 EntityTypeDef {
331 kind: EntityKind::Artifact,
332 type_name: "export",
333 aliases: &[],
334 },
335 EntityTypeDef {
336 kind: EntityKind::Artifact,
337 type_name: "embedding_index",
338 aliases: &["embed_index"],
339 },
340 EntityTypeDef {
341 kind: EntityKind::Artifact,
342 type_name: "state_bundle",
343 aliases: &[],
344 },
345 EntityTypeDef {
346 kind: EntityKind::Artifact,
347 type_name: "profile",
348 aliases: &[],
349 },
350 EntityTypeDef {
354 kind: EntityKind::Service,
355 type_name: "inference_engine",
356 aliases: &[],
357 },
358 EntityTypeDef {
359 kind: EntityKind::Service,
360 type_name: "retrieval_engine",
361 aliases: &[],
362 },
363 EntityTypeDef {
364 kind: EntityKind::Service,
365 type_name: "embedding_engine",
366 aliases: &[],
367 },
368 EntityTypeDef {
369 kind: EntityKind::Service,
370 type_name: "api",
371 aliases: &["endpoint"],
372 },
373 EntityTypeDef {
374 kind: EntityKind::Service,
375 type_name: "database",
376 aliases: &["db"],
377 },
378 EntityTypeDef {
379 kind: EntityKind::Service,
380 type_name: "search_engine",
381 aliases: &[],
382 },
383 EntityTypeDef {
384 kind: EntityKind::Service,
385 type_name: "mcp_server",
386 aliases: &["mcp"],
387 },
388 EntityTypeDef {
389 kind: EntityKind::Service,
390 type_name: "site",
391 aliases: &["origin"],
392 },
393 ];
395
396#[derive(Clone, Debug, PartialEq, Eq)]
398pub struct ResolvedEntityType {
399 pub kind: EntityKind,
401 pub entity_type: Option<String>,
404}
405
406#[derive(Clone, Debug, PartialEq, Eq)]
411pub enum EntityTypeError {
412 WrongKind {
414 raw_type: String,
415 actual_kind: &'static str,
416 expected_kind: &'static str,
417 valid: String,
418 },
419 UnknownType {
421 raw_type: String,
422 kind: &'static str,
423 valid: String,
424 },
425}
426
427impl fmt::Display for EntityTypeError {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 match self {
430 Self::WrongKind {
431 raw_type,
432 actual_kind,
433 expected_kind,
434 valid,
435 } => write!(
436 f,
437 "entity_type {raw_type:?} belongs to {actual_kind:?}, not {expected_kind:?}; \
438 valid types for {expected_kind:?}: {valid}",
439 ),
440 Self::UnknownType {
441 raw_type,
442 kind,
443 valid,
444 } => write!(
445 f,
446 "unknown entity_type {raw_type:?} for {kind:?}; valid: {valid}"
447 ),
448 }
449 }
450}
451
452#[cfg(feature = "std")]
453impl std::error::Error for EntityTypeError {}
454
455#[derive(Clone)]
457pub struct EntityTypeRegistry {
458 lookup: BTreeMap<String, usize>,
461 defs: Vec<EntityTypeDef>,
462}
463
464impl EntityTypeRegistry {
465 pub fn new(defs: impl IntoIterator<Item = EntityTypeDef>) -> Self {
474 let defs: Vec<EntityTypeDef> = defs.into_iter().collect();
475 let mut lookup: BTreeMap<String, usize> = BTreeMap::new();
476 for (idx, def) in defs.iter().enumerate() {
477 let canonical_type = to_snake_case(def.type_name);
478 let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
479 lookup.insert(canonical_key, idx);
480 lookup.entry(canonical_type).or_insert(idx);
482 for alias in def.aliases {
483 let normalised_alias = to_snake_case(alias);
484 let kind_alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
485 lookup.insert(kind_alias_key, idx);
486 lookup.entry(normalised_alias).or_insert(idx);
487 }
488 }
489 Self { lookup, defs }
490 }
491
492 pub fn builtin() -> Self {
494 Self::new(BUILTIN_DEFS.iter().cloned())
495 }
496
497 pub fn with_extra(extra: impl IntoIterator<Item = EntityTypeDef>) -> Self {
500 let defs: Vec<EntityTypeDef> = BUILTIN_DEFS.iter().cloned().chain(extra).collect();
501 Self::new(defs)
502 }
503
504 pub fn definitions(&self) -> &[EntityTypeDef] {
506 &self.defs
507 }
508
509 pub fn check_extra_collisions<'a>(
534 extras: impl IntoIterator<Item = (&'a str, &'a EntityTypeDef)>,
535 ) -> Result<(), String> {
536 let mut all: Vec<(&'a str, &'a EntityTypeDef)> =
537 BUILTIN_DEFS.iter().map(|def| ("builtin", def)).collect();
538 all.extend(extras);
539
540 let mut seen: BTreeMap<String, (&'a str, String)> = BTreeMap::new();
541 for (owner, def) in all {
542 let canonical_type = to_snake_case(def.type_name);
543 let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
544 if let Some((first_owner, first_type)) = seen.get(&canonical_key) {
545 if *first_owner != owner {
546 return Err(format!(
547 "duplicate entity_type {canonical_key:?}: claimed by both \
548 {first_owner:?} and {owner:?}"
549 ));
550 }
551 if *first_type != canonical_type {
552 return Err(format!(
553 "entity_type alias {canonical_key:?}: claimed by both {first_owner:?} \
554 (canonical {first_type:?}) and {owner:?} (canonical {canonical_type:?})"
555 ));
556 }
557 } else {
558 seen.insert(canonical_key, (owner, canonical_type.clone()));
559 }
560
561 for alias in def.aliases {
562 let normalised_alias = to_snake_case(alias);
563 let alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
564 if let Some((first_owner, first_type)) = seen.get(&alias_key) {
565 if *first_owner != owner || *first_type != canonical_type {
566 return Err(format!(
567 "entity_type alias {alias_key:?}: claimed by both {first_owner:?} \
568 (canonical {first_type:?}) and {owner:?} (canonical {canonical_type:?})",
569 ));
570 }
571 } else {
572 seen.insert(alias_key, (owner, canonical_type.clone()));
573 }
574 }
575 }
576 Ok(())
577 }
578
579 pub fn register(&mut self, def: EntityTypeDef) {
584 let idx = self.defs.len();
585 let canonical_type = to_snake_case(def.type_name);
586 let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
587 self.lookup.insert(canonical_key, idx);
588 self.lookup.entry(canonical_type).or_insert(idx);
589 for alias in def.aliases {
590 let normalised_alias = to_snake_case(alias);
591 let kind_alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
592 self.lookup.insert(kind_alias_key, idx);
593 self.lookup.entry(normalised_alias).or_insert(idx);
594 }
595 self.defs.push(def);
596 }
597
598 pub fn resolve(
600 &self,
601 kind: EntityKind,
602 entity_type: Option<&str>,
603 ) -> Result<ResolvedEntityType, EntityTypeError> {
604 let Some(raw_type) = entity_type else {
605 return Ok(ResolvedEntityType {
606 kind,
607 entity_type: None,
608 });
609 };
610
611 let normalised = to_snake_case(raw_type.trim());
612
613 let kind_key = format!("{}:{}", kind.name(), normalised);
615 if let Some(&idx) = self.lookup.get(&kind_key) {
616 let def = &self.defs[idx];
617 return Ok(ResolvedEntityType {
618 kind,
619 entity_type: Some(def.type_name.to_string()),
620 });
621 }
622
623 if let Some(&idx) = self.lookup.get(&normalised) {
625 let def = &self.defs[idx];
626 if def.kind == kind {
627 return Ok(ResolvedEntityType {
628 kind,
629 entity_type: Some(def.type_name.to_string()),
630 });
631 }
632 return Err(EntityTypeError::WrongKind {
634 raw_type: raw_type.to_string(),
635 actual_kind: def.kind.name(),
636 expected_kind: kind.name(),
637 valid: self.valid_types_for(kind),
638 });
639 }
640
641 Err(EntityTypeError::UnknownType {
643 raw_type: raw_type.to_string(),
644 kind: kind.name(),
645 valid: self.valid_types_for(kind),
646 })
647 }
648
649 pub fn valid_types_for(&self, kind: EntityKind) -> String {
651 let mut names: Vec<&str> = self
652 .defs
653 .iter()
654 .filter(|d| d.kind == kind)
655 .map(|d| d.type_name)
656 .collect();
657 names.sort_unstable();
658 if names.is_empty() {
659 "(none registered)".to_string()
660 } else {
661 names.join(" | ")
662 }
663 }
664}
665
666#[cfg(feature = "std")]
669static GLOBAL_REGISTRY: std::sync::OnceLock<EntityTypeRegistry> = std::sync::OnceLock::new();
670
671#[cfg(feature = "std")]
672impl EntityTypeRegistry {
673 pub fn global() -> &'static EntityTypeRegistry {
675 GLOBAL_REGISTRY.get_or_init(EntityTypeRegistry::builtin)
676 }
677}
678
679#[cfg(test)]
682#[path = "entity_type_tests.rs"]
683mod tests;