///|
/// Rebuild inherited content after removing mixin relationships, without
/// running validation or reintroducing prelude shapes into projected models.
pub fn Model::remove_mixin_relationships(
self : Model,
removed : Array[ShapeId],
) -> Model raise {
if !self.shapes
.values()
.any(shape => shape.mixins.iter().any(id => removed.contains(id.root()))) {
return self
}
let document = self.to_json_ast(include_prelude=true, preserve_mixins=true)
let assembly = Assembly::new()
for id, shape in self.shapes {
assembly.kinds[id.to_string()] = if shape.is_legacy_set() {
"set"
} else {
shape.body.keyword()
}
assembly.locations[id.to_string()] = shape.source
}
for id, entry in document.shapes.unwrap_or(Map([])) {
let source = self
.get(id)
.map(shape => shape.source)
.unwrap_or(location(""))
let fields = copy(object(@json.to_json(entry)))
if entry.kind is Apply {
let removed_member = self
.get(id)
.map(shape => shape.mixins.iter().any(id => removed.contains(id.root())))
.unwrap_or(false)
if !removed_member {
assembly.applications.push({
target: id,
traits: object_field(fields, "traits"),
source,
})
}
} else {
if fields.get("mixins") is Some(Array(mixins)) {
let kept = mixins.filter(value => {
string_field(object(value), "target")
.bind(ShapeId::parse)
.map(id => !removed.contains(id))
.unwrap_or(true)
})
if kept.is_empty() {
fields.remove("mixins")
} else {
fields["mixins"] = Json::array(kept)
}
}
if self.get(id).map(shape => shape.is_legacy_set()).unwrap_or(false) {
fields["type"] = Json::string("set")
}
assembly.raw[id] = { id, fields, source, }
assembly.known.add(id)
}
}
let flattened = assembly.flatten()
let shapes : Map[ShapeId, Shape] = Map([])
for _, raw in flattened {
assembly.lower(raw, shapes, flattened)
}
// Restore the original locations even for member declarations represented as
// apply entries in the preserved JSON AST.
for id, shape in shapes {
if self.shapes.get(id) is Some(original) {
shapes[id] = { ..shape, source: original.source, }
}
}
{ shapes, metadata: self.metadata, }
}
///|
fn node_fields(fields : Map[String, Json]) -> Map[String, NodeValue] raise {
let out : Map[String, NodeValue] = Map([])
for key, value in fields {
out[key] = NodeValue::from_json(value)
}
out
}
///|
fn Assembly::inherited_fields(
_self : Assembly,
raw : RawShape,
finished : Map[String, RawShape],
) -> Map[String, Json] {
let fields : Map[String, Json] = Map([])
let members : Map[String, Json] = Map([])
let traits : Map[String, Json] = Map([])
for reference_ in array_field(raw.fields, "mixins") {
guard string_field(object(reference_), "target") is Some(id) &&
finished.get(id) is Some(base) else {
continue
}
let base_traits = object_field(base.fields, "traits")
let local_traits = array_field(
object(base_traits.get("smithy.api#mixin").unwrap_or(Json::null())),
"localTraits",
)
for id, value in base_traits {
if id != "smithy.api#mixin" && !local_traits.contains(Json::string(id)) {
traits[id] = value
}
}
for name, member_ in raw_members(base.fields) {
let member_fields = copy(object(member_))
let member_traits = copy(
object_field(
object(members.get(name).unwrap_or(Json::empty_object())),
"traits",
),
)
override(member_traits, object_field(member_fields, "traits"))
if !member_traits.is_empty() {
member_fields["traits"] = Json::object(member_traits)
}
members[name] = Json::object(member_fields)
}
for key, value in base.fields {
if ["type", "traits", "mixins", "members", "member", "key", "value"].contains(
key,
) {
continue
}
match (fields.get(key), value) {
(Some(Array(a)), Array(b)) => fields[key] = Json::array(unique(a, b))
(Some(Object(a)), Object(b)) => {
let out = copy(a)
override(out, b)
fields[key] = Json::object(out)
}
_ => fields[key] = value
}
}
}
fields["type"] = raw.fields.get("type").unwrap_or(Json::null())
set_members(fields, members)
fields["traits"] = Json::object(traits)
fields
}
///|
fn override(fields : Map[String, Json], incoming : Map[String, Json]) -> Unit {
for key, value in incoming {
fields[key] = value
}
}
///|
fn unique(values : Array[Json], incoming : Array[Json]) -> Array[Json] {
let out = values.copy()
for value in incoming {
if !out.contains(value) {
out.push(value)
}
}
out
}
///|
fn raw_members(fields : Map[String, Json]) -> Map[String, Json] {
let kind = string_field(fields, "type").unwrap_or("")
if kind == "list" || kind == "set" || kind == "map" {
let out : Map[String, Json] = Map([])
for key in ["member", "key", "value"] {
if fields.get(key) is Some(value) {
out[key] = value
}
}
out
} else {
copy(object_field(fields, "members"))
}
}
///|
fn set_members(fields : Map[String, Json], members : Map[String, Json]) -> Unit {
let kind = string_field(fields, "type").unwrap_or("")
if kind == "list" || kind == "set" || kind == "map" {
for key, value in members {
fields[key] = value
}
} else if ["structure", "union", "enum", "intEnum"].contains(kind) {
fields["members"] = Json::object(members)
}
}
///|
fn Assembly::merge_member(
self : Assembly,
out : Map[String, Json],
name : String,
incoming : Json,
source : SourceLocation,
parent : String,
) -> Unit {
let fields = copy(object(incoming))
if out.get(name) is Some(Object(old)) {
if string_field(old, "target") is Some(a) &&
string_field(fields, "target") is Some(b) &&
a != b {
self.problem(
"SM-MODEL-008",
"I found conflicting targets for member \{parent}$\{name}: \{a} and \{b}.",
source,
shape=parent + "$" + name,
)
}
let merged = copy(old)
override(merged, fields)
let traits = copy(object_field(old, "traits"))
override(traits, object_field(fields, "traits"))
if !traits.is_empty() {
merged["traits"] = Json::object(traits)
}
out[name] = Json::object(merged)
} else {
out[name] = incoming
}
}
///|
fn Assembly::apply_roots(self : Assembly) -> Unit {
for application in self.applications {
guard ShapeId::parse(application.target) is Some(id) else {
self.problem(
"SM-MODEL-002",
"I expected an absolute apply target, but found \{application.target}.",
application.source,
)
continue
}
let builtin = self.raw
.get(id.root().to_string())
.map(r => is_builtin_source(r.source.file))
.unwrap_or(false)
if (id.namespace == "smithy.api" || builtin) &&
!is_builtin_source(application.source.file) {
self.problem(
"SM-MODEL-014",
"I cannot apply traits to the built-in shape \{id}.",
application.source,
shape=application.target,
)
continue
}
if id.member is Some(_) {
if !self.raw.contains(id.root().to_string()) {
self.problem(
"SM-MODEL-013",
"I could not resolve the apply target \{application.target}.",
application.source,
shape=application.target,
)
}
continue
}
guard self.raw.get(application.target) is Some(raw) else {
self.problem(
"SM-MODEL-013",
"I could not resolve the apply target \{application.target}.",
application.source,
shape=application.target,
)
continue
}
let traits = copy(object_field(raw.fields, "traits"))
self.merge_traits(traits, application.traits, application.source, raw.id)
if !traits.is_empty() {
raw.fields["traits"] = Json::object(traits)
}
}
}
///|
fn Assembly::flatten_one(
self : Assembly,
raw : RawShape,
finished : Map[String, RawShape],
) -> RawShape {
let fields : Map[String, Json] = {
"type": raw.fields.get("type").unwrap_or(Json::null()),
}
let traits : Map[String, Json] = Map([])
let members : Map[String, Json] = Map([])
let kind = string_field(raw.fields, "type").unwrap_or("")
let mixin_ids = array_field(raw.fields, "mixins")
let seen : Set[String] = Set([])
for mixin in mixin_ids {
guard string_field(object(mixin), "target") is Some(id) else { continue }
if seen.contains(id) {
self.problem(
"SM-MODEL-008",
"I found the duplicate mixin \{id} on \{raw.id}.",
raw.source,
shape=raw.id,
)
}
seen.add(id)
guard finished.get(id) is Some(base) else { continue }
let base_traits = object_field(base.fields, "traits")
if !base_traits.contains("smithy.api#mixin") ||
string_field(base.fields, "type") != Some(kind) {
self.problem(
"SM-MODEL-008",
"I expected a \{kind} mixin for \{raw.id}, but found \{id}.",
raw.source,
shape=raw.id,
)
continue
}
let local_traits = array_field(
object(base_traits.get("smithy.api#mixin").unwrap_or(Json::null())),
"localTraits",
)
for key, value in base_traits {
if key != "smithy.api#mixin" && !local_traits.contains(Json::string(key)) {
traits[key] = value
}
}
for name, member_ in raw_members(base.fields) {
self.merge_member(members, name, member_, raw.source, raw.id)
}
for key, value in base.fields {
if ["type", "traits", "mixins", "members", "member", "key", "value"].contains(
key,
) {
continue
}
match (fields.get(key), value) {
(Some(Array(a)), Array(b)) => fields[key] = Json::array(unique(a, b))
(Some(Object(a)), Object(b)) => {
let merged = copy(a)
override(merged, b)
fields[key] = Json::object(merged)
}
_ => fields[key] = value
}
}
}
let locals = raw_members(raw.fields)
let resource = self.bindings.get(raw.id).bind(id => finished.get(id))
if self.bindings.get(raw.id) is Some(id) && resource is None {
self.problem(
"SM-MODEL-012",
"I could not resolve resource binding \{id} for \{raw.id}.",
raw.source,
shape=raw.id,
)
}
if resource is Some(resource) &&
string_field(resource.fields, "type") != Some("resource") {
self.problem(
"SM-MODEL-005",
"I expected a resource binding for \{raw.id}, but found \{resource.id}.",
raw.source,
shape=raw.id,
)
}
for name, value in locals {
let member_ = copy(object(value))
if resource is Some(resource) &&
string_field(member_, "target") is Some(explicit) {
let candidate = match
object_field(resource.fields, "identifiers").get(name) {
Some(value) => Some(value)
None => object_field(resource.fields, "properties").get(name)
}
if candidate.bind(v => string_field(object(v), "target"))
is Some(expected) &&
explicit != expected {
self.problem(
"SM-MODEL-012",
"I expected resource-bound member \{raw.id}$\{name} to target \{expected}, but found \{explicit}.",
raw.source,
shape=raw.id,
)
}
}
if !member_.contains("target") {
let inherited = members
.get(name)
.bind(v => string_field(object(v), "target"))
let bound = resource.bind(r => {
let value = match object_field(r.fields, "identifiers").get(name) {
Some(value) => Some(value)
None => object_field(r.fields, "properties").get(name)
}
value.bind(v => string_field(object(v), "target"))
})
if inherited is Some(a) && bound is Some(b) && a != b {
self.problem(
"SM-MODEL-012",
"I found conflicting mixin and resource targets for \{raw.id}$\{name}.",
raw.source,
shape=raw.id,
)
}
match (if bound is Some(_) { bound } else { inherited }) {
Some(target) => member_["target"] = Json::string(target)
None =>
self.problem(
"SM-MODEL-012",
"I could not infer a target for the elided member \{raw.id}$\{name}.",
self.locations.get(raw.id + "$" + name).unwrap_or(raw.source),
shape=raw.id,
)
}
}
self.merge_member(members, name, Json::object(member_), raw.source, raw.id)
}
// External applications override inherited traits, and conflict with direct
// local applications exactly as traits written on the member do.
let external : Map[String, Map[String, Json]] = Map([])
for application in self.applications {
guard ShapeId::parse(application.target) is Some(id) &&
id.root().to_string() == raw.id &&
id.member is Some(name) else {
continue
}
if is_builtin_source(raw.source.file) &&
!is_builtin_source(application.source.file) {
continue
}
guard members.get(name) is Some(Object(member_)) else {
self.problem(
"SM-MODEL-013",
"I could not resolve the apply target \{application.target}.",
application.source,
shape=application.target,
)
continue
}
let direct = external
.get(name)
.unwrap_or_else(() => {
copy(
object_field(
object(locals.get(name).unwrap_or(Json::null())),
"traits",
),
)
})
self.merge_traits(
direct,
application.traits,
application.source,
application.target,
)
external[name] = direct
let ts = copy(object_field(member_, "traits"))
override(ts, direct)
let member_ = copy(member_)
member_["traits"] = Json::object(ts)
members[name] = Json::object(member_)
}
override(traits, object_field(raw.fields, "traits"))
for key, value in raw.fields {
if ["type", "traits", "members", "member", "key", "value"].contains(key) {
continue
}
match (fields.get(key), value) {
(Some(Array(a)), Array(b)) if key != "mixins" =>
fields[key] = Json::array(unique(a, b))
(Some(Object(a)), Object(b)) => {
let merged = copy(a)
override(merged, b)
fields[key] = Json::object(merged)
}
_ => fields[key] = value
}
}
if !traits.is_empty() {
fields["traits"] = Json::object(traits)
}
if kind == "enum" {
for name, value in members {
let m = copy(object(value))
let ts = copy(object_field(m, "traits"))
if !ts.contains("smithy.api#enumValue") {
ts["smithy.api#enumValue"] = Json::string(name)
}
m["traits"] = Json::object(ts)
members[name] = Json::object(m)
}
}
set_members(fields, members)
{ ..raw, fields, }
}
///|
fn Assembly::flatten(self : Assembly) -> Map[String, RawShape] {
let finished : Map[String, RawShape] = Map([])
let active : Set[String] = Set([])
for root, _ in self.raw {
let stack = [(root, false)]
while stack.pop() is Some((id, leaving)) {
if finished.contains(id) {
continue
}
guard self.raw.get(id) is Some(raw) else { continue }
if leaving {
finished[id] = self.flatten_one(raw, finished)
active.remove(id)
continue
}
if active.contains(id) {
self.problem(
"SM-MODEL-009",
"I found a cycle while resolving mixins or resource bindings at \{id}.",
raw.source,
shape=id,
)
continue
}
active.add(id)
stack.push((id, true))
let dependencies : Array[String] = []
for mixin in array_field(raw.fields, "mixins") {
if string_field(object(mixin), "target") is Some(target) {
dependencies.push(target)
}
}
if self.bindings.get(id) is Some(resource) {
dependencies.push(resource)
}
for dependency in dependencies.rev() {
if !self.raw.contains(dependency) {
self.problem(
"SM-MODEL-004",
"I could not resolve dependency \{dependency} of \{id}.",
raw.source,
shape=id,
)
} else if !finished.contains(dependency) {
stack.push((dependency, false))
}
}
}
}
finished
}