smithy-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smithy_python/__init__.py +5 -0
- smithy_python/__main__.py +7 -0
- smithy_python/cli.py +180 -0
- smithy_python/environment.py +40 -0
- smithy_python/exceptions.py +27 -0
- smithy_python/model/__init__.py +56 -0
- smithy_python/model/_loader.py +573 -0
- smithy_python/model/_model.py +134 -0
- smithy_python/model/_prelude.py +60 -0
- smithy_python/model/_shape_id.py +115 -0
- smithy_python/model/_shapes.py +251 -0
- smithy_python/py.typed +0 -0
- smithy_python/selection.py +157 -0
- smithy_python-0.1.0.dist-info/METADATA +47 -0
- smithy_python-0.1.0.dist-info/RECORD +18 -0
- smithy_python-0.1.0.dist-info/WHEEL +4 -0
- smithy_python-0.1.0.dist-info/entry_points.txt +2 -0
- smithy_python-0.1.0.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Load a Smithy JSON AST into a flattened :class:`Model`.
|
|
4
|
+
|
|
5
|
+
Loading happens in phases:
|
|
6
|
+
|
|
7
|
+
1. Decode and structurally check the JSON AST, parsing every shape into an
|
|
8
|
+
intermediate ``_RawShape`` (input order is kept everywhere; Python dicts
|
|
9
|
+
preserve insertion order, and ``json`` decodes objects in document order).
|
|
10
|
+
2. Add the embedded prelude shapes the input does not define.
|
|
11
|
+
3. Apply ``"type": "apply"`` entries in input order. Traits applied to a shape
|
|
12
|
+
or to one of its locally declared members are merged into that local
|
|
13
|
+
definition (so they propagate through mixins); traits applied to a member
|
|
14
|
+
the shape only inherits are kept aside and merged after flattening.
|
|
15
|
+
4. Flatten mixins (depth-first, memoized, cycle-checked).
|
|
16
|
+
5. Resolve every relationship and member target, then build the immutable
|
|
17
|
+
shape objects.
|
|
18
|
+
|
|
19
|
+
Trait merge order, used both for mixins and member overrides, is ``dict``
|
|
20
|
+
update order: traits inherited from each mixin in declaration order, then the
|
|
21
|
+
shape's own traits, then applied traits. A key keeps the position where it was
|
|
22
|
+
first introduced and takes the value from the last writer.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from types import MappingProxyType
|
|
31
|
+
from typing import Final, cast
|
|
32
|
+
|
|
33
|
+
from ..exceptions import InvalidShapeIdError, ModelError
|
|
34
|
+
from ._model import Model
|
|
35
|
+
from ._prelude import prelude_ast
|
|
36
|
+
from ._shape_id import ShapeId
|
|
37
|
+
from ._shapes import (
|
|
38
|
+
MIXIN_TRAIT,
|
|
39
|
+
SHAPE_CLASSES,
|
|
40
|
+
UNIT,
|
|
41
|
+
MemberShape,
|
|
42
|
+
Node,
|
|
43
|
+
OperationShape,
|
|
44
|
+
ResourceShape,
|
|
45
|
+
ServiceShape,
|
|
46
|
+
Shape,
|
|
47
|
+
ShapeType,
|
|
48
|
+
SimpleShape,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Smithy 1.0 differs semantically (set shapes, boxing), so only 2.x is loaded.
|
|
52
|
+
SUPPORTED_MAJOR_VERSION: Final = "2"
|
|
53
|
+
|
|
54
|
+
type _Json = dict[str, object]
|
|
55
|
+
type _Traits = dict[ShapeId, Node]
|
|
56
|
+
|
|
57
|
+
_LIFECYCLE: Final = ("create", "put", "read", "update", "delete", "list")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_model(source: bytes | str | Mapping[str, object]) -> Model:
|
|
61
|
+
"""Load a Smithy JSON AST model.
|
|
62
|
+
|
|
63
|
+
:param source: The JSON AST as bytes, text, or an already-decoded mapping.
|
|
64
|
+
:returns: The flattened model, with embedded prelude shapes.
|
|
65
|
+
:raises ModelError: If the input is malformed or a reference is dangling.
|
|
66
|
+
"""
|
|
67
|
+
root = _decode(source)
|
|
68
|
+
version = root.get("smithy")
|
|
69
|
+
if not isinstance(version, str):
|
|
70
|
+
raise ModelError('Smithy JSON AST model must contain a "smithy" version string')
|
|
71
|
+
if version.partition(".")[0] != SUPPORTED_MAJOR_VERSION:
|
|
72
|
+
raise ModelError(
|
|
73
|
+
f"Unsupported Smithy version {version!r}: only Smithy "
|
|
74
|
+
f"{SUPPORTED_MAJOR_VERSION}.x JSON ASTs are supported"
|
|
75
|
+
)
|
|
76
|
+
metadata = _freeze(_object(root.get("metadata", {}), '"metadata"'))
|
|
77
|
+
shapes_node = _object(root.get("shapes", {}), '"shapes"')
|
|
78
|
+
|
|
79
|
+
raw_shapes: dict[ShapeId, _RawShape] = {}
|
|
80
|
+
applies: list[tuple[ShapeId, _Traits]] = []
|
|
81
|
+
for key, value in shapes_node.items():
|
|
82
|
+
shape_id = _parse_id(key, "Shape ID")
|
|
83
|
+
node = _object(value, f"Shape {key}")
|
|
84
|
+
type_name = node.get("type")
|
|
85
|
+
if type_name == "apply":
|
|
86
|
+
traits = _parse_traits(node.get("traits", {}), key)
|
|
87
|
+
applies.append((shape_id, traits))
|
|
88
|
+
continue
|
|
89
|
+
if shape_id.member is not None:
|
|
90
|
+
raise ModelError(
|
|
91
|
+
f"Shape ID {key} must not contain a member; "
|
|
92
|
+
"only apply entries may target members"
|
|
93
|
+
)
|
|
94
|
+
raw_shapes[shape_id] = _parse_shape(shape_id, node)
|
|
95
|
+
|
|
96
|
+
for key, value in prelude_ast().items():
|
|
97
|
+
shape_id = ShapeId.from_string(key)
|
|
98
|
+
if shape_id not in raw_shapes:
|
|
99
|
+
raw_shapes[shape_id] = _parse_shape(shape_id, dict(value))
|
|
100
|
+
|
|
101
|
+
for target, traits in applies:
|
|
102
|
+
_apply(raw_shapes, target, traits)
|
|
103
|
+
|
|
104
|
+
flattener = _Flattener(raw_shapes)
|
|
105
|
+
flat = {shape_id: flattener.flatten(shape_id) for shape_id in raw_shapes}
|
|
106
|
+
|
|
107
|
+
for raw in flat.values():
|
|
108
|
+
_check_required_members(raw)
|
|
109
|
+
_check_references(raw, flat)
|
|
110
|
+
|
|
111
|
+
return Model(
|
|
112
|
+
smithy_version=version,
|
|
113
|
+
metadata=cast(Mapping[str, Node], metadata),
|
|
114
|
+
shapes={shape_id: _build(raw) for shape_id, raw in flat.items()},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(slots=True)
|
|
119
|
+
class _RawMember:
|
|
120
|
+
target: ShapeId
|
|
121
|
+
traits: _Traits
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(slots=True)
|
|
125
|
+
class _RawShape:
|
|
126
|
+
id: ShapeId
|
|
127
|
+
type: ShapeType
|
|
128
|
+
traits: _Traits = field(default_factory=dict[ShapeId, Node])
|
|
129
|
+
mixins: list[ShapeId] = field(default_factory=list[ShapeId])
|
|
130
|
+
members: dict[str, _RawMember] = field(default_factory=dict[str, _RawMember])
|
|
131
|
+
# Service / resource / operation properties. ``None`` means "not set".
|
|
132
|
+
version: str | None = None
|
|
133
|
+
refs: dict[str, ShapeId] = field(default_factory=dict[str, ShapeId])
|
|
134
|
+
"""Single references: resource lifecycle operations, operation input/output."""
|
|
135
|
+
ref_lists: dict[str, list[ShapeId]] = field(
|
|
136
|
+
default_factory=dict[str, list[ShapeId]]
|
|
137
|
+
)
|
|
138
|
+
"""operations, resources, errors, collectionOperations."""
|
|
139
|
+
named_refs: dict[str, dict[str, ShapeId]] = field(
|
|
140
|
+
default_factory=dict[str, dict[str, ShapeId]]
|
|
141
|
+
)
|
|
142
|
+
"""identifiers, properties."""
|
|
143
|
+
rename: dict[ShapeId, str] = field(default_factory=dict[ShapeId, str])
|
|
144
|
+
applied_member_traits: dict[str, _Traits] = field(
|
|
145
|
+
default_factory=dict[str, _Traits]
|
|
146
|
+
)
|
|
147
|
+
"""Traits applied to members this shape does not declare locally."""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# --- Decoding -----------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _reject_constant(value: str) -> object:
|
|
154
|
+
# Python's json module accepts NaN/Infinity by default; JSON does not.
|
|
155
|
+
raise ValueError(f"{value} is not valid JSON")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _decode(source: bytes | str | Mapping[str, object]) -> _Json:
|
|
159
|
+
if isinstance(source, bytes | str):
|
|
160
|
+
try:
|
|
161
|
+
decoded: object = json.loads(source, parse_constant=_reject_constant)
|
|
162
|
+
except ValueError as error:
|
|
163
|
+
raise ModelError(f"Invalid JSON in Smithy model: {error}") from None
|
|
164
|
+
else:
|
|
165
|
+
decoded = source
|
|
166
|
+
if not isinstance(decoded, Mapping):
|
|
167
|
+
raise ModelError("Smithy JSON AST model must be a JSON object")
|
|
168
|
+
return dict(cast(Mapping[str, object], decoded))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _object(value: object, what: str) -> _Json:
|
|
172
|
+
if not isinstance(value, Mapping):
|
|
173
|
+
raise ModelError(f"{what} must be a JSON object")
|
|
174
|
+
return dict(cast(Mapping[str, object], value))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _array(value: object, what: str) -> list[object]:
|
|
178
|
+
if not isinstance(value, list):
|
|
179
|
+
raise ModelError(f"{what} must be a JSON array")
|
|
180
|
+
return cast(list[object], value)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _parse_id(value: object, what: str) -> ShapeId:
|
|
184
|
+
if not isinstance(value, str):
|
|
185
|
+
raise ModelError(f"{what} must be a string shape ID, found {value!r}")
|
|
186
|
+
try:
|
|
187
|
+
return ShapeId.from_string(value)
|
|
188
|
+
except InvalidShapeIdError as error:
|
|
189
|
+
raise InvalidShapeIdError(f"{what}: {error}") from None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_ref(value: object, what: str) -> ShapeId:
|
|
193
|
+
"""Parse a ``{"target": "ns#Name"}`` reference."""
|
|
194
|
+
node = _object(value, what)
|
|
195
|
+
if "target" not in node:
|
|
196
|
+
raise ModelError(f'{what} is missing a "target"')
|
|
197
|
+
return _parse_id(node["target"], what)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _parse_traits(value: object, owner: str) -> _Traits:
|
|
201
|
+
node = _object(value, f'{owner}: "traits"')
|
|
202
|
+
return {
|
|
203
|
+
_parse_id(key, f"{owner}: trait"): _freeze(trait) for key, trait in node.items()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _freeze(value: object) -> Node:
|
|
208
|
+
"""Copy a decoded JSON value into a deeply immutable form.
|
|
209
|
+
|
|
210
|
+
Objects become read-only mappings (keeping key order) and arrays become
|
|
211
|
+
tuples. Values inherited through mixins are shared between shapes, so
|
|
212
|
+
freezing them keeps one consumer from changing another shape's traits.
|
|
213
|
+
"""
|
|
214
|
+
if isinstance(value, Mapping):
|
|
215
|
+
return MappingProxyType(
|
|
216
|
+
{k: _freeze(v) for k, v in cast(Mapping[str, object], value).items()}
|
|
217
|
+
)
|
|
218
|
+
if isinstance(value, list | tuple):
|
|
219
|
+
return tuple(_freeze(v) for v in cast(Sequence[object], value))
|
|
220
|
+
return cast(Node, value)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_member(container: ShapeId, name: str, value: object) -> _RawMember:
|
|
224
|
+
member_id = str(container.with_member(name))
|
|
225
|
+
node = _object(value, f"Member {member_id}")
|
|
226
|
+
if "target" not in node:
|
|
227
|
+
raise ModelError(f'Member {member_id} is missing a "target"')
|
|
228
|
+
return _RawMember(
|
|
229
|
+
target=_parse_id(node["target"], f"Member {member_id}"),
|
|
230
|
+
traits=_parse_traits(node.get("traits", {}), f"Member {member_id}"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_shape(shape_id: ShapeId, node: _Json) -> _RawShape:
|
|
235
|
+
if "type" not in node:
|
|
236
|
+
raise ModelError(f'Shape {shape_id} is missing a "type"')
|
|
237
|
+
type_name = node["type"]
|
|
238
|
+
try:
|
|
239
|
+
shape_type = ShapeType(type_name)
|
|
240
|
+
except ValueError:
|
|
241
|
+
shape_type = None
|
|
242
|
+
if shape_type is None or shape_type is ShapeType.MEMBER:
|
|
243
|
+
raise ModelError(f"Shape {shape_id} has unknown type {type_name!r}")
|
|
244
|
+
|
|
245
|
+
what = f"Shape {shape_id}"
|
|
246
|
+
raw = _RawShape(
|
|
247
|
+
id=shape_id,
|
|
248
|
+
type=shape_type,
|
|
249
|
+
traits=_parse_traits(node.get("traits", {}), what),
|
|
250
|
+
mixins=[
|
|
251
|
+
_parse_ref(ref, f"{what}: mixin")
|
|
252
|
+
for ref in _array(node.get("mixins", []), f'{what}: "mixins"')
|
|
253
|
+
],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
match shape_type:
|
|
257
|
+
case ShapeType.LIST:
|
|
258
|
+
_parse_named_members(raw, node, ("member",))
|
|
259
|
+
case ShapeType.MAP:
|
|
260
|
+
_parse_named_members(raw, node, ("key", "value"))
|
|
261
|
+
case (
|
|
262
|
+
ShapeType.STRUCTURE | ShapeType.UNION | ShapeType.ENUM | ShapeType.INT_ENUM
|
|
263
|
+
):
|
|
264
|
+
members = _object(node.get("members", {}), f'{what}: "members"')
|
|
265
|
+
for name, member in members.items():
|
|
266
|
+
raw.members[name] = _parse_member(shape_id, name, member)
|
|
267
|
+
case ShapeType.SERVICE:
|
|
268
|
+
if "version" in node:
|
|
269
|
+
version = node["version"]
|
|
270
|
+
if not isinstance(version, str):
|
|
271
|
+
raise ModelError(f'{what}: "version" must be a string')
|
|
272
|
+
raw.version = version
|
|
273
|
+
_parse_ref_lists(raw, node, ("operations", "resources", "errors"))
|
|
274
|
+
rename = _object(node.get("rename", {}), f'{what}: "rename"')
|
|
275
|
+
for key, name in rename.items():
|
|
276
|
+
if not isinstance(name, str):
|
|
277
|
+
raise ModelError(f'{what}: "rename" values must be strings')
|
|
278
|
+
raw.rename[_parse_id(key, f"{what}: rename")] = name
|
|
279
|
+
case ShapeType.RESOURCE:
|
|
280
|
+
for prop in ("identifiers", "properties"):
|
|
281
|
+
named = _object(node.get(prop, {}), f'{what}: "{prop}"')
|
|
282
|
+
raw.named_refs[prop] = {
|
|
283
|
+
name: _parse_ref(ref, f"{what}: {prop} {name!r}")
|
|
284
|
+
for name, ref in named.items()
|
|
285
|
+
}
|
|
286
|
+
_parse_refs(raw, node, _LIFECYCLE)
|
|
287
|
+
_parse_ref_lists(
|
|
288
|
+
raw, node, ("operations", "collectionOperations", "resources")
|
|
289
|
+
)
|
|
290
|
+
case ShapeType.OPERATION:
|
|
291
|
+
_parse_refs(raw, node, ("input", "output"))
|
|
292
|
+
_parse_ref_lists(raw, node, ("errors",))
|
|
293
|
+
case _:
|
|
294
|
+
pass
|
|
295
|
+
return raw
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _parse_named_members(raw: _RawShape, node: _Json, names: Iterable[str]) -> None:
|
|
299
|
+
for name in names:
|
|
300
|
+
if name in node:
|
|
301
|
+
raw.members[name] = _parse_member(raw.id, name, node[name])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _parse_refs(raw: _RawShape, node: _Json, keys: Iterable[str]) -> None:
|
|
305
|
+
for key in keys:
|
|
306
|
+
if key in node:
|
|
307
|
+
raw.refs[key] = _parse_ref(node[key], f'Shape {raw.id}: "{key}"')
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _parse_ref_lists(raw: _RawShape, node: _Json, keys: Iterable[str]) -> None:
|
|
311
|
+
for key in keys:
|
|
312
|
+
what = f'Shape {raw.id}: "{key}"'
|
|
313
|
+
if key in node:
|
|
314
|
+
raw.ref_lists[key] = [
|
|
315
|
+
_parse_ref(ref, what) for ref in _array(node[key], what)
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# --- Apply ----------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _merge_applied(traits: _Traits, applied: Mapping[ShapeId, Node]) -> None:
|
|
323
|
+
"""Merge applied traits; array values are concatenated as the spec requires."""
|
|
324
|
+
for key, value in applied.items():
|
|
325
|
+
existing = traits.get(key)
|
|
326
|
+
if isinstance(existing, tuple) and isinstance(value, tuple):
|
|
327
|
+
traits[key] = (*existing, *value)
|
|
328
|
+
else:
|
|
329
|
+
traits[key] = value
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _apply(
|
|
333
|
+
raw_shapes: Mapping[ShapeId, _RawShape], target: ShapeId, traits: _Traits
|
|
334
|
+
) -> None:
|
|
335
|
+
raw = raw_shapes.get(target.root)
|
|
336
|
+
if raw is None:
|
|
337
|
+
raise ModelError(
|
|
338
|
+
f"Cannot apply traits to {target}: shape {target.root} is not defined"
|
|
339
|
+
)
|
|
340
|
+
if target.member is None:
|
|
341
|
+
_merge_applied(raw.traits, traits)
|
|
342
|
+
elif (member := raw.members.get(target.member)) is not None:
|
|
343
|
+
_merge_applied(member.traits, traits)
|
|
344
|
+
else:
|
|
345
|
+
# Possibly an inherited member; checked after flattening.
|
|
346
|
+
_merge_applied(raw.applied_member_traits.setdefault(target.member, {}), traits)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --- Mixins ---------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _inheritable_traits(mixin: _RawShape) -> _Traits:
|
|
353
|
+
local: set[ShapeId] = {MIXIN_TRAIT}
|
|
354
|
+
mixin_trait = mixin.traits.get(MIXIN_TRAIT)
|
|
355
|
+
if isinstance(mixin_trait, Mapping):
|
|
356
|
+
local_traits = cast(Mapping[str, Node], mixin_trait).get("localTraits", [])
|
|
357
|
+
if isinstance(local_traits, tuple):
|
|
358
|
+
for trait in local_traits:
|
|
359
|
+
local.add(_parse_id(trait, f"Mixin {mixin.id}: localTraits"))
|
|
360
|
+
return {k: v for k, v in mixin.traits.items() if k not in local}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _union(*lists: Iterable[ShapeId]) -> list[ShapeId]:
|
|
364
|
+
return list(dict.fromkeys(item for ids in lists for item in ids))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class _Flattener:
|
|
368
|
+
def __init__(self, raw_shapes: Mapping[ShapeId, _RawShape]) -> None:
|
|
369
|
+
self._raw = raw_shapes
|
|
370
|
+
self._done: dict[ShapeId, _RawShape] = {}
|
|
371
|
+
self._visiting: list[ShapeId] = []
|
|
372
|
+
|
|
373
|
+
def flatten(self, shape_id: ShapeId) -> _RawShape:
|
|
374
|
+
if (done := self._done.get(shape_id)) is not None:
|
|
375
|
+
return done
|
|
376
|
+
if shape_id in self._visiting:
|
|
377
|
+
cycle = [*self._visiting[self._visiting.index(shape_id) :], shape_id]
|
|
378
|
+
raise ModelError(
|
|
379
|
+
"Mixin cycle detected: " + " -> ".join(str(i) for i in cycle)
|
|
380
|
+
)
|
|
381
|
+
raw = self._raw[shape_id]
|
|
382
|
+
self._visiting.append(shape_id)
|
|
383
|
+
try:
|
|
384
|
+
mixins: list[_RawShape] = []
|
|
385
|
+
for mixin_id in raw.mixins:
|
|
386
|
+
if mixin_id not in self._raw:
|
|
387
|
+
raise ModelError(
|
|
388
|
+
f"Shape {shape_id} uses mixin {mixin_id}, which is not "
|
|
389
|
+
"defined in the model or prelude"
|
|
390
|
+
)
|
|
391
|
+
mixin = self.flatten(mixin_id)
|
|
392
|
+
if mixin.type is not raw.type:
|
|
393
|
+
raise ModelError(
|
|
394
|
+
f"Shape {shape_id} ({raw.type}) cannot use mixin "
|
|
395
|
+
f"{mixin_id} ({mixin.type})"
|
|
396
|
+
)
|
|
397
|
+
mixins.append(mixin)
|
|
398
|
+
flat = _merge(raw, mixins)
|
|
399
|
+
finally:
|
|
400
|
+
self._visiting.pop()
|
|
401
|
+
self._done[shape_id] = flat
|
|
402
|
+
return flat
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _merge(raw: _RawShape, mixins: list[_RawShape]) -> _RawShape:
|
|
406
|
+
traits: _Traits = {}
|
|
407
|
+
members: dict[str, _RawMember] = {}
|
|
408
|
+
for mixin in mixins:
|
|
409
|
+
traits.update(_inheritable_traits(mixin))
|
|
410
|
+
for name, member in mixin.members.items():
|
|
411
|
+
inherited = members.get(name)
|
|
412
|
+
merged = {**inherited.traits} if inherited else {}
|
|
413
|
+
merged.update(member.traits)
|
|
414
|
+
members[name] = _RawMember(member.target, merged)
|
|
415
|
+
traits.update(raw.traits)
|
|
416
|
+
for name, member in raw.members.items():
|
|
417
|
+
inherited = members.get(name)
|
|
418
|
+
merged = {**inherited.traits, **member.traits} if inherited else member.traits
|
|
419
|
+
members[name] = _RawMember(member.target, dict(merged))
|
|
420
|
+
for name, applied in raw.applied_member_traits.items():
|
|
421
|
+
member = members.get(name)
|
|
422
|
+
if member is None:
|
|
423
|
+
raise ModelError(
|
|
424
|
+
f"Cannot apply traits to {raw.id.with_member(name)}: "
|
|
425
|
+
f"member is not defined on {raw.id}"
|
|
426
|
+
)
|
|
427
|
+
_merge_applied(member.traits, applied)
|
|
428
|
+
|
|
429
|
+
flat = _RawShape(
|
|
430
|
+
id=raw.id,
|
|
431
|
+
type=raw.type,
|
|
432
|
+
traits=traits,
|
|
433
|
+
mixins=list(raw.mixins),
|
|
434
|
+
members=members,
|
|
435
|
+
version=raw.version,
|
|
436
|
+
)
|
|
437
|
+
for mixin in mixins:
|
|
438
|
+
if flat.version is None:
|
|
439
|
+
flat.version = mixin.version
|
|
440
|
+
if raw.version is not None:
|
|
441
|
+
flat.version = raw.version
|
|
442
|
+
|
|
443
|
+
for key in {k: None for m in [*mixins, raw] for k in m.refs}:
|
|
444
|
+
value = raw.refs.get(key)
|
|
445
|
+
for mixin in reversed(mixins):
|
|
446
|
+
if value is not None:
|
|
447
|
+
break
|
|
448
|
+
value = mixin.refs.get(key)
|
|
449
|
+
if value is not None:
|
|
450
|
+
flat.refs[key] = value
|
|
451
|
+
for key in {k: None for m in [*mixins, raw] for k in m.ref_lists}:
|
|
452
|
+
flat.ref_lists[key] = _union(
|
|
453
|
+
*(m.ref_lists.get(key, ()) for m in [*mixins, raw])
|
|
454
|
+
)
|
|
455
|
+
for key in {k: None for m in [*mixins, raw] for k in m.named_refs}:
|
|
456
|
+
named: dict[str, ShapeId] = {}
|
|
457
|
+
for m in [*mixins, raw]:
|
|
458
|
+
named.update(m.named_refs.get(key, {}))
|
|
459
|
+
flat.named_refs[key] = named
|
|
460
|
+
for m in [*mixins, raw]:
|
|
461
|
+
flat.rename.update(m.rename)
|
|
462
|
+
return flat
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# --- Validation -------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _check_required_members(raw: _RawShape) -> None:
|
|
469
|
+
required: tuple[str, ...] = ()
|
|
470
|
+
if raw.type is ShapeType.LIST:
|
|
471
|
+
required = ("member",)
|
|
472
|
+
elif raw.type is ShapeType.MAP:
|
|
473
|
+
required = ("key", "value")
|
|
474
|
+
for name in required:
|
|
475
|
+
if name not in raw.members:
|
|
476
|
+
raise ModelError(f'Shape {raw.id} ({raw.type}) is missing its "{name}"')
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _check_references(raw: _RawShape, shapes: Mapping[ShapeId, _RawShape]) -> None:
|
|
480
|
+
def missing(target: ShapeId) -> bool:
|
|
481
|
+
return target not in shapes
|
|
482
|
+
|
|
483
|
+
for name, member in raw.members.items():
|
|
484
|
+
if missing(member.target):
|
|
485
|
+
raise ModelError(
|
|
486
|
+
f"Member {raw.id.with_member(name)} targets {member.target}, "
|
|
487
|
+
"which is not defined in the model or prelude"
|
|
488
|
+
)
|
|
489
|
+
refs: list[ShapeId] = [
|
|
490
|
+
*raw.refs.values(),
|
|
491
|
+
*(ref for refs in raw.ref_lists.values() for ref in refs),
|
|
492
|
+
*(ref for named in raw.named_refs.values() for ref in named.values()),
|
|
493
|
+
*raw.rename,
|
|
494
|
+
]
|
|
495
|
+
for ref in refs:
|
|
496
|
+
if missing(ref):
|
|
497
|
+
raise ModelError(
|
|
498
|
+
f"Shape {raw.id} references {ref}, which is not defined in the "
|
|
499
|
+
"model or prelude"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# --- Building -----------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _freeze_traits(traits: _Traits) -> Mapping[ShapeId, Node]:
|
|
507
|
+
return MappingProxyType(dict(traits))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _build(raw: _RawShape) -> Shape:
|
|
511
|
+
traits = _freeze_traits(raw.traits)
|
|
512
|
+
mixins = tuple(raw.mixins)
|
|
513
|
+
members = MappingProxyType(
|
|
514
|
+
{
|
|
515
|
+
name: MemberShape(
|
|
516
|
+
id=raw.id.with_member(name),
|
|
517
|
+
target=member.target,
|
|
518
|
+
traits=_freeze_traits(member.traits),
|
|
519
|
+
)
|
|
520
|
+
for name, member in raw.members.items()
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
cls = SHAPE_CLASSES[raw.type]
|
|
524
|
+
shape_id, shape_type = raw.id, raw.type
|
|
525
|
+
lists = {k: tuple(v) for k, v in raw.ref_lists.items()}
|
|
526
|
+
if cls is ServiceShape:
|
|
527
|
+
return ServiceShape(
|
|
528
|
+
id=shape_id,
|
|
529
|
+
type=shape_type,
|
|
530
|
+
traits=traits,
|
|
531
|
+
mixins=mixins,
|
|
532
|
+
version=raw.version,
|
|
533
|
+
operations=lists.get("operations", ()),
|
|
534
|
+
resources=lists.get("resources", ()),
|
|
535
|
+
errors=lists.get("errors", ()),
|
|
536
|
+
rename=MappingProxyType(dict(raw.rename)),
|
|
537
|
+
)
|
|
538
|
+
if cls is ResourceShape:
|
|
539
|
+
return ResourceShape(
|
|
540
|
+
id=shape_id,
|
|
541
|
+
type=shape_type,
|
|
542
|
+
traits=traits,
|
|
543
|
+
mixins=mixins,
|
|
544
|
+
identifiers=MappingProxyType(raw.named_refs.get("identifiers", {})),
|
|
545
|
+
properties=MappingProxyType(raw.named_refs.get("properties", {})),
|
|
546
|
+
create=raw.refs.get("create"),
|
|
547
|
+
put=raw.refs.get("put"),
|
|
548
|
+
read=raw.refs.get("read"),
|
|
549
|
+
update=raw.refs.get("update"),
|
|
550
|
+
delete=raw.refs.get("delete"),
|
|
551
|
+
list=raw.refs.get("list"),
|
|
552
|
+
operations=lists.get("operations", ()),
|
|
553
|
+
collection_operations=lists.get("collectionOperations", ()),
|
|
554
|
+
resources=lists.get("resources", ()),
|
|
555
|
+
)
|
|
556
|
+
if cls is OperationShape:
|
|
557
|
+
return OperationShape(
|
|
558
|
+
id=shape_id,
|
|
559
|
+
type=shape_type,
|
|
560
|
+
traits=traits,
|
|
561
|
+
mixins=mixins,
|
|
562
|
+
input=raw.refs.get("input", UNIT),
|
|
563
|
+
output=raw.refs.get("output", UNIT),
|
|
564
|
+
errors=lists.get("errors", ()),
|
|
565
|
+
)
|
|
566
|
+
# Simple shapes carry no members; aggregate shapes carry all of theirs.
|
|
567
|
+
return cls(
|
|
568
|
+
id=shape_id,
|
|
569
|
+
type=shape_type,
|
|
570
|
+
traits=traits,
|
|
571
|
+
mixins=mixins,
|
|
572
|
+
members=members if cls is not SimpleShape else MappingProxyType({}),
|
|
573
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""The loaded model and its navigation API."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import overload
|
|
10
|
+
|
|
11
|
+
from ..exceptions import ModelError, ShapeNotFoundError
|
|
12
|
+
from ._shape_id import PRELUDE_NAMESPACE, ShapeId, as_shape_id
|
|
13
|
+
from ._shapes import MemberShape, Node, ServiceShape, Shape, ShapeType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Model:
|
|
17
|
+
"""A loaded, mixin-flattened Smithy model.
|
|
18
|
+
|
|
19
|
+
``shapes`` holds top-level shapes (never members) in input order followed
|
|
20
|
+
by any embedded prelude shapes the input did not define. Members are
|
|
21
|
+
reachable via their container or ``get_shape`` with a member ID.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("_metadata", "_shapes", "_smithy_version")
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
smithy_version: str,
|
|
30
|
+
metadata: Mapping[str, Node],
|
|
31
|
+
shapes: Mapping[ShapeId, Shape],
|
|
32
|
+
) -> None:
|
|
33
|
+
self._smithy_version = smithy_version
|
|
34
|
+
self._metadata: Mapping[str, Node] = MappingProxyType(dict(metadata))
|
|
35
|
+
self._shapes: Mapping[ShapeId, Shape] = MappingProxyType(dict(shapes))
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def smithy_version(self) -> str:
|
|
39
|
+
"""The ``smithy`` version string from the JSON AST."""
|
|
40
|
+
return self._smithy_version
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def metadata(self) -> Mapping[str, Node]:
|
|
44
|
+
"""Model metadata in input key order."""
|
|
45
|
+
return self._metadata
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def shapes(self) -> Mapping[ShapeId, Shape]:
|
|
49
|
+
"""All top-level shapes, including prelude shapes."""
|
|
50
|
+
return self._shapes
|
|
51
|
+
|
|
52
|
+
def __contains__(self, shape_id: object) -> bool:
|
|
53
|
+
if isinstance(shape_id, str):
|
|
54
|
+
try:
|
|
55
|
+
shape_id = as_shape_id(shape_id)
|
|
56
|
+
except ModelError:
|
|
57
|
+
return False
|
|
58
|
+
return isinstance(shape_id, ShapeId) and self.find_shape(shape_id) is not None
|
|
59
|
+
|
|
60
|
+
def find_shape(self, shape_id: ShapeId | str) -> Shape | None:
|
|
61
|
+
"""Return the shape (or member) with this ID, or None."""
|
|
62
|
+
shape_id = as_shape_id(shape_id)
|
|
63
|
+
shape = self._shapes.get(shape_id.root)
|
|
64
|
+
if shape is None or shape_id.member is None:
|
|
65
|
+
return shape
|
|
66
|
+
return shape.members.get(shape_id.member)
|
|
67
|
+
|
|
68
|
+
def get_shape(self, shape_id: ShapeId | str) -> Shape:
|
|
69
|
+
"""Return the shape (or member) with this ID or raise ShapeNotFoundError."""
|
|
70
|
+
shape = self.find_shape(shape_id)
|
|
71
|
+
if shape is None:
|
|
72
|
+
raise ShapeNotFoundError(f"Shape {shape_id} is not defined in the model")
|
|
73
|
+
return shape
|
|
74
|
+
|
|
75
|
+
def expect_shape[T: Shape](self, shape_id: ShapeId | str, kind: type[T]) -> T:
|
|
76
|
+
"""Return the shape with this ID, requiring it to be a ``kind``."""
|
|
77
|
+
shape = self.get_shape(shape_id)
|
|
78
|
+
if not isinstance(shape, kind):
|
|
79
|
+
raise ModelError(
|
|
80
|
+
f"Shape {shape_id} is a {type(shape).__name__}; "
|
|
81
|
+
f"expected {kind.__name__}"
|
|
82
|
+
)
|
|
83
|
+
return shape
|
|
84
|
+
|
|
85
|
+
def get_target(self, member: MemberShape) -> Shape:
|
|
86
|
+
"""Return the shape a member targets."""
|
|
87
|
+
return self.get_shape(member.target)
|
|
88
|
+
|
|
89
|
+
def is_prelude(self, shape: Shape | ShapeId) -> bool:
|
|
90
|
+
"""Whether a shape belongs to the Smithy prelude (never generated)."""
|
|
91
|
+
shape_id = shape if isinstance(shape, ShapeId) else shape.id
|
|
92
|
+
return shape_id.namespace == PRELUDE_NAMESPACE
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def iter_shapes(
|
|
96
|
+
self, kind: None = None, *, include_prelude: bool = False
|
|
97
|
+
) -> Iterator[Shape]: ...
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def iter_shapes[T: Shape](
|
|
101
|
+
self, kind: type[T], *, include_prelude: bool = False
|
|
102
|
+
) -> Iterator[T]: ...
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def iter_shapes(
|
|
106
|
+
self, kind: ShapeType, *, include_prelude: bool = False
|
|
107
|
+
) -> Iterator[Shape]: ...
|
|
108
|
+
|
|
109
|
+
def iter_shapes(
|
|
110
|
+
self,
|
|
111
|
+
kind: type[Shape] | ShapeType | None = None,
|
|
112
|
+
*,
|
|
113
|
+
include_prelude: bool = False,
|
|
114
|
+
) -> Iterator[Shape]:
|
|
115
|
+
"""Iterate top-level shapes in model order, optionally filtered.
|
|
116
|
+
|
|
117
|
+
``kind`` may be a shape class or a ShapeType. Prelude shapes are
|
|
118
|
+
skipped unless ``include_prelude`` is set. Mixin shapes are included;
|
|
119
|
+
filter with ``Shape.is_mixin``.
|
|
120
|
+
"""
|
|
121
|
+
for shape in self._shapes.values():
|
|
122
|
+
if not include_prelude and self.is_prelude(shape):
|
|
123
|
+
continue
|
|
124
|
+
if kind is None:
|
|
125
|
+
yield shape
|
|
126
|
+
elif isinstance(kind, ShapeType):
|
|
127
|
+
if shape.type is kind:
|
|
128
|
+
yield shape
|
|
129
|
+
elif isinstance(shape, kind):
|
|
130
|
+
yield shape
|
|
131
|
+
|
|
132
|
+
def services(self) -> tuple[ServiceShape, ...]:
|
|
133
|
+
"""All non-mixin service shapes in model order."""
|
|
134
|
+
return tuple(s for s in self.iter_shapes(ServiceShape) if not s.is_mixin)
|