Skip to content

Commit a2d7644

Browse files
balhoffclaude
andauthored
Fix docs data-preview: column order and label linkification (#530)
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) <noreply@anthropic.com>
1 parent 3a796bb commit a2d7644

4 files changed

Lines changed: 97 additions & 12 deletions

File tree

src/main/scala/org/monarchinitiative/dosdp/cli/Docs.scala

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ object Docs {
5858
axioms <- Generate.renderPattern(dosdp, prefixes, fillers, Some(ontology), true, true, None, false, OboInOwlSource, false, Map(RDFSLabel.getIRI -> variableReadableIdentifiers))
5959
patternIRI = IRI.create(iri)
6060
docAxioms = findDocAxioms(patternIRI, axioms, target, config.dataLocationPrefix)
61-
data = columns.to(List) :: rows.take(5).map(formatDataRow(_, columns.to(List), prefixes)) ::: Nil
61+
entityColumns = dosdp.vars.getOrElse(Map.empty).keySet ++ dosdp.list_vars.getOrElse(Map.empty).keySet + DOSDP.DefinedClassVariable
62+
data = columns.to(List) :: rows.take(5).map(formatDataRow(_, columns.to(List), entityColumns, prefixes)) ::: Nil
6263
markdown <- DocsMarkdown.markdown(compiled, docAxioms, renderer, data)
6364
_ <- ZIO.attemptBlockingIO(new PrintWriter(target.outputFile, "utf-8")).acquireReleaseWithAuto { writer =>
6465
ZIO.attemptBlockingIO(writer.print(markdown))
@@ -97,8 +98,16 @@ object Docs {
9798
}
9899

99100
//TODO better handling for list values?
100-
private def formatDataRow(row: Map[String, String], columns: List[String], prefixes: PartialFunction[String, String]): List[String] =
101-
columns.map(c => row.getOrElse(c, "")).map(v => Prefixes.idToIRI(v, prefixes).map(iri => s"[$v]($iri)").getOrElse(v))
101+
// Only entity-valued columns (pattern vars/list_vars and defined_class) are
102+
// linkified. Free-text columns (labels, data_vars) can legitimately contain a
103+
// colon, which would otherwise be misparsed as a CURIE and turned into a bogus
104+
// OBO link.
105+
private[cli] def formatDataRow(row: Map[String, String], columns: List[String], entityColumns: Set[String], prefixes: PartialFunction[String, String]): List[String] =
106+
columns.map { c =>
107+
val v = row.getOrElse(c, "")
108+
if (entityColumns(c)) Prefixes.idToIRI(v, prefixes).map(iri => s"[$v]($iri)").getOrElse(v)
109+
else v
110+
}
102111

103112
private final case class DocsTarget(templateFile: String, inputFile: String, outputFile: String) {
104113

src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,11 @@ object Generate {
113113
cleaned <- ZIO.attemptBlockingIO(Source.fromFile(file, StandardCharsets.UTF_8.name())).acquireReleaseWithAuto { source =>
114114
ZIO.attemptBlockingIO(source.getLines().filterNot(_.trim.isEmpty).mkString("\n"))
115115
}.flatMapError(e => logError("Unable to read input table", e))
116-
columns <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader =>
117-
ZIO.succeed {
118-
val iteratorToCheckColumns = reader.iteratorWithHeaders
119-
if (iteratorToCheckColumns.hasNext) iteratorToCheckColumns.next().keys.to(Seq) else Seq.empty[String]
120-
}
121-
}
122-
data <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader =>
123-
ZIO.succeed(reader.iteratorWithHeaders.toList)
116+
columnsAndData <- ZIO.succeed(CSVReader.open(new StringReader(cleaned))(sepFormat)).acquireReleaseWithAuto { reader =>
117+
ZIO.succeed(reader.allWithOrderedHeaders())
124118
}
125-
} yield columns -> data
119+
(columns, data) = columnsAndData
120+
} yield columns.to(Seq) -> data
126121

127122
def axiomsOutputChoice(kind: AxiomKind): (Boolean, Boolean) = kind match {
128123
case AllAxioms => (true, true)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.monarchinitiative.dosdp.cli
2+
3+
import org.monarchinitiative.dosdp.{DOSDP, OBOPrefixes}
4+
import zio.test.Assertion._
5+
import zio.test._
6+
7+
object FormatDataRowTest extends ZIOSpecDefault {
8+
9+
private val columns = List("input1", "input1_label", "defined_class", "defined_class_label")
10+
private val entityColumns = Set("input1", DOSDP.DefinedClassVariable)
11+
12+
def spec = suite("Docs.formatDataRow")(
13+
test("entity columns are linkified") {
14+
val row = Map("input1" -> "CHEBI:15378", DOSDP.DefinedClassVariable -> "GO:0015649")
15+
val out = Docs.formatDataRow(row, columns, entityColumns, OBOPrefixes)
16+
assert(out)(equalTo(List(
17+
"[CHEBI:15378](http://purl.obolibrary.org/obo/CHEBI_15378)",
18+
"",
19+
"[GO:0015649](http://purl.obolibrary.org/obo/GO_0015649)",
20+
""
21+
)))
22+
},
23+
test("label columns containing a colon are not misparsed as CURIEs") {
24+
val row = Map(
25+
"input1" -> "CHEBI:15378",
26+
"input1_label" -> "hydron",
27+
DOSDP.DefinedClassVariable -> "GO:0015649",
28+
"defined_class_label" -> "2-keto-3-deoxygluconate:proton symporter activity"
29+
)
30+
val out = Docs.formatDataRow(row, columns, entityColumns, OBOPrefixes)
31+
assert(out)(equalTo(List(
32+
"[CHEBI:15378](http://purl.obolibrary.org/obo/CHEBI_15378)",
33+
"hydron",
34+
"[GO:0015649](http://purl.obolibrary.org/obo/GO_0015649)",
35+
"2-keto-3-deoxygluconate:proton symporter activity"
36+
)))
37+
},
38+
test("missing cells render as empty strings") {
39+
val out = Docs.formatDataRow(Map.empty, columns, entityColumns, OBOPrefixes)
40+
assert(out)(equalTo(List("", "", "", "")))
41+
}
42+
)
43+
44+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.monarchinitiative.dosdp.cli
2+
3+
import com.github.tototoshi.csv.TSVFormat
4+
import zio.test.Assertion._
5+
import zio.test._
6+
import zio.{Console => _, _}
7+
8+
import java.io.{File, PrintWriter}
9+
import java.nio.file.Files
10+
11+
object ReadFillersTest extends ZIOSpecDefault {
12+
13+
private val header = List("input2", "input1", "input1_label", "input2_label", "defined_class", "defined_class_label")
14+
15+
private def writeTsv(rows: List[List[String]]): Task[File] =
16+
ZIO.attempt {
17+
val f = Files.createTempFile("fillers-", ".tsv").toFile
18+
f.deleteOnExit()
19+
val w = new PrintWriter(f, "utf-8")
20+
try rows.foreach(r => w.println(r.mkString("\t")))
21+
finally w.close()
22+
f
23+
}
24+
25+
def spec = suite("Generate.readFillers")(
26+
test("columns are returned in file order") {
27+
val dataRow = List("CHEBI:1", "CHEBI:2", "alpha", "beta", "GO:1", "alpha:beta activity")
28+
for {
29+
file <- writeTsv(List(header, dataRow))
30+
result <- Generate.readFillers(file, new TSVFormat {})
31+
(columns, rows) = result
32+
} yield assert(columns.toList)(equalTo(header)) &&
33+
assert(rows)(equalTo(List(header.zip(dataRow).toMap)))
34+
}
35+
)
36+
37+
}

0 commit comments

Comments
 (0)