///|
/// The settings of the MoonBit plugin, after the shared settings.
let moonbit_settings : Array[String] = ["module"]
///|
/// The symbol of the runtime package. Each generated file imports it.
fn runtime_symbol(module_name : String) -> @codegen.Symbol {
@codegen.Symbol::new(
@codegen.SymbolName::parse("Peer").unwrap(),
ns=module_namespace(module_name + "/rpc"),
)
}
///|
/// The import policy of MoonBit: a foreign symbol is `@pkg.Name`, and the
/// package goes into `moon.pkg`. The qualifier is the last path segment.
fn moonbit_imports() -> @codegen.ImportPolicy {
@codegen.ImportPolicy::by_package(
qualify=(qualifier, symbol) => {
"@\{qualifier.to_string()}.\{symbol.name.to_string()}"
},
qualifier=ns => {
let parts = ns.text.split("/").map(part => part.to_owned()).collect()
@codegen.SymbolName::parse(parts[parts.length() - 1]).unwrap()
},
locals_block_qualifiers=false,
)
}
///|
/// The symbol of a shape: its contextual type name in the root package of the
/// module. An error shape is in `errors.mbt`, another shape in `types.mbt`.
fn symbol_provider(
names : @codegen.ServiceNames,
module_name : String,
) -> @codegen.SymbolProvider {
@codegen.SymbolProvider::new(shape => {
let name = type_name(names.shape_name(shape.id))
guard @codegen.SymbolName::parse(name) is Some(symbol_name) else {
raise @codegen.InvalidSettings(
"I could not make a MoonBit type name for shape `\{shape.id}`.",
)
}
let path = if shape.trait_value("smithy.api#error") is Some(_) {
errors_path()
} else {
types_path()
}
@codegen.Symbol::new(
symbol_name,
ns=module_namespace(module_name),
definition=Some(path),
)
})
}
///|
/// Generate a MoonBit JSON-RPC peer. Settings: `service`, `protocol`, `peer`,
/// `transports` (see `PeerSettings`) and `module`, the MoonBit module name of
/// the output, such as `example/terminal`.
pub fn run(
plugin : @codegen.PluginContext,
) -> Map[String, String] raise @codegen.CodegenError {
let settings = @jsonrpc.PeerSettings::from_context(
plugin,
extra=moonbit_settings,
)
guard plugin.setting_string("module") is Some(module_name) else {
raise @codegen.InvalidSettings(
"I expected setting `module`, the MoonBit module name of the output.",
)
}
check_module_name(module_name)
let names = settings.names(plugin.model)
let values : Map[@smithy.ShapeId, @validation.StructureContract] = Map([])
let mut value_config = @validation.Config::new()
for service in [settings.peer.accepts, settings.peer.calls] {
guard service is Some(description) else { continue }
value_config = description.value_config
for contract in description.values {
if !values.contains(contract.shape) {
values[contract.shape] = contract
}
}
}
let packages : Map[String, String] = Map([])
let rpc = runtime_symbol(module_name)
let render = (writer : @codegen.CodeWriter) => {
for entry in writer.package_imports() {
packages[entry.ns.text] = entry.qualifier.to_string()
}
generated_header + "\n\n" + tidy(writer.to_string())
}
let provider = symbol_provider(names, module_name)
let director : @codegen.DirectedCodegen[Gen] = @codegen.DirectedCodegen::new(
create_symbol_provider=(_, _) => provider,
create_context=info => {
model: info.model,
settings,
names,
module_name,
delegator: @codegen.WriterDelegator::new(
info.provider,
factory=(_, ns) => {
let writer = @codegen.CodeWriter::new(
ns~,
indent_text=" ",
imports=moonbit_imports(),
)
writer.add_import(rpc)
writer
},
render~,
),
values,
value_config,
written: Map([]),
errors: [],
type_names: Map([]),
generated_names: generated_type_names(settings, names),
},
delegator=gen => gen.delegator,
generate_service=(_, _) => (),
generate_structure=(gen, shape) => gen.write_shape(shape),
generate_error=(gen, shape) => gen.write_shape(shape),
generate_union=(gen, shape) => gen.write_shape(shape),
generate_enum=(gen, shape) => gen.write_shape(shape),
generate_int_enum=(gen, shape) => gen.write_shape(shape),
customize_before_integrations=gen => {
// The director walks the closure of the `service` setting. A protocol
// peer also needs the shapes of the other service.
for service in settings.services() {
let order = @codegen.topological_order(
gen.model,
@codegen.service_closure(gen.model, service),
)
for id in order {
if gen.model.shapes.get(id) is Some(shape) {
gen.write_shape(shape)
}
}
}
gen.write_errors()
gen.write_handler()
gen.write_client()
gen.write_session()
gen.write_test()
},
)
let files = @codegen.run_codegen(plugin, director)
files["moon.mod"] = module_file(module_name)
files["moon.pkg"] = package_file(packages)
for entry in runtime_files {
files["rpc/" + entry.0] = entry.1
}
files
}
///|
/// The text with at most one blank line in a row and one newline at the end.
fn tidy(text : String) -> String {
let out = StringBuilder()
let mut blank = 0
for line in text.split("\n") {
if line.is_empty() {
blank += 1
continue
}
if out.to_string().length() > 0 {
out.write_string(if blank > 0 { "\n\n" } else { "\n" })
}
blank = 0
out.write_string(line.to_owned())
}
out.write_string("\n")
out.to_string()
}
///|
/// The type names that the generated code declares: the error type, and the
/// handler trait and the client of each service.
fn generated_type_names(
settings : @jsonrpc.PeerSettings,
names : @codegen.ServiceNames,
) -> Array[String] {
let out = [service_error_name]
for service in settings.services() {
let base = type_name(names.shape_name(service))
out.push(base + "Handler")
out.push(base + "Client")
}
out
}
///|
/// The text of `moon.mod`, with the pinned dependencies of the runtime.
fn module_file(module_name : String) -> String {
(
$|name = "\{module_name}"
$|
$|version = "0.0.0"
$|
$|preferred_target = "native"
$|
$|warnings = "-implicit_impl_as_method"
$|
$|import {
$| "moonbitlang/async@\{async_version}",
$|}
$|
)
}
///|
/// The text of the root `moon.pkg`: the packages that the files import, in
/// lexical order.
fn package_file(packages : Map[String, String]) -> String {
let paths = packages.keys().collect()
paths.sort_by((a, b) => a.lexical_compare(b))
let out = StringBuilder()
out.write_string("import {\n")
for path in paths {
out.write_string(" \"\{path}\" @\{packages[path]},\n")
}
out.write_string("}\n")
out.to_string()
}
///|
/// A test that builds the session rules and a peer.
fn Gen::write_test(self : Gen) -> Unit raise @codegen.CodegenError {
let path = @codegen.OutputPath::parse("session_wbtest.mbt").unwrap()
self.with_file(path, w => {
w.write("///|")
w.write("test \"session rules and peer\" {")
w.indent()
w.write("let _ = session_rules()")
w.write("let _ = new_peer()")
w.dedent()
w.write("}")
})
}
///|
/// The plugin for the build runner. Register it as `moonbit-jsonrpc-codegen`.
pub fn plugin(
model : @smithy.Model,
settings : Json,
) -> Map[String, String] raise @build.BuildError {
@codegen.build_plugin(run)(model, settings)
}