///|
fn idl_keyword(shape : @syntax.Shape) -> String {
match shape {
Simple(kind~, ..) => kind.keyword()
Enum(kind=Enum, ..) => "enum"
Enum(kind=IntEnum, ..) => "intEnum"
Aggregate(kind~, ..) =>
match kind {
List => "list"
Set => "set"
Map => "map"
Structure => "structure"
Union => "union"
}
Entity(kind=Service, ..) => "service"
Entity(kind=Resource, ..) => "resource"
Operation(..) => "operation"
}
}
///|
fn suffix(file : @syntax.IdlFile, input : Bool) -> String {
file
.control_value(
if input {
"operationInputSuffix"
} else {
"operationOutputSuffix"
},
)
.bind(v => v.as_string())
.unwrap_or(if input { "Input" } else { "Output" })
}
///|
fn Assembly::collect_idl_names(
self : Assembly,
name : String,
file : @syntax.IdlFile,
) -> Unit {
let ns = file.namespace.map(n => n.namespace.value).unwrap_or("")
for statement in file.shapes() {
let id = ns + "#" + statement.shape.name().value
self.known.add(id)
self.kinds[id] = idl_keyword(statement.shape)
if name == "" &&
!statement.traits.iter().any(t => t.id.value.name == "private") {
self.public_prelude.add(id)
}
if statement.shape is Operation(properties~, ..) {
for property in properties {
match property {
Input(Inline(_), _) => {
let inline_id = id + suffix(file, true)
self.known.add(inline_id)
self.kinds[inline_id] = "structure"
}
Output(Inline(_), _) => {
let inline_id = id + suffix(file, false)
self.known.add(inline_id)
self.kinds[inline_id] = "structure"
}
_ => ()
}
}
}
}
}
///|
fn Assembly::resolve(
self : Assembly,
id : @syntax.ShapeId,
ns : String,
uses : Map[String, String],
) -> String {
let ns = if ns.is_empty() { "smithy.api" } else { ns }
let root = match id.namespace {
Some(namespace) => namespace + "#" + id.name
None =>
match uses.get(id.name) {
Some(imported) => imported
None => {
let local = ns + "#" + id.name
let builtin = "smithy.api#" + id.name
if self.known.contains(local) {
local
} else if self.public_prelude.contains(builtin) {
builtin
} else {
local
}
}
}
}
match id.member {
Some(member_) => root + "$" + member_
None => root
}
}
///|
fn Assembly::node_json(
self : Assembly,
node : @syntax.NodeValue,
ns : String,
uses : Map[String, String],
source : SourceLocation,
) -> Json {
match node {
Array(values, _) =>
Json::array(values.map(v => self.node_json(v, ns, uses, source)))
Object(kvps, _) => {
let fields : Map[String, Json] = Map([])
for kvp in kvps {
if fields.contains(kvp.key.value) {
self.problem(
"SM-MODEL-010",
"I found the duplicate object property \{kvp.key.value}.",
{ ..source, span: Some(kvp.span), },
)
}
fields[kvp.key.value] = self.node_json(kvp.value, ns, uses, source)
}
Json::object(fields)
}
String(s, _) | TextBlock(s, _) => Json::string(s)
ShapeId(id, span) => {
let target = self.resolve(id, ns, uses)
self.syntactic.push((target, { ..source, span: Some(span), }))
Json::string(target)
}
Number(number, _) => Json::number(number.as_double(), repr=number.text)
Keyword(True, _) => Json::boolean(true)
Keyword(False, _) => Json::boolean(false)
Keyword(Null, _) => Json::null()
}
}
///|
fn docs_text(docs : ArrayView[@syntax.DocComment]) -> String {
let lines = docs.map(d => d.text)
let mut indent = 2147483647
for line in lines {
if !line.trim().is_empty() {
let trimmed = line.trim_start(chars=" \t")
let count = line.length() - trimmed.length()
if count < indent {
indent = count
}
}
}
lines
.map(line => {
if indent < line.length() {
line.unsafe_substring(start=indent, end=line.length())
} else {
""
}
})
.join("\n")
}
///|
fn Assembly::idl_traits(
self : Assembly,
traits : ArrayView[@syntax.Trait],
docs : ArrayView[@syntax.DocComment],
ns : String,
uses : Map[String, String],
source : SourceLocation,
target : String,
) -> Map[String, Json] {
let out : Map[String, Json] = Map([])
if !docs.is_empty() {
out["smithy.api#documentation"] = Json::string(docs_text(docs))
}
for trait_ in traits {
let id = self.resolve(trait_.id.value, ns, uses)
let value = match trait_.body {
Some(Node(node)) => self.node_json(node, ns, uses, source)
Some(Structure(kvps)) if kvps.is_empty() => self.omitted_trait_value(id)
Some(Structure(kvps)) =>
self.node_json(
@syntax.NodeValue::Object(kvps, trait_.span),
ns,
uses,
source,
)
None => self.omitted_trait_value(id)
}
let incoming : Map[String, Json] = Map([])
incoming[id] = value
self.merge_traits(
out,
incoming,
{ ..source, span: Some(trait_.span), },
target,
)
}
out
}
///|
fn Assembly::omitted_trait_value(self : Assembly, id : String) -> Json {
match self.kinds.get(id) {
Some("list" | "set") => Json::array([])
Some("structure" | "map") => Json::empty_object()
_ => Json::null()
}
}
///|
fn Assembly::idl_members(
self : Assembly,
members : ArrayView[@syntax.Member],
parent : String,
ns : String,
uses : Map[String, String],
source : SourceLocation,
) -> Map[String, Json] {
let out : Map[String, Json] = Map([])
for member_ in members {
let id = parent + "$" + member_.name.value
let source = { ..source, span: Some(member_.span), }
self.locations[id] = source
let fields : Map[String, Json] = Map([])
if member_.target is Explicit(target) {
fields["target"] = Json::string(self.resolve(target.value, ns, uses))
}
let traits = self.idl_traits(
member_.traits,
member_.docs,
ns,
uses,
source,
id,
)
if member_.default is Some(value) {
let incoming : Map[String, Json] = {
"smithy.api#default": self.node_json(value, ns, uses, source),
}
self.merge_traits(traits, incoming, source, id)
}
if !traits.is_empty() {
fields["traits"] = Json::object(traits)
}
out[member_.name.value] = Json::object(fields)
}
out
}
///|
fn Assembly::idl_mixins(
self : Assembly,
fields : Map[String, Json],
mixins : @syntax.Mixins?,
ns : String,
uses : Map[String, String],
) -> Unit {
if mixins is Some(mixins) {
fields["mixins"] = Json::array(
mixins.ids.map(id => reference(self.resolve(id.value, ns, uses))),
)
}
}
///|
fn Assembly::idl_inline(
self : Assembly,
inline_ : @syntax.InlineShape,
id : String,
input : Bool,
ns : String,
uses : Map[String, String],
source : SourceLocation,
) -> Unit {
let fields : Map[String, Json] = {
"type": "structure",
"members": Json::object(
self.idl_members(inline_.members, id, ns, uses, source),
),
}
let traits = self.idl_traits(
inline_.traits,
inline_.docs,
ns,
uses,
source,
id,
)
traits[if input { "smithy.api#input" } else { "smithy.api#output" }] = Json::empty_object()
fields["traits"] = Json::object(traits)
self.idl_mixins(fields, inline_.mixins, ns, uses)
if inline_.for_resource is Some(resource) {
self.bindings[id] = self.resolve(resource.value, ns, uses)
}
self.add_shape({
id,
fields,
source: { ..source, span: Some(inline_.span), },
})
}
///|
fn Assembly::add_idl(
self : Assembly,
name : String,
file : @syntax.IdlFile,
) -> Unit {
let ns = file.namespace.map(n => n.namespace.value).unwrap_or("")
let source = location(name, span=file.span)
let uses : Map[String, String] = Map([])
let v2 = file
.control_value("version")
.bind(v => v.as_string())
.unwrap_or("1")
.has_prefix("2")
for use_ in file.uses {
let id = use_.id.value
if id.namespace is None || id.member is Some(_) {
self.problem(
"SM-MODEL-002",
"I expected an absolute root ID in a use statement, but found \{id}.",
{ ..source, span: Some(use_.span), },
)
continue
}
let imported = id.to_string()
if uses.contains(id.name) ||
(
self.known.contains(ns + "#" + id.name) &&
ns + "#" + id.name != imported
) {
self.problem(
"SM-MODEL-002",
"I found the conflicting import name \{id.name}.",
{ ..source, span: Some(use_.span), },
)
}
uses[id.name] = imported
}
for metadata in file.metadata {
self.metadata_value(
metadata.key.value,
self.node_json(metadata.value, ns, uses, source),
{ ..source, span: Some(metadata.span), },
)
}
for statement in file.statements {
match statement {
Apply(apply_) => {
let target = self.resolve(apply_.target.value, ns, uses)
if !v2 &&
apply_.traits
.iter()
.any(t => self.resolve(t.id.value, ns, uses) == "smithy.api#mixin") {
self.problem(
"SM-MODEL-008",
"I found a mixin trait application in a Smithy 1.0 document.",
{ ..source, span: Some(apply_.span), },
shape=target,
)
}
self.applications.push({
target,
traits: self.idl_traits(apply_.traits, [], ns, uses, source, target),
source: { ..source, span: Some(apply_.span), },
})
}
Shape(statement) => {
let shape = statement.shape
let id = ns + "#" + shape.name().value
let source = { ..source, span: Some(statement.span), }
let fields : Map[String, Json] = {
"type": Json::string(idl_keyword(shape)),
}
let traits = self.idl_traits(
statement.traits,
statement.docs,
ns,
uses,
source,
id,
)
if !v2 && traits.contains("smithy.api#mixin") {
self.problem(
"SM-MODEL-008",
"I found a mixin trait on \{id} in a Smithy 1.0 document.",
source,
shape=id,
)
}
if !traits.is_empty() {
fields["traits"] = Json::object(traits)
}
match shape {
Simple(mixins~, ..) => self.idl_mixins(fields, mixins, ns, uses)
Enum(mixins~, members~, ..) => {
self.idl_mixins(fields, mixins, ns, uses)
let ms : Map[String, Json] = Map([])
for member_ in members {
let mid = id + "$" + member_.name.value
self.locations[mid] = { ..source, span: Some(member_.span), }
let ts = self.idl_traits(
member_.traits,
member_.docs,
ns,
uses,
source,
mid,
)
if member_.value is Some(value) {
self.merge_traits(
ts,
{
"smithy.api#enumValue": self.node_json(
value, ns, uses, source,
),
},
source,
mid,
)
}
let m : Map[String, Json] = { "target": "smithy.api#Unit" }
if !ts.is_empty() {
m["traits"] = Json::object(ts)
}
ms[member_.name.value] = Json::object(m)
}
fields["members"] = Json::object(ms)
}
Aggregate(kind~, for_resource~, mixins~, members~, ..) => {
self.idl_mixins(fields, mixins, ns, uses)
if for_resource is Some(resource) {
self.bindings[id] = self.resolve(resource.value, ns, uses)
}
let ms = self.idl_members(members, id, ns, uses, source)
match kind {
List | Set | Map =>
for key, member_ in ms {
fields[key] = member_
}
_ => fields["members"] = Json::object(ms)
}
}
Entity(kind~, mixins~, body~, ..) => {
self.idl_mixins(fields, mixins, ns, uses)
let body = object(self.node_json(body, ns, uses, source))
for key, value in body {
let value = if [
"operations", "resources", "errors", "collectionOperations",
].contains(key) {
match value {
Array(ids) =>
Json::array(
ids.map(v => {
match v {
String(id) => reference(id)
_ => v
}
}),
)
_ => value
}
} else if kind == Resource &&
["create", "put", "read", "update", "delete", "list"].contains(
key,
) {
match value {
String(id) => reference(id)
_ => value
}
} else if kind == Resource &&
["identifiers", "properties"].contains(key) {
let out : Map[String, Json] = Map([])
match value {
Object(values) => {
for name, value in values {
out[name] = match value {
String(id) => reference(id)
_ => value
}
}
Json::object(out)
}
_ => value
}
} else {
value
}
fields[key] = value
}
}
Operation(mixins~, properties~, ..) => {
self.idl_mixins(fields, mixins, ns, uses)
for property in properties {
match property {
Input(target, _) | Output(target, _) => {
let input = property is Input(_, _)
let key = if input { "input" } else { "output" }
let target = match target {
Reference(target) => self.resolve(target.value, ns, uses)
Inline(inline_) => {
let target = id + suffix(file, input)
self.idl_inline(inline_, target, input, ns, uses, source)
target
}
}
fields[key] = reference(target)
}
Errors(errors, _) =>
fields["errors"] = Json::array(
errors.map(e => reference(self.resolve(e.value, ns, uses))),
)
}
}
}
}
if idl_keyword(shape) == "set" &&
file
.control_value("version")
.bind(v => v.as_string())
.unwrap_or("1")
.has_prefix("2") {
self.problem(
"SM-MODEL-010",
"I found a set shape \{id} in a Smithy 2.0 document; I expected a list.",
source,
shape=id,
)
}
self.add_shape({ id, fields, source, })
}
}
}
}