///|
/// One ordinary package import from the consuming `moon.pkg`. `alias` is the
/// name used after `@` in MoonBit source, while `import_path` is the canonical
/// package path materialized under `.mooncakes`.
priv struct PackageImport {
import_alias : String
import_path : String
}
///|
fn PackageImport::PackageImport(
manifest_path : String,
import_path : String,
explicit_alias : String?,
) -> PackageImport raise SubclassCommandError {
guard import_path != "" else {
raise SubclassCommandError::Failure(
"\{manifest_path}: package import path must not be empty",
)
}
let import_alias = if explicit_alias is Some(explicit_import_alias) {
explicit_import_alias
} else {
import_path
.rev_split_once("/")
.map_or(import_path, pair => pair.1.to_owned())
}
guard import_alias != "" else {
raise SubclassCommandError::Failure(
"\{manifest_path}: package import '\{import_path}' has no usable alias",
)
}
{ import_alias, import_path }
}
///|
fn PackageImport::from_ast(
manifest_path : String,
ast : @moon_config.Ast,
) -> PackageImport raise SubclassCommandError {
match ast {
@moon_config.Str(import_path, ..) =>
PackageImport(manifest_path, import_path, None)
@moon_config.Obj(fields, ..) => {
let mut import_path : String? = None
let mut explicit_alias : String? = None
for field in fields {
let (key, value) = field
match (key, value) {
("path", @moon_config.Str(path, ..)) => import_path = Some(path)
("alias", @moon_config.Str(import_alias, ..)) =>
explicit_alias = Some(import_alias)
_ => ()
}
}
guard import_path is Some(import_path) else {
raise SubclassCommandError::Failure(
"\{manifest_path}: package import has no string 'path'",
)
}
PackageImport(manifest_path, import_path, explicit_alias)
}
_ =>
raise SubclassCommandError::Failure(
"\{manifest_path}: ordinary package imports must be strings or path/alias objects",
)
}
}
///|
/// In a bindings monorepo, sibling binding modules are conventionally named
/// after the final component of their canonical import path.
fn PackageImport::local_config_path(
self : PackageImport,
directory : @path.Path,
) -> @path.Path {
let binding_directory_name = self.import_path
.rev_split_once("/")
.map_or(self.import_path, pair => pair.1.to_owned())
directory
.join(@path.Path(binding_directory_name))
.join(@path.Path("gir.json"))
}
///|
/// Moon materializes a dependency at `.mooncakes/`.
/// Custom source aliases never participate in this filesystem path.
fn PackageImport::materialized_config_path(
self : PackageImport,
mooncakes : @path.Path,
) -> @path.Path {
mooncakes.join(@path.Path(self.import_path)).join(@path.Path("gir.json"))
}
///|
/// The subset of a consuming package manifest needed to resolve the package
/// alias named by `#gobject.subclass(parent=...)`.
priv struct PackageManifest {
path : String
imports : Array[PackageImport]
}
///|
fn PackageManifest::parse(
path : String,
source : String,
) -> PackageManifest raise SubclassCommandError {
let (ast, reports) = @moon_config.parse_moon_pkg(source, name=path)
let diagnostics : Array[String] = []
for report in reports {
// moon_config 0.3.9 predates Moon's current executable-package spelling.
// The parsed AST is still usable, so ignore only that known compatibility
// diagnostic and continue to reject syntax or unrelated manifest errors.
if report.msg != "Invalid moon.pkg config: unexpected key `pkgtype`." {
diagnostics.push(report.to_string())
}
}
if !diagnostics.is_empty() {
raise SubclassCommandError::Failure(
"\{path}: MoonBit manifest parser rejected the input\n\{diagnostics.join("\n")}",
)
}
guard ast is @moon_config.Obj(fields, ..) else {
raise SubclassCommandError::Failure(
"\{path}: MoonBit manifest parser did not return an object",
)
}
let imports : Array[PackageImport] = []
for field in fields {
let (key, value) = field
if key == "import" {
guard value is @moon_config.Arr(entries, ..) else {
raise SubclassCommandError::Failure(
"\{path}: ordinary package imports are not an array",
)
}
for entry in entries {
imports.push(PackageImport::from_ast(path, entry))
}
}
}
{ path, imports }
}
///|
/// Resolve the source-level alias to the canonical package import path. The
/// generator must not confuse a custom alias with a `.mooncakes` directory.
fn PackageManifest::resolve_import(
self : PackageManifest,
import_alias : String,
) -> PackageImport raise SubclassCommandError {
let mut resolved : PackageImport? = None
for package_import in self.imports {
if package_import.import_alias == import_alias {
if resolved is Some(existing) {
raise SubclassCommandError::Failure(
"\{self.path}: @\{import_alias} refers to both '\{existing.import_path}' and '\{package_import.import_path}'",
)
}
resolved = Some(package_import)
}
}
if resolved is Some(package_import) {
return package_import
}
raise SubclassCommandError::Failure(
"\{self.path}: @\{import_alias} is not an ordinary package import",
)
}
///|
/// A MoonBit source file belongs to the package whose `moon.pkg` is in the
/// same directory. All inputs in one invocation must share that package.
async fn SubclassCommandOptions::load_package_manifest(
self : SubclassCommandOptions,
) -> PackageManifest raise SubclassCommandError {
guard self.inputs.get(0) is Some(first) else {
raise SubclassCommandError::Failure("at least one input file is required")
}
let package_directory = @path.Path(first).resolve().dirname()
for input in self.inputs[1:] {
if @path.Path(input).resolve().dirname() != package_directory {
raise SubclassCommandError::Failure(
"input files belong to different MoonBit packages; pass one package per invocation",
)
}
}
let manifest_path = package_directory.join(@path.Path("moon.pkg"))
if !self.path_exists(manifest_path) {
raise SubclassCommandError::Failure(
"cannot resolve the parent import because \{manifest_path.0} does not exist; pass --config",
)
}
let source = self.read_text(manifest_path.0)
PackageManifest::parse(manifest_path.0, source)
}