@oml/language 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,11 +2,23 @@
2
2
 
3
3
  import { DocumentState, URI, type LangiumDocument } from 'langium';
4
4
  import type { LangiumSharedServices } from 'langium/lsp';
5
- import { isAspect, isConcept, isConceptInstance, isDescription, isOntology, isRelationEntity, isRelationInstance, isScalar } from './generated/ast.js';
5
+ import { isAspect, isConcept, isConceptInstance, isDescription, isOntology, isRelationEntity, isRelationInstance, isScalar, isUnit } from './generated/ast.js';
6
6
  import { collectOntologyMembers, getIriForNode, getSourceLocation, type SourceLocation } from './oml-utils.js';
7
7
 
8
8
  const OML_LABEL_IRI = 'http://opencaesar.io/oml#label';
9
9
 
10
+ function ontologyIriFromMemberIri(memberIri: string): string | undefined {
11
+ const hash = memberIri.lastIndexOf('#');
12
+ if (hash >= 0) {
13
+ return memberIri.slice(0, hash);
14
+ }
15
+ const slash = memberIri.lastIndexOf('/');
16
+ if (slash >= 0) {
17
+ return memberIri.slice(0, slash);
18
+ }
19
+ return undefined;
20
+ }
21
+
10
22
  const ontologyIndexByShared = new WeakMap<object, OmlIndex>();
11
23
 
12
24
  export interface OmlIndexedInstance {
@@ -20,6 +32,28 @@ export interface OmlIndexedInstance {
20
32
  location?: SourceLocation;
21
33
  }
22
34
 
35
+ export interface OmlIndexedUnit {
36
+ unitIri: string;
37
+ symbol: string;
38
+ kindIris: string[];
39
+ ontologyIri: string;
40
+ modelUri: string;
41
+ }
42
+
43
+ export type OmlClassKind = 'aspect' | 'concept' | 'relationEntity';
44
+
45
+ export interface OmlIndexedClass {
46
+ classIri: string;
47
+ kind: OmlClassKind;
48
+ name?: string;
49
+ label?: string;
50
+ prefix?: string;
51
+ ontologyIri: string;
52
+ modelUri: string;
53
+ superIris: string[];
54
+ location?: SourceLocation;
55
+ }
56
+
23
57
  export function getOntologyModelIndex(shared: LangiumSharedServices): OmlIndex {
24
58
  const key = shared as object;
25
59
  const existing = ontologyIndexByShared.get(key);
@@ -34,6 +68,7 @@ export function getOntologyModelIndex(shared: LangiumSharedServices): OmlIndex {
34
68
  export class OmlIndex {
35
69
  private readonly workspaceModelUrisByOntologyIri = new Map<string, Set<string>>();
36
70
  private readonly workspaceNamespaceByModelUri = new Map<string, string>();
71
+ private readonly prefixByOntologyIri = new Map<string, string>();
37
72
  private readonly memberLabelByIri = new Map<string, string>();
38
73
  private readonly labeledMemberIrisByModelUri = new Map<string, Set<string>>();
39
74
  private readonly instanceIrisByTypeIri = new Map<string, Set<string>>();
@@ -42,6 +77,11 @@ export class OmlIndex {
42
77
  private readonly indexedInstanceIrisByModelUri = new Map<string, Set<string>>();
43
78
  private readonly subTypeIrisByTypeIri = new Map<string, Set<string>>();
44
79
  private readonly typeSuperIrisByModelUri = new Map<string, Map<string, Set<string>>>();
80
+ private readonly unitIrisBySymbol = new Map<string, Set<string>>();
81
+ private readonly indexedUnitsByIri = new Map<string, OmlIndexedUnit>();
82
+ private readonly unitIrisByModelUri = new Map<string, Set<string>>();
83
+ private readonly indexedClassesByIri = new Map<string, OmlIndexedClass>();
84
+ private readonly classIrisByModelUri = new Map<string, Set<string>>();
45
85
 
46
86
  constructor(private readonly shared: LangiumSharedServices) {
47
87
  this.shared.workspace.DocumentBuilder.onUpdate((_changed, deleted) => {
@@ -246,6 +286,8 @@ export class OmlIndex {
246
286
  this.removeTypesForModelUri(modelUri);
247
287
  this.removeIndexedInstancesForModelUri(modelUri);
248
288
  this.removeTypeHierarchyForModelUri(modelUri);
289
+ this.removeUnitsForModelUri(modelUri);
290
+ this.removeIndexedClassesForModelUri(modelUri);
249
291
  this.unindexModelUri(modelUri);
250
292
  }
251
293
 
@@ -275,6 +317,23 @@ export class OmlIndex {
275
317
  modelUris.add(modelUri);
276
318
  this.workspaceModelUrisByOntologyIri.set(ontologyIri, modelUris);
277
319
  this.workspaceNamespaceByModelUri.set(modelUri, namespace);
320
+ const prefix = this.getPrefix(document);
321
+ if (prefix) {
322
+ this.prefixByOntologyIri.set(ontologyIri, prefix);
323
+ }
324
+ }
325
+
326
+ getPrefixForOntologyIri(ontologyIri: string): string | undefined {
327
+ this.ensureIndexedFromLoadedDocuments();
328
+ return this.prefixByOntologyIri.get(ontologyIri);
329
+ }
330
+
331
+ getPrefixForMemberIri(memberIri: string): string | undefined {
332
+ const normalized = this.normalizeIri(memberIri);
333
+ if (!normalized) return undefined;
334
+ const ontologyIri = ontologyIriFromMemberIri(normalized);
335
+ if (!ontologyIri) return undefined;
336
+ return this.getPrefixForOntologyIri(ontologyIri);
278
337
  }
279
338
 
280
339
  private indexLinkedDocument(document: LangiumDocument): void {
@@ -283,12 +342,16 @@ export class OmlIndex {
283
342
  this.removeTypesForModelUri(modelUri);
284
343
  this.removeIndexedInstancesForModelUri(modelUri);
285
344
  this.removeTypeHierarchyForModelUri(modelUri);
345
+ this.removeUnitsForModelUri(modelUri);
346
+ this.removeIndexedClassesForModelUri(modelUri);
286
347
  const root = document.parseResult?.value;
287
348
  if (!isOntology(root)) {
288
349
  return;
289
350
  }
290
351
 
291
352
  this.indexTypeHierarchy(root as any, modelUri);
353
+ this.indexUnits(root as any, modelUri);
354
+ this.indexClasses(root as any, modelUri);
292
355
 
293
356
  if (!isDescription(root)) {
294
357
  return;
@@ -354,6 +417,141 @@ export class OmlIndex {
354
417
  }
355
418
  }
356
419
 
420
+ getUnitsBySymbol(symbol: string): OmlIndexedUnit[] {
421
+ this.ensureIndexedFromLoadedDocuments();
422
+ const iris = this.unitIrisBySymbol.get(symbol);
423
+ if (!iris) return [];
424
+ const results: OmlIndexedUnit[] = [];
425
+ for (const iri of iris) {
426
+ const entry = this.indexedUnitsByIri.get(iri);
427
+ if (entry) results.push(entry);
428
+ }
429
+ return results;
430
+ }
431
+
432
+ getAllUnits(): OmlIndexedUnit[] {
433
+ this.ensureIndexedFromLoadedDocuments();
434
+ return [...this.indexedUnitsByIri.values()];
435
+ }
436
+
437
+ private indexUnits(root: any, modelUri: string): void {
438
+ const ontologyIri = this.resolveOntologyIri(modelUri) ?? '';
439
+ const modelUnitIris = new Set<string>();
440
+ for (const member of collectOntologyMembers(root as any)) {
441
+ if (!isUnit(member)) continue;
442
+ const unitIri = this.normalizeIri(getIriForNode(member) ?? '');
443
+ if (!unitIri) continue;
444
+ const symbols = (((member as any).symbol ?? []) as unknown[]).filter((s): s is string => typeof s === 'string' && s.length > 0);
445
+ if (symbols.length === 0) continue;
446
+ const kindIris = (((member as any).kinds ?? []) as any[])
447
+ .map((kindRef) => this.normalizeIri(getIriForNode(kindRef?.ref) ?? ''))
448
+ .filter((iri): iri is string => Boolean(iri));
449
+ for (const symbol of symbols) {
450
+ const entry: OmlIndexedUnit = { unitIri, symbol, kindIris, ontologyIri, modelUri };
451
+ this.indexedUnitsByIri.set(unitIri, entry);
452
+ const byIri = this.unitIrisBySymbol.get(symbol) ?? new Set<string>();
453
+ byIri.add(unitIri);
454
+ this.unitIrisBySymbol.set(symbol, byIri);
455
+ modelUnitIris.add(unitIri);
456
+ }
457
+ }
458
+ if (modelUnitIris.size > 0) {
459
+ this.unitIrisByModelUri.set(modelUri, modelUnitIris);
460
+ }
461
+ }
462
+
463
+ getAllClasses(): OmlIndexedClass[] {
464
+ this.ensureIndexedFromLoadedDocuments();
465
+ return [...this.indexedClassesByIri.values()];
466
+ }
467
+
468
+ private indexClasses(root: any, modelUri: string): void {
469
+ const ontologyIri = this.resolveOntologyIri(modelUri) ?? '';
470
+ const rootPrefix = typeof root?.prefix === 'string' && root.prefix.length > 0 ? root.prefix : undefined;
471
+ if (ontologyIri && rootPrefix) {
472
+ this.prefixByOntologyIri.set(ontologyIri, rootPrefix);
473
+ }
474
+ const modelClassIris = new Set<string>();
475
+ for (const member of collectOntologyMembers(root as any)) {
476
+ let kind: OmlClassKind | undefined;
477
+ if (isAspect(member)) {
478
+ kind = 'aspect';
479
+ } else if (isConcept(member)) {
480
+ kind = 'concept';
481
+ } else if (isRelationEntity(member)) {
482
+ kind = 'relationEntity';
483
+ }
484
+ if (!kind) {
485
+ continue;
486
+ }
487
+ const classIri = this.normalizeIri(getIriForNode(member) ?? '');
488
+ if (!classIri) {
489
+ continue;
490
+ }
491
+ const label = this.extractLabel(member);
492
+ if (label !== undefined) {
493
+ this.memberLabelByIri.set(classIri, label);
494
+ const labeled = this.labeledMemberIrisByModelUri.get(modelUri) ?? new Set<string>();
495
+ labeled.add(classIri);
496
+ this.labeledMemberIrisByModelUri.set(modelUri, labeled);
497
+ }
498
+ const specializations = Array.isArray((member as any).ownedSpecializations) ? (member as any).ownedSpecializations : [];
499
+ const superIris: string[] = [];
500
+ for (const specialization of specializations) {
501
+ const superIri = this.normalizeIri(getIriForNode(specialization?.superTerm?.ref) ?? '');
502
+ if (superIri) {
503
+ superIris.push(superIri);
504
+ }
505
+ }
506
+ const entry: OmlIndexedClass = {
507
+ classIri,
508
+ kind,
509
+ name: typeof (member as any).name === 'string' ? (member as any).name : undefined,
510
+ label,
511
+ prefix: rootPrefix ?? this.prefixByOntologyIri.get(ontologyIri),
512
+ ontologyIri,
513
+ modelUri,
514
+ superIris,
515
+ location: getSourceLocation(member as any),
516
+ };
517
+ this.indexedClassesByIri.set(classIri, entry);
518
+ modelClassIris.add(classIri);
519
+ }
520
+ if (modelClassIris.size > 0) {
521
+ this.classIrisByModelUri.set(modelUri, modelClassIris);
522
+ }
523
+ }
524
+
525
+ private removeIndexedClassesForModelUri(modelUri: string): void {
526
+ const classIris = this.classIrisByModelUri.get(modelUri);
527
+ if (!classIris) {
528
+ return;
529
+ }
530
+ for (const classIri of classIris) {
531
+ this.indexedClassesByIri.delete(classIri);
532
+ }
533
+ this.classIrisByModelUri.delete(modelUri);
534
+ }
535
+
536
+ private removeUnitsForModelUri(modelUri: string): void {
537
+ const unitIris = this.unitIrisByModelUri.get(modelUri);
538
+ if (!unitIris) return;
539
+ for (const unitIri of unitIris) {
540
+ const entry = this.indexedUnitsByIri.get(unitIri);
541
+ if (entry) {
542
+ const bySymbol = this.unitIrisBySymbol.get(entry.symbol);
543
+ if (bySymbol) {
544
+ bySymbol.delete(unitIri);
545
+ if (bySymbol.size === 0) {
546
+ this.unitIrisBySymbol.delete(entry.symbol);
547
+ }
548
+ }
549
+ }
550
+ this.indexedUnitsByIri.delete(unitIri);
551
+ }
552
+ this.unitIrisByModelUri.delete(modelUri);
553
+ }
554
+
357
555
  private indexTypeHierarchy(root: any, modelUri: string): void {
358
556
  const typeSuperIrisByTypeIri = new Map<string, Set<string>>();
359
557
  for (const member of collectOntologyMembers(root as any)) {
@@ -506,6 +704,15 @@ export class OmlIndex {
506
704
  return namespace;
507
705
  }
508
706
 
707
+ private getPrefix(document: LangiumDocument): string | undefined {
708
+ const root = document.parseResult?.value;
709
+ if (!isOntology(root)) {
710
+ return undefined;
711
+ }
712
+ const prefix = (root as any).prefix;
713
+ return typeof prefix === 'string' && prefix.length > 0 ? prefix : undefined;
714
+ }
715
+
509
716
  private ontologyIriFromNamespace(namespace: string): string {
510
717
  return namespace.replace(/[\/#]+$/, '');
511
718
  }
@@ -845,29 +845,35 @@ const serializeLiteral = (literal: Literal): string => {
845
845
  return literal.value ? 'true' : 'false';
846
846
  }
847
847
  if (isIntegerLiteral(literal)) {
848
- return `${literal.value}`;
848
+ return appendUnitSuffix(`${literal.value}`, literal);
849
849
  }
850
850
  if (isDecimalLiteral(literal)) {
851
851
  const v = literal.value as unknown;
852
852
  // Langium parses decimal literals to JS numbers; integer-valued ones (e.g. 567.0→567)
853
853
  // must be written with a decimal point to remain valid DECIMAL syntax.
854
- if (typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v)) {
855
- return `${v}.0`;
856
- }
857
- return `${literal.value}`;
854
+ const base = (typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v))
855
+ ? `${v}.0`
856
+ : `${literal.value}`;
857
+ return appendUnitSuffix(base, literal);
858
858
  }
859
859
  if (isDoubleLiteral(literal)) {
860
860
  const v = literal.value as unknown;
861
861
  // Langium parses double literals to JS numbers, losing the required e-notation.
862
862
  // OML DOUBLE terminal requires e/E; always emit it.
863
- if (typeof v === 'number' && Number.isFinite(v)) {
864
- return Number.isInteger(v) ? `${v}.0e0` : `${v}e0`;
865
- }
866
- return `${literal.value}`;
863
+ const base = (typeof v === 'number' && Number.isFinite(v))
864
+ ? (Number.isInteger(v) ? `${v}.0e0` : `${v}e0`)
865
+ : `${literal.value}`;
866
+ return appendUnitSuffix(base, literal);
867
867
  }
868
868
  return '';
869
869
  };
870
870
 
871
+ const appendUnitSuffix = (base: string, literal: any): string => {
872
+ const unitRef = literal?.unit;
873
+ const refText = unitRef?.$refText ?? unitRef?.ref?.name;
874
+ return refText ? `${base}^^${refText}` : base;
875
+ };
876
+
871
877
  const serializeQuotedLiteral = (literal: QuotedLiteral): string => {
872
878
  const base = serializeStringLiteral(literal.value ?? '');
873
879
  if (literal.type?.$refText) {
@@ -1215,17 +1215,22 @@ async function ensureReferenceImport(shared: any, ontology: any, iri: string): P
1215
1215
  const importedNamespace = normalizeNamespace(String(resolveImportNamespaceRaw(imp) ?? ''));
1216
1216
  return importedNamespace === target.namespace;
1217
1217
  });
1218
+ const importedOntology = await findOntologyByNamespace(shared, target.namespace);
1219
+ const declaredPrefix = typeof importedOntology?.prefix === 'string' ? importedOntology.prefix.trim() : '';
1220
+ const preferredPrefix = declaredPrefix && !usedPrefixes.has(declaredPrefix)
1221
+ ? declaredPrefix
1222
+ : pickImportPrefix(target.namespace, usedPrefixes);
1218
1223
  if (existingImport) {
1219
1224
  // If the existing import has no prefix (e.g., a bundle `includes` added without one),
1220
1225
  // patch it now so that the reference can use the abbreviated form.
1221
1226
  if (!resolveImportPrefix(existingImport)) {
1222
- existingImport.prefix = pickImportPrefix(target.namespace, usedPrefixes);
1227
+ existingImport.prefix = preferredPrefix;
1223
1228
  }
1224
1229
  return;
1225
1230
  }
1226
- const prefix = pickImportPrefix(target.namespace, usedPrefixes);
1231
+ const prefix = preferredPrefix;
1227
1232
  const importedRef = `<${target.namespace}${target.separator}>`;
1228
- const importKind = await resolveImportKind(shared, ontology, target.namespace);
1233
+ const importKind = await resolveImportKind(shared, ontology, target.namespace, importedOntology);
1229
1234
  const importStatement: any = {
1230
1235
  $type: 'Import',
1231
1236
  kind: importKind,
@@ -1418,6 +1423,21 @@ function matchesRefTextFragment(statement: any, fragment: string): boolean {
1418
1423
  }
1419
1424
 
1420
1425
  function asLiteral(ontology: any, value: unknown): any {
1426
+ if (isQuantityLiteralTransport(value)) {
1427
+ const raw = value.value;
1428
+ const num = typeof raw === 'number' ? raw : Number(raw);
1429
+ const $type = !Number.isFinite(num)
1430
+ ? 'DecimalLiteral'
1431
+ : Number.isInteger(num)
1432
+ ? 'IntegerLiteral'
1433
+ : (typeof raw === 'string' && /[eE]/.test(raw) ? 'DoubleLiteral' : 'DecimalLiteral');
1434
+ const literal: Record<string, unknown> = { $type, value: num };
1435
+ const unitIri = literalUnitIriFromTransport(ontology, value);
1436
+ if (unitIri) {
1437
+ literal.unit = { $refText: toRefText(ontology, unitIri) };
1438
+ }
1439
+ return literal;
1440
+ }
1421
1441
  if (isTypedQuotedLiteralTransport(value)) {
1422
1442
  const literalValue = String(value.value ?? '');
1423
1443
  const langTag = literalLanguageTagFromTransport(value);
@@ -1453,14 +1473,18 @@ function asLiteral(ontology: any, value: unknown): any {
1453
1473
  }
1454
1474
 
1455
1475
  async function ensureTypedLiteralDatatypeImport(shared: any, ontology: any, value: unknown): Promise<void> {
1456
- if (!isTypedQuotedLiteralTransport(value)) {
1457
- return;
1476
+ if (isTypedQuotedLiteralTransport(value)) {
1477
+ const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1478
+ if (datatypeIri && !isXsdStringDatatypeIri(ontology, datatypeIri)) {
1479
+ await ensureReferenceImport(shared, ontology, datatypeIri);
1480
+ }
1458
1481
  }
1459
- const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1460
- if (!datatypeIri || isXsdStringDatatypeIri(ontology, datatypeIri)) {
1461
- return;
1482
+ if (isQuantityLiteralTransport(value)) {
1483
+ const unitIri = literalUnitIriFromTransport(ontology, value);
1484
+ if (unitIri) {
1485
+ await ensureReferenceImport(shared, ontology, unitIri);
1486
+ }
1462
1487
  }
1463
- await ensureReferenceImport(shared, ontology, datatypeIri);
1464
1488
  }
1465
1489
 
1466
1490
  function isTypedQuotedLiteralTransport(value: unknown): value is {
@@ -1490,6 +1514,43 @@ function isTypedQuotedLiteralTransport(value: unknown): value is {
1490
1514
  ) && 'value' in record;
1491
1515
  }
1492
1516
 
1517
+ function isQuantityLiteralTransport(value: unknown): value is {
1518
+ value: number | string;
1519
+ unitIri?: unknown;
1520
+ unitRefText?: unknown;
1521
+ unit?: unknown;
1522
+ $type?: unknown;
1523
+ } {
1524
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1525
+ return false;
1526
+ }
1527
+ const record = value as Record<string, unknown>;
1528
+ const hasUnit = typeof record.unitIri === 'string'
1529
+ || typeof record.unitRefText === 'string'
1530
+ || typeof record.unit === 'string';
1531
+ if (!hasUnit) return false;
1532
+ const v = record.value;
1533
+ return typeof v === 'number' || (typeof v === 'string' && v.length > 0);
1534
+ }
1535
+
1536
+ function literalUnitIriFromTransport(ontology: any, value: {
1537
+ unitIri?: unknown;
1538
+ unitRefText?: unknown;
1539
+ unit?: unknown;
1540
+ }): string {
1541
+ const directIri = typeof value.unitIri === 'string'
1542
+ ? value.unitIri.trim()
1543
+ : (typeof value.unit === 'string' && value.unit.includes('://') ? value.unit.trim() : '');
1544
+ if (directIri) {
1545
+ return normalizeIri(directIri);
1546
+ }
1547
+ const refText = typeof value.unitRefText === 'string'
1548
+ ? value.unitRefText.trim()
1549
+ : (typeof value.unit === 'string' ? value.unit.trim() : '');
1550
+ if (!refText) return '';
1551
+ return normalizeIri(expandRefTextToIri(ontology, refText) ?? refText);
1552
+ }
1553
+
1493
1554
  function literalLanguageTag(literal: any): string {
1494
1555
  if (!literal || typeof literal !== 'object') {
1495
1556
  return '';