///|
/// 审计报告的呈现方式。
enum View {
Text
Json
Tree
SpatialTree
}
///|
/// 命令行用法说明。
fn usage() -> String {
"MoonIFC Inspector - IFC STEP structural auditor\n\n" +
"USAGE:\n" +
" moon run apps/inspect_cli -- [OPTIONS] [FILE.ifc]\n\n" +
"OPTIONS:\n" +
" --text print the text report (default)\n" +
" --json print the JSON report\n" +
" --tree print the entity reference list\n" +
" --spatial-tree print the indented spatial hierarchy\n" +
" --federation check GlobalId collisions across multiple files\n" +
" --help print this message\n\n" +
"Federation usage: moon run apps/inspect_cli -- --federation [--json] MODEL1.ifc MODEL2.ifc [...]\n" +
"Federation mode reports cross-file GlobalId collisions only; use audit for per-file rules.\n\n" +
"Without FILE, a built-in sample model is audited.\n" +
"Exit codes: 0 clean, 1 findings, 2 STEP parse diagnostics or read error."
}
///|
/// 解析视图参数;不是视图开关时返回 None。
fn parse_view(arg : String) -> View? {
match arg {
"--text" => Some(Text)
"--json" => Some(Json)
"--tree" => Some(Tree)
"--spatial-tree" => Some(SpatialTree)
_ => None
}
}
///|
/// 编译期固定的内置示例模型:一处指向 `#99` 的悬空引用,用于演示 REF001。
fn sample() -> String {
"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n#1=IFCPROJECT('P',());\n#2=IFCWALL('W',#99);\nENDSEC;\nEND-ISO-10303-21;"
}
///|
/// 按视图渲染报告;引用列表视图需要关系图,因此单独传入。
fn render(
view : View,
report : @model_rules.AuditReport,
map : @relation_map.RelationMap,
) -> String {
match view {
Text => @report_view.text(report)
Json => @report_view.json(report)
Tree => @report_view.tree(map)
SpatialTree => @report_view.spatial_tree(map)
}
}
///|
/// 解析并审计一段 IFC STEP 文本。返回退出码:0 无发现,1 有发现,2 存在解析诊断。
fn inspect(source : String, view : View) -> Int {
let document = @step_text.parse(source)
let map = @relation_map.build(@ifc_entities.build(document))
let report = @model_rules.audit(map)
println(render(view, report, map))
// 审计报告不并入 STEP 解析诊断,此处单独呈现,避免畸形输入被静默忽略。
let errors = document.errors()
if errors.length() > 0 {
println("")
println("STEP parse diagnostics: " + errors.length().to_string())
for error in errors {
println(" " + error.location().to_string() + " " + error.message())
}
return 2
}
return if report.findings().length() > 0 { 1 } else { 0 }
}
///|
/// 读取 `.ifc` 文件;失败时返回可读的错误说明。
fn read_source(file : String) -> Result[String, String] {
if !@fs.path_exists(file) {
return Err("file not found: " + file)
}
let loaded : Result[String, String] = Ok(@fs.read_file_to_string(file)) catch {
_ => Err("cannot read file: " + file)
}
loaded
}
///|
/// 读取并审计一个 `.ifc` 文件。返回退出码:0 无发现,1 有发现,2 解析诊断或读取错误。
fn inspect_file(file : String, view : View) -> Int {
match read_source(file) {
Ok(source) => inspect(source, view)
Err(message) => {
println("error: " + message)
return 2
}
}
}
///|
/// Audit shared GlobalIds across multiple IFC sources, retaining each source's provenance.
fn inspect_federation(files : Array[String], view : View) -> Int {
match view {
Text | Json => ()
Tree | SpatialTree => {
println("error: federation mode supports only --text or --json")
return 2
}
}
let models : Array[@model_federation.ModelSource] = []
let mut input_errors = 0
for file in files {
match read_source(file) {
Ok(source) => {
let document = @step_text.parse(source)
let errors = document.errors()
if errors.length() > 0 {
input_errors += errors.length()
println("STEP parse diagnostics in " + file + ":")
for error in errors {
println(" " + error.location().to_string() + " " + error.message())
}
}
models.push(
@model_federation.ModelSource::new(
name=file,
entities=@ifc_entities.build(document),
),
)
}
Err(message) => {
input_errors += 1
println("error: " + message)
}
}
}
let report = @model_federation.audit(models)
let output = match view {
Text => @report_view.federation_text(report)
Json => @report_view.federation_json(report)
Tree | SpatialTree => ""
}
println(output)
if input_errors > 0 {
2
} else if report.collisions().length() > 0 {
1
} else {
0
}
}
///|
/// Node 参数包含运行时路径和脚本路径,跳过两个前缀。
#cfg(target="js")
fn user_args(args : Array[String]) -> Array[String] {
match args {
[_, _, .. rest] => rest.to_owned()
[] | [_] => []
}
}
///|
/// 其他目标下首元素是可执行前缀。
#cfg(not(target="js"))
fn user_args(args : Array[String]) -> Array[String] {
match args {
[_, .. rest] => rest.to_owned()
[] => []
}
}
///|
struct Arguments {
view : View
federation : Bool
paths : Array[String]
help : Bool
}
///|
fn parse_arguments(args : Array[String]) -> Arguments {
let mut view : View = Text
let mut federation = false
let paths : Array[String] = []
let mut help = false
for arg in args {
match arg {
"--federation" => federation = true
_ =>
match parse_view(arg) {
Some(next) => view = next
None =>
match arg {
"--help" => help = true
_ =>
if arg.has_prefix("--") {
println("unknown option: " + arg)
println("")
help = true
} else {
paths.push(arg)
}
}
}
}
}
{ view, federation, paths, help, }
}
///|
/// Keep duplicate path arguments from treating one file as two federation members.
fn unique_paths(paths : Array[String]) -> Array[String] {
let unique : Array[String] = []
for path in paths {
if !unique.contains(path) {
unique.push(path)
}
}
unique
}
///|
fn main {
let args = parse_arguments(user_args(@env.args()))
let federation_paths = unique_paths(args.paths)
let code = if args.help {
println(usage())
0
} else if args.federation {
if federation_paths.length() < 2 {
println("error: --federation requires at least two IFC files")
println(usage())
2
} else {
inspect_federation(federation_paths, args.view)
}
} else if args.paths.length() > 1 {
println("error: expected at most one IFC file outside federation mode")
println(usage())
2
} else {
match args.paths.get(0) {
Some(file) => {
println("file: " + file)
inspect_file(file, args.view)
}
None => {
println("sample model (built-in)")
inspect(sample(), args.view)
}
}
}
if code != 0 {
@sys.exit(code)
}
}