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))) + } + ) + +}