Skip to content

Commit

Permalink
Tidy up code. Add valid / non valid predicates
Browse files Browse the repository at this point in the history
  • Loading branch information
Roaders committed Nov 22, 2024
1 parent f186fe7 commit 524834b
Show file tree
Hide file tree
Showing 2 changed files with 492 additions and 288 deletions.
281 changes: 180 additions & 101 deletions packages/fdc3-schema/code-generation/generate-type-predicates.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,136 @@
import { InterfaceDeclaration, MethodDeclaration, Project, SyntaxKind } from "ts-morph"
import { InterfaceDeclaration, MethodDeclaration, Project, SyntaxKind, TypeAliasDeclaration } from 'ts-morph';

// open a new project with just BrowserTypes as the only source file
// open a new project with just BrowserTypes as the only source file
const project = new Project();
const sourceFile = project.addSourceFileAtPath("./generated/api/BrowserTypes.ts");
const sourceFile = project.addSourceFileAtPath('./generated/api/BrowserTypes.ts');

const typeAliases = sourceFile.getChildrenOfKind(SyntaxKind.TypeAliasDeclaration);

/**
* We generate the union types and remove the existing interfaces first so that we are not left with a generated type predicate for the removed base interface
*/
writeMessageUnionTypes();
writeTypePredicates();

// get the types listed in the types union type
// i.e. look for: export type RequestMessageType = "addContextListenerRequest" | "whatever"
const requestMessageUnion = findUnionType("RequestMessageType");
if(requestMessageUnion != null){
// Write a union type of all interfaces that have a type that extends RequestMessageType
writeUnionType("AppRequestMessage", requestMessageUnion);
}

const responseMessageUnion = findUnionType("ResponseMessageType");
if(responseMessageUnion != null){
writeUnionType("AgentResponseMessage", responseMessageUnion);
}

const eventMessageUnion = findUnionType("EventMessageType");
if(eventMessageUnion != null){
writeUnionType("AgentEventMessage", eventMessageUnion);
}

// get a list of all conversion functions in the Convert class
const convert = sourceFile.getClass("Convert");
const convertFunctions = (convert?.getChildrenOfKind(SyntaxKind.MethodDeclaration) ?? []).filter(func => func.getReturnType().getText() === "string");


//get a list of all interfaces in the file
let messageInterfaces = sourceFile.getChildrenOfKind(SyntaxKind.InterfaceDeclaration);

// generate a list of Interfaces that have an associated conversion function
const matchedInterfaces = convertFunctions.map(func => {
const valueParameter = func.getParameter("value");
sourceFile.formatText();
project.saveSync();

const matchingInterface = messageInterfaces.find(interfaceNode => {
return valueParameter?.getType().getText(valueParameter) === interfaceNode.getName();
});

/**
* Replaces the existing interfaces AppRequestMessage, AgentResponseMessage and AgentEventMessage with unions of INterfaces instead of a base type
*/
function writeMessageUnionTypes() {
const typeAliases = sourceFile.getChildrenOfKind(SyntaxKind.TypeAliasDeclaration);

// get the types listed in the types union type
// i.e. look for: export type RequestMessageType = "addContextListenerRequest" | "whatever"
const requestMessageTypeUnion = findUnionType(typeAliases, 'RequestMessageType');
if (requestMessageTypeUnion != null) {
// Write a union type of all interfaces that have a type that extends RequestMessageType
// i.e. export type AppRequestMessage = AddContextListenerRequest | AddEventListenerRequest | AddIntentListenerRequest;
writeUnionType('AppRequestMessage', requestMessageTypeUnion);
}

if (matchingInterface != null) {
return { func, matchingInterface };
const responseMessageTypeUnion = findUnionType(typeAliases, 'ResponseMessageType');
if (responseMessageTypeUnion != null) {
writeUnionType('AgentResponseMessage', responseMessageTypeUnion);
}

return undefined;
}).filter(((value => value != null) as <T>(value: T | null | undefined) => value is T));
const eventMessageTypeUnion = findUnionType(typeAliases, 'EventMessageType');
if (eventMessageTypeUnion != null) {
writeUnionType('AgentEventMessage', eventMessageTypeUnion);
}
}

// write a type predicate for each matched interface
matchedInterfaces.forEach(matched => {
writePredicate(matched.matchingInterface, matched.func)
writeTypeConstant(matched.matchingInterface)
});
/**
* Writes type predicates for all interfaces found that have a matching convert function
*/
function writeTypePredicates(){
// get a list of all conversion functions in the Convert class that return a string
const convert = sourceFile.getClass('Convert');
const convertFunctions = (convert?.getChildrenOfKind(SyntaxKind.MethodDeclaration) ?? []).filter(
func => func.getReturnType().getText() === 'string'
);

//get a list of all interfaces in the file
let messageInterfaces = sourceFile.getChildrenOfKind(SyntaxKind.InterfaceDeclaration);

// generate a list of Interfaces that have an associated conversion function
const matchedInterfaces = convertFunctions
.map(func => {
const valueParameter = func.getParameter('value');

const matchingInterface = messageInterfaces.find(interfaceNode => {
/// Find an interface who's name matches the type passed into the value parameter of the convert function
return valueParameter?.getType().getText(valueParameter) === interfaceNode.getName();
});

if (matchingInterface != null) {
return { func, matchingInterface };
}

return undefined;
})
.filter(isDefined);

// write a type predicate for each matched interface
matchedInterfaces.forEach(matched => {
writeFastPredicate(matched.matchingInterface);
writeValidPredicate(matched.matchingInterface, matched.func);
writeTypeConstant(matched.matchingInterface);
});
}


/**
* Looks for a string union type in the form:
* export type NAME = "stringOne" | "stringTwo" | "stringThree";
* and returns the string values
* if the union type is not found returns undefined
* @param name
* @returns
* @param name
* @returns
*/
function findUnionType(name: string): string[] | undefined {
const typeAlias = typeAliases.find(alias => {
const identifiers = alias.getChildrenOfKind(SyntaxKind.Identifier);

return identifiers[0].getText() === name;

});

return typeAlias?.getChildrenOfKind(SyntaxKind.UnionType)?.[0]
.getDescendantsOfKind(SyntaxKind.StringLiteral)
.map(literal => literal.getLiteralText());
}

function findUnionType(typeAliases: TypeAliasDeclaration[], name: string): string[] | undefined {
const typeAlias = typeAliases.find(alias => {
const identifiers = alias.getChildrenOfKind(SyntaxKind.Identifier);

return identifiers[0].getText() === name;
});

return typeAlias
?.getChildrenOfKind(SyntaxKind.UnionType)?.[0]
.getDescendantsOfKind(SyntaxKind.StringLiteral)
.map(literal => literal.getLiteralText());
}

/**
* Finds an existing declaration with the given type and name
* @param name
* @param kind
* @returns
*/
function findExisting<T extends SyntaxKind>(name: string, kind: T) {
return sourceFile.getChildrenOfKind(kind).filter(child => {
const identifier = child.getDescendantsOfKind(SyntaxKind.Identifier)[0];
return sourceFile.getChildrenOfKind(kind).filter(child => {
const identifier = child.getDescendantsOfKind(SyntaxKind.Identifier)[0];

return identifier?.getText() === name;
})
return identifier?.getText() === name;
});
}

function writePredicate(matchingInterface: InterfaceDeclaration, func: MethodDeclaration): void {
const predicateName = `is${matchingInterface.getName()}`;
/**
* Writes a type predicate for the given interface using the Convert method declaration
* @param matchingInterface
* @param func
*/
function writeValidPredicate(matchingInterface: InterfaceDeclaration, func: MethodDeclaration): void {
const predicateName = `isValid${matchingInterface.getName()}`;

// remove existing instances
findExisting(predicateName, SyntaxKind.FunctionDeclaration).forEach(node => node.remove());
// remove existing instances
findExisting(predicateName, SyntaxKind.FunctionDeclaration).forEach(node => node.remove());

sourceFile.addStatements(`
sourceFile.addStatements(`
/**
* TODO: WIP
*/
export function ${predicateName}(value: any): value is ${matchingInterface.getName()} {
try{
Convert.${func.getName()}(value);
Expand All @@ -108,54 +141,100 @@ export function ${predicateName}(value: any): value is ${matchingInterface.getNa
}`);
}

/**
* Writes a type predicate for the given interface checking just the value of the type property
* @param matchingInterface
* @param func
*/
function writeFastPredicate(matchingInterface: InterfaceDeclaration): void {
const predicateName = `is${matchingInterface.getName()}`;

// remove existing instances
findExisting(predicateName, SyntaxKind.FunctionDeclaration).forEach(node => node.remove());

const typePropertyValue = extractTypePropertyValue(matchingInterface);

function writeTypeConstant(matchingInterface: InterfaceDeclaration): void {

const constantName = `${matchingInterface.getName().replaceAll(/([A-Z])/g, '_$1').toUpperCase().substring(1)}_TYPE`;

//remove existing
findExisting(constantName, SyntaxKind.VariableStatement).forEach(node => node.remove());
if(typePropertyValue == null){
return;
}

sourceFile.addStatements(`
export const ${matchingInterface.getName().replaceAll(/([A-Z])/g, '_$1').toUpperCase().substring(1)}_TYPE = "${matchingInterface.getName()}";`);
/**
* Returns true if the value has the correct type. This is a fast check that does not check the format of the message
*/
export function ${predicateName}(value: any): value is ${matchingInterface.getName()} {
return value != null && value.type === '${typePropertyValue}';
}`);
}

function writeTypeConstant(matchingInterface: InterfaceDeclaration): void {
const constantName = `${matchingInterface
.getName()
.replaceAll(/([A-Z])/g, '_$1')
.toUpperCase()
.substring(1)}_TYPE`;

//remove existing
findExisting(constantName, SyntaxKind.VariableStatement).forEach(node => node.remove());

sourceFile.addStatements(`
export const ${matchingInterface
.getName()
.replaceAll(/([A-Z])/g, '_$1')
.toUpperCase()
.substring(1)}_TYPE = "${matchingInterface.getName()}";`);
}

/**
* Writes a union type of all the interfaces that have a type property that extends the type values passed in.
* For example:
* export type RequestMessage = AddContextListenerRequest | AddEventListenerRequest ...
* @param unionName
* @param interfaces
* @param typeValues
* @param unionName
* @param interfaces
* @param typeValues
*/
function writeUnionType(unionName: string, typeValues: string[]): void {
// generate interfaces list again as we may have just removed some
const unionInterfaces = sourceFile.getChildrenOfKind(SyntaxKind.InterfaceDeclaration);

// look for interfaces that have a type property that extends one of the values in typeValues
const matchingInterfaces = unionInterfaces.filter(currentInterface => {
const typeProperty = currentInterface.getChildrenOfKind(SyntaxKind.PropertySignature).filter(propertySignature => {
return propertySignature.getChildrenOfKind(SyntaxKind.Identifier).find(identifier => identifier.getText() === "type") != null;
})[0];
// generate interfaces list again as we may have just removed some
const unionInterfaces = sourceFile.getChildrenOfKind(SyntaxKind.InterfaceDeclaration);

if(typeProperty == null){
return false;
}
// look for interfaces that have a type property that extends one of the values in typeValues
const matchingInterfaces = unionInterfaces.filter(currentInterface => {
const typePropertyValue = extractTypePropertyValue(currentInterface);

const stringLiterals = typeProperty.getDescendantsOfKind(SyntaxKind.StringLiteral)
.map(literal => literal.getLiteralText());
return typeValues.some(typeValue => typeValue === typePropertyValue);
});

return stringLiterals.some(literal => typeValues.some(typeValue => typeValue === literal));
})

//remove existing Type
findExisting(unionName, SyntaxKind.InterfaceDeclaration).forEach(node => node.remove());
//remove existing Type
findExisting(unionName, SyntaxKind.InterfaceDeclaration).forEach(node => node.remove());

sourceFile.addStatements(`
export type ${unionName} = ${matchingInterfaces.map(match => match.getName()).join(" | ")}; `);
sourceFile.addStatements(`
export type ${unionName} = ${matchingInterfaces.map(match => match.getName()).join(' | ')}; `);
}

sourceFile.formatText();
/**
* Extract the type string constant from an interface such as
* interface ExampleMessage{
* type: "stringConstant";
* }
* @param parentInterface
* @returns
*/
function extractTypePropertyValue(parentInterface: InterfaceDeclaration): string | undefined{
const typeProperty = parentInterface.getChildrenOfKind(SyntaxKind.PropertySignature).filter(propertySignature => {
return (
propertySignature
.getChildrenOfKind(SyntaxKind.Identifier)
.find(identifier => identifier.getText() === 'type') != null
);
})[0];

return typeProperty?.getDescendantsOfKind(SyntaxKind.StringLiteral)
.map(literal => literal.getLiteralText())[0];
}

project.saveSync();
/**
* Type predicate to test that value is defined
*/
function isDefined<T>(value: T | null | undefined): value is T{
return value != null;
}
Loading

0 comments on commit 524834b

Please sign in to comment.