From 1f64fee8a104b76a5ed8627756cd93e47e6148ac Mon Sep 17 00:00:00 2001 From: Jim Balhoff Date: Wed, 20 May 2026 21:53:13 -0400 Subject: [PATCH] Fix docs data-preview: column order and label linkification Read TSV headers in file order via allWithOrderedHeaders instead of deriving them from Map keys (hash order), so the docs preview table matches the source file. Only linkify entity-valued columns (vars, list_vars, defined_class); free-text columns whose values may contain a colon were being misparsed as CURIEs and rendered as bogus OBO links. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../monarchinitiative/dosdp/cli/Docs.scala | 15 +++++-- .../dosdp/cli/Generate.scala | 13 ++---- .../dosdp/cli/FormatDataRowTest.scala | 44 +++++++++++++++++++ .../dosdp/cli/ReadFillersTest.scala | 37 ++++++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/test/scala/org/monarchinitiative/dosdp/cli/FormatDataRowTest.scala create mode 100644 src/test/scala/org/monarchinitiative/dosdp/cli/ReadFillersTest.scala diff --git a/src/main/scala/org/monarchinitiative/dosdp/cli/Docs.scala b/src/main/scala/org/monarchinitiative/dosdp/cli/Docs.scala index e0412f6..21da81f 100644 --- a/src/main/scala/org/monarchinitiative/dosdp/cli/Docs.scala +++ b/src/main/scala/org/monarchinitiative/dosdp/cli/Docs.scala @@ -58,7 +58,8 @@ object Docs { axioms <- Generate.renderPattern(dosdp, prefixes, fillers, Some(ontology), true, true, None, false, OboInOwlSource, false, Map(RDFSLabel.getIRI -> variableReadableIdentifiers)) patternIRI = IRI.create(iri) docAxioms = findDocAxioms(patternIRI, axioms, target, config.dataLocationPrefix) - data = columns.to(List) :: rows.take(5).map(formatDataRow(_, columns.to(List), prefixes)) ::: Nil + entityColumns = dosdp.vars.getOrElse(Map.empty).keySet ++ dosdp.list_vars.getOrElse(Map.empty).keySet + DOSDP.DefinedClassVariable + data = columns.to(List) :: rows.take(5).map(formatDataRow(_, columns.to(List), entityColumns, prefixes)) ::: Nil markdown <- DocsMarkdown.markdown(compiled, docAxioms, renderer, data) _ <- ZIO.attemptBlockingIO(new PrintWriter(target.outputFile, "utf-8")).acquireReleaseWithAuto { writer => ZIO.attemptBlockingIO(writer.print(markdown)) @@ -97,8 +98,16 @@ object Docs { } //TODO better handling for list values? - private def formatDataRow(row: Map[String, String], columns: List[String], prefixes: PartialFunction[String, String]): List[String] = - columns.map(c => row.getOrElse(c, "")).map(v => Prefixes.idToIRI(v, prefixes).map(iri => s"[$v]($iri)").getOrElse(v)) + // Only entity-valued columns (pattern vars/list_vars and defined_class) are + // linkified. Free-text columns (labels, data_vars) can legitimately contain a + // colon, which would otherwise be misparsed as a CURIE and turned into a bogus + // OBO link. + private[cli] def formatDataRow(row: Map[String, String], columns: List[String], entityColumns: Set[String], prefixes: PartialFunction[String, String]): List[String] = + columns.map { c => + val v = row.getOrElse(c, "") + if (entityColumns(c)) Prefixes.idToIRI(v, prefixes).map(iri => s"[$v]($iri)").getOrElse(v) + else v + } private final case class DocsTarget(templateFile: String, inputFile: String, outputFile: String) { diff --git a/src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala b/src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala index 7163d34..b5fb565 100644 --- a/src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala +++ b/src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala @@ -113,16 +113,11 @@ object Generate { cleaned <- ZIO.attemptBlockingIO(Source.fromFile(file, StandardCharsets.UTF_8.name())).acquireReleaseWithAuto { source => ZIO.attemptBlockingIO(source.getLines().filterNot(_.trim.isEmpty).mkString("\n")) }.flatMapError(e => logError("Unable to read input table", e)) - columns <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader => - ZIO.succeed { - val iteratorToCheckColumns = reader.iteratorWithHeaders - if (iteratorToCheckColumns.hasNext) iteratorToCheckColumns.next().keys.to(Seq) else Seq.empty[String] - } - } - data <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader => - ZIO.succeed(reader.iteratorWithHeaders.toList) + columnsAndData <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader => + ZIO.succeed(reader.allWithOrderedHeaders()) } - } yield columns -> data + (columns, data) = columnsAndData + } yield columns.to(Seq) -> data def axiomsOutputChoice(kind: AxiomKind): (Boolean, Boolean) = kind match { case AllAxioms => (true, true) diff --git a/src/test/scala/org/monarchinitiative/dosdp/cli/FormatDataRowTest.scala b/src/test/scala/org/monarchinitiative/dosdp/cli/FormatDataRowTest.scala new file mode 100644 index 0000000..5f9804c --- /dev/null +++ b/src/test/scala/org/monarchinitiative/dosdp/cli/FormatDataRowTest.scala @@ -0,0 +1,44 @@ +package org.monarchinitiative.dosdp.cli + +import org.monarchinitiative.dosdp.{DOSDP, OBOPrefixes} +import zio.test.Assertion._ +import zio.test._ + +object FormatDataRowTest extends ZIOSpecDefault { + + private val columns = List("input1", "input1_label", "defined_class", "defined_class_label") + private val entityColumns = Set("input1", DOSDP.DefinedClassVariable) + + def spec = suite("Docs.formatDataRow")( + test("entity columns are linkified") { + val row = Map("input1" -> "CHEBI:15378", DOSDP.DefinedClassVariable -> "GO:0015649") + val out = Docs.formatDataRow(row, columns, entityColumns, OBOPrefixes) + assert(out)(equalTo(List( + "[CHEBI:15378](http://purl.obolibrary.org/obo/CHEBI_15378)", + "", + "[GO:0015649](http://purl.obolibrary.org/obo/GO_0015649)", + "" + ))) + }, + test("label columns containing a colon are not misparsed as CURIEs") { + val row = Map( + "input1" -> "CHEBI:15378", + "input1_label" -> "hydron", + DOSDP.DefinedClassVariable -> "GO:0015649", + "defined_class_label" -> "2-keto-3-deoxygluconate:proton symporter activity" + ) + val out = Docs.formatDataRow(row, columns, entityColumns, OBOPrefixes) + assert(out)(equalTo(List( + "[CHEBI:15378](http://purl.obolibrary.org/obo/CHEBI_15378)", + "hydron", + "[GO:0015649](http://purl.obolibrary.org/obo/GO_0015649)", + "2-keto-3-deoxygluconate:proton symporter activity" + ))) + }, + test("missing cells render as empty strings") { + val out = Docs.formatDataRow(Map.empty, columns, entityColumns, OBOPrefixes) + assert(out)(equalTo(List("", "", "", ""))) + } + ) + +} \ No newline at end of file diff --git a/src/test/scala/org/monarchinitiative/dosdp/cli/ReadFillersTest.scala b/src/test/scala/org/monarchinitiative/dosdp/cli/ReadFillersTest.scala new file mode 100644 index 0000000..2ab6b25 --- /dev/null +++ b/src/test/scala/org/monarchinitiative/dosdp/cli/ReadFillersTest.scala @@ -0,0 +1,37 @@ +package org.monarchinitiative.dosdp.cli + +import com.github.tototoshi.csv.TSVFormat +import zio.test.Assertion._ +import zio.test._ +import zio.{Console => _, _} + +import java.io.{File, PrintWriter} +import java.nio.file.Files + +object ReadFillersTest extends ZIOSpecDefault { + + private val header = List("input2", "input1", "input1_label", "input2_label", "defined_class", "defined_class_label") + + private def writeTsv(rows: List[List[String]]): Task[File] = + ZIO.attempt { + val f = Files.createTempFile("fillers-", ".tsv").toFile + f.deleteOnExit() + val w = new PrintWriter(f, "utf-8") + try rows.foreach(r => w.println(r.mkString("\t"))) + finally w.close() + f + } + + def spec = suite("Generate.readFillers")( + test("columns are returned in file order") { + val dataRow = List("CHEBI:1", "CHEBI:2", "alpha", "beta", "GO:1", "alpha:beta activity") + for { + file <- writeTsv(List(header, dataRow)) + result <- Generate.readFillers(file, new TSVFormat {}) + (columns, rows) = result + } yield assert(columns.toList)(equalTo(header)) && + assert(rows)(equalTo(List(header.zip(dataRow).toMap))) + } + ) + +}