add readableName to shexJ

main
Laurin Weger 3 weeks ago
parent ed7bbe65ab
commit 2db70df471
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 366
      package-lock.json
  2. 5
      package.json
  3. 10
      packages/cli/src/build.ts
  4. 1
      packages/schema-converter-shex/src/index.ts
  5. 469
      packages/schema-converter-shex/src/typing/ShexJTypingTransformerCompact.ts
  6. 13
      packages/schema-converter-shex/src/typing/shexjToTypingCompact.ts
  7. 133
      packages/schema-converter-shex/src/util/annotateReadablePredicates.ts
  8. 10
      packages/schema-converter-shex/test/testData/propertyCollision.ts

366
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -32,5 +32,8 @@
"ts-jest": "^29.3.0", "ts-jest": "^29.3.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vitest": "^3.1.3" "vitest": "^3.1.3"
},
"dependencies": {
"prettier-eslint": "^16.4.2"
} }
} }

@ -9,6 +9,7 @@ import loading from "loading-cli";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { forAllShapes } from "./util/forAllShapes.js"; import { forAllShapes } from "./util/forAllShapes.js";
import { annotateReadablePredicates } from "@ldo/schema-converter-shex";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@ -45,6 +46,9 @@ export async function build(options: BuildOptions) {
console.error(`Error processing ${fileName}: ${errMessage}`); console.error(`Error processing ${fileName}: ${errMessage}`);
return; return;
} }
// Pre-annotate schema with readablePredicate to unify naming across outputs
annotateReadablePredicates(schema);
// Convert the content to types // Convert the content to types
const format = options.format || "ldo"; const format = options.format || "ldo";
const [typings, context] = await schemaConverterShex(schema, { format }); const [typings, context] = await schemaConverterShex(schema, { format });
@ -66,13 +70,13 @@ export async function build(options: BuildOptions) {
schema: JSON.stringify(schema, null, 2), schema: JSON.stringify(schema, null, 2),
context: JSON.stringify(context, null, 2), context: JSON.stringify(context, null, 2),
format, format,
} },
); );
await fs.promises.writeFile( await fs.promises.writeFile(
path.join(options.output, `${fileName}.${templateName}.ts`), path.join(options.output, `${fileName}.${templateName}.ts`),
await prettier.format(finalContent, { parser: "typescript" }) await prettier.format(finalContent, { parser: "typescript" }),
); );
}) }),
); );
}); });

@ -1,3 +1,4 @@
import { shexjToTyping } from "./typing/shexjToTyping.js"; import { shexjToTyping } from "./typing/shexjToTyping.js";
export { annotateReadablePredicates } from "./util/annotateReadablePredicates.js";
export default shexjToTyping; export default shexjToTyping;

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ShexJTraverser from "@ldo/traverser-shexj"; import ShexJTraverser from "@ldo/traverser-shexj";
import type { Annotation } from "shexj"; import type { Annotation } from "shexj";
import { nameFromObject } from "../context/JsonLdContextBuilder.js"; import { nameFromObject } from "../context/JsonLdContextBuilder.js";
import type { ShapeInterfaceDeclaration } from "./ShapeInterfaceDeclaration.js"; import type { ShapeInterfaceDeclaration } from "./ShapeInterfaceDeclaration.js";
import { getRdfTypesForTripleConstraint } from "../util/getRdfTypesForTripleConstraint.js";
import * as dom from "dts-dom"; import * as dom from "dts-dom";
// Collected enum alias names (e.g., AuthenticatedAgentId) to emit at end // Collected enum alias names (e.g., AuthenticatedAgentId) to emit at end
@ -44,65 +45,58 @@ function isPrimitiveLike(t: dom.Type): boolean {
return intrinsicKinds.has(kind || ""); return intrinsicKinds.has(kind || "");
} }
// Property name collision resolution using predicate IRI mapping // Small helpers for unions and alias naming
const predicateIriByProp = new WeakMap<dom.PropertyDeclaration, string>(); function isUnionType(t: dom.Type): t is dom.UnionType {
/** return (t as any)?.kind === "union";
* resolveCollisions }
* -----------------
* Purpose: ensure that properties derived from different predicate IRIs but
* sharing the same local ending (final segment after '/', '#' or ':') become
* uniquely named TypeScript properties.
*
* Strategy (simplified):
* 1. Group properties by their current (local) name.
* 2. For any group with more than one property, rename each to
* `${secondLast}_${local}` where `secondLast` is the segment immediately
* before the local segment in the predicate IRI.
* 3. If collisions still remain (i.e. two IRIs share both secondLast and local
* segments), fall back to using a sanitized form of the full IRI (without the
* protocol) as the property name.
*/
function resolveCollisions(props: dom.PropertyDeclaration[]): void {
const groups = new Map<string, dom.PropertyDeclaration[]>();
props.forEach((p) => {
const name = p.name;
if (!groups.has(name)) groups.set(name, []);
groups.get(name)!.push(p);
});
groups.forEach((list) => {
if (list.length < 2) return;
const predicateIris = new Set( function unionOf(types: dom.Type[]): dom.Type {
list.map((p) => predicateIriByProp.get(p)).filter(Boolean), const flat: dom.Type[] = [];
); const collect = (tt: dom.Type) => {
if (predicateIris.size < 2) { if (isUnionType(tt)) tt.members.forEach(collect);
return; else flat.push(tt);
};
types.forEach(collect);
const seen = new Set<string>();
const unique: dom.Type[] = [];
flat.forEach((m) => {
const key =
(m as any).name ||
(m as any).value ||
(m as any).kind + JSON.stringify(m);
if (!seen.has(key)) {
seen.add(key);
unique.push(m);
} }
// First pass rename using second last segment
list.forEach((prop) => {
const iri = predicateIriByProp.get(prop);
if (!iri) return;
const segs = iri.split(/[#:\/]/).filter(Boolean);
if (segs.length < 2) return;
const local = segs.at(-1)!;
const secondLast = segs.at(-2)!;
prop.name = `${secondLast}_${local}`;
});
// Detect any remaining duplicates after first pass
const nameCounts = new Map<string, number>();
list.forEach((p) =>
nameCounts.set(p.name, (nameCounts.get(p.name) || 0) + 1),
);
list.forEach((p) => {
if (nameCounts.get(p.name)! > 1) {
const iri = predicateIriByProp.get(p);
if (iri) p.name = iri;
}
});
}); });
if (unique.length === 0) return dom.type.any as unknown as dom.Type;
if (unique.length === 1) return unique[0];
return dom.create.union(unique);
} }
function setOf(inner: dom.Type): dom.NamedTypeReference {
return {
kind: "name",
name: "Set",
typeArguments: [inner],
} as any;
}
function recordOf(key: dom.Type, value: dom.Type): dom.NamedTypeReference {
return {
kind: "name",
name: "Record",
typeArguments: [key, value],
} as any;
}
// Note: aliasing helpers previously used in earlier versions were removed.
// Property name collision resolution using predicate IRI mapping
const predicateIriByProp = new WeakMap<dom.PropertyDeclaration, string>();
// Note: collisions are handled by annotateReadablePredicates pre-pass.
// Merge duplicate properties without introducing LdSet. If a property appears multiple // Merge duplicate properties without introducing LdSet. If a property appears multiple
// times (e.g., via EXTENDS or grouped expressions) we: // times (e.g., via EXTENDS or grouped expressions) we:
// - union the types (flattening existing unions) // - union the types (flattening existing unions)
@ -112,88 +106,110 @@ function resolveCollisions(props: dom.PropertyDeclaration[]): void {
function dedupeCompactProperties( function dedupeCompactProperties(
props: dom.PropertyDeclaration[], props: dom.PropertyDeclaration[],
): dom.PropertyDeclaration[] { ): dom.PropertyDeclaration[] {
const byName: Record<string, dom.PropertyDeclaration> = {};
const isSetRef = (t: dom.Type): t is dom.NamedTypeReference => const isSetRef = (t: dom.Type): t is dom.NamedTypeReference =>
(t as any).kind === "name" && (t as any).name === "Set"; (t as any).kind === "name" && (t as any).name === "Set";
const getSetInner = (t: dom.Type): dom.Type => const getSetInner = (t: dom.Type): dom.Type =>
isSetRef(t) ? (t as any).typeArguments[0] : t; isSetRef(t) ? (t as any).typeArguments[0] : t;
const toSet = (inner: dom.Type): dom.Type =>
({ kind: "name", name: "Set", typeArguments: [inner] }) as any; // Group by composite key (name + predicate IRI)
const makeUnion = (a: dom.Type, b: dom.Type): dom.Type => { const groups = new Map<string, dom.PropertyDeclaration[]>();
const collect = (t: dom.Type, acc: dom.Type[]) => { for (const p of props) {
if ((t as any).kind === "union") { const pred = predicateIriByProp.get(p) || "";
(t as any).members.forEach((m: dom.Type) => collect(m, acc)); const key = `${p.name}\u0000${pred}`;
} else acc.push(t); if (!groups.has(key)) groups.set(key, []);
}; groups.get(key)!.push(p);
const members: dom.Type[] = []; }
collect(a, members);
collect(b, members); const merged: dom.PropertyDeclaration[] = [];
// de-dup via string emission heuristic for (const [, group] of groups) {
const seen = new Set<string>(); if (group.length === 1) {
const filtered: dom.Type[] = []; merged.push(group[0]);
members.forEach((m) => { continue;
const key =
(m as any).name ||
(m as any).value ||
(m as any).kind + JSON.stringify(m);
if (!seen.has(key)) {
seen.add(key);
filtered.push(m);
}
});
if (filtered.length === 1) return filtered[0];
return dom.create.union(filtered);
};
props.forEach((p) => {
const existing = byName[p.name];
if (!existing) {
byName[p.name] = p;
return;
}
// If predicates differ, keep both (assign numeric suffix to new one)
const predExisting = predicateIriByProp.get(existing);
const predNew = predicateIriByProp.get(p);
if (predExisting && predNew && predExisting !== predNew) {
const base = p.name;
let counter = 2;
while (byName[`${base}${counter}`]) counter++;
const newName = `${base}${counter}`;
const clone = dom.create.property(newName, p.type, p.flags);
clone.jsDocComment = p.jsDocComment;
if (predNew) predicateIriByProp.set(clone, predNew);
byName[newName] = clone;
return;
} }
const existingSet = isSetRef(existing.type); let acc = group[0];
const newSet = isSetRef(p.type); for (let i = 1; i < group.length; i++) {
let mergedType: dom.Type; const next = group[i];
if (existingSet && newSet) { const accSet = isSetRef(acc.type);
mergedType = toSet( const nextSet = isSetRef(next.type);
makeUnion(getSetInner(existing.type), getSetInner(p.type)), let mergedType: dom.Type;
); if (accSet && nextSet) {
} else if (existingSet && !newSet) { mergedType = setOf(
mergedType = toSet(makeUnion(getSetInner(existing.type), p.type)); unionOf([getSetInner(acc.type), getSetInner(next.type)]),
} else if (!existingSet && newSet) { );
mergedType = toSet(makeUnion(existing.type, getSetInner(p.type))); } else if (accSet && !nextSet) {
} else { mergedType = setOf(unionOf([getSetInner(acc.type), next.type]));
mergedType = makeUnion(existing.type, p.type); } else if (!accSet && nextSet) {
mergedType = setOf(unionOf([acc.type, getSetInner(next.type)]));
} else {
mergedType = unionOf([acc.type, next.type]);
}
const optional =
acc.flags === dom.DeclarationFlags.Optional ||
next.flags === dom.DeclarationFlags.Optional
? dom.DeclarationFlags.Optional
: dom.DeclarationFlags.None;
const mergedProp = dom.create.property(acc.name, mergedType, optional);
mergedProp.jsDocComment =
acc.jsDocComment && next.jsDocComment
? `${acc.jsDocComment} | ${next.jsDocComment}`
: acc.jsDocComment || next.jsDocComment;
const pred = predicateIriByProp.get(acc) || predicateIriByProp.get(next);
if (pred) predicateIriByProp.set(mergedProp, pred);
acc = mergedProp;
} }
const optional = merged.push(acc);
existing.flags === dom.DeclarationFlags.Optional || }
p.flags === dom.DeclarationFlags.Optional return merged;
? dom.DeclarationFlags.Optional }
: dom.DeclarationFlags.None;
const merged = dom.create.property(p.name, mergedType, optional); // Helpers to add id: IRI to anonymous object(-union) types
merged.jsDocComment = function ensureIdOnMembers(members?: any[]): void {
existing.jsDocComment && p.jsDocComment if (!members) return;
? `${existing.jsDocComment} | ${p.jsDocComment}` const props = (members.filter?.((m: any) => m?.kind === "property") ||
: existing.jsDocComment || p.jsDocComment; []) as dom.PropertyDeclaration[];
// Preserve predicate mapping if (!props.some((m) => m.name === "id")) {
const pred = predicateIriByProp.get(existing) || predicateIriByProp.get(p); members.unshift(
if (pred) predicateIriByProp.set(merged, pred); dom.create.property(
byName[p.name] = merged; "id",
}); dom.create.namedTypeReference("IRI"),
return Object.values(byName); dom.DeclarationFlags.None,
),
);
}
}
function withIdOnAnonymousObject(t: dom.Type): dom.Type {
if ((t as any)?.kind === "object") {
const mems = (t as any).members as dom.PropertyDeclaration[] | undefined;
ensureIdOnMembers(mems as any);
return t;
}
return t;
}
function withIdInUnionObjectMembers(t: dom.Type): dom.Type {
if (!isUnionType(t)) return t;
const members = (t as dom.UnionType).members.map((m) =>
(m as any)?.kind === "object" ? withIdOnAnonymousObject(m) : m,
);
return dom.create.union(members);
}
// Create property and attach predicate IRI and annotations consistently
function createProperty(
name: string,
type: dom.Type,
flags: dom.DeclarationFlags,
predicateIri?: string,
annotations?: Annotation[],
): dom.PropertyDeclaration {
const prop = dom.create.property(name, type, flags);
if (predicateIri) predicateIriByProp.set(prop, predicateIri);
const cmt = commentFromAnnotations(annotations) || "";
prop.jsDocComment = cmt
? `${cmt}\n\nOriginal IRI: ${predicateIri ?? ""}`.trim()
: `Original IRI: ${predicateIri ?? ""}`;
return prop;
} }
export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer< export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
@ -334,17 +350,16 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
if (kind === "property") { if (kind === "property") {
inputProps.push(expr as dom.PropertyDeclaration); inputProps.push(expr as dom.PropertyDeclaration);
} else if (kind === "object" || kind === "interface") { } else if (kind === "object" || kind === "interface") {
(expr as dom.ObjectType | dom.InterfaceDeclaration).members.forEach( const mlist = (expr as dom.ObjectType | dom.InterfaceDeclaration)
(m) => { .members;
if ((m as any).kind === "property") { mlist.forEach((m) => {
inputProps.push(m as dom.PropertyDeclaration); if ((m as any).kind === "property") {
} inputProps.push(m as dom.PropertyDeclaration);
}, }
); });
} }
}); });
const deduped = dedupeCompactProperties(inputProps); const deduped = dedupeCompactProperties(inputProps);
resolveCollisions(deduped);
objectType.members.push(...deduped); objectType.members.push(...deduped);
return objectType; return objectType;
}, },
@ -360,11 +375,10 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
context, context,
) => { ) => {
const transformedChildren = await getTransformedChildren(); const transformedChildren = await getTransformedChildren();
const rdfTypes = getRdfTypesForTripleConstraint(node); const baseName =
const baseName = context.getNameFromIri( ((tripleConstraint as any).readablePredicate as string | undefined) ??
tripleConstraint.predicate, context.getNameFromIri(tripleConstraint.predicate);
rdfTypes[0],
);
const max = tripleConstraint.max; const max = tripleConstraint.max;
const isPlural = max === -1 || (max !== undefined && max !== 1); const isPlural = max === -1 || (max !== undefined && max !== 1);
const isOptional = tripleConstraint.min === 0; const isOptional = tripleConstraint.min === 0;
@ -373,6 +387,48 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
if (transformedChildren.valueExpr) if (transformedChildren.valueExpr)
valueType = transformedChildren.valueExpr as dom.Type; valueType = transformedChildren.valueExpr as dom.Type;
// Generic: If valueExpr is a NodeConstraint with concrete `values`,
// build a union of named alias references derived from those values.
// Works for any predicate (not only rdf:type).
const originalValueExpr: any = (tripleConstraint as any)?.valueExpr;
if (
originalValueExpr &&
typeof originalValueExpr === "object" &&
originalValueExpr.type === "NodeConstraint" &&
Array.isArray(originalValueExpr.values) &&
originalValueExpr.values.length > 0
) {
const aliasRefs: dom.Type[] = [];
for (const v of originalValueExpr.values) {
// valueSetValue can be string IRIREF or ObjectLiteral or other stems; handle IRIREF and ObjectLiteral
if (typeof v === "string") {
// For concrete IRIREF values, use a string literal of the IRI
aliasRefs.push(dom.type.stringLiteral(v));
} else if (v && typeof v === "object") {
// ObjectLiteral has `value`; use that literal as alias base
const literalVal = (v as any).value as string | undefined;
if (literalVal) {
// For explicit literal values, use a string literal type
aliasRefs.push(dom.type.stringLiteral(literalVal));
}
// For other union members (IriStem, ranges, Language, etc.), skip here; fall back covered below if none collected
}
}
if (aliasRefs.length > 0) {
const union = unionOf(aliasRefs);
const final = isPlural ? setOf(union) : union;
return createProperty(
baseName,
final,
isOptional
? dom.DeclarationFlags.Optional
: dom.DeclarationFlags.None,
tripleConstraint.predicate,
tripleConstraint.annotations,
);
}
}
if ( if (
(valueType as dom.InterfaceDeclaration).kind === "interface" && (valueType as dom.InterfaceDeclaration).kind === "interface" &&
!(valueType as dom.InterfaceDeclaration).name !(valueType as dom.InterfaceDeclaration).name
@ -421,119 +477,36 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
) { ) {
const ifaceName = (valueType as dom.InterfaceDeclaration).name; const ifaceName = (valueType as dom.InterfaceDeclaration).name;
// Dictionary of full object instances keyed by IRI // Dictionary of full object instances keyed by IRI
finalType = { finalType = recordOf(
kind: "name", dom.create.namedTypeReference("IRI"),
name: "Record", dom.create.namedTypeReference(ifaceName),
typeArguments: [ );
dom.create.namedTypeReference("IRI"),
dom.create.namedTypeReference(ifaceName),
],
} as dom.Type;
} else { } else {
// Anonymous object or union of anonymous/interface objects // Anonymous object or union of anonymous/interface objects
let valueForRecord: dom.Type = valueType; let valueForRecord: dom.Type = valueType;
if (unionAllObjLike) { if (unionAllObjLike) {
// Ensure each union member has id?: IRI if anonymous object // Ensure each union member has id?: IRI if anonymous object
(valueType as dom.UnionType).members = ( valueForRecord = withIdInUnionObjectMembers(valueType);
valueType as dom.UnionType
).members.map((m) => {
if ((m as dom.InterfaceDeclaration).kind === "interface")
return m;
if ((m as dom.ObjectType).kind === "object") {
const anonMembers = (
m as unknown as { members?: dom.PropertyDeclaration[] }
).members;
const hasId = (anonMembers || []).some(
(mm) => mm.name === "id",
);
if (!hasId && anonMembers) {
anonMembers.unshift(
dom.create.property(
"id",
dom.create.namedTypeReference("IRI"),
dom.DeclarationFlags.None,
),
);
}
}
return m;
});
valueForRecord = valueType; // union retained
} else { } else {
const anon = valueType as dom.ObjectType; valueForRecord = withIdOnAnonymousObject(valueType);
const anonMembers = (
anon as unknown as { members?: dom.PropertyDeclaration[] }
).members;
const hasId = (anonMembers || []).some((m) => m.name === "id");
if (!hasId && anonMembers) {
anonMembers.unshift(
dom.create.property(
"id",
dom.create.namedTypeReference("IRI"),
dom.DeclarationFlags.None,
),
);
}
valueForRecord = anon as dom.Type;
} }
finalType = { finalType = recordOf(
kind: "name", dom.create.namedTypeReference("IRI"),
name: "Record", valueForRecord,
typeArguments: [ );
dom.create.namedTypeReference("IRI"),
valueForRecord,
],
} as dom.Type;
} }
} else { } else {
finalType = { finalType = setOf(valueType);
kind: "name",
name: "Set",
typeArguments: [valueType],
} as dom.Type;
} }
} else { } else {
// Singular // Singular
// If anonymous object or union of object-like types, ensure id: IRI is present (mandatory) // If anonymous object or union of object-like types, ensure id: IRI is present (mandatory)
if (objLike) { if (objLike) {
if ((valueType as dom.ObjectType).kind === "object") { if ((valueType as dom.ObjectType).kind === "object") {
const members = ( valueType = withIdOnAnonymousObject(valueType);
valueType as unknown as {
members?: dom.PropertyDeclaration[];
}
).members;
const hasId = (members || []).some((m) => m.name === "id");
if (!hasId && members) {
members.unshift(
dom.create.property(
"id",
dom.create.namedTypeReference("IRI"),
dom.DeclarationFlags.None,
),
);
}
} }
} else if (isUnion && unionAllObjLike) { } else if (isUnion && unionAllObjLike) {
(valueType as dom.UnionType).members = ( valueType = withIdInUnionObjectMembers(valueType);
valueType as dom.UnionType
).members.map((m) => {
if ((m as dom.ObjectType).kind === "object") {
const mems = (
m as unknown as { members?: dom.PropertyDeclaration[] }
).members;
const hasId = (mems || []).some((mm) => mm.name === "id");
if (!hasId && mems) {
mems.unshift(
dom.create.property(
"id",
dom.create.namedTypeReference("IRI"),
dom.DeclarationFlags.None,
),
);
}
}
return m;
});
} }
// Singular: always the interface/object type itself (never Id union) // Singular: always the interface/object type itself (never Id union)
if ( if (
@ -547,23 +520,13 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
finalType = valueType; finalType = valueType;
} }
} }
return createProperty(
const prop = dom.create.property(
baseName, baseName,
finalType, finalType,
isOptional ? dom.DeclarationFlags.Optional : dom.DeclarationFlags.None, isOptional ? dom.DeclarationFlags.Optional : dom.DeclarationFlags.None,
tripleConstraint.predicate,
tripleConstraint.annotations,
); );
predicateIriByProp.set(prop, tripleConstraint.predicate);
prop.jsDocComment =
commentFromAnnotations(tripleConstraint.annotations) || "";
// Always append original predicate IRI reference (compact format only)
// If an existing comment is present, add a blank line before the Original IRI line.
if (prop.jsDocComment) {
prop.jsDocComment = `${prop.jsDocComment}\n\nOriginal IRI: ${tripleConstraint.predicate}`;
} else {
prop.jsDocComment = `Original IRI: ${tripleConstraint.predicate}`;
}
return prop;
}, },
}, },

@ -8,6 +8,7 @@ import {
} from "./ShexJTypingTransformerCompact.js"; } from "./ShexJTypingTransformerCompact.js";
import * as dom from "dts-dom"; import * as dom from "dts-dom";
import type { TypeingReturn } from "./shexjToTypingLdo.js"; import type { TypeingReturn } from "./shexjToTypingLdo.js";
import { annotateReadablePredicates } from "../util/annotateReadablePredicates.js";
export async function shexjToTypingCompact( export async function shexjToTypingCompact(
shexj: Schema, shexj: Schema,
@ -24,6 +25,9 @@ export async function shexjToTypingCompact(
const nameBuilder = new JsonLdContextBuilder(); const nameBuilder = new JsonLdContextBuilder();
await ShexJNameVisitor.visit(processedShexj, "Schema", nameBuilder); await ShexJNameVisitor.visit(processedShexj, "Schema", nameBuilder);
// Ensure collisions are pre-resolved and stored in readablePredicate on EachOf TCs
annotateReadablePredicates(processedShexj);
additionalCompactEnumAliases.clear(); additionalCompactEnumAliases.clear();
const declarations = await ShexJTypingTransformerCompact.transform( const declarations = await ShexJTypingTransformerCompact.transform(
processedShexj, processedShexj,
@ -34,9 +38,14 @@ export async function shexjToTypingCompact(
); );
// Append only enum aliases (no interface Id aliases in compact format now) // Append only enum aliases (no interface Id aliases in compact format now)
const hasName = (d: unknown): d is { name: string } =>
typeof (d as { name?: unknown }).name === "string";
additionalCompactEnumAliases.forEach((alias) => { additionalCompactEnumAliases.forEach((alias) => {
const exists = declarations.some((d) => (d as any).name === alias); const exists = declarations.some((d) => hasName(d) && d.name === alias);
if (!exists) declarations.push(dom.create.alias(alias, dom.type.string)); if (!exists)
declarations.push(
dom.create.alias(alias, dom.create.namedTypeReference("IRI")),
);
}); });
const typings = declarations.map((declaration) => ({ const typings = declarations.map((declaration) => ({

@ -0,0 +1,133 @@
import type { Schema, ShapeDecl, Shape, EachOf, TripleConstraint } from "shexj";
// Split IRI by colon, slash and hash; drop empties
const splitIriTokens = (iri: string): string[] =>
iri.split(/[:/#]+/).filter(Boolean);
// Keep dots and dashes (so 0.1 stays as 0.1) but sanitize everything else
const sanitize = (s: string) => s.replace(/[^\w.\-]/g, "_");
type TCwReadable = TripleConstraint & { readablePredicate?: string };
/**
* Annotate EachOf-level TripleConstraints with a collision-free readablePredicate.
* Rule: for any group that shares the same local token, rename all members using
* prefix-first `${prefix}_${local}` from left to right; fallback to composite.
*/
export function annotateReadablePredicates(schema: Schema): void {
const shapes = schema.shapes ?? [];
const annotateEachOf = (eachOf: EachOf): void => {
if (
!eachOf ||
eachOf.type !== "EachOf" ||
!Array.isArray(eachOf.expressions)
)
return;
const tcs = (eachOf.expressions as unknown[]).filter(
(e): e is TCwReadable =>
typeof e === "object" &&
e !== null &&
(e as any).type === "TripleConstraint",
);
if (tcs.length > 0) {
// Group by local token (last segment of IRI) and set a base readablePredicate for all
const groups = new Map<string, TCwReadable[]>();
for (const tc of tcs) {
const tokens = splitIriTokens(tc.predicate);
const local = tokens.length ? tokens[tokens.length - 1] : tc.predicate;
// default base name for non-colliders
tc.readablePredicate = local;
const arr = groups.get(local) ?? [];
arr.push(tc);
groups.set(local, arr);
}
// Resolve each group (rename all in collisions)
for (const [, arr] of groups) {
if (arr.length <= 1) continue;
const used = new Set<string>();
const local = splitIriTokens(arr[0].predicate).slice(-1)[0] ?? "";
for (const tc of arr) {
const tokens = splitIriTokens(tc.predicate);
let localIdx = tokens.lastIndexOf(local);
if (localIdx === -1) localIdx = Math.max(tokens.length - 1, 0);
let prefixIdx = localIdx - 1;
let assigned = false;
while (prefixIdx >= 0) {
const cand = `${sanitize(tokens[prefixIdx])}_${sanitize(
tokens[localIdx],
)}`;
if (!used.has(cand)) {
tc.readablePredicate = cand;
used.add(cand);
assigned = true;
break;
}
prefixIdx -= 1;
}
if (!assigned) {
const iriNoProto = tc.predicate.replace(/^[a-z]+:\/\//i, "");
const composite = sanitize(
iriNoProto
.split(/[:/#]+/)
.slice(0, -1)
.join("_") || "iri",
);
let cand = `${composite}_${sanitize(tokens[localIdx] || local)}`;
let n = 1;
while (used.has(cand)) cand = `${cand}_${n++}`;
tc.readablePredicate = cand;
used.add(cand);
}
}
}
// Recurse into nested valueExpr shapes of each TC
for (const tc of tcs) {
const ve: any = (tc as any).valueExpr;
if (ve && typeof ve === "object") {
const t = (ve as any).type;
if (t === "Shape" && (ve as any).expression)
annotateEachOf((ve as any).expression as EachOf);
else if (t === "EachOf") annotateEachOf(ve as EachOf);
else if (t === "ShapeOr" && Array.isArray((ve as any).shapeExprs)) {
for (const sub of (ve as any).shapeExprs) annotateFromExpr(sub);
} else if (
t === "ShapeAnd" &&
Array.isArray((ve as any).shapeExprs)
) {
for (const sub of (ve as any).shapeExprs) annotateFromExpr(sub);
}
}
}
}
// Also recurse into any inline sub-EachOf/Shape expressions found directly in expressions
for (const ex of eachOf.expressions as any[]) {
if (ex && typeof ex === "object") annotateFromExpr(ex);
}
};
const annotateFromExpr = (expr: any): void => {
if (!expr || typeof expr !== "object") return;
const t = (expr as any).type;
if (t === "Shape" && (expr as any).expression)
annotateEachOf((expr as any).expression as EachOf);
else if (t === "EachOf") annotateEachOf(expr as EachOf);
else if (t === "ShapeOr" && Array.isArray((expr as any).shapeExprs)) {
for (const sub of (expr as any).shapeExprs) annotateFromExpr(sub);
} else if (t === "ShapeAnd" && Array.isArray((expr as any).shapeExprs)) {
for (const sub of (expr as any).shapeExprs) annotateFromExpr(sub);
} else if (t === "TripleConstraint") {
const ve = (expr as any).valueExpr;
if (ve && typeof ve === "object") annotateFromExpr(ve);
}
};
for (const s of shapes) {
const sd = s as ShapeDecl;
const shape = (sd.shapeExpr || (sd as any)) as Shape | undefined;
if (shape?.expression) annotateFromExpr(shape as any);
}
}

@ -21,23 +21,23 @@ export interface C {
/** /**
* Original IRI: http://ex/label * Original IRI: http://ex/label
*/ */
label: any; ex_label: any;
/** /**
* Original IRI: http://ex2/label * Original IRI: http://ex2/label
*/ */
label2: any; ex2_label: any;
/** /**
* Original IRI: http://xmlns.com/foaf/0.1/label * Original IRI: http://xmlns.com/foaf/0.1/label
*/ */
label3: any; "0.1_label": any;
/** /**
* Original IRI: http://example.com/v1#label * Original IRI: http://example.com/v1#label
*/ */
label4: any; v1_label: any;
/** /**
* Original IRI: http://api.example.com/v2.1:label * Original IRI: http://api.example.com/v2.1:label
*/ */
"v2.1:label": any; "v2.1_label": any;
} }
`, `,
}; };

Loading…
Cancel
Save