private static tokenizeComplexType()

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);
    }