Skip to content

Commit

Permalink
Support https://schema.org/Role properly.
Browse files Browse the repository at this point in the history
  • Loading branch information
Eyas committed Aug 7, 2021
1 parent f4c0856 commit 5666b6f
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 66 deletions.
19 changes: 16 additions & 3 deletions src/transform/toClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@
import {Log} from '../logging';
import {ObjectPredicate, Topic, TypedTopic} from '../triples/triple';
import {UrlNode} from '../triples/types';
import {IsNamedClass, IsWellKnown, IsDataType} from '../triples/wellKnown';
import {AliasBuiltin, Class, ClassMap, DataTypeUnion} from '../ts/class';
import {IsNamedClass, IsDataType, ClassIsDataType} from '../triples/wellKnown';
import {
AliasBuiltin,
Class,
ClassMap,
DataTypeUnion,
RoleBuiltin,
} from '../ts/class';
import {assert} from '../util/assert';

function toClass(cls: Class, topic: Topic, map: ClassMap): Class {
Expand Down Expand Up @@ -51,6 +57,11 @@ const wellKnownTypes = [
new AliasBuiltin('http://schema.org/Date', AliasBuiltin.Alias('string')),
new AliasBuiltin('http://schema.org/DateTime', AliasBuiltin.Alias('string')),
new AliasBuiltin('http://schema.org/Boolean', AliasBuiltin.Alias('boolean')),
new RoleBuiltin(UrlNode.Parse('http://schema.org/Role')),
new RoleBuiltin(UrlNode.Parse('http://schema.org/OrganizationRole')),
new RoleBuiltin(UrlNode.Parse('http://schema.org/EmployeeRole')),
new RoleBuiltin(UrlNode.Parse('http://schema.org/LinkRole')),
new RoleBuiltin(UrlNode.Parse('http://schema.org/PerformanceRole')),
];

// Should we allow 'string' to be a valid type for all values of this type?
Expand Down Expand Up @@ -78,12 +89,14 @@ function ForwardDeclareClasses(topics: readonly TypedTopic[]): ClassMap {
} else if (!IsNamedClass(topic)) continue;

const wk = wellKnownTypes.find(wk => wk.subject.equivTo(topic.Subject));
if (IsWellKnown(topic)) {
if (ClassIsDataType(topic)) {
assert(
wk,
`${topic.Subject.toString()} must have corresponding well-known type.`
);
dataType.wk.push(wk);

wk['_isDataType'] = true;
}

const cls = wk || new Class(topic.Subject);
Expand Down
12 changes: 10 additions & 2 deletions src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export async function WriteDeclarations(
ProcessEnums(topics, classes);
const sorted = Array.from(classes.values()).sort(Sort);

const properties = {
hasRole: !!(
classes.get('http://schema.org/Role') ||
classes.get('https://schema.org/Role')
),
skipDeprecatedProperties: !includeDeprecated,
};

const source = createSourceFile(
'result.ts',
'',
Expand All @@ -68,7 +76,7 @@ export async function WriteDeclarations(
);
const printer = createPrinter({newLine: NewLineKind.LineFeed});

for (const helperType of HelperTypes(context)) {
for (const helperType of HelperTypes(context, properties)) {
write(printer.printNode(EmitHint.Unspecified, helperType, source));
write('\n');
}
Expand All @@ -77,7 +85,7 @@ export async function WriteDeclarations(
for (const cls of sorted) {
if (cls.deprecated && !includeDeprecated) continue;

for (const node of cls.toNode(context, !includeDeprecated)) {
for (const node of cls.toNode(context, properties)) {
const result = printer.printNode(EmitHint.Unspecified, node, source);
await write(result);
await write('\n');
Expand Down
2 changes: 1 addition & 1 deletion src/triples/wellKnown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function IsDataType(t: TTypeName): boolean {
}

/** Returns true iff a Topic represents a DataType. */
export function IsWellKnown(topic: TypedTopic): boolean {
export function ClassIsDataType(topic: TypedTopic): boolean {
if (topic.types.some(IsDataType)) return true;
return false;
}
Expand Down
170 changes: 143 additions & 27 deletions src/ts/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TypeNode,
SyntaxKind,
InterfaceDeclaration,
TypeParameterDeclaration,
} from 'typescript';

import {Log} from '../logging';
Expand All @@ -31,6 +32,9 @@ import {
GetSubClassOf,
IsSupersededBy,
IsClassType,
IsDataType,
IsType,
IsTypeName,
} from '../triples/wellKnown';

import {Context} from './context';
Expand All @@ -43,6 +47,7 @@ import {assert} from '../util/assert';
import {IdReferenceName} from './helper_types';

import {shim as shimFlatMap} from 'array.prototype.flatmap';
import {typeUnion} from './util/union';
shimFlatMap();

/** Maps fully qualified IDs of each Class to the class itself. */
Expand All @@ -62,6 +67,7 @@ export type ClassMap = Map<string, Class>;
export class Class {
private _comment?: string;
private _typedefs: TypeNode[] = [];
private _isDataType = false;
private readonly children: Class[] = [];
private readonly _parents: Class[] = [];
private readonly _props: Set<Property> = new Set();
Expand All @@ -78,7 +84,7 @@ export class Class {
}

isNodeType(): boolean {
if (this instanceof Builtin) return false;
if (this._isDataType) return false;
if (this._props.size > 0) return true;

return this.allParents().every(n => n.isNodeType());
Expand Down Expand Up @@ -126,7 +132,7 @@ export class Class {
);
}

private baseName(): string | undefined {
protected baseName(): string | undefined {
// If Skip Base, we use the parent type instead.
if (this.skipBase()) {
if (this.namedParents().length === 0) return undefined;
Expand All @@ -137,7 +143,7 @@ export class Class {
return toClassName(this.subject) + 'Base';
}

private leafName(): string | undefined {
protected leafName(): string | undefined {
// If the leaf has no node type and doesn't refer to any parent,
// skip defining it.
if (!this.isNodeType() && this.namedParents().length === 0) {
Expand Down Expand Up @@ -219,8 +225,8 @@ export class Class {
}

private baseDecl(
skipDeprecatedProperties: boolean,
context: Context
context: Context,
properties: {skipDeprecatedProperties: boolean; hasRole: boolean}
): InterfaceDeclaration | undefined {
if (this.skipBase()) {
return undefined;
Expand Down Expand Up @@ -251,8 +257,10 @@ export class Class {
);

const members = this.properties()
.filter(property => !property.deprecated || !skipDeprecatedProperties)
.map(prop => prop.toNode(context));
.filter(
property => !property.deprecated || !properties.skipDeprecatedProperties
)
.map(prop => prop.toNode(context, properties));

return factory.createInterfaceDeclaration(
/*decorators=*/ [],
Expand All @@ -264,7 +272,7 @@ export class Class {
);
}

protected leafDecl(context: Context): InterfaceDeclaration | undefined {
protected leafDecl(context: Context): DeclarationStatement | undefined {
const leafName = this.leafName();
if (!leafName) return undefined;

Expand All @@ -274,10 +282,6 @@ export class Class {
//
// so when "Leaf" is present, Base will always be present.
assert(baseName, 'Expect baseName to exist when leafName exists.');
const baseTypeReference = factory.createTypeReferenceNode(
baseName,
/*typeArguments=*/ []
);

return factory.createInterfaceDeclaration(
/*decorators=*/ [],
Expand All @@ -303,41 +307,59 @@ export class Class {
.map(child =>
factory.createTypeReferenceNode(
child.className(),
/*typeArguments=*/ []
/*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]
? [factory.createTypeReferenceNode(upRef, typeArgs), ...children]
: children;
}

private totalType(context: Context, skipDeprecated: boolean): TypeNode {
const isEnum = this._enums.size > 0;
return typeUnion(
...this.enums().flatMap(e => e.toTypeLiteral(context)),
...this.nonEnumType(skipDeprecated)
);
}

if (isEnum) {
return factory.createUnionTypeNode([
...this.enums().flatMap(e => e.toTypeLiteral(context)),
...this.nonEnumType(skipDeprecated),
]);
} else {
return factory.createUnionTypeNode(this.nonEnumType(skipDeprecated));
}
/** Generic Type Parameter Declarations for this class */
protected typeParameters(): readonly TypeParameterDeclaration[] {
return [];
}

/** Generic Types to pass to this total type when referencing it. */
protected typeArguments(
available: readonly TypeParameterDeclaration[]
): readonly TypeNode[] {
return [];
}

toNode(context: Context, skipDeprecated: boolean): readonly Statement[] {
const typeValue: TypeNode = this.totalType(context, skipDeprecated);
protected leafTypeArguments(): readonly TypeNode[] {
return [];
}

toNode(
context: Context,
properties: {skipDeprecatedProperties: boolean; hasRole: boolean}
): readonly Statement[] {
const typeValue: TypeNode = this.totalType(
context,
properties.skipDeprecatedProperties
);
const declaration = withComments(
this.comment,
factory.createTypeAliasDeclaration(
/* decorators = */ [],
factory.createModifiersFromModifierFlags(ModifierFlags.Export),
this.className(),
[],
this.typeParameters(),
typeValue
)
);
Expand All @@ -357,7 +379,7 @@ export class Class {
// |Child1|Child2|... // Child Piece: Optional.
// //-------------------------------------------//
return arrayOf<Statement>(
this.baseDecl(skipDeprecated, context),
this.baseDecl(context, properties),
this.leafDecl(context),
declaration
);
Expand Down Expand Up @@ -396,6 +418,100 @@ export class AliasBuiltin extends Builtin {
}
}

export class RoleBuiltin extends Builtin {
private static readonly kContentTypename = 'TContent';
private static readonly kPropertyTypename = 'TProperty';

protected typeParameters(): readonly TypeParameterDeclaration[] {
return [
factory.createTypeParameterDeclaration(
/*name=*/ RoleBuiltin.kContentTypename,
/*constraint=*/ undefined,
/*default=*/ factory.createTypeReferenceNode('never')
),
factory.createTypeParameterDeclaration(
/*name=*/ RoleBuiltin.kPropertyTypename,
/*constraint=*/ factory.createTypeReferenceNode('string'),
/*default=*/ factory.createTypeReferenceNode('never')
),
];
}

protected leafTypeArguments(): readonly TypeNode[] {
return [
factory.createTypeReferenceNode(RoleBuiltin.kContentTypename),
factory.createTypeReferenceNode(RoleBuiltin.kPropertyTypename),
];
}

protected typeArguments(
availableParams: readonly TypeParameterDeclaration[]
): TypeNode[] {
const hasTContent = !!availableParams.find(
param => param.name.escapedText === RoleBuiltin.kContentTypename
);
const hasTProperty = !!availableParams.find(
param => param.name.escapedText === 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),
]
: [];
}

protected leafDecl(context: Context): DeclarationStatement {
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(
/*decorators=*/ [],
/*modifiers=*/ [],
leafName,
/*typeParameters=*/ [
factory.createTypeParameterDeclaration(
/*name=*/ RoleBuiltin.kContentTypename,
/*constraint=*/ undefined
),
factory.createTypeParameterDeclaration(
/*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(
'key',
/*constraint=*/ factory.createTypeReferenceNode(
RoleBuiltin.kPropertyTypename
)
),
/*nameType=*/ undefined,
/*questionToken=*/ undefined,
/*type=*/ factory.createTypeReferenceNode(
RoleBuiltin.kContentTypename
)
),
])
);
}
}

export class DataTypeUnion extends Builtin {
constructor(url: string, readonly wk: Builtin[]) {
super(UrlNode.Parse(url));
Expand Down
Loading

0 comments on commit 5666b6f

Please sign in to comment.