///|
/// The first line of each generated MoonBit file.
let generated_header : String = "// Code generated by moonbit-jsonrpc-codegen. DO NOT EDIT."
///|
/// The version of moonbitlang/async that the generated module imports. The
/// runtime in `runtimes/moonbit` is tested with this version.
let async_version : String = "0.22.5"
///|
/// The state of one generator run.
priv struct Gen {
model : @smithy.Model
settings : @jsonrpc.PeerSettings
names : @codegen.ServiceNames
module_name : String
delegator : @codegen.WriterDelegator
// The prepared structure contracts of the peer's services, by shape.
values : Map[@smithy.ShapeId, @validation.StructureContract]
value_config : @validation.Config
// The shapes that I wrote, and the error shapes in the order that I wrote them.
written : Map[@smithy.ShapeId, Unit]
errors : Array[@smithy.ShapeId]
// The generated type names, to find two shapes with one name.
type_names : Map[String, @smithy.ShapeId]
// The type names that the generator itself declares.
generated_names : Array[String]
}
///|
fn module_namespace(module_name : String) -> @codegen.Namespace {
@codegen.Namespace::parse(module_name, separator="/").unwrap()
}
///|
/// I accept a module name such as `example/terminal`: two or more segments of
/// lower-case ASCII letters, digits, `_` and `-`, joined by `/`.
fn check_module_name(text : String) -> Unit raise @codegen.CodegenError {
let segments = text.split("/").map(part => part.to_owned()).collect()
let valid = segments.length() >= 2 &&
segments
.iter()
.all(segment => {
!segment.is_empty() &&
segment
.iter()
.all(c => {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-'
})
})
if !valid {
raise @codegen.InvalidSettings(
"I expected setting `module` to be a MoonBit module name such as `example/terminal`, found `\{text}`.",
)
}
}
///|
fn Gen::shape(
self : Gen,
id : @smithy.ShapeId,
) -> @smithy.Shape raise @codegen.CodegenError {
match self.model.shapes.get(id) {
Some(shape) => shape
None =>
raise @codegen.InvalidSettings(
"I could not find shape `\{id}` in the model.",
)
}
}
///|
/// The type name of a generated shape.
fn Gen::type_name_of(self : Gen, id : @smithy.ShapeId) -> String {
type_name(self.names.shape_name(id))
}
///|
/// The timestamp format of a member and its target, as a runtime constructor.
fn Gen::timestamp_format(
self : Gen,
member_shape : @smithy.Shape?,
target : @smithy.Shape?,
) -> String {
let from_trait = (shape : @smithy.Shape?) => {
match shape {
Some(shape) =>
match shape.trait_value("smithy.api#timestampFormat") {
Some(String("date-time")) => Some("DateTime")
Some(String("http-date")) => Some("HttpDate")
Some(String("epoch-seconds")) => Some("EpochSeconds")
_ => None
}
None => None
}
}
match (from_trait(member_shape), from_trait(target)) {
(Some(format), _) | (None, Some(format)) => format
(None, None) =>
match self.value_config.timestamp_format {
DateTime => "DateTime"
EpochSeconds => "EpochSeconds"
}
}
}
///|
/// The type of the value of a member or of a target shape. A list, a map and
/// a sparse wrapper become layers; the walk keeps its own stack of layers.
fn Gen::member_ty(
self : Gen,
member_id : @smithy.ShapeId,
) -> Ty raise @codegen.CodegenError {
// Each step finds the target of a member and records a wrapper layer.
let layers : Array[Int32] = [] // 0 list, 1 map, 2 optional item
let mut member_shape = self.shape(member_id)
let leaf = for ;; {
guard member_shape.body is Member(target~) else {
raise @codegen.InvalidSettings(
"I expected `\{member_shape.id}` to be a member shape.",
)
}
if target.namespace() == "smithy.api" {
let target_shape = self.model.shapes.get(target)
let format = self.timestamp_format(Some(member_shape), target_shape)
match prelude_ty(target.name(), format) {
Some(ty) => break ty
None =>
raise @codegen.InvalidSettings(
"I cannot map prelude shape `\{target}` to a MoonBit type.",
)
}
}
let target_shape = self.shape(target)
match target_shape.body {
Simple(simple) =>
break simple_ty(
simple,
self.timestamp_format(Some(member_shape), Some(target_shape)),
)
List(item) => {
layers.push(0)
if target_shape.trait_value("smithy.api#sparse") is Some(_) {
layers.push(2)
}
member_shape = self.shape(item)
}
Map(key~, value~) => {
let key_shape = self.shape(key)
if key_shape.body is Member(target=key_target) &&
!self.is_string(key_target) {
raise @codegen.InvalidSettings(
"I expected the key of map `\{target}` to be a string.",
)
}
layers.push(1)
if target_shape.trait_value("smithy.api#sparse") is Some(_) {
layers.push(2)
}
member_shape = self.shape(value)
}
Structure(_) | Union(_) | Enum(_) | IntEnum(_) =>
break Named(self.type_name_of(target))
_ =>
raise @codegen.InvalidSettings(
"I cannot use shape `\{target}` (\{target_shape.body.keyword()}) as a value in MoonBit.",
)
}
}
let mut ty = leaf
for index = layers.length() - 1; index >= 0; index = index - 1 {
ty = match layers[index] {
0 => ListOf(ty)
1 => MapOf(ty)
_ => Opt(ty)
}
}
ty
}
///|
fn Gen::is_string(self : Gen, id : @smithy.ShapeId) -> Bool {
if id == @smithy.ShapeId::parse("smithy.api#String").unwrap() {
return true
}
match self.model.shapes.get(id) {
Some({ body: Simple(String) | Enum(_), .. }) => true
_ => false
}
}
///|
/// A field of a generated structure.
priv struct Field {
field_name : String
wire_name : String
ty : Ty
required : Bool
}
///|
/// The fields of a structure, in model order, with the wire names and the
/// required flags of the prepared contract.
fn Gen::fields(
self : Gen,
shape : @smithy.Shape,
) -> Array[Field] raise @codegen.CodegenError {
guard shape.body is Structure(members) else { return [] }
let contract = self.values.get(shape.id)
let out : Array[Field] = []
let seen : Map[String, String] = Map([])
for member_id in members {
let member_name = member_id.member_name().unwrap_or("")
let member_contract = contract.bind(c => {
c.members.iter().find_first(m => m.shape == member_id)
})
let wire_name = match member_contract {
Some(m) => m.wire_name
None => member_name
}
let required = match member_contract {
Some(m) => m.required
None =>
self.shape(member_id).trait_value("smithy.api#required") is Some(_)
}
let field_name = snake_name(member_name)
if seen.get(field_name) is Some(other) {
raise @codegen.InvalidSettings(
"I found members `\{other}` and `\{member_name}` of `\{shape.id}` with the same MoonBit field name `\{field_name}`.",
)
}
seen[field_name] = member_name
let ty = self.member_ty(member_id)
out.push({ field_name, wire_name, ty, required, })
}
out
}
///|
fn Gen::reject_unknown(self : Gen, shape : @smithy.Shape) -> Bool {
match self.values.get(shape.id) {
Some(contract) => contract.unknown_members is Reject
None => false
}
}
///|
fn Gen::claim_type_name(
self : Gen,
shape : @smithy.Shape,
) -> String raise @codegen.CodegenError {
let name = self.type_name_of(shape.id)
if self.generated_names.contains(name) {
raise @codegen.InvalidSettings(
"I cannot use the MoonBit name `\{name}` for shape `\{shape.id}`: the generated code uses it. Add a rename to the service.",
)
}
match self.type_names.get(name) {
Some(other) if other != shape.id =>
raise @codegen.InvalidSettings(
"I found shapes `\{other}` and `\{shape.id}` with the same MoonBit type name `\{name}`.",
)
_ => self.type_names[name] = shape.id
}
name
}
///|
fn types_path() -> @codegen.OutputPath {
@codegen.OutputPath::parse("types.mbt").unwrap()
}
///|
fn errors_path() -> @codegen.OutputPath {
@codegen.OutputPath::parse("errors.mbt").unwrap()
}
///|
fn Gen::with_file(
self : Gen,
path : @codegen.OutputPath,
body : (@codegen.CodeWriter) -> Unit raise @codegen.CodegenError,
) -> Unit raise @codegen.CodegenError {
self.delegator.use_file_writer(path, module_namespace(self.module_name), body)
}
///|
/// Write the declaration, `to_json` and `from_json` of a structure.
fn Gen::write_structure(
self : Gen,
shape : @smithy.Shape,
path : @codegen.OutputPath,
) -> Unit raise @codegen.CodegenError {
let name = self.claim_type_name(shape)
let fields = self.fields(shape)
let reject = self.reject_unknown(shape)
self.with_file(path, w => {
w.write("///|")
if fields.is_empty() {
w.write("pub(all) struct $L {} derive(Eq, Debug)", args=[Text(name)])
} else {
w.write("pub(all) struct $L {", args=[Text(name)])
w.indent()
for field in fields {
let text = if field.required {
field.ty.text()
} else {
Opt(field.ty).text()
}
w.write("$L : $L", args=[Text(field.field_name), Text(text)])
}
w.dedent()
w.write("} derive(Eq, Debug)")
}
w.blank_line()
// to_json
w.write("///|")
let self_name = if fields.is_empty() { "_self" } else { "self" }
w.write("pub fn $L::to_json($L : $L) -> Json {", args=[
Text(name),
Text(self_name),
Text(name),
])
w.indent()
if fields.is_empty() {
w.write("Json::empty_object()")
} else {
w.write("let fields : Map[String, Json] = Map([])")
for field in fields {
let key = mbt_string(field.wire_name)
if field.required {
w.write("fields[$L] = $L", args=[
Text(key),
Text(field.ty.encode("self." + field.field_name)),
])
} else {
w.write("if self.$L is Some(value) {", args=[Text(field.field_name)])
w.indent()
w.write("fields[$L] = $L", args=[
Text(key),
Text(field.ty.encode("value")),
])
w.dedent()
w.write("}")
}
}
w.write("Json::object(fields)")
}
w.dedent()
w.write("}")
w.blank_line()
// from_json
w.write("///|")
w.write(
"pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
args=[Text(name), Text(name)],
)
w.indent()
if fields.is_empty() && !reject {
w.write("let _ = @rpc.object_of(json, path~)")
w.write("$L::{ }", args=[Text(name)])
} else {
w.write("let fields = @rpc.object_of(json, path~)")
if reject {
let known = fields.map(field => mbt_string(field.wire_name)).join(", ")
w.write("@rpc.reject_unknown(fields, [$L], path~)", args=[Text(known)])
}
if fields.is_empty() {
w.write("$L::{ }", args=[Text(name)])
} else {
w.write("$L::{", args=[Text(name)])
w.indent()
for field in fields {
let key = mbt_string(field.wire_name)
let member_path = "path + " +
mbt_string("/" + pointer_token(field.wire_name))
if field.required {
w.write("$L: $L,", args=[
Text(field.field_name),
Text(
field.ty.decode(
"@rpc.required(fields, \{key}, path~)",
member_path,
),
),
])
} else {
w.write("$L: match fields.get($L) {", args=[
Text(field.field_name),
Text(key),
])
w.indent()
w.write("None | Some(Null) => None")
w.write("Some(value) => Some($L)", args=[
Text(field.ty.decode("value", member_path)),
])
w.dedent()
w.write("},")
}
}
w.dedent()
w.write("}")
}
}
w.dedent()
w.write("}")
w.blank_line()
})
}
///|
/// The wire name of a union member: its member name, or its `jsonName` when
/// the value configuration uses JSON names.
fn Gen::union_tag(
self : Gen,
member_id : @smithy.ShapeId,
) -> String raise @codegen.CodegenError {
let member_name = member_id.member_name().unwrap_or("")
if self.value_config.member_names is JsonName &&
self.shape(member_id).trait_value("smithy.api#jsonName")
is Some(String(name)) {
return name
}
member_name
}
///|
fn Gen::write_union(
self : Gen,
shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
guard shape.body is Union(members) else { return }
let name = self.claim_type_name(shape)
let variants : Array[(String, String, Ty)] = []
let seen : Map[String, Unit] = Map([])
for member_id in members {
let variant = variant_name(member_id.member_name().unwrap_or(""))
if seen.contains(variant) {
raise @codegen.InvalidSettings(
"I found two members of union `\{shape.id}` with the variant name `\{variant}`.",
)
}
seen[variant] = ()
variants.push(
(variant, self.union_tag(member_id), self.member_ty(member_id)),
)
}
let tags = variants.map(v => v.1)
tags.sort_by((a, b) => a.lexical_compare(b))
self.with_file(types_path(), w => {
w.write("///|")
w.write("pub(all) enum $L {", args=[Text(name)])
w.indent()
for variant in variants {
if variant.2 is UnitT {
w.write("$L", args=[Text(variant.0)])
} else {
w.write("$L($L)", args=[Text(variant.0), Text(variant.2.text())])
}
}
w.dedent()
w.write("} derive(Eq, Debug)")
w.blank_line()
w.write("///|")
w.write("pub fn $L::to_json(self : $L) -> Json {", args=[
Text(name),
Text(name),
])
w.indent()
w.write("match self {")
w.indent()
for variant in variants {
let tag = mbt_string(variant.1)
if variant.2 is UnitT {
w.write("$L => Json::object({ $L: Json::empty_object() })", args=[
Text(variant.0),
Text(tag),
])
} else {
w.write("$L(value) => Json::object({ $L: $L })", args=[
Text(variant.0),
Text(tag),
Text(variant.2.encode("value")),
])
}
}
w.dedent()
w.write("}")
w.dedent()
w.write("}")
w.blank_line()
w.write("///|")
w.write(
"pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
args=[Text(name), Text(name)],
)
w.indent()
let uses_value = variants.iter().any(v => !(v.2 is UnitT))
let value_name = if uses_value { "value" } else { "_" }
w.write("let (tag, $L) = @rpc.union_member(json, path~)", args=[
Text(value_name),
])
w.write("match tag {")
w.indent()
for variant in variants {
let tag = mbt_string(variant.1)
if variant.2 is UnitT {
w.write("$L => $L::$L", args=[Text(tag), Text(name), Text(variant.0)])
} else {
let member_path = "path + " + mbt_string("/" + pointer_token(variant.1))
w.write("$L => $L::$L($L)", args=[
Text(tag),
Text(name),
Text(variant.0),
Text(variant.2.decode("value", member_path)),
])
}
}
let message = "I expected a member of union `\{shape.id.name()}` (\{tags.join(", ")}), found `"
w.write("_ =>", args=[])
w.indent()
w.write("raise @rpc.DecodeError(path~, message=$L + tag + $L)", args=[
Text(mbt_string(message)),
Text(mbt_string("`.")),
])
w.dedent()
w.dedent()
w.write("}")
w.dedent()
w.write("}")
w.blank_line()
})
}
///|
fn Gen::write_enum(
self : Gen,
shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
let (members, int_enum) = match shape.body {
Enum(members) => (members, false)
IntEnum(members) => (members, true)
_ => return
}
let name = self.claim_type_name(shape)
let variants : Array[(String, String)] = []
let seen : Map[String, Unit] = Map([])
for member_id in members {
let member_name = member_id.member_name().unwrap_or("")
let variant = variant_name(member_name)
if seen.contains(variant) {
raise @codegen.InvalidSettings(
"I found two members of enum `\{shape.id}` with the variant name `\{variant}`.",
)
}
seen[variant] = ()
let value = match
self.shape(member_id).trait_value("smithy.api#enumValue") {
Some(String(text)) => mbt_string(text)
Some(Number(number)) => number.to_string()
_ => mbt_string(member_name)
}
variants.push((variant, value))
}
let values = variants.map(v => v.1)
values.sort_by((a, b) => a.lexical_compare(b))
self.with_file(types_path(), w => {
w.write("///|")
w.write("pub(all) enum $L {", args=[Text(name)])
w.indent()
for variant in variants {
w.write("$L", args=[Text(variant.0)])
}
w.dedent()
w.write("} derive(Eq, Debug)")
w.blank_line()
w.write("///|")
w.write("pub fn $L::to_json(self : $L) -> Json {", args=[
Text(name),
Text(name),
])
w.indent()
let wrap = if int_enum { "Json::number" } else { "Json::string" }
w.write("$L(", args=[Text(wrap)])
w.indent()
w.write("match self {")
w.indent()
for variant in variants {
let value = if int_enum { variant.1 + ".0" } else { variant.1 }
w.write("$L => $L", args=[Text(variant.0), Text(value)])
}
w.dedent()
w.write("},")
w.dedent()
w.write(")")
w.dedent()
w.write("}")
w.blank_line()
w.write("///|")
w.write(
"pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
args=[Text(name), Text(name)],
)
w.indent()
let decode = if int_enum { "@rpc.decode_int" } else { "@rpc.decode_string" }
w.write("let value = $L(json, path~)", args=[Text(decode)])
w.write("match value {")
w.indent()
for variant in variants {
w.write("$L => $L::$L", args=[
Text(variant.1),
Text(name),
Text(variant.0),
])
}
let message = "I expected a value of enum `\{shape.id.name()}` (\{values.join(", ")}), found `"
let shown = if int_enum { "value.to_string()" } else { "value" }
w.write("_ =>")
w.indent()
w.write("raise @rpc.DecodeError(path~, message=$L + $L + $L)", args=[
Text(mbt_string(message)),
Text(shown),
Text(mbt_string("`.")),
])
w.dedent()
w.dedent()
w.write("}")
w.dedent()
w.write("}")
w.blank_line()
})
}
///|
fn Gen::write_shape(
self : Gen,
shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
if self.written.contains(shape.id) || shape.id.namespace() == "smithy.api" {
return
}
match shape.body {
Structure(_) =>
if shape.trait_value("smithy.api#error") is Some(_) {
self.written[shape.id] = ()
self.errors.push(shape.id)
self.write_structure(shape, errors_path())
} else {
self.written[shape.id] = ()
self.write_structure(shape, types_path())
}
Union(_) => {
self.written[shape.id] = ()
self.write_union(shape)
}
Enum(_) | IntEnum(_) => {
self.written[shape.id] = ()
self.write_enum(shape)
}
_ => ()
}
}