TODO: The separation of code between ldo and compact type generation is bad. There could be more tests for compact, jsdoc for generated typings could be better. Better have a review over all of this again.main
parent
c461beb5a5
commit
f9e433cfd4
@ -0,0 +1,14 @@ |
||||
import { CompactShapeType } from "@ldo/ldo"; |
||||
import { <%- fileName %>Schema } from "./<%- fileName %>.schema"; |
||||
import { |
||||
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%> |
||||
<%- typing.dts.name %>, |
||||
<% } }); -%>} from "./<%- fileName %>.typings"; |
||||
|
||||
// Compact ShapeTypes for <%- fileName %> |
||||
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%> |
||||
export const <%- typing.dts.name %>ShapeType: CompactShapeType<<%- typing.dts.name %>> = { |
||||
schema: <%- fileName %>Schema, |
||||
shape: "<%- typing.dts.shapeId %>", |
||||
}; |
||||
<% } }); -%> |
@ -0,0 +1,601 @@ |
||||
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 || ""); |
||||
} |
||||
|
||||
// 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; |
||||
|
||||
// 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)"); |
||||
}, |
||||
}, |
||||
}); |
@ -1,59 +1,18 @@ |
||||
import type { ContextDefinition } from "jsonld"; |
||||
import type { Schema } from "shexj"; |
||||
import { JsonLdContextBuilder } from "../context/JsonLdContextBuilder.js"; |
||||
import { ShexJNameVisitor } from "../context/ShexJContextVisitor.js"; |
||||
import { jsonld2graphobject } from "jsonld2graphobject"; |
||||
import { ShexJTypingTransformer } from "./ShexJTypingTransformer.js"; |
||||
import * as dom from "dts-dom"; |
||||
import type { TypeingReturn } from "./shexjToTypingLdo.js"; |
||||
import { shexjToTypingLdo } from "./shexjToTypingLdo.js"; |
||||
import { shexjToTypingCompact } from "./shexjToTypingCompact.js"; |
||||
|
||||
export interface TypeingReturn { |
||||
typingsString: string; |
||||
typings: { |
||||
typingString: string; |
||||
dts: dom.TopLevelDeclaration; |
||||
}[]; |
||||
export interface TypingsOptions { |
||||
format?: "ldo" | "compact"; |
||||
} |
||||
|
||||
export async function shexjToTyping( |
||||
shexj: Schema, |
||||
): Promise<[TypeingReturn, ContextDefinition]> { |
||||
const processedShexj: Schema = (await jsonld2graphobject( |
||||
{ |
||||
...shexj, |
||||
"@id": "SCHEMA", |
||||
"@context": "http://www.w3.org/ns/shex.jsonld", |
||||
}, |
||||
"SCHEMA", |
||||
)) as unknown as Schema; |
||||
const jsonLdContextBuilder = new JsonLdContextBuilder(); |
||||
await ShexJNameVisitor.visit(processedShexj, "Schema", jsonLdContextBuilder); |
||||
|
||||
const declarations = await ShexJTypingTransformer.transform( |
||||
processedShexj, |
||||
"Schema", |
||||
{ |
||||
getNameFromIri: |
||||
jsonLdContextBuilder.getNameFromIri.bind(jsonLdContextBuilder), |
||||
}, |
||||
); |
||||
const typings = declarations.map((declaration) => { |
||||
return { |
||||
typingString: dom |
||||
.emit(declaration, { |
||||
rootFlags: dom.ContextFlags.InAmbientNamespace, |
||||
}) |
||||
.replace(/\r\n/g, "\n"), |
||||
dts: declaration, |
||||
}; |
||||
}); |
||||
const typingsString = |
||||
`import { LdSet, LdoJsonldContext } from "@ldo/ldo"\n\n` + |
||||
typings.map((typing) => `export ${typing.typingString}`).join(""); |
||||
|
||||
const typeingReturn: TypeingReturn = { |
||||
typingsString, |
||||
typings, |
||||
}; |
||||
|
||||
return [typeingReturn, jsonLdContextBuilder.generateJsonldContext()]; |
||||
options: TypingsOptions = {}, |
||||
): Promise<[TypeingReturn, ContextDefinition | undefined]> { |
||||
const format = options.format || "ldo"; |
||||
if (format === "compact") return shexjToTypingCompact(shexj); |
||||
return shexjToTypingLdo(shexj); |
||||
} |
||||
|
@ -0,0 +1,52 @@ |
||||
import type { Schema } from "shexj"; |
||||
import { jsonld2graphobject } from "jsonld2graphobject"; |
||||
import { ShexJNameVisitor } from "../context/ShexJContextVisitor.js"; |
||||
import { JsonLdContextBuilder } from "../context/JsonLdContextBuilder.js"; |
||||
import { |
||||
ShexJTypingTransformerCompact, |
||||
additionalCompactEnumAliases, |
||||
} from "./ShexJTypingTransformerCompact.js"; |
||||
import * as dom from "dts-dom"; |
||||
import type { TypeingReturn } from "./shexjToTypingLdo.js"; |
||||
|
||||
export async function shexjToTypingCompact( |
||||
shexj: Schema, |
||||
): Promise<[TypeingReturn, undefined]> { |
||||
// Prepare processed schema (names still rely on context visitor)
|
||||
const processedShexj: Schema = (await jsonld2graphobject( |
||||
{ |
||||
...shexj, |
||||
"@id": "SCHEMA", |
||||
"@context": "http://www.w3.org/ns/shex.jsonld", |
||||
}, |
||||
"SCHEMA", |
||||
)) as unknown as Schema; |
||||
const nameBuilder = new JsonLdContextBuilder(); |
||||
await ShexJNameVisitor.visit(processedShexj, "Schema", nameBuilder); |
||||
|
||||
additionalCompactEnumAliases.clear(); |
||||
const declarations = await ShexJTypingTransformerCompact.transform( |
||||
processedShexj, |
||||
"Schema", |
||||
{ |
||||
getNameFromIri: nameBuilder.getNameFromIri.bind(nameBuilder), |
||||
}, |
||||
); |
||||
|
||||
// Append only enum aliases (no interface Id aliases in compact format now)
|
||||
additionalCompactEnumAliases.forEach((alias) => { |
||||
const exists = declarations.some((d) => (d as any).name === alias); |
||||
if (!exists) declarations.push(dom.create.alias(alias, dom.type.string)); |
||||
}); |
||||
|
||||
const typings = declarations.map((declaration) => ({ |
||||
typingString: dom |
||||
.emit(declaration, { rootFlags: dom.ContextFlags.InAmbientNamespace }) |
||||
.replace(/\r\n/g, "\n"), |
||||
dts: declaration, |
||||
})); |
||||
const header = `export type IRI = string;\n\n`; |
||||
const typingsString = |
||||
header + typings.map((t) => `export ${t.typingString}`).join(""); |
||||
return [{ typingsString, typings }, undefined]; |
||||
} |
@ -0,0 +1,58 @@ |
||||
import type { ContextDefinition } from "jsonld"; |
||||
import type { Schema } from "shexj"; |
||||
import { JsonLdContextBuilder } from "../context/JsonLdContextBuilder.js"; |
||||
import { ShexJNameVisitor } from "../context/ShexJContextVisitor.js"; |
||||
import { jsonld2graphobject } from "jsonld2graphobject"; |
||||
import { ShexJTypingTransformer } from "./ShexJTypingTransformer.js"; |
||||
import * as dom from "dts-dom"; |
||||
|
||||
export interface TypeingReturn { |
||||
typingsString: string; |
||||
typings: { |
||||
typingString: string; |
||||
dts: dom.TopLevelDeclaration; |
||||
}[]; |
||||
} |
||||
|
||||
export async function shexjToTypingLdo( |
||||
shexj: Schema, |
||||
): Promise<[TypeingReturn, ContextDefinition]> { |
||||
const processedShexj: Schema = (await jsonld2graphobject( |
||||
{ |
||||
...shexj, |
||||
"@id": "SCHEMA", |
||||
"@context": "http://www.w3.org/ns/shex.jsonld", |
||||
}, |
||||
"SCHEMA", |
||||
)) as unknown as Schema; |
||||
const jsonLdContextBuilder = new JsonLdContextBuilder(); |
||||
await ShexJNameVisitor.visit(processedShexj, "Schema", jsonLdContextBuilder); |
||||
|
||||
const declarations = await ShexJTypingTransformer.transform( |
||||
processedShexj, |
||||
"Schema", |
||||
{ |
||||
getNameFromIri: |
||||
jsonLdContextBuilder.getNameFromIri.bind(jsonLdContextBuilder), |
||||
}, |
||||
); |
||||
|
||||
const typings = declarations.map((declaration) => { |
||||
return { |
||||
typingString: dom |
||||
.emit(declaration, { |
||||
rootFlags: dom.ContextFlags.InAmbientNamespace, |
||||
}) |
||||
.replace(/\r\n/g, "\n"), |
||||
dts: declaration, |
||||
}; |
||||
}); |
||||
|
||||
const header = `import { LdSet, LdoJsonldContext } from "@ldo/ldo"\n\n`; |
||||
const typingsString = |
||||
header + typings.map((t) => `export ${t.typingString}`).join(""); |
||||
|
||||
const typeingReturn: TypeingReturn = { typingsString, typings }; |
||||
|
||||
return [typeingReturn, jsonLdContextBuilder.generateJsonldContext()]; |
||||
} |
@ -0,0 +1,17 @@ |
||||
import type { TestData } from "./testData.js"; |
||||
|
||||
export const mixedPluralUnionError: TestData = { |
||||
name: "mixed plural union error", |
||||
shexc: ` |
||||
PREFIX ex: <http://ex/> |
||||
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> |
||||
ex:FooShape { ex:mixed ( @ex:BarShape OR @ex:BazShape )* } |
||||
ex:BarShape { ex:label . } |
||||
ex:BazShape { ex:other . } |
||||
`,
|
||||
sampleTurtle: ``, |
||||
baseNode: "http://ex/foo2", |
||||
successfulContext: {}, |
||||
successfulTypings: "", |
||||
successfulCompactTypings: `export type IRI = string;\n\nexport interface Foo {\n id?: IRI;\n /**\n * Original IRI: http://ex/mixed\n */\n mixed?: Record<IRI, Bar | Baz>;\n}\n\nexport interface Bar {\n id?: IRI;\n /**\n * Original IRI: http://ex/label\n */\n label: any;\n}\n\nexport interface Baz {\n id?: IRI;\n /**\n * Original IRI: http://ex/other\n */\n other: any;\n}\n\n`, |
||||
}; |
@ -0,0 +1,15 @@ |
||||
import type { TestData } from "./testData.js"; |
||||
|
||||
export const pluralAnonymous: TestData = { |
||||
name: "plural anonymous", |
||||
shexc: ` |
||||
PREFIX ex: <http://ex/> |
||||
ex:ConfigHolderShape { ex:configs @ex:ConfigShape* } |
||||
ex:ConfigShape { ex:key . ; ex:val . } |
||||
`,
|
||||
sampleTurtle: ``, |
||||
baseNode: "http://ex/cfg1", |
||||
successfulContext: {}, |
||||
successfulTypings: "", |
||||
successfulCompactTypings: `export type IRI = string;\n\nexport interface ConfigHolder {\n id?: IRI;\n /**\n * Original IRI: http://ex/configs\n */\n configs?: Record<IRI, Config>;\n}\n\nexport interface Config {\n id?: IRI;\n /**\n * Original IRI: http://ex/key\n */\n key: any;\n /**\n * Original IRI: http://ex/val\n */\n val: any;\n}\n\n`, |
||||
}; |
@ -0,0 +1,15 @@ |
||||
import type { TestData } from "./testData.js"; |
||||
|
||||
export const pluralObjects: TestData = { |
||||
name: "plural objects", |
||||
shexc: ` |
||||
PREFIX ex: <http://ex/> |
||||
ex:FooShape { ex:bars @ex:BarShape* } |
||||
ex:BarShape { ex:name . } |
||||
`,
|
||||
sampleTurtle: ``, |
||||
baseNode: "http://ex/foo1", |
||||
successfulContext: {}, |
||||
successfulTypings: "", // not used in this test context
|
||||
successfulCompactTypings: `export type IRI = string;\n\nexport interface Foo {\n id?: IRI;\n /**\n * Original IRI: http://ex/bars\n */\n bars?: Record<IRI, Bar>;\n}\n\nexport interface Bar {\n id?: IRI;\n /**\n * Original IRI: http://ex/name\n */\n name: any;\n}\n\n`, |
||||
}; |
@ -0,0 +1,16 @@ |
||||
import type { TestData } from "./testData.js"; |
||||
|
||||
export const pluralUnionObjects: TestData = { |
||||
name: "plural union objects", |
||||
shexc: ` |
||||
PREFIX ex: <http://ex/> |
||||
ex:A { ex:items ( @ex:Foo OR @ex:Bar )* } |
||||
ex:Foo { ex:f . } |
||||
ex:Bar { ex:b . } |
||||
`,
|
||||
sampleTurtle: ``, |
||||
baseNode: "http://ex/a1", |
||||
successfulContext: {} as any, |
||||
successfulTypings: "", |
||||
successfulCompactTypings: `export type IRI = string;\n\nexport interface A {\n id?: IRI;\n /**\n * Original IRI: http://ex/items\n */\n items?: Record<IRI, Foo | Bar>;\n}\n\nexport interface Foo {\n id?: IRI;\n /**\n * Original IRI: http://ex/f\n */\n f: any;\n}\n\nexport interface Bar {\n id?: IRI;\n /**\n * Original IRI: http://ex/b\n */\n b: any;\n}\n\n`, |
||||
}; |
@ -0,0 +1,18 @@ |
||||
import type { TestData } from "./testData.js"; |
||||
|
||||
export const propertyCollision: TestData = { |
||||
name: "property collision", |
||||
shexc: ` |
||||
PREFIX ex: <http://ex/> |
||||
PREFIX ex2: <http://ex2/> |
||||
PREFIX foaf: <http://xmlns.com/foaf/0.1/> |
||||
PREFIX v1: <http://example.com/v1#> |
||||
PREFIX ver: <http://api.example.com/v2.1:> |
||||
ex:C { ex:label . ; ex2:label . ; foaf:label . ; v1:label . ; ver:label . } |
||||
`,
|
||||
sampleTurtle: ``, |
||||
baseNode: "http://ex/c1", |
||||
successfulContext: {} as any, |
||||
successfulTypings: "", |
||||
successfulCompactTypings: `export type IRI = string;\n\nexport interface C {\n id?: IRI;\n /**\n * Original IRI: http://ex/label\n */\n ex_label: any;\n /**\n * Original IRI: http://ex2/label\n */\n ex2_label: any;\n /**\n * Original IRI: http://xmlns.com/foaf/0.1/label\n */\n "0.1_label": any;\n /**\n * Original IRI: http://example.com/v1#label\n */\n v1_label: any;\n /**\n * Original IRI: http://api.example.com/v2.1:label\n */\n "v2.1:label": any;\n}\n\n`, |
||||
}; |
@ -0,0 +1,24 @@ |
||||
import parser from "@shexjs/parser"; |
||||
import { testData } from "./testData/testData.js"; |
||||
import { shexjToTyping } from "../src/typing/shexjToTyping.js"; |
||||
import type { Schema } from "shexj"; |
||||
|
||||
console.warn = () => {}; |
||||
|
||||
describe("typing-compact", () => { |
||||
testData.forEach((td) => { |
||||
const { name, shexc, successfulCompactTypings } = td; |
||||
if (!successfulCompactTypings) return; // skip if neither
|
||||
it(`Creates compact typings for ${name}`, async () => { |
||||
const schema: Schema = parser |
||||
.construct("https://ldo.js.org/") |
||||
.parse(shexc); |
||||
const [compact] = await shexjToTyping(schema, { format: "compact" }); |
||||
const normalize = (s: string) => |
||||
s.replace(/\r\n/g, "\n").replace(/\n+$/s, "\n"); |
||||
expect(normalize(compact.typingsString)).toBe( |
||||
normalize(successfulCompactTypings) |
||||
); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue