diff --git a/README.md b/README.md index 500eb46..aef62df 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ After updating your OpenAPI spec, regenerate the typed scaffolding without touch mapi generate --spec openapi.json --project ./my-api ``` +`generate` treats `--project` as an existing MoonBit package root: it reads the package name from `moon.mod.json`, regenerates `src/generated/*`, and leaves your existing module manifest untouched. Only files with the `// Code generated by mapi. DO NOT EDIT.` header are modified. Your handlers are safe. --- diff --git a/main/generator.mbt b/main/generator.mbt index 61a76a1..08d203c 100644 --- a/main/generator.mbt +++ b/main/generator.mbt @@ -5,9 +5,15 @@ let generated_header = "// Code generated by mapi. DO NOT EDIT.\n" let user_header = "// User-owned file. Safe from regeneration.\n" ///| -fn generate_project(ir : ApiIR, module_name : String) -> Map[String, String] { +fn generate_project( + ir : ApiIR, + module_name : String, + include_module_manifest : Bool, +) -> Map[String, String] { let files = {} - files["moon.mod.json"] = generate_moon_mod(module_name) + if include_module_manifest { + files["moon.mod.json"] = generate_moon_mod(module_name) + } files["src/generated/moon.pkg"] = generate_generated_pkg(module_name) files["src/generated/schemas.mbt"] = generate_schemas_file(ir) files["src/generated/operations.mbt"] = generate_operations_file(ir) diff --git a/main/main.mbt b/main/main.mbt index 0c9809a..091a485 100644 --- a/main/main.mbt +++ b/main/main.mbt @@ -43,10 +43,17 @@ fn run_command(args : Array[String], mode : CommandMode) -> Unit { { title: "", version: "", schemas: [], operations: [] } } } - let module_name = infer_module_name(config.output_dir) - let files = generate_project(ir, module_name) - write_project(files, config.output_dir, mode == Generate) catch { - err => fail_with_message("Failed to write project: ", debug_string(err)) + let include_module_manifest = mode == Init + match resolve_module_name(config.output_dir, include_module_manifest) { + Ok(module_name) => { + let files = generate_project(ir, module_name, include_module_manifest) + write_project(files, config.output_dir, mode == Generate) catch { + err => + fail_with_message("Failed to write project: ", debug_string(err)) + } + } + Err(message) => + fail_with_message("Failed to resolve module name: ", message) } write_state(ir, config.spec_path, config.output_dir) catch { err => fail_with_message("Failed to write state: ", debug_string(err)) @@ -67,6 +74,11 @@ struct CliConfig { output_dir : String } +///| +struct MoonModConfig { + name : String +} derive(FromJson) + ///| fn parse_cli_options( args : Array[String], @@ -155,6 +167,62 @@ fn infer_module_name(output_dir : String) -> String { } } +///| +fn resolve_module_name( + output_dir : String, + include_module_manifest : Bool, +) -> Result[String, String] { + if include_module_manifest { + Ok(infer_module_name(output_dir)) + } else { + read_module_name_from_moon_mod(output_dir) + } +} + +///| +fn read_module_name_from_moon_mod( + project_dir : String, +) -> Result[String, String] { + let mod_path = project_dir + "/moon.mod.json" + if !@fs.path_exists(mod_path) { + return Err("moon.mod.json not found in " + project_dir) + } + let raw_result = Ok(@fs.read_file_to_string(mod_path)) catch { + err => Err("cannot read moon.mod.json: " + debug_string(err)) + } + let raw = match raw_result { + Ok(value) => value + Err(message) => return Err(message) + } + parse_module_name_from_moon_mod(raw) +} + +///| +fn parse_module_name_from_moon_mod(raw : String) -> Result[String, String] { + let parsed = @json.parse(raw) catch { + _ => return Err("moon.mod.json is not valid JSON") + } + let mod_config : MoonModConfig = @json.from_json(parsed) catch { + _ => return Err("missing or invalid \"name\" field in moon.mod.json") + } + let module_name = mod_config.name.trim().to_string() + if module_name.is_empty() { + Err("empty \"name\" value in moon.mod.json") + } else if contains_whitespace(module_name) { + Err("invalid \"name\" value in moon.mod.json: contains whitespace") + } else { + Ok(module_name) + } +} + +///| +fn contains_whitespace(value : String) -> Bool { + value.find(" ") is Some(_) || + value.find("\t") is Some(_) || + value.find("\n") is Some(_) || + value.find("\r") is Some(_) +} + ///| fn success_message(mode : CommandMode, output_dir : String) -> String { match mode { diff --git a/main/moon.pkg b/main/moon.pkg index f391e63..7f8fa0c 100644 --- a/main/moon.pkg +++ b/main/moon.pkg @@ -1,5 +1,6 @@ import { "moonbitlang/core/debug" @debug, + "moonbitlang/core/json" @json, "moonbitlang/x/fs" @fs, "moonbitlang/x/sys" @sys, "cogna-dev/x/openapi" @openapi,