1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::OnceLock;
3
4use crate::grammar::GRAMMAR_CONCEPTS;
5use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
6use crate::lino_serialization::LinoSerializationError;
7use serde_json::Value;
8
9const EXTERNAL_ID_VOCABULARY_PREFIX: &str = "external-id:";
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub struct ConceptOntologyImportReport {
14 concepts: usize,
15 alias_links: usize,
16 syntax_mappings: usize,
17}
18
19impl ConceptOntologyImportReport {
20 const fn new(concepts: usize, alias_links: usize, syntax_mappings: usize) -> Self {
21 Self {
22 concepts,
23 alias_links,
24 syntax_mappings,
25 }
26 }
27
28 #[must_use]
30 pub const fn concepts(self) -> usize {
31 self.concepts
32 }
33
34 #[must_use]
36 pub const fn alias_links(self) -> usize {
37 self.alias_links
38 }
39
40 #[must_use]
42 pub const fn syntax_mappings(self) -> usize {
43 self.syntax_mappings
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct ConceptOntologySeedReport {
50 lexicon_concepts: usize,
51 structural_concepts: usize,
52 grammar_concepts: usize,
53 formatting_concepts: usize,
54 alias_links: usize,
55 syntax_mappings: usize,
56}
57
58impl ConceptOntologySeedReport {
59 const fn new(
60 lexicon_concepts: usize,
61 structural_concepts: usize,
62 grammar_concepts: usize,
63 formatting_concepts: usize,
64 alias_links: usize,
65 syntax_mappings: usize,
66 ) -> Self {
67 Self {
68 lexicon_concepts,
69 structural_concepts,
70 grammar_concepts,
71 formatting_concepts,
72 alias_links,
73 syntax_mappings,
74 }
75 }
76
77 #[must_use]
79 pub const fn lexicon_concepts(self) -> usize {
80 self.lexicon_concepts
81 }
82
83 #[must_use]
85 pub const fn structural_concepts(self) -> usize {
86 self.structural_concepts
87 }
88
89 #[must_use]
91 pub const fn grammar_concepts(self) -> usize {
92 self.grammar_concepts
93 }
94
95 #[must_use]
97 pub const fn formatting_concepts(self) -> usize {
98 self.formatting_concepts
99 }
100
101 #[must_use]
103 pub const fn alias_links(self) -> usize {
104 self.alias_links
105 }
106
107 #[must_use]
109 pub const fn syntax_mappings(self) -> usize {
110 self.syntax_mappings
111 }
112}
113
114struct SemanticLexicon {
115 concept_count: usize,
116 concepts: Vec<SemanticLexiconConcept>,
117}
118
119struct SemanticLexiconConcept {
120 id: String,
121 entity_id: Option<String>,
122 url: Option<String>,
123 description: Option<String>,
124 labels: BTreeMap<String, Vec<String>>,
125 primary: BTreeMap<String, String>,
126}
127
128impl SemanticLexiconConcept {
129 fn id(&self) -> &str {
130 &self.id
131 }
132
133 fn definition(&self) -> String {
134 let mut details = Vec::new();
135 if let Some(entity_id) = &self.entity_id {
136 if is_wikidata_qid(entity_id) {
137 details.push(format!("Wikidata {entity_id}"));
138 } else {
139 details.push(format!("entity {entity_id}"));
140 }
141 } else {
142 details.push(format!("concept {}", self.id));
143 }
144
145 if let Some(description) = &self.description {
146 details.push(description.clone());
147 }
148 if let Some(url) = &self.url {
149 details.push(url.clone());
150 }
151
152 details.join("; ")
153 }
154
155 fn syntax_entries(&self) -> Vec<ConceptSyntaxEntry<'_>> {
156 let primary_languages = self
157 .primary
158 .keys()
159 .map(String::as_str)
160 .collect::<BTreeSet<_>>();
161 let mut seen = BTreeSet::new();
162 let mut entries = Vec::new();
163
164 for (language, syntax) in &self.primary {
165 push_syntax_entry(&mut entries, &mut seen, language, syntax, true);
166 }
167
168 for (language, labels) in &self.labels {
169 for (index, label) in labels.iter().enumerate() {
170 let canonical = !primary_languages.contains(language.as_str()) && index == 0;
171 push_syntax_entry(&mut entries, &mut seen, language, label, canonical);
172 }
173 }
174
175 entries
176 }
177}
178
179struct ConceptSyntaxEntry<'a> {
180 language: &'a str,
181 syntax: &'a str,
182 canonical: bool,
183}
184
185struct StructuralConcept {
186 id: &'static str,
187 definition: &'static str,
188 syntax: &'static [(&'static str, &'static str)],
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub struct StatehoodConceptIds {
193 pub proposition: LinkId,
194 pub subject: LinkId,
195 pub object: LinkId,
196}
197
198const STATEHOOD_PROPOSITION_SYNTAX: &[(&str, &str)] = &[
199 ("English", "Hawaii is a state."),
200 ("en", "Hawaii is a state."),
201 ("Russian", "Гавайи это штат."),
202 ("ru", "Гавайи это штат."),
203];
204
205const HAWAII_ENTITY_SYNTAX: &[(&str, &str)] = &[
206 ("English", "Hawaii"),
207 ("en", "Hawaii"),
208 ("Russian", "Гавайи"),
209 ("ru", "Гавайи"),
210];
211
212const UNITED_STATES_STATE_SYNTAX: &[(&str, &str)] = &[
213 ("English", "state"),
214 ("en", "state"),
215 ("Russian", "штат"),
216 ("ru", "штат"),
217];
218
219const STRUCTURAL_CONCEPTS: &[StructuralConcept] = &[
220 StructuralConcept {
221 id: "function",
222 definition: "Reusable computation with parameters and a result boundary.",
223 syntax: &[
224 ("Rust", "fn"),
225 ("Python", "def"),
226 ("JavaScript", "function"),
227 ("C", "function"),
228 ("C++", "function"),
229 ("C#", "method"),
230 ("Java", "method"),
231 ("Visual Basic", "Function"),
232 ("R", "function"),
233 ("sql-ansi", "CREATE FUNCTION"),
234 ("Delphi/Object Pascal", "function"),
235 ],
236 },
237 StructuralConcept {
238 id: "binding",
239 definition: "Association between a name and a value or computation.",
240 syntax: &[
241 ("Rust", "let"),
242 ("Python", "="),
243 ("JavaScript", "let"),
244 ("C", "="),
245 ("C++", "="),
246 ("C#", "="),
247 ("Java", "="),
248 ("Visual Basic", "Dim"),
249 ("R", "<-"),
250 ("sql-ansi", "AS"),
251 ("Delphi/Object Pascal", ":="),
252 ],
253 },
254 StructuralConcept {
255 id: "application",
256 definition: "Application of a callable expression to arguments.",
257 syntax: &[
258 ("Rust", "call(...)"),
259 ("Python", "call(...)"),
260 ("JavaScript", "call(...)"),
261 ("C", "call(...)"),
262 ("C++", "call(...)"),
263 ("C#", "call(...)"),
264 ("Java", "call(...)"),
265 ("Visual Basic", "Call"),
266 ("R", "call(...)"),
267 ("sql-ansi", "CALL"),
268 ("Delphi/Object Pascal", "call(...)"),
269 ],
270 },
271 StructuralConcept {
272 id: "sequence",
273 definition: "Ordered execution or evaluation of multiple operations.",
274 syntax: &[
275 ("Rust", ";"),
276 ("Python", "newline"),
277 ("JavaScript", ";"),
278 ("C", ";"),
279 ("C++", ";"),
280 ("C#", ";"),
281 ("Java", ";"),
282 ("Visual Basic", "newline"),
283 ("R", ";"),
284 ("sql-ansi", ";"),
285 ("Delphi/Object Pascal", "begin ... end"),
286 ],
287 },
288 StructuralConcept {
289 id: "branch",
290 definition: "Conditional selection among alternative operations.",
291 syntax: &[
292 ("Rust", "if"),
293 ("Python", "if"),
294 ("JavaScript", "if"),
295 ("C", "if"),
296 ("C++", "if"),
297 ("C#", "if"),
298 ("Java", "if"),
299 ("Visual Basic", "If"),
300 ("R", "if"),
301 ("sql-ansi", "CASE"),
302 ("Delphi/Object Pascal", "if"),
303 ],
304 },
305 StructuralConcept {
306 id: "loop",
307 definition: "Repeated execution or evaluation over a condition or iterable.",
308 syntax: &[
309 ("Rust", "loop"),
310 ("Python", "for"),
311 ("JavaScript", "for"),
312 ("C", "for"),
313 ("C++", "for"),
314 ("C#", "for"),
315 ("Java", "for"),
316 ("Visual Basic", "For"),
317 ("R", "for"),
318 ("sql-ansi", "WHILE"),
319 ("Delphi/Object Pascal", "for"),
320 ],
321 },
322 StructuralConcept {
323 id: "parameter",
324 definition: "Named input accepted by a function abstraction.",
325 syntax: &[
326 ("Rust", "parameter"),
327 ("Python", "parameter"),
328 ("JavaScript", "parameter"),
329 ("C", "parameter"),
330 ("C++", "parameter"),
331 ("C#", "parameter"),
332 ("Java", "parameter"),
333 ("Visual Basic", "parameter"),
334 ("R", "parameter"),
335 ("sql-ansi", "parameter"),
336 ("Delphi/Object Pascal", "parameter"),
337 ],
338 },
339 StructuralConcept {
340 id: "argument",
341 definition: "Concrete input supplied to a function application.",
342 syntax: &[
343 ("Rust", "argument"),
344 ("Python", "argument"),
345 ("JavaScript", "argument"),
346 ("C", "argument"),
347 ("C++", "argument"),
348 ("C#", "argument"),
349 ("Java", "argument"),
350 ("Visual Basic", "argument"),
351 ("R", "argument"),
352 ("sql-ansi", "argument"),
353 ("Delphi/Object Pascal", "argument"),
354 ],
355 },
356 StructuralConcept {
357 id: "return",
358 definition: "Transfer of a function result to its caller.",
359 syntax: &[
360 ("Rust", "return"),
361 ("Python", "return"),
362 ("JavaScript", "return"),
363 ("C", "return"),
364 ("C++", "return"),
365 ("C#", "return"),
366 ("Java", "return"),
367 ("Visual Basic", "Return"),
368 ("R", "return"),
369 ("sql-ansi", "RETURN"),
370 ("Delphi/Object Pascal", "Result"),
371 ],
372 },
373 StructuralConcept {
374 id: "assignment",
375 definition: "Update that stores a value into a named location.",
376 syntax: &[
377 ("Rust", "="),
378 ("Python", "="),
379 ("JavaScript", "="),
380 ("C", "="),
381 ("C++", "="),
382 ("C#", "="),
383 ("Java", "="),
384 ("Visual Basic", "="),
385 ("R", "<-"),
386 ("sql-ansi", "="),
387 ("Delphi/Object Pascal", ":="),
388 ],
389 },
390];
391
392impl LinkNetwork {
393 pub(crate) fn seed_statehood_worked_example(&mut self) -> StatehoodConceptIds {
394 let proposition = self.insert_typed_point(
395 "statehood",
396 LinkType::Concept,
397 Some("Statehood proposition connecting Hawaii (Q782) to U.S. state (Q35657)."),
398 );
399 let subject = self.insert_typed_point(
400 "Q782",
401 LinkType::Concept,
402 Some("Wikidata Q782; Hawaii; state of the United States."),
403 );
404 let object = self.insert_typed_point(
405 "Q35657",
406 LinkType::Concept,
407 Some("Wikidata Q35657; state of the United States."),
408 );
409
410 for (language, syntax) in STATEHOOD_PROPOSITION_SYNTAX {
411 self.insert_concept_syntax_mapping(proposition, "statehood", language, syntax, true);
412 }
413 for (language, syntax) in HAWAII_ENTITY_SYNTAX {
414 self.insert_concept_syntax_mapping(subject, "Q782", language, syntax, true);
415 }
416 for (language, syntax) in UNITED_STATES_STATE_SYNTAX {
417 self.insert_concept_syntax_mapping(object, "Q35657", language, syntax, true);
418 }
419
420 StatehoodConceptIds {
421 proposition,
422 subject,
423 object,
424 }
425 }
426
427 #[must_use]
433 pub fn seed_common_concept_ontology(&mut self) -> ConceptOntologySeedReport {
434 let lexicon = semantic_lexicon();
435 let mut alias_links = 0;
436 let mut syntax_mappings = 0;
437
438 for concept in &lexicon.concepts {
439 let definition = concept.definition();
440 let concept_link = self.intern_concept(concept.id(), Some(&definition));
441 alias_links += self.insert_external_aliases(concept_link, concept);
442
443 for entry in concept.syntax_entries() {
444 self.insert_concept_syntax_mapping(
445 concept_link,
446 concept.id(),
447 entry.language,
448 entry.syntax,
449 entry.canonical,
450 );
451 syntax_mappings += 1;
452 }
453 }
454
455 let mut structural_concepts = BTreeSet::new();
456 for concept in STRUCTURAL_CONCEPTS {
457 structural_concepts.insert(concept.id);
458 let concept_link = self.intern_concept(concept.id, Some(concept.definition));
459
460 for (language, syntax) in concept.syntax {
461 self.insert_concept_syntax_mapping(
462 concept_link,
463 concept.id,
464 language,
465 syntax,
466 true,
467 );
468 syntax_mappings += 1;
469 }
470 }
471
472 let grammar_concepts = self.seed_grammar_concept_ontology();
473 syntax_mappings += GRAMMAR_CONCEPTS
474 .iter()
475 .map(|concept| concept.syntax.len())
476 .sum::<usize>();
477
478 let formatting = self.seed_document_formatting_concepts();
479 syntax_mappings += formatting.syntax_mappings();
480
481 let statehood = self.seed_statehood_worked_example();
482 for (concept_link, external_id) in
483 [(statehood.subject, "Q782"), (statehood.object, "Q35657")]
484 {
485 let (_alias, inserted) =
486 self.insert_concept_alias_link(concept_link, "Wikidata", external_id);
487 if inserted {
488 alias_links += 1;
489 }
490 }
491 syntax_mappings += STATEHOOD_PROPOSITION_SYNTAX.len()
492 + HAWAII_ENTITY_SYNTAX.len()
493 + UNITED_STATES_STATE_SYNTAX.len();
494
495 ConceptOntologySeedReport::new(
496 lexicon.concept_count,
497 structural_concepts.len(),
498 grammar_concepts,
499 formatting.concepts(),
500 alias_links,
501 syntax_mappings,
502 )
503 }
504
505 pub fn intern_concept(&mut self, exact_id: &str, definition: Option<&str>) -> LinkId {
511 self.insert_typed_point(exact_id, LinkType::Concept, definition)
512 }
513
514 pub fn insert_concept_expression(
519 &mut self,
520 concept: &str,
521 language: &str,
522 expression: &str,
523 ) -> LinkId {
524 let concept_link = self.find_term(concept).unwrap_or_else(|| {
525 self.intern_concept(
526 concept,
527 Some("A language-free concept shared by exact interlingual id."),
528 )
529 });
530 self.insert_concept_syntax_mapping(concept_link, concept, language, expression, true)
531 }
532
533 pub fn insert_concept_mapping(
535 &mut self,
536 concept: &str,
537 language: &str,
538 syntax: &str,
539 ) -> LinkId {
540 self.insert_concept_expression(concept, language, syntax)
541 }
542
543 pub fn insert_concept_alias(
545 &mut self,
546 concept_link: LinkId,
547 vocabulary: &str,
548 external_id: &str,
549 ) -> LinkId {
550 self.insert_concept_alias_link(concept_link, vocabulary, external_id)
551 .0
552 }
553
554 pub fn import_concept_ontology_lino(
560 &mut self,
561 text: &str,
562 ) -> Result<ConceptOntologyImportReport, LinoSerializationError> {
563 let source = Self::from_lino(text)?;
564 Ok(self.import_concept_ontology_network(&source))
565 }
566
567 fn import_concept_ontology_network(&mut self, source: &Self) -> ConceptOntologyImportReport {
568 let mut concept_links: BTreeMap<LinkId, (LinkId, String)> = BTreeMap::new();
569 let mut concepts = 0;
570 let mut alias_links = 0;
571 let mut syntax_mappings = 0;
572
573 for link in source.links() {
574 if link.metadata().link_type() != Some(LinkType::Concept) {
575 continue;
576 }
577 let Some(term) = link.metadata().term() else {
578 continue;
579 };
580 let concept_link = self.intern_concept(term, link.metadata().definition());
581 concept_links.insert(link.id(), (concept_link, term.to_string()));
582 concepts += 1;
583 }
584
585 for link in source.links() {
586 if link.metadata().link_type() != Some(LinkType::Semantic) {
587 continue;
588 }
589 let [source_concept, source_context] = link.references() else {
590 continue;
591 };
592 let Some((target_concept, concept_id)) = concept_links.get(source_concept) else {
593 continue;
594 };
595 let Some(context) = source.link(*source_context) else {
596 continue;
597 };
598 let Some(term) = link.metadata().term() else {
599 continue;
600 };
601
602 match context.metadata().link_type() {
603 Some(LinkType::Language) => {
604 if let Some(language) = link
605 .metadata()
606 .language()
607 .or_else(|| context.metadata().term())
608 {
609 self.insert_concept_syntax_mapping(
610 *target_concept,
611 concept_id,
612 language,
613 term,
614 false,
615 );
616 syntax_mappings += 1;
617 }
618 }
619 Some(LinkType::Type) => {
620 let vocabulary = link.metadata().language().or_else(|| {
621 context
622 .metadata()
623 .term()
624 .and_then(external_vocabulary_from_term)
625 });
626 if let Some(vocabulary) = vocabulary {
627 self.insert_concept_alias(*target_concept, vocabulary, term);
628 alias_links += 1;
629 }
630 }
631 _ => {}
632 }
633 }
634
635 ConceptOntologyImportReport::new(concepts, alias_links, syntax_mappings)
636 }
637
638 fn insert_external_aliases(
639 &mut self,
640 concept_link: LinkId,
641 concept: &SemanticLexiconConcept,
642 ) -> usize {
643 let mut aliases = BTreeSet::new();
644 if let Some(vocabulary) = external_vocabulary_for_id(concept.id()) {
645 aliases.insert((vocabulary, concept.id()));
646 }
647 if let Some(entity_id) = concept.entity_id.as_deref() {
648 if let Some(vocabulary) = external_vocabulary_for_id(entity_id) {
649 aliases.insert((vocabulary, entity_id));
650 }
651 }
652
653 aliases
654 .into_iter()
655 .filter(|(vocabulary, external_id)| {
656 let (_alias, inserted) =
657 self.insert_concept_alias_link(concept_link, vocabulary, external_id);
658 inserted
659 })
660 .count()
661 }
662
663 fn insert_concept_alias_link(
664 &mut self,
665 concept_link: LinkId,
666 vocabulary: &str,
667 external_id: &str,
668 ) -> (LinkId, bool) {
669 let vocabulary_term = external_vocabulary_term(vocabulary);
670 let vocabulary_link = self.insert_typed_point(
671 &vocabulary_term,
672 LinkType::Type,
673 Some("External concept identifier vocabulary."),
674 );
675
676 if let Some(existing) =
677 self.find_concept_alias(concept_link, vocabulary_link, vocabulary, external_id)
678 {
679 return (existing, false);
680 }
681
682 (
683 self.insert_link(
684 [concept_link, vocabulary_link],
685 LinkMetadata::new()
686 .with_link_type(LinkType::Semantic)
687 .with_named(true)
688 .with_term(external_id)
689 .with_language(vocabulary),
690 ),
691 true,
692 )
693 }
694
695 pub(crate) fn insert_concept_syntax_mapping(
696 &mut self,
697 concept_link: LinkId,
698 concept: &str,
699 language: &str,
700 syntax: &str,
701 update_reconstruction: bool,
702 ) -> LinkId {
703 let language_link = self.insert_typed_point(language, LinkType::Language, None);
704 self.cache_concept_syntax(concept, language, syntax, update_reconstruction);
705
706 if let Some(existing) =
707 self.find_concept_syntax_mapping(concept_link, language_link, syntax, language)
708 {
709 return existing;
710 }
711
712 self.insert_link(
713 [concept_link, language_link],
714 LinkMetadata::new()
715 .with_link_type(LinkType::Semantic)
716 .with_named(true)
717 .with_term(syntax)
718 .with_language(language),
719 )
720 }
721
722 fn find_concept_syntax_mapping(
723 &self,
724 concept_link: LinkId,
725 language_link: LinkId,
726 syntax: &str,
727 language: &str,
728 ) -> Option<LinkId> {
729 self.links()
730 .find(|link| {
731 let references = link.references();
732 link.metadata().link_type() == Some(LinkType::Semantic)
733 && references.len() == 2
734 && references[0] == concept_link
735 && references[1] == language_link
736 && link.metadata().term() == Some(syntax)
737 && link.metadata().language() == Some(language)
738 })
739 .map(Link::id)
740 }
741
742 fn find_concept_alias(
743 &self,
744 concept_link: LinkId,
745 vocabulary_link: LinkId,
746 vocabulary: &str,
747 external_id: &str,
748 ) -> Option<LinkId> {
749 self.links()
750 .find(|link| {
751 let references = link.references();
752 link.metadata().link_type() == Some(LinkType::Semantic)
753 && references.len() == 2
754 && references[0] == concept_link
755 && references[1] == vocabulary_link
756 && link.metadata().term() == Some(external_id)
757 && link.metadata().language() == Some(vocabulary)
758 })
759 .map(Link::id)
760 }
761}
762
763const SEMANTIC_LEXICON_JSON: &str = include_str!("data/semantic-lexicon.json");
764
765fn semantic_lexicon() -> &'static SemanticLexicon {
766 static LEXICON: OnceLock<SemanticLexicon> = OnceLock::new();
767 LEXICON.get_or_init(parse_semantic_lexicon)
768}
769
770fn parse_semantic_lexicon() -> SemanticLexicon {
771 let root: Value =
772 serde_json::from_str(SEMANTIC_LEXICON_JSON).expect("semantic lexicon JSON must parse");
773 let root = root
774 .as_object()
775 .expect("semantic lexicon root must be an object");
776 let concepts = root
777 .get("concepts")
778 .and_then(Value::as_array)
779 .expect("semantic lexicon concepts must be an array")
780 .iter()
781 .map(parse_concept)
782 .collect::<Vec<_>>();
783 let concept_count = root
784 .get("conceptCount")
785 .and_then(Value::as_u64)
786 .map_or(concepts.len(), |count| {
787 usize::try_from(count).expect("semantic lexicon concept count must fit usize")
788 });
789
790 assert_eq!(
791 concept_count,
792 concepts.len(),
793 "semantic lexicon conceptCount must match concepts array length"
794 );
795
796 SemanticLexicon {
797 concept_count,
798 concepts,
799 }
800}
801
802fn parse_concept(value: &Value) -> SemanticLexiconConcept {
803 let concept = value
804 .as_object()
805 .expect("semantic lexicon concept must be an object");
806 SemanticLexiconConcept {
807 id: required_string_field(concept, "id"),
808 entity_id: optional_string_field(concept, "entityId"),
809 url: optional_string_field(concept, "url"),
810 description: optional_string_field(concept, "description"),
811 labels: string_list_map_field(concept, "labels"),
812 primary: string_map_field(concept, "primary"),
813 }
814}
815
816fn required_string_field(object: &serde_json::Map<String, Value>, field: &str) -> String {
817 object
818 .get(field)
819 .and_then(Value::as_str)
820 .unwrap_or_else(|| panic!("semantic lexicon field {field} must be a string"))
821 .to_string()
822}
823
824fn optional_string_field(object: &serde_json::Map<String, Value>, field: &str) -> Option<String> {
825 object
826 .get(field)
827 .and_then(Value::as_str)
828 .map(str::to_string)
829}
830
831fn string_map_field(
832 object: &serde_json::Map<String, Value>,
833 field: &str,
834) -> BTreeMap<String, String> {
835 object
836 .get(field)
837 .and_then(Value::as_object)
838 .map(|entries| {
839 entries
840 .iter()
841 .filter_map(|(language, value)| {
842 Some((language.clone(), value.as_str()?.to_string()))
843 })
844 .collect()
845 })
846 .unwrap_or_default()
847}
848
849fn string_list_map_field(
850 object: &serde_json::Map<String, Value>,
851 field: &str,
852) -> BTreeMap<String, Vec<String>> {
853 object
854 .get(field)
855 .and_then(Value::as_object)
856 .map(|entries| {
857 entries
858 .iter()
859 .map(|(language, values)| {
860 (
861 language.clone(),
862 values
863 .as_array()
864 .into_iter()
865 .flatten()
866 .filter_map(Value::as_str)
867 .map(str::to_string)
868 .collect(),
869 )
870 })
871 .collect()
872 })
873 .unwrap_or_default()
874}
875
876fn push_syntax_entry<'a>(
877 entries: &mut Vec<ConceptSyntaxEntry<'a>>,
878 seen: &mut BTreeSet<(&'a str, &'a str)>,
879 language: &'a str,
880 syntax: &'a str,
881 canonical: bool,
882) {
883 if seen.insert((language, syntax)) {
884 entries.push(ConceptSyntaxEntry {
885 language,
886 syntax,
887 canonical,
888 });
889 }
890}
891
892fn is_wikidata_qid(value: &str) -> bool {
893 value.strip_prefix('Q').is_some_and(|suffix| {
894 !suffix.is_empty() && suffix.chars().all(|character| character.is_ascii_digit())
895 })
896}
897
898fn is_wordnet_cili_id(value: &str) -> bool {
899 value.starts_with("ili:") || value.starts_with("ili-")
900}
901
902fn external_vocabulary_for_id(value: &str) -> Option<&'static str> {
903 if is_wikidata_qid(value) {
904 Some("Wikidata")
905 } else if is_wordnet_cili_id(value) {
906 Some("WordNet CILI")
907 } else {
908 None
909 }
910}
911
912fn external_vocabulary_term(vocabulary: &str) -> String {
913 format!("{EXTERNAL_ID_VOCABULARY_PREFIX}{vocabulary}")
914}
915
916fn external_vocabulary_from_term(term: &str) -> Option<&str> {
917 term.strip_prefix(EXTERNAL_ID_VOCABULARY_PREFIX)
918}