Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/mq/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# mq

`mq` is the native command for parsing MoonBit configuration DSL files. It
lives at `moonbitlang/parser/cmd/mq` and uses `moonbitlang/async`.

The WASI wasm package is documented separately in `cmd/mq_wasm`.

The `legacy` subcommand prints the post-processed JSON form that is compatible
with the old JSON configuration format.

```bash
mq legacy moon.pkg
mq legacy moon.mod
mq legacy moon.work
mq legacy moon.pkg -o moon.pkg.json
mq legacy --file-type mod -c 'name = "demo/mod"'
cat moon.work | mq legacy - --file-type work
```

When reading from stdin or `-c/--code`, pass `--file-type pkg`,
`--file-type mod`, or `--file-type work` to the `legacy` subcommand.

install:

```bash
moon install moonbitlang/parser/cmd/mq
```
227 changes: 227 additions & 0 deletions cmd/mq/cli.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
///|
fn ends_with(str : String, suffix : String) -> Bool {
let str_len = str.length()
let suffix_len = suffix.length()
str_len >= suffix_len && str[str_len - suffix_len:].to_owned() == suffix
}

///|
fn config_kind(name : String) -> String? {
if ends_with(name, "moon.pkg") {
Some("pkg")
} else if ends_with(name, "moon.mod") {
Some("mod")
} else if ends_with(name, "moon.work") {
Some("work")
} else {
None
}
}

///|
fn file_type_kind(file_type : String) -> String? {
match file_type {
"pkg" => Some("pkg")
"mod" => Some("mod")
"work" => Some("work")
_ => None
}
}

///|
fn kind_name(kind : String) -> String {
match kind {
"pkg" => "moon.pkg"
"mod" => "moon.mod"
"work" => "moon.work"
_ => "moon.pkg"
}
}

///|
fn parse_config(kind : String, source : String) -> @config.Ast {
let name = kind_name(kind)
let (ast, _) = match kind {
"pkg" => @config.parse_moon_pkg(name~, source)
"mod" => @config.parse_moon_mod(name~, source)
"work" => @config.parse_moon_work(name~, source)
_ => @config.parse_moon_pkg(name~, source)
}
ast
}

///|
enum InputSource {
File(String)
Stdin
Code(String)
}

///|
struct LegacyOptions {
source : InputSource
file_type : String?
output : String?
}

///|
enum CliAction {
RunLegacy(LegacyOptions)
Help(String)
Error(String)
}

///|
fn root_usage() -> String {
(
#|Usage: mq <command>
#|
#|Mooncakes.io backend MoonBit config tool
#|
#|Commands:
#| legacy Convert Moon config DSL to legacy Mooncakes.io JSON output
#| help Print help for the subcommand.
#|
#|Options:
#| -h, --help Show help information.
)
}

///|
fn legacy_usage() -> String {
(
#|Usage: mq legacy [options] [input]
#|
#|Convert Moon config DSL to legacy Mooncakes.io JSON output
#|
#|Arguments:
#| input Path to moon.pkg/moon.mod/moon.work, or - for stdin
#|
#|Options:
#| -h, --help Show help information.
#| --file-type <file-type> File type for stdin or -c input: pkg, mod, or work
#| -c, --code <source> Read config source from command line
#| -o, --output <output> Write output to file instead of stdout
)
}

///|
fn cli_error(message : String, usage : String) -> CliAction {
Error("error: \{message}\n\n\{usage}")
}

///|
fn is_help_arg(arg : String) -> Bool {
arg == "-h" || arg == "--help"
}

///|
fn parse_cli(args : ArrayView[String]) -> CliAction {
if args.length() == 0 {
return cli_error("expected subcommand: legacy", root_usage())
}
let first = args[0]
if is_help_arg(first) {
return Help(root_usage())
}
if first == "help" {
if args.length() == 1 {
return Help(root_usage())
} else if args.length() == 2 && args[1] == "legacy" {
return Help(legacy_usage())
} else {
return cli_error("unknown help topic", root_usage())
}
}
if first != "legacy" {
return cli_error("unknown subcommand: \{first}", root_usage())
}
parse_legacy(args[1:])
}

///|
fn parse_legacy(args : ArrayView[String]) -> CliAction {
let mut input : String? = None
let mut code : String? = None
let mut file_type : String? = None
let mut output : String? = None
let mut i = 0
while i < args.length() {
let arg = args[i]
if is_help_arg(arg) {
return Help(legacy_usage())
} else if arg == "--file-type" {
if i + 1 >= args.length() {
return cli_error("missing value for --file-type", legacy_usage())
}
file_type = Some(args[i + 1])
i = i + 2
continue
} else if arg.has_prefix("--file-type=") {
file_type = Some(arg["--file-type=".length():].to_owned())
i = i + 1
continue
} else if arg == "-c" || arg == "--code" {
if i + 1 >= args.length() {
return cli_error("missing value for \{arg}", legacy_usage())
}
code = Some(args[i + 1])
i = i + 2
continue
} else if arg.has_prefix("--code=") {
code = Some(arg["--code=".length():].to_owned())
i = i + 1
continue
} else if arg == "-o" || arg == "--output" {
if i + 1 >= args.length() {
return cli_error("missing value for \{arg}", legacy_usage())
}
output = Some(args[i + 1])
i = i + 2
continue
} else if arg.has_prefix("--output=") {
output = Some(arg["--output=".length():].to_owned())
i = i + 1
continue
} else if arg.has_prefix("-") && arg != "-" {
return cli_error("unknown option: \{arg}", legacy_usage())
} else {
if input is Some(_) {
return cli_error("unexpected argument: \{arg}", legacy_usage())
}
input = Some(arg)
i = i + 1
}
}
if code is Some(source) {
if input is Some(_) {
return cli_error(
"-c/--code cannot be used with an input path",
legacy_usage(),
)
}
return RunLegacy(LegacyOptions::{ source: Code(source), file_type, output })
}
let source = match input {
Some("-") => Stdin
Some(path) => File(path)
None => return cli_error("missing input", legacy_usage())
}
RunLegacy(LegacyOptions::{ source, file_type, output })
}

///|
fn kind_from_options(options : LegacyOptions) -> String? {
match options.file_type {
Some(file_type) =>
match file_type_kind(file_type) {
Some(kind) => Some(kind)
None => None
}
None =>
match options.source {
File(path) => config_kind(path)
Stdin | Code(_) => None
}
}
}
100 changes: 100 additions & 0 deletions cmd/mq/main.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
///|
async fn die(message : String) -> Unit {
@stdio.stderr.write("\{message}\n") catch {
_ => ()
}
@sys.exit(1)
}

///|
async fn write_stdout(message : String) -> Unit {
try @stdio.stdout.write(message) catch {
err => die("error: failed to write stdout: \{err}")
} noraise {
_ => ()
}
}

///|
fn program_args() -> ArrayView[String] {
let args = @sys.get_cli_args()
if args.length() > 1 {
args[1:]
} else {
[]
}
}

///|
async fn read_file(path : String) -> String {
try @fs.read_file(path).text() catch {
err => {
die("error: failed to read \{path}: \{err}")
""
}
} noraise {
source => source
}
}

///|
async fn read_stdin() -> String {
try @stdio.stdin.read_all().text() catch {
err => {
die("error: failed to read stdin: \{err}")
""
}
} noraise {
source => source
}
}

///|
async fn write_result(output : String?, content : String) -> Unit {
let content = "\{content}\n"
match output {
Some(path) =>
try
@fs.write_file(
path,
content,
create_mode=@fs.CreateMode::CreateOrTruncate,
)
catch {
err => die("error: failed to write \{path}: \{err}")
} noraise {
_ => ()
}
None => write_stdout(content)
}
}

///|
async fn run_legacy(options : LegacyOptions) -> Unit {
let kind = match kind_from_options(options) {
Some(kind) => kind
None => {
die("error: --file-type pkg|mod|work is required for stdin or -c input")
"pkg"
}
}
let source = match options.source {
File(path) => read_file(path)
Stdin => read_stdin()
Code(source) => source
}
let ast = parse_config(kind, source)
write_result(options.output, ast.to_json().stringify())
}

///|
async fn main {
match parse_cli(program_args()) {
Help(text) => {
write_stdout("\{text}\n")
@sys.exit(0)
}
Error(message) => die(message)
RunLegacy(options) => run_legacy(options)
}
}
15 changes: 15 additions & 0 deletions cmd/mq/moon.pkg
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
"moonbitlang/parser/moon_config" @config,
"moonbitlang/async",
"moonbitlang/async/fs",
"moonbitlang/async/stdio",
"moonbitlang/async/io",
"moonbitlang/x/sys",
"moonbitlang/core/json",
}

supported_targets = "+native"

options(
"is-main": true,
)
12 changes: 12 additions & 0 deletions cmd/mq/pkg.generated.mbti
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Generated using `moon info`, DON'T EDIT IT
package "moonbitlang/parser/cmd/mq"

// Values

// Errors

// Types and methods

// Type aliases

// Traits
Loading
Loading