From 159b482bd448e7f049e99ddb57a7daffc046747e Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 18 Apr 2020 14:19:19 -0700 Subject: [PATCH 1/7] Tolerate unknown function, rename :each This commit updates selectors to tolerate unknown functions and renames the ":each" selector to ":is" to match the CSS4 name of ":is", which is the same functionality. Support for ":each" is still present and functions as an alias. --- docs/source/spec/aws/amazon-apigateway.rst | 2 +- docs/source/spec/core/auth-traits.rst | 2 +- docs/source/spec/core/behavior-traits.rst | 2 +- docs/source/spec/core/constraint-traits.rst | 2 +- .../source/spec/core/documentation-traits.rst | 4 +- docs/source/spec/core/protocol-traits.rst | 2 +- docs/source/spec/core/resource-traits.rst | 2 +- docs/source/spec/core/selectors.rst | 107 +++++++++++------- docs/source/spec/core/stream-traits.rst | 6 +- docs/source/spec/core/xml-traits.rst | 4 +- .../spec/http-protocol-compliance-tests.rst | 2 +- docs/source/spec/validation.rst | 4 +- .../emit-each-selector-validator.errors | 54 ++++----- .../emit-each-selector-validator.json | 20 ++-- .../missing-documentation-selector-test.json | 2 +- .../smithy/model/knowledge/BottomUpIndex.java | 2 +- .../{EachSelector.java => IsSelector.java} | 6 +- .../amazon/smithy/model/selector/Parser.java | 25 ++-- .../smithy/model/loader/prelude-traits.smithy | 26 ++--- ...hSelectorTest.java => IsSelectorTest.java} | 4 +- .../smithy/model/selector/SelectorTest.java | 20 ++++ .../event-payload-validation.errors | 6 +- .../validators/sensitive-trait.errors | 2 +- .../META-INF/smithy/smithy.test.smithy | 2 +- 24 files changed, 179 insertions(+), 129 deletions(-) rename smithy-model/src/main/java/software/amazon/smithy/model/selector/{EachSelector.java => IsSelector.java} (91%) rename smithy-model/src/test/java/software/amazon/smithy/model/selector/{EachSelectorTest.java => IsSelectorTest.java} (94%) diff --git a/docs/source/spec/aws/amazon-apigateway.rst b/docs/source/spec/aws/amazon-apigateway.rst index e116c2a0d08..57c241db7f1 100644 --- a/docs/source/spec/aws/amazon-apigateway.rst +++ b/docs/source/spec/aws/amazon-apigateway.rst @@ -228,7 +228,7 @@ Summary Authorizers are resolved hierarchically: an operation inherits the effective authorizer applied to a parent resource or operation. Trait selector - ``:each(service, resource, operation)`` + ``:is(service, resource, operation)`` *A service, resource, or operation* Value type diff --git a/docs/source/spec/core/auth-traits.rst b/docs/source/spec/core/auth-traits.rst index 88282f9d41c..bba82b28597 100644 --- a/docs/source/spec/core/auth-traits.rst +++ b/docs/source/spec/core/auth-traits.rst @@ -275,7 +275,7 @@ Summary supported by the operation, overriding any ``auth`` trait specified on a service. Trait selector - ``:test(service, operation)`` + ``:is(service, operation)`` *Service or operation shapes* Value type diff --git a/docs/source/spec/core/behavior-traits.rst b/docs/source/spec/core/behavior-traits.rst index aab686ed9ad..5a2321c2d71 100644 --- a/docs/source/spec/core/behavior-traits.rst +++ b/docs/source/spec/core/behavior-traits.rst @@ -170,7 +170,7 @@ Summary the number of results returned in a single response and that multiple invocations might be necessary to retrieve all results. Trait selector - ``:test(operation, service)`` + ``:is(operation, service)`` *An operation or service* Value type diff --git a/docs/source/spec/core/constraint-traits.rst b/docs/source/spec/core/constraint-traits.rst index 6651115a362..42477a64d21 100644 --- a/docs/source/spec/core/constraint-traits.rst +++ b/docs/source/spec/core/constraint-traits.rst @@ -318,7 +318,7 @@ Given the following model, Summary Constrains a shape to minimum and maximum number of elements or size. Trait selector - ``:test(list, map, string, blob, member > :each(list, map, string, blob))`` + ``:test(list, map, string, blob, member > :is(list, map, string, blob))`` *Any list, map, string, or blob; or a member that targets one of these shapes* Value type diff --git a/docs/source/spec/core/documentation-traits.rst b/docs/source/spec/core/documentation-traits.rst index 57977bcf85f..45ffe51b0a1 100644 --- a/docs/source/spec/core/documentation-traits.rst +++ b/docs/source/spec/core/documentation-traits.rst @@ -275,7 +275,7 @@ Summary Indicates that the data stored in the shape or member is sensitive and MUST be handled with care. Trait selector - ``:not(:test(service, operation, resource))`` + ``:not(:is(service, operation, resource))`` *Any shape that is not a service, operation, or resource.* Value type @@ -345,7 +345,7 @@ Summary used in automatically generated documentation and other contexts to provide a user friendly name for services and resources. Trait selector - ``:test(service, resource)`` + ``:is(service, resource)`` *Any service or resource* Value type diff --git a/docs/source/spec/core/protocol-traits.rst b/docs/source/spec/core/protocol-traits.rst index 546d3b4e5bb..95434ebeded 100644 --- a/docs/source/spec/core/protocol-traits.rst +++ b/docs/source/spec/core/protocol-traits.rst @@ -207,7 +207,7 @@ Summary Describes the contents of a blob or string shape using a media type as defined by :rfc:`6838` (e.g., "video/quicktime"). Trait selector - ``:test(blob, string)`` + ``:is(blob, string)`` *Any blob or string* Value type diff --git a/docs/source/spec/core/resource-traits.rst b/docs/source/spec/core/resource-traits.rst index 067ba7b0154..b0425127ea0 100644 --- a/docs/source/spec/core/resource-traits.rst +++ b/docs/source/spec/core/resource-traits.rst @@ -93,7 +93,7 @@ Summary to resolve references by name, allowing the end-user to invoke operations on a specific referenced resource. Trait selector - ``:test(structure, string)`` + ``:is(structure, string)`` *Any structure or string* Value type diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 1e9f05d7a30..5e57310d023 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -59,6 +59,12 @@ The following selector matches all string shapes in a model: string +The following selector matches all numbers defined in a model: + +.. code-block:: none + + number + Attribute selectors =================== @@ -205,9 +211,16 @@ Available attributes Neighbors ========= -The *current* shape evaluated by a selector is changed using a neighbor token, -``>``. A neighbor token returns every shape that is connected to the current -shape. For example, the following selector returns the key and value members of +The *current* shapes evaluated by a selector is changed using a +:token:`neighbor` token. + + +Undirected neighbor +~~~~~~~~~~~~~~~~~~~ + +An :token:`undirected neighbor ` (``>``) changes the +current set of shapes to every shape that is connected to the current shapes. +For example, the following selector returns the key and value members of every map: .. code-block:: none @@ -405,73 +418,84 @@ Functions Functions are used to filter shapes. Functions always start with ``:``. +.. important:: -:each -~~~~~ + Implementations MUST tolerate parsing unknown function names. When + evaluated, the unknown function matches no shapes. -The ``:each`` function is used to map over the current shape with multiple -selectors and returns all of the shapes returned from each selector. The -``:each`` function accepts a variadic list of selectors each separated by a -comma (","). -The following selector matches all string and number shapes: +``:test`` +~~~~~~~~~ + +The ``:test`` function is used to test if a shape is contained within any of +the provided predicate selector return values without changing the current +shape. + +The following selector is used to match all list shapes that target a string: .. code-block:: none - :each(string, number) + list:test(> member > string) -Each can be used inside of neighbors too. The following selector -matches all members that target a string or number: +The following example matches all shapes that are bound to a resource and have +no documentation: .. code-block:: none - member > :each(string, number) + :test(-[bound, resource]->) :not([trait|documentation]) -The following ``:each`` selector matches all shapes that are either -targeted by a list member or targeted by a map member: -.. code-block:: none +``:is`` +~~~~~~~ - :each(list > member > *, map > member > *) +The ``:is`` function is used to map over the current shape with multiple +selectors and returns all of the shapes returned from each selector. The +``:is`` function accepts a variadic list of selectors each separated by a +comma (","). -The following selector matches all list and map shapes that target strings: +The following selector matches all string and number shapes: .. code-block:: none - :each(:test(list > member > string), :test(map > member > string)) + :is(string, number) -Because none of the selectors in the ``:each`` function are intended to -change the current node, this can be reduced to the following selector: +Each can be used inside of neighbors too. The following selector +matches all members that target a string or number: .. code-block:: none - :test(:each(list > member > string, map > member > string)) + member > :is(string, number) +The following ``:is`` selector matches all shapes that are either +targeted by a list member or targeted by a map member: -:test -~~~~~ +.. code-block:: none -The ``:test`` function is used to test if a shape is contained within any of -the provided predicate selector return values without changing the current -shape. + :is(list > member > *, map > member > *) -The following selector is used to match all string and number shapes: +The following selector matches all list and map shapes that target strings: .. code-block:: none - :test(string, number) + :is(:test(list > member > string), :test(map > member > string)) -The ``:test`` function is much more interesting when used to test if a shape -contains a neighbor in addition to other filtering. The following example -matches all shapes that are bound to a resource and have no documentation: +Because none of the selectors in the ``:is`` function are intended to +change the current node, this can be reduced to the following selector: .. code-block:: none - :test(-[bound, resource]->) :not([trait|documentation]) + :test(:is(list > member > string, map > member > string)) + +.. note:: + + This function was previously named ``:each``. Implementations that wish + to maintain backward compatibility with the old function name MAY + treat ``:each`` as an alias for ``:is``, and models that use ``:each`` + SHOULD update to use ``:is``. -:not -~~~~ +``:not`` +~~~~~~~~ The *:not* function is used to filter out shapes. This function accepts a list of selector arguments, and the shapes returned from each predicate are @@ -550,8 +574,8 @@ in the model: :not(* -[trait]-> *)[trait|trait] -:of -~~~ +``:of`` +~~~~~~~ The ``:of`` function is used to match members based on their containers (i.e., the shape that defines the member). The ``:of`` function accepts one @@ -585,7 +609,7 @@ Selectors are defined by the following ABNF_ grammar. .. productionlist:: selectors selector :`selector_expression` *(`selector_expression`) - selector_expression :`shape_types` / `attr` / `function_expression` / `neighbors` + selector_expression :`shape_types` / `attr` / `function_expression` / `neighbor` shape_types :"*" :/ "blob" :/ "boolean" @@ -612,7 +636,8 @@ Selectors are defined by the following ABNF_ grammar. :/ "number" :/ "simpleType" :/ "collection" - neighbors :">" / `directed_neighbor` / `recursive_neighbor` + neighbor :`undirected_neighbor` / `directed_neighbor` / `recursive_neighbor` + undirected_neighbor :">" directed_neighbor :"-[" `relationship_type` *("," `relationship_type`) "]->" recursive_neighbor :"~>" relationship_type :"identifier" @@ -640,7 +665,7 @@ Selectors are defined by the following ABNF_ grammar. service_attribute :"service|version" comparator :"^=" / "$=" / "*=" / "=" function_expression :":" `function` "(" `selector` *("," `selector`) ")" - function :"each" / "test" / "of" / "not" + function :"test" / "is" / "not" / "of" selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` selector_single_quoted_text :"'" 1*`selector_single_quoted_char` "'" selector_double_quoted_text :DQUOTE 1*`selector_double_quoted_char` DQUOTE diff --git a/docs/source/spec/core/stream-traits.rst b/docs/source/spec/core/stream-traits.rst index 5cb876284cd..c0d6c4888dd 100644 --- a/docs/source/spec/core/stream-traits.rst +++ b/docs/source/spec/core/stream-traits.rst @@ -29,7 +29,7 @@ Summary When applied to a union, it indicates that shape represents an :ref:`event stream `. Trait selector:: - ``:each(blob, union)`` + ``:is(blob, union)`` Value type Annotation trait Validation @@ -654,7 +654,7 @@ Summary Trait selector .. code-block:: css - member:of(structure):test( > :each(boolean, byte, short, integer, long, blob, string, timestamp)) + member:of(structure):test( > :is(boolean, byte, short, integer, long, blob, string, timestamp)) *Member of a structure that targets a boolean, byte, short, integer, long, blob, string, or timestamp shape* Value type @@ -718,7 +718,7 @@ Summary Trait selector .. code-block:: css - member:of(structure):test(> :each(blob, string, structure, union)) + member:of(structure):test(> :is(blob, string, structure, union)) *Structure member that targets a blob, string, structure, or union* Value type diff --git a/docs/source/spec/core/xml-traits.rst b/docs/source/spec/core/xml-traits.rst index 7e71fda97b4..ce3c98e9b20 100644 --- a/docs/source/spec/core/xml-traits.rst +++ b/docs/source/spec/core/xml-traits.rst @@ -703,7 +703,7 @@ The XML serialization is: Summary Unwraps the values of a list or map into the containing structure. Trait selector - ``:test(member:of(structure, union) > :each(collection, map))`` + ``:test(member:of(structure, union) > :is(collection, map))`` *Member of a structure or union that targets a list, set, or map* Value type @@ -854,7 +854,7 @@ Summary Changes the serialized element or attribute name of a structure, union, or member. Trait selector - ``:test(structure, union, member)`` + ``:is(structure, union, member)`` *A structure, union, or member* Value type diff --git a/docs/source/spec/http-protocol-compliance-tests.rst b/docs/source/spec/http-protocol-compliance-tests.rst index 93410db4ec8..f78fa168cc3 100644 --- a/docs/source/spec/http-protocol-compliance-tests.rst +++ b/docs/source/spec/http-protocol-compliance-tests.rst @@ -316,7 +316,7 @@ Summary Trait selector .. code-block:: css - :each(operation, structure[trait|error]) + :is(operation, structure[trait|error]) Value type ``list`` of ``HttpResponseTestCase`` structures diff --git a/docs/source/spec/validation.rst b/docs/source/spec/validation.rst index 3be42708b39..3959ceda094 100644 --- a/docs/source/spec/validation.rst +++ b/docs/source/spec/validation.rst @@ -764,7 +764,7 @@ following constraints: selector: """ :not([trait|documentation]) :not(simpleType) - :not(member:of(:each(list, map))) + :not(member:of(:is(list, map))) :not(:test(member > [trait|documentation]))""" } }] @@ -864,6 +864,6 @@ traits. No instances of the enum, pattern, length, or range trait could be found. Did you forget to apply these traits?""", configuration: { - selector: ":each([trait|enum], [trait|pattern], [trait|length], [trait|range])", + selector: ":is([trait|enum], [trait|pattern], [trait|length], [trait|range])", } }] 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 dbd955535ef..33fb03e4241 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 @@ -8,17 +8,17 @@ [DANGER] ns.foo#Integer: Selector capture matched selector: integer | integer [DANGER] ns.foo#Integer: Selector capture matched selector: number | number [DANGER] ns.foo#Integer: Selector capture matched selector: simpleType | simpleType -[DANGER] ns.foo#Long: Selector capture matched selector: :each(long, float, boolean) | any +[DANGER] ns.foo#Long: Selector capture matched selector: :is(long, float, boolean) | any [DANGER] ns.foo#Long: Selector capture matched selector: long | long [DANGER] ns.foo#Long: Selector capture matched selector: number | number [DANGER] ns.foo#Long: Selector capture matched selector: simpleType | simpleType [NOTE] ns.foo#Long: The long shape is not connected to from any service shape. | UnreferencedShape -[DANGER] ns.foo#Float: Selector capture matched selector: :each(long, float, boolean) | any +[DANGER] ns.foo#Float: Selector capture matched selector: :is(long, float, boolean) | any [DANGER] ns.foo#Float: Selector capture matched selector: float | float [DANGER] ns.foo#Float: Selector capture matched selector: number | number [DANGER] ns.foo#Float: Selector capture matched selector: simpleType | simpleType [NOTE] ns.foo#Float: The float shape is not connected to from any service shape. | UnreferencedShape -[DANGER] ns.foo#Boolean: Selector capture matched selector: :each(long, float, boolean) | any +[DANGER] ns.foo#Boolean: Selector capture matched selector: :is(long, float, boolean) | any [DANGER] ns.foo#Boolean: Selector capture matched selector: boolean | boolean [DANGER] ns.foo#Boolean: Selector capture matched selector: simpleType | simpleType [NOTE] ns.foo#Boolean: The boolean shape is not connected to from any service shape. | UnreferencedShape @@ -34,60 +34,60 @@ [DANGER] ns.foo#Blob: Selector capture matched selector: blob | blob [DANGER] ns.foo#Blob: Selector capture matched selector: simpleType | simpleType [NOTE] ns.foo#Blob: The blob shape is not connected to from any service shape. | UnreferencedShape -[DANGER] ns.foo#List: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#List: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#List: Selector capture matched selector: list | list [NOTE] ns.foo#List: The list shape is not connected to from any service shape. | UnreferencedShape -[DANGER] ns.foo#List$member: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#List$member: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#List$member: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString [DANGER] ns.foo#List$member: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#List$member: Selector capture matched selector: member | member -[DANGER] ns.foo#Map: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#Map: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#Map: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#Map: Selector capture matched selector: map | map -[DANGER] ns.foo#Map$key: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#Map$key: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#Map$key: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString [DANGER] ns.foo#Map$key: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#Map$key: Selector capture matched selector: member | member -[DANGER] ns.foo#Map$value: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#Map$value: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#Map$value: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString [DANGER] ns.foo#Map$value: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#Map$value: Selector capture matched selector: member | member -[DANGER] ns.foo#MyService: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#MyService: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#MyService: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#MyService: Selector capture matched selector: [service|version^=2017] | serviceVersion -[DANGER] ns.foo#MyResource: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#MyResource: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#MyResource: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#MyResource: Selector capture matched selector: resource | resource [DANGER] ns.foo#MyResource: Selector capture matched selector: service -[resource]-> resource | serviceChild [DANGER] ns.foo#MyResourceIdentifier: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#MyResourceIdentifier: Selector capture matched selector: resource -[identifier]-> string | identifier [DANGER] ns.foo#MyResourceIdentifier: Selector capture matched selector: simpleType | simpleType -[DANGER] ns.foo#OperationA: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationA: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationA: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationA: Selector capture matched selector: operation | operation -[DANGER] ns.foo#OperationAInput: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationAInput: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationAInput: Selector capture matched selector: > | valid-neighbor-only -[DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString [DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: [id|member=memberName] | shapeMember [DANGER] ns.foo#OperationAInput$memberName: Selector capture matched selector: member | member -[DANGER] ns.foo#OperationAInput$otherMemberName: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationAInput$otherMemberName: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationAInput$otherMemberName: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationAInput$otherMemberName: Selector capture matched selector: member | member -[DANGER] ns.foo#OperationAOutput: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationAOutput: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationAOutput: Selector capture matched selector: > | valid-neighbor-only -[DANGER] ns.foo#OperationAOutput$b: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationAOutput$b: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationAOutput$b: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationAOutput$b: Selector capture matched selector: member | member [DANGER] ns.foo#OperationErrorA: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationErrorB: Selector capture matched selector: > | valid-neighbor-only -[DANGER] ns.foo#OperationB: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationB: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationB: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationB: Selector capture matched selector: operation | operation -[DANGER] ns.foo#OperationBInput: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationBInput: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationBInput: Selector capture matched selector: > | valid-neighbor-only -[DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: :not(:each([trait|error], simpleType)) | not +[DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: > | valid-neighbor-only [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: member | member [DANGER] ns.foo#UtcTimestamp: Selector capture matched selector: simpleType | simpleType @@ -112,19 +112,19 @@ [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 1 of 7, near `foo="]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 12, near `foo==value]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 9, near `foo^foo]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; expression `[foo^foo]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(:not(string) > list": Unable to deserialize Node using fromNode method: Syntax error at character 25 of 25, near ``: Expected one of the following tokens: `)`, `,`; expression `:each(:not(string) > list` | 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 0 of 9, near `foo -[]->`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 0 of 14, near `foo -[input]->`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ":each": Unable to deserialize Node using fromNode method: Syntax error at character 5 of 5, near ``: Expected one of the following tokens: `(`; expression `:each` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(": Unable to deserialize Node using fromNode method: Syntax error at character 6 of 6, near ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:each(` | 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 1 of 6, near `nay()`: Expected one of the following tokens: `each`, `not`, `of`, `test`; expression `:nay()` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(string": Unable to deserialize Node using fromNode method: Syntax error at character 12 of 12, near ``: Expected one of the following tokens: `)`, `,`; expression `:each(string` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(string, ": Unable to deserialize Node using fromNode method: Syntax error at character 14 of 14, near ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:each(string, ` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(string, )": Unable to deserialize Node using fromNode method: Syntax error at character 14 of 15, near `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:each(string, )` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from ":each(string, :not())": Unable to deserialize Node using fromNode method: Syntax error at character 19 of 21, near `))`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:each(string, :not())` | 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `))`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:is(string, :not())` | 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 1 of 5, near `foo]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; expression `[foo]` | Model [ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=baz]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 9, near `foo=baz]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; expression `[foo=baz]` | Model diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.json b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.json index 38f792b0d10..8ead8e96626 100644 --- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.json +++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/emit-each-selector-validator.json @@ -260,7 +260,7 @@ "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(:not(string) > list" + "selector": ":is(:not(string) > list" } }, { @@ -309,14 +309,14 @@ "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each" + "selector": ":is" } }, { "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(" + "selector": ":is(" } }, { @@ -330,28 +330,28 @@ "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(string" + "selector": ":is(string" } }, { "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(string, " + "selector": ":is(string, " } }, { "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(string, )" + "selector": ":is(string, )" } }, { "id": "invalid", "name": "EmitEachSelector", "configuration": { - "selector": ":each(string, :not())" + "selector": ":is(string, :not())" } }, { @@ -582,14 +582,14 @@ "id": "any", "name": "EmitEachSelector", "configuration": { - "selector": ":each(long, float, boolean)" + "selector": ":is(long, float, boolean)" } }, { "id": "not", "name": "EmitEachSelector", "configuration": { - "selector": ":not(:each([trait|error], simpleType))" + "selector": ":not(:is([trait|error], simpleType))" } }, { @@ -624,7 +624,7 @@ "id": "ignored", "name": "EmitEachSelector", "configuration": { - "selector": "list :each(-[identifier]->, -[bound]->) *" + "selector": "list :is(-[identifier]->, -[bound]->) *" } } ] diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-documentation-selector-test.json b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-documentation-selector-test.json index 3a716937c61..c77ace4aeb2 100644 --- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-documentation-selector-test.json +++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-documentation-selector-test.json @@ -80,7 +80,7 @@ "id": "MissingDocumentation", "message": "This shape should have documentation", "configuration": { - "selector": " :not([trait|documentation])\n :not(simpleType)\n :not(member:of(:each(list, map)))\n :not(:test(member > [trait|documentation]))" + "selector": " :not([trait|documentation])\n :not(simpleType)\n :not(member:of(:is(list, map)))\n :not(:test(member > [trait|documentation]))" } } ] diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/BottomUpIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/BottomUpIndex.java index 44faadfa259..19b7e035184 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/BottomUpIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/BottomUpIndex.java @@ -35,7 +35,7 @@ * Computes all of the parent shapes of resources and operations from the bottom-up. */ public final class BottomUpIndex implements KnowledgeIndex { - private static final Selector SELECTOR = Selector.parse(":each(resource, operation)"); + private static final Selector SELECTOR = Selector.parse(":is(resource, operation)"); private final Map>> parentBindings = new HashMap<>(); public BottomUpIndex(Model model) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/EachSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java similarity index 91% rename from smithy-model/src/main/java/software/amazon/smithy/model/selector/EachSelector.java rename to smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java index 2930885df5a..b3a9a693fe9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/EachSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java @@ -25,15 +25,15 @@ /** * Maps input over each function and returns the concatenated result. */ -final class EachSelector implements Selector { +final class IsSelector implements Selector { private final List selectors; - private EachSelector(List predicates) { + private IsSelector(List predicates) { this.selectors = predicates; } static Selector of(List predicates) { - return predicates.size() == 1 ? predicates.get(0) : new EachSelector(predicates); + return predicates.size() == 1 ? predicates.get(0) : new IsSelector(predicates); } @Override 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 3c0ff0b42f0..94139b5dbd9 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 @@ -18,10 +18,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Function; import java.util.logging.Logger; import software.amazon.smithy.model.neighbor.RelationshipType; import software.amazon.smithy.model.shapes.CollectionShape; @@ -40,7 +40,6 @@ final class Parser { private static final Logger LOGGER = Logger.getLogger(Parser.class.getName()); private static final Set BREAK_TOKENS = SetUtils.of(',', ']', ')'); private static final Set REL_TYPES = new HashSet<>(); - private static final List FUNCTIONS = ListUtils.of("test", "each", "of", "not"); private static final List ATTRIBUTES = ListUtils.of( "trait|", "id|namespace", "id|name", "id|member", "id", "service|version"); private static final List AFTER_ATTRIBUTE = ListUtils.of("=", "^=", "$=", "*=", "]"); @@ -173,17 +172,23 @@ private Selector parseMultiEdgeDirectedNeighbor() { } private Selector parseFunction() { - String name = expect(FUNCTIONS); + String name = parseIdentifier(); + List selectors = parseVariadic(); switch (name) { - case "not": return parseVariadic(NotSelector::new); - case "test": return parseVariadic(TestSelector::new); - case "each": return parseVariadic(EachSelector::of); - case "of": return parseVariadic(OfSelector::new); - default: throw new RuntimeException("Unreachable function case " + name); + case "not": return new NotSelector(selectors); + case "test": return new TestSelector(selectors); + case "is": return IsSelector.of(selectors); + case "each": + LOGGER.warning("The `:each` selector function has been renamed to `:is`: " + expression); + return IsSelector.of(selectors); + case "of": return new OfSelector(selectors); + default: + LOGGER.warning(String.format("Unknown function name `%s` found in selector: %s", name, expression)); + return (model, neighborProvider, shapes) -> Collections.emptySet(); } } - private Selector parseVariadic(Function, Selector> creator) { + private List parseVariadic() { List selectors = new ArrayList<>(); expect(START_FUNCTION); String next; @@ -191,7 +196,7 @@ private Selector parseVariadic(Function, Selector> creator) { selectors.add(AndSelector.of(recursiveParse())); next = expect(FUNCTION_ARG_NEXT_TOKEN); } while (!next.equals(")")); - return creator.apply(selectors); + return selectors; } private Selector parseAttribute() { diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy index e9f944a5dd1..ff533901ae1 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy @@ -3,7 +3,7 @@ $version: "1.0.0" namespace smithy.api /// Makes a shape a trait. -@trait(selector: ":each(simpleType, list, map, set, structure, union)") +@trait(selector: ":is(simpleType, list, map, set, structure, union)") @tags(["diff.error.add", "diff.error.remove"]) structure trait { /// The valid places in a model that the trait can be applied. @@ -64,7 +64,7 @@ map externalDocumentation { } /// Defines the list of authentication schemes supported by a service or operation. -@trait(selector: ":test(service, operation)") +@trait(selector: ":is(service, operation)") @uniqueItems list auth { member: AuthTraitReference @@ -221,12 +221,12 @@ string jsonName structure xmlAttribute {} /// Unwraps the values of a list, set, or map into the containing structure/union. -@trait(selector: ":test(member:of(structure, union) > :each(collection, map))") +@trait(selector: ":test(member:of(structure, union) > :is(collection, map))") @tags(["diff.error.const"]) structure xmlFlattened {} /// Changes the serialized element or attribute name of a structure, union, or member. -@trait(selector: ":test(structure, union, member)") +@trait(selector: ":is(structure, union, member)") @tags(["diff.error.const"]) @pattern("^[a-zA-Z_][a-zA-Z_0-9-]*(:[a-zA-Z_][a-zA-Z_0-9-]*)?$") string xmlName @@ -255,14 +255,14 @@ structure noReplace {} /// Describes the contents of a blob shape using a media type as defined by /// RFC 6838 (e.g., "video/quicktime"). -@trait(selector: ":each(blob, string)") +@trait(selector: ":is(blob, string)") @tags(["diff.error.remove"]) string mediaType /// Defines the resource shapes that are referenced by a string shape or a /// structure shape and the members of the structure that provide values for /// the identifiers of the resource. -@trait(selector: ":test(structure, string)") +@trait(selector: ":is(structure, string)") list references { member: Reference } @@ -308,7 +308,7 @@ string resourceIdentifier structure private {} /// Indicates that the data stored in the shape or member is sensitive and MUST be handled with care. -@trait(selector: ":not(:test(service, operation, resource))") +@trait(selector: ":not(:is(service, operation, resource))") structure sensitive {} /// Defines the version or date in which a shape or member was added to the model. @@ -319,7 +319,7 @@ string since /// be stored in memory, or that the size of the data stored in the shape is /// unknown at the start of a request. If the target is a union then the shape /// represents a stream of events. -@trait(selector: ":each(blob, union)", structurallyExclusive: "target") +@trait(selector: ":is(blob, union)", structurallyExclusive: "target") @tags(["diff.error.const"]) structure streaming {} @@ -340,7 +340,7 @@ list tags { /// This title can be used in automatically generated documentation /// and other contexts to provide a user friendly name for services /// and resources. -@trait(selector: ":test(service, resource)") +@trait(selector: ":is(service, resource)") string title /// Constrains the acceptable values of a string to a fixed set @@ -378,7 +378,7 @@ structure EnumDefinition { string EnumConstantBodyName /// Constrains a shape to minimum and maximum number of elements or size. -@trait(selector: ":test(collection, map, string, blob, member > :each(collection, map, string, blob))") +@trait(selector: ":test(collection, map, string, blob, member > :is(collection, map, string, blob))") structure length { /// Integer value that represents the minimum inclusive length of a shape. min: Long, @@ -418,7 +418,7 @@ structure unstable {} /// The paginated trait indicates that an operation intentionally limits the number /// of results returned in a single response and that multiple invocations might be /// necessary to retrieve all results. -@trait(selector: ":each(service, operation)") +@trait(selector: ":is(service, operation)") @tags(["diff.error.remove"]) structure paginated { /// The name of the operation input member that represents the continuation token. @@ -546,14 +546,14 @@ list NonEmptyStringList { } /// Marks a member as the payload of an event. -@trait(selector: "member:of(structure):test(> :each(blob, string, structure, union))", +@trait(selector: "member:of(structure):test(> :is(blob, string, structure, union))", conflicts: [eventHeader], structurallyExclusive: "member") @tags(["diff.error.const"]) structure eventPayload {} /// Marks a member as a header of an event. -@trait(selector: "member:of(structure):test( > :each(boolean, byte, short, integer, long, blob, string, timestamp))", +@trait(selector: "member:of(structure):test( > :is(boolean, byte, short, integer, long, blob, string, timestamp))", conflicts: [eventPayload]) @tags(["diff.error.const"]) structure eventHeader {} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/EachSelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/IsSelectorTest.java similarity index 94% rename from smithy-model/src/test/java/software/amazon/smithy/model/selector/EachSelectorTest.java rename to smithy-model/src/test/java/software/amazon/smithy/model/selector/IsSelectorTest.java index 9639de4a45b..37733d447a0 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/selector/EachSelectorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/IsSelectorTest.java @@ -28,10 +28,10 @@ import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.StringShape; -public class EachSelectorTest { +public class IsSelectorTest { @Test public void projectsOutValues() { - Selector selector = EachSelector.of(Arrays.asList( + Selector selector = IsSelector.of(Arrays.asList( new ShapeTypeSelector(ShapeType.STRING), new ShapeTypeSelector(ShapeType.INTEGER))); Shape a = IntegerShape.builder().id("foo.baz#Bar").build(); 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 2339c35b82d..a867e7edddf 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 @@ -121,4 +121,24 @@ public void toleratesUnexpectedRelationshipTypes() { assertThat(expr, equalTo(selector.toString())); } + + @Test + public void toleratesUnexpectedFunctionNames() { + String expr = ":unknownFunction(string)"; + Selector selector = Selector.parse(expr); + + assertThat(expr, equalTo(selector.toString())); + } + + @Test + public void supportsDeprecatedEachFunction() { + Model model = Model.assembler().addImport(getClass().getResource("model.json")) + .disablePrelude() + .assemble() + .unwrap(); + Set result1 = Selector.parse(":each(collection)").select(model); + Set result2 = Selector.parse(":is(collection)").select(model); + + assertThat(result1, equalTo(result2)); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors index 946f2ea3f67..98e41f43306 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors @@ -1,6 +1,6 @@ [ERROR] ns.foo#InvalidEvent1: This event structure contains a member marked with the `eventPayload` trait, so all other members must be marked with the `eventHeader` trait. However, the following member(s) are not marked with the eventHeader trait: `baz` | EventPayloadTrait -[ERROR] ns.foo#InvalidEvent2$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent2$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test(> :each(blob, string, structure, union)) | TraitTarget +[ERROR] ns.foo#InvalidEvent2$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent2$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test(> :is(blob, string, structure, union)) | TraitTarget [ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait [ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait -[ERROR] ns.foo#InvalidEvent3$foo: Trait `eventHeader` cannot be applied to `ns.foo#InvalidEvent3$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test( > :each(boolean, byte, short, integer, long, blob, string, timestamp)) | TraitTarget -[ERROR] ns.foo#InvalidEvent4$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent4$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test(> :each(blob, string, structure, union)) | TraitTarget +[ERROR] ns.foo#InvalidEvent3$foo: Trait `eventHeader` cannot be applied to `ns.foo#InvalidEvent3$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test( > :is(boolean, byte, short, integer, long, blob, string, timestamp)) | TraitTarget +[ERROR] ns.foo#InvalidEvent4$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent4$foo`. This trait may only be applied to shapes that match the following selector: member:of(structure):test(> :is(blob, string, structure, union)) | TraitTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sensitive-trait.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sensitive-trait.errors index 34f93e3f29c..e8627450df2 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sensitive-trait.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sensitive-trait.errors @@ -1 +1 @@ -[ERROR] ns.foo#Operation: Trait `sensitive` cannot be applied to `ns.foo#Operation`. This trait may only be applied to shapes that match the following selector: :not(:test(service, operation, resource)) | TraitTarget \ No newline at end of file +[ERROR] ns.foo#Operation: Trait `sensitive` cannot be applied to `ns.foo#Operation`. This trait may only be applied to shapes that match the following selector: :not(:is(service, operation, resource)) | TraitTarget diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy index 1346210b54a..deeafcb56a5 100644 --- a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy @@ -134,7 +134,7 @@ list StringList { /// Define how an HTTP response is serialized given a specific protocol, /// authentication scheme, and set of output or error parameters. -@trait(selector: ":each(operation, structure[trait|error])") +@trait(selector: ":is(operation, structure[trait|error])") @length(min: 1) list httpResponseTests { member: HttpResponseTestCase, From c9ec20d08540e3755531286eef4f3c40d6d374ac Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 18 Apr 2020 14:43:50 -0700 Subject: [PATCH 2/7] Add selector support for "!=" The "!=" attribute comparator is used to find shapes that have an attribute, but the attribute is not equal to a given value. For example, the following selector: ``` [trait|error!=client] ``` Is a simplification of the following: ``` [trait|error]:not([trait|error=client]) ``` --- docs/source/spec/core/selectors.rst | 34 +++++++++----- .../model/selector/AttributeSelector.java | 1 + .../amazon/smithy/model/selector/Parser.java | 5 +- .../smithy/model/selector/SelectorTest.java | 46 ++++++++++++------- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 5e57310d023..4eb22f33a66 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -84,6 +84,10 @@ Attribute selectors support the following comparators: - Description * - ``=`` - Matches if the attribute value is equal to the expected value. + * - ``!=`` + - Matches if the attribute value is not equal to the expected value. + Note that this comparator is never matched if the resolved attribute + does not exist. * - ``^=`` - Matches if the attribute value starts with the expected value. * - ``$=`` @@ -92,13 +96,13 @@ Attribute selectors support the following comparators: - Matches if the attribute value contains with the expected value. Attribute comparisons can be made case-insensitive by preceding the closing -bracket with " i" (e.g., ``string[trait|time=DATE i]``). +bracket with " i" (for example, ``string[trait|time=DATE i]``). Matching traits ~~~~~~~~~~~~~~~ -We can match shapes based on traits using an *attribute selector*. The +Shapes can be matched based on traits using an *attribute selector*. The following selector finds all structure shapes with the :ref:`error-trait` trait: @@ -107,18 +111,26 @@ trait: structure[trait|error] The ``trait|`` is called a *namespace prefix*. This particular prefix tells -the selector that we are interested in a trait applied to the current shape, -and that that specific trait is ``time``. +the selector to match on a trait applied to the current shape, and that that +specific trait is ``time``. -We can match string shapes that have a specific trait value: +The following selector matches string shapes that have a specific trait +value: .. code-block:: none structure[trait|error=client] +The following selector matches string shapes that have a specific trait +but the trait value does not match the provided value: + +.. code-block:: none + + structure[trait|error!=client] + Matching on trait values only works for traits that have a scalar value -(e.g., strings, numbers, and booleans). We can also match case-insensitvely -on the value by appending " i" before the closing bracket: +(for example, strings, numbers, and booleans). Selectors can also match +case-insensitively on the value by appending " i" before the closing bracket: .. code-block:: none @@ -203,7 +215,7 @@ Available attributes - ``service[service|version^='2018-']`` * - ``trait|*`` - Gets the value of a trait applied to a shape, where "*" is the name - of a trait (e.g., ``trait|error``). Boolean trait values are + of a trait (for example, ``trait|error``). Boolean trait values are converted to "true" or "false". - ``client`` @@ -227,8 +239,8 @@ every map: map > member -We can return just the key members or just the value members by adding an -attribute selector on the ``id|member``: +Selectors can return just the key members or just the value members by adding +an attribute selector on the ``id|member``: .. code-block:: none @@ -663,7 +675,7 @@ Selectors are defined by the following ABNF_ grammar. attr_value :`attr_identifier` / `selector_text` attr_identifier :1*(ALPHA / DIGIT / "_") *(ALPHA / DIGIT / "_" / "-" / "." / "#") service_attribute :"service|version" - comparator :"^=" / "$=" / "*=" / "=" + comparator :"^=" / "$=" / "*=" / "!=" / "=" function_expression :":" `function` "(" `selector` *("," `selector`) ")" function :"test" / "is" / "not" / "of" selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` 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 8d53018ff07..e77b2f134f0 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 @@ -33,6 +33,7 @@ */ final class AttributeSelector implements Selector { static final Comparator EQUALS = String::equals; + static final Comparator NOT_EQUALS = (a, b) -> !a.equals(b); static final Comparator STARTS_WITH = String::startsWith; static final Comparator ENDS_WITH = String::endsWith; static final Comparator CONTAINS = String::contains; 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 94139b5dbd9..047ae7d3563 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 @@ -42,7 +42,7 @@ final class Parser { private static final Set REL_TYPES = new HashSet<>(); private static final List ATTRIBUTES = ListUtils.of( "trait|", "id|namespace", "id|name", "id|member", "id", "service|version"); - private static final List AFTER_ATTRIBUTE = ListUtils.of("=", "^=", "$=", "*=", "]"); + private static final List AFTER_ATTRIBUTE = ListUtils.of("=", "!=", "^=", "$=", "*=", "]"); private static final List AFTER_ATTRIBUTE_RHS = ListUtils.of("i]", "]"); private static final List START_FUNCTION = ListUtils.of("("); private static final List FUNCTION_ARG_NEXT_TOKEN = ListUtils.of(")", ","); @@ -210,6 +210,9 @@ private Selector parseAttribute() { case "=": comparator = AttributeSelector.EQUALS; break; + case "!=": + comparator = AttributeSelector.NOT_EQUALS; + break; case "^=": comparator = AttributeSelector.STARTS_WITH; break; 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 a867e7edddf..7f929082d7d 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 @@ -21,7 +21,9 @@ import static org.hamcrest.Matchers.equalTo; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; @@ -36,13 +38,37 @@ public class SelectorTest { - @Test - public void selectsCollections() { - Model model = Model.assembler().addImport(getClass().getResource("model.json")) + private static Model modelJson; + + @BeforeAll + public static void before() { + modelJson = Model.assembler().addImport(SelectorTest.class.getResource("model.json")) .disablePrelude() .assemble() .unwrap(); - Set result = Selector.parse("collection").select(model); + } + + @Test + public void supportsDeprecatedEachFunction() { + Set result1 = Selector.parse(":each(collection)").select(modelJson); + Set result2 = Selector.parse(":is(collection)").select(modelJson); + + assertThat(result1, equalTo(result2)); + } + + @Test + public void supportsNotEqualsAttribute() { + Set result = Selector.parse("[id|member!=member]").select(modelJson).stream() + .map(Shape::getId) + .map(ShapeId::toString) + .collect(Collectors.toSet()); + + assertThat(result, containsInAnyOrder("ns.foo#Map$key", "ns.foo#Map$value")); + } + + @Test + public void selectsCollections() { + Set result = Selector.parse("collection").select(modelJson); assertThat(result, containsInAnyOrder( SetShape.builder() @@ -129,16 +155,4 @@ public void toleratesUnexpectedFunctionNames() { assertThat(expr, equalTo(selector.toString())); } - - @Test - public void supportsDeprecatedEachFunction() { - Model model = Model.assembler().addImport(getClass().getResource("model.json")) - .disablePrelude() - .assemble() - .unwrap(); - Set result1 = Selector.parse(":each(collection)").select(model); - Set result2 = Selector.parse(":is(collection)").select(model); - - assertThat(result1, equalTo(result2)); - } } From a4a9daf17edc4d5d7a33e6d99ed154290c71ea37 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sun, 19 Apr 2020 12:42:27 -0700 Subject: [PATCH 3/7] Add support for selecting nested trait properties Nested trait properties can be selected using a "|" delimited syntax. Values or lists and objects can be "projected" using the special `(values)` property. Keys of objects can be projected using the special `(keys)` property. --- docs/source/spec/core/selectors.rst | 212 ++++++++++++------ .../model/selector/AttributeSelector.java | 4 +- .../amazon/smithy/model/selector/Parser.java | 21 +- .../model/selector/TraitAttributeKey.java | 85 ++++++- .../smithy/model/traits/EnumDefinition.java | 67 +++++- .../amazon/smithy/model/traits/EnumTrait.java | 27 +-- .../smithy/model/selector/SelectorTest.java | 99 +++++++- .../model/selector/nested-traits.smithy | 70 ++++++ 8 files changed, 468 insertions(+), 117 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 4eb22f33a66..ee939173154 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -88,7 +88,7 @@ Attribute selectors support the following comparators: - Matches if the attribute value is not equal to the expected value. Note that this comparator is never matched if the resolved attribute does not exist. - * - ``^=`` + * - ``~=`` - Matches if the attribute value starts with the expected value. * - ``$=`` - Matches if the attribute value ends with the expected value. @@ -99,125 +99,190 @@ Attribute comparisons can be made case-insensitive by preceding the closing bracket with " i" (for example, ``string[trait|time=DATE i]``). -Matching traits -~~~~~~~~~~~~~~~ +``id`` attribute +~~~~~~~~~~~~~~~~ -Shapes can be matched based on traits using an *attribute selector*. The -following selector finds all structure shapes with the :ref:`error-trait` -trait: +Gets the full shape ID of a shape. + +The following example matches only the ``foo.baz#Structure`` shape: .. code-block:: none - structure[trait|error] + [id=foo.baz#Structure] + +The following example matches only the ``foo.baz#Structure$foo`` shape: + +.. code-block:: none + + [id=foo.baz#Structure$foo] -The ``trait|`` is called a *namespace prefix*. This particular prefix tells -the selector to match on a trait applied to the current shape, and that that -specific trait is ``time``. -The following selector matches string shapes that have a specific trait -value: +``id|namespace`` attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Gets the namespace part of a shape ID. + +The following example matches all shapes in the ``foo.baz`` namespace: .. code-block:: none - structure[trait|error=client] + [id|namespace=foo.baz] + -The following selector matches string shapes that have a specific trait -but the trait value does not match the provided value: +``id|name`` attribute +~~~~~~~~~~~~~~~~~~~~~ + +Gets the name part of a shape ID. + +The following example matches all shapes in the model that have a shape +name of ``MyShape``. .. code-block:: none - structure[trait|error!=client] + [id|name=MyShape] + + +``id|member`` attribute +~~~~~~~~~~~~~~~~~~~~~~~ + +Gets the member part of a shape ID (if available). + +The following example matches all members in the model that have a member +name of ``foo``. + +.. code-block:: none + + [id|member=foo] + + +``service|version`` attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Gets the version property of a service shape if the shape is +a service. + +The following example matches all service shapes that have a version +property that starts with ``2018-``: + +.. code-block:: none -Matching on trait values only works for traits that have a scalar value -(for example, strings, numbers, and booleans). Selectors can also match -case-insensitively on the value by appending " i" before the closing bracket: + [service|version~='2018-'] + + +``trait|*`` attribute +~~~~~~~~~~~~~~~~~~~~~ + +Gets the value of a trait applied to a shape, where "*" is the ID +of a trait. The ``smithy.api`` namespace MAY be omitted from shape IDs +provided to the ``trait`` attribute. Traits are converted to their +serialized :token:`node ` form when matching against their values. +Only string, Boolean, and numeric values can be compared with an expected +value. Boolean values are converted to "true" or "false". Numeric values +are converted to their string representation. + +The following selector finds all structure shapes with the :ref:`error-trait` +trait, and the ``error`` trait can be set to any value: + +.. code-block:: none + + structure[trait|error] + +The following selector finds all structure shapes with the :ref:`error-trait` +set to ``client``: .. code-block:: none - structure[trait|error=CLIENT i] + structure[trait|error=client] -Fully-qualified trait names are also supported: +The following selector finds all structure shapes with the :ref:`error-trait`, +but the trait is not set to ``client``: + +.. code-block:: none + + structure[trait|error!=client] + +Fully-qualified trait names are supported: .. code-block:: none string[trait|smithy.example#customTrait=foo] -Matching on shape ID -~~~~~~~~~~~~~~~~~~~~ +Nested trait properties +^^^^^^^^^^^^^^^^^^^^^^^^ + +Nested properties of a trait can be selected using subsequent pipe (``|``) +delimited property names. -Attribute selectors can be used to match the :ref:`shape ID `. The -following example matches a single resource shape with an ID of -``smithy.example#Foo``: +The following example matches all shapes that have a :ref:`range-trait` +with a ``min`` property set to ``1``: .. code-block:: none - resource[id='smithy.example#Foo'] + [trait|range|min=1] -Notice that the value of an attribute selector can be quoted. The example -above uses single quotes, but double quotes work too. +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. -Smithy provides several attributes in the ``id`` namespace to make matching -on a shape ID easier. The following example finds all shapes that are in the -"smithy.example" namespace: +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 +``internal``: .. code-block:: none - resource[id|namespace=smithy.example] + [trait|enum|(values)|tags|(values)=internal] -Though not as clear, matching shapes in a specific namespace can also be -achieved using the ``^=`` comparator against ``id``: +An empty list is not considered present when checking for existence. + +The following example matches all shapes that have an ``enum`` trait, +the trait contains at least one ``enum`` entry, and one or more entries +contains a non-empty ``tags`` list. .. code-block:: none - resource[id^=smithy.example#] + [trait|enum|(values)|tags|(values)] + +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 member shapes that have a member name of -"key": +The following example matches all shapes that have an :ref:`externalDocumentation-trait` +that has a value set to ``https://example.com``: .. code-block:: none - resource[id|member=key] + [trait|externalDocumentation|(values)='https://example.com'] -Though not as clear, matching members with a member name of "key" can also be -achieved using the ``$=`` comparator against ``id``: +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 following example matches all shapes that have an ``externalDocumentation`` +trait that has an entry named ``Homepage``: .. code-block:: none - resource[id$="$key"] + [trait|externalDocumentation|(keys)=Homepage] +Like the ``(list)`` property, the ``(keys)`` property also treats empty +objects as not present. -Available attributes -~~~~~~~~~~~~~~~~~~~~ +The following example matches all shapes that have a trait named +``myMapTrait`` that has at least one entry: -.. list-table:: - :header-rows: 1 - :widths: 10 50 40 +.. code-block:: none - * - Attribute - - Description - - Example result - * - ``id`` - - The full shape ID of a shape - - ``foo.baz#Structure$memberName`` - * - ``id|namespace`` - - The namespace part of a shape ID - - ``foo.baz`` - * - ``id|name`` - - The name part of a shape ID - - ``Structure`` - * - ``id|member`` - - The member part of a shape ID (if available) - - ``memberName`` - * - ``service|version`` - - Gets the version property of a service shape if the shape is - a service. - - ``service[service|version^='2018-']`` - * - ``trait|*`` - - Gets the value of a trait applied to a shape, where "*" is the name - of a trait (for example, ``trait|error``). Boolean trait values are - converted to "true" or "false". - - ``client`` + [trait|smithy.example#myMapTrait|(keys)] + +Implementations MUST tolerate expressions that do not perform a valid +traversal of a trait. The following example attempts to descend into +non-existent properties of the :ref:`documentation-trait`. This example +MUST not cause an error and MUST match no shapes: + +.. code-block:: none + + [trait|documentation|invalid|child=Hi] Neighbors @@ -671,11 +736,12 @@ Selectors are defined by the following ABNF_ grammar. attr :"[" `attr_key` *(`comparator` `attr_value` ["i"]) "]" attr_key :`id_attribute` / `trait_attribute` / `service_attribute` id_attribute :"id" ["|" ("namespace" / "name" / "member")] - trait_attribute :"trait" "|" `attr_value` *("|" `attr_value`) + trait_attribute :"trait" "|" `attr_value` *("|" `trait_attr_value`) attr_value :`attr_identifier` / `selector_text` attr_identifier :1*(ALPHA / DIGIT / "_") *(ALPHA / DIGIT / "_" / "-" / "." / "#") + trait_attr_value :"(values)" / "(keys)" / `attr_value` service_attribute :"service|version" - comparator :"^=" / "$=" / "*=" / "!=" / "=" + comparator :"~=" / "$=" / "*=" / "!=" / "=" function_expression :":" `function` "(" `selector` *("," `selector`) ")" function :"test" / "is" / "not" / "of" selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` 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 e77b2f134f0..b374c4fca61 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 @@ -91,6 +91,8 @@ private boolean matchesAttribute(List result) { String rhs = caseInsensitive ? expected.toLowerCase(Locale.US) : expected; return result.stream() .map(value -> caseInsensitive ? value.toLowerCase(Locale.US) : value) - .anyMatch(lhs -> comparator.apply(lhs, rhs)); + // The returned attribute value might be null if + // the value exists, but isn't comparable. + .anyMatch(lhs -> lhs != null && comparator.apply(lhs, rhs)); } } 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 047ae7d3563..d73d83b2a07 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 @@ -236,7 +236,7 @@ private Selector parseAttribute() { private AttributeSelector.KeyGetter parseAttributeKey() { String namespace = expect(ATTRIBUTES); switch (namespace) { - case "trait|": return new TraitAttributeKey(parseAttributeValue()); + case "trait|": return new TraitAttributeKey(parseAttributeValue(), parsePipeDelimitedTraitAttributes()); case "id": return AttributeSelector.KEY_ID; case "id|namespace": return AttributeSelector.KEY_ID_NAMESPACE; case "id|name": return AttributeSelector.KEY_ID_NAME; @@ -246,6 +246,25 @@ private AttributeSelector.KeyGetter parseAttributeKey() { } } + private List parsePipeDelimitedTraitAttributes() { + List result = new ArrayList<>(); + ws(); + + while (charPeek() == '|') { + position++; + ws(); + if (compareExpressionSlice("(values)")) { + result.add(expect(Collections.singleton("(values)"))); + } else if (compareExpressionSlice("(keys)")) { + result.add(expect(Collections.singleton("(keys)"))); + } else { + result.add(parseAttributeValue()); + } + } + + return result; + } + private String parseAttributeValue() { switch (charPeek()) { case '\'': return consumeInside('\''); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TraitAttributeKey.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TraitAttributeKey.java index a469c059125..a25a58a4773 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TraitAttributeKey.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TraitAttributeKey.java @@ -15,37 +15,104 @@ package software.amazon.smithy.model.selector; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import software.amazon.smithy.model.node.BooleanNode; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NodeVisitor; import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.utils.ListUtils; final class TraitAttributeKey implements AttributeSelector.KeyGetter { + private static final NodeToString NODE_TO_STRING = new NodeToString(); + private final String traitName; + private final List traitPath; - private final String trait; + TraitAttributeKey(String traitName, List traitPath) { + this.traitName = traitName; + this.traitPath = traitPath; + } - TraitAttributeKey(String trait) { - this.trait = trait; + TraitAttributeKey(String traitName) { + this(traitName, Collections.emptyList()); } @Override public List apply(Shape shape) { - return shape.findTrait(trait) - .map(Trait::toNode) - .map(node -> ListUtils.of(node.accept(NODE_TO_STRING))) - .orElse(ListUtils.of()); + Trait trait = shape.findTrait(traitName).orElse(null); + + if (trait == null) { + // An empty list means the trait does not exist. + return Collections.emptyList(); + } else if (traitPath.isEmpty()) { + // A list with a value of null means it exists but isn't comparable. + // A list with a non-null values means it exists and is comparable. + return Collections.singletonList(trait.toNode().accept(NODE_TO_STRING)); + } else { + // Path into the trait to see if a value exists / is comparable. + List result = new ArrayList<>(); + evaluateNode(trait.toNode(), 0, result); + return result; + } + } + + private void evaluateNode(Node node, int pathPosition, List result) { + // Terminal state attempts to take the current value and + // add it to the result. This is executes when pathing into + // object node values. + if (pathPosition >= traitPath.size()) { + result.add(node.accept(NODE_TO_STRING)); + return; + } + + String path = traitPath.get(pathPosition); + if (node.isObjectNode()) { + ObjectNode value = node.expectObjectNode(); + if (path.equals("(keys)")) { + projectedEvaluate(value.getMembers().keySet(), pathPosition + 1, result); + } else if (path.equals("(values)")) { + projectedEvaluate(value.getMembers().values(), pathPosition + 1, result); + } else if (value.getMember(path).isPresent()) { + evaluateNode(value.expectMember(path), pathPosition + 1, result); + } + } else if (node.isArrayNode()) { + // The only valid path after an array is (values). + if (path.equals("(values)")) { + projectedEvaluate(node.expectArrayNode().getElements(), pathPosition + 1, result); + } + } + } + + private void projectedEvaluate(Collection nodes, int pathPosition, List result) { + // If projecting on the last path item (i.e., an expression that ends + // with (values)), then populate the result set with the evaluated values. + if (pathPosition == traitPath.size()) { + // Note that empty lists do not appear in the result set. Do not + // project lists if you need to match on empty lists. + for (Node element : nodes) { + result.add(element.accept(NODE_TO_STRING)); + } + } else { + // Continue projecting and evaluating values inside of the trait. + for (Node element : nodes) { + evaluateNode(element, pathPosition, result); + } + } } + // Only strings, booleans, and numbers are converted to + // comparable strings. All other values become null, meaning + // that the value is present, but not actually comparable. private static final class NodeToString extends NodeVisitor.Default { @Override protected String getDefault(Node node) { - return ""; + return null; } @Override diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index ecaab9f53e1..30ddc6cfea5 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -20,6 +20,10 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.node.ToNode; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.Tagged; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -27,22 +31,26 @@ /** * An enum definition for the enum trait. */ -public final class EnumDefinition implements ToSmithyBuilder, Tagged { +public final class EnumDefinition implements ToNode, ToSmithyBuilder, Tagged { + public static final String VALUE = "value"; public static final String NAME = "name"; public static final String DOCUMENTATION = "documentation"; public static final String TAGS = "tags"; + public static final String DEPRECATED = "deprecated"; private final String value; private final String documentation; private final List tags; private final String name; + private final boolean deprecated; private EnumDefinition(Builder builder) { value = SmithyBuilder.requiredState("value", builder.value); name = builder.name; documentation = builder.documentation; tags = new ArrayList<>(builder.tags); + deprecated = builder.deprecated; } public static Builder builder() { @@ -61,6 +69,45 @@ public Optional getDocumentation() { return Optional.ofNullable(documentation); } + public boolean isDeprecated() { + return deprecated; + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = Node.objectNodeBuilder(); + builder.withMember(EnumDefinition.VALUE, getValue()) + .withOptionalMember(EnumDefinition.NAME, getName().map(Node::from)) + .withOptionalMember(EnumDefinition.DOCUMENTATION, getDocumentation().map(Node::from)); + + if (!tags.isEmpty()) { + builder.withMember(EnumDefinition.TAGS, Node.fromStrings(getTags())); + } + + if (isDeprecated()) { + builder.withMember(EnumDefinition.DEPRECATED, true); + } + + return builder.build(); + } + + public static EnumDefinition fromNode(Node node) { + ObjectNode value = node.expectObjectNode(); + EnumDefinition.Builder builder = EnumDefinition.builder() + .value(value.expectStringMember(EnumDefinition.VALUE).getValue()) + .name(value.getStringMember(EnumDefinition.NAME).map(StringNode::getValue).orElse(null)) + .documentation(value.getStringMember(EnumDefinition.DOCUMENTATION) + .map(StringNode::getValue) + .orElse(null)) + .deprecated(value.getBooleanMemberOrDefault(EnumDefinition.DEPRECATED)); + + value.getMember(EnumDefinition.TAGS).ifPresent(tags -> { + builder.tags(Node.loadArrayOfString(EnumDefinition.TAGS, tags)); + }); + + return builder.build(); + } + @Override public List getTags() { return tags; @@ -68,7 +115,12 @@ public List getTags() { @Override public Builder toBuilder() { - return builder().value(value).tags(tags).documentation(documentation).name(name); + return builder() + .name(name) + .value(value) + .tags(tags) + .documentation(documentation) + .deprecated(deprecated); } @Override @@ -81,12 +133,13 @@ public boolean equals(Object other) { return value.equals(otherEnum.value) && Objects.equals(name, otherEnum.name) && Objects.equals(documentation, otherEnum.documentation) - && tags.equals(otherEnum.tags); + && tags.equals(otherEnum.tags) + && deprecated == otherEnum.deprecated; } @Override public int hashCode() { - return Objects.hash(value, name, tags, documentation); + return Objects.hash(value, name, tags, documentation, deprecated); } /** @@ -96,6 +149,7 @@ public static final class Builder implements SmithyBuilder { private String value; private String documentation; private String name; + private boolean deprecated; private final List tags = new ArrayList<>(); @Override @@ -133,5 +187,10 @@ public Builder clearTags() { tags.clear(); return this; } + + public Builder deprecated(boolean deprecated) { + this.deprecated = deprecated; + return this; + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java index 4f1e0c0b82f..352f079bd30 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java @@ -22,7 +22,6 @@ import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; -import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -75,14 +74,7 @@ public boolean hasNames() { @Override protected Node createNode() { - return definitions.stream() - .map(definition -> Node.objectNodeBuilder() - .withMember(EnumDefinition.VALUE, definition.getValue()) - .withOptionalMember(EnumDefinition.NAME, definition.getName().map(Node::from)) - .withOptionalMember(EnumDefinition.DOCUMENTATION, - definition.getDocumentation().map(Node::from)) - .build()) - .collect(ArrayNode.collect()); + return definitions.stream().map(EnumDefinition::toNode).collect(ArrayNode.collect()); } @Override @@ -141,24 +133,9 @@ public ShapeId getShapeId() { public EnumTrait createTrait(ShapeId target, Node value) { Builder builder = builder().sourceLocation(value); for (ObjectNode definition : value.expectArrayNode().getElementsAs(ObjectNode.class)) { - builder.addEnum(parseEnum(definition)); + builder.addEnum(EnumDefinition.fromNode(definition)); } return builder.build(); } - - private EnumDefinition parseEnum(ObjectNode value) { - EnumDefinition.Builder builder = EnumDefinition.builder() - .value(value.expectStringMember(EnumDefinition.VALUE).getValue()) - .name(value.getStringMember(EnumDefinition.NAME).map(StringNode::getValue).orElse(null)) - .documentation(value.getStringMember(EnumDefinition.DOCUMENTATION) - .map(StringNode::getValue) - .orElse(null)); - - value.getMember(EnumDefinition.TAGS).ifPresent(node -> { - builder.tags(Node.loadArrayOfString(EnumDefinition.TAGS, node)); - }); - - return builder.build(); - } } } 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 7f929082d7d..31bc747ced6 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 @@ -18,8 +18,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; @@ -39,6 +43,7 @@ public class SelectorTest { private static Model modelJson; + private static Model traitModel; @BeforeAll public static void before() { @@ -46,6 +51,10 @@ public static void before() { .disablePrelude() .assemble() .unwrap(); + traitModel = Model.assembler() + .addImport(SelectorTest.class.getResource("nested-traits.smithy")) + .assemble() + .unwrap(); } @Test @@ -56,16 +65,98 @@ public void supportsDeprecatedEachFunction() { assertThat(result1, equalTo(result2)); } - @Test - public void supportsNotEqualsAttribute() { - Set result = Selector.parse("[id|member!=member]").select(modelJson).stream() + private List ids(Model model, String expression) { + return Selector.parse(expression) + .select(model) + .stream() .map(Shape::getId) .map(ShapeId::toString) - .collect(Collectors.toSet()); + .collect(Collectors.toList()); + } + + @Test + public void selectsUsingNestedTraitValues() { + List result = ids(traitModel, "[trait|range|min=1]"); + + assertThat(result, hasItem("smithy.example#RangeInt1")); + assertThat(result, not(hasItem("smithy.example#RangeInt2"))); + assertThat(result, not(hasItem("smithy.example#EnumString"))); + } + + @Test + public void selectsUsingNestedTraitValuesUsingNegation() { + List result = ids(traitModel, "[trait|range|min!=1]"); + + assertThat(result, hasItem("smithy.example#RangeInt2")); + assertThat(result, not(hasItem("smithy.example#RangeInt1"))); + assertThat(result, not(hasItem("smithy.example#EnumString"))); + } + + @Test + public void selectsUsingNestedTraitValuesThroughProjection() { + List result = ids(traitModel, "[trait|enum|(values)|deprecated=true]"); + + assertThat(result, hasItem("smithy.example#EnumString")); + assertThat(result, not(hasItem("smithy.example#DocumentedString"))); + } + + @Test + public void canSelectOnTraitObjectKeys() { + List result = ids(traitModel, "[trait|externalDocumentation|(keys)=Homepage]"); + + assertThat(result, hasItem("smithy.example#DocumentedString1")); + assertThat(result, not(hasItem("smithy.example#DocumentedString2"))); + } + + @Test + public void canSelectOnTraitObjectValues() { + List result = ids(traitModel, "[trait|externalDocumentation|(values)='https://www.anotherexample.com/']"); + + assertThat(result, hasItem("smithy.example#DocumentedString2")); + assertThat(result, not(hasItem("smithy.example#DocumentedString1"))); + } + + @Test + public void pathThroughTerminalValueReturnsNoResults() { + List result = ids(traitModel, "[trait|documentation|foo|baz='nope']"); + + assertThat(result, empty()); + } + + @Test + public void pathThroughArrayWithInvalidItemReturnsNoResults() { + List result = ids(traitModel, "[trait|tags|foo|baz='nope']"); + + assertThat(result, empty()); + } + + @Test + public void supportsNotEqualsAttribute() { + List result = ids(modelJson, "[id|member!=member]"); assertThat(result, containsInAnyOrder("ns.foo#Map$key", "ns.foo#Map$value")); } + @Test + public void supportsMatchingDeeplyOnTraitValues() { + List result1 = ids(traitModel, "[trait|smithy.example#nestedTrait|foo|foo|bar='hi']"); + List result2 = ids(traitModel, "[trait|smithy.example#nestedTrait|foo|foo|bar='bye']"); + + assertThat(result1, hasItem("smithy.example#DocumentedString1")); + assertThat(result1, not(hasItem("smithy.example#DocumentedString2"))); + assertThat(result2, hasItem("smithy.example#DocumentedString2")); + assertThat(result2, not(hasItem("smithy.example#DocumentedString1"))); + } + + @Test + public void emptyListDoesNotAppearWhenProjecting() { + List result = ids(traitModel, "[trait|enum|(values)|tags|(values)]"); + + assertThat(result, hasItem("smithy.example#EnumString")); + assertThat(result, hasItem("smithy.example#DocumentedString1")); + assertThat(result, not(hasItem("smithy.example#DocumentedString2"))); + } + @Test public void selectsCollections() { Set result = Selector.parse("collection").select(modelJson); 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 new file mode 100644 index 00000000000..5efe7ce85b7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/nested-traits.smithy @@ -0,0 +1,70 @@ +namespace smithy.example + +@enum([ + { + value: "t2.nano", + name: "T2_NANO", + documentation: """ + T2 instances are Burstable Performance + Instances that provide a baseline level of CPU + performance with the ability to burst above the + baseline.""", + tags: ["ebsOnly"] + }, + { + value: "t2.micro", + name: "T2_MICRO", + documentation: """ + T2 instances are Burstable Performance + Instances that provide a baseline level of CPU + performance with the ability to burst above the + baseline.""", + tags: ["ebsOnly"] + }, + { + value: "m256.mega", + name: "M256_MEGA", + deprecated: true + } +]) +@tags(["foo", "baz"]) +string EnumString + +@range(min: 1, max: 10) +integer RangeInt1 + +@range(min: 100, max: 1000) +integer RangeInt2 + +@externalDocumentation( + "Homepage": "https://www.example.com/", + "API Reference": "https://www.example.com/api-ref", +) +@enum([ + { + value: "m256.mega", + name: "M256_MEGA", + tags: ["notEbs"] + } +]) +@nestedTrait(foo: {foo: {bar: "hi"}}) +string DocumentedString1 + +@documentation("Hi") +@externalDocumentation("Foo": "https://www.anotherexample.com/") +@nestedTrait(foo: {foo: {bar: "bye"}}) +@enum([{value: "m256.mega", tags: []}]) +string DocumentedString2 + +@trait +structure nestedTrait { + foo: MoreNesting, +} + +structure MoreNesting { + foo: EvenMoreNesting, +} + +structure EvenMoreNesting { + bar: String, +} From ef01725a7fe754ac547dac85e29289bfab44ea72 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sun, 19 Apr 2020 19:29:55 -0700 Subject: [PATCH 4/7] Fine-tune Selector grammar and parser * Fix "~=" in docs to properly reference "^=". * Rewrite Selector parser to be more restrictive and fix several bugs. * Adds support for numbers in attribute selectors. * Shape IDs with members must be quoted. * The parser and grammar are more resilient to unknown attributes, functions, and attribute paths. * pseudo-properties are now formalized in the grammar and parser as `"(" identifier ")"`, allowing us to add more in the future as needed. --- docs/source/spec/core/lexical-structure.rst | 7 +- docs/source/spec/core/selectors.rst | 116 ++--- .../emit-each-selector-validator.errors | 62 ++- .../model/selector/AttributeSelector.java | 7 +- .../amazon/smithy/model/selector/Parser.java | 397 +++++++++++++----- .../smithy/model/selector/SelectorTest.java | 194 ++++++++- 6 files changed, 560 insertions(+), 223 deletions(-) diff --git a/docs/source/spec/core/lexical-structure.rst b/docs/source/spec/core/lexical-structure.rst index 2cbc7b4ed02..dde9716e2e9 100644 --- a/docs/source/spec/core/lexical-structure.rst +++ b/docs/source/spec/core/lexical-structure.rst @@ -281,9 +281,10 @@ Shape IDs are formally defined by the following ABNF: .. productionlist:: smithy identifier :(ALPHA / "_") *(ALPHA / DIGIT / "_") namespace :`identifier` *("." `identifier`) - shape_id :`absolute_shape_id` / `relative_shape_id` - absolute_shape_id :`namespace` "#" `relative_shape_id` - relative_shape_id :`identifier` ["$" `identifier`] + shape_id :`root_shape_id` [`shape_id_member`] + root_shape_id :`absolute_shape_id` / `identifier` + absolute_shape_id :`namespace` "#" `identifier` + shape_id_member :"$" `identifier` LOALPHA :%x61-7A ; a-z .. admonition:: Lexical note diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index ee939173154..47efd1e6049 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -88,7 +88,7 @@ Attribute selectors support the following comparators: - Matches if the attribute value is not equal to the expected value. Note that this comparator is never matched if the resolved attribute does not exist. - * - ``~=`` + * - ``^=`` - Matches if the attribute value starts with the expected value. * - ``$=`` - Matches if the attribute value ends with the expected value. @@ -98,6 +98,12 @@ Attribute selectors support the following comparators: Attribute comparisons can be made case-insensitive by preceding the closing bracket with " i" (for example, ``string[trait|time=DATE i]``). +.. important:: + + Implementations MUST NOT fail when unknown attribute keys are + encountered; implementations SHOULD emit a warning and match no results + when an unknown attribute is encountered. + ``id`` attribute ~~~~~~~~~~~~~~~~ @@ -110,11 +116,12 @@ The following example matches only the ``foo.baz#Structure`` shape: [id=foo.baz#Structure] -The following example matches only the ``foo.baz#Structure$foo`` shape: +Matching on a shape ID that contains a member requires that the shape ID +is enclosed in single or double quotes: .. code-block:: none - [id=foo.baz#Structure$foo] + [id='foo.baz#Structure$foo'] ``id|namespace`` attribute @@ -166,7 +173,7 @@ property that starts with ``2018-``: .. code-block:: none - [service|version~='2018-'] + [service|version^='2018-'] ``trait|*`` attribute @@ -289,16 +296,16 @@ Neighbors ========= The *current* shapes evaluated by a selector is changed using a -:token:`neighbor` token. +:token:`selector_neighbor` token. Undirected neighbor ~~~~~~~~~~~~~~~~~~~ -An :token:`undirected neighbor ` (``>``) changes the -current set of shapes to every shape that is connected to the current shapes. -For example, the following selector returns the key and value members of -every map: +An :token:`undirected neighbor ` (``>``) changes +the current set of shapes to every shape that is connected to the current +shapes. For example, the following selector returns the key and value +members of every map: .. code-block:: none @@ -685,69 +692,32 @@ Selectors are defined by the following ABNF_ grammar. changing the semantics of a selector. .. productionlist:: selectors - selector :`selector_expression` *(`selector_expression`) - selector_expression :`shape_types` / `attr` / `function_expression` / `neighbor` - shape_types :"*" - :/ "blob" - :/ "boolean" - :/ "document" - :/ "string" - :/ "byte" - :/ "short" - :/ "integer" - :/ "long" - :/ "float" - :/ "double" - :/ "bigDecimal" - :/ "bigInteger" - :/ "timestamp" - :/ "list" - :/ "map" - :/ "set" - :/ "structure" - :/ "union" - :/ "service" - :/ "operation" - :/ "resource" - :/ "member" - :/ "number" - :/ "simpleType" - :/ "collection" - neighbor :`undirected_neighbor` / `directed_neighbor` / `recursive_neighbor` - undirected_neighbor :">" - directed_neighbor :"-[" `relationship_type` *("," `relationship_type`) "]->" - recursive_neighbor :"~>" - relationship_type :"identifier" - :/ "create" - :/ "read" - :/ "update" - :/ "delete" - :/ "list" - :/ "member" - :/ "input" - :/ "output" - :/ "error" - :/ "operation" - :/ "collectionOperation" - :/ "instanceOperation" - :/ "resource" - :/ "bound" - :/ "trait" - attr :"[" `attr_key` *(`comparator` `attr_value` ["i"]) "]" - attr_key :`id_attribute` / `trait_attribute` / `service_attribute` - id_attribute :"id" ["|" ("namespace" / "name" / "member")] - trait_attribute :"trait" "|" `attr_value` *("|" `trait_attr_value`) - attr_value :`attr_identifier` / `selector_text` - attr_identifier :1*(ALPHA / DIGIT / "_") *(ALPHA / DIGIT / "_" / "-" / "." / "#") - trait_attr_value :"(values)" / "(keys)" / `attr_value` - service_attribute :"service|version" - comparator :"~=" / "$=" / "*=" / "!=" / "=" - function_expression :":" `function` "(" `selector` *("," `selector`) ")" - function :"test" / "is" / "not" / "of" - selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` - selector_single_quoted_text :"'" 1*`selector_single_quoted_char` "'" - selector_double_quoted_text :DQUOTE 1*`selector_double_quoted_char` DQUOTE - selector_single_quoted_char :%x20-26 / %x28-5B / %x5D-10FFFF ; Excludes (') - selector_double_quoted_char :%x20-21 / %x23-5B / %x5D-10FFFF ; Excludes (") + selector :`selector_expression` *(`selector_expression`) + selector_expression :`selector_shape_types` + :/ `selector_attr` + :/ `selector_function_expression` + :/ `selector_neighbor` + selector_shape_types :"*" / `identifier` + selector_neighbor :`selector_undirected_neighbor` + :/ `selector_directed_neighbor` + :/ `selector_recursive_neighbor` + selector_undirected_neighbor :">" + selector_directed_neighbor :"-[" `selector_rel_type` *("," `selector_rel_type`) "]->" + selector_recursive_neighbor :"~>" + selector_rel_type :`identifier` + selector_attr :"[" `selector_key` *(`selector_comparator` `selector_value` ["i"]) "]" + selector_key :`identifier` *("|" `selector_key_path`) + selector_key_path :`selector_pseudo_key` / `selector_value` + selector_value :`selector_text` / `number` / `root_shape_id` + selector_absolute_root_shape_id :`namespace` "#" `identifier` + selector_pseudo_key :"(" `identifier` ")" + selector_comparator :"^=" / "$=" / "*=" / "!=" / "=" + selector_function_expression :":" `selector_function` "(" `selector` *("," `selector`) ")" + selector_function :`identifier` + selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` + selector_single_quoted_text :"'" 1*`selector_single_quoted_char` "'" + selector_double_quoted_text :DQUOTE 1*`selector_double_quoted_char` DQUOTE + selector_single_quoted_char :%x20-26 / %x28-5B / %x5D-10FFFF ; Excludes (') + selector_double_quoted_char :%x20-21 / %x23-5B / %x5D-10FFFF ; Excludes (") .. _ABNF: https://tools.ietf.org/html/rfc5234 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 33fb03e4241..f8272884761 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,35 +96,33 @@ [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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `!`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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'`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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"`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 0 of 7, near `invalid`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 6, near `foo|]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 `|]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 4, near `a=]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 4, near `a=b`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 1 of 7, near `foo=']`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 7, near `foo="]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 12, near `foo==value]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 1 of 9, near `foo^foo]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; 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 0 of 9, near `foo -[]->`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 0 of 14, near `foo -[input]->`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 ``: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `)`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; 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 `))`: Expected one of the following tokens: `*`, `-[`, `:`, `>`, `[`, `bigDecimal`, `bigInteger`, `blob`, `boolean`, `byte`, `collection`, `document`, `double`, `float`, `integer`, `list`, `long`, `map`, `member`, `number`, `operation`, `resource`, `service`, `set`, `short`, `simpleType`, `string`, `structure`, `timestamp`, `union`, `~>`; expression `:is(string, :not())` | 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 1 of 5, near `foo]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; expression `[foo]` | Model -[ERROR] -: Error creating `EmitEachSelector` validator: Deserialization error at (/selector): unable to create software.amazon.smithy.model.selector.Selector from "[foo=baz]": Unable to deserialize Node using fromNode method: Syntax error at character 1 of 9, near `foo=baz]`: Expected one of the following tokens: `id`, `id|member`, `id|name`, `id|namespace`, `service|version`, `trait|`; expression `[foo=baz]` | 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 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/AttributeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java index b374c4fca61..ecac6451125 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 @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -26,6 +27,7 @@ import software.amazon.smithy.model.neighbor.NeighborProvider; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.FunctionalUtils; import software.amazon.smithy.utils.ListUtils; /** @@ -90,9 +92,10 @@ private boolean matchesAttribute(List result) { String rhs = caseInsensitive ? expected.toLowerCase(Locale.US) : expected; return result.stream() - .map(value -> caseInsensitive ? value.toLowerCase(Locale.US) : value) // The returned attribute value might be null if // the value exists, but isn't comparable. - .anyMatch(lhs -> lhs != null && comparator.apply(lhs, rhs)); + .filter(FunctionalUtils.not(Objects::isNull)) + .map(value -> caseInsensitive ? value.toLowerCase(Locale.ENGLISH) : value) + .anyMatch(lhs -> comparator.apply(lhs, rhs)); } } 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 d73d83b2a07..574b534790d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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. @@ -16,8 +16,6 @@ package software.amazon.smithy.model.selector; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -28,7 +26,6 @@ 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.model.validation.ValidationUtils; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.SetUtils; @@ -40,26 +37,12 @@ final class Parser { private static final Logger LOGGER = Logger.getLogger(Parser.class.getName()); private static final Set BREAK_TOKENS = SetUtils.of(',', ']', ')'); private static final Set REL_TYPES = new HashSet<>(); - private static final List ATTRIBUTES = ListUtils.of( - "trait|", "id|namespace", "id|name", "id|member", "id", "service|version"); - private static final List AFTER_ATTRIBUTE = ListUtils.of("=", "!=", "^=", "$=", "*=", "]"); - private static final List AFTER_ATTRIBUTE_RHS = ListUtils.of("i]", "]"); - private static final List START_FUNCTION = ListUtils.of("("); - private static final List FUNCTION_ARG_NEXT_TOKEN = ListUtils.of(")", ","); - private static final List MULTI_EDGE_NEXT_ARG_TOKEN = ListUtils.of(",", "]->"); - private static final List EXPRESSION_TOKENS = new ArrayList<>(Arrays.asList( - ":", "[", ">", "-[", "*", "~>", "number", "simpleType", "collection")); static { - // Adds selector relationship labels. + // Adds selector relationship labels for warnings when unknown relationship names are used. for (RelationshipType rel : RelationshipType.values()) { rel.getSelectorLabel().ifPresent(REL_TYPES::add); } - - // Adds all shape types as possible tokens. - for (ShapeType type : ShapeType.values()) { - EXPRESSION_TOKENS.add(type.toString()); - } } private final String expression; @@ -67,7 +50,6 @@ final class Parser { private Parser(String selector) { this.expression = selector; - ws(); // Skip leading whitespace. } static Selector parse(String selector) { @@ -75,99 +57,140 @@ static Selector parse(String selector) { } private List expression() { - List selectors = recursiveParse(); - ws(); // Skip trailing whitespace. - if (position != expression.length()) { - throw syntax("Invalid expression"); - } - return selectors; + return recursiveParse(); } private List recursiveParse() { List selectors = new ArrayList<>(); - // Require at least one selector. - selectors.add(createSelector(expect(EXPRESSION_TOKENS))); + + // 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 (position != expression.length() && !BREAK_TOKENS.contains(expression.charAt(position))) { - selectors.add(createSelector(expect(EXPRESSION_TOKENS))); + selectors.add(createSelector()); + // Always skip ws after calling createSelector. + ws(); } + return selectors; } + private Selector createSelector() { + ws(); + + // Require at least one selector. + switch (charPeek()) { + case ':': // function + position++; + return parseFunction(); + case '[': // attribute + position++; + return parseAttribute(); + case '>': // undirected neighbor + position++; + return new NeighborSelector(ListUtils.of()); + case '~': // ~> + position++; + expect('>'); + return new RecursiveNeighborSelector(); + case '-': // directed neighbor + position++; + expect('['); + return parseMultiEdgeDirectedNeighbor(); + case '*': // Any shape + position++; + return Selector.IDENTITY; + default: + if (validIdentifierStart(charPeek())) { + String identifier = parseIdentifier(); + 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 { + char c = charPeek(); + if (c == Character.MIN_VALUE) { + throw syntax("Unexpected selector EOF"); + } else { + throw syntax("Unexpected selector character: " + charPeek()); + } + } + } + } + private void ws() { - while (position < expression.length() && Character.isWhitespace(expression.charAt(position))) { + while (position < expression.length() && isWhitespace(expression.charAt(position))) { position++; } } + 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); } - private String expect(Collection tokens) { - for (String token : tokens) { - if (compareExpressionSlice(token)) { - position += token.length(); - ws(); // Skip whitespace after taking token. + private char expect(char... tokens) { + for (char token : tokens) { + if (charPeek() == token) { + position++; return token; } } - throw syntax("Expected one of the following tokens: " + ValidationUtils.tickedList(tokens)); - } - - private boolean compareExpressionSlice(String token) { - if (token.length() > expression.length() - position) { - return false; - } - - for (int i = 0; i < token.length(); i++) { - if (token.charAt(i) != expression.charAt(position + i)) { - return false; - } + StringBuilder message = new StringBuilder("Expected one of the following tokens:"); + for (char c : tokens) { + message.append(' ').append('\'').append(c).append('\''); } - return true; + throw syntax(message.toString()); } private SelectorSyntaxException syntax(String message) { return new SelectorSyntaxException(message, expression, position); } - private Selector createSelector(String token) { - switch (token) { - case ">": return new NeighborSelector(ListUtils.of()); - case "-[": return parseMultiEdgeDirectedNeighbor(); - case "[": return parseAttribute(); - case ":": return parseFunction(); - case "*": return Selector.IDENTITY; - case "~>": return new RecursiveNeighborSelector(); - case "number": return new ShapeTypeCategorySelector(NumberShape.class); - case "simpleType": return new ShapeTypeCategorySelector(SimpleShape.class); - case "collection": return new ShapeTypeCategorySelector(CollectionShape.class); - // Anything else matches shapes by name (e.g., "structure"). - default: - ShapeType shape = ShapeType.fromString(token).orElseThrow(() -> syntax("Unreachable token " + token)); - return new ShapeTypeSelector(shape); - } - } - private Selector parseMultiEdgeDirectedNeighbor() { // Parses a multi edge neighbor selector: "-[" relationship-type *("," relationship-type) "]" List relationships = new ArrayList<>(); String next; + char peek; + do { // Requires at least one relationship type. + ws(); next = parseIdentifier(); 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)); } - next = expect(MULTI_EDGE_NEXT_ARG_TOKEN); - } while (!next.equals("]->")); + + ws(); + peek = expect(']', ','); + } while (peek != ']'); + + // Get the remainder of the "]->" token. + expect('-'); + expect('>'); + return new NeighborSelector(relationships); } @@ -175,13 +198,17 @@ private Selector parseFunction() { String name = parseIdentifier(); List selectors = parseVariadic(); switch (name) { - case "not": return new NotSelector(selectors); - case "test": return new TestSelector(selectors); - case "is": return IsSelector.of(selectors); + case "not": + return new NotSelector(selectors); + case "test": + return new TestSelector(selectors); + case "is": + return IsSelector.of(selectors); case "each": LOGGER.warning("The `:each` selector function has been renamed to `:is`: " + expression); return IsSelector.of(selectors); - case "of": return new OfSelector(selectors); + case "of": + return new OfSelector(selectors); default: LOGGER.warning(String.format("Unknown function name `%s` found in selector: %s", name, expression)); return (model, neighborProvider, shapes) -> Collections.emptySet(); @@ -189,87 +216,171 @@ private Selector parseFunction() { } private List parseVariadic() { + ws(); List selectors = new ArrayList<>(); - expect(START_FUNCTION); - String next; + expect('('); + char next; + do { selectors.add(AndSelector.of(recursiveParse())); - next = expect(FUNCTION_ARG_NEXT_TOKEN); - } while (!next.equals(")")); + ws(); + next = expect(')', ','); + } while (next != ')'); + return selectors; } private Selector parseAttribute() { + ws(); AttributeSelector.KeyGetter attributeKey = parseAttributeKey(); - String comparatorLexeme = expect(AFTER_ATTRIBUTE); + ws(); + char next = expect(']', '=', '!', '^', '$', '*'); AttributeSelector.Comparator comparator; - switch (comparatorLexeme) { - case "]": + switch (next) { + case ']': return new AttributeSelector(attributeKey); - case "=": + case '=': comparator = AttributeSelector.EQUALS; break; - case "!=": + case '!': + expect('='); comparator = AttributeSelector.NOT_EQUALS; break; - case "^=": + case '^': + expect('='); comparator = AttributeSelector.STARTS_WITH; break; - case "$=": + case '$': + expect('='); comparator = AttributeSelector.ENDS_WITH; break; - case "*=": + case '*': + expect('='); comparator = AttributeSelector.CONTAINS; break; default: - throw syntax("Unreachable attribute comparator case for " + comparatorLexeme); + // Unreachable + throw syntax("Unknown attribute comparator token '" + next + "'"); } String value = parseAttributeValue(); ws(); - String afterValue = expect(AFTER_ATTRIBUTE_RHS); - boolean insensitive = afterValue.equals("i]"); // Case insensitive. + + boolean insensitive = charPeek() == 'i'; + if (insensitive) { + position++; + ws(); + } + + expect(']'); return new AttributeSelector(attributeKey, comparator, value, insensitive); } private AttributeSelector.KeyGetter parseAttributeKey() { - String namespace = expect(ATTRIBUTES); + // Parse the top-level namespace key. + String namespace = parseIdentifier(); + + // It is optionally followed by "|" delimited path keys. + List path = parsePipeDelimitedTraitAttributes(); + switch (namespace) { - case "trait|": return new TraitAttributeKey(parseAttributeValue(), parsePipeDelimitedTraitAttributes()); - case "id": return AttributeSelector.KEY_ID; - case "id|namespace": return AttributeSelector.KEY_ID_NAMESPACE; - case "id|name": return AttributeSelector.KEY_ID_NAME; - case "id|member": return AttributeSelector.KEY_ID_MEMBER; - case "service|version": return AttributeSelector.KEY_SERVICE_VERSION; - default: throw syntax("Unreachable attribute case for " + namespace); + case "id": + if (path.isEmpty()) { + return AttributeSelector.KEY_ID; + } else if (path.size() == 1) { + switch (path.get(0)) { + case "namespace": + return AttributeSelector.KEY_ID_NAMESPACE; + case "name": + return AttributeSelector.KEY_ID_NAME; + case "member": + return AttributeSelector.KEY_ID_MEMBER; + default: + // Unknown attributes always return no result. + LOGGER.warning("Unknown selector attribute `id` path " + path.get(0) + ": " + expression); + return s -> Collections.emptyList(); + } + } else { + // Unknown attributes always return no result. + LOGGER.warning("Too many selector attribute `id` paths " + path + ": " + expression); + return s -> Collections.emptyList(); + } + case "service": + if (path.size() != 1) { + throw syntax("service attributes require exactly one path item"); + } else if (path.get(0).equals("version")) { + return AttributeSelector.KEY_SERVICE_VERSION; + } else { + // Unknown attributes always return no result. + LOGGER.warning("Unknown selector service attribute path " + path + ": " + expression); + return s -> Collections.emptyList(); + } + case "trait": + if (path.isEmpty()) { + throw syntax("Trait attributes require a trait shape ID"); + } else if (path.size() == 1) { + return new TraitAttributeKey(path.get(0), Collections.emptyList()); + } else { + return new TraitAttributeKey(path.get(0), path.subList(1, path.size())); + } + default: + // Unknown attributes always return no result. + LOGGER.warning("Unknown selector attribute `" + namespace + "` " + expression); + return s -> Collections.emptyList(); } } + // Can be a shape_id, quoted string, number, or pseudo_key. private List parsePipeDelimitedTraitAttributes() { - List result = new ArrayList<>(); ws(); - while (charPeek() == '|') { - position++; + if (charPeek() != '|') { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + do { + position++; // skip '|' ws(); - if (compareExpressionSlice("(values)")) { - result.add(expect(Collections.singleton("(values)"))); - } else if (compareExpressionSlice("(keys)")) { - result.add(expect(Collections.singleton("(keys)"))); + // Handle pseudo-keys enclosed in "(" identifier ")". + if (charPeek() == '(') { + position++; + String propertyName = parseIdentifier(); + expect(')'); + result.add("(" + propertyName + ")"); } else { result.add(parseAttributeValue()); } - } + } while (charPeek() == '|'); return result; } private String parseAttributeValue() { + ws(); + switch (charPeek()) { - case '\'': return consumeInside('\''); - case '"': return consumeInside('"'); - default: return parseIdentifier(); + case '\'': + return consumeInside('\''); + case '"': + return consumeInside('"'); + case '-': + position++; + return parseNumber(true); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return parseNumber(false); + default: + return parseShapeId(); } } @@ -292,7 +403,7 @@ private String parseIdentifier() { char current = charPeek(); // needs at least one character - if (!validAttributeIdentifier(current)) { + if (!validIdentifierStart(current)) { throw syntax("Invalid attribute start character `" + current + "`"); } @@ -300,7 +411,7 @@ private String parseIdentifier() { position++; current = charPeek(); - while (validInnerAttributeIdentifier(current)) { + while (validIdentifierInner(current)) { builder.append(current); position++; current = charPeek(); @@ -309,11 +420,73 @@ private String parseIdentifier() { return builder.toString(); } - private boolean validAttributeIdentifier(char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; + private boolean validIdentifierStart(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + private boolean validIdentifierInner(char c) { + return validIdentifierStart(c) || (c >= '0' && c <= '9'); + } + + private String parseShapeId() { + StringBuilder builder = new StringBuilder(); + builder.append(parseIdentifier()); + + if (charPeek() == '.') { + do { + position++; + builder.append('.').append(parseIdentifier()); + } while (charPeek() == '.'); + // "." is only allowed in the namespace part, so it must be followed by a "#". + expect('#'); + builder.append('#').append(parseIdentifier()); + } else if (charPeek() == '#') { // a shape id with no namespace dots, but with a namespace. + position++; + builder.append('#').append(parseIdentifier()); + } + + // Note that members are not supported in this production! + return builder.toString(); + } + + private String parseNumber(boolean negative) { + StringBuilder result = new StringBuilder(); + + if (negative) { + result.append('-'); + } + + addSimpleNumberToBuilder(result); + + // Consume the fraction part. + if (charPeek() == '.') { + result.append('.'); + position++; + addSimpleNumberToBuilder(result); + } + + // Consume the exponent, if present. + if (charPeek() == 'e') { + result.append('e'); + position++; + if (charPeek() == '-' || charPeek() == '+') { + result.append(charPeek()); + position++; + } + addSimpleNumberToBuilder(result); + } + + return result.toString(); } - private boolean validInnerAttributeIdentifier(char c) { - return validAttributeIdentifier(c) || c == '.' || c == '-' || c == '#'; + private void addSimpleNumberToBuilder(StringBuilder result) { + // Require at least one numeric value. + result.append(expect('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')); + + // Consume all numbers after the first number. + while (Character.isDigit(charPeek())) { + result.append(charPeek()); + position++; + } } } 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 31bc747ced6..0b42f839d81 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 @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.SetShape; @@ -39,6 +40,7 @@ import software.amazon.smithy.model.traits.DynamicTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; public class SelectorTest { @@ -201,7 +203,7 @@ public void selectsCustomTraits() { public void requiresValidAttribute() { Throwable thrown = Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse("[id=-]")); - assertThat(thrown.getMessage(), containsString("Invalid attribute start character")); + assertThat(thrown.getMessage(), containsString("Syntax error at character 5 of 6, near `]`")); } @Test @@ -245,5 +247,195 @@ public void toleratesUnexpectedFunctionNames() { Selector selector = Selector.parse(expr); assertThat(expr, equalTo(selector.toString())); + // Using the selector for an invalid function always returns an empty result. + assertThat(selector.select(Model.builder().build()), empty()); + } + + @Test + public void toleratesUnknownSelectorAttributes() { + String expr = "[foof]"; + Selector selector = Selector.parse(expr); + + assertThat(expr, equalTo(selector.toString())); + assertThat(selector.select(Model.builder().build()), empty()); + } + + @Test + public void throwsOnInvalidShapeType() { + SelectorSyntaxException e = Assertions.assertThrows( + SelectorSyntaxException.class, + () -> Selector.parse("foo")); + + assertThat(e.getMessage(), containsString("Unknown shape type")); + } + + @Test + public void parsesEachKindOfAttributeSelector() { + Selector.parse("[id=abc][id!=abc][id^=abc][id$=abc][id*=abc]"); + Selector.parse("[ id = abc ] [ \nid\n!=\nabc\n]\n"); + } + + @Test + public void parsesNestedTraitEndsWith() { + Selector.parse("[trait|mediaType$='plain']"); + } + + @Test + public void throwsOnInvalidComparator() { + SelectorSyntaxException e = Assertions.assertThrows( + SelectorSyntaxException.class, + () -> Selector.parse("[id%=100]")); + + assertThat(e.getMessage(), containsString("Expected one of the following tokens")); + } + + @Test + public void parsesCaseInsensitiveAttributes() { + Selector.parse("[id=abc i][id=abc i ]"); + } + + @Test + public void detectsInvalidShapeIds() { + List exprs = ListUtils.of( + "[id=#]", + "[id=com#]", + "[id=com.foo#]", + "[id=com.foo#$]", + "[id=com.foo#baz$]", + "[id=com.foo#.baz$]", + "[id=com..foo#baz]", + "[id=com#baz$.]", + "[id=com#baz$bar$]", + "[id=com#baz$bar#]", + "[id=com#baz$bar.bam]", + "[id=com.baz#foo$bar]", // Members are not permitted and must be quoted. + "[id=Com.Baz#Foo$Bar123]", + "[id=Com._Baz_#_Foo_$_Bar_123__]"); + + for (String expr : exprs) { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(expr)); + } + } + + @Test + public void parsesValidShapeIds() { + List exprs = ListUtils.of( + "[id=com#foo]", + "[id=com.baz#foo]", + "[id=com.baz.bar#foo]", + "[id=Foo]"); + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void detectsInvalidNumbers() { + List exprs = ListUtils.of( + "[id=1-]", + "[id=-]", + "[id=1.]", + "[id=1..1]", + "[id=1.0e]", + "[id=1.0e+]", + "[id=1e+]", + "[id=1e++]", + "[id=1+]", + "[id=+1]"); + + for (String expr : exprs) { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(expr)); + } + } + + @Test + public void parsesValidNumbers() { + List exprs = ListUtils.of( + "[id=1]", + "[id=123]", + "[id=0]", + "[id=-1]", + "[id=-10]", + "[id=123456789]", + "[id=1.5]", + "[id=1.123456789]", + "[id=1.1e+1]", + "[id=1.1e-1]", + "[id=1.1e-0]", + "[id=1.1e-123456789]", + "[id=1e+123456789]", + "[id=1e-123456789]"); + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void parsesValidQuotedAttributes() { + List exprs = ListUtils.of( + "[id='1']", + "[id=\"1\"]", + "[id='aaaaa']", + "[id=\"aaaaaa\"]", + "[trait|'foo'=\"aaaaaa\"]", + "[trait|\"foo\"=\"aaaaaa\"]"); + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void detectsInvalidKnownAttributePaths() { + List exprs = ListUtils.of( + "[service]", + "[service|version|foo]", + "[trait]"); + + for (String expr : exprs) { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(expr)); + } + } + + @Test + public void parsesValidAttributePathsAndToleratesUnknownPaths() { + List exprs = ListUtils.of( + "[id=abc]", + "[id|name=abc]", + "[id|member=abc]", + "[id|namespace=abc]", + "[id|namespace|(foo)=abc]", // invalid, tolerated + "[id|blurb=abc]", // invalid, tolerated + "[service|version=abc]", + "[service|blurb=abc]", // invalid, tolerated + "[trait|foo=abc]"); + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void canDeserializeSelectors() { + String selector = "[trait|mediaType$='plain']"; + Node node = Node.objectNode().withMember("selector", selector); + NodeMapper mapper = new NodeMapper(); + Pojo pojo = mapper.deserialize(node, Pojo.class); + + assertThat(pojo.getSelector().toString(), equalTo(selector)); + } + + public static final class Pojo { + private Selector selector; + + public Selector getSelector() { + return selector; + } + + public void setSelector(Selector selector) { + this.selector = selector; + } } } From b7218e63f516714495bb672cace3deedf0f5acbe Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sun, 19 Apr 2020 20:16:31 -0700 Subject: [PATCH 5/7] Add support for CSV values in attribute selectors --- docs/source/spec/core/selectors.rst | 94 ++++++++++++++----- .../model/selector/AttributeSelector.java | 40 +++++--- .../amazon/smithy/model/selector/Parser.java | 18 +++- .../smithy/model/selector/SelectorTest.java | 21 +++++ 4 files changed, 137 insertions(+), 36 deletions(-) diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 47efd1e6049..65359ff0672 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -69,10 +69,60 @@ The following selector matches all numbers defined in a model: Attribute selectors =================== -*Attribute selectors* are used to match shapes based on the -:ref:`shape ID `, :ref:`traits `, and member target. -Attribute selectors take one of two forms: existence of an attribute and -comparison of an attribute value to an expected value. +*Attribute selectors* are used to match shapes based on +:ref:`shape IDs `, :ref:`traits `, and other properties. + + +Attribute existence +------------------- + +Checks for the existence of an attribute without any kind of +comparison. + +The following selector checks if a shape has the :ref:`deprecated-trait`: + +.. code-block:: none + + [trait|deprecated] + + +Attribute comparison +-------------------- + +An attribute selector with a comparator checks for the existence of an +attribute and compares the resolved attribute values to a comma separated +list of values. + +The following selector matches shapes that have the :ref:`documentation-trait` +with a value set to an empty string: + +.. code-block:: none + + [trait|documentation=""] + +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 shapes that have the :ref:`tags-trait` in +which one or more tags matches either "foo" or "baz". + +.. code-block:: none + + [trait|tags|(values)=foo, baz] + +Attribute comparisons can be made case-insensitive by preceding the closing +bracket with ``i``. + +The following selector matches shapes that have a documentation string +that case-insensitively contains the word "FIXME": + +.. code-block:: none + + [trait|documentation*=FIXME i] + + +Attribute comparators +--------------------- Attribute selectors support the following comparators: @@ -95,9 +145,6 @@ Attribute selectors support the following comparators: * - ``*=`` - Matches if the attribute value contains with the expected value. -Attribute comparisons can be made case-insensitive by preceding the closing -bracket with " i" (for example, ``string[trait|time=DATE i]``). - .. important:: Implementations MUST NOT fail when unknown attribute keys are @@ -106,7 +153,7 @@ bracket with " i" (for example, ``string[trait|time=DATE i]``). ``id`` attribute -~~~~~~~~~~~~~~~~ +---------------- Gets the full shape ID of a shape. @@ -125,7 +172,7 @@ is enclosed in single or double quotes: ``id|namespace`` attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- Gets the namespace part of a shape ID. @@ -137,7 +184,7 @@ The following example matches all shapes in the ``foo.baz`` namespace: ``id|name`` attribute -~~~~~~~~~~~~~~~~~~~~~ +--------------------- Gets the name part of a shape ID. @@ -150,7 +197,7 @@ name of ``MyShape``. ``id|member`` attribute -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- Gets the member part of a shape ID (if available). @@ -163,7 +210,7 @@ name of ``foo``. ``service|version`` attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- Gets the version property of a service shape if the shape is a service. @@ -177,7 +224,7 @@ property that starts with ``2018-``: ``trait|*`` attribute -~~~~~~~~~~~~~~~~~~~~~ +--------------------- Gets the value of a trait applied to a shape, where "*" is the ID of a trait. The ``smithy.api`` namespace MAY be omitted from shape IDs @@ -216,7 +263,7 @@ Fully-qualified trait names are supported: Nested trait properties -^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~ Nested properties of a trait can be selected using subsequent pipe (``|``) delimited property names. @@ -300,7 +347,7 @@ The *current* shapes evaluated by a selector is changed using a Undirected neighbor -~~~~~~~~~~~~~~~~~~~ +------------------- An :token:`undirected neighbor ` (``>``) changes the current set of shapes to every shape that is connected to the current @@ -327,7 +374,7 @@ selector returns strings that are targeted by list members: Directed neighbors -~~~~~~~~~~~~~~~~~~ +------------------ The ``>`` neighbor selector is an *undirected* edge traversal. Sometimes a directed edge traversal is necessary to match the appropriate shapes. For @@ -368,7 +415,7 @@ selector finds all service shapes that have a protocol trait applied to it Recursive neighbors -~~~~~~~~~~~~~~~~~~~ +------------------- The ``~>`` neighbor selector finds all shapes that are recursively connected in the closure of another shape. @@ -392,7 +439,7 @@ trait: .. _selector-relationships: Relationships -~~~~~~~~~~~~~ +------------- The table below lists the labeled directed relationships from each shape. @@ -509,7 +556,7 @@ Functions are used to filter shapes. Functions always start with ``:``. ``:test`` -~~~~~~~~~ +--------- The ``:test`` function is used to test if a shape is contained within any of the provided predicate selector return values without changing the current @@ -530,7 +577,7 @@ no documentation: ``:is`` -~~~~~~~ +------- The ``:is`` function is used to map over the current shape with multiple selectors and returns all of the shapes returned from each selector. The @@ -579,7 +626,7 @@ change the current node, this can be reduced to the following selector: ``:not`` -~~~~~~~~ +-------- The *:not* function is used to filter out shapes. This function accepts a list of selector arguments, and the shapes returned from each predicate are @@ -659,7 +706,7 @@ in the model: ``:of`` -~~~~~~~ +------- The ``:of`` function is used to match members based on their containers (i.e., the shape that defines the member). The ``:of`` function accepts one @@ -705,9 +752,10 @@ Selectors are defined by the following ABNF_ grammar. selector_directed_neighbor :"-[" `selector_rel_type` *("," `selector_rel_type`) "]->" selector_recursive_neighbor :"~>" selector_rel_type :`identifier` - selector_attr :"[" `selector_key` *(`selector_comparator` `selector_value` ["i"]) "]" + selector_attr :"[" `selector_key` *(`selector_comparator` `selector_values` ["i"]) "]" selector_key :`identifier` *("|" `selector_key_path`) selector_key_path :`selector_pseudo_key` / `selector_value` + selector_values :`selector_value` *("," `selector_value`) selector_value :`selector_text` / `number` / `root_shape_id` selector_absolute_root_shape_id :`namespace` "#" `identifier` selector_pseudo_key :"(" `identifier` ")" 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 ecac6451125..ba59ea68fe3 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 @@ -18,7 +18,6 @@ import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -27,7 +26,6 @@ import software.amazon.smithy.model.neighbor.NeighborProvider; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.utils.FunctionalUtils; import software.amazon.smithy.utils.ListUtils; /** @@ -51,7 +49,7 @@ final class AttributeSelector implements Selector { .orElseGet(Collections::emptyList); private final KeyGetter key; - private final String expected; + private final List expected; private final Comparator comparator; private final boolean caseInsensitive; @@ -69,13 +67,21 @@ interface Comparator extends BiFunction {} AttributeSelector( KeyGetter key, Comparator comparator, - String expected, + List expected, boolean caseInsensitive ) { this.expected = expected; this.key = key; this.caseInsensitive = caseInsensitive; this.comparator = comparator; + + // Case insensitive comparisons are made by converting both + // side of the comparison to lowercase. + if (caseInsensitive) { + for (int i = 0; i < expected.size(); i++) { + expected.set(i, expected.get(i).toLowerCase(Locale.ENGLISH)); + } + } } @Override @@ -90,12 +96,24 @@ private boolean matchesAttribute(List result) { return !result.isEmpty(); } - String rhs = caseInsensitive ? expected.toLowerCase(Locale.US) : expected; - return result.stream() - // The returned attribute value might be null if - // the value exists, but isn't comparable. - .filter(FunctionalUtils.not(Objects::isNull)) - .map(value -> caseInsensitive ? value.toLowerCase(Locale.ENGLISH) : value) - .anyMatch(lhs -> comparator.apply(lhs, rhs)); + for (String attribute : result) { + // The returned attribute value might be null if + // the value exists, but isn't comparable. + if (attribute == null) { + continue; + } + + if (caseInsensitive) { + attribute = attribute.toLowerCase(Locale.ENGLISH); + } + + for (String value : expected) { + if (comparator.apply(attribute, value)) { + return true; + } + } + } + + return false; } } 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 574b534790d..c862029e193 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 @@ -264,7 +264,7 @@ private Selector parseAttribute() { throw syntax("Unknown attribute comparator token '" + next + "'"); } - String value = parseAttributeValue(); + List values = parseAttributeValues(); ws(); boolean insensitive = charPeek() == 'i'; @@ -274,7 +274,7 @@ private Selector parseAttribute() { } expect(']'); - return new AttributeSelector(attributeKey, comparator, value, insensitive); + return new AttributeSelector(attributeKey, comparator, values, insensitive); } private AttributeSelector.KeyGetter parseAttributeKey() { @@ -357,6 +357,20 @@ private List parsePipeDelimitedTraitAttributes() { return result; } + private List parseAttributeValues() { + List result = new ArrayList<>(); + result.add(parseAttributeValue()); + ws(); + + while (charPeek() == ',') { + position++; + result.add(parseAttributeValue()); + ws(); + } + + return result; + } + private String parseAttributeValue() { ws(); 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 0b42f839d81..e6874fb1469 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 @@ -438,4 +438,25 @@ public void setSelector(Selector selector) { this.selector = selector; } } + + @Test + public void canMatchUsingCommaSeparatedAttributeValues() { + List matches1 = ids(traitModel, "[trait|enum|(values)|value='m256.mega', 'nope']"); + List matches2 = ids(traitModel, "[trait|enum|(values)|value = 'm256.mega' ,'nope' ]"); + List matches3 = ids(traitModel, "[trait|enum|(values)|value = 'm256.mega' , nope ]"); + + assertThat(matches1, equalTo(matches2)); + assertThat(matches1, equalTo(matches3)); + assertThat(matches1, containsInAnyOrder( + "smithy.example#DocumentedString1", + "smithy.example#DocumentedString2", + "smithy.example#EnumString")); + } + + @Test + public void detectsInvalidAttributeCsv() { + Assertions.assertThrows( + SelectorSyntaxException.class, + () -> Selector.parse("[trait|enum|(values)|value='m256.mega',]")); + } } From 44e34cee954ee943d8fe09a8dbf1296493468430 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sun, 19 Apr 2020 21:02:01 -0700 Subject: [PATCH 6/7] Add support for numeric attribute comparators --- docs/source/spec/core/selectors.rst | 52 ++++++++++++--- .../model/selector/AttributeSelector.java | 35 +++++++++- .../amazon/smithy/model/selector/Parser.java | 18 ++++- .../smithy/model/selector/SelectorTest.java | 66 +++++++++++++++++++ .../model/selector/nested-traits.smithy | 8 +++ 5 files changed, 168 insertions(+), 11 deletions(-) diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 65359ff0672..5a1c75c74f2 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -72,6 +72,12 @@ Attribute selectors *Attribute selectors* are used to match shapes based on :ref:`shape IDs `, :ref:`traits `, and other properties. +.. important:: + + Implementations MUST NOT fail when unknown attribute keys are + encountered; implementations SHOULD emit a warning and match no results + when an unknown attribute is encountered. + Attribute existence ------------------- @@ -133,23 +139,51 @@ Attribute selectors support the following comparators: * - Comparator - Description * - ``=`` - - Matches if the attribute value is equal to the expected value. + - Matches if the attribute value is equal to the comparison value. * - ``!=`` - - Matches if the attribute value is not equal to the expected value. + - Matches if the attribute value is not equal to the comparison value. Note that this comparator is never matched if the resolved attribute does not exist. * - ``^=`` - - Matches if the attribute value starts with the expected value. + - Matches if the attribute value starts with the comparison value. * - ``$=`` - - Matches if the attribute value ends with the expected value. + - Matches if the attribute value ends with the comparison value. * - ``*=`` - - Matches if the attribute value contains with the expected value. + - Matches if the attribute value contains with the comparison value. + * - ``>`` + - Matches if the attribute value is greater than the comparison value. + * - ``>=`` + - Matches if the attribute value is greater than or equal to the + comparison value. + * - ``<`` + - Matches if the attribute value is less than the comparison value. + * - ``<=`` + - Matches if the attribute value is less than or equal to the + comparison value. -.. important:: +The ``<``, ``<=``, ``>``, ``>=`` comparators only match if both the attribute +value and comparison value contain valid :token:`number` productions. If +either is not a number, then the selector does not match. - Implementations MUST NOT fail when unknown attribute keys are - encountered; implementations SHOULD emit a warning and match no results - when an unknown attribute is encountered. +The following selector matches shapes that have an :ref:`httpError-trait` +value that is greater than or equal to `500`: + +.. code-block:: none + + [trait|httpError >= 500] + +The following selector is equivalent: + +.. code-block:: none + + [trait|httpError >= '500'] + +The following selector does not match any shapes because the comparison value +is not a valid number: + +.. code-block:: none + + [trait|httpError >= "not a number!"] ``id`` attribute 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 ba59ea68fe3..b97278c0b25 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 @@ -15,6 +15,7 @@ package software.amazon.smithy.model.selector; +import java.math.BigDecimal; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -32,11 +33,18 @@ * Matches shapes with a specific attribute. */ final class AttributeSelector implements Selector { + static final Comparator EQUALS = String::equals; static final Comparator NOT_EQUALS = (a, b) -> !a.equals(b); static final Comparator STARTS_WITH = String::startsWith; static final Comparator ENDS_WITH = String::endsWith; static final Comparator CONTAINS = String::contains; + + static final Comparator GT = (a, b) -> numericComparison(a, b, i -> i == 1); + static final Comparator GTE = (a, b) -> numericComparison(a, b, i -> i >= 0); + static final Comparator LT = (a, b) -> numericComparison(a, b, i -> i <= -1); + static final Comparator LTE = (a, b) -> numericComparison(a, b, i -> i <= 0); + static final KeyGetter KEY_ID = (shape) -> ListUtils.of(shape.getId().toString()); static final KeyGetter KEY_ID_NAMESPACE = (shape) -> ListUtils.of(shape.getId().getNamespace()); static final KeyGetter KEY_ID_NAME = (shape) -> ListUtils.of(shape.getId().getName()); @@ -92,7 +100,7 @@ public Set select(Model model, NeighborProvider neighborProvider, Set result) { - if (comparator == null || expected == null) { + if (comparator == null) { return !result.isEmpty(); } @@ -116,4 +124,29 @@ private boolean matchesAttribute(List result) { return false; } + + // Try to parse both numbers, ignore numeric failures since that's acceptable, + // then pass the result of calling compareTo on the numbers to the given + // evaluator. The evaluator then determines if the comparison is what was expected. + private static boolean numericComparison(String lhs, String rhs, Function evaluator) { + BigDecimal lhsNumber = parseNumber(lhs); + if (lhsNumber == null) { + return false; + } + + BigDecimal rhsNumber = parseNumber(rhs); + if (rhsNumber == null) { + return false; + } + + return evaluator.apply(lhsNumber.compareTo(rhsNumber)); + } + + private static BigDecimal parseNumber(String token) { + try { + return new BigDecimal(token); + } catch (NumberFormatException e) { + return null; + } + } } 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 c862029e193..246b97f16d5 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 @@ -234,7 +234,7 @@ private Selector parseAttribute() { ws(); AttributeSelector.KeyGetter attributeKey = parseAttributeKey(); ws(); - char next = expect(']', '=', '!', '^', '$', '*'); + char next = expect(']', '=', '!', '^', '$', '*', '>', '<'); AttributeSelector.Comparator comparator; switch (next) { @@ -259,6 +259,22 @@ private Selector parseAttribute() { expect('='); comparator = AttributeSelector.CONTAINS; break; + case '>': + if (charPeek() == '=') { + position++; + comparator = AttributeSelector.GTE; + } else { + comparator = AttributeSelector.GT; + } + break; + case '<': + if (charPeek() == '=') { + position++; + comparator = AttributeSelector.LTE; + } else { + comparator = AttributeSelector.LT; + } + break; default: // Unreachable throw syntax("Unknown attribute comparator token '" + next + "'"); 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 e6874fb1469..f97c5d753e2 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 @@ -16,11 +16,13 @@ package software.amazon.smithy.model.selector; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import java.util.List; @@ -439,6 +441,24 @@ public void setSelector(Selector selector) { } } + @Test + public void canMatchOnExistence() { + assertThat(ids(traitModel, "[id|namespace='smithy.example'][trait|tags]"), + contains("smithy.example#EnumString")); + } + + @Test + public void canMatchUsingCaseInsensitiveComparison() { + List matches = ids(traitModel, "[trait|error = 'CLIENT' i]"); + + assertThat(matches, containsInAnyOrder("smithy.example#ErrorStruct2")); + } + + @Test + public void cannotMatchOnNonComparableAttributes() { + assertThat(ids(traitModel, "[trait|tags='foo']"), empty()); + } + @Test public void canMatchUsingCommaSeparatedAttributeValues() { List matches1 = ids(traitModel, "[trait|enum|(values)|value='m256.mega', 'nope']"); @@ -459,4 +479,50 @@ public void detectsInvalidAttributeCsv() { SelectorSyntaxException.class, () -> Selector.parse("[trait|enum|(values)|value='m256.mega',]")); } + + @Test + public void parsedRelativeComparators() { + List exprs = ListUtils.of( + "[trait|httpError > 500]", + "[trait|httpError >= 500]", + "[trait|httpError < 500]", + "[trait|httpError <= 500]", + "[trait|httpError>500]", + "[trait|httpError>=500]", + "[trait|httpError<500]", + "[trait|httpError<=500]", + "[trait|httpError > 500, 400]", // silly, but supported + "[trait|httpError >= 500, 400]", // silly, but supported + "[trait|httpError < 500, 400]", // silly, but supported + "[trait|httpError <= 500, 400]"); // silly, but supported + + for (String expr : exprs) { + Selector.parse(expr); + } + } + + @Test + public void canMatchUsingRelativeSelectors() { + List matches1 = ids(traitModel, "[trait|httpError >= 500]"); + List matches2 = ids(traitModel, "[trait|httpError > 499]"); + List matches3 = ids(traitModel, "[trait|httpError >= 400]"); + List matches4 = ids(traitModel, "[trait|httpError > 399]"); + List matches5 = ids(traitModel, "[trait|httpError <= 500]"); + List matches6 = ids(traitModel, "[trait|httpError < 500]"); + List matches7 = ids(traitModel, "[trait|httpError >= 500e0]"); + + assertThat(matches1, containsInAnyOrder("smithy.example#ErrorStruct1")); + assertThat(matches2, containsInAnyOrder("smithy.example#ErrorStruct1")); + assertThat(matches3, containsInAnyOrder("smithy.example#ErrorStruct1", "smithy.example#ErrorStruct2")); + assertThat(matches4, containsInAnyOrder("smithy.example#ErrorStruct1", "smithy.example#ErrorStruct2")); + assertThat(matches5, containsInAnyOrder("smithy.example#ErrorStruct1", "smithy.example#ErrorStruct2")); + assertThat(matches6, containsInAnyOrder("smithy.example#ErrorStruct2")); + assertThat(matches7, containsInAnyOrder("smithy.example#ErrorStruct1")); + } + + @Test + public void invalidNumbersFailsGracefully() { + assertThat(ids(traitModel, "[trait|httpError >= 'nope']"), empty()); + assertThat(ids(traitModel, "[trait|error >= 500]"), empty()); + } } 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 5efe7ce85b7..7635b2ebcf7 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 @@ -68,3 +68,11 @@ structure MoreNesting { structure EvenMoreNesting { bar: String, } + +@error("server") +@httpError(500) +structure ErrorStruct1 {} + +@error("client") +@httpError(400) +structure ErrorStruct2 {} From 243428ba1f979ad07c92a9b46a0cfad32c83e386 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Mon, 20 Apr 2020 10:34:03 -0700 Subject: [PATCH 7/7] Perform minor selector spec cleanup --- docs/source/spec/core/selectors.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/spec/core/selectors.rst b/docs/source/spec/core/selectors.rst index 5a1c75c74f2..32c2c9279bd 100644 --- a/docs/source/spec/core/selectors.rst +++ b/docs/source/spec/core/selectors.rst @@ -353,7 +353,7 @@ trait that has an entry named ``Homepage``: [trait|externalDocumentation|(keys)=Homepage] -Like the ``(list)`` property, the ``(keys)`` property also treats empty +Like the ``(values)`` property, the ``(keys)`` property also treats empty objects as not present. The following example matches all shapes that have a trait named @@ -376,7 +376,7 @@ MUST not cause an error and MUST match no shapes: Neighbors ========= -The *current* shapes evaluated by a selector is changed using a +The *current shapes* evaluated by a selector are changed using a :token:`selector_neighbor` token. @@ -613,7 +613,7 @@ no documentation: ``:is`` ------- -The ``:is`` function is used to map over the current shape with multiple +The ``:is`` function is used to map over the current shapes with multiple selectors and returns all of the shapes returned from each selector. The ``:is`` function accepts a variadic list of selectors each separated by a comma (","). @@ -793,7 +793,7 @@ Selectors are defined by the following ABNF_ grammar. selector_value :`selector_text` / `number` / `root_shape_id` selector_absolute_root_shape_id :`namespace` "#" `identifier` selector_pseudo_key :"(" `identifier` ")" - selector_comparator :"^=" / "$=" / "*=" / "!=" / "=" + selector_comparator :"^=" / "$=" / "*=" / "!=" / ">=" / ">" / "<=" / "<" / "=" selector_function_expression :":" `selector_function` "(" `selector` *("," `selector`) ")" selector_function :`identifier` selector_text :`selector_single_quoted_text` / `selector_double_quoted_text`