Skip to content

Commit

Permalink
Correct issues and bugs with neighbor traversal
Browse files Browse the repository at this point in the history
This commit fixes a few fundamental issues with how neighbors and
model graph traversal was implemented. These changes are bug fixes and
correctness fixes, though they could potentially impact existing
selectors of a very small set of users (e.g., there is no known impact
to Amazon's very large corpus of Smithy models).

The theme of this change is more accurately exposing the Smithy model
as a graph, making graph traversal unambiguous. There are a few major
issues addressed in this CR.

1. The bound relationship is no longer emitted or documented. It can
   still be used if referred to explicitly in a directed neighbor
   selector.
2. instanceOperation relationship is now "synthetic", deprecated, and
   not documented. It is no longer emitted from a NeighborVisitor but
   can be used if referred to explicitly in a directed neighbor selector.
3. The operation relationship now only includes operations bound to
   resources and service via the operations property.
4. The collectionOperation relationship now only includes operations
   bound to a resource vai the collectionOperations property.
5. Recursive neighbor traversal from a member no longer includes the
   containing shape.

Further background and justification can be found below.

**Bound relationships are confusing and buggy**

Smithy today documents a “bound” relationship as:

> resource → bound: The service or resource to which the resource is bound.
> operation → bound: The service or resource to which the operation is bound.

However, the implementation of bound is inconsistent and buggy, and worse,
the idea itself is confusing: there should be no relationship from a shape to
shapes that refer to it. The only reason the “bound” relationship exists is that
we added it before reverse neighbor selectors were added to Smithy.

Consider the following model that is commented with each neighbor we previously
emitted:

```
$version: "2.0"

namespace smithy.example

// [Relationship shape="smithy.example#Foo" type="RESOURCE" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Bar" type="BOUND" neighbor="smithy.example#Foo"]
service Foo {
    resources: [Bar]
}

// [Relationship shape="smithy.example#Bar" type="OPERATION" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#Bar" type="READ" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#Bar" type="INSTANCE_OPERATION" neighbor="smithy.example#GetBar"]
// [Relationship shape="smithy.example#GetBar" type="BOUND" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Foo" type="RESOURCE" neighbor="smithy.example#Bar"]
// [Relationship shape="smithy.example#Bar" type="BOUND" neighbor="smithy.example#Foo"]
resource Bar {
    read: GetBar
}

// [Relationship shape="smithy.example#GetBar" type="INPUT" neighbor="smithy.example#GetBarInput"]
@readonly
operation GetBar {
    // [Relationship shape="smithy.example#GetBarInput" type="STRUCTURE_MEMBER" neighbor="smithy.example#GetBarInput$greeting"]
    input := {
        // [Relationship shape="smithy.example#GetBarInput$greeting" type="MEMBER_CONTAINER" neighbor="smithy.example#GetBarInput"]
        // [Relationship shape="smithy.example#GetBarInput$greeting" type="MEMBER_TARGET" neighbor="smithy.example#Greeting"]
        greeting: Greeting
    }
}

// No relationships
string Greeting
```

Let’s walk through each shape and document what the relationships currently are,
what they should be according to documentation, and what they are after removing
BOUND.

Previously, we emitted:

- Foo -[resource]→ Bar
  - This is correct.
- Bar -[bound]→ Foo
  - This is wrong in both docs and the ideal. Emitting the bound relationship
    here is wrong since the documentation states it’s between the resource and a
    service, so looking at relationships from a service should not emit BOUND.

This shape now only emits:

- Foo -[resource]→ Bar

Previously, we emitted:

- Bar -[read]→ GetBar
  - This is correct and expected.
- Bar -[instanceOperation]→ GetBar
  - This is correct, though it should be a “synthetic” relationship that
    selectors know about but not actually emitted from `NeighborVisitor`.
- Bar -[operation]→ GetBar
  - This is correct according to the current documentation, but is confusing
    because we don’t know if the operation was bound using read or the
    “operations” property. (see next topic).
- Foo -[resource]→ Bar
  - This is completely wrong and not supported by documentation. It’s
    unquestionably a bug.
- GetBar -[bound]→ Bar
  - This is wrong since according to the documentation, this relationship should
    come from GetBar not Bar.
- Bar -[bound]→ Foo
  - This is correct according to the documentation, but shouldn’t be emitted at
    all ideally since this shape has no defined edges from it to Foo and these
    kinds of inverted edges are confusing.

This shape now only emits:

- Bar -[read]→ GetBar

Today, we emit (and continue to emit after this change):

- GetBar -[input] → GetBarInput

According to documentation, we should emit:

- GetBar -[input]→ GetBarInput
- GetBar -[bound] → Bar

However, given bound relationships are buggy and counter-intuitive,
we will continue to not emit a bound relationship here.

Because these neighbors are implemented so inconsistently, using them with
selectors is confusing and buggy to the point of the feature being unusable.

This selector works as expected and sees that `GetBar` is connected to `Bar`
using a `READ` relationship.

```
operation <
```

Returns:

- smithy.example#Bar

The documentation says that operations have a BOUND relationship to any resource
or service they are bound to, but that doesn't work today:

```
operation -[bound]->
```

Returns: []

```
operation <-[bound]-
```

Returns []

If we check if a resource has a `bound` relationship from the operation, it
surprisingly works. This is very wrong considering there's no edge found from
the operation to the resource, but the resource can find the edge (this is due
to not implementing the reverse lookups in neighbor provider).

```
resource <-[bound]-
```

Returns:

- smithy.example#GetBar

Strangely, if we check the bound relationships from resources, we get the
resource itself back:

```
resource -[bound]->
```

Returns:

- smithy.example#Bar
  - No resource is bound to another resource in this model, so that's clearly
    wrong. What's happening here is that due to a bug, the resource emits
    a bound relationship back to itself.
- smithy.example#Foo

Just getting all edges using ">" and "<" returns the same results:

```
operation >
```

Incorrectly (according to docs previously) returns:

- smithy.example#GetBarInput

```
operation <
```

Correctly (according to the docs previously) returns:

- smithy.example#Bar

However, getting all directed edges of a resource is strange:

```
resource >
```

Returns:

- smithy.example#Bar
  - Bad, and due to the BOUND edge from the service to the resource
- smithy.example#Foo
  - Strange, given the resource doesn't have a direct edge to the service other
    than BOUND.
- smithy.example#GetBar

Getting even weirder due to bugs in the current implementation is getting
reverse edges of a resource:

```
resource <
```

Returns:

- smithy.example#Foo (good, the service refers to it)
- smithy.example#GetBar (bad, because there is no corresponding edge when you
  get directed neighbors from GetBar)

Making this all more confusing, using a Walker will include service shapes a
resource is bound to:

```
walker.walkShapeIds(model.expectShape(ShapeId.from("smithy.example#Bar")))
```

Or, using a selector:

```
resource ~>
```

Returns:

- smithy.example#Foo (bad since it traversed up)
- smithy.example#Bar ✅ (walker always includes the given shape)
- smithy.example#GetBar ✅
- smithy.example#GetBarInput ✅
- smithy.example#GetBarInput$greeting ✅
- smithy.example#Greeting ✅

However, if we check `GetBar`, we don't crawl up to the resource! (this is
actually better behavior but different from the above).

```
operation ~>
```

Returns:

- smithy.example#GetBar
- smithy.example#GetBarInput
- smithy.example#GetBarInput$greeting
- smithy.example#Greeting

**Operation relationships were ambiguous**

When it comes to traversing the graph of shapes to operations, the
current implementation leaves things ambiguous. We emit the following
kinds of relationships today (omitting BOUND relationships since
they’re covered above):

- Service “operations”
  - Service → OPERATION → Operation shape
- Resource “operations”
  - Resource → OPERATION → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
- Resource “collectionOperations”
  - Resource → OPERATION → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
- Resource “create”
  - Resource → CREATE → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “list”
  - Resource → LIST → Operation shape
  - Resource → COLLECTION_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “put”
  - Resource → PUT → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “read”
  - Resource → READ → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “update”
  - Resource → UPDATE → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape
- Resource “delete”
  - Resource → DELETE → Operation shape
  - Resource → INSTANCE_OPERATION → Operation shape
  - Resource → OPERATION → Operation shape

These relationships are problematic for several reasons:

- Emitting an OPERATION relationship for every operation makes it impossible to
  determine how an operation was bound to a resource. Was it bound through a
  lifecycle? Through “operations”? Through “collectionOperations”?
- Emitting an INSTANCE_OPERATION relationship is unnecessary and could become a
  synthetic kind of relationship (that is, rather than actually emit it, just
  implement special handling for `resource -[instanceOperation]->`.
- Emitting COLLECTION_OPERATION for `list` and `create` is problematic because
  you can’t tell if everything returned from `resource -[collectionOperation]->`
  was bound through `collectionOperations` or not without convoluted `:not`
  clauses.

We now only emit the following events from resources and services for
operation bindings:

- Service “operations”
  - Service → OPERATION → Operation shape
- Resource “operations”
  - Resource → OPERATION → Operation shape
- Resource “collectionOperations”
  - Resource → COLLECTION_OPERATION → Operation shape
- Resource “create”
  - Resource → CREATE → Operation shape
- Resource “list”
  - Resource → LIST → Operation shape
- Resource “put”
  - Resource → PUT → Operation shape
- Resource “read”
  - Resource → READ → Operation shape
- Resource “update”
  - Resource → UPDATE → Operation shape
- Resource “delete”
  - Resource → DELETE → Operation shape

The "instanceOperation" relationship is now deprecated, no longer
emitted, and no longer documented. Because "collectionOperation" is
now fixed to only include operations actually bound through
"collectionOperations" it would be confusing to have
"instanceOperation" act as a synthetic grouping of relationships
while "collectionOperation" does not, despite being the other kind of
resource binding than instance operations.

You can still refer to it using `-[instanceOperation]->` but a
relationship isn't emitted via NeighborVisitor.

**Recursive member traversal crawled up to its container**

When you use a recursive neighbor selector on a member shape, it crawls up to
the containing shape. This is wrong because it makes getting the closure of a
single member is impossible. For example:

```
structure Foo {
    bar: String
    baz: Integer
}
```

Today, using `member [id|member = 'bar'] ~>` would return:

- Foo
- Bar
- Baz
- String
- Integer

The intended result was just `String`. The previous result was wrong
because the edge between the member and its container wasn't filtered
out of recursive neighbor traversal, causing all shapes in the
containing shape to be returned in the closure.
  • Loading branch information
mtdowling committed Oct 25, 2023
1 parent 5b4c3b1 commit e47f881
Show file tree
Hide file tree
Showing 17 changed files with 606 additions and 320 deletions.
87 changes: 39 additions & 48 deletions docs/source-1.0/spec/core/selectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1151,61 +1151,50 @@ The table below lists the labeled directed relationships from each shape.
- Description
* - service
- operation
- Each operation that is bound to a service.
- Each operation bound to a service.
* - service
- resource
- Each resource that is bound to a service.
- Each resource bound to a service.
* - service
- error
- Each error structure referenced by the service (if present).
- Each error structure referenced by the service.
* - resource
- identifier
- The identifier referenced by the resource (if specified).
- Each identifier shape of a resource.
* - resource
- operation
- Each operation that is bound to a resource through the
"operations", "create", "put", "read", "update", "delete", and "list"
properties.
- property
- Each property shape of a resource.
* - resource
- instanceOperation
- Each operation that is bound to a resource through the
"operations", "put", "read", "update", and "delete" properties.
- resource
- Each resource bound to a resource.
* - resource
- collectionOperation
- Each operation that is bound to a resource through the
"collectionOperations", "create", and "list" properties.
- operation
- Each operation bound to a resource through the "operations" property.
* - resource
- resource
- Each resource that is bound to a resource.
- collectionOperation
- Each operation bound to a resource through the "collectionOperations"
property.
* - resource
- create
- The operation referenced by the :ref:`create-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`create-lifecycle` of a resource.
* - resource
- read
- The operation referenced by the :ref:`read-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`read-lifecycle` of a resource.
* - resource
- update
- The operation referenced by the :ref:`update-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`update-lifecycle` of a resource.
* - resource
- delete
- The operation referenced by the :ref:`delete-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`delete-lifecycle` of a resource.
* - resource
- list
- The operation referenced by the :ref:`list-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`list-lifecycle` of a resource.
* - resource
- bound
- The service or resource to which the resource is bound.
* - operation
- bound
- The service or resource to which the operation is bound.
- put
- The operation defined as the :ref:`put-lifecycle` of a resource.
* - operation
- input
- The input structure of the operation (if present).
- The input structure of an operation.

.. note::

Expand All @@ -1214,7 +1203,7 @@ The table below lists the labeled directed relationships from each shape.

* - operation
- output
- The output structure of the operation (if present).
- The output structure of an operation.

.. note::

Expand All @@ -1223,23 +1212,25 @@ The table below lists the labeled directed relationships from each shape.

* - operation
- error
- Each error structure referenced by the operation (if present).
- Each error structure of an operation.
* - list
- member
- The :ref:`member` of the list. Note that this is not the shape targeted
by the member.
- The :ref:`member <member>` of a list.
* - map
- member
- The key and value members of the map. Note that these are not the
shapes targeted by the member.
- The key and value members of a map.
* - structure
- member
- Each structure member. Note that these are not the shapes targeted by
the members.
- Each structure member.
* - union
- member
- Each union member. Note that these are not the shapes targeted by
the members.
- Each union member.
* - enum
- member
- Each enum member.
* - intEnum
- member
- Each intEnum member.
* - member
-
- The shape targeted by the member. Note that member targets have no
Expand All @@ -1249,13 +1240,13 @@ The table below lists the labeled directed relationships from each shape.
- Each trait applied to a shape. The neighbor shape is the shape that
defines the trait. This kind of relationship is only traversed if the
``trait`` relationship is explicitly stated as a desired directed
neighbor relationship type.
neighbor relationship type (for example, ``-[trait]->``).

.. important::
.. note::

Implementations MUST tolerate parsing unknown relationship types. When
evaluated, the directed traversal of unknown relationship types yields
no shapes.
Implementations MAY tolerate parsing unknown relationship types. When
evaluated, the traversal of unknown relationship types SHOULD yield
nothing.


Functions
Expand Down Expand Up @@ -1300,7 +1291,7 @@ no documentation:

.. code-block:: none
:test(-[bound, resource]->)
:test(resource >)
:not([trait|documentation])
Expand Down
96 changes: 41 additions & 55 deletions docs/source-2.0/spec/selectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -999,9 +999,8 @@ Forward undirected neighbor
----------------------------

A :token:`forward undirected neighbor <selectors:SelectorForwardUndirectedNeighbor>`
(``>``) yields every shape that is connected to the current shape. For
example, the following selector matches the key and value members of
every map:
(``>``) yields every shape referred to by the current shape. For example, the
following selector matches the key and value members of every map:

.. code-block:: none
Expand All @@ -1020,7 +1019,7 @@ Forward directed neighbors

The forward undirected neighbor selector (``>``) is an *undirected* edge
traversal. Sometimes, a directed edge traversal is necessary. For example,
the following selector matches the "bound", "input", "output", and "error"
the following selector matches the "input", "output", "error", and "mixin"
relationships of each operation:

.. code-block:: none
Expand Down Expand Up @@ -1159,64 +1158,50 @@ The table below lists the labeled directed relationships from each shape.
- Description
* - service
- operation
- Each operation that is bound to a service.
- Each operation bound to a service.
* - service
- resource
- Each resource that is bound to a service.
- Each resource bound to a service.
* - service
- error
- Each error structure referenced by the service (if present).
- Each error structure referenced by the service.
* - resource
- identifier
- An identifier referenced by the resource (if specified).
- Each identifier shape of a resource.
* - resource
- property
- A property referenced by the resource.
- Each property shape of a resource.
* - resource
- operation
- Each operation that is bound to a resource through the
"operations", "create", "put", "read", "update", "delete", and "list"
properties.
- resource
- Each resource bound to a resource.
* - resource
- instanceOperation
- Each operation that is bound to a resource through the
"operations", "put", "read", "update", and "delete" properties.
- operation
- Each operation bound to a resource through the "operations" property.
* - resource
- collectionOperation
- Each operation that is bound to a resource through the
"collectionOperations", "create", and "list" properties.
* - resource
- resource
- Each resource that is bound to a resource.
- Each operation bound to a resource through the "collectionOperations"
property.
* - resource
- create
- The operation referenced by the :ref:`create-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`create-lifecycle` of a resource.
* - resource
- read
- The operation referenced by the :ref:`read-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`read-lifecycle` of a resource.
* - resource
- update
- The operation referenced by the :ref:`update-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`update-lifecycle` of a resource.
* - resource
- delete
- The operation referenced by the :ref:`delete-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`delete-lifecycle` of a resource.
* - resource
- list
- The operation referenced by the :ref:`list-lifecycle` property of
a resource (if present).
- The operation defined as the :ref:`list-lifecycle` of a resource.
* - resource
- bound
- The service or resource to which the resource is bound.
* - operation
- bound
- The service or resource to which the operation is bound.
- put
- The operation defined as the :ref:`put-lifecycle` of a resource.
* - operation
- input
- The input structure of the operation (if present).
- The input structure of an operation.

.. note::

Expand All @@ -1225,7 +1210,7 @@ The table below lists the labeled directed relationships from each shape.

* - operation
- output
- The output structure of the operation (if present).
- The output structure of an operation.

.. note::

Expand All @@ -1234,23 +1219,25 @@ The table below lists the labeled directed relationships from each shape.

* - operation
- error
- Each error structure referenced by the operation (if present).
- Each error structure of an operation.
* - list
- member
- The :ref:`member` of the list, including if it was inherited from a
mixin. Note that this is not the shape targeted by the member.
- The :ref:`member <member>` of a list.
* - map
- member
- The key and value members of the map, including those inherited from
mixins. Note that these are not the shapes targeted by the member.
- The key and value members of a map.
* - structure
- member
- Each structure member, including members inherited from mixins. Note
that these are not the shapes targeted by the members.
- Each structure member.
* - union
- member
- Each union member, including members inherited from mixins. Note that
these are not the shapes targeted by the members.
- Each union member.
* - enum
- member
- Each enum member.
* - intEnum
- member
- Each intEnum member.
* - member
-
- The shape targeted by the member. Note that member targets have no
Expand All @@ -1260,21 +1247,21 @@ The table below lists the labeled directed relationships from each shape.
- Each trait applied to a shape. The neighbor shape is the shape that
defines the trait. This kind of relationship is only traversed if the
``trait`` relationship is explicitly stated as a desired directed
neighbor relationship type.
neighbor relationship type (for example, ``-[trait]->``).
* - ``*``
- mixin
- Every mixin applied to the shape.

.. note::

A normal ``member`` relationship exists from a given shape to all
its inherited mixin members.
its mixed in members.

.. important::
.. note::

Implementations MUST tolerate parsing unknown relationship types. When
evaluated, the directed traversal of unknown relationship types yields
no shapes.
Implementations MAY tolerate parsing unknown relationship types. When
evaluated, the traversal of unknown relationship types SHOULD yield
nothing.


Functions
Expand Down Expand Up @@ -1319,8 +1306,7 @@ no documentation:

.. code-block:: none
:test(-[bound, resource]->)
:not([trait|documentation])
:test(< resource) :not([trait|documentation])
``:is``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.neighbor.NeighborProvider;
import software.amazon.smithy.model.neighbor.Relationship;
import software.amazon.smithy.model.neighbor.RelationshipType;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ResourceShape;
Expand All @@ -48,18 +49,8 @@ public TopDownIndex(Model model) {

// Only traverse resource and operation bindings.
Predicate<Relationship> filter = rel -> {
switch (rel.getRelationshipType()) {
case RESOURCE:
case OPERATION:
case CREATE:
case READ:
case UPDATE:
case DELETE:
case LIST:
return true;
default:
return false;
}
RelationshipType type = rel.getRelationshipType();
return type == RelationshipType.RESOURCE || type.isOperationBinding();
};

for (ResourceShape resource : model.getResourceShapes()) {
Expand Down
Loading

0 comments on commit e47f881

Please sign in to comment.