///|
/// The name of the error type that holds the modeled errors of the peer.
let service_error_name : String = "ServiceError"
///|
fn Gen::int_code(
self : Gen,
code : @rpc.ErrorCode,
owner : @smithy.ShapeId,
) -> String raise @codegen.CodegenError {
ignore(self)
let text = code.to_string()
if !fits_int(text) {
raise @codegen.InvalidSettings(
"I expected the error code \{text} of `\{owner}` to fit in a MoonBit Int.",
)
}
text
}
///|
/// Whether integer text fits in a 32-bit signed integer.
fn fits_int(text : String) -> Bool {
let digits = if text.has_prefix("-") {
text.length() - 1
} else {
text.length()
}
if digits < 1 || digits > 10 {
return false
}
let mut value = 0L
for c in text {
if c == '-' {
continue
}
if !c.is_ascii_digit() {
return false
}
value = value * 10L + (c.to_int() - '0'.to_int()).to_int64()
}
let limit = if text.has_prefix("-") { 2147483648L } else { 2147483647L }
value <= limit
}
///|
/// The error contracts of the peer's services, one for each error shape, in the
/// order of the written error shapes.
fn Gen::error_contracts(self : Gen) -> Array[@rpc.ErrorContract] {
let found : Map[@smithy.ShapeId, @rpc.ErrorContract] = Map([])
for service in [self.settings.peer.accepts, self.settings.peer.calls] {
guard service is Some(description) else { continue }
for method_ in description.methods {
for error in method_.errors {
if !found.contains(error.shape) {
found[error.shape] = error
}
}
}
}
self.errors.filter_map(id => found.get(id))
}
///|
/// How the data of an error maps to its structure, for the generated code.
priv enum DataMapping {
/// The structure is the data.
Whole
/// One member is the data.
OneMember(Field)
/// The error has no data.
NoData
}
///|
fn Gen::data_mapping(
self : Gen,
error : @rpc.ErrorContract,
) -> DataMapping raise @codegen.CodegenError {
let shape = self.shape(error.shape)
let fields = self.fields(shape)
match error.data {
None => {
if fields.iter().any(field => field.required) {
raise @codegen.InvalidSettings(
"I cannot decode error `\{error.shape}`: it has no data, but it has required members.",
)
}
NoData
}
Some(target) if target == error.shape => Whole
Some(target) => {
let member_name = target.member_name().unwrap_or("")
let field_name = snake_name(member_name)
guard fields.iter().find_first(field => field.field_name == field_name)
is Some(field) else {
raise @codegen.InvalidSettings(
"I could not find data member `\{target}` of error `\{error.shape}`.",
)
}
if fields
.iter()
.any(other => other.required && other.field_name != field_name) {
raise @codegen.InvalidSettings(
"I cannot decode error `\{error.shape}`: its data is member `\{member_name}`, but other members are required.",
)
}
OneMember(field)
}
}
}
///|
/// The struct literal of an error. Each optional field is `None`, except the
/// data member, which gets its decoded value.
fn Gen::error_literal(
self : Gen,
shape : @smithy.ShapeId,
data : (String, String)?,
) -> String raise @codegen.CodegenError {
let name = self.type_name_of(shape)
let fields = self.fields(self.shape(shape))
if fields.is_empty() {
return "\{name}::{ }"
}
let parts = fields.map(field => {
match data {
Some((data_field, value)) if data_field == field.field_name =>
"\{field.field_name}: \{value}"
_ => "\{field.field_name}: None"
}
})
"\{name}::{ \{parts.join(", ")} }"
}
///|
/// Write the error type, the conversion to `@rpc.RpcError` and the decoders.
fn Gen::write_errors(self : Gen) -> Unit raise @codegen.CodegenError {
let contracts = self.error_contracts()
if contracts.is_empty() {
return
}
let mappings = contracts.map(contract => {
(contract, self.data_mapping(contract))
})
let accepts_errors = match self.settings.peer.accepts {
Some(description) =>
description.methods.iter().any(m => !m.errors.is_empty())
None => false
}
let called_errors : Array[@smithy.ShapeId] = []
if self.settings.peer.calls is Some(description) {
for method_ in description.methods {
for error in method_.errors {
if !called_errors.contains(error.shape) {
called_errors.push(error.shape)
}
}
}
}
let calls_errors = match self.settings.peer.calls {
Some(description) =>
description.methods.iter().any(m => !m.errors.is_empty())
None => false
}
let codes = contracts.map(contract => {
self.int_code(contract.code, contract.shape)
})
self.with_file(errors_path(), w => {
w.write("///|")
w.write(
"/// The modeled errors of the services. A handler raises one of them;",
)
w.write(
"/// a client call raises one of them for a modeled error response.",
)
w.write("pub(all) suberror $L {", args=[Text(service_error_name)])
w.indent()
for contract in contracts {
let name = self.type_name_of(contract.shape)
w.write("$L($L)", args=[Text(name), Text(name)])
}
w.dedent()
w.write("}")
w.blank_line()
if accepts_errors {
w.write("///|")
w.write(
"/// The response error of a modeled error, with its modeled code and data.",
)
w.write("fn to_rpc_error(error : Error) -> Error {")
w.indent()
w.write("match error {")
w.indent()
for index, entry in mappings {
let (contract, mapping) = entry
let name = self.type_name_of(contract.shape)
let message = mbt_string(self.names.shape_name(contract.shape))
let (binder, data) = match mapping {
Whole => ("value", "Some(value.to_json())")
OneMember(field) => {
let expr = "value." + field.field_name
let data = if field.required {
"Some(\{field.ty.encode(expr)})"
} else {
"\{expr}.map(item => \{field.ty.encode("item")})"
}
("value", data)
}
NoData => ("_", "None")
}
w.write("$L::$L($L) =>", args=[
Text(service_error_name),
Text(name),
Text(binder),
])
w.indent()
w.write("@rpc.RpcError(code=$L, message=$L, data=$L)", args=[
Text(codes[index]),
Text(message),
Text(data),
])
w.dedent()
}
w.write("other => other")
w.dedent()
w.write("}")
w.dedent()
w.write("}")
w.blank_line()
}
if calls_errors {
for entry in mappings {
let (contract, mapping) = entry
let name = self.type_name_of(contract.shape)
// An error without data needs no decoder: the client builds it inline.
// A decoder is only for an error of the service that the peer calls.
if mapping is NoData || !called_errors.contains(contract.shape) {
continue
}
w.write("///|")
w.write(
"fn $L_from_data(data : Json?) -> Error raise @rpc.DecodeError {",
args=[Text(snake_name(self.names.shape_name(contract.shape)))],
)
w.indent()
let value = match mapping {
Whole =>
"\{name}::from_json(data.unwrap_or(Json::null()), path=\"/error/data\")"
NoData => ""
OneMember(field) => {
let decoded = if field.required {
field.ty.decode("data.unwrap_or(Json::null())", "\"/error/data\"")
} else {
"match data { None | Some(Null) => None; Some(item) => Some(\{field.ty.decode("item", "\"/error/data\"")}) }"
}
self.error_literal(
contract.shape,
Some((field.field_name, decoded)),
)
}
}
w.write("$L::$L($L)", args=[
Text(service_error_name),
Text(name),
Text(value),
])
w.dedent()
w.write("}")
w.blank_line()
}
w.write("///|")
w.write(
"/// The modeled error of an error response, or the error itself.",
)
w.write("fn modeled_error(")
w.indent()
w.write("error : Error,")
w.write(
"decoders : Array[(Int, (Json?) -> Error raise @rpc.DecodeError)],",
)
w.dedent()
w.write(") -> Error {")
w.indent()
w.write(
"guard error is @rpc.RpcError(code~, data~, ..) else { return error }",
)
w.write("for decoder in decoders {")
w.indent()
w.write("if decoder.0 == code {")
w.indent()
w.write("return (decoder.1)(data) catch { _ => error }")
w.dedent()
w.write("}")
w.dedent()
w.write("}")
w.write("error")
w.dedent()
w.write("}")
w.blank_line()
}
})
}
///|
/// The handler method name of an operation.
fn Gen::method_name(self : Gen, operation : @smithy.ShapeId) -> String {
snake_name(self.names.shape_name(operation))
}
///|
/// The type of the result of a method: the output, the result member or Unit.
fn Gen::result_ty(
self : Gen,
method_ : @rpc.MethodContract,
) -> Ty raise @codegen.CodegenError {
match method_.result {
// An operation without output answers an empty object.
OutputStructure(id) if id == unit_id => UnitT
OutputStructure(id) => Named(self.type_name_of(id))
OutputMember(member_id) => self.member_ty(member_id)
NullResult => UnitT
}
}
///|
fn has_input(method_ : @rpc.MethodContract) -> Bool {
method_.input != unit_id
}
///|
/// The wire names of the positional members of a method, in order.
fn Gen::positional_names(self : Gen, method_ : @rpc.MethodContract) -> String {
let names = []
let contract = self.values.get(method_.input)
for member_id in method_.positional {
let wire = contract
.bind(c => c.members.iter().find_first(m => m.shape == member_id))
.map(m => m.wire_name)
.unwrap_or(member_id.member_name().unwrap_or(""))
names.push(mbt_string(wire))
}
"[" + names.join(", ") + "]"
}
///|
fn kind_text(kind : @rpc.MethodKind) -> String {
match kind {
RequestOnly => "@rpc.RequestOnly"
NotificationOnly => "@rpc.NotificationOnly"
RequestOrNotification => "@rpc.RequestOrNotification"
}
}
///|
/// The session rule of a method that this peer receives, when it cancels.
fn Gen::cancels(self : Gen, method_ : @rpc.MethodContract) -> Bool {
guard self.settings.description.session is Some(session) else { return false }
session.methods
.iter()
.any(rule => {
rule.receiver == self.settings.peer.name &&
rule.method_name == method_.name &&
rule.cancels is Some(_)
})
}
///|
fn Gen::service_type_name(self : Gen, service : @smithy.ShapeId) -> String {
type_name(self.names.shape_name(service))
}
///|
/// Write the handler trait and the function that registers it on a peer.
fn Gen::write_handler(self : Gen) -> Unit raise @codegen.CodegenError {
guard self.settings.peer.accepts is Some(description) else { return }
let service = self.service_type_name(description.service)
let trait_name = service + "Handler"
let methods = description.methods.filter(m => !self.cancels(m))
let with_errors = description.methods.iter().any(m => !m.errors.is_empty())
let path = @codegen.OutputPath::parse("handler.mbt").unwrap()
self.with_file(path, w => {
w.write("///|")
w.write(
"/// The handlers of service `$L`. The generated code decodes the",
args=[Text(description.service.to_string())],
)
w.write("/// parameters, calls the handler and encodes the result.")
w.write("pub(open) trait $L {", args=[Text(trait_name)])
w.indent()
let used : Map[String, String] = Map([])
for method_ in methods {
let handler_name = self.method_name(method_.operation)
if used.get(handler_name) is Some(other) {
raise @codegen.InvalidSettings(
"I found methods `\{other}` and `\{method_.name.to_string()}` with the same MoonBit handler method name `\{handler_name}`.",
)
}
used[handler_name] = method_.name.to_string()
}
for method_ in methods {
let input = if has_input(method_) {
", " + self.type_name_of(method_.input)
} else {
""
}
let result = if method_.kind is NotificationOnly {
"Unit"
} else {
self.result_ty(method_).text()
}
w.write("async $L(Self$L, @rpc.CallContext) -> $L raise", args=[
Text(self.method_name(method_.operation)),
Text(input),
Text(result),
])
}
w.dedent()
w.write("}")
w.blank_line()
w.write("///|")
w.write("/// Register the handlers of `$L` on a peer.", args=[
Text(trait_name),
])
let register = "register_" +
snake_name(self.names.shape_name(description.service))
w.write("pub fn $L(peer : @rpc.Peer, handler : &$L) -> Unit {", args=[
Text(register),
Text(trait_name),
])
w.indent()
for method_ in description.methods {
if method_.refers_to_call is Some(reference) {
let position = match reference.position {
Some(index) => "Some(\{index})"
None => "None"
}
w.write("peer.call_reference($L, id_member=$L, position=$L)", args=[
Text(mbt_string(method_.name.to_string())),
Text(mbt_string(reference.wire_name)),
Text(position),
])
}
}
for method_ in methods {
let notification = method_.kind is NotificationOnly
let register_fn = if notification {
"on_notification"
} else {
"on_request"
}
let params = if has_input(method_) { "params" } else { "_params" }
w.write("peer.$L($L, $L, async fn($L, context) {", args=[
Text(register_fn),
Text(mbt_string(method_.name.to_string())),
Text(kind_text(method_.kind)),
Text(params),
])
w.indent()
let mut args = "context"
if has_input(method_) {
let input_name = self.type_name_of(method_.input)
let params_expr = match method_.mode {
Named => "@rpc.named_params(params, path=\"/params\")"
Positional =>
"@rpc.positional_params(params, \{self.positional_names(method_)}, either=false, path=\"/params\")"
Either =>
"@rpc.positional_params(params, \{self.positional_names(method_)}, either=true, path=\"/params\")"
}
w.write("let input = $L::from_json($L, path=\"/params\")", args=[
Text(input_name),
Text(params_expr),
])
args = "input, context"
}
let call = "handler.\{self.method_name(method_.operation)}(\{args})"
let call = if with_errors && !method_.errors.is_empty() {
"\{call} catch { error => raise to_rpc_error(error) }"
} else {
call
}
if notification {
w.write("$L", args=[Text(call)])
} else {
let result = self.result_ty(method_)
if result is UnitT {
w.write("$L", args=[Text(call)])
if method_.result is NullResult {
w.write("Json::null()")
} else {
w.write("Json::empty_object()")
}
} else {
w.write("let result = $L", args=[Text(call)])
w.write("$L", args=[Text(result.encode("result"))])
}
}
w.dedent()
w.write("})")
}
w.dedent()
w.write("}")
w.blank_line()
})
}
///|
/// Write the typed client of the service that the peer calls.
fn Gen::write_client(self : Gen) -> Unit raise @codegen.CodegenError {
guard self.settings.peer.calls is Some(description) else { return }
let client = self.service_type_name(description.service) + "Client"
let path = @codegen.OutputPath::parse("client.mbt").unwrap()
self.with_file(path, w => {
w.write("///|")
w.write(
"/// A typed client of service `$L`. With `checked=true`, the peer",
args=[Text(description.service.to_string())],
)
w.write(
"/// refuses a call that the session rules do not allow, and sends nothing.",
)
w.write("pub struct $L {", args=[Text(client)])
w.indent()
w.write("peer : @rpc.Peer")
w.dedent()
w.write("}")
w.blank_line()
w.write("///|")
w.write("pub fn $L::new(peer : @rpc.Peer) -> $L {", args=[
Text(client),
Text(client),
])
w.indent()
w.write("$L::{ peer, }", args=[Text(client)])
w.dedent()
w.write("}")
w.blank_line()
// `new` is the constructor of the client; a method named so gets a suffix.
let used : Map[String, String] = Map([])
let claim = (fn_name : String, method_ : @rpc.MethodContract) => {
if used.get(fn_name) is Some(other) {
raise @codegen.InvalidSettings(
"I found methods `\{other}` and `\{method_.name.to_string()}` with the same MoonBit client method name `\{fn_name}`.",
)
}
used[fn_name] = method_.name.to_string()
}
for method_ in description.methods {
let name = match self.method_name(method_.operation) {
"new" => "new_"
other => other
}
if !(method_.kind is NotificationOnly) {
claim(name, method_)
}
if !(method_.kind is RequestOnly) {
claim(
if method_.kind is NotificationOnly {
name
} else {
name + "_notification"
},
method_,
)
}
let input_param = if has_input(method_) {
"input : \{self.type_name_of(method_.input)}, "
} else {
""
}
let params = if has_input(method_) {
match method_.mode {
Named | Either => "Some(input.to_json())"
Positional =>
"Some(@rpc.to_positional(input.to_json(), \{self.positional_names(method_)}))"
}
} else {
"None"
}
let wire = mbt_string(method_.name.to_string())
if !(method_.kind is RequestOnly) {
let fn_name = if method_.kind is NotificationOnly {
name
} else {
name + "_notification"
}
w.write("///|")
w.write(
"pub async fn $L::$L(self : $L, $Lchecked~ : Bool = true) -> Unit raise {",
args=[Text(client), Text(fn_name), Text(client), Text(input_param)],
)
w.indent()
w.write("self.peer.notify($L, $L, checked~)", args=[
Text(wire),
Text(params),
])
w.dedent()
w.write("}")
w.blank_line()
}
if method_.kind is NotificationOnly {
continue
}
let result = self.result_ty(method_)
w.write("///|")
w.write(
"pub async fn $L::$L(self : $L, $Lchecked~ : Bool = true) -> $L raise {",
args=[
Text(client),
Text(name),
Text(client),
Text(input_param),
Text(result.text()),
],
)
w.indent()
let request = "self.peer.request(\{wire}, \{params}, checked~)"
let request = if method_.errors.is_empty() {
request
} else {
let decoders = []
for error in method_.errors {
let decoder = if error.data is None {
let name = self.type_name_of(error.shape)
"_ => \{service_error_name}::\{name}(\{self.error_literal(error.shape, None)})"
} else {
"\{snake_name(self.names.shape_name(error.shape))}_from_data"
}
decoders.push("(\{error.code.to_string()}, \{decoder})")
}
let decoders = decoders.join(", ")
"\{request} catch { error => raise modeled_error(error, [\{decoders}]) }"
}
if result is UnitT {
w.write("let _ = $L", args=[Text(request)])
} else {
w.write("let result = $L", args=[Text(request)])
w.write("$L", args=[Text(result.decode("result", "\"/result\""))])
}
w.dedent()
w.write("}")
w.blank_line()
}
})
}
///|
fn option_text(value : String?) -> String {
match value {
Some(text) => "Some(\{mbt_string(text)})"
None => "None"
}
}
///|
/// Write the session rules and the constructor of a peer.
fn Gen::write_session(self : Gen) -> Unit raise @codegen.CodegenError {
let peer_name = self.settings.peer.name.to_string()
let remote = self.settings.description.peers
.iter()
.find_first(peer => peer.name != self.settings.peer.name)
.map(peer => peer.name.to_string())
.unwrap_or("")
let path = @codegen.OutputPath::parse("session.mbt").unwrap()
self.with_file(path, w => {
w.write("///|")
w.write(
"/// The session rules of the protocol, from the prepared contract.",
)
w.write("pub fn session_rules() -> @rpc.SessionRules {")
w.indent()
match self.settings.description.session {
None => w.write("@rpc.SessionRules::none()")
Some(session) => {
w.write("@rpc.SessionRules::new(")
w.indent()
let states = session.states
.map(state => mbt_string(state.to_string()))
.join(", ")
w.write("states=[$L],", args=[Text(states)])
w.write("capabilities=[")
w.indent()
for capability in session.capabilities {
w.write(
"@rpc.CapabilitySource::new(name=$L, method_name=$L, pointer=$L),",
args=[
Text(mbt_string(capability.name)),
Text(mbt_string(capability.method_name.to_string())),
Text(mbt_string(capability.pointer.to_string())),
],
)
}
w.dedent()
w.write("],")
w.write("methods=[")
w.indent()
for rule in session.methods {
let allowed = match rule.allowed {
Some(states) =>
"Some([" +
states.map(state => mbt_string(state.to_string())).join(", ") +
"])"
None => "None"
}
let requires = rule.requires.map(mbt_string).join(", ")
let cancels = match rule.cancels {
Some(cancel) => {
let position = match cancel.position {
Some(index) => "Some(\{index})"
None => "None"
}
let code = match cancel.code {
Some(code) =>
"Some(\{self.int_code(code, @smithy.ShapeId::parse("smithy.rpc#jsonRpcCancels").unwrap())})"
None => "None"
}
"Some(@rpc.CancelRule::new(id_member=\{mbt_string(cancel.wire_name)}, position=\{position}, code=\{code}))"
}
None => "None"
}
w.write("@rpc.MethodRule::new(")
w.indent()
w.write("receiver=$L,", args=[
Text(mbt_string(rule.receiver.to_string())),
])
w.write("method_name=$L,", args=[
Text(mbt_string(rule.method_name.to_string())),
])
w.write("allowed=$L,", args=[Text(allowed)])
w.write("on_call=$L,", args=[
Text(option_text(rule.on_call.map(s => s.to_string()))),
])
w.write("on_success=$L,", args=[
Text(option_text(rule.on_success.map(s => s.to_string()))),
])
w.write("on_error=$L,", args=[
Text(option_text(rule.on_error.map(s => s.to_string()))),
])
w.write("requires=[$L],", args=[Text(requires)])
w.write("cancels=$L,", args=[Text(cancels)])
w.dedent()
w.write("),")
}
w.dedent()
w.write("],")
let refusals = session.refusals
.map(refusal => {
"(\{mbt_string(refusal.state.to_string())}, \{self.int_code(refusal.code, @smithy.ShapeId::parse("smithy.rpc#jsonRpcSession").unwrap())})"
})
.join(", ")
w.write("refusals=[$L],", args=[Text(refusals)])
let capability_refusal = match session.capability_refusal {
Some(code) =>
"Some(\{self.int_code(code, @smithy.ShapeId::parse("smithy.rpc#jsonRpcSession").unwrap())})"
None => "None"
}
w.write("capability_refusal=$L,", args=[Text(capability_refusal)])
w.dedent()
w.write(")")
}
}
w.dedent()
w.write("}")
w.blank_line()
w.write("///|")
w.write(
"/// A peer `$L` that talks to peer `$L`, with the session rules.",
args=[Text(peer_name), Text(remote)],
)
w.write("pub fn new_peer(log? : @rpc.TranscriptLog) -> @rpc.Peer {")
w.indent()
w.write(
"@rpc.Peer::new(self_name=$L, remote_name=$L, rules=session_rules(), log~)",
args=[Text(mbt_string(peer_name)), Text(mbt_string(remote))],
)
w.dedent()
w.write("}")
w.blank_line()
})
}