blob: f1c927f3cac0cec6268dfb587118d67da0832737 [file] [log] [blame]
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as fs from "fs";
import * as path from "path";
import { XmlParserTs, getParser } from "@kie-tools/xml-parser-ts";
import {
XptcTsPrimitiveType,
XptcElement,
XptcSimpleType,
XptcComplexType,
XptcComplexTypeAnonymous,
XptcMetaType,
XptcMetaTypeProperty,
XptcAttribute,
XptcTopLevelAttributeGroup,
} from "./types";
import {
XsdAttribute,
XsdComplexType,
XsdSchema,
XsdSequence,
XsdSimpleType,
} from "./schemas/xsd-incomplete--manually-written/ts-gen/types";
import { ns as xsdNs, meta as xsdMeta } from "./schemas/xsd-incomplete--manually-written/ts-gen/meta";
import { mergeWith } from "lodash";
export const __XSD_PARSER = getParser<XsdSchema>({
ns: xsdNs,
meta: xsdMeta,
subs: {},
elements: {},
root: { element: "xsd:schema", type: "schema" },
});
export type Unpacked<T> = T extends (infer U)[] ? U : T;
const __LOGS = {
done: (location: string) => `[xml-parser-ts-codegen] Done for '${location}'.`,
};
export const XSD__TYPES = new Map<string, XptcTsPrimitiveType>([
["xsd:boolean", { type: "primitive", tsEquivalent: "boolean", annotation: "xsd:boolean" }],
["xsd:QName", { type: "primitive", tsEquivalent: "string", annotation: "xsd:QName" }],
["xsd:string", { type: "primitive", tsEquivalent: "string", annotation: "xsd:string" }],
["xsd:int", { type: "primitive", tsEquivalent: "number", annotation: "xsd:int" }],
["xsd:integer", { type: "primitive", tsEquivalent: "number", annotation: "xsd:integer" }],
["xsd:dateTime", { type: "primitive", tsEquivalent: "string", annotation: "xsd:dateTime" }],
["xsd:double", { type: "primitive", tsEquivalent: "number", annotation: "xsd:double" }],
["xsd:long", { type: "primitive", tsEquivalent: "number", annotation: "xsd:long" }],
["xsd:float", { type: "primitive", tsEquivalent: "number", annotation: "xsd:float" }],
["xsd:duration", { type: "primitive", tsEquivalent: "string", annotation: "xsd:duration" }],
["xsd:IDREF", { type: "primitive", tsEquivalent: "string", annotation: "xsd:IDREF" }],
["xsd:anyURI", { type: "primitive", tsEquivalent: "string", annotation: "xsd:anyURI" }],
["xsd:anyType", { type: "primitive", tsEquivalent: "string", annotation: "xsd:antType" }],
["xsd:IDREFS", { type: "primitive", tsEquivalent: "string", annotation: "xsd:IDREFS" }],
["xsd:ID", { type: "primitive", tsEquivalent: "string", annotation: "xsd:ID" }],
]);
// TODO: Tiago --> Write unit tests
async function fetchXsdString(baseLocation: string, relativeLocation: string) {
try {
const url = new URL(relativeLocation);
throw new Error("URLs not yet supported.");
} catch (e) {
const p = path.resolve(baseLocation, relativeLocation);
return { location: p, xsdString: fs.readFileSync(p) };
}
}
// TODO: Tiago --> Write unit tests
async function parseDeep(
__XSD_PARSER: XmlParserTs<XsdSchema>,
baseLocation: string,
relativeLocation: string
): Promise<[string, XsdSchema][]> {
const { xsdString } = await fetchXsdString(baseLocation, relativeLocation);
const { json: schema } = __XSD_PARSER.parse({ type: "xml", xml: xsdString });
const includePromises = (schema["xsd:schema"]["xsd:include"] ?? []).map((i) =>
parseDeep(__XSD_PARSER, baseLocation, i["@_schemaLocation"])
);
const importPromises = (schema["xsd:schema"]["xsd:import"] ?? []).map((i) =>
parseDeep(__XSD_PARSER, baseLocation, i["@_schemaLocation"])
);
const includes = (await Promise.all(includePromises)).flatMap((s) => s);
const imports = (await Promise.all(importPromises)).flatMap((s) => s);
const sameNs = includes.filter(
([k, v]) => v["xsd:schema"]["@_targetNamespace"] === schema["xsd:schema"]["@_targetNamespace"]
);
let schemaPlusIncludes = schema;
for (const [k, v] of sameNs) {
schemaPlusIncludes = mergeWith(schemaPlusIncludes, v, (a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return [...a, ...b];
}
});
}
const includesFromOtherNs = includes.filter(
([k, v]) => v["xsd:schema"]["@_targetNamespace"] !== schema["xsd:schema"]["@_targetNamespace"]
);
return [[relativeLocation, schema], ...imports, ...includesFromOtherNs];
}
async function main() {
const __LOCATION = process.argv[2];
const __ROOT_ELEMENT_NAME = process.argv[3];
const __BASE_LOCATION = path.dirname(__LOCATION);
const __RELATIVE_LOCATION = path.basename(__LOCATION);
const __ROOT_ELEMENT = `${__RELATIVE_LOCATION}__${__ROOT_ELEMENT_NAME}`;
const __RELATIVE_LOCATION_WITHOUT_EXTENSION = __RELATIVE_LOCATION.replace(path.extname(__RELATIVE_LOCATION), "");
const __CONVENTIONS = {
outputFileForGeneratedTypes: path.resolve(".", path.join(__BASE_LOCATION, "ts-gen/types.ts")),
outputFileForGeneratedMeta: path.resolve(".", path.join(__BASE_LOCATION, "ts-gen/meta.ts")),
};
// gather all the XSDs
const __XSDS = new Map<string, XsdSchema>(await parseDeep(__XSD_PARSER, __BASE_LOCATION, __RELATIVE_LOCATION));
// // process <xsd:simpleType>'s
const __SIMPLE_TYPES: XptcSimpleType[] = Array.from(__XSDS.entries()).flatMap(([location, schema]) =>
(schema["xsd:schema"]["xsd:simpleType"] || []).flatMap((xsdSimpleType) => {
if (xsdSimpleType["xsd:union"]) {
if (xsdSimpleType["xsd:union"]["@_memberTypes"] === "xsd:anyURI") {
return [
{
comment: "xsd:anyURI",
type: "simple",
kind: "enum",
name: xsdSimpleType["@_name"]!,
declaredAtRelativeLocation: location,
values: [],
},
];
}
return (xsdSimpleType["xsd:union"]["xsd:simpleType"] ?? []).flatMap((ss) =>
xsdSimpleTypeToXptcSimpleType(ss, location, xsdSimpleType["@_name"]!)
);
} else {
return xsdSimpleTypeToXptcSimpleType(xsdSimpleType, location, xsdSimpleType["@_name"]!);
}
})
);
const __ATTRIBUTE_GROUPS_BY_QNAME: Map<string, XptcTopLevelAttributeGroup> = new Map(
Array.from(__XSDS.entries()).flatMap(([location, schema]) =>
(schema["xsd:schema"]["xsd:attributeGroup"] || []).flatMap((xsdAttrGroup) => {
const qNamePrefix = Object.keys(schema["xsd:schema"])
.find(
(key: keyof (typeof schema)["xsd:schema"]) =>
key.startsWith("@_xmlns:") && // is a xml namespace declaration
schema["xsd:schema"][key] === schema["xsd:schema"]["@_targetNamespace"]
)
?.split(":")[1];
if (!qNamePrefix) {
return [];
}
return [
[
`${qNamePrefix}:${xsdAttrGroup["@_name"]}`,
{
name: xsdAttrGroup["@_name"],
attributes: (xsdAttrGroup["xsd:attribute"] ?? []).map((xsdAttr) =>
xsdAttributeToXptcAttribute(xsdAttr, location)
),
},
],
];
})
)
);
// // process <xsd:complexType>'s
const __COMPLEX_TYPES: XptcComplexType[] = [];
for (const [location, xsd] of __XSDS.entries()) {
for (const xsdCt of xsd["xsd:schema"]["xsd:complexType"] || []) {
const isAbstract = xsdCt["@_abstract"] ?? false;
const extensionElement =
xsdCt["xsd:complexContent"]?.["xsd:extension"] ?? xsdCt["xsd:simpleContent"]?.["xsd:extension"];
__COMPLEX_TYPES.push({
type: "complex",
comment: isAbstract ? "abstract" : "",
isMixed: xsdCt["@_mixed"] ?? false,
isAbstract,
isAnonymous: false,
name: xsdCt["@_name"]!,
isSimpleContent: !!xsdCt["xsd:simpleContent"],
needsExtensionType: !!xsdCt["xsd:anyAttribute"] || !!xsdCt["xsd:sequence"]?.["xsd:any"],
declaredAtRelativeLocation: location,
childOf: extensionElement?.["@_base"],
elements: [
...(xsdCt["xsd:all"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location)
),
...(xsdCt["xsd:sequence"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location)
),
...(extensionElement?.["xsd:sequence"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location)
),
...(extensionElement?.["xsd:sequence"]?.["xsd:choice"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location, { forceOptional: true })
),
...(extensionElement?.["xsd:choice"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location, { forceOptional: true })
),
...(extensionElement?.["xsd:choice"]?.["xsd:sequence"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(__ATTRIBUTE_GROUPS_BY_QNAME, xsdCt["@_name"]!, s, location, { forceOptional: true })
),
],
attributes: [
...(xsdCt["xsd:attribute"] ?? []).map((a) => xsdAttributeToXptcAttribute(a, location)),
...(extensionElement?.["xsd:attribute"] ?? []).map((a) => xsdAttributeToXptcAttribute(a, location)),
...(xsdCt["xsd:attributeGroup"] ?? []).flatMap(
(attrGroup) => __ATTRIBUTE_GROUPS_BY_QNAME.get(attrGroup["@_ref"])?.attributes ?? []
),
],
});
}
}
// // process <xsd:element>'s
const __GLOBAL_ELEMENTS = new Map<string, XptcElement>();
for (const [location, xsd] of __XSDS.entries()) {
for (const e of xsd["xsd:schema"]["xsd:element"] || []) {
const a = xsdElementToXptcElement(
__ATTRIBUTE_GROUPS_BY_QNAME,
"GLOBAL",
{ ...e, "@_minOccurs": 0, "@_maxOccurs": "unbounded" },
location,
{
forceOptional: false,
}
);
let isAbstract = e["@_abstract"];
if (a.kind === "ofNamedType") {
const split = a.typeName?.split(":") ?? [];
const qNamePrefix = split.length === 2 ? split[0] : undefined;
const typeName = split.length === 2 ? split[1] : split[0];
const namespace = xsd["xsd:schema"][`@_xmlns:${qNamePrefix}`];
const [typeXsdLocation, typeXsd] =
[...__XSDS.entries()].find(([location, xsd]) => xsd["xsd:schema"]["@_targetNamespace"] === namespace) ?? [];
const type = __COMPLEX_TYPES
.filter((s) => !s.isAnonymous)
.filter((s) => (qNamePrefix ? s.declaredAtRelativeLocation === typeXsdLocation : true))
.find((s) => s.name === typeName);
if (!type) {
throw new Error(`Can't find type '${typeName}' for element ${e["@_name"]}`);
}
isAbstract ??= type?.isAbstract;
}
isAbstract ??= false;
__GLOBAL_ELEMENTS.set(`${location}__${e["@_name"]}`, {
name: e["@_name"],
isAbstract,
substitutionGroup: e["@_substitutionGroup"],
type: e["@_type"],
declaredAtRelativeLocation: location,
anonymousType: a.kind === "ofAnonymousType" ? a.anonymousType : undefined,
});
}
}
// // substitutionGroups are SCOPED. Meaning that we need to consider only what the current XSD is importing into it.
// // This map goes from a relativeLocation to an elementName to a list of elementNames.
const __SUBSTITUTIONS = new Map<string, Map<string, string[]>>();
for (const [baseLoc, _] of __XSDS.entries()) {
const xsds = new Map<string, XsdSchema>(await parseDeep(__XSD_PARSER, __BASE_LOCATION, baseLoc));
for (const [xLocation, xsd] of xsds.entries()) {
const localizedSubstitutions = __SUBSTITUTIONS.get(xLocation) ?? new Map<string, string[]>();
__SUBSTITUTIONS.set(xLocation, localizedSubstitutions);
for (const e of xsd["xsd:schema"]["xsd:element"] || []) {
if (e["@_substitutionGroup"]) {
const subsGroup = getXptcElementFromElementQName(
__XSDS,
__GLOBAL_ELEMENTS,
xLocation,
e["@_substitutionGroup"]
);
if (!subsGroup) {
throw new Error(`Invalid subsitution group for element '${e["@_name"]}'`);
}
const elem = getXptcElementFromElementQName(__XSDS, __GLOBAL_ELEMENTS, xLocation, e["@_name"]);
if (!elem) {
throw new Error(`Invalid element '${e["@_name"]}'`);
}
const localizedElementName = `${subsGroup.declaredAtRelativeLocation}__${subsGroup.name}`;
// Using this strategy to remove duplicates.
const accumulatedSubstitutionElements = new Set([
...(localizedSubstitutions.get(localizedElementName) ?? []),
`${xLocation}__${elem.name}`,
...(subsGroup.isAbstract ? [] : [localizedElementName]), // Include itself if not abstract
]);
localizedSubstitutions.set(localizedElementName, [...accumulatedSubstitutionElements]);
}
}
}
}
Array.from(__GLOBAL_ELEMENTS.values()).forEach((e) => {
if (!e.anonymousType) {
return;
} else {
__COMPLEX_TYPES.push(e.anonymousType);
}
});
const __NAMED_TYPES_BY_TS_NAME = new Map<string, XptcComplexType | XptcSimpleType>([
...__SIMPLE_TYPES.map(
(st) => [getTsNameFromNamedType(st.declaredAtRelativeLocation, st.name), st] as [string, XptcSimpleType]
),
...__COMPLEX_TYPES.map((ct) => {
if (ct.isAnonymous) {
const name = getAnonymousMetaTypeName(ct.forElementWithName, "GLOBAL");
return [getTsNameFromNamedType(ct.declaredAtRelativeLocation, name), ct] as [string, XptcComplexType];
} else {
return [getTsNameFromNamedType(ct.declaredAtRelativeLocation, ct.name), ct] as [string, XptcComplexType];
}
}),
]);
const __META_TYPE_MAPPING = new Map<string, XptcMetaType>();
const rootTsTypeName = getTsNameFromNamedType(
__RELATIVE_LOCATION_WITHOUT_EXTENSION,
__GLOBAL_ELEMENTS.get(__ROOT_ELEMENT)!.type ?? getAnonymousMetaTypeName(__ROOT_ELEMENT_NAME, "GLOBAL")
);
let ts = "";
for (const sp of __SIMPLE_TYPES) {
if (sp.kind === "int") {
// ignore int types, they're only interesting for validation.
continue;
}
const enumName = getTsNameFromNamedType(sp.declaredAtRelativeLocation, sp.name);
if (sp.comment === "xsd:anyURI") {
ts += `
export type ${enumName} = string; // ${sp.comment}
`;
} else if (sp.kind === "enum") {
ts += `
export type ${enumName} = |
${sp.values.map((v) => ` '${v}'`).join(" |\n")}
`;
}
}
for (const ct of __COMPLEX_TYPES) {
const typeName = getTsNameFromNamedType(
ct.declaredAtRelativeLocation,
ct.isAnonymous ? getAnonymousMetaTypeName(ct.forElementWithName, "GLOBAL") : ct.name
);
const { metaProperties, needsExtensionType, anonymousTypes } = getMetaProperties(
__RELATIVE_LOCATION,
__META_TYPE_MAPPING,
__GLOBAL_ELEMENTS,
__SUBSTITUTIONS,
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct,
typeName
);
const properties = metaProperties
.map((p) => {
const optionalMarker = p.isOptional ? "?" : "";
const arrayMarker = p.isArray ? "[]" : "";
const tsType =
p.metaType.name === "integer" || p.metaType.name === "float" || p.metaType.name === "long"
? "number"
: p.metaType.name;
const ns = getMetaPropertyNs(__RELATIVE_LOCATION, p);
return ` "${ns}${p.name}"${optionalMarker}: ${p.typeBody?.(tsType) ?? tsType}${arrayMarker}; // from type ${
p.fromType
} @ ${p.declaredAt}`;
})
.join("\n");
const doc = ct.comment.trim() ? `/* ${ct.comment} */` : "";
const anonymousTypesString = anonymousTypes
.map((anonType) => {
const anonymousTypesProperties = anonType.properties.map(
(p) =>
` "${p.name}": ${
p.metaType.name === "integer" || p.metaType.name === "float" || p.metaType.name === "long"
? "number"
: p.metaType.name
};`
);
// FIXME: Tiago: Not all anonymous types are extensible!
return `export interface ${anonType.name} {
__?: undefined;
${anonymousTypesProperties.join("\n")}
}`;
})
.join("\n");
if (needsExtensionType) {
const rootElementBaseType = rootTsTypeName === typeName ? "extends XmlParserTsRootElementBaseType" : "";
ts += `
export interface ${typeName} ${rootElementBaseType} ${doc} {
__?: undefined;
${properties}
}
${anonymousTypesString}
`;
} else {
const rootElementBaseType = rootTsTypeName === typeName ? "XmlParserTsRootElementBaseType & " : "";
ts += `
export type ${typeName} = ${rootElementBaseType} ${doc} {
${properties}
}
${anonymousTypesString}
`;
}
}
ts = `import { XmlParserTsRootElementBaseType } from "@kie-tools/xml-parser-ts"
${ts}
`;
ts = `// This file was automatically generated
${ts}
`;
fs.mkdirSync(path.dirname(__CONVENTIONS.outputFileForGeneratedTypes), { recursive: true });
fs.writeFileSync(__CONVENTIONS.outputFileForGeneratedTypes, ts);
// meta
let meta = `
export const root = {
element: "${getRealtiveLocationNs(__RELATIVE_LOCATION, __RELATIVE_LOCATION) + __ROOT_ELEMENT_NAME}",
type: "${rootTsTypeName}"
} as const;
export const ns = new Map<string, string>([
${[...__XSDS.entries()]
.map(([k, v]) => {
const uri = v["xsd:schema"]["@_targetNamespace"];
const ns = getRealtiveLocationNs(__RELATIVE_LOCATION, k);
return ` ["${uri}", "${ns}"],
["${ns}", "${uri}"],`;
})
.join("\n")}
]);
export const subs = {
${Array.from(__SUBSTITUTIONS.entries())
.map(
([namespace, subs]) => ` "${getRealtiveLocationNs(__RELATIVE_LOCATION, namespace)}": {
${Array.from(subs.entries())
.map(
([head, elements]) =>
`${elements
.flatMap((e) => {
const elementName = `${getRealtiveLocationNs(__RELATIVE_LOCATION, e.split("__")[0]) + e.split("__")[1]}`;
const headName = `${getRealtiveLocationNs(__RELATIVE_LOCATION, head.split("__")[0]) + head.split("__")[1]}`;
if (elementName === headName) {
return []; // Do not serialize itself as a substitution.
} else {
return ` "${elementName}": "${headName}",`;
}
})
.join("\n")}`
)
.join("\n")}
},`
)
.join("\n")}
};
export const elements = {
${Array.from(__GLOBAL_ELEMENTS.entries())
.map(([k, v]) => {
const s = v.type?.split(":") || [getAnonymousMetaTypeName(v.name, "GLOBAL")];
const elementName = `${getRealtiveLocationNs(__RELATIVE_LOCATION, k.split("__")[0])}${v.name}`;
const elementType = `${getTsNameFromNamedType(v.declaredAtRelativeLocation, s.length === 1 ? s[0] : s[1])}`;
return ` "${elementName}": "${elementType}",`;
})
.join("\n")}
};
export const meta = {
`;
Array.from(__META_TYPE_MAPPING.entries()).forEach(([name, type]) => {
meta += ` "${name}": {
`;
type.properties.forEach((p) => {
const ns = getMetaPropertyNs(__RELATIVE_LOCATION, p);
meta += ` "${ns}${p.name}": { type: "${p.metaType.name}", isArray: ${p.isArray}, fromType: "${p.fromType}", xsdType: "${p.metaType.xsdType}" },
`;
});
meta += ` },
`;
});
meta += `} as const;
`;
fs.mkdirSync(path.dirname(__CONVENTIONS.outputFileForGeneratedMeta), { recursive: true });
fs.writeFileSync(__CONVENTIONS.outputFileForGeneratedMeta, meta);
console.log(__LOGS.done(__LOCATION));
}
main();
function getMetaPropertyNs(__RELATIVE_LOCATION: string, p: XptcMetaTypeProperty) {
return p.name.startsWith("@_")
? ""
: getRealtiveLocationNs(__RELATIVE_LOCATION, p.elem?.declaredAtRelativeLocation ?? p.declaredAt);
}
function getRealtiveLocationNs(__RELATIVE_LOCATION: string, relativeLocation: string) {
return relativeLocation === __RELATIVE_LOCATION
? ""
: `${relativeLocation.replace(".xsd", "").toLocaleLowerCase().replaceAll(/\d/g, "")}:`;
}
function resolveElementRef(
__GLOBAL_ELEMENTS: Map<string, XptcElement>,
__XSDS: Map<string, XsdSchema>,
substitutions: Map<string, string[]>,
referencedElement: XptcElement
): XptcElement[] {
const key = `${referencedElement.declaredAtRelativeLocation}__${referencedElement.name}`;
const substitutionNamesForReferencedElement = substitutions.get(key);
if (!substitutionNamesForReferencedElement) {
return [referencedElement];
}
const substitutionsWithoutSelfReferences = new Map(
[...substitutions.entries()].map(([key, value]) => [key, value.filter((v) => v !== key)] as const)
);
const resolved = substitutionNamesForReferencedElement.flatMap((substitutionElementName) => {
const substitutionElement = __GLOBAL_ELEMENTS.get(substitutionElementName);
if (!substitutionElement) {
throw new Error(`Can't find element '${substitutionElementName}' for substitution ${key}`);
}
if (substitutionElement.isAbstract) {
return resolveElementRef(__GLOBAL_ELEMENTS, __XSDS, substitutionsWithoutSelfReferences, substitutionElement);
}
// Include itself if not abstract.
return [
substitutionElement,
...resolveElementRef(__GLOBAL_ELEMENTS, __XSDS, substitutionsWithoutSelfReferences, substitutionElement),
];
});
const seen = new Set<string>();
return resolved.filter((s) => {
const key = `${s.declaredAtRelativeLocation}__${s.name}`;
if (seen.has(key)) {
return false;
} else {
seen.add(key);
return true;
}
});
}
function getMetaTypeName({ name, annotation }: { name: string; annotation: string }) {
return name === "number" ? (annotation === "xsd:double" || annotation === "xsd:float" ? "float" : "integer") : name;
}
function getTypeBodyForElementRef(
__RELATIVE_LOCATION: string,
__META_TYPE_MAPPING: Map<string, XptcMetaType>,
__GLOBAL_ELEMENTS: Map<string, XptcElement>,
__SUBSTITUTIONS: Map<string, Map<string, string[]>>,
__XSDS: Map<string, XsdSchema>,
__NAMED_TYPES_BY_TS_NAME: Map<string, XptcComplexType | XptcSimpleType>,
ct: XptcComplexType,
referencedElement: XptcElement
) {
const resolutions = resolveElementRef(
__GLOBAL_ELEMENTS,
__XSDS,
__SUBSTITUTIONS.get(ct.declaredAtRelativeLocation)!,
referencedElement
);
// No substitutions occured, proceed with normal type.
if (resolutions.length === 1 && resolutions[0] === referencedElement) {
return undefined;
}
return `( /* From subsitution groups */
${resolutions
.flatMap((element) => {
const elementNs = getRealtiveLocationNs(__RELATIVE_LOCATION, element.declaredAtRelativeLocation);
const elementName = `${elementNs}${element.name}`;
return [
` ({ __$$element: "${elementName}" } & ${
getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct.declaredAtRelativeLocation,
element.type ?? getAnonymousMetaTypeName(element.name, "GLOBAL")
).name
})`,
];
})
.join(" |\n")}
)`;
}
function getMetaProperties(
__RELATIVE_LOCATION: string,
__META_TYPE_MAPPING: Map<string, XptcMetaType>,
__GLOBAL_ELEMENTS: Map<string, XptcElement>,
__SUBSTITUTIONS: Map<string, Map<string, string[]>>,
__XSDS: Map<string, XsdSchema>,
__NAMED_TYPES_BY_TS_NAME: Map<string, XptcComplexType | XptcSimpleType>,
ct: XptcComplexType,
metaTypeName: string
): { anonymousTypes: XptcMetaType[]; needsExtensionType: boolean; metaProperties: XptcMetaTypeProperty[] } {
/** Accumulates all properties of this complex type (ct). Attributes and elements. */
let ctMetaProperties: XptcMetaTypeProperty[] = [];
/** Accumulates all anonymous types instantiated on this complex type's hierarchy */
const anonymousTypes: XptcMetaType[] = [];
const immediateParentType = ct.childOf
? getTsTypeFromQName(__XSDS, __NAMED_TYPES_BY_TS_NAME, ct.declaredAtRelativeLocation, ct.childOf)
: undefined;
let curParentCt = immediateParentType ? __NAMED_TYPES_BY_TS_NAME.get(immediateParentType.name) : undefined;
let needsExtensionType = ct.needsExtensionType;
let isMixed = ct.isMixed;
while (curParentCt) {
const curParentCtMetaProperties: XptcMetaTypeProperty[] = [];
if (curParentCt?.type === "complex") {
const curParentCtMetaTypeName = getTsNameFromNamedType(
curParentCt.declaredAtRelativeLocation,
curParentCt.isAnonymous ? getAnonymousMetaTypeName(curParentCt.forElementWithName, "GLOBAL") : curParentCt.name
);
needsExtensionType = needsExtensionType || curParentCt.needsExtensionType;
if (curParentCt.isAnonymous) {
throw new Error("Anonymous types are never parent types.");
}
isMixed ||= curParentCt.isMixed;
for (const a of curParentCt.attributes) {
const attributeType = getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
curParentCt.declaredAtRelativeLocation,
a.localTypeRef
);
if (!attributeType) {
throw new Error(`Can't resolve local type ref ${a.localTypeRef}`);
}
curParentCtMetaProperties.push({
declaredAt: curParentCt.declaredAtRelativeLocation,
fromType: curParentCtMetaTypeName,
elem: undefined,
name: `@_${a.name}`,
metaType: { name: getMetaTypeName(attributeType), xsdType: attributeType.annotation },
isArray: false,
isOptional: a.isOptional,
});
}
for (const e of curParentCt.elements) {
if (e.kind === "ofAnonymousType") {
const anonymousTypeName = getAnonymousMetaTypeName(e.name, metaTypeName);
const mp = getMetaProperties(
__RELATIVE_LOCATION,
__META_TYPE_MAPPING,
__GLOBAL_ELEMENTS,
__SUBSTITUTIONS,
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
e.anonymousType,
anonymousTypeName
);
anonymousTypes.push({ name: anonymousTypeName, properties: mp.metaProperties });
anonymousTypes.push(...mp.anonymousTypes);
__META_TYPE_MAPPING.set(anonymousTypeName, {
name: anonymousTypeName,
properties: mp.metaProperties,
});
curParentCtMetaProperties.push({
elem: undefined, // REALLY?
declaredAt: curParentCt.declaredAtRelativeLocation,
fromType: curParentCtMetaTypeName,
name: e.name,
metaType: { name: anonymousTypeName, xsdType: "Anonumous type..." },
isArray: e.isArray,
isOptional: e.isOptional,
});
} else if (e.kind === "ofNamedType") {
const tsType = getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct.declaredAtRelativeLocation,
e.typeName
);
curParentCtMetaProperties.push({
declaredAt: curParentCt.declaredAtRelativeLocation,
fromType: curParentCtMetaTypeName,
elem: undefined, // REALLY?
name: e.name,
metaType: { name: getMetaTypeName(tsType), xsdType: tsType.annotation },
typeBody: getTsTypeBody(tsType),
isArray: e.isArray,
isOptional: e.isOptional,
});
} else if (e.kind === "ofRef") {
const referencedElement = getXptcElementFromElementQName(
__XSDS,
__GLOBAL_ELEMENTS,
ct.declaredAtRelativeLocation,
e.ref
);
if (!referencedElement) {
throw new Error(`Can't find reference to element '${e.ref}'`);
}
const tsType = referencedElement.type
? getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct.declaredAtRelativeLocation,
referencedElement.type
)
: {
name: getTsNameFromNamedType(
ct.declaredAtRelativeLocation,
getAnonymousMetaTypeName(referencedElement.name, "GLOBAL")
),
annotation: "Anonymous type from element " + referencedElement.name,
};
curParentCtMetaProperties.push({
declaredAt: referencedElement?.declaredAtRelativeLocation,
fromType: ct.isAnonymous ? "" : curParentCtMetaTypeName,
name: referencedElement.name,
elem: referencedElement,
metaType: { name: getMetaTypeName(tsType), xsdType: tsType.annotation },
typeBody: () =>
getTypeBodyForElementRef(
__RELATIVE_LOCATION,
__META_TYPE_MAPPING,
__GLOBAL_ELEMENTS,
__SUBSTITUTIONS,
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct,
referencedElement
),
isArray: e.isArray,
isOptional: e.isOptional,
});
} else {
throw new Error("Unknonwn type of element " + e);
}
}
const nextParentType = curParentCt.childOf
? getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
curParentCt.declaredAtRelativeLocation,
curParentCt.childOf
)
: undefined;
// Make sure the inheritance order is respected. Elements should be listed always from the most generic to the most specific type.
// Since we're iterating upwards in the hierarchy, we need to invert prepend the array with the props we find on each step of the hierarchy.
ctMetaProperties = [...curParentCtMetaProperties, ...ctMetaProperties];
curParentCt = nextParentType ? __NAMED_TYPES_BY_TS_NAME.get(nextParentType.name) : undefined;
} else if (curParentCt?.type === "simple") {
throw new Error("Can't have a non-complex type as parent of another.");
} else {
curParentCt = undefined;
}
}
// Own properties are parsed later to ensure xsd:sequence order.
for (const a of ct.attributes) {
const attributeType = getTsTypeFromQName(
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct.declaredAtRelativeLocation,
a.localTypeRef
);
ctMetaProperties.push({
declaredAt: ct.declaredAtRelativeLocation,
fromType: metaTypeName,
name: `@_${a.name}`,
elem: undefined,
metaType: { name: getMetaTypeName(attributeType), xsdType: attributeType.annotation },
isArray: false,
isOptional: a.isOptional,
});
}
for (const e of ct.elements) {
if (e.kind === "ofRef") {
const referencedElement = getXptcElementFromElementQName(
__XSDS,
__GLOBAL_ELEMENTS,
ct.declaredAtRelativeLocation,
e.ref
);
if (!referencedElement) {
throw new Error(`Can't find reference to element '${e.ref}'`);
}
const tsType = referencedElement.type
? getTsTypeFromQName(__XSDS, __NAMED_TYPES_BY_TS_NAME, ct.declaredAtRelativeLocation, referencedElement.type)
: {
name: getTsNameFromNamedType(
ct.declaredAtRelativeLocation,
getAnonymousMetaTypeName(referencedElement.name, "GLOBAL")
),
annotation: "Anonymous type from element " + referencedElement.name,
};
ctMetaProperties.push({
declaredAt: referencedElement?.declaredAtRelativeLocation,
fromType: ct.isAnonymous ? "" : metaTypeName,
name: referencedElement.name,
elem: referencedElement,
metaType: { name: getMetaTypeName(tsType), xsdType: tsType.annotation },
typeBody: () =>
getTypeBodyForElementRef(
__RELATIVE_LOCATION,
__META_TYPE_MAPPING,
__GLOBAL_ELEMENTS,
__SUBSTITUTIONS,
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
ct,
referencedElement
),
isArray: e.isArray,
isOptional: e.isOptional,
});
} else if (e.kind === "ofNamedType") {
const tsType = getTsTypeFromQName(__XSDS, __NAMED_TYPES_BY_TS_NAME, ct.declaredAtRelativeLocation, e.typeName);
ctMetaProperties.push({
declaredAt: ct.declaredAtRelativeLocation,
fromType: metaTypeName,
name: e.name,
elem: undefined, // REALLY?
metaType: { name: getMetaTypeName(tsType), xsdType: tsType.annotation },
typeBody: getTsTypeBody(tsType),
isArray: e.isArray,
isOptional: e.isOptional,
});
} else if (e.kind === "ofAnonymousType") {
const anonymousTypeName = getAnonymousMetaTypeName(e.name, metaTypeName);
const mp = getMetaProperties(
__RELATIVE_LOCATION,
__META_TYPE_MAPPING,
__GLOBAL_ELEMENTS,
__SUBSTITUTIONS,
__XSDS,
__NAMED_TYPES_BY_TS_NAME,
e.anonymousType,
anonymousTypeName
);
anonymousTypes.push({ name: anonymousTypeName, properties: mp.metaProperties });
anonymousTypes.push(...mp.anonymousTypes);
__META_TYPE_MAPPING.set(anonymousTypeName, {
name: anonymousTypeName,
properties: mp.metaProperties,
});
ctMetaProperties.push({
declaredAt: ct.declaredAtRelativeLocation,
fromType: metaTypeName,
name: e.name,
elem: undefined, // REALLY?
metaType: { name: anonymousTypeName, xsdType: "Anonymous type..." },
isArray: e.isArray,
isOptional: e.isOptional,
});
} else {
throw new Error(`Unknown kind of XptcComplexType '${e}'`);
}
}
if (ct.isSimpleContent && ct.childOf) {
const t = getTsTypeFromQName(__XSDS, __NAMED_TYPES_BY_TS_NAME, ct.declaredAtRelativeLocation, ct.childOf);
ctMetaProperties.push({
declaredAt: ct.declaredAtRelativeLocation,
fromType: metaTypeName,
name: `__$$text`,
elem: undefined,
metaType: {
name: t.name,
xsdType: t.annotation,
},
isArray: false,
isOptional: false,
});
}
if (isMixed) {
ctMetaProperties.push({
declaredAt: ct.declaredAtRelativeLocation,
fromType: metaTypeName,
name: `__$$text`,
elem: undefined,
metaType: {
name: "string",
xsdType: "xsd:string",
},
isArray: false,
isOptional: true,
});
}
if (!(ct.type === "complex" && !ct.isAnonymous && ct.isAbstract)) {
__META_TYPE_MAPPING.set(metaTypeName, {
name: metaTypeName,
properties: [...ctMetaProperties.reduce((acc, p) => acc.set(p.name, p), new Map()).values()], // Removing duplicates.
});
}
return { metaProperties: ctMetaProperties, needsExtensionType, anonymousTypes };
}
function getAnonymousMetaTypeName(elementName: string, metaTypeName: string) {
return `${metaTypeName}__${elementName}`;
}
function getTsNameFromNamedType(relativeLocation: string, namedTypeName: string) {
const filenameWithoutExtension = path.basename(relativeLocation).replace(path.extname(relativeLocation), "");
return `${filenameWithoutExtension}__${namedTypeName}`.replaceAll(/[ -.]/g, "_");
}
function getTsTypeFromQName(
__XSDS: Map<string, XsdSchema>,
__NAMED_TYPES_BY_TS_NAME: Map<string, XptcComplexType | XptcSimpleType>,
relativeLocation: string,
namedTypeQName: string
): { name: string; annotation: string } {
// check if it's a local ref to another namespace
if (namedTypeQName.includes(":") && namedTypeQName.split(":").length === 2) {
const [localNsName, namedTypeName] = namedTypeQName.split(":");
const xmlnsKey = `@_xmlns:${localNsName}`;
const namespace = (__XSDS.get(relativeLocation)?.["xsd:schema"] as any)[xmlnsKey];
// short circuit here. we don't parse XSD's XSD.
if (namespace === "http://www.w3.org/2001/XMLSchema") {
const xsdType = XSD__TYPES.get(namedTypeQName);
if (!xsdType) {
throw new Error(`Unknown XSD type '${namedTypeQName}'`);
}
return { name: xsdType.tsEquivalent, annotation: xsdType.annotation };
}
// find the XSD with matching namespace declaration.
const referencedXsd = [...__XSDS.entries()].find(([_, s]) => s["xsd:schema"]["@_targetNamespace"] === namespace);
if (!referencedXsd) {
throw new Error(`Can't find referenced namespace '${namespace}'`);
}
const [referencedXsdRelativeLocation, _] = referencedXsd;
// with the referenced XSD location, we can build the TS type name.
const tsTypeName = getTsNameFromNamedType(referencedXsdRelativeLocation, namedTypeName);
const namedType = __NAMED_TYPES_BY_TS_NAME.get(tsTypeName);
if (!namedType) {
throw new Error(`Named type not found with name '${namedType}'`);
}
// some simple types are declared just because of the restrictions it has, they're not interesting for the generated structure.
if (namedType.type === "simple" && namedType.kind === "int" && namedType.restrictionBase) {
return getTsTypeFromQName(__XSDS, __NAMED_TYPES_BY_TS_NAME, relativeLocation, namedType.restrictionBase);
}
// found it!
return { name: tsTypeName, annotation: `type found from namespace with declaration name '${localNsName}'.` };
}
// not a reference to a type in another namespace. simply local name.
return {
name: getTsNameFromNamedType(relativeLocation, namedTypeQName),
annotation: "// local type",
};
}
function xsdSimpleTypeToXptcSimpleType(
xsdSimpleType: XsdSimpleType,
location: string,
nameIfUnnamed: string
): XptcSimpleType {
if (
xsdSimpleType["xsd:restriction"]?.["@_base"] === "xsd:string" ||
xsdSimpleType["xsd:restriction"]?.["@_base"] === "xsd:token"
) {
if (xsdSimpleType["xsd:restriction"]["xsd:enumeration"]) {
return {
comment: "enum",
type: "simple",
kind: "enum",
name: xsdSimpleType["@_name"] ?? nameIfUnnamed,
declaredAtRelativeLocation: location,
values: xsdSimpleType["xsd:restriction"]["xsd:enumeration"].map((e) => e["@_value"]),
};
} else {
return {
comment: "string",
type: "simple",
kind: "string",
name: xsdSimpleType["@_name"] ?? nameIfUnnamed,
declaredAtRelativeLocation: location,
};
}
} else if (
xsdSimpleType["xsd:restriction"]?.["@_base"] === "xsd:int" ||
xsdSimpleType["xsd:restriction"]?.["@_base"] === "xsd:integer"
) {
return {
comment: "int",
type: "simple",
kind: "int",
restrictionBase: xsdSimpleType["xsd:restriction"]["@_base"],
name: xsdSimpleType["@_name"] ?? nameIfUnnamed,
declaredAtRelativeLocation: location,
minInclusive: xsdSimpleType["xsd:restriction"]["xsd:minInclusive"]?.["@_value"],
maxInclusive: xsdSimpleType["xsd:restriction"]["xsd:maxInclusive"]?.["@_value"],
};
} else {
throw new Error(`Unknown xsd:simpleType --> ${JSON.stringify(xsdSimpleType, undefined, 2)}`);
}
}
function getXptcElementFromElementQName(
__XSDS: Map<string, XsdSchema>,
__GLOBAL_ELEMENTS: Map<string, XptcElement>,
relativeLocation: string,
elementQName: string
): XptcElement | undefined {
// check if it's a QName to another namespace
if (elementQName.includes(":") && elementQName.split(":").length === 2) {
const [localNsName, referencedElementName] = elementQName.split(":");
const xmlnsKey = `@_xmlns:${localNsName}`;
const namespace = (__XSDS.get(relativeLocation)?.["xsd:schema"] as any)[xmlnsKey];
// find the XSD with matching namespace declaration.
const referencedXsd = [...__XSDS.entries()].find(([_, s]) => s["xsd:schema"]["@_targetNamespace"] === namespace);
if (!referencedXsd) {
throw new Error(`Can't find referenced namespace '${namespace}'`);
}
const [referencedXsdRelativeLocation, _] = referencedXsd;
return __GLOBAL_ELEMENTS.get(`${referencedXsdRelativeLocation}__${referencedElementName}`);
}
return __GLOBAL_ELEMENTS.get(`${relativeLocation}__${elementQName}`);
}
function xsdElementToXptcElement(
__ATTRIBUTE_GROUPS_BY_QNAME: Map<string, XptcTopLevelAttributeGroup>,
parentIdentifierForExtensionType: string,
xsdElement: NonNullable<Unpacked<XsdSequence["xsd:element"]>>,
location: string,
args?: { forceOptional: boolean }
): Unpacked<XptcComplexType["elements"]> {
const minOccurs = xsdElement["@_minOccurs"] ?? 1;
const maxOccurs = xsdElement["@_maxOccurs"] ?? 1;
let isArray = false;
let isOptional = false;
if (maxOccurs !== "unbounded" && maxOccurs < minOccurs) {
throw new Error(`maxOccurs cannot be smaller than minOccurs (at '${parentIdentifierForExtensionType}').`);
}
if (minOccurs === 0) {
if (maxOccurs === "unbounded") {
isArray = true;
isOptional = true;
} else if (maxOccurs === 0) {
throw new Error(
`minOccurs and maxOccurs cannot be 0 at the same time at element '${parentIdentifierForExtensionType}'.`
);
} else if (maxOccurs === 1) {
isOptional = true;
} else if (maxOccurs > 1) {
isArray = true; // at max 'maxOccurs' --> validation
} else {
throw new Error(`Impossible scenario for minOccurs === 0. maxOccurs is '${maxOccurs}'.`);
}
} else if (minOccurs === 1) {
if (maxOccurs === "unbounded") {
isArray = true; // at least one element --> validation
} else if (maxOccurs === 1) {
isOptional = false;
} else if (maxOccurs > 1) {
isArray = true; // at least one element and at max 'maxOccurs' --> validation
} else {
throw new Error(`Impossible scenario for minOccurs === 1. maxOccurs is '${maxOccurs}'.`);
}
} else if (minOccurs > 1) {
if (maxOccurs === "unbounded") {
isArray = true; // at least 'minOccurs' elements --> validation
} else if (maxOccurs >= minOccurs) {
isArray = true; // at least one element and at max 'maxOccurs' --> validation
} else {
throw new Error(`Impossible scenario for minOccurs > 1. maxOccurs is '${maxOccurs}'.`);
}
} else {
throw new Error(
`Impossible scenario for minOccurs. minOccurs is '${minOccurs}(${typeof minOccurs})' inside element '${JSON.stringify(
xsdElement
)}'.`
);
}
if (args?.forceOptional) {
isOptional = true;
}
if (xsdElement["@_type"] && xsdElement["@_name"]) {
return {
name: xsdElement["@_name"],
typeName: xsdElement["@_type"],
kind: "ofNamedType",
isArray,
isOptional,
};
}
if (xsdElement["xsd:complexType"] && xsdElement["@_name"]) {
return {
name: xsdElement["@_name"],
kind: "ofAnonymousType",
isArray,
isOptional,
anonymousType: xsdComplexTypeToAnonymousXptcComplexType(
__ATTRIBUTE_GROUPS_BY_QNAME,
parentIdentifierForExtensionType,
xsdElement["xsd:complexType"],
location,
xsdElement["@_name"]
),
};
}
if (xsdElement["xsd:simpleType"] && xsdElement["@_name"]) {
throw new Error("Simple types not implemented for anonymous element types.");
}
if (xsdElement["@_ref"]) {
return {
ref: xsdElement["@_ref"],
kind: "ofRef",
isArray,
isOptional,
};
}
throw new Error(`Unknown xsd:element structure. ${JSON.stringify(xsdElement)}`);
}
function xsdAttributeToXptcAttribute(xsdAttribute: XsdAttribute, location: string): XptcAttribute {
return {
name: xsdAttribute["@_name"],
localTypeRef: xsdAttribute["@_type"],
isOptional: xsdAttribute["@_use"] === undefined || xsdAttribute["@_use"] === "optional",
simpleType: xsdAttribute["xsd:simpleType"]
? xsdSimpleTypeToXptcSimpleType(xsdAttribute["xsd:simpleType"], location, `${xsdAttribute["@_name"]}simpleType`)
: undefined,
};
}
function xsdComplexTypeToAnonymousXptcComplexType(
__ATTRIBUTE_GROUPS_BY_QNAME: Map<string, XptcTopLevelAttributeGroup>,
parentIdentifierForExtensionType: string,
xsdCt: XsdComplexType,
location: string,
element: string
): XptcComplexTypeAnonymous {
return {
type: "complex",
comment: "",
isMixed: xsdCt["@_mixed"] ?? false,
isSimpleContent: false, // No reason why an anonymous type couldn't be simpleContent... Could be implemented.
isAnonymous: true,
parentIdentifierForExtensionType,
forElementWithName: element,
needsExtensionType: !!xsdCt["xsd:anyAttribute"] || !!xsdCt["xsd:sequence"]?.["xsd:any"],
declaredAtRelativeLocation: location,
childOf: xsdCt["xsd:complexContent"]?.["xsd:extension"]?.["@_base"],
elements: [
...(xsdCt["xsd:sequence"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(
__ATTRIBUTE_GROUPS_BY_QNAME,
`${parentIdentifierForExtensionType}__${element}`,
s,
location
)
),
...(xsdCt["xsd:complexContent"]?.["xsd:extension"]?.["xsd:sequence"]?.["xsd:element"] ?? []).map((s) =>
xsdElementToXptcElement(
__ATTRIBUTE_GROUPS_BY_QNAME,
`${parentIdentifierForExtensionType}__${element}`,
s,
location
)
),
],
attributes: [
...(xsdCt["xsd:attribute"] ?? []).map((a) => xsdAttributeToXptcAttribute(a, location)),
...(xsdCt["xsd:complexContent"]?.["xsd:extension"]?.["xsd:attribute"] ?? []).map((a) =>
xsdAttributeToXptcAttribute(a, location)
),
...(xsdCt["xsd:attributeGroup"] ?? []).flatMap(
(attrGroup) => __ATTRIBUTE_GROUPS_BY_QNAME.get(attrGroup["@_ref"])?.attributes ?? []
),
],
};
}
function getTsTypeBody(tsType: { name: string; annotation: string }): XptcMetaTypeProperty["typeBody"] {
return tsType.annotation.startsWith("xsd:") ? (tsTypeName) => `{ __$$text: ${tsTypeName} }` : undefined;
}