All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.selector.SelectorParser Maven / Gradle / Ivy

Go to download

This module provides the core implementation of loading, validating, traversing, mutating, and serializing a Smithy model.

The newest version!
/*
 * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.smithy.model.selector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import software.amazon.smithy.model.loader.ParserUtils;
import software.amazon.smithy.model.neighbor.RelationshipType;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.NumberShape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.SimpleShape;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.SimpleParser;

/**
 * Parses a selector expression.
 */
final class SelectorParser extends SimpleParser {

    private static final Logger LOGGER = Logger.getLogger(SelectorParser.class.getName());
    private static final Set BREAK_TOKENS = SetUtils.of(',', ']', ')');
    private static final Set REL_TYPES = new HashSet<>();
    private final List roots = new ArrayList<>();

    static {
        // Adds selector relationship labels for warnings when unknown relationship names are used.
        for (RelationshipType rel : RelationshipType.values()) {
            rel.getSelectorLabel().ifPresent(REL_TYPES::add);
        }
    }

    private SelectorParser(String selector) {
        super(selector);
    }

    static Selector parse(String selector) {
        SelectorParser parser = new SelectorParser(selector);
        List result = parser.parse();
        return new WrappedSelector(selector, result, parser.roots);
    }

    List parse() {
        return recursiveParse();
    }

    private List recursiveParse() {
        List selectors = new IgnoreIdentitySelectorArray();

        // createSelector() will strip leading ws.
        selectors.add(createSelector());

        // Need to always strip after calling createSelector in case we are at EOF.
        ws();

        // Parse until a break token: ",", "]", and ")".
        while (!eof() && !BREAK_TOKENS.contains(peek())) {
            selectors.add(createSelector());
            // Always skip ws after calling createSelector.
            ws();
        }

        return selectors;
    }

    /**
     * Filter out unnecessary identity selectors when creating the finalized AST to evaluate selectors.
     */
    private static final class IgnoreIdentitySelectorArray extends ArrayList {
        @Override
        public boolean add(InternalSelector o) {
            return o != InternalSelector.IDENTITY && super.add(o);
        }
    }

    private InternalSelector createSelector() {
        ws();

        // Require at least one selector.
        switch (peek()) {
            case ':': // function
                skip();
                return parseSelectorFunction();
            case '[': // attribute
                skip();
                if (peek() == '@') {
                    skip();
                    return parseScopedAttribute();
                } else {
                    return parseAttribute();
                }
            case '>': // forward undirected neighbor
                skip();
                return NeighborSelector.FORWARD;
            case '<': // reverse [un]directed neighbor
                skip();
                if (peek() == '-') { // reverse directed neighbor (<-[X, Y, Z]-)
                    skip();
                    expect('[');
                    return parseSelectorDirectedReverseNeighbor();
                } else { // reverse undirected neighbor (<)
                    return NeighborSelector.REVERSE;
                }
            case '~': // ~>
                skip();
                expect('>');
                return new RecursiveNeighborSelector();
            case '-': // forward directed neighbor
                skip();
                expect('[');
                return parseSelectorForwardDirectedNeighbor();
            case '*': // Any shape
                skip();
                return InternalSelector.IDENTITY;
            case '$': // variable
                skip();
                return parseVariable();
            default:
                if (ParserUtils.isIdentifierStart(peek())) {
                    String identifier = ParserUtils.parseIdentifier(this);
                    switch (identifier) {
                        case "number":
                            return new ShapeTypeCategorySelector(NumberShape.class);
                        case "simpleType":
                            return new ShapeTypeCategorySelector(SimpleShape.class);
                        case "collection":
                            return new ShapeTypeCategorySelector(CollectionShape.class);
                        default:
                            ShapeType shape = ShapeType.fromString(identifier)
                                    .orElseThrow(() -> syntax("Unknown shape type: " + identifier));
                            return new ShapeTypeSelector(shape);
                    }
                } else if (peek() == Character.MIN_VALUE) {
                    throw syntax("Unexpected selector EOF");
                } else {
                    throw syntax("Unexpected selector character: " + peek());
                }
        }
    }

    @Override
    public SelectorSyntaxException syntax(String message) {
        return new SelectorSyntaxException(message, input().toString(), position(), line(), column());
    }

    private InternalSelector parseVariable() {
        ws();

        if (peek() == '{') {
            skip();
            ws();
            String variableName = ParserUtils.parseIdentifier(this);
            ws();
            expect('}');
            return new VariableGetSelector(variableName);
        }

        String name = ParserUtils.parseIdentifier(this);
        ws();
        expect('(');
        ws();
        InternalSelector selector = AndSelector.of(recursiveParse());
        ws();
        expect(')');

        return new VariableStoreSelector(name, selector);
    }

    // Parses a multi edge neighbor selector: "-[" relationship-type *("," relationship-type) "]"
    private InternalSelector parseSelectorForwardDirectedNeighbor() {
        List relationships = parseSelectorDirectedRelationships();
        // Get the remainder of the "]->" token.
        expect('-');
        expect('>');
        return NeighborSelector.forward(relationships);
    }

    private InternalSelector parseSelectorDirectedReverseNeighbor() {
        List relationships = parseSelectorDirectedRelationships();
        expect('-');
        return NeighborSelector.reverse(relationships);
    }

    private List parseSelectorDirectedRelationships() {
        List relationships = new ArrayList<>();
        String next;
        char peek;

        do {
            // Requires at least one relationship type.
            ws();
            next = ParserUtils.parseIdentifier(this);
            relationships.add(next);

            // Tolerate unknown relationships, but log a warning.
            if (!REL_TYPES.contains(next)) {
                LOGGER.warning(String.format(
                        "Unknown relationship type '%s' found near %s. Expected one of: %s",
                        next, position() - next.length(), REL_TYPES));
            }

            ws();
            peek = expect(']', ',');
        } while (peek != ']');

        return relationships;
    }

    private InternalSelector parseSelectorFunction() {
        int functionPosition = position();
        String name = ParserUtils.parseIdentifier(this);
        List selectors = parseSelectorFunctionArgs();
        switch (name) {
            case "not":
                if (selectors.size() != 1) {
                    throw new SelectorSyntaxException(
                            "The :not function requires a single selector argument",
                            input().toString(), functionPosition, line(), column());
                }
                return new NotSelector(selectors.get(0));
            case "test":
                return new TestSelector(selectors);
            case "is":
                return IsSelector.of(selectors);
            case "in":
                if (selectors.size() != 1) {
                    throw new SelectorSyntaxException(
                            "The :in function requires a single selector argument",
                            input().toString(), functionPosition, line(), column());
                }
                return new InSelector(selectors.get(0));
            case "root":
                if (selectors.size() != 1) {
                    throw new SelectorSyntaxException(
                            "The :root function requires a single selector argument",
                            input().toString(), functionPosition, line(), column());
                }
                InternalSelector root = new RootSelector(selectors.get(0), roots.size());
                roots.add(selectors.get(0));
                return root;
            case "topdown":
                if (selectors.size() > 2) {
                    throw new SelectorSyntaxException(
                            "The :topdown function accepts 1 or 2 selectors, but found " + selectors.size(),
                            input().toString(), functionPosition, line(), column());
                }
                return new TopDownSelector(selectors);
            case "recursive":
                if (selectors.size() != 1) {
                    throw new SelectorSyntaxException(
                            "The :recursive function requires a single selector argument",
                            input().toString(), functionPosition, line(), column());
                }
                return new RecursiveSelector(selectors.get(0));
            case "each":
                LOGGER.warning("The `:each` selector function has been renamed to `:is`: " + input());
                return IsSelector.of(selectors);
            default:
                LOGGER.warning(String.format("Unknown function name `%s` found in selector: %s",
                                             name, input()));
                return (context, shape, next) -> InternalSelector.Response.CONTINUE;
        }
    }

    private List parseSelectorFunctionArgs() {
        ws();
        List selectors = new ArrayList<>();
        expect('(');
        char next;

        do {
            selectors.add(AndSelector.of(recursiveParse()));
            ws();
            next = expect(')', ',');
        } while (next != ')');

        return selectors;
    }

    private InternalSelector parseAttribute() {
        ws();
        List path = parseAttributePath();
        ws();
        char next = expect(']', '=', '!', '^', '$', '*', '?', '>', '<');

        if (next == ']') {
            return AttributeSelector.existence(path);
        }

        AttributeComparator comparator = parseComparator(next);
        List values = parseAttributeValues();
        boolean insensitive = parseCaseInsensitiveToken();
        expect(']');
        return new AttributeSelector(path, values, comparator, insensitive);
    }

    private boolean parseCaseInsensitiveToken() {
        ws();
        boolean insensitive = peek() == 'i';
        if (insensitive) {
            skip();
            ws();
        }
        return insensitive;
    }

    private AttributeComparator parseComparator(char next) {
        AttributeComparator comparator;
        switch (next) {
            case '=': // =
                comparator = AttributeComparator.EQUALS;
                break;
            case '!':
                expect('='); // !=
                comparator = AttributeComparator.NOT_EQUALS;
                break;
            case '^':
                expect('='); // ^=
                comparator = AttributeComparator.STARTS_WITH;
                break;
            case '$':
                expect('='); // $=
                comparator = AttributeComparator.ENDS_WITH;
                break;
            case '*':
                expect('='); // *=
                comparator = AttributeComparator.CONTAINS;
                break;
            case '?':
                expect('='); // ?=
                comparator = AttributeComparator.EXISTS;
                break;
            case '>':
                if (peek() == '=') { // >=
                    skip();
                    comparator = AttributeComparator.GTE;
                } else { // >
                    comparator = AttributeComparator.GT;
                }
                break;
            case '<':
                if (peek() == '=') { // <=
                    skip();
                    comparator = AttributeComparator.LTE;
                } else { // <
                    comparator = AttributeComparator.LT;
                }
                break;
            case '{': // projection comparators
                char nextSet = expect('<', '=', '!');
                if (nextSet == '<') {
                    if (peek() == '<') {
                        expect('<'); // {<<}
                        comparator = AttributeComparator.PROPER_SUBSET;
                    } else { // {<}
                        comparator = AttributeComparator.SUBSET;
                    }
                } else if (nextSet == '=') { // {=}
                    comparator = AttributeComparator.PROJECTION_EQUALS;
                } else { // {!=}
                    expect('=');
                    comparator = AttributeComparator.PROJECTION_NOT_EQUALS;
                }
                expect('}');
                break;
            default:
                // Unreachable
                throw syntax("Unknown attribute comparator token '" + next + "'");
        }

        ws();
        return comparator;
    }

    // "[@" selector_key ":" selector_scoped_comparisons "]"
    private InternalSelector parseScopedAttribute() {
        ws();
        List path = parseAttributePath();
        ws();
        expect(':');
        ws();
        return new ScopedAttributeSelector(path, parseScopedAssertions());
    }

    // selector_scoped_comparison *("&&" selector_scoped_comparison)
    private List parseScopedAssertions() {
        List assertions = new ArrayList<>();
        assertions.add(parseScopedAssertion());
        ws();

        while (peek() == '&') {
            expect('&');
            expect('&');
            ws();
            assertions.add(parseScopedAssertion());
        }

        expect(']');
        return assertions;
    }

    private ScopedAttributeSelector.Assertion parseScopedAssertion() {
        ScopedAttributeSelector.ScopedFactory lhs = parseScopedValue();
        char next = peek();
        skip();
        AttributeComparator comparator = parseComparator(next);

        List rhs = new ArrayList<>();
        rhs.add(parseScopedValue());

        while (peek() == ',') {
            skip();
            rhs.add(parseScopedValue());
        }

        boolean insensitive = parseCaseInsensitiveToken();
        return new ScopedAttributeSelector.Assertion(lhs, comparator, rhs, insensitive);
    }

    private ScopedAttributeSelector.ScopedFactory parseScopedValue() {
        ws();
        if (peek() == '@') {
            List path = parseScopedValuePath(this);
            ws();
            return value -> value.getPath(path);
        } else {
            String parsedValue = parseAttributeValue(this);
            ws();
            return value -> AttributeValue.literal(parsedValue);
        }
    }

    private List parseAttributePath() {
        ws();

        // '[@:' binds the current shape as the context.
        if (peek() == ':') {
            return Collections.emptyList();
        }

        List path = new ArrayList<>();
        // Parse the top-level namespace key.
        path.add(ParserUtils.parseIdentifier(this));

        // It is optionally followed by "|" delimited path keys.
        path.addAll(parseSelectorPath(this));

        return path;
    }

    private List parseAttributeValues() {
        List result = new ArrayList<>();
        result.add(parseAttributeValue(this));
        ws();

        while (peek() == ',') {
            skip();
            result.add(parseAttributeValue(this));
            ws();
        }

        return result;
    }

    /*
     * The following methods are static methods that aren't coupled to the
     * SelectorParser, but rather a SimpleParser. This allows the AttributeValue#parseScopedAttribute
     * method to accept a SimpleParser and then use this method to perform the actual
     * parsing of a scoped attribute value.
     *
     * This is used to parse scoped attribute values from EmitEachSelector message
     * templates.
     */

    static List parseScopedValuePath(SimpleParser parser) {
        parser.expect('@');
        parser.expect('{');
        // parse at least one path segment, followed by any number of
        // comma separated segments.
        List path = new ArrayList<>();
        path.add(parseSelectorPathSegment(parser));
        path.addAll(parseSelectorPath(parser));
        parser.expect('}');
        return path;
    }

    private static String parseSelectorPathSegment(SimpleParser parser) {
        parser.ws();
        // Handle function properties enclosed in "(" identifier ")".
        if (parser.peek() == '(') {
            parser.skip();
            String propertyName = ParserUtils.parseIdentifier(parser);
            parser.expect(')');
            return "(" + propertyName + ")";
        } else {
            return parseAttributeValue(parser);
        }
    }

    private static String parseAttributeValue(SimpleParser parser) {
        parser.ws();

        switch (parser.peek()) {
            case '\'':
                return consumeInside(parser, '\'');
            case '"':
                return consumeInside(parser, '"');
            case '-':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return ParserUtils.parseNumber(parser);
            default:
                return ParserUtils.parseRootShapeId(parser);
        }
    }

    private static String consumeInside(SimpleParser parser, char c) {
        parser.skip(); // skip the opening character.
        int start = parser.position();

        while (!parser.eof()) {
            if (parser.peek() == c) {
                String result = parser.sliceFrom(start);
                parser.skip();
                parser.ws();
                return result;
            }
            parser.skip();
        }

        throw parser.syntax("Expected " + c + " to close " + parser.sliceFrom(start));
    }

    // Can be a shape_id, quoted string, number, or function key.
    private static List parseSelectorPath(SimpleParser parser) {
        parser.ws();

        if (parser.peek() != '|') {
            return Collections.emptyList();
        }

        List result = new ArrayList<>();
        do {
            parser.skip(); // skip '|'
            result.add(parseSelectorPathSegment(parser));
        } while (parser.peek() == '|');

        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy