///|
fn print_usage() -> Unit {
println(
"MoonEcoGuard - biodiversity data quality and sensitive coordinate protection",
)
println("")
println("Usage:")
println(" moon run cmd/main -- validate FILE [--policy policy.json]")
println(
" moon run cmd/main -- report FILE --format markdown [--policy policy.json]",
)
println(
" moon run cmd/main -- protect FILE --policy policy.json [--output public.csv]",
)
println(" moon run cmd/main -- convert FILE --format geojson")
println(" moon run cmd/main -- stats FILE [--policy policy.json]")
println(
" moon run cmd/main -- geo-check FILE --bounds minLat,minLon,maxLat,maxLon",
)
println(" moon run cmd/main -- diff private.csv public.csv")
println(" moon run cmd/main -- dwca-check DIRECTORY")
println(" moon run cmd/main -- dwca-convert DIRECTORY --format csv")
println("")
println(
"Note: coordinate protection reduces disclosure risk but does not replace expert review.",
)
}
///|
fn read_text(path : String) -> String? {
Some(@fs.read_file_to_string(path)) catch {
_ => None
}
}
///|
fn read_dataset(path : String) -> @darwincore.Dataset? {
match read_text(path) {
None => None
Some(text) =>
if path.to_lower().has_suffix(".tsv") {
Some(@ecodata.read_tsv_string(text))
} else {
Some(@ecodata.read_csv_string(text))
}
}
}
///|
fn arg_after(args : Array[String], flag : String) -> String? {
let mut index = 0
while index + 1 < args.length() {
if args[index] == flag {
return Some(args[index + 1])
}
index = index + 1
}
None
}
///|
fn parse_double_local(text : String) -> Double? {
let trimmed = text.trim()
if trimmed.is_empty() {
return None
}
let mut sign = 1.0
let mut value = 0.0
let mut factor = 0.1
let mut after_dot = false
let mut seen_digit = false
let mut index = 0
trimmed
.iter()
.each(ch => {
if index == 0 && ch == '-' {
sign = -1.0
} else if index == 0 && ch == '+' {
()
} else if ch == '.' {
after_dot = true
} else if ch >= '0' && ch <= '9' {
seen_digit = true
let digit = (ch.to_int() - '0'.to_int()).to_double()
if after_dot {
value = value + digit * factor
factor = factor / 10.0
} else {
value = value * 10.0 + digit
}
}
index = index + 1
})
if seen_digit {
Some(value * sign)
} else {
None
}
}
///|
fn parse_bounds(text : String) -> @geospatial.Bounds? {
let parts = text.split(",").to_array()
if parts.length() != 4 {
return None
}
match
(
parse_double_local(parts[0].to_owned()),
parse_double_local(parts[1].to_owned()),
parse_double_local(parts[2].to_owned()),
parse_double_local(parts[3].to_owned()),
) {
(
Some(min_latitude),
Some(min_longitude),
Some(max_latitude),
Some(max_longitude),
) => Some({ min_latitude, min_longitude, max_latitude, max_longitude })
_ => None
}
}
///|
fn read_policy(args : Array[String]) -> @privacy.Policy? {
match arg_after(args, "--policy") {
Some(path) =>
match read_text(path) {
Some(text) => Some(@privacy.policy_from_json(text))
None => None
}
None => Some(@privacy.default_policy())
}
}
///|
fn policy_error(args : Array[String]) -> String {
match arg_after(args, "--policy") {
Some(path) => "Unable to read policy file: " + path
None => "Unable to read policy file"
}
}
///|
fn rules_from_policy(
policy : @privacy.Policy,
bounds : @geospatial.Bounds?,
) -> @validation.ValidationRules {
{
required_occurrence_id: true,
required_scientific_name: true,
require_event_ids_to_exist: false,
sensitive_taxa: policy.sensitive_taxa,
max_individual_count: 10000,
coordinate_bounds: bounds,
}
}
///|
fn command_validate(
path : String,
args : Array[String],
bounds : @geospatial.Bounds?,
) -> Unit {
match read_dataset(path) {
None => println("Unable to read file: " + path)
Some(dataset) =>
match read_policy(args) {
None => println(policy_error(args))
Some(policy) => {
let report = @validation.validate(
dataset,
rules=rules_from_policy(policy, bounds),
)
println("MoonEcoGuard validation report")
println("Records: " + report.record_count.to_string())
println("Valid: " + report.valid_count.to_string())
println("Warning: " + report.warning_count.to_string())
println("Error: " + report.error_count.to_string())
for issue in report.diagnostics {
let severity = match issue.severity {
@validation.Severity::Info => "INFO"
@validation.Severity::Warning => "WARNING"
@validation.Severity::Error => "ERROR"
}
println(severity + " " + issue.code + " " + issue.message)
}
}
}
}
}
///|
fn command_report(path : String, args : Array[String]) -> Unit {
match arg_after(args, "--format") {
Some("markdown") =>
match read_dataset(path) {
None => println("Unable to read file: " + path)
Some(dataset) =>
match read_policy(args) {
None => println(policy_error(args))
Some(policy) => {
let report = @validation.validate(
dataset,
rules=rules_from_policy(policy, None),
)
println(@formats.validation_markdown(report))
}
}
}
Some(format) => println("Unsupported report format: " + format)
None => println("Missing report format; try --format markdown")
}
}
///|
fn print_protection_summary(summary : @privacy.ProtectionSummary) -> Unit {
println("Protection summary")
println("Records: " + summary.record_count.to_string())
println(
"Sensitive records: " + summary.sensitive_record_count.to_string(),
)
println(
"Coordinates changed: " + summary.coordinate_changed_count.to_string(),
)
println(
"Coordinates removed: " + summary.coordinate_removed_count.to_string(),
)
println(
"Observer contacts removed: " +
summary.observer_contact_removed_count.to_string(),
)
println(
"Remarks redacted: " + summary.remarks_redacted_count.to_string(),
)
}
///|
fn command_protect(path : String, args : Array[String]) -> Unit {
match read_dataset(path) {
None => println("Unable to read file: " + path)
Some(dataset) =>
match read_policy(args) {
None => println(policy_error(args))
Some(policy) => {
let result = @privacy.protect_with_summary(dataset, policy)
print_protection_summary(result.summary)
let output = @formats.occurrences_csv(result.dataset)
match arg_after(args, "--output") {
Some(out_path) => {
let ok = try {
@fs.write_string_to_file(out_path, output)
true
} catch {
_ => false
}
if ok {
println("Wrote protected CSV: " + out_path)
} else {
println("Unable to write protected CSV: " + out_path)
}
}
None => println(output)
}
}
}
}
}
///|
fn command_convert(path : String, args : Array[String]) -> Unit {
match read_dataset(path) {
None => println("Unable to read file: " + path)
Some(dataset) =>
match arg_after(args, "--format") {
Some("geojson") => println(@formats.to_geojson(dataset))
Some("json") => println(@formats.to_json(dataset))
_ => println("Unsupported format; try --format geojson")
}
}
}
///|
fn command_stats(path : String, args : Array[String]) -> Unit {
match read_dataset(path) {
None => println("Unable to read file: " + path)
Some(dataset) =>
match read_policy(args) {
None => println(policy_error(args))
Some(policy) =>
println(
@formats.stats_markdown(
@statistics.summarize(
dataset,
sensitive_taxa=policy.sensitive_taxa,
),
),
)
}
}
}
///|
fn read_dwca_dataset(path : String) -> @darwincore.Dataset? {
let meta_path = path + "/meta.xml"
match read_text(meta_path) {
None => None
Some(meta_xml) => {
let manifest = @dwca.parse_meta_xml(meta_xml)
match manifest.core {
Some(core) if !core.location.is_empty() =>
match read_text(path + "/" + core.location) {
Some(source) => @dwca.parse_occurrence_text(manifest, source)
None => None
}
_ => None
}
}
}
}
///|
fn command_dwca_check(path : String) -> Unit {
match read_text(path + "/meta.xml") {
None => println("Unable to read DwCA meta.xml: " + path)
Some(meta_xml) => {
let manifest = @dwca.parse_meta_xml(meta_xml)
match manifest.core {
None => println("DwCA manifest has no supported core: " + path)
Some(core) if core.location.is_empty() =>
println("DwCA core has no location: " + path)
Some(core) =>
match read_dwca_dataset(path) {
None => println("Unable to read DwCA core file: " + core.location)
Some(dataset) => {
println("MoonEcoGuard DwCA directory check")
println("Core row type: " + core.row_type)
println("Core location: " + core.location)
println("Core ID field: " + core.id_field)
println("Mapped fields: " + core.fields.length().to_string())
println(
"Occurrence records: " +
dataset.occurrences.length().to_string(),
)
}
}
}
}
}
}
///|
fn command_dwca_convert(path : String, args : Array[String]) -> Unit {
match arg_after(args, "--format") {
Some("csv") =>
match read_dwca_dataset(path) {
None => println("Unable to read DwCA directory: " + path)
Some(dataset) => println(@formats.occurrences_csv(dataset))
}
Some(format) => println("Unsupported DwCA format: " + format)
None => println("Missing DwCA format; try --format csv")
}
}
///|
fn command_diff(private_path : String, public_path : String) -> Unit {
match (read_dataset(private_path), read_dataset(public_path)) {
(Some(private_dataset), Some(public_dataset)) => {
let result = @privacy.diff(private_dataset, public_dataset)
println(
"Private records: " +
result.private_record_count.to_string(),
)
println(
"Public records: " +
result.public_record_count.to_string(),
)
println(
"Matched records: " + result.matched_count.to_string(),
)
println("Added records: " + result.added_count.to_string())
println(
"Removed records: " + result.removed_count.to_string(),
)
println(
"Coordinates changed: " +
result.coordinate_changed_count.to_string(),
)
println(
"Coordinates removed: " +
result.coordinate_removed_count.to_string(),
)
println(
"Observer contacts changed: " +
result.observer_contact_changed_count.to_string(),
)
println(
"Remarks changed: " +
result.remarks_changed_count.to_string(),
)
}
_ => println("Unable to read one or both files")
}
}
///|
fn main {
let args = @env.args()
if args.length() < 2 {
print_usage()
return
}
match args[1] {
"validate" if args.length() >= 3 => command_validate(args[2], args, None)
"report" if args.length() >= 3 => command_report(args[2], args)
"protect" if args.length() >= 3 => command_protect(args[2], args)
"convert" if args.length() >= 3 => command_convert(args[2], args)
"stats" if args.length() >= 3 => command_stats(args[2], args)
"geo-check" if args.length() >= 3 =>
match arg_after(args, "--bounds") {
None => println("Missing bounds; expected minLat,minLon,maxLat,maxLon")
Some(value) =>
match parse_bounds(value) {
None => println("Invalid bounds: " + value)
Some(bounds) => command_validate(args[2], args, Some(bounds))
}
}
"diff" if args.length() >= 4 => command_diff(args[2], args[3])
"dwca-check" if args.length() >= 3 => command_dwca_check(args[2])
"dwca-convert" if args.length() >= 3 => command_dwca_convert(args[2], args)
_ => print_usage()
}
}