///|
fn export_traits(traits : Map[ShapeId, NodeValue]) -> Map[String, Json] {
let out : Map[String, Json] = Map([])
for id, value in traits {
out[id.to_string()] = @json.to_json(value)
}
out
}
///|
fn export_references(ids : Array[ShapeId]) -> Json {
let ids = ids.copy()
ids.sort_by((a, b) => a.to_string().compare(b.to_string()))
Json::array(ids.map(id => reference(id.to_string())))
}
///|
fn export_reference_map(ids : Map[String, ShapeId]) -> Json {
let out : Map[String, Json] = Map([])
for name, id in ids {
out[name] = reference(id.to_string())
}
Json::object(out)
}
///|
fn Model::export_member(
self : Model,
id : ShapeId,
introduced~ : Bool = false,
) -> Json raise BuildError {
guard self.shapes.get(id) is Some(shape) && shape.body is Member(target~) else {
raise InvalidShape(
"I expected a member shape at \{id} while serializing the model.",
)
}
let fields : Map[String, Json] = {
"target": Json::string(target.to_string()),
}
let traits = if introduced { shape.introduced_traits() } else { shape.traits }
if !traits.is_empty() {
fields["traits"] = Json::object(export_traits(traits))
}
Json::object(fields)
}
///|
fn export_introduced_body(shape : Shape, fields : Map[String, Json]) -> Unit {
guard shape.provenance is Some(origin) && !shape.mixins.is_empty() else {
return
}
let introduced = origin.introduced_fields
let inherited = origin.inherited_fields
let keys = match shape.body {
Service(..) => ["operations", "resources", "errors"]
Operation(..) => ["errors"]
Resource(..) => ["operations", "resources"]
_ => []
}
for key in keys {
guard fields.get(key) is Some(Array(current)) else { continue }
let inherited = match inherited.get(key) {
Some(Array(values)) => values.map(@json.to_json)
_ => []
}
let introduced = match introduced.get(key) {
Some(Array(values)) => values.map(@json.to_json)
_ => []
}
let kept = current.filter(value => {
introduced.contains(value) || !inherited.contains(value)
})
if kept.is_empty() {
fields.remove(key)
} else {
fields[key] = Json::array(kept)
}
}
if shape.body is Service(version~, rename~, ..) {
if version.trim().is_empty() ||
(
!introduced.contains("version") &&
inherited.get("version") == Some(String(version))
) {
fields.remove("version")
}
let old = match inherited.get("rename") {
Some(Object(fields)) => fields
_ => Map([])
}
let local = match introduced.get("rename") {
Some(Object(fields)) => fields
_ => Map([])
}
let kept : Map[String, Json] = Map([])
for id, name in rename {
let key = id.to_string()
if local.contains(key) || old.get(key) != Some(String(name)) {
kept[key] = Json::string(name)
}
}
if kept.is_empty() {
fields.remove("rename")
} else {
fields["rename"] = Json::object(kept)
}
}
}
///|
/// Serialize the assembled graph to a flattened JSON AST. Mixins remain
/// available as shape provenance in memory; their inherited contents are
/// explicit in this representation. Built-in prelude and contract definitions
/// are omitted unless include_prelude is true.
pub fn Model::to_json_ast(
self : Model,
include_prelude~ : Bool = false,
preserve_mixins~ : Bool = false,
) -> @syntax.JsonAst raise {
let shapes : Map[String, Json] = Map([])
for id, shape in self.shapes {
if id.member is Some(_) ||
(!include_prelude && is_builtin_source(shape.source.file)) {
continue
}
let fields : Map[String, Json] = {
"type": Json::string(shape.body.keyword()),
}
match shape.body {
Simple(_) => ()
Member(..) => continue
List(member_) => fields["member"] = self.export_member(member_)
Map(key~, value~) => {
fields["key"] = self.export_member(key)
fields["value"] = self.export_member(value)
}
Structure(members) | Union(members) | Enum(members) | IntEnum(members) => {
let ms : Map[String, Json] = Map([])
for member_ in members {
if member_.member is Some(name) {
ms[name] = self.export_member(member_)
}
}
fields["members"] = Json::object(ms)
}
Operation(input~, output~, errors~) => {
fields["input"] = reference(input.to_string())
fields["output"] = reference(output.to_string())
if !errors.is_empty() {
fields["errors"] = export_references(errors)
}
}
Service(version~, operations~, resources~, errors~, rename~) => {
fields["version"] = Json::string(version)
if !operations.is_empty() {
fields["operations"] = export_references(operations)
}
if !resources.is_empty() {
fields["resources"] = export_references(resources)
}
if !errors.is_empty() {
fields["errors"] = export_references(errors)
}
if !rename.is_empty() {
let names : Map[String, Json] = Map([])
for id, name in rename {
names[id.to_string()] = Json::string(name)
}
fields["rename"] = Json::object(names)
}
}
Resource(
identifiers~,
properties~,
create~,
put~,
read~,
update~,
delete~,
list~,
operations~,
collectionOperations~,
resources~
) => {
if !identifiers.is_empty() {
fields["identifiers"] = export_reference_map(identifiers)
}
if !properties.is_empty() {
fields["properties"] = export_reference_map(properties)
}
for
(key, value) in [
("create", create),
("put", put),
("read", read),
("update", update),
("delete", delete),
("list", list),
] {
if value is Some(id) {
fields[key] = reference(id.to_string())
}
}
if !operations.is_empty() {
fields["operations"] = export_references(operations)
}
if !collectionOperations.is_empty() {
fields["collectionOperations"] = export_references(
collectionOperations,
)
}
if !resources.is_empty() {
fields["resources"] = export_references(resources)
}
}
}
if preserve_mixins {
if !shape.mixins.is_empty() {
fields["mixins"] = Json::array(
shape.mixins.map(id => reference(id.to_string())),
)
}
export_introduced_body(shape, fields)
let members = match shape.body {
List(member_) => [member_]
Map(key~, value~) => [key, value]
Structure(members)
| Union(members)
| Enum(members)
| IntEnum(members) => members
_ => []
}
let named = match fields.get("members") {
Some(Object(members)) => members
_ => Map([])
}
for member_id in members {
guard self.shapes.get(member_id) is Some(member_) else { continue }
let name = member_id.member.unwrap_or("")
if member_.mixins.is_empty() {
let exported = self.export_member(member_id, introduced=true)
if shape.body is (List(_) | Map(..)) {
fields[name] = exported
} else {
named[name] = exported
}
} else {
if shape.body is (List(_) | Map(..)) {
fields.remove(name)
} else {
named.remove(name)
}
let traits = member_.introduced_traits()
if !traits.is_empty() {
shapes[member_id.to_string()] = Json::object({
"type": "apply",
"traits": Json::object(export_traits(traits)),
})
}
}
}
if shape.body is (Structure(_) | Union(_) | Enum(_) | IntEnum(_)) {
fields["members"] = Json::object(named)
}
}
let traits = if preserve_mixins {
shape.introduced_traits()
} else {
shape.traits
}
if !traits.is_empty() {
fields["traits"] = Json::object(export_traits(traits))
}
shapes[id.to_string()] = Json::object(fields)
}
let fields : Map[String, Json] = {
"smithy": "2.0",
"shapes": Json::object(shapes),
}
if !self.metadata.is_empty() {
let metadata : Map[String, Json] = Map([])
for key, value in self.metadata {
metadata[key] = @json.to_json(value)
}
fields["metadata"] = Json::object(metadata)
}
@syntax.decode_json_ast(Json::object(fields))
}