Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for union type literals in general #28

Merged
merged 2 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import { expect, test } from "vitest";
import { compile, compileSync } from "./compiler.js";
import { serializeGrammar } from "./grammar.js";

test("union types", () => {
const grammar = compileSync(`
interface Person {
age: number[] | string[];
}
`, "Person"
);

expect(serializeGrammar(grammar).trimEnd()).toEqual(
String.raw`
root ::= Person
Person ::= "{" ws "\"age\":" ws ( stringlist | numberlist ) "}"
Personlist ::= "[]" | "[" ws Person ("," ws Person)* "]"
string ::= "\"" ([^"]*) "\""
boolean ::= "true" | "false"
ws ::= [ \t\n]*
number ::= [0-9]+ "."? [0-9]*
stringlist ::= "[" ws "]" | "[" ws string ("," ws string)* ws "]"
numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws "]"`.trim())
});

test("Single interface generation", () => {
const postalAddressGrammar = compileSync(
`interface PostalAddress {
Expand Down
110 changes: 69 additions & 41 deletions src/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import { Project, ts, InterfaceDeclaration, EnumDeclaration } from "ts-morph";
import {
Grammar,
GrammarElement,
GrammarRule,
RuleReference,
group,
literal,
reference,
sequence,
} from "./grammar.js";
import { EnumDeclaration, InterfaceDeclaration, Project, ts } from "ts-morph";
import { alternatives, Grammar, GrammarElement, GrammarRule, group, literal, reference, sequence, } from "./grammar.js";

import {
toElementId,
toListElementId,
WS_REF,
getDefaultGrammar,
GrammarRegister,
registerToGrammar,
toElementId,
toListElementId,
WS_REF,
} from "./util.js";

// Turn interface properties into Grammar References
export function toGrammar(iface: Interface): Grammar {

function inferReferenceRule(propType: PropertyType) {
if (propType.type === "simple") {
return reference(propType.name);
}

if (propType.type === "array") {
return reference(toListElementId(propType.reference));
}

throw new Error(`Expected a simple or array type, received ${propType}`);
}

function propertyRules(prop: InterfaceProperty): Array<GrammarRule> {
const { name, type } = prop;
let typeRef: RuleReference;
let typeRef: GrammarRule;

// TODO: Throw exception error if grammar type not found ?
if (typeof type === "string") {
typeRef = reference(type);
} else if (type.isArray) {
typeRef = reference(toListElementId(type.reference));
if (type.type === "union") {
typeRef = alternatives(
...type.refs.map(propType => inferReferenceRule(propType))
);
} else {
typeRef = reference(toElementId(type.reference));
typeRef = inferReferenceRule(type);
}

return [WS_REF, literal(`"${name}":`), WS_REF, typeRef];
Expand Down Expand Up @@ -75,7 +78,10 @@ export function toGrammar(iface: Interface): Grammar {
}

// Parameterized list of things
export type PropertyType = string | { reference: string; isArray: boolean };
export type PropertyType =
| { type: "simple", name: string }
| { type: "array", reference: string }
| { type: "union", refs: PropertyType[] };

export interface InterfaceProperty {
name: string;
Expand Down Expand Up @@ -109,6 +115,34 @@ function handleEnum(enumNode: EnumDeclaration): GrammarElement {
return { identifier: enumNode.getName(), alternatives: choices };
}

/**
* Infer {@link PropertyType} from a declared property name or set of property names.
*/
function inferPropType(
register: Map<string, Array<GrammarRule>>,
propType: string,
declaredTypes: Set<string>,
declaredArrayTypes: Map<string, string>,
propName: string
): PropertyType {
if (register.has(propType)) {
return { type: "simple", name: propType };
} else if (propType === "string[]" || propType === "Array<string>") {
return { type: "simple", name: "stringlist" };
} else if (propType === "number[]" || propType === "Array<number>") {
return { type: "simple", name: "numberlist" };
} else if (declaredTypes.has(propType)) {
return { type: "simple", name: propType };
} else if (declaredArrayTypes.has(propType)) {
const baseType = declaredArrayTypes.get(propType)!;
return { type: "array", reference: baseType };
}

throw new Error(
`Failed validating parameter ${propName}: unsupported type ${propType}`
);
}

function handleInterface(
iface: InterfaceDeclaration,
declaredTypes: Set<string>,
Expand All @@ -118,6 +152,7 @@ function handleInterface(
const declaredArrayTypes: Map<string, string> = new Map();
for (const declType of declaredTypes) {
declaredArrayTypes.set(`${declType}[]`, declType);
declaredArrayTypes.set(`Array<${declType}>`, declType);
}

if (iface.getTypeParameters().length > 0) {
Expand All @@ -137,24 +172,18 @@ function handleInterface(
const propName = child.getName();
const propType = child.getType().getText();

// Validate one of the accepted types
let propTypeValidated: PropertyType;
if (register.has(propType)) {
propTypeValidated = propType;
} else if (propType === "string[]" || propType === "Array<string>") {
propTypeValidated = "stringlist";
} else if (propType === "number[]" || propType === "Array<number>") {
propTypeValidated = "numberlist";
} else if (declaredTypes.has(propType)) {
propTypeValidated = { reference: propType, isArray: false };
} else if (declaredArrayTypes.has(propType)) {
const baseType = declaredArrayTypes.get(propType)!;
propTypeValidated = { reference: baseType, isArray: true };
} else {
throw new Error(
`Failed validating parameter ${propName}: unsupported type ${propType}`
);
}
const unionTypes = (child.getType().isUnion() && !child.getType().isEnum() && !child.getType().isBoolean())
? child.getType().getUnionTypes().map(typ => typ.getText(child))
: [];

// Properties can either be simple singular types, or union types
let propTypeValidated: PropertyType = unionTypes.length === 0
? inferPropType(register, propType, declaredTypes, declaredArrayTypes, propName)
: {
type: "union",
refs: unionTypes.map(typ => inferPropType(register, typ, declaredTypes, declaredArrayTypes, propName))
};

props.push({
name: propName,
type: propTypeValidated,
Expand All @@ -169,7 +198,6 @@ function handleInterface(

/**
* Async variant of main compilation function, targeting {@link Grammar} type from raw TypeScript interface source code.
* @param source
* @returns
*/
export async function compile(
Expand Down
24 changes: 24 additions & 0 deletions src/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface GrammarElement {

export type GrammarRule =
| RuleSequence
| RuleAlternatives
| RuleGroup
| RuleLiteral
| RuleReference
Expand All @@ -19,6 +20,11 @@ export interface RuleSequence {
rules: Array<GrammarRule>;
}

export interface RuleAlternatives {
type: "alt";
rules: Array<GrammarRule>;
}

export interface RuleGroup {
type: "group";
rules: RuleSequence;
Expand All @@ -44,6 +50,10 @@ export function isSequence(rule: GrammarRule): rule is RuleSequence {
return rule.type === "sequence";
}

export function isAlternatives(rule: GrammarRule): rule is RuleAlternatives {
return rule.type === "alt";
}

export function isGroup(rule: GrammarRule): rule is RuleGroup {
return rule.type === "group";
}
Expand All @@ -65,6 +75,8 @@ function serializeRule(rule: GrammarRule): string {
return serializeSequence(rule);
} else if (isGroup(rule)) {
return serializeGroup(rule);
} else if (isAlternatives(rule)) {
return serializeAlternatives(rule)
} else if (isLiteral(rule)) {
return serializeLiteralRule(rule);
} else if (isReference(rule)) {
Expand All @@ -89,6 +101,11 @@ function serializeGroup(rule: RuleGroup): string {
return `(${serializeSequence(rule.rules)})${multiplicity}`;
}

function serializeAlternatives(alt: RuleAlternatives): string {
const alternatives = alt.rules.map(rule => serializeRule(rule));
return "( " + alternatives.join(" | ") + " )";
}

function serializeLiteralRule(rule: RuleLiteral): string {
return JSON.stringify(rule.literal);
}
Expand Down Expand Up @@ -163,6 +180,13 @@ export function reference(value: string): RuleReference {
};
}

export function alternatives(...values: Array<GrammarRule>): RuleAlternatives {
return {
type: "alt",
rules: values,
}
}

export function group(
rules: RuleSequence,
multiplicity: RuleGroup["multiplicity"]
Expand Down
Loading