-
Notifications
You must be signed in to change notification settings - Fork 180
/
ValidateUnsupportedConstraints.kt
303 lines (270 loc) · 14 KB
/
ValidateUnsupportedConstraints.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.smithy
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.BlobShape
import software.amazon.smithy.model.shapes.ByteShape
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.shapes.IntegerShape
import software.amazon.smithy.model.shapes.ListShape
import software.amazon.smithy.model.shapes.LongShape
import software.amazon.smithy.model.shapes.MapShape
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.SetShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.ShortShape
import software.amazon.smithy.model.traits.LengthTrait
import software.amazon.smithy.model.traits.RangeTrait
import software.amazon.smithy.model.traits.RequiredTrait
import software.amazon.smithy.model.traits.StreamingTrait
import software.amazon.smithy.model.traits.Trait
import software.amazon.smithy.model.traits.UniqueItemsTrait
import software.amazon.smithy.rust.codegen.core.smithy.DirectedWalker
import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticEventStreamUnionTrait
import software.amazon.smithy.rust.codegen.core.util.expectTrait
import software.amazon.smithy.rust.codegen.core.util.hasTrait
import software.amazon.smithy.rust.codegen.core.util.inputShape
import software.amazon.smithy.rust.codegen.core.util.orNull
import java.util.logging.Level
private sealed class UnsupportedConstraintMessageKind {
private val constraintTraitsUberIssue = "https://github.com/awslabs/smithy-rs/issues/1401"
fun intoLogMessage(ignoreUnsupportedConstraints: Boolean): LogMessage {
fun buildMessage(intro: String, willSupport: Boolean, trackingIssue: String? = null, canBeIgnored: Boolean = true): String {
var msg = """
$intro
This is not supported in the smithy-rs server SDK."""
if (willSupport) {
msg += """
It will be supported in the future."""
}
if (trackingIssue != null) {
msg += """
For more information, and to report if you're affected by this, please use the tracking issue: $trackingIssue."""
}
if (canBeIgnored) {
msg += """
If you want to go ahead and generate the server SDK ignoring unsupported constraint traits, set the key `ignoreUnsupportedConstraints`
inside the `runtimeConfig.codegen` JSON object in your `smithy-build.json` to `true`."""
}
return msg.trimIndent().replace("\n", " ")
}
fun buildMessageShapeHasUnsupportedConstraintTrait(
shape: Shape,
constraintTrait: Trait,
trackingIssue: String,
) =
buildMessage(
"The ${shape.type} shape `${shape.id}` has the constraint trait `${constraintTrait.toShapeId()}` attached.",
willSupport = true,
trackingIssue,
)
val level = if (ignoreUnsupportedConstraints) Level.WARNING else Level.SEVERE
return when (this) {
is UnsupportedConstraintOnMemberShape -> LogMessage(
level,
buildMessageShapeHasUnsupportedConstraintTrait(shape, constraintTrait, constraintTraitsUberIssue),
)
is UnsupportedConstraintOnShapeReachableViaAnEventStream -> LogMessage(
Level.SEVERE,
buildMessage(
"""
The ${shape.type} shape `${shape.id}` has the constraint trait `${constraintTrait.toShapeId()}` attached.
This shape is also part of an event stream; it is unclear what the semantics for constrained shapes in event streams are.
Please remove the trait from the shape to synthesize your model.
""".trimIndent().replace("\n", " "),
willSupport = false,
"https://github.com/awslabs/smithy/issues/1388",
canBeIgnored = false,
),
)
is UnsupportedLengthTraitOnStreamingBlobShape -> LogMessage(
level,
buildMessage(
"""
The ${shape.type} shape `${shape.id}` has both the `${lengthTrait.toShapeId()}` and `${streamingTrait.toShapeId()}` constraint traits attached.
It is unclear what the semantics for streaming blob shapes are.
""".trimIndent().replace("\n", " "),
willSupport = false,
"https://github.com/awslabs/smithy/issues/1389",
),
)
is UnsupportedRangeTraitOnShape -> LogMessage(
level,
buildMessageShapeHasUnsupportedConstraintTrait(shape, rangeTrait, constraintTraitsUberIssue),
)
is UnsupportedUniqueItemsTraitOnShape -> LogMessage(
level,
buildMessageShapeHasUnsupportedConstraintTrait(shape, uniqueItemsTrait, constraintTraitsUberIssue),
)
is UnsupportedMapShapeReachableFromUniqueItemsList -> LogMessage(
Level.SEVERE,
buildMessage(
"""
The map shape `${mapShape.id}` is reachable from the list shape `${listShape.id}`, which has the
`@uniqueItems` trait attached.
""".trimIndent().replace("\n", " "),
willSupport = false,
trackingIssue = "https://github.com/awslabs/smithy/issues/1567",
canBeIgnored = false,
),
)
}
}
}
private data class OperationWithConstrainedInputWithoutValidationException(val shape: OperationShape)
private data class UnsupportedConstraintOnMemberShape(val shape: MemberShape, val constraintTrait: Trait) :
UnsupportedConstraintMessageKind()
private data class UnsupportedConstraintOnShapeReachableViaAnEventStream(val shape: Shape, val constraintTrait: Trait) :
UnsupportedConstraintMessageKind()
private data class UnsupportedLengthTraitOnStreamingBlobShape(
val shape: BlobShape,
val lengthTrait: LengthTrait,
val streamingTrait: StreamingTrait,
) : UnsupportedConstraintMessageKind()
private data class UnsupportedRangeTraitOnShape(val shape: Shape, val rangeTrait: RangeTrait) :
UnsupportedConstraintMessageKind()
private data class UnsupportedUniqueItemsTraitOnShape(val shape: Shape, val uniqueItemsTrait: UniqueItemsTrait) :
UnsupportedConstraintMessageKind()
private data class UnsupportedMapShapeReachableFromUniqueItemsList(
val listShape: ListShape,
val uniqueItemsTrait: UniqueItemsTrait,
val mapShape: MapShape,
) : UnsupportedConstraintMessageKind()
data class LogMessage(val level: Level, val message: String)
data class ValidationResult(val shouldAbort: Boolean, val messages: List<LogMessage>) :
Throwable(message = messages.joinToString("\n") { it.message })
private val unsupportedConstraintsOnMemberShapes = allConstraintTraits - RequiredTrait::class.java
/**
* Validate that all constrained operations have the shape [validationExceptionShapeId] shape attached to their errors.
*/
fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached(
model: Model,
service: ServiceShape,
validationExceptionShapeId: ShapeId,
): ValidationResult {
// Traverse the model and error out if an operation uses constrained input, but it does not have
// `ValidationException` attached in `errors`. https://github.com/awslabs/smithy-rs/pull/1199#discussion_r809424783
// TODO(https://github.com/awslabs/smithy-rs/issues/1401): This check will go away once we add support for
// `disableDefaultValidation` set to `true`, allowing service owners to map from constraint violations to operation errors.
val walker = DirectedWalker(model)
val operationsWithConstrainedInputWithoutValidationExceptionSet = walker.walkShapes(service)
.filterIsInstance<OperationShape>()
.asSequence()
.filter { operationShape ->
// Walk the shapes reachable via this operation input.
walker.walkShapes(operationShape.inputShape(model))
.any { it is SetShape || it is EnumShape || it.hasConstraintTrait() }
}
.filter { !it.errors.contains(validationExceptionShapeId) }
.map { OperationWithConstrainedInputWithoutValidationException(it) }
.toSet()
val messages =
operationsWithConstrainedInputWithoutValidationExceptionSet.map {
LogMessage(
Level.SEVERE,
"""
Operation ${it.shape.id} takes in input that is constrained
(https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html), and as such can fail with a
validation exception. You must model this behavior in the operation shape in your model file.
""".trimIndent().replace("\n", " ") +
"""
```smithy
use $validationExceptionShapeId
operation ${it.shape.id.name} {
...
errors: [..., ${validationExceptionShapeId.name}] // <-- Add this.
}
```
""".trimIndent(),
)
}
return ValidationResult(shouldAbort = messages.any { it.level == Level.SEVERE }, messages)
}
fun validateUnsupportedConstraints(
model: Model,
service: ServiceShape,
codegenConfig: ServerCodegenConfig,
): ValidationResult {
// Traverse the model and error out if:
val walker = DirectedWalker(model)
// 1. Constraint traits on streaming blob shapes are used. Their semantics are unclear.
// TODO(https://github.com/awslabs/smithy/issues/1389)
val unsupportedLengthTraitOnStreamingBlobShapeSet = walker
.walkShapes(service)
.asSequence()
.filterIsInstance<BlobShape>()
.filter { it.hasTrait<LengthTrait>() && it.hasTrait<StreamingTrait>() }
.map { UnsupportedLengthTraitOnStreamingBlobShape(it, it.expectTrait(), it.expectTrait()) }
.toSet()
// 2. Constraint traits in event streams are used. Their semantics are unclear.
// TODO(https://github.com/awslabs/smithy/issues/1388)
val eventStreamShapes = walker
.walkShapes(service)
.asSequence()
.filter { it.hasTrait<SyntheticEventStreamUnionTrait>() }
val unsupportedConstraintOnNonErrorShapeReachableViaAnEventStreamSet = eventStreamShapes
.flatMap { walker.walkShapes(it) }
.filterMapShapesToTraits(allConstraintTraits)
.map { (shape, trait) -> UnsupportedConstraintOnShapeReachableViaAnEventStream(shape, trait) }
.toSet()
val eventStreamErrors = eventStreamShapes.map {
it.expectTrait<SyntheticEventStreamUnionTrait>()
}.map { it.errorMembers }
val unsupportedConstraintErrorShapeReachableViaAnEventStreamSet = eventStreamErrors
.flatMap { it }
.flatMap { walker.walkShapes(it) }
.filterMapShapesToTraits(allConstraintTraits)
.map { (shape, trait) -> UnsupportedConstraintOnShapeReachableViaAnEventStream(shape, trait) }
.toSet()
val unsupportedConstraintShapeReachableViaAnEventStreamSet =
unsupportedConstraintOnNonErrorShapeReachableViaAnEventStreamSet + unsupportedConstraintErrorShapeReachableViaAnEventStreamSet
// 3. Range trait used on unsupported shapes.
// TODO(https://github.com/awslabs/smithy-rs/issues/1401)
val unsupportedRangeTraitOnShapeSet = walker
.walkShapes(service)
.asSequence()
.filterNot { it is IntegerShape || it is ShortShape || it is LongShape || it is ByteShape }
.filterMapShapesToTraits(setOf(RangeTrait::class.java))
.map { (shape, rangeTrait) -> UnsupportedRangeTraitOnShape(shape, rangeTrait as RangeTrait) }
.toSet()
// 4. `@uniqueItems` cannot reach a map shape.
// See https://github.com/awslabs/smithy/issues/1567.
val mapShapeReachableFromUniqueItemsListShapeSet = walker
.walkShapes(service)
.asSequence()
.filterMapShapesToTraits(setOf(UniqueItemsTrait::class.java))
.flatMap { (listShape, uniqueItemsTrait) ->
walker.walkShapes(listShape).filterIsInstance<MapShape>().map { mapShape ->
UnsupportedMapShapeReachableFromUniqueItemsList(
listShape as ListShape,
uniqueItemsTrait as UniqueItemsTrait,
mapShape,
)
}
}
.toSet()
val messages =
unsupportedLengthTraitOnStreamingBlobShapeSet.map {
it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints)
} +
unsupportedConstraintShapeReachableViaAnEventStreamSet.map {
it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints)
} +
unsupportedRangeTraitOnShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } +
mapShapeReachableFromUniqueItemsListShapeSet.map {
it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints)
}
return ValidationResult(shouldAbort = messages.any { it.level == Level.SEVERE }, messages)
}
/**
* Returns a sequence over pairs `(shape, trait)`.
* The returned sequence contains one pair per shape in the input iterable that has attached a trait contained in [traits].
*/
private fun Sequence<Shape>.filterMapShapesToTraits(traits: Set<Class<out Trait>>): Sequence<Pair<Shape, Trait>> =
this.map { shape -> shape to traits.mapNotNull { shape.getTrait(it).orNull() } }
.flatMap { (shape, traits) -> traits.map { shape to it } }