Skip to main content

meta_language/
concept_ontology.rs

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/// Summary returned after importing concept links from an ontology source.
12#[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    /// Number of language-free concepts imported from the source.
29    #[must_use]
30    pub const fn concepts(self) -> usize {
31        self.concepts
32    }
33
34    /// Number of external-id alias links imported from the source.
35    #[must_use]
36    pub const fn alias_links(self) -> usize {
37        self.alias_links
38    }
39
40    /// Number of language-bound expression mappings imported from the source.
41    #[must_use]
42    pub const fn syntax_mappings(self) -> usize {
43        self.syntax_mappings
44    }
45}
46
47/// Summary returned after seeding the shared concept ontology into a network.
48#[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    /// Number of concepts imported from meta-expression's semantic lexicon JSON.
78    #[must_use]
79    pub const fn lexicon_concepts(self) -> usize {
80        self.lexicon_concepts
81    }
82
83    /// Number of built-in structural programming-language concepts seeded.
84    #[must_use]
85    pub const fn structural_concepts(self) -> usize {
86        self.structural_concepts
87    }
88
89    /// Number of built-in grammar-construct concepts seeded.
90    #[must_use]
91    pub const fn grammar_concepts(self) -> usize {
92        self.grammar_concepts
93    }
94
95    /// Number of shared document-formatting concepts seeded.
96    #[must_use]
97    pub const fn formatting_concepts(self) -> usize {
98        self.formatting_concepts
99    }
100
101    /// Number of external-id alias links attached to seeded concepts.
102    #[must_use]
103    pub const fn alias_links(self) -> usize {
104        self.alias_links
105    }
106
107    /// Number of semantic concrete-syntax mapping links surfaced by the seed.
108    #[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    /// Seeds the network with the shared common concept ontology.
428    ///
429    /// The seed combines meta-expression's semantic lexicon with structural
430    /// programming-language concepts that are shared across the current
431    /// language targets.
432    #[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    /// Interns a language-free concept by exact identifier.
506    ///
507    /// The identifier is matched exactly: case changes, diacritic changes, or
508    /// sense suffixes are distinct concept ids and therefore produce distinct
509    /// concept links.
510    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    /// Inserts a language-bound expression linked to a language-free concept.
515    ///
516    /// The concept is reused only when `concept` exactly matches an existing
517    /// concept id; otherwise a new concept link is minted.
518    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    /// Inserts a concept-to-language syntax mapping and returns the semantic link id.
534    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    /// Attaches an external vocabulary id to a concept without changing its exact concept id.
544    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    /// Imports concept, expression, and alias links from canonical `LiNo` text.
555    ///
556    /// The input is the links-notation text produced by [`LinkNetwork::to_lino`].
557    /// Importing the same text repeatedly is idempotent because concepts,
558    /// expressions, and aliases are all deduplicated by exact link shape.
559    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}