Skip to content

Commit

Permalink
Simplify and comment
Browse files Browse the repository at this point in the history
  • Loading branch information
cromoteca committed Sep 22, 2023
1 parent d99f209 commit 86a34be
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,23 @@
import java.util.function.Function;
import java.util.stream.Stream;

/**
* This plugin adds support for {@code @JsonTypeInfo} and {@code @JsonSubTypes}.
*/
public final class SubTypesPlugin extends AbstractPlugin<PluginConfiguration> {
@Override
public void enter(NodePath<?> nodePath) {
}

@Override
public void exit(NodePath<?> nodePath) {
// deal with the union nodes, which does not correspond to an existing class, but express the union of all the @JsonSubTypes
if (nodePath.getNode() instanceof UnionNode) {
var unionNode = (UnionNode) nodePath.getNode();
var cls = (Class<?>) unionNode.getSource().get();

// verify that the class has a @JsonTypeInfo annotation
// and then add all the @JsonSubTypes to the schema as a `oneOf`
if (cls.getAnnotationsByType(JsonTypeInfo.class).length > 0) {
var schema = (Schema<?>) unionNode.getTarget();
getJsonSubTypes(cls).map(JsonSubTypes.Type::value)
Expand All @@ -53,11 +59,13 @@ public void exit(NodePath<?> nodePath) {
});
}

// attach the schema to the openapi
EntityPlugin.attachSchemaWithNameToOpenApi(unionNode.getTarget(),
cls.getName() + "Union",
(OpenAPI) nodePath.getParentPath().getNode().getTarget());
}

// entity nodes whose superclass has a @JsonSubTypes annotation must have a @type property whose value comes from the annotation
if (nodePath.getNode() instanceof EntityNode) {
var entityNode = (EntityNode) nodePath.getNode();
var cls = (Class<?>) entityNode.getSource().get();
Expand Down Expand Up @@ -104,10 +112,12 @@ public NodeDependencies scan(@Nonnull NodeDependencies nodeDependencies) {
return nodeDependencies;
}

// all types mentioned in @JsonSubTypes must be parsed, even if they are not used directly
Class<?> refClass = (Class<?>) ref.getClassInfo().get();
var subTypes = getJsonSubTypes(refClass).map(JsonSubTypes.Type::value)
.map(ClassInfoModel::of).<Node<?, ?>> map(EntityNode::of);

// create a union node for classes annotated with @JsonTypeInfo
if (refClass.getAnnotationsByType(JsonTypeInfo.class).length > 0) {
var unionType = UnionNode.of(ref.getClassInfo());
subTypes = Stream.concat(Stream.of(unionType), subTypes);
Expand All @@ -123,6 +133,9 @@ private static Stream<JsonSubTypes.Type> getJsonSubTypes(Class<?> cls) {
.map(JsonSubTypes::value).stream().flatMap(Arrays::stream);
}

/**
* A node that represents the union of all the mentioned subclasses of a class annotated with {@code @JsonSubTypes}.
*/
public static class UnionNode
extends AbstractNode<ClassInfoModel, Schema<?>> {
private UnionNode(@Nonnull ClassInfoModel source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,17 @@ export class ModelFixProcessor {

process(): ts.SourceFile {
const statements = this.#source.statements.map((statement) => {
if (statement.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = statement as ClassDeclaration;
const members = classDeclaration.members.filter((member) => {
if (
member.kind === ts.SyntaxKind.GetAccessor &&
propertyNameToString((member as GetAccessorDeclaration).name) === '@type'
) {
return false;
}

return true;
});
// filter out the @type property from all models
if (ts.isClassDeclaration(statement)) {
const members = statement.members.filter(
(member) => !(ts.isGetAccessor(member) && propertyNameToString(member.name) === '@type'),
);

return ts.factory.createClassDeclaration(
classDeclaration.modifiers,
classDeclaration.name,
classDeclaration.typeParameters,
classDeclaration.heritageClauses,
statement.modifiers,
statement.name,
statement.typeParameters,
statement.heritageClauses,
members,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@ export class SubTypesProcessor {

process(): ts.SourceFile {
const { exports, imports, paths } = this.#dependencies;

// import all sub types and return them
const subTypes = this.#oneOf.map((schema) => {
const path = paths.createRelativePath(convertReferenceSchemaToPath(schema));
const subType = convertReferenceSchemaToSpecifier(schema);
return imports.default.add(path, subType, true);
});

// create a union type from the sub types
const union = ts.factory.createUnionTypeNode(
subTypes.map((subType) => ts.factory.createTypeReferenceNode(subType)),
);

// create the statement
const { fileName, statements } = this.#source;
const unionTypeName = `${simplifyFullyQualifiedName(this.#typeName)}`;
const unionIdentifier = ts.factory.createIdentifier(unionTypeName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { dirname } from 'path/posix';
import { convertFullyQualifiedNameToRelativePath } from '@hilla/generator-typescript-core/utils.js';
import createSourceFile from '@hilla/generator-typescript-utils/createSourceFile.js';
import DependencyManager from '@hilla/generator-typescript-utils/dependencies/DependencyManager.js';
import PathManager from '@hilla/generator-typescript-utils/dependencies/PathManager.js';
import ts from 'typescript';

function propertyNameToString(node: ts.PropertyName): string | null {
Expand All @@ -13,41 +9,27 @@ function propertyNameToString(node: ts.PropertyName): string | null {
}

export class TypeFixProcessor {
readonly #typeName: string;
readonly #source: ts.SourceFile;
readonly #typeValue: string;
readonly #dependencies;

constructor(typeName: string, source: ts.SourceFile, typeValue: string) {
this.#typeName = typeName;
constructor(source: ts.SourceFile, typeValue: string) {
this.#source = source;
this.#typeValue = typeValue;
this.#dependencies = new DependencyManager(
new PathManager({ extension: '.js', relativeTo: dirname(source.fileName) }),
);
}

process(): ts.SourceFile {
const { paths } = this.#dependencies;
const path = paths.createRelativePath(convertFullyQualifiedNameToRelativePath(this.#typeName));
const statements = this.#source.statements.map((statement) => {
if (
ts.isImportDeclaration(statement) &&
ts.isStringLiteral(statement.moduleSpecifier) &&
propertyNameToString(statement.moduleSpecifier) === path
) {
return undefined;
} else if (ts.isInterfaceDeclaration(statement)) {
// search in the interface definition
if (ts.isInterfaceDeclaration(statement)) {
const members = statement.members.map((member) => {
if (ts.isPropertySignature(member)) {
if (propertyNameToString(member.name) === '@type') {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createStringLiteral('@type'),
undefined,
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(this.#typeValue)),
);
}
// search for the @type property and replace it with a quoted string
if (ts.isPropertySignature(member) && propertyNameToString(member.name) === '@type') {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createStringLiteral('@type'),
undefined,
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(this.#typeValue)),
);
}

return member;
Expand All @@ -65,6 +47,6 @@ export class TypeFixProcessor {
return statement;
});

return createSourceFile(statements.filter((s) => s !== undefined) as ts.Statement[], this.#source.fileName);
return createSourceFile(statements, this.#source.fileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ export default class SubTypesPlugin extends Plugin {
}

Object.entries(components).forEach(([baseKey, baseComponent]) => {
// search for components with oneOf: those are union types
if ('oneOf' in baseComponent && Array.isArray(baseComponent.oneOf)) {
const fn = `${convertFullyQualifiedNameToRelativePath(baseKey)}.ts`;
const source = sources.find(({ fileName }) => fileName === fn)!;
// replace the (empty) source with a newly-generated one
const newSource = new SubTypesProcessor(baseKey, source, baseComponent.oneOf).process();
sources.splice(sources.indexOf(source), 1, newSource);

// mentioned types in the oneOf need to be fixed as well
baseComponent.oneOf.forEach((schema) => {
if ('$ref' in schema) {
const path = (schema as ReferenceSchema).$ref;
Expand All @@ -39,9 +42,11 @@ export default class SubTypesPlugin extends Plugin {
const typeValue = s.properties['@type'].example as string;
const subFn = `${convertFullyQualifiedNameToRelativePath(subKey)}.ts`;
const subSource = sources.find(({ fileName }) => fileName === subFn)!;
const fixedSource = new TypeFixProcessor(baseKey, subSource, typeValue).process();
// fix the source to replace the @type property name with a quoted string
const fixedSource = new TypeFixProcessor(subSource, typeValue).process();
sources.splice(sources.indexOf(subSource), 1, fixedSource);

// fix the model to remove the @type property
const modelFn = `${convertFullyQualifiedNameToRelativePath(subKey)}Model.ts`;
const modelSource = sources.find(({ fileName }) => fileName === modelFn)!;
const fixedModelSource = new ModelFixProcessor(modelSource).process();
Expand All @@ -53,6 +58,7 @@ export default class SubTypesPlugin extends Plugin {
}
});

// remove the union type model file
const unionFn = `${convertFullyQualifiedNameToRelativePath(baseKey)}Union.ts`;
const unionSource = sources.find(({ fileName }) => fileName === unionFn)!;
sources.splice(sources.indexOf(unionSource), 1);
Expand Down

0 comments on commit 86a34be

Please sign in to comment.