schema-dts-gen
Version:
Generate TypeScript Definitions for Schema.org Schema
425 lines • 17.2 kB
JavaScript
/**
* Copyright 2023 Google LLC
*
* Licensed 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
*
* https://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 ts from 'typescript';
const { factory, ModifierFlags, SyntaxKind } = ts;
import { Log } from '../logging/index.js';
import { namedPortion, namedPortionOrEmpty } from '../triples/term_utils.js';
import { GetComment, GetSubClassOf, IsSupersededBy, IsClassType, } from '../triples/wellKnown.js';
import { Context } from './context.js';
import { TypeProperty } from './property.js';
import { arrayOf } from './util/arrayof.js';
import { appendParagraph, withComments } from './util/comments.js';
import { toClassName } from './util/names.js';
import { assert } from '../util/assert.js';
import { IdReferenceName } from './helper_types.js';
import { typeUnion } from './util/union.js';
/**
* Represents a "Class" in Schema.org, except in cases where it is better
* described by Builtin (i.e. is a DataType).
*
* In TypeScript, this corresponds to a collection of declarations:
* 1. If the class has enum values, an Enum declaration.
* 2. If the class has properties, the properties in an object literal.
* 3. If the class has children,
* a type union over all children.
* otherwise, a "type" property.
*/
export class Class {
allParents() {
return this._parents;
}
namedParents(context) {
return this._parents
.map(p => p.baseName(context))
.filter((name) => !!name);
}
isNodeType() {
if (this._isDataType)
return false;
if (this._props.size > 0)
return true;
return this.allParents().every(n => n.isNodeType());
}
get deprecated() {
return this._supersededBy.size > 0;
}
comment(context) {
if (!this.deprecated)
return this._comment;
const deprecated = `@deprecated Use ${this.supersededBy()
.map(c => c.className(context))
.join(' or ')} instead.`;
return appendParagraph(this._comment, deprecated);
}
get typedefs() {
const parents = this.allParents().flatMap(p => p.typedefs);
return Array.from(new Map([...this._typedefs, ...parents].map(t => [JSON.stringify(t), t])))
.sort(([key1], [key2]) => key1.localeCompare(key2))
.map(([_, value]) => value);
}
properties() {
return Array.from(this._props.values()).sort((a, b) => CompareKeys(a.key, b.key));
}
supersededBy() {
return Array.from(this._supersededBy).sort((a, b) => CompareKeys(a.subject, b.subject));
}
enums() {
return Array.from(this._enums).sort((a, b) => CompareKeys(a.value, b.value));
}
baseName(context) {
// If Skip Base, we use the parent type instead.
if (this.skipBase()) {
if (this.namedParents(context).length === 0)
return undefined;
assert(this.namedParents(context).length === 1);
return this.namedParents(context)[0];
}
return toClassName(this.subject, context) + 'Base';
}
leafName(context) {
// If the leaf has no node type and doesn't refer to any parent,
// skip defining it.
if (!this.isNodeType() && this.namedParents(context).length === 0) {
return undefined;
}
return toClassName(this.subject, context) + 'Leaf';
}
className(context) {
return toClassName(this.subject, context);
}
constructor(subject) {
this.subject = subject;
this._typedefs = [];
this._isDataType = false;
this._explicitlyMarkedAsClass = false;
this.children = [];
this._parents = [];
this._props = new Set();
this._enums = new Set();
this._supersededBy = new Set();
}
add(value, classMap) {
const c = GetComment(value);
if (c) {
if (this._comment) {
Log(`Duplicate comments provided on class ${this.subject.id}. It will be overwritten.`);
}
this._comment = c.comment;
return true;
}
const s = GetSubClassOf(value);
if (s) {
// DataType subclasses rdfs:Class (since it too is a 'meta' type).
// We don't represent this well right now, but we want to skip it.
if (IsClassType(s.subClassOf))
return false;
const parentClass = classMap.get(s.subClassOf.id);
if (parentClass) {
this._parents.push(parentClass);
parentClass.children.push(this);
}
else {
throw new Error(`Couldn't find parent of ${this.subject.value}, ${s.subClassOf.value} (available: ${Array.from(classMap.keys()).join(', ')})`);
}
return true;
}
if (IsSupersededBy(value.predicate)) {
const supersededBy = classMap.get(value.object.value);
if (!supersededBy) {
throw new Error(`Couldn't find class ${value.object.value}, which supersedes class ${this.subject.value}`);
}
this._supersededBy.add(supersededBy);
return true;
}
return false;
}
addTypedef(typedef) {
this._typedefs.push(typedef);
}
markAsExplicitClass() {
this._explicitlyMarkedAsClass = true;
}
isMarkedAsClass(visited) {
if (visited.has(this))
return false;
visited.add(this);
return (this._explicitlyMarkedAsClass ||
this._parents.some(p => p.isMarkedAsClass(visited)));
}
validateClass() {
if (!this.isMarkedAsClass(new WeakSet())) {
throw new Error(`Class ${this.className(new Context())} is not marked as an rdfs:Class, and neither are any of its parents.`);
}
}
addProp(p) {
this._props.add(p);
}
addEnum(e) {
this._enums.add(e);
}
skipBase() {
if (!this.isNodeType())
return true;
return (this.namedParents(new Context()).length === 1 && this._props.size === 0);
}
baseDecl(context, properties) {
if (this.skipBase()) {
return undefined;
}
const baseName = this.baseName(context);
assert(baseName, 'If a baseNode is defined, baseName must be defined.');
const parentTypes = this.namedParents(context).map(p => factory.createExpressionWithTypeArguments(factory.createIdentifier(p), []));
const heritage = factory.createHeritageClause(SyntaxKind.ExtendsKeyword, parentTypes.length === 0
? [
factory.createExpressionWithTypeArguments(factory.createIdentifier('Partial'),
/*typeArguments=*/ [
factory.createTypeReferenceNode(IdReferenceName,
/*typeArguments=*/ []),
]),
]
: parentTypes);
const members = this.properties()
.filter(property => !property.deprecated || !properties.skipDeprecatedProperties)
.map(prop => prop.toNode(context, properties));
return factory.createInterfaceDeclaration(
/*modifiers=*/ [], baseName,
/*typeParameters=*/ [],
/*heritageClause=*/ [heritage],
/*members=*/ members);
}
leafDecl(context) {
const leafName = this.leafName(context);
if (!leafName)
return undefined;
const baseName = this.baseName(context);
// Leaf is missing if !isNodeType && namedParents.length == 0
// Base is missing if !isNodeType || (namedParents.length == 1 && numProps == 0)
//
// so when "Leaf" is present, Base will always be present.
assert(baseName, 'Expect baseName to exist when leafName exists.');
return factory.createInterfaceDeclaration(factory.createModifiersFromModifierFlags(ModifierFlags.Export), leafName,
/*typeParameters=*/ [],
/*heritage=*/ [
factory.createHeritageClause(SyntaxKind.ExtendsKeyword, [
factory.createExpressionWithTypeArguments(factory.createIdentifier(baseName),
/*typeArguments=*/ []),
]),
],
/*members=*/ [new TypeProperty(this.subject).toNode(context)]);
}
nonEnumType(context, skipDeprecated) {
this.children.sort((a, b) => CompareKeys(a.subject, b.subject));
const children = this.children
.filter(child => !(child.deprecated && skipDeprecated))
.map(child => factory.createTypeReferenceNode(child.className(context),
/*typeArguments=*/ child.typeArguments(this.typeParameters())));
// A type can have a valid typedef, add that if so.
children.push(...this.typedefs);
const upRef = this.leafName(context) || this.baseName(context);
const typeArgs = this.leafName(context) ? this.leafTypeArguments() : [];
return upRef
? [factory.createTypeReferenceNode(upRef, typeArgs), ...children]
: children;
}
totalType(context, skipDeprecated) {
return typeUnion(...this.enums().flatMap(e => e.toTypeLiteral(context)), ...this.nonEnumType(context, skipDeprecated));
}
/** Generic Type Parameter Declarations for this class */
typeParameters() {
return [];
}
/** Generic Types to pass to this total type when referencing it. */
typeArguments(_) {
return [];
}
leafTypeArguments() {
return [];
}
toNode(context, properties) {
const typeValue = this.totalType(context, properties.skipDeprecatedProperties);
if (typeValue.kind === SyntaxKind.NeverKeyword) {
return [];
}
const declaration = withComments(this.comment(context), factory.createTypeAliasDeclaration(factory.createModifiersFromModifierFlags(ModifierFlags.Export), this.className(context), this.typeParameters(), typeValue));
// Guide to Code Generated:
// // Base: Always There -----------------------//
// type XyzBase = (Parents) & {
// ... props;
// };
// // Leaf:
// export interface XyzLeaf extends XyzBase {
// '@type': 'Xyz'
// }
// // Complete Type ----------------------------//
// export type Xyz = "Enum1"|"Enum2"|... // Enum Piece: Optional.
// |XyzLeaf // 'Leaf' Piece.
// |Child1|Child2|... // Child Piece: Optional.
// //-------------------------------------------//
return arrayOf(this.baseDecl(context, properties), this.leafDecl(context), declaration);
}
}
/**
* Represents a DataType.
*/
export class Builtin extends Class {
constructor(subject) {
super(subject);
this.markAsExplicitClass();
}
}
/**
* A "Native" Schema.org object that is best represented
* in JSON-LD and JavaScript as a typedef to a native type.
*/
export class AliasBuiltin extends Builtin {
constructor(subject, ...equivTo) {
super(subject);
for (const t of equivTo)
this.addTypedef(t);
}
static Alias(equivTo) {
return factory.createTypeReferenceNode(equivTo, /*typeArgs=*/ []);
}
static NumberStringLiteral() {
return factory.createTemplateLiteralType(factory.createTemplateHead(/* text= */ ''), [
factory.createTemplateLiteralTypeSpan(factory.createTypeReferenceNode('number'), factory.createTemplateTail(/* text= */ '')),
]);
}
}
export class RoleBuiltin extends Builtin {
typeParameters() {
return [
factory.createTypeParameterDeclaration(
/*modifiers=*/ [],
/*name=*/ RoleBuiltin.kContentTypename,
/*constraint=*/ undefined,
/*default=*/ factory.createTypeReferenceNode('never')),
factory.createTypeParameterDeclaration(
/*modifiers=*/ [],
/*name=*/ RoleBuiltin.kPropertyTypename,
/*constraint=*/ factory.createTypeReferenceNode('string'),
/*default=*/ factory.createTypeReferenceNode('never')),
];
}
leafTypeArguments() {
return [
factory.createTypeReferenceNode(RoleBuiltin.kContentTypename),
factory.createTypeReferenceNode(RoleBuiltin.kPropertyTypename),
];
}
typeArguments(availableParams) {
const hasTContent = !!availableParams.find(param => param.name.text === RoleBuiltin.kContentTypename);
const hasTProperty = !!availableParams.find(param => param.name.text === RoleBuiltin.kPropertyTypename);
assert((hasTProperty && hasTContent) || (!hasTProperty && !hasTContent), `hasTcontent and hasTProperty should be both true or both false, but saw (${hasTContent}, ${hasTProperty})`);
return hasTContent && hasTProperty
? [
factory.createTypeReferenceNode(RoleBuiltin.kContentTypename),
factory.createTypeReferenceNode(RoleBuiltin.kPropertyTypename),
]
: [];
}
leafDecl(context) {
const leafName = this.leafName(context);
const baseName = this.baseName(context);
assert(leafName, 'Role must have Leaf Name');
assert(baseName, 'Role must have Base Name.');
return factory.createTypeAliasDeclaration(factory.createModifiersFromModifierFlags(ModifierFlags.Export), leafName,
/*typeParameters=*/ [
factory.createTypeParameterDeclaration(
/*modifiers=*/ [],
/*name=*/ RoleBuiltin.kContentTypename,
/*constraint=*/ undefined),
factory.createTypeParameterDeclaration(
/*modifiers=*/ [],
/*name=*/ RoleBuiltin.kPropertyTypename,
/*constraint=*/ factory.createTypeReferenceNode('string')),
],
/*type=*/
factory.createIntersectionTypeNode([
factory.createTypeReferenceNode(
/*typeName=*/ 'Omit',
/*typeArguments=*/ [
factory.createTypeReferenceNode(baseName),
factory.createTypeReferenceNode(RoleBuiltin.kPropertyTypename),
]),
factory.createTypeLiteralNode([
new TypeProperty(this.subject).toNode(context),
]),
factory.createMappedTypeNode(
/*initialToken=*/ undefined,
/*typeParameter=*/ factory.createTypeParameterDeclaration(
/*modifiers=*/ [], 'key',
/*constraint=*/ factory.createTypeReferenceNode(RoleBuiltin.kPropertyTypename)),
/*nameType=*/ undefined,
/*questionToken=*/ undefined,
/*type=*/ factory.createTypeReferenceNode(RoleBuiltin.kContentTypename),
/*members=*/ undefined),
]));
}
}
RoleBuiltin.kContentTypename = 'TContent';
RoleBuiltin.kPropertyTypename = 'TProperty';
export class DataTypeUnion extends Builtin {
constructor(subject, wk) {
super(subject);
this.wk = wk;
}
toNode(context) {
this.wk.sort(Sort);
return [
withComments(this.comment(context), factory.createTypeAliasDeclaration(factory.createModifiersFromModifierFlags(ModifierFlags.Export), namedPortion(this.subject),
/*typeParameters=*/ [], factory.createUnionTypeNode(this.wk.map(wk => factory.createTypeReferenceNode(namedPortion(wk.subject),
/*typeArguments=*/ []))))),
];
}
}
/**
* Defines a Sort order between Class declarations.
*
* DataTypes come first, next the 'DataType' union itself, followed by all
* regular classes. Within each group, class names are ordered alphabetically in
* UTF-16 code units order.
*/
export function Sort(a, b) {
if (a instanceof Builtin && !(a instanceof DataTypeUnion)) {
if (b instanceof Builtin && !(b instanceof DataTypeUnion)) {
return CompareKeys(a.subject, b.subject);
}
else {
return -1;
}
}
else if (b instanceof Builtin && !(b instanceof DataTypeUnion)) {
return +1;
}
else if (a instanceof DataTypeUnion) {
return b instanceof DataTypeUnion ? 0 : -1;
}
else if (b instanceof DataTypeUnion) {
// If we are here, a is never a DataTypeUnion.
return +1;
}
else {
return CompareKeys(a.subject, b.subject);
}
}
function CompareKeys(a, b) {
const byName = (namedPortionOrEmpty(a) || '').localeCompare(namedPortionOrEmpty(b) || '');
if (byName !== 0)
return byName;
return a.id.localeCompare(b.id);
}
//# sourceMappingURL=class.js.map