You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
ldo-compact-fork/packages/schema-converter-shex/src/typing/ShexJTypingTransformerCompa...

623 lines
23 KiB

import ShexJTraverser from "@ldo/traverser-shexj";
import type { Annotation } from "shexj";
import { nameFromObject } from "../context/JsonLdContextBuilder.js";
import type { ShapeInterfaceDeclaration } from "./ShapeInterfaceDeclaration.js";
import { getRdfTypesForTripleConstraint } from "../util/getRdfTypesForTripleConstraint.js";
import * as dom from "dts-dom";
// Collected enum alias names (e.g., AuthenticatedAgentId) to emit at end
export const additionalCompactEnumAliases = new Set<string>();
export interface CompactTransformerContext {
getNameFromIri: (iri: string, rdfType?: string) => string;
}
function commentFromAnnotations(
annotations?: Annotation[]
): string | undefined {
const commentAnnotationObject = annotations?.find(
(annotation) =>
annotation.predicate === "http://www.w3.org/2000/01/rdf-schema#comment"
)?.object;
if (typeof commentAnnotationObject === "string")
return commentAnnotationObject;
return commentAnnotationObject?.value;
}
// Helper: classify a dom.Type into categories we care about.
function isObjectLike(t: dom.Type): boolean {
return (
(t as dom.ObjectType).kind === "object" ||
(t as dom.InterfaceDeclaration).kind === "interface"
);
}
function isPrimitiveLike(t: dom.Type): boolean {
const kind = (t as any)?.kind;
if (kind === "name") return true; // named references and intrinsic tokens
if (kind === "union") {
return (t as dom.UnionType).members.every(isPrimitiveLike);
}
if (kind === "type-parameter") return true;
// Fallback: treat scalar intrinsic tokens as primitive
const intrinsicKinds = new Set(["string", "number", "boolean", "undefined"]);
return intrinsicKinds.has(kind || "");
}
function normalizeAnonymousInterface(t: dom.Type): dom.Type {
if (
(t as dom.InterfaceDeclaration).kind === "interface" &&
!(t as dom.InterfaceDeclaration).name
) {
return dom.create.objectType(
(t as dom.InterfaceDeclaration).members as dom.PropertyDeclaration[]
);
}
return t;
}
// Property name collision resolution using predicate IRI mapping
const predicateIriByProp = new WeakMap<dom.PropertyDeclaration, string>();
/**
* 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 base = p.name.replace(/\d+$/, "");
if (!groups.has(base)) groups.set(base, []);
groups.get(base)!.push(p);
});
groups.forEach((list) => {
if (list.length < 2) return;
// First pass rename using second last segment
list.forEach((prop) => {
const iri = predicateIriByProp.get(prop) || prop.name;
const segs = iri.split(/[#:\/]/).filter(Boolean);
if (!segs.length) return;
const local = segs.at(-1)!;
const secondLast = segs.length > 1 ? segs.at(-2)! : undefined;
if (secondLast) {
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) {
p.name = predicateIriByProp.get(p) || p.name;
}
});
});
}
// Merge duplicate properties without introducing LdSet. If a property appears multiple
// times (e.g., via EXTENDS or grouped expressions) we:
// - union the types (flattening existing unions)
// - if one side is Set<T> and the other is plain U, produce Set<T|U>
// - if both are Set<A>, Set<B> -> Set<A|B>
// - preserve optional flag if any occurrence optional
function dedupeCompactProperties(
props: dom.PropertyDeclaration[]
): dom.PropertyDeclaration[] {
const byName: Record<string, dom.PropertyDeclaration> = {};
const isSetRef = (t: dom.Type): t is dom.NamedTypeReference =>
(t as any).kind === "name" && (t as any).name === "Set";
const getSetInner = (t: dom.Type): dom.Type =>
isSetRef(t) ? (t as any).typeArguments[0] : t;
const toSet = (inner: dom.Type): dom.Type =>
({ kind: "name", name: "Set", typeArguments: [inner] }) as any;
const makeUnion = (a: dom.Type, b: dom.Type): dom.Type => {
const collect = (t: dom.Type, acc: dom.Type[]) => {
if ((t as any).kind === "union") {
(t as any).members.forEach((m: dom.Type) => collect(m, acc));
} else acc.push(t);
};
const members: dom.Type[] = [];
collect(a, members);
collect(b, members);
// de-dup via string emission heuristic
const seen = new Set<string>();
const filtered: dom.Type[] = [];
members.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);
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);
const newSet = isSetRef(p.type);
let mergedType: dom.Type;
if (existingSet && newSet) {
mergedType = toSet(
makeUnion(getSetInner(existing.type), getSetInner(p.type))
);
} else if (existingSet && !newSet) {
mergedType = toSet(makeUnion(getSetInner(existing.type), p.type));
} else if (!existingSet && newSet) {
mergedType = toSet(makeUnion(existing.type, getSetInner(p.type)));
} else {
mergedType = makeUnion(existing.type, p.type);
}
const optional =
existing.flags === dom.DeclarationFlags.Optional ||
p.flags === dom.DeclarationFlags.Optional
? dom.DeclarationFlags.Optional
: dom.DeclarationFlags.None;
const merged = dom.create.property(p.name, mergedType, optional);
merged.jsDocComment =
existing.jsDocComment && p.jsDocComment
? `${existing.jsDocComment} | ${p.jsDocComment}`
: existing.jsDocComment || p.jsDocComment;
// Preserve predicate mapping
const pred = predicateIriByProp.get(existing) || predicateIriByProp.get(p);
if (pred) predicateIriByProp.set(merged, pred);
byName[p.name] = merged;
});
return Object.values(byName);
}
export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
{
Schema: { return: dom.TopLevelDeclaration[] };
ShapeDecl: { return: dom.InterfaceDeclaration };
Shape: { return: dom.InterfaceDeclaration };
EachOf: { return: dom.ObjectType | dom.InterfaceDeclaration };
TripleConstraint: { return: dom.PropertyDeclaration };
NodeConstraint: { return: dom.Type };
ShapeOr: { return: dom.UnionType };
ShapeAnd: { return: dom.IntersectionType };
ShapeNot: { return: never };
ShapeExternal: { return: never };
},
CompactTransformerContext
>({
// Transformer from Schema to interfaces
Schema: {
transformer: async (_schema, getTransformedChildren) => {
const transformedChildren = await getTransformedChildren();
const interfaces: dom.TopLevelDeclaration[] = [];
transformedChildren.shapes?.forEach((shape) => {
if (
typeof shape !== "string" &&
(shape as dom.InterfaceDeclaration).kind === "interface"
) {
interfaces.push(shape as dom.InterfaceDeclaration);
}
});
return interfaces;
},
},
// Transformer from ShapeDecl to interface
ShapeDecl: {
transformer: async (shapeDecl, getTransformedChildren) => {
const shapeName = nameFromObject(shapeDecl) || "Shape";
const { shapeExpr } = await getTransformedChildren();
if ((shapeExpr as dom.InterfaceDeclaration).kind === "interface") {
const shapeInterface = shapeExpr as ShapeInterfaceDeclaration;
shapeInterface.name = shapeName;
// Preserve shape id for downstream shapeTypes generation
// (mirrors standard transformer behavior)
shapeInterface.shapeId = shapeDecl.id;
if (
!shapeInterface.members.find(
(m) => m.kind === "property" && m.name === "id"
)
) {
shapeInterface.members.unshift(
dom.create.property(
"id",
dom.create.namedTypeReference("IRI"),
dom.DeclarationFlags.Optional
)
);
}
return shapeInterface;
}
throw new Error(
"Unsupported direct shape expression on ShapeDecl for compact format."
);
},
},
// Transformer from Shape to interface
Shape: {
transformer: async (_shape, getTransformedChildren, setReturnPointer) => {
const newInterface: ShapeInterfaceDeclaration = dom.create.interface("");
setReturnPointer(newInterface);
const transformedChildren = await getTransformedChildren();
if (
typeof transformedChildren.expression !== "string" &&
transformedChildren.expression &&
((transformedChildren.expression as dom.ObjectType).kind === "object" ||
(transformedChildren.expression as dom.InterfaceDeclaration).kind ===
"interface")
) {
newInterface.members.push(
...(transformedChildren.expression as dom.ObjectType).members
);
} else if (
(transformedChildren.expression as dom.PropertyDeclaration)?.kind ===
"property"
) {
newInterface.members.push(
transformedChildren.expression as dom.PropertyDeclaration
);
}
if (transformedChildren.extends) {
transformedChildren.extends.forEach((ext) => {
const extInt = ext as dom.InterfaceDeclaration;
if (extInt.kind === "interface") {
const merged = [
...extInt.members.filter(
(m) => !(m.kind === "property" && m.name === "id")
),
...newInterface.members,
].filter(
(m): m is dom.PropertyDeclaration => m.kind === "property"
);
newInterface.members = dedupeCompactProperties(merged);
}
});
}
// Final pass: ensure only a single id property
const idSeen = new Set<number>();
newInterface.members = newInterface.members.filter((m, idx) => {
if (m.kind !== "property" || m.name !== "id") return true;
if (idSeen.size === 0) {
idSeen.add(idx);
// normalize id type to IRI
m.type = dom.create.namedTypeReference("IRI");
m.flags = dom.DeclarationFlags.Optional;
return true;
}
return false;
});
return newInterface;
},
},
// Transformer from EachOf to object type. EachOf contains the `expressions` array of properties (TripleConstraint)
EachOf: {
transformer: async (eachOf, getTransformedChildren, setReturnPointer) => {
const transformedChildren = await getTransformedChildren();
const name = nameFromObject(eachOf);
const objectType = name
? dom.create.interface(name)
: dom.create.objectType([]);
setReturnPointer(objectType);
const inputProps: dom.PropertyDeclaration[] = [];
transformedChildren.expressions.forEach((expr) => {
if (!expr || typeof expr === "string") return;
const kind = (expr as any).kind;
if (kind === "property") {
inputProps.push(expr as dom.PropertyDeclaration);
} else if (kind === "object" || kind === "interface") {
(expr as dom.ObjectType | dom.InterfaceDeclaration).members.forEach(
(m) => {
if ((m as any).kind === "property") {
inputProps.push(m as dom.PropertyDeclaration);
}
}
);
}
});
const deduped = dedupeCompactProperties(inputProps);
resolveCollisions(deduped);
objectType.members.push(...deduped);
return objectType;
},
},
// Transformer from triple constraints to type properties.
TripleConstraint: {
transformer: async (
tripleConstraint,
getTransformedChildren,
_setReturnPointer,
node,
context
) => {
const transformedChildren = await getTransformedChildren();
const rdfTypes = getRdfTypesForTripleConstraint(node);
const baseName = context.getNameFromIri(
tripleConstraint.predicate,
rdfTypes[0]
);
const max = tripleConstraint.max;
const isPlural = max === -1 || (max !== undefined && max !== 1);
const isOptional = tripleConstraint.min === 0;
let valueType: dom.Type = dom.type.any;
if (transformedChildren.valueExpr)
valueType = transformedChildren.valueExpr as dom.Type;
if (
(valueType as dom.InterfaceDeclaration).kind === "interface" &&
!(valueType as dom.InterfaceDeclaration).name
) {
valueType = dom.create.objectType(
(valueType as dom.InterfaceDeclaration)
.members as dom.PropertyDeclaration[]
);
}
// Normalize NodeConstraint returned object forms for IRIs into IRI
// Heuristic: existing transformer (compact) returns string/number/boolean OR object/interface.
// We treat any simple string/number/boolean/name as primitive.
// Determine category
const objLike = isObjectLike(valueType);
const isUnion =
(valueType as unknown as { kind?: string })?.kind === "union";
const unionMembers: dom.Type[] = isUnion
? (valueType as dom.UnionType).members
: [];
const unionAllObjLike =
isUnion && unionMembers.length > 0 && unionMembers.every(isObjectLike);
const primLike = isPrimitiveLike(valueType);
if (
!primLike &&
!objLike &&
(valueType as dom.UnionType).kind === "union"
) {
const u = valueType as dom.UnionType;
const hasObj = u.members.some(isObjectLike);
const hasPrim = u.members.some(isPrimitiveLike);
if (isPlural && hasObj && hasPrim) {
throw new Error(
`Mixed plural union (object + primitive) not supported for predicate ${tripleConstraint.predicate}`
);
}
}
let finalType: dom.Type;
if (isPlural) {
if (objLike || unionAllObjLike) {
if (
(valueType as dom.InterfaceDeclaration).kind === "interface" &&
(valueType as dom.InterfaceDeclaration).name
) {
const ifaceName = (valueType as dom.InterfaceDeclaration).name;
// Dictionary of full object instances keyed by IRI
finalType = {
kind: "name",
name: "Record",
typeArguments: [
dom.create.namedTypeReference("IRI"),
dom.create.namedTypeReference(ifaceName),
],
} as dom.Type;
} else {
// Anonymous object or union of anonymous/interface objects
let valueForRecord: dom.Type = valueType;
if (unionAllObjLike) {
// Ensure each union member has id?: IRI if anonymous object
(valueType as dom.UnionType).members = (
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.Optional
)
);
}
}
return m;
});
valueForRecord = valueType; // union retained
} else {
const anon = valueType as dom.ObjectType;
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.Optional
)
);
}
valueForRecord = anon as dom.Type;
}
finalType = {
kind: "name",
name: "Record",
typeArguments: [
dom.create.namedTypeReference("IRI"),
valueForRecord,
],
} as dom.Type;
}
} else {
finalType = {
kind: "name",
name: "Set",
typeArguments: [valueType],
} as dom.Type;
}
} else {
// Singular: always the interface/object type itself (never Id union)
if (
(valueType as dom.InterfaceDeclaration).kind === "interface" &&
(valueType as dom.InterfaceDeclaration).name
) {
finalType = dom.create.namedTypeReference(
(valueType as dom.InterfaceDeclaration).name
);
} else {
finalType = valueType;
}
}
const prop = dom.create.property(
baseName,
finalType,
isOptional ? dom.DeclarationFlags.Optional : dom.DeclarationFlags.None
);
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;
},
},
// Transformer from node constraint to type
NodeConstraint: {
transformer: async (nodeConstraint) => {
if (nodeConstraint.datatype) {
switch (nodeConstraint.datatype) {
case "http://www.w3.org/2001/XMLSchema#boolean":
return dom.type.boolean;
case "http://www.w3.org/2001/XMLSchema#byte":
case "http://www.w3.org/2001/XMLSchema#decimal":
case "http://www.w3.org/2001/XMLSchema#double":
case "http://www.w3.org/2001/XMLSchema#float":
case "http://www.w3.org/2001/XMLSchema#int":
case "http://www.w3.org/2001/XMLSchema#integer":
case "http://www.w3.org/2001/XMLSchema#long":
case "http://www.w3.org/2001/XMLSchema#negativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonNegativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonPositiveInteger":
case "http://www.w3.org/2001/XMLSchema#positiveInteger":
case "http://www.w3.org/2001/XMLSchema#short":
case "http://www.w3.org/2001/XMLSchema#unsignedLong":
case "http://www.w3.org/2001/XMLSchema#unsignedInt":
case "http://www.w3.org/2001/XMLSchema#unsignedShort":
case "http://www.w3.org/2001/XMLSchema#unsignedByte":
return dom.type.number;
default:
return dom.type.string; // treat most as string
}
}
if (nodeConstraint.nodeKind) {
switch (nodeConstraint.nodeKind) {
case "iri":
return dom.create.namedTypeReference("IRI");
case "bnode":
return dom.type.string; // opaque id as string
case "nonliteral":
return dom.create.namedTypeReference("IRI");
case "literal":
default:
return dom.type.string;
}
}
if (nodeConstraint.values) {
const u = dom.create.union([]);
nodeConstraint.values.forEach((v) => {
if (typeof v === "string") u.members.push(dom.type.stringLiteral(v));
});
if (!u.members.length) return dom.type.string;
if (u.members.length === 1) return u.members[0];
return u;
}
return dom.type.any;
},
},
// Transformer from ShapeOr to union type
ShapeOr: {
transformer: async (_shapeOr, getTransformedChildren) => {
const tc = await getTransformedChildren();
const valid: dom.Type[] = [];
tc.shapeExprs.forEach((t) => {
if (typeof t === "object") valid.push(t);
});
return dom.create.union(valid);
},
},
// Transformer from ShapeAnd to intersection type
ShapeAnd: {
transformer: async (_shapeAnd, getTransformedChildren) => {
const tc = await getTransformedChildren();
const valid: dom.Type[] = [];
tc.shapeExprs.forEach((t) => {
if (typeof t === "object") valid.push(t);
});
return dom.create.intersection(valid);
},
},
// Transformer from ShapeNot to type - not supported.
ShapeNot: {
transformer: async () => {
throw new Error("ShapeNot not supported (compact)");
},
},
// Transformer from ShapeExternal to type - not supported.
ShapeExternal: {
transformer: async () => {
throw new Error("ShapeExternal not supported (compact)");
},
},
});