From 361adff96251414d20b31ae296cfd3fcc7955fd2 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 21 Apr 2020 19:41:23 -0700 Subject: [PATCH] Implement scoped attribute selectors --- docs/source/spec/core/selectors.rst | 111 +++++++------ .../emit-each-selector-validator.errors | 59 ++++--- .../model/selector/AttributeComparator.java | 23 +++ .../model/selector/AttributeSelector.java | 2 +- .../smithy/model/selector/AttributeValue.java | 73 +++++---- .../amazon/smithy/model/selector/Parser.java | 148 ++++++++++++++---- .../selector/ScopedAttributeSelector.java | 109 +++++++++++++ .../selector/SelectorSyntaxException.java | 2 +- .../smithy/model/selector/SelectorTest.java | 99 ++++++++++++ .../model/selector/nested-traits.smithy | 35 ++++- 10 files changed, 520 insertions(+), 141 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 41429e556ab..c8e0b97a47d 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -383,14 +383,24 @@ with a ``min`` property set to ``1``: [trait|range|min=1] +Attribute projections +--------------------- + +*Attribute projections* are values that perform set intersections with other +values. A projection is formed using either the ``(values)`` or ``(keys)`` +:token:`pseudo-property `. + + .. _values-property: -``(values)`` pseudo-property -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``(values)`` projection +~~~~~~~~~~~~~~~~~~~~~~~ -Values of a :token:`list ` can be selected using the special -``(values)`` syntax. Each element from the value currently being evaluated -is used as a new value to check subsequent properties against. +The ``(values)`` property creates a *projection* of all values contained +in a :token:`list ` or :token:`object `. Each +element from the value currently being evaluated is used as a new value +to check subsequent properties against. A ``(values)`` projection on any +value other than an array or object yields no result. The following example matches all shapes that have an :ref:`enum-trait` that contains an enum definition with a ``tags`` property that is set to @@ -400,10 +410,6 @@ that contains an enum definition with a ``tags`` property that is set to [trait|enum|(values)|tags|(values)=internal] -Values of an :token:`object ` can also be selected using the -special ``(values)`` syntax. Each value from object currently being evaluated -is used as a new value to check subsequent properties against. - The following example matches all shapes that have an :ref:`externalDocumentation-trait` that has a value set to ``https://example.com``: @@ -421,12 +427,14 @@ that contains a '$' character: .. _keys-property: -``(keys)`` pseudo-property -~~~~~~~~~~~~~~~~~~~~~~~~~~ +``(keys)`` projection +~~~~~~~~~~~~~~~~~~~~~ -Keys of an object can be selected using the special ``(keys)`` syntax. Each -key currently being evaluated is used as a new value to check subsequent -properties against. +The ``(keys)`` property creates a *projection* of all keys of an +:token:`object `. Each key of the object currently being +evaluated is used as a new value to check subsequent properties against. +A ``(keys)`` projection on any value other than an object yields no +result. The following example matches all shapes that have an ``externalDocumentation`` trait that has an entry named ``Homepage``: @@ -443,6 +451,16 @@ The following selector matches shapes that apply any traits in the [trait|(keys)^='smithy.example#'] +Projection comparisons +~~~~~~~~~~~~~~~~~~~~~~ + +When a projection is compared against a scalar value, the comparison matches +if any value in the projection satisfies the comparator assertion against the +scalar value. When a projection is compared against another projection, the +comparison matches if any value in the left projection satisfies the +comparator when compared against any value in the right projection. + + Error handling ~~~~~~~~~~~~~~ @@ -463,15 +481,6 @@ A :token:`scoped attribute selector ` is similar to an attribute selector, but it allows multiple complex comparisons to be made against a scoped attribute. -In the following example, the ``trait|range`` attribute is used as the scoped -attribute of the expression, and the selector matches all shapes marked with -the :ref:`range-trait` where the ``min`` value is greater than the ``max`` -value: - -.. code-block:: none - - [@trait|range: @{min} > @{max}] - Context values -------------- @@ -481,8 +490,20 @@ for the expression, followed by ``:``. The scoped attribute is accessed using a :token:`context value ` in the form of ``@{`` :token:`identifier` ``}``. -The ``(values)`` and ``(keys)`` pseudo-properties MAY be used in context -values that branch off of the scoped attribute. +In the following example, the ``trait|range`` attribute is used as the scoped +attribute of the expression, and the selector matches all shapes marked with +the :ref:`range-trait` where the ``min`` value is greater than the ``max`` +value: + +.. code-block:: none + + [@trait|range: @{min} > @{max}] + +The ``(values)`` and ``(keys)`` projections MAY be used as the scoped +attribute context value. When the scoped attribute context value is a +projection, each flattened value of the projection is individually tested +against each assertion. If any value from the projection matches the +assertions, then the selector matches the shape. The following selector matches shapes that have an :ref:`enum-trait` where one or more of the enum definitions is both marked as ``deprecated`` and contains @@ -515,15 +536,15 @@ Like non-scoped selectors, multiple values can be provided using a comma separated list. One or more resolved attribute values MUST match one or more provided values. -The following selector matches all shapes with the :ref:`paginated-trait` -where the ``inputToken`` is ``token`` or ``continuationToken``, and -the ``outputToken`` is ``token`` or ``nextToken``: +The following selector matches all shapes with the :ref:`httpApiKeyAuth-trait` +where the ``in`` property is ``header`` and the ``name`` property is neither +``x-api-token`` or ``authorization``: .. code-block:: none - [@trait|paginated: - @{inputToken}=token, continuationToken && - @{outputToken}=token, nextToken] + [@trait|httpApiKeyAuth: + @{name}=header && + @{in}!='x-api-token', 'authorization'] Case insensitive comparisons @@ -532,23 +553,23 @@ Case insensitive comparisons The ``i`` token used before ``&&`` or the closing ``]`` makes a comparison case-insensitive. -The following selector matches on the ``paginated`` trait using +The following selector matches on the ``httpApiKeyAuth`` trait using case-insensitive comparisons: .. code-block:: none - [@trait|paginated: - @{inputToken}=token, continuationToken i && - @{outputToken}=token, nextToken i] + [@trait|httpApiKeyAuth: + @{name}=header i && + @{in}!='x-api-token', 'authorization' i] -The following selector matches on the ``paginated`` trait but only uses -a case-insensitive comparison on the ``inputToken``: +The following selector matches on the ``httpApiKeyAuth`` trait but only +uses a case-insensitive comparison on ``in``: .. code-block:: none - [@trait|paginated: - @{inputToken}=token, continuationToken i && - @{outputToken}=token, nextToken] + [@trait|httpApiKeyAuth: + @{name}=header && + @{in}!='x-api-token', 'authorization' i] Neighbors @@ -968,15 +989,15 @@ Selectors are defined by the following ABNF_ grammar. selector_attr :"[" `selector_key` *(`selector_comparator` `selector_values` ["i"]) "]" selector_key :`identifier` ["|" `selector_path`] selector_path :`selector_path_segment` *("|" `selector_path_segment`) - selector_path_segment :`selector_value` / `selector_pseudo_key` + selector_path_segment :`selector_value` / `selector_pseudo_property` selector_value :`selector_text` / `number` / `root_shape_id` - selector_pseudo_key :"(" `identifier` ")" + selector_pseudo_property :"(" `identifier` ")" selector_values :`selector_value` *("," `selector_value`) selector_comparator :"^=" / "$=" / "*=" / "!=" / ">=" / ">" / "<=" / "<" / "?=" / "=" selector_absolute_root_shape_id :`namespace` "#" `identifier` - selector_scoped_attr :"[@" `selector_key` ":" `selector_scoped_comparisons` "]" - selector_scoped_comparisons :`selector_scoped_comparison` *("&&" `selector_scoped_comparison`) - selector_scoped_comparison :`selector_scoped_value` `selector_comparator` `selector_scoped_values` ["i"] + selector_scoped_attr :"[@" `selector_key` ":" `selector_scoped_assertions` "]" + selector_scoped_assertions :`selector_scoped_assertion` *("&&" `selector_scoped_assertion`) + selector_scoped_assertion :`selector_scoped_value` `selector_comparator` `selector_scoped_values` ["i"] selector_scoped_value :`selector_value` / `selector_context_value` selector_context_value :"@{" `selector_path` "}" selector_scoped_values :`selector_scoped_value` *("," `selector_scoped_value`) diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.errors b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.errors index f8272884761..a03af84e154 100644 --- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.errors +++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.errors @@ -96,33 +96,32 @@ [DANGER] other.ns#String: Selector capture matched selector: [id|name='String'] | shapeName [DANGER] other.ns#String: Selector capture matched selector: simpleType | simpleType [NOTE] other.ns#String: The string shape is not connected to from any service shape. | UnreferencedShape -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 0, near ``: Unexpected selector EOF; expression `` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 0, near ``: Unexpected selector EOF; expression `` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "!": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 1, near `!`: Unexpected selector character: !; expression `!` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "'foo'": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 5, near `'foo'`: Unexpected selector character: '; expression `'foo'` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "\"foo\"": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 5, near `"foo"`: Unexpected selector character: "; expression `"foo"` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "invalid": Unable to deserialize Node using fromNode method: Syntax error at character 7 of 7, near ``: Unknown shape type: invalid; expression `invalid` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 2, near `]`: Invalid attribute start character `]`; expression `[]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo|]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `]`: Invalid attribute start character `]`; expression `[foo|]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[|]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 3, near `|]`: Invalid attribute start character `|`; expression `[|]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[a=]": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 4, near `]`: Invalid attribute start character `]`; expression `[a=]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[a=b": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Expected one of the following tokens: ']'; expression `[a=b` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "string=b": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 8, near `=b`: Unexpected selector character: =; expression `string=b` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=']": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 7, near `]`: Expected ' to close ]; expression `[foo=']` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=\"]": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 7, near `]`: Expected " to close ]; expression `[foo="]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo==value]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 12, near `=value]`: Invalid attribute start character `=`; expression `[foo==value]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo^foo]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 9, near `foo]`: Expected one of the following tokens: '='; expression `[foo^foo]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(:not(string) > list": Unable to deserialize Node using fromNode method: Syntax error at character 23 of 23, near ``: Expected one of the following tokens: ')' ','; expression `:is(:not(string) > list` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "foo -[]->": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 9, near ` -[]->`: Unknown shape type: foo; expression `foo -[]->` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "foo -[input]->": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 14, near ` -[input]->`: Unknown shape type: foo; expression `foo -[input]->` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Expected one of the following tokens: '('; expression `:not` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not(": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 5, near ``: Unexpected selector EOF; expression `:not(` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not()": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `)`: Unexpected selector character: ); expression `:not()` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not(string": Unable to deserialize Node using fromNode method: Syntax error at character 11 of 11, near ``: Expected one of the following tokens: ')' ','; expression `:not(string` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 3, near ``: Expected one of the following tokens: '('; expression `:is` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Unexpected selector EOF; expression `:is(` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":nay()": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `)`: Unexpected selector character: ); expression `:nay()` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string": Unable to deserialize Node using fromNode method: Syntax error at character 10 of 10, near ``: Expected one of the following tokens: ')' ','; expression `:is(string` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, ": Unable to deserialize Node using fromNode method: Syntax error at character 12 of 12, near ``: Unexpected selector EOF; expression `:is(string, ` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, )": Unable to deserialize Node using fromNode method: Syntax error at character 12 of 13, near `)`: Unexpected selector character: ); expression `:is(string, )` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, :not())": Unable to deserialize Node using fromNode method: Syntax error at character 17 of 19, near `))`: Unexpected selector character: ); expression `:is(string, :not())` | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 0, near ``: Unexpected selector EOF; expression: | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "!": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 1, near `!`: Unexpected selector character: !; expression: ! | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "'foo'": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 5, near `'foo'`: Unexpected selector character: '; expression: 'foo' | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "\"foo\"": Unable to deserialize Node using fromNode method: Syntax error at character 0 of 5, near `"foo"`: Unexpected selector character: "; expression: "foo" | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "invalid": Unable to deserialize Node using fromNode method: Syntax error at character 7 of 7, near ``: Unknown shape type: invalid; expression: invalid | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 2, near `]`: Invalid attribute start character `]`; expression: [] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo|]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `]`: Invalid attribute start character `]`; expression: [foo|] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[|]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 3, near `|]`: Invalid attribute start character `|`; expression: [|] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[a=]": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 4, near `]`: Invalid attribute start character `]`; expression: [a=] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[a=b": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Expected one of the following tokens: ']'; expression: [a=b | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "string=b": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 8, near `=b`: Unexpected selector character: =; expression: string=b | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=']": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 7, near `]`: Expected ' to close ]; expression: [foo='] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=\"]": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 7, near `]`: Expected " to close ]; expression: [foo="] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo==value]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 12, near `=value]`: Invalid attribute start character `=`; expression: [foo==value] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo^foo]": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 9, near `foo]`: Expected one of the following tokens: '='; expression: [foo^foo] | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(:not(string) > list": Unable to deserialize Node using fromNode method: Syntax error at character 23 of 23, near ``: Expected one of the following tokens: ')' ','; expression: :is(:not(string) > list | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "foo -[]->": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 9, near ` -[]->`: Unknown shape type: foo; expression: foo -[]-> | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "foo -[input]->": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 14, near ` -[input]->`: Unknown shape type: foo; expression: foo -[input]-> | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Expected one of the following tokens: '('; expression: :not | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not(": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 5, near ``: Unexpected selector EOF; expression: :not( | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not()": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `)`: Unexpected selector character: ); expression: :not() | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":not(string": Unable to deserialize Node using fromNode method: Syntax error at character 11 of 11, near ``: Expected one of the following tokens: ')' ','; expression: :not(string | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is": Unable to deserialize Node using fromNode method: Syntax error at character 3 of 3, near ``: Expected one of the following tokens: '('; expression: :is | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(": Unable to deserialize Node using fromNode method: Syntax error at character 4 of 4, near ``: Unexpected selector EOF; expression: :is( | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":nay()": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 6, near `)`: Unexpected selector character: ); expression: :nay() | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string": Unable to deserialize Node using fromNode method: Syntax error at character 10 of 10, near ``: Expected one of the following tokens: ')' ','; expression: :is(string | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, ": Unable to deserialize Node using fromNode method: Syntax error at character 12 of 12, near ``: Unexpected selector EOF; expression: :is(string, | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, )": Unable to deserialize Node using fromNode method: Syntax error at character 12 of 13, near `)`: Unexpected selector character: ); expression: :is(string, ) | Model +[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":is(string, :not())": Unable to deserialize Node using fromNode method: Syntax error at character 17 of 19, near `))`: Unexpected selector character: ); expression: :is(string, :not()) | Model diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeComparator.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeComparator.java index e1de7fd6d58..66add40a948 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeComparator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeComparator.java @@ -47,6 +47,29 @@ interface AttributeComparator { */ boolean compare(AttributeValue lhs, AttributeValue rhs, boolean caseInsensitive); + /** + * Compares the given attribute values by flattening each side of the + * comparison, and comparing each value. + * + *

This method is necessary in order to support matching on projections. + * + * @param lhs The left hand side of the comparison. + * @param rhs The right hand side of the comparison. + * @param insensitive Whether or not to use a case-insensitive comparison. + * @return Returns true if the attributes match the comparator. + */ + default boolean flattenedCompare(AttributeValue lhs, AttributeValue rhs, boolean insensitive) { + for (AttributeValue l : lhs.getFlattenedValues()) { + for (AttributeValue r : rhs.getFlattenedValues()) { + if (compare(l, r, insensitive)) { + return true; + } + } + } + + return false; + } + // String comparators simplify how comparisons are made on attribute // values that MUST resolve to strings. static AttributeComparator stringComparator(BiFunction compare) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java index 595d96dc0ec..c7deca98a2a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java @@ -74,7 +74,7 @@ private boolean matchesAttribute(Shape shape) { } for (AttributeValue rhs : expected) { - if (lhs.compare(comparator, rhs, caseInsensitive)) { + if (comparator.flattenedCompare(lhs, rhs, caseInsensitive)) { return true; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeValue.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeValue.java index 772c94da956..aade98878f1 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeValue.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeValue.java @@ -51,6 +51,8 @@ boolean isPresent() { static final Factory NULL_FACTORY = shape -> NULL; private static final Logger LOGGER = Logger.getLogger(AttributeValue.class.getName()); + private static final String KEYS = "(keys)"; + private static final String VALUES = "(values)"; @FunctionalInterface interface Factory { @@ -89,18 +91,15 @@ boolean isPresent() { } /** - * Compares the given attribute value with the other attribute value using - * a {@link AttributeComparator}. + * Gets all of the attribute values contained in the attribute value. * - *

This method is necessary in order to support matching on projections. + *

This will yield a single result for normal attributes, or a list + * of multiple values for projections. * - * @param comparator Comparator to use for the comparison. - * @param other The attribute to compare against. - * @param insensitive Whether or not to use a case-insensitive comparison. - * @return Returns true if the attribute match the comparison. + * @return Returns the flattened attribute values contained in the attribute value. */ - boolean compare(AttributeComparator comparator, AttributeValue other, boolean insensitive) { - return comparator.compare(this, other, insensitive); + Collection getFlattenedValues() { + return Collections.singleton(this); } /** @@ -110,7 +109,7 @@ boolean compare(AttributeComparator comparator, AttributeValue other, boolean in * @param path The parsed path to select from the value. * @return Returns the created selector value. */ - private static AttributeValue createPathSelector(AttributeValue current, List path) { + static AttributeValue createPathSelector(AttributeValue current, List path) { if (path.isEmpty()) { return current; } @@ -121,6 +120,7 @@ private static AttributeValue createPathSelector(AttributeValue current, Listmap(NodeValue::new) .orElse(NULL); } - } else if (value.isArrayNode() && key.equals("(values)")) { + } else if (value.isArrayNode() && key.equals(VALUES)) { return project(value.expectArrayNode().getElements()); } else { return NULL; @@ -224,6 +224,7 @@ public String booleanNode(BooleanNode node) { */ static final class Projection extends AttributeValue { final Collection values; + Collection flattened; Projection(Collection values) { this.values = values; @@ -244,23 +245,33 @@ AttributeValue getProperty(String key) { return new Projection(result); } + /** + * Computes the flattened values of the projection. + * + * @return Returns the flattened values of the projection. + */ + Collection getFlattenedValues() { + if (flattened == null) { + List result = new ArrayList<>(values.size()); + for (AttributeValue value : values) { + // Projections need to be flattened! + if (value instanceof Projection) { + result.addAll(((Projection) value).getFlattenedValues()); + } else { + result.add(value); + } + } + flattened = result; + } + + return flattened; + } + @Override boolean isPresent() { // An empty projection is not considered present. return !values.isEmpty(); } - - @Override - public boolean compare(AttributeComparator comparator, AttributeValue other, boolean insensitive) { - // Projections match if any shape contained in the projection matches. - for (AttributeValue value : values) { - if (value.compare(comparator, other, insensitive)) { - return true; - } - } - - return false; - } } /** @@ -297,9 +308,9 @@ AttributeValue getProperty(String key) { switch (key) { case "version": return new Literal(service.getVersion()); - case "(keys)": + case KEYS: return new Projection(Collections.singleton(new Literal("version"))); - case "(values)": + case VALUES: return new Projection(Collections.singleton(new Literal(service.getVersion()))); default: return NULL; @@ -351,13 +362,13 @@ AttributeValue getProperty(String property) { return id.getMember() .map(Literal::new) .orElse(NULL); - case "(keys)": + case KEYS: List keys = new ArrayList<>(3); keys.add(new Literal("namespace")); keys.add(new Literal("name")); id.getMember().ifPresent(member -> keys.add(new Literal("member"))); return new Projection(keys); - case "(values)": + case VALUES: List values = new ArrayList<>(3); values.add(new Literal(id.getNamespace())); values.add(new Literal(id.getName())); @@ -395,7 +406,7 @@ static AttributeValue.Factory createFactory(List path) { @Override AttributeValue getProperty(String property) { - if (property.equals("(keys)")) { + if (property.equals(KEYS)) { // This allows the projected keys to be used like shape IDs: // [trait|(keys)|namespace='com.foo'] List values = new ArrayList<>(); @@ -403,7 +414,7 @@ AttributeValue getProperty(String property) { values.add(new Id(id)); } return new Projection(values); - } else if (property.equals("(values)")) { + } else if (property.equals(VALUES)) { // This allows the projected values to be used as nodes. This // selector finds all traits that have 'foo' property. // [trait|(values)|foo] diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/Parser.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/Parser.java index f0f322bf0a7..0a4ec21952f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/Parser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/Parser.java @@ -46,10 +46,12 @@ final class Parser { } private final String expression; + private final int length; private int position = 0; private Parser(String selector) { - this.expression = selector; + expression = selector; + length = expression.length(); } static Selector parse(String selector) { @@ -70,7 +72,7 @@ private List recursiveParse() { ws(); // Parse until a break token: ",", "]", and ")". - while (position != expression.length() && !BREAK_TOKENS.contains(expression.charAt(position))) { + while (position != length && !BREAK_TOKENS.contains(expression.charAt(position))) { selectors.add(createSelector()); // Always skip ws after calling createSelector. ws(); @@ -89,7 +91,12 @@ private Selector createSelector() { return parseFunction(); case '[': // attribute position++; - return parseAttribute(); + if (charPeek() == '@') { + position++; + return parseScopedAttribute(); + } else { + return parseAttribute(); + } case '>': // undirected neighbor position++; return new NeighborSelector(ListUtils.of()); @@ -131,17 +138,16 @@ private Selector createSelector() { } private void ws() { - while (position < expression.length() && isWhitespace(expression.charAt(position))) { - position++; + for (; position < length; position++) { + char c = expression.charAt(position); + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { + break; + } } } - private boolean isWhitespace(char c) { - return c == ' ' || c == '\t' || c == '\r' || c == '\n'; - } - private char charPeek() { - return position == expression.length() ? Character.MIN_VALUE : expression.charAt(position); + return position == length ? Character.MIN_VALUE : expression.charAt(position); } private char expect(char... tokens) { @@ -235,11 +241,31 @@ private Selector parseAttribute() { AttributeValue.Factory keyFactory = parseAttributePath(); ws(); char next = expect(']', '=', '!', '^', '$', '*', '?', '>', '<'); - AttributeComparator comparator; + if (next == ']') { + return AttributeSelector.existence(keyFactory); + } + + AttributeComparator comparator = parseComparator(next); + List values = parseAttributeValues(); + boolean insensitive = parseCaseInsensitiveToken(); + expect(']'); + return new AttributeSelector(keyFactory, values, comparator, insensitive); + } + + private boolean parseCaseInsensitiveToken() { + ws(); + boolean insensitive = charPeek() == 'i'; + if (insensitive) { + position++; + ws(); + } + return insensitive; + } + + private AttributeComparator parseComparator(char next) { + AttributeComparator comparator; switch (next) { - case ']': - return AttributeSelector.existence(keyFactory); case '=': comparator = AttributeComparator.EQUALS; break; @@ -284,17 +310,73 @@ private Selector parseAttribute() { throw syntax("Unknown attribute comparator token '" + next + "'"); } - List values = parseAttributeValues(); ws(); + return comparator; + } - boolean insensitive = charPeek() == 'i'; - if (insensitive) { - position++; + // "[@" selector_key ":" selector_scoped_comparisons "]" + private Selector parseScopedAttribute() { + ws(); + AttributeValue.Factory keyScope = parseAttributePath(); + ws(); + expect(':'); + ws(); + return new ScopedAttributeSelector(keyScope, parseScopedAssertions()); + } + + // selector_scoped_comparison *("&&" selector_scoped_comparison) + private List parseScopedAssertions() { + List assertions = new ArrayList<>(); + assertions.add(parseScopedAssertion()); + ws(); + + while (charPeek() == '&') { + expect('&'); + expect('&'); ws(); + assertions.add(parseScopedAssertion()); } expect(']'); - return new AttributeSelector(keyFactory, values, comparator, insensitive); + return assertions; + } + + private ScopedAttributeSelector.Assertion parseScopedAssertion() { + ScopedAttributeSelector.ScopedFactory lhs = parseScopedValue(); + char next = charPeek(); + position++; + AttributeComparator comparator = parseComparator(next); + + List rhs = new ArrayList<>(); + rhs.add(parseScopedValue()); + + while (charPeek() == ',') { + position++; + rhs.add(parseScopedValue()); + } + + boolean insensitive = parseCaseInsensitiveToken(); + return new ScopedAttributeSelector.Assertion(lhs, comparator, rhs, insensitive); + } + + private ScopedAttributeSelector.ScopedFactory parseScopedValue() { + ws(); + if (charPeek() == '@') { + position++; + expect('{'); + // parse at least one path segment, followed by any number of + // comma separated segments. + List path = new ArrayList<>(); + path.add(parseSelectorPathSegment()); + path.addAll(parseSelectorPath()); + expect('}'); + ws(); + return value -> AttributeValue.createPathSelector(value, path); + } else { + String parsedValue = parseAttributeValue(); + ws(); + return value -> new AttributeValue.Literal(parsedValue); + } } private AttributeValue.Factory parseAttributePath() { @@ -302,7 +384,7 @@ private AttributeValue.Factory parseAttributePath() { String namespace = parseIdentifier(); // It is optionally followed by "|" delimited path keys. - List path = parsePipeDelimitedTraitAttributes(); + List path = parseSelectorPath(); switch (namespace) { case "id": @@ -319,7 +401,7 @@ private AttributeValue.Factory parseAttributePath() { } // Can be a shape_id, quoted string, number, or pseudo_key. - private List parsePipeDelimitedTraitAttributes() { + private List parseSelectorPath() { ws(); if (charPeek() != '|') { @@ -329,21 +411,25 @@ private List parsePipeDelimitedTraitAttributes() { List result = new ArrayList<>(); do { position++; // skip '|' - ws(); - // Handle pseudo-keys enclosed in "(" identifier ")". - if (charPeek() == '(') { - position++; - String propertyName = parseIdentifier(); - expect(')'); - result.add("(" + propertyName + ")"); - } else { - result.add(parseAttributeValue()); - } + result.add(parseSelectorPathSegment()); } while (charPeek() == '|'); return result; } + private String parseSelectorPathSegment() { + ws(); + // Handle pseudo-keys enclosed in "(" identifier ")". + if (charPeek() == '(') { + position++; + String propertyName = parseIdentifier(); + expect(')'); + return "(" + propertyName + ")"; + } else { + return parseAttributeValue(); + } + } + private List parseAttributeValues() { List result = new ArrayList<>(); result.add(parseAttributeValue()); @@ -387,7 +473,7 @@ private String parseAttributeValue() { private String consumeInside(char c) { int i = ++position; - while (i < expression.length()) { + while (i < length) { if (expression.charAt(i) == c) { String result = expression.substring(position, i); position = i + 1; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java new file mode 100644 index 00000000000..6dda2e4e921 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java @@ -0,0 +1,109 @@ +/* + * 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.List; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.neighbor.NeighborProvider; +import software.amazon.smithy.model.shapes.Shape; + +/** + * Matches a scoped attribute or projection against a set of assertions that + * can path into the scoped attribute. + */ +final class ScopedAttributeSelector implements Selector { + + static final class Assertion { + private final ScopedFactory lhs; + private final AttributeComparator comparator; + private final List rhs; + private final boolean caseInsensitive; + + Assertion( + ScopedFactory lhs, + AttributeComparator comparator, + List rhs, + boolean caseInsensitive + ) { + this.lhs = lhs; + this.comparator = comparator; + this.rhs = rhs; + this.caseInsensitive = caseInsensitive; + } + } + + /** + * Creates an AttributeValue from the given scope value. + * + *

This is useful for pathing into scopes. + */ + @FunctionalInterface + interface ScopedFactory { + AttributeValue create(AttributeValue value); + } + + private final AttributeValue.Factory keyScope; + private final List assertions; + + ScopedAttributeSelector(AttributeValue.Factory keyScope, List assertions) { + this.keyScope = keyScope; + this.assertions = assertions; + } + + @Override + public Set select(Model model, NeighborProvider neighborProvider, Set shapes) { + return shapes.stream() + .filter(this::matchesAssertions) + .collect(Collectors.toSet()); + } + + private boolean matchesAssertions(Shape shape) { + // First resolve the scope of the assertions. + AttributeValue scope = keyScope.create(shape); + + // If it's not present, then nothing could ever match. + if (!scope.isPresent()) { + return false; + } + + // When dealing with a projection, each flattened projection value is + // used as a scope and then passed to the assertions one at a time. + for (AttributeValue value : scope.getFlattenedValues()) { + if (compareWithScope(value)) { + return true; + } + } + + return false; + } + + private boolean compareWithScope(AttributeValue scope) { + // Ensure that each assertion matches, and provide them the scope. + for (Assertion assertion : assertions) { + AttributeValue lhs = assertion.lhs.create(scope); + for (ScopedFactory factory : assertion.rhs) { + AttributeValue rhs = factory.create(scope); + if (!assertion.comparator.flattenedCompare(lhs, rhs, assertion.caseInsensitive)) { + return false; + } + } + } + + return true; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorSyntaxException.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorSyntaxException.java index 440c6f2d042..d174cb7352c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorSyntaxException.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorSyntaxException.java @@ -28,6 +28,6 @@ private static String createMessage(String message, String expression, int pos) if (pos <= expression.length()) { result += ", near `" + expression.substring(pos) + "`"; } - return result + ": " + message + "; expression `" + expression + "`"; + return result + ": " + message + "; expression: " + expression; } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java index ed73e9e33ca..89bc1c959dd 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java @@ -646,4 +646,103 @@ public void projectsTraitValuesAsNodes() { assertThat(shapes1, containsInAnyOrder("smithy.example#RangeInt1", "smithy.example#RangeInt2")); assertThat(shapes2, equalTo(shapes1)); } + + @Test + public void parsesValidScopedAttributes() { + List exprs = ListUtils.of( + "[@trait: 10=10]", + "[@trait: @{foo}=10]", + "[@trait: @{foo}=@{foo}]", + "[@trait: @{foo}=@{foo} && bar=bar]", + "[@trait: @{foo}=@{foo} && bar=10]", + "[@trait: @{foo}=@{foo} && bar=@{baz}]", + "[@trait: @{foo}=@{foo} && bar=@{baz} && bam='abc']", + "[@trait: @{foo}=@{foo} i && bar=@{baz} i && bam='abc' i ]", + "[@ trait : @{foo}=@{foo} i &&bar=@{baz} i&&bam='abc'i\n]", + "[@\r\n\t trait\r\n\t : @{foo}=@{foo}]\r\n\t ", + "[@trait: @{foo|baz|bam|(boo)}=@{foo|bar|(boo)|baz}]", + "[@trait: @{foo|baz|bam|(boo)}=@{foo|bar|(boo)|baz}, @{foo|bam}]", + // Comma separated values are or'd together. + "[@trait: @{foo|baz|bam|(boo)}=@{foo|bar|(boo)|baz}, @{foo|bam} i && 10=10 i]"); + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void detectsInvalidScopedAttributes() { + List exprs = ListUtils.of( + "[@", + "[@foo", + "[@foo:", + "[@foo: bar", + "[@foo: bar=", + "[@foo: bar=bam", + "[@foo: bar=bam i", + "[@foo: bar+bam]", // Invalid comparator + "[@foo: @", + "[@foo: @{", + "[@foo: @{abc", + "[@foo: @{abc}", + "[@foo: @{abc}]", + "[@foo: @{abc}=", + "[@foo: @{abc}=10", + "[@foo: @{abc}=10 &&", + "[@foo: @{abc}=10 && abc", + "[@foo: @{abc}=10 && abc=", + "[@foo: @{abc}=10 && abc=def", + "[@foo: @{abc}=10 && abc=def i", + "[@foo: @{abc{}}=10]", + "[@foo: @{abc|}=10]", + "[@foo: @{abc|def(baz)}=10]", // not a valid segment + "[@foo: @{abc|()}=10]", // missing contents of () + "[@foo: @{abc|(.)}=10]"); // invalid contents of ()); + + for (String expr : exprs) { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(expr)); + } + } + + @Test + public void evaluatesScopedAttributes() { + Set shapes1 = ids(traitModel, "[@trait|range: @{min}=1 && @{max}=10]"); + // Can scope to `trait` and then do assertions on all traits. + // Not very useful, but technically supported. + Set shapes2 = ids(traitModel, "[@trait: @{range|min}=1 && @{range|max}=10]"); + + assertThat(shapes1, contains("smithy.example#RangeInt1")); + assertThat(shapes2, equalTo(shapes1)); + } + + @Test + public void evaluatesScopedAttributesWithProjections() { + // Note that the projection can be on either side. + Set shapes1 = ids(traitModel, "[@trait|enum|(values): @{name}=@{value} && @{tags|(values)}=hi]"); + Set shapes2 = ids(traitModel, "[@trait|enum|(values): @{name}=@{value} && hi=@{tags|(values)}]"); + + assertThat(shapes1, contains("smithy.example#DocumentedString1")); + assertThat(shapes2, equalTo(shapes1)); + } + + @Test + public void projectionsCanMatchThemselvesThroughIntersection() { + // Any enum with tags should match it's own tags. + Set shapes1 = ids(traitModel, "[@trait|enum|(values): @{tags|(values)}=@{tags|(values)}]"); + Set shapes2 = ids(traitModel, "[@trait|enum|(values): @{tags}?=true]"); + + assertThat(shapes1, not(empty())); + assertThat(shapes2, equalTo(shapes1)); + } + + @Test + public void nestedProjectionsAreFlattened() { + Set shapes1 = ids(traitModel, "[@trait|smithy.example#listyTrait|(values)|(values)|(values): @{foo}=a]"); + Set shapes2 = ids(traitModel, "[@trait|smithy.example#listyTrait|(values)|(values)|(values): @{foo}=b]"); + Set shapes3 = ids(traitModel, "[@trait|smithy.example#listyTrait|(values)|(values)|(values): @{foo}=c]"); + + assertThat(shapes1, contains("smithy.example#MyService")); + assertThat(shapes2, equalTo(shapes1)); + assertThat(shapes3, equalTo(shapes1)); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy index e03a73805fb..0c9e395d3d9 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy @@ -25,7 +25,15 @@ namespace smithy.example value: "m256.mega", name: "M256_MEGA", deprecated: true - } + }, + { + value: "hi", + name: "bye" + }, + { + value: "bye", + name: "hi" + }, ]) @tags(["foo", "baz"]) string EnumString @@ -45,7 +53,12 @@ integer RangeInt2 value: "m256.mega", name: "M256_MEGA", tags: ["notEbs"] - } + }, + { + value: "hi", + name: "hi", + tags: ["hi", "there"] + }, ]) @nestedTrait(foo: {foo: {bar: "hi"}}) string DocumentedString1 @@ -77,6 +90,24 @@ structure ErrorStruct1 {} @httpError(400) structure ErrorStruct2 {} +@listyTrait([[[{foo: "a"}, {foo: "b"}]], [[{foo: "c"}]]]) service MyService { version: "2020-04-21" } + +@trait +list listyTrait { + member: ListyTraitMember1, +} + +list ListyTraitMember1 { + member: ListyTraitMember2, +} + +list ListyTraitMember2 { + member: ListyTraitStruct, +} + +structure ListyTraitStruct { + foo: String +}