///|
suberror CliError {
CliError(String)
}
///|
struct CliOptions {
old_path : String
new_path : String
rules_path : String?
markdown_path : String?
json_path : String?
shim_path : String?
fail_on_breaking : Bool
}
///|
fn help_text() -> String {
let text =
#|MoonShim - compile-tested compatibility shims for MoonBit APIs
#|
#|USAGE
#| moon run cmd/main -- diff [OPTIONS]
#|
#|OPTIONS
#| --rules Load explicit rename rules from JSON
#| --markdown Write a Markdown migration report
#| --json Write a machine-readable JSON report
#| --shim Write generated MoonBit compatibility source
#| --fail-on-breaking Exit with status 2 when review is required
#| --help Show this help
#|
#|Generate interfaces with:
#| moon info
#| cp pkg.generated.mbti old.mbti
text
}
///|
fn require_next(
args : ArrayView[String],
index : Int32,
flag : String,
) -> String raise CliError {
if index + 1 >= args.length() {
raise CliError("Missing value after \{flag}")
}
args[index + 1]
}
///|
fn parse_options(args : ArrayView[String]) -> CliOptions? raise CliError {
let subcommand_index = match args.search("diff") {
Some(index) => index
None => {
if args.contains("--help") || args.contains("-h") {
println(help_text())
return None
}
raise CliError(
"Expected the `diff` subcommand. Run with `--help` for usage.",
)
}
}
let positionals = []
let mut rules_path : String? = None
let mut markdown_path : String? = None
let mut json_path : String? = None
let mut shim_path : String? = None
let mut fail_on_breaking = false
let mut index = subcommand_index + 1
while index < args.length() {
let arg = args[index]
match arg {
"--rules" | "-r" => {
rules_path = Some(require_next(args, index, arg))
index += 2
}
"--markdown" | "-m" => {
markdown_path = Some(require_next(args, index, arg))
index += 2
}
"--json" | "-j" => {
json_path = Some(require_next(args, index, arg))
index += 2
}
"--shim" | "-s" => {
shim_path = Some(require_next(args, index, arg))
index += 2
}
"--fail-on-breaking" => {
fail_on_breaking = true
index += 1
}
"--help" | "-h" => {
println(help_text())
return None
}
_ =>
if arg.has_prefix("-") {
raise CliError("Unknown option `\{arg}`")
} else {
positionals.push(arg)
index += 1
}
}
}
if positionals.length() != 2 {
raise CliError(
"The `diff` command requires exactly two `.mbti` paths; received \{positionals.length()}.",
)
}
Some(CliOptions::{
old_path: positionals[0],
new_path: positionals[1],
rules_path,
markdown_path,
json_path,
shim_path,
fail_on_breaking,
})
}
///|
fn load_rules(path : String?) -> Array[@moonshim.MigrationRule] raise {
match path {
Some(path) => {
let source = @fs.read_file_to_string(path, encoding="utf8")
@moonshim.rules_from_json(source)
}
None => []
}
}
///|
fn print_plan_summary(plan : @moonshim.MigrationPlan) -> Unit {
let mut added = 0
let mut shimmed = 0
let mut blocked = 0
for change in plan.changes {
if change.change_kind is @moonshim.Added {
added += 1
} else if change.safely_shimmable {
shimmed += 1
} else {
blocked += 1
}
}
println(
"MoonShim: \{added} added, \{shimmed} shimmable, \{blocked} blocked, \{plan.shim_sources.length()} shim declaration(s)",
)
}
///|
fn run(args : ArrayView[String]) -> Int32 raise {
let options = match parse_options(args) {
Some(options) => options
None => return 0
}
let old_source = @fs.read_file_to_string(options.old_path, encoding="utf8")
let new_source = @fs.read_file_to_string(options.new_path, encoding="utf8")
let old_snapshot = @moonshim.parse_interface(old_source)
let new_snapshot = @moonshim.parse_interface(new_source)
let rules = load_rules(options.rules_path)
let plan = @moonshim.plan_migration(old_snapshot, new_snapshot, rules)
let markdown = @moonshim.render_markdown(
plan,
options.old_path,
options.new_path,
)
let json = @moonshim.render_json(plan, options.old_path, options.new_path)
print_plan_summary(plan)
match options.markdown_path {
Some(path) => @fs.write_string_to_file(path, markdown, encoding="utf8")
None => println(markdown)
}
match options.json_path {
Some(path) => @fs.write_string_to_file(path, json, encoding="utf8")
None => ()
}
match options.shim_path {
Some(path) => {
let source = @moonshim.render_shim_source(plan)
@fs.write_string_to_file(path, source, encoding="utf8")
}
None => ()
}
if options.fail_on_breaking && @moonshim.has_unresolved_breaking_changes(plan) {
2
} else {
0
}
}
///|
fn main {
let code = run(@env.args()) catch {
error => {
eprintln("MoonShim error: \{error}")
2
}
}
if code != 0 {
@sys.exit(code)
}
}