UNPKG

schema-dts-gen

Version:

Generate TypeScript Definitions for Schema.org Schema

418 lines 16.5 kB
/** * 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 { 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() { return this._parents .map(p => p.baseName()) .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; } get comment() { if (!this.deprecated) return this._comment; const deprecated = `@deprecated Use ${this.supersededBy() .map(c => c.className()) .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() { // If Skip Base, we use the parent type instead. if (this.skipBase()) { if (this.namedParents().length === 0) return undefined; assert(this.namedParents().length === 1); return this.namedParents()[0]; } return toClassName(this.subject) + 'Base'; } leafName() { // If the leaf has no node type and doesn't refer to any parent, // skip defining it. if (!this.isNodeType() && this.namedParents().length === 0) { return undefined; } return toClassName(this.subject) + 'Leaf'; } className() { return toClassName(this.subject); } 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()} 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().length === 1 && this._props.size === 0; } baseDecl(context, properties) { if (this.skipBase()) { return undefined; } const baseName = this.baseName(); assert(baseName, 'If a baseNode is defined, baseName must be defined.'); const parentTypes = this.namedParents().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(); if (!leafName) return undefined; const baseName = this.baseName(); // Leaf is missing if !isNodeType || namedParents.length == 0 // Base is missing if !isNodeType && namedParents.length == 0 && numProps == 0 // // so when "Leaf" is present, Base will always be present. assert(baseName, 'Expect baseName to exist when leafName exists.'); return factory.createInterfaceDeclaration( /*modifiers=*/ [], leafName, /*typeParameters=*/ [], /*heritage=*/ [ factory.createHeritageClause(SyntaxKind.ExtendsKeyword, [ factory.createExpressionWithTypeArguments(factory.createIdentifier(baseName), /*typeArguments=*/ []), ]), ], /*members=*/ [new TypeProperty(this.subject).toNode(context)]); } nonEnumType(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(), /*typeArguments=*/ child.typeArguments(this.typeParameters()))); // A type can have a valid typedef, add that if so. children.push(...this.typedefs); const upRef = this.leafName() || this.baseName(); const typeArgs = this.leafName() ? this.leafTypeArguments() : []; return upRef ? [factory.createTypeReferenceNode(upRef, typeArgs), ...children] : children; } totalType(context, skipDeprecated) { return typeUnion(...this.enums().flatMap(e => e.toTypeLiteral(context)), ...this.nonEnumType(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); const declaration = withComments(this.comment, factory.createTypeAliasDeclaration(factory.createModifiersFromModifierFlags(ModifierFlags.Export), this.className(), this.typeParameters(), typeValue)); // Guide to Code Generated: // // Base: Always There -----------------------// // type XyzBase = (Parents) & { // ... props; // }; // // Leaf: // export type XyzLeaf = 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(); const baseName = this.baseName(); assert(leafName, 'Role must have Leaf Name'); assert(baseName, 'Role must have Base Name.'); return factory.createTypeAliasDeclaration( /*modifiers=*/ [], 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(baseName), 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() { this.wk.sort(Sort); return [ withComments(this.comment, 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