in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [1011:1201]
private static tokenizeComplexType(type: string, removeGenerics: boolean = true): string[]
{
const enum TokenType { None, Name, SimpleType, ComplexType }
type = type.trim();
if (type[0] !== "{")
{
throw new Error(`The supplied type ('${type}') is not a complex type`);
}
if (removeGenerics)
{
type = Type.removeGenericsSpecifiers(type);
}
if (Type.getArraySuffix(type))
{
throw new Error(`The supplied type ('${type}') cannot be tokenized because it has an array suffix`);
}
// Example: The type "{ addressLines: string[], name: { firstName: string, lastName: { middleInitial: { mi: string }[], lastName: string }[][] }[], startDate: number }"
// would yield these 6 [top-level] tokens:
// "addressLines:" -> Name
// "string[]" -> Simple Type
// "name:" -> Name
// "{ firstName: string, lastName: { middleInitial: { mi: string }[], lastName: string }[][] }[]" -> Complex Type
// "startDate:" -> Name
// "number" -> Simple Type
let tokens: string[] = [];
let nameToken: string = ""; // The current name token
let simpleTypeToken: string = ""; // The current simple-type token
let complexTypeToken: string = ""; // The current complex-type token
let currentTokenType: TokenType = TokenType.None;
let depth: number = -1;
let complexTypeStartDepth: number = 0;
let validCharRegEx: RegExp = /[ A-Za-z0-9_\[\]"'|&]/;
for (let pos = 0; pos < type.length; pos++)
{
let char: string = type[pos];
switch (char)
{
case "{":
switch (currentTokenType)
{
case TokenType.Name:
if (nameToken.length === 0)
{
throw new Error(`Unexpected character '${char}' at position ${pos} of "${type}"`);
}
// Fall-through
case TokenType.SimpleType: // Our earlier assumption that the next token would be a SimpleType was wrong
currentTokenType = TokenType.ComplexType;
complexTypeToken = char;
complexTypeStartDepth = depth;
break;
case TokenType.ComplexType:
complexTypeToken += char;
break;
case TokenType.None: // Only happens when pos = 0
currentTokenType = TokenType.Name;
break;
}
depth++;
break;
case ":":
switch (currentTokenType)
{
case TokenType.Name:
if (nameToken.length === 0)
{
throw new Error(`Unexpected character '${char}' at position ${pos} of "${type}"`);
}
nameToken += char; // Including the trailing ":" makes it easy to distinguish names from types in the returned tokens list
tokens.push(nameToken);
nameToken = "";
currentTokenType = TokenType.SimpleType; // We'll assume this until we see "{"
break;
case TokenType.SimpleType:
throw new Error(`Unexpected character '${char}' at position ${pos} of "${type}"`);
case TokenType.ComplexType:
complexTypeToken += char;
break;
}
break;
case "}":
depth--;
switch (currentTokenType)
{
case TokenType.SimpleType:
tokens.push(simpleTypeToken);
simpleTypeToken = "";
// We should now be at the end of 'type'
if (pos !== type.length - 1)
{
throw new Error("tokenizeComplexType() logic error");
}
break;
case TokenType.ComplexType:
complexTypeToken += char;
if (depth === complexTypeStartDepth)
{
if ((pos < type.length - 1) && (type[pos + 1] === "["))
{
// Note: We're not validating the type here (we're just tokenizing it), so we don't check for balanced bracket characters
while ((++pos < type.length) && ((type[pos] === "[") || (type[pos] === "]")))
{
complexTypeToken += type[pos];
}
if (!complexTypeToken.endsWith("]"))
{
throw new Error(`Unexpected character '${complexTypeToken[complexTypeToken.length - 1]}' at position ${pos} of "${type}"`);
}
}
tokens.push(complexTypeToken.trim());
complexTypeToken = "";
currentTokenType = TokenType.Name;
}
break;
}
break;
case ",":
switch (currentTokenType)
{
case TokenType.ComplexType:
complexTypeToken += char;
break;
case TokenType.SimpleType:
tokens.push(simpleTypeToken);
simpleTypeToken = "";
currentTokenType = TokenType.Name;
break;
}
break;
case "<":
if (currentTokenType !== TokenType.SimpleType)
{
throw new Error(`Unexpected character '${char}' at position ${pos} of "${type}"`);
}
let nestLevel: number = 1;
while ((nestLevel > 0) && (pos < type.length - 1))
{
simpleTypeToken += type[pos++];
if (type[pos] === "<") { nestLevel++; }
if (type[pos] === ">") { nestLevel--; }
if (nestLevel === 0)
{
simpleTypeToken += type[pos]; // Add the trailing ">"
}
}
break;
default: // Space, alphanumeric, double/single quote, union (|), intersection (&), non-terminal [ and ]
if (!validCharRegEx.test(char) ||
((char === "[") && (pos < type.length - 1) && (type[pos + 1] !== "]")) || // "[" not followed by "]"
((char === "]") && (pos > 0) && (type[pos - 1] !== "["))) // "]" not preceded by "["
{
throw new Error(`Unexpected character '${char}' at position ${pos} of "${type}"`);
}
switch (currentTokenType)
{
case TokenType.None:
throw new Error(`Unexpected character '${complexTypeToken[complexTypeToken.length - 1]}' at position ${pos} of "${type}"`);
case TokenType.Name:
if (char !== " ")
{
nameToken += char;
}
break;
case TokenType.SimpleType:
if (char !== " ")
{
simpleTypeToken += char;
}
break;
case TokenType.ComplexType:
complexTypeToken += char;
break;
}
break;
}
}
if ((nameToken.length > 0) || (simpleTypeToken.length > 0) || (complexTypeToken.length > 0) || (tokens.length % 2 !== 0))
{
throw new Error(`The type definition is incomplete ("${type}")`);
}
return (tokens);
}