Skip to content

Commit 43e7e1b

Browse files
Add trait to declare types for metadata
This adds a new trait to declare types for metadata keys. Values for these keys will be validated against the shape targeted by the trait. While this enables modeling of the suppressions and validators keys, actually adding those to the model will be done later.
1 parent 31cb566 commit 43e7e1b

35 files changed

Lines changed: 859 additions & 123 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"description": "Added a new `metadata` trait that allows model authors to declare types for metadata keys that will be automatically validated when building models.",
4+
"pull_requests": []
5+
}

designs/typed-metadata.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Typed Metadata
2+
3+
Metadata is a schema-less extensibility mechanism used to associate metadata to
4+
an entire model. For example, metadata is used to define validators and
5+
model-wide suppressions.
6+
7+
This document describes a way to define typing information for metadata that is
8+
automatically validated by Smithy.
9+
10+
## Motivation
11+
12+
The schema-less nature of metadata has allowed it to be used for any purpose
13+
without much hassle. However, that has come at the cost of increased validation
14+
complexity. Any tool, including Smithy itself, that uses metadata in a
15+
structured way has to perform validation itself. By allowing opt-in validation,
16+
we can centralize and deduplicate that effort.
17+
18+
## Proposal
19+
20+
Model authors may globally declare the type of a metadata key by targeting a
21+
shape with the `@metadata` trait.
22+
23+
```smithy
24+
/// Defines a type for a metadata key.
25+
///
26+
/// If a matching key is defined in the model, its value will be validated
27+
/// according to the targeted shape.
28+
///
29+
/// The type for any metadata key MUST only be defined once.
30+
@trait(selector: "dataType :not([trait|input]) :not([trait|output])")
31+
structure metadata {
32+
/// The metadata key to validate. Each key MUST only be defined once.
33+
@required
34+
@length(min: 1)
35+
key: String
36+
}
37+
```
38+
39+
For example, the
40+
[suppressions metadata](https://smithy.io/2.0/spec/model-validation.html#suppressions-metadata)
41+
could be defined as:
42+
43+
```smithy
44+
$version: "2.0"
45+
46+
namespace smithy.api
47+
48+
@metadata(key: "suppressions")
49+
list MetadataSuppressions {
50+
member: MetadataSuppression
51+
}
52+
53+
structure MetadataSuppression {
54+
@required
55+
id: String
56+
57+
@required
58+
@pattern("^(\*|[_a-zA-Z]\w*(\.[_a-zA-Z]\w*)*)$")
59+
namespace: String
60+
61+
reason: String
62+
}
63+
```
64+
65+
### Validation
66+
67+
A metadata key MUST NOT be defined more than once with the `@metadata` trait. If
68+
a metadata key is defined more than once with the `@metadata` trait, an
69+
`ERROR`-level validation event will be emitted.
70+
71+
Validation of metadata values will behave no differently than validation
72+
anywhere else, with severity levels being determined by each individual
73+
validator.
74+
75+
Shapes targeted by the metadata shape and shapes transitively referenced by
76+
those shapes will not trigger the unreferenced shapes validator.
77+
78+
Adding or removing this trait is considered backwards-compatible because
79+
metadata does not inherently change any interface. It may cause a build to break
80+
if the new validation rejects existing metadata, but that is intended behavior.
81+
82+
## Alternatives
83+
84+
### Inline definitions
85+
86+
The metadata trait globally defines the shape of a metadata key, but we could
87+
allow local, inline definitions additionally or instead. This can be achieved by
88+
using a special `$type` key in metadata's node value.
89+
90+
```smithy
91+
$version: "2.0"
92+
93+
metadata foo = {
94+
"$type": "com.example#Foo"
95+
"bar": "baz"
96+
}
97+
98+
namespace com.example
99+
100+
structure Foo {
101+
bar: String
102+
}
103+
```
104+
105+
The problem with this strategy is that it can only be applied to structure and
106+
union shapes, because other shape types have nowhere to add the `$type` key or
107+
it would potentially shadow a valid value key.
108+
109+
Other shapes could instead be defined via a `__type__` metadata key:
110+
111+
```smithy
112+
$version: "2.0"
113+
114+
metadata "__type__": [
115+
["foo", "com.example#Foo"]
116+
]
117+
118+
metadata foo = "bar"
119+
120+
namespace com.example
121+
122+
string Foo
123+
124+
@metadata(key: "__type__")
125+
list MetdataTypeDeclarations {
126+
member: MetadataTypeDeclaration
127+
}
128+
129+
@length(min: 2, max: 2)
130+
list MetadataTypeDeclaration {
131+
member: String
132+
}
133+
```
134+
135+
Unfortunately, this is inherently a global declaration that is not inline with
136+
the value, defeating the intent of an inline definition. The metadata trait is
137+
better suited to this purpose, as it is clearer, easier to validate, more easily
138+
discoverable, and more easily trackable.
139+
140+
Smithy 2.1 could introduce new syntax to declare inline metadata types that isn't
141+
tied to the value, but the AST could not support it without either exposing the
142+
same problem or making a breaking change.
143+
144+
## FAQ
145+
146+
### Does this interact with metadata merging semantics?
147+
148+
When a given metadata key is defined more than once, it usually results in a
149+
conflict that fails the build. If both definitions are arrays, however, they are
150+
instead merged. If a metadata key is defined as a list, the merged array will be
151+
validated.
152+
153+
This trait does not change merging semantics.
154+
155+
### Will the existing defined metadata keys get metadata-trait-based definitions?
156+
157+
Smithy currently has definitions for three metadata keys: `severityOverrides`,
158+
`suppressions`, and `validators`. All three could be defined with the metadata
159+
trait.
160+
161+
However, these three keys are currently parsed and validated early on when
162+
loading a model, before validation generally is run. They will need to be
163+
special-cased to either run early or to be skipped in favor of existing
164+
validation.
165+
166+
These keys will initially be reserved so they can't be defined. As the keys get
167+
full definitions, their reserve list entries will be removed.
168+
169+
### What happens if a defined metadata key is not used?
170+
171+
Nothing. Metadata keys are inherently optional. A `required` member may be added
172+
later to enforce presence if there is a need.

docs/source-2.0/spec/model-validation.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,3 +867,86 @@ Validator definition
867867
- ``string``
868868
- The :ref:`severity <severity-definition>` to use when an
869869
incompatible shape is found. Defaults to ``ERROR`` if not set.
870+
871+
872+
.. smithy-trait:: smithy.api#metadata
873+
.. _metadata-trait:
874+
875+
------------------
876+
``metadata`` trait
877+
------------------
878+
879+
:ref:`Metadata <metadata>` is schema-less by default. Any tool that consumes
880+
metadata, including Smithy itself, must validate it independently. The
881+
``metadata`` trait lets model authors define a type for a metadata key.
882+
When the type for a metadata key is defined this way, Smithy will automatically
883+
validate any value for that metadata key against its defined type.
884+
885+
Summary
886+
Defines a type for a metadata key by targeting a shape. When a metadata
887+
entry with a matching key is defined in the model, its value is
888+
validated against the targeted shape.
889+
Trait selector
890+
``dataType :not([trait|input]) :not([trait|output])``
891+
892+
*A :ref:`simple shape <simple-types>` or
893+
:ref:`aggregate shape <aggregate-types>` that is not marked with the
894+
:ref:`input trait <input-trait>` or :ref:`output trait <output-trait>`.*
895+
Value type
896+
``structure``
897+
898+
The ``metadata`` trait is a structure that contains the following members:
899+
900+
.. list-table::
901+
:header-rows: 1
902+
:widths: 10 10 80
903+
904+
* - Property
905+
- Type
906+
- Description
907+
* - key
908+
- ``string``
909+
- **Required**. The metadata key whose type is being defined. The
910+
type of any metadata key may only be defined once across the entire
911+
model. This value must be non-empty.
912+
913+
.. rubric:: Example
914+
915+
Consider a service that configures an ``auditLevel`` metadata entry. Defining
916+
a type for the key gives Smithy enough information to validate each usage:
917+
918+
.. code-block:: smithy
919+
920+
$version: "2"
921+
namespace smithy.example
922+
923+
@metadata(key: "auditLevel")
924+
enum AuditLevel {
925+
LOW
926+
MEDIUM
927+
HIGH
928+
}
929+
930+
With that declaration in place, any ``auditLevel`` metadata entry in the
931+
model is automatically validated against the ``AuditLevel`` shape. For example,
932+
the following model would produce a validation event because an invalid enum
933+
value is provided:
934+
935+
.. code-block:: smithy
936+
937+
$version: "2"
938+
939+
metadata auditLevel = "unknown"
940+
941+
Building the above model results in the following validation event:
942+
943+
.. code-block::
944+
945+
── ERROR ──────────────────────────────────────────── TypedMetadata.auditLevel
946+
File: example.smithy:3:23
947+
948+
3| metadata auditLevel = "unknown"
949+
| ^
950+
951+
metadata.auditLevel: String value provided for smithy.example#AuditLevel must
952+
be one of the following values: HIGH, LOW, MEDIUM

smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313
import software.amazon.smithy.model.selector.Selector;
1414
import software.amazon.smithy.model.shapes.Shape;
1515
import software.amazon.smithy.model.shapes.ShapeId;
16+
import software.amazon.smithy.model.traits.MetadataTrait;
1617
import software.amazon.smithy.model.traits.TraitDefinition;
1718
import software.amazon.smithy.utils.FunctionalUtils;
1819

1920
/**
20-
* Finds shapes that are not connected to a "root" shape, are not trait definitions, are not referenced by trait
21-
* definitions, and are not referenced in trait values through
22-
* {@link software.amazon.smithy.model.traits.IdRefTrait}.
21+
* Finds shapes that do not meet any of the following criteria:
22+
*
23+
* <ul>
24+
* <li>The shape is connected to a "root" shape.
25+
* <li>The shape is a trait definition or is connected to a trait definition.
26+
* <li>The shape is referenced in a trait value through {@link software.amazon.smithy.model.traits.IdRefTrait}.
27+
* <li>The shape is a metadata definition or is connected to a metadata definition.
28+
* </ul>
2329
*
2430
* <p>The "root" shapes defaults to all service shapes in the model. You can customize this by providing a selector
2531
* that considers every matching shape a root shape. For example, a model might consider all shapes marked with
@@ -82,6 +88,11 @@ public Set<Shape> compute(Model model) {
8288
shapeWalker.iterateShapes(trait, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
8389
}
8490

91+
// Don't remove shapes that are metadata definitions or connected to metadata definitions.
92+
for (Shape metadata : model.getShapesWithTrait(MetadataTrait.class)) {
93+
shapeWalker.iterateShapes(metadata, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
94+
}
95+
8596
// Any shape that wasn't identified as connected to a root is considered unreferenced.
8697
Set<Shape> result = new HashSet<>();
8798
for (Shape shape : model.toSet()) {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.model.traits;
6+
7+
import software.amazon.smithy.model.node.Node;
8+
import software.amazon.smithy.model.shapes.ShapeId;
9+
import software.amazon.smithy.utils.SmithyBuilder;
10+
import software.amazon.smithy.utils.ToSmithyBuilder;
11+
12+
/**
13+
* Globally declares the type of a metadata key.
14+
*
15+
* <p>When this trait is applied to a shape, any metadata entry in the model
16+
* with the same key is validated against the targeted shape. A given
17+
* metadata key may only have its type declared once across the entire model.
18+
*/
19+
public final class MetadataTrait extends AbstractTrait implements ToSmithyBuilder<MetadataTrait> {
20+
public static final ShapeId ID = ShapeId.from("smithy.api#metadata");
21+
22+
private final String key;
23+
24+
private MetadataTrait(Builder builder) {
25+
super(ID, builder.getSourceLocation());
26+
this.key = SmithyBuilder.requiredState("key", builder.key);
27+
}
28+
29+
/**
30+
* @return Creates a builder for a {@link MetadataTrait}.
31+
*/
32+
public static Builder builder() {
33+
return new Builder();
34+
}
35+
36+
/**
37+
* @return The metadata key that this trait defines a type for.
38+
*/
39+
public String getKey() {
40+
return key;
41+
}
42+
43+
@Override
44+
public Builder toBuilder() {
45+
return builder().sourceLocation(getSourceLocation()).key(key);
46+
}
47+
48+
@Override
49+
protected Node createNode() {
50+
return Node.objectNodeBuilder()
51+
.sourceLocation(getSourceLocation())
52+
.withMember("key", key)
53+
.build();
54+
}
55+
56+
public static final class Provider extends AbstractTrait.Provider {
57+
public Provider() {
58+
super(ID);
59+
}
60+
61+
@Override
62+
public Trait createTrait(ShapeId target, Node value) {
63+
Builder builder = builder().sourceLocation(value.getSourceLocation());
64+
builder.key(value.expectObjectNode().expectStringMember("key").getValue());
65+
MetadataTrait result = builder.build();
66+
result.setNodeCache(value);
67+
return result;
68+
}
69+
}
70+
71+
public static final class Builder extends AbstractTraitBuilder<MetadataTrait, Builder> {
72+
private String key;
73+
74+
private Builder() {}
75+
76+
/**
77+
* Sets the metadata key that this trait defines a type for.
78+
*
79+
* @param key The metadata key. Required.
80+
* @return Returns the builder.
81+
*/
82+
public Builder key(String key) {
83+
this.key = key;
84+
return this;
85+
}
86+
87+
@Override
88+
public MetadataTrait build() {
89+
return new MetadataTrait(this);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)