diff --git a/build.sbt b/build.sbt index effa69e3..0f5b46c0 100644 --- a/build.sbt +++ b/build.sbt @@ -22,7 +22,7 @@ lazy val core = project "org.slf4j" % "slf4j-api" % slf4jVersion, // for doc/help - "com.massisframework" % "j-text-utils" % "0.3.4", + "de.vandermeer" % "asciitable" % "0.3.2", "net.oneandone.reflections8" % "reflections8" % "0.11.7", ) ) diff --git a/core/src/main/scala/flatgraph/help/Table.scala b/core/src/main/scala/flatgraph/help/Table.scala index cfb1250d..9aef4db0 100644 --- a/core/src/main/scala/flatgraph/help/Table.scala +++ b/core/src/main/scala/flatgraph/help/Table.scala @@ -1,21 +1,43 @@ package flatgraph.help -import dnl.utils.text.table.TextTable -import java.io.{ByteArrayOutputStream, PrintStream} -import java.nio.charset.StandardCharsets -import scala.util.Using - -case class Table(columnNames: Iterable[String], rows: Iterable[Iterable[String]]) { - - lazy val render: String = { - Using.Manager { use => - val charset = StandardCharsets.UTF_8 - val baos = use(new ByteArrayOutputStream) - val ps = use(new PrintStream(baos, true, charset.name)) - val rowsAsArray = rows.map(_.map(_ + " ").toArray.asInstanceOf[Array[Object]]).toArray - new TextTable(columnNames.toArray, rowsAsArray).printTable(ps, 0) - new String(baos.toByteArray, charset) - }.get +import de.vandermeer.asciitable.AsciiTable +import de.vandermeer.asciithemes.TA_GridThemes +import de.vandermeer.skb.interfaces.transformers.textformat.TextAlignment +import scala.jdk.CollectionConverters.SeqHasAsJava + +import Table.* + +case class Table(columnNames: Seq[String], rows: Seq[Seq[String]]) { + + def render(implicit availableWidthProvider: AvailableWidthProvider): String = { + if (columnNames.isEmpty && rows.isEmpty) { + "" + } else { + val table = new AsciiTable() + table.addRule() + table.addRow(columnNames.asJava) + table.addRule() + if (rows.nonEmpty) { + rows.map(_.asJava).foreach(table.addRow) + } + table.addRule() + table.getContext.setGridTheme(TA_GridThemes.FULL) + table.setTextAlignment(TextAlignment.LEFT) + + // some terminal emulators (e.g. on github actions CI) report to have a width of 0... + // that doesn't work for rendering a table, so we compensate by using a minimum width + val renderingWidth = math.max(availableWidthProvider.apply(), 60) + table.render(renderingWidth) + } + } + +} + +object Table { + trait AvailableWidthProvider extends (() => Int) + + class ConstantWidth(width: Int) extends AvailableWidthProvider { + override def apply() = width } } diff --git a/core/src/main/scala/flatgraph/help/TraversalHelp.scala b/core/src/main/scala/flatgraph/help/TraversalHelp.scala index 2abf1f9a..e721d62f 100644 --- a/core/src/main/scala/flatgraph/help/TraversalHelp.scala +++ b/core/src/main/scala/flatgraph/help/TraversalHelp.scala @@ -3,6 +3,7 @@ package flatgraph.help import flatgraph.GNode import flatgraph.help import flatgraph.help.DocFinder.StepDoc +import flatgraph.help.Table.AvailableWidthProvider import java.lang.annotation.Annotation as JAnnotation import org.reflections8.Reflections @@ -21,7 +22,7 @@ import scala.jdk.CollectionConverters.* class TraversalHelp(packageNamesToSearch: DocSearchPackages) { import TraversalHelp._ - def forElementSpecificSteps(elementClass: Class[_], verbose: Boolean): String = { + def forElementSpecificSteps(elementClass: Class[_], verbose: Boolean)(implicit availableWidthProvider: AvailableWidthProvider): String = { val isNode = classOf[GNode].isAssignableFrom(elementClass) val stepDocs = { @@ -51,7 +52,7 @@ class TraversalHelp(packageNamesToSearch: DocSearchPackages) { |""".stripMargin } - def forTraversalSources(verbose: Boolean): String = { + def forTraversalSources(verbose: Boolean)(implicit availableWidthProvider: AvailableWidthProvider): String = { val stepDocs = for { packageName <- packageNamesToSearch() traversal <- findClassesAnnotatedWith(packageName, classOf[help.TraversalSource]) @@ -106,5 +107,5 @@ class TraversalHelp(packageNamesToSearch: DocSearchPackages) { } object TraversalHelp { - private val ColumnNames = Array("step", "description") + private val ColumnNames = Seq("step", "description") } diff --git a/core/src/main/scala/flatgraph/traversal/Language.scala b/core/src/main/scala/flatgraph/traversal/Language.scala index fdbd417e..621d2e81 100644 --- a/core/src/main/scala/flatgraph/traversal/Language.scala +++ b/core/src/main/scala/flatgraph/traversal/Language.scala @@ -1,6 +1,7 @@ package flatgraph.traversal import flatgraph.help.{Doc, DocSearchPackages, Traversal, TraversalHelp} +import flatgraph.help.Table.AvailableWidthProvider import flatgraph.{Accessors, Edge, GNode, MultiPropertyKey, OptionalPropertyKey, PropertyKey, Schema, SinglePropertyKey} import scala.annotation.implicitNotFound @@ -37,7 +38,11 @@ class GenericSteps[A](iterator: Iterator[A]) extends AnyVal { |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` |""".stripMargin) - def help[B >: A](implicit elementType: ClassTag[B], searchPackages: DocSearchPackages): String = + def help[B >: A](implicit + elementType: ClassTag[B], + searchPackages: DocSearchPackages, + availableWidthProvider: AvailableWidthProvider + ): String = new TraversalHelp(searchPackages).forElementSpecificSteps(elementType.runtimeClass, verbose = false) @Doc(info = "print verbose help/documentation based on the current elementType `A`.") @@ -46,7 +51,11 @@ class GenericSteps[A](iterator: Iterator[A]) extends AnyVal { |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` |""".stripMargin) - def helpVerbose[B >: A](implicit elementType: ClassTag[B], searchPackages: DocSearchPackages): String = + def helpVerbose[B >: A](implicit + elementType: ClassTag[B], + searchPackages: DocSearchPackages, + availableWidthProvider: AvailableWidthProvider + ): String = new TraversalHelp(searchPackages).forElementSpecificSteps(elementType.runtimeClass, verbose = true) /** Execute the traversal and convert the result to a list - shorthand for `toList` */ diff --git a/core/src/test/scala/flatgraph/help/TableTests.scala b/core/src/test/scala/flatgraph/help/TableTests.scala new file mode 100644 index 00000000..e6901689 --- /dev/null +++ b/core/src/test/scala/flatgraph/help/TableTests.scala @@ -0,0 +1,60 @@ +package flatgraph.help + +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.wordspec.AnyWordSpec +import flatgraph.help.Table.AvailableWidthProvider + +class TableTests extends AnyWordSpec { + + "render a nice generic table" in { + val table = Table(Seq("column a", "column b"), Seq(Seq("abc 1", "bde 1"), Seq("abc 2", "bde 2"))) + + implicit val availableWidthProvider: AvailableWidthProvider = new Table.ConstantWidth(100) + table.render.trim shouldBe + """┌─────────────────────────────────────────────────┬────────────────────────────────────────────────┐ + |│column a │column b │ + |├─────────────────────────────────────────────────┼────────────────────────────────────────────────┤ + |│abc 1 │bde 1 │ + |│abc 2 │bde 2 │ + |└─────────────────────────────────────────────────┴────────────────────────────────────────────────┘ + |""".stripMargin.trim + } + + "adapt to dynamically changing terminal width" in { + val table = Table( + Seq("lorem ipsum"), + Seq( + Seq( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et" + + " dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " + + "ex ea commodo consequat." + ) + ) + ) + + var currentTerminalWidth = 80 // think "looking up current value from an actual terminal" + implicit val availableWidthProvider: AvailableWidthProvider = () => currentTerminalWidth + + table.render.trim shouldBe + """┌──────────────────────────────────────────────────────────────────────────────┐ + |│lorem ipsum │ + |├──────────────────────────────────────────────────────────────────────────────┤ + |│Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor│ + |│incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis │ + |│nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. │ + |└──────────────────────────────────────────────────────────────────────────────┘ + |""".stripMargin.trim + + currentTerminalWidth = 100 // emulating: terminal size has changed + table.render.trim shouldBe + """┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ + |│lorem ipsum │ + |├──────────────────────────────────────────────────────────────────────────────────────────────────┤ + |│Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut │ + |│labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris │ + |│nisi ut aliquip ex ea commodo consequat. │ + |└──────────────────────────────────────────────────────────────────────────────────────────────────┘ + |""".stripMargin.trim + } + +} diff --git a/core/src/test/scala/flatgraph/traversal/TraversalTests.scala b/core/src/test/scala/flatgraph/traversal/TraversalTests.scala index 057ce94f..4f98e425 100644 --- a/core/src/test/scala/flatgraph/traversal/TraversalTests.scala +++ b/core/src/test/scala/flatgraph/traversal/TraversalTests.scala @@ -2,7 +2,8 @@ package flatgraph.traversal import flatgraph.Implicits.start import flatgraph.GNode -import flatgraph.help.DocSearchPackages +import flatgraph.help.{DocSearchPackages, Table} +import flatgraph.help.Table.AvailableWidthProvider import flatgraph.traversal.Language.* import flatgraph.traversal.testdomains.simple.SimpleDomain.Thing import flatgraph.traversal.testdomains.simple.{ExampleGraphSetup, SimpleDomain} @@ -88,18 +89,20 @@ class TraversalTests extends AnyWordSpec with ExampleGraphSetup { ".help step" should { // a specific domain would provide it's own DocSearchPackage implementation, to specify where we're supposed to scan for @Doc annotations - given DocSearchPackages = DocSearchPackages.default + given DocSearchPackages = DocSearchPackages.default + given AvailableWidthProvider = new Table.ConstantWidth(120) + "generic help for `int`" in { val helpText = Iterator(1, 2, 3, 4).help helpText should include(".cast") helpText should include("casts all elements to given type") helpText should include(".whereNot") - helpText should include(" only preserves elements if the provided traversal does _not_ have any results") + helpText should include("only preserves elements if the provided traversal does") val helpTextVerbose = Iterator(1, 2, 3, 4).helpVerbose helpTextVerbose should include(".cast") helpTextVerbose should include(".whereNot") - helpTextVerbose should include("""flatgraph.traversal.GenericSteps""") // should contain the location of the step definition... + helpTextVerbose should include("""flatgraph.traversal.GenericSt""") // should contain the location of the step definition... } "help for nodes" in { @@ -114,8 +117,8 @@ class TraversalTests extends AnyWordSpec with ExampleGraphSetup { helpTextVerbose should include(".label") helpTextVerbose should include(".property") helpTextVerbose should include(".cast") - helpTextVerbose should include("""flatgraph.traversal.GenericSteps""") // should contain the location of the step definition... - helpTextVerbose should include("""flatgraph.traversal.NodeSteps""") // should contain the location of the step definition... + helpTextVerbose should include("""flatgraph.traversal.GenericSt""") // should contain the location of the step definition... + helpTextVerbose should include("""flatgraph.traversal.NodeSteps""") // should contain the location of the step definition... } "give a domain overview" in { @@ -142,11 +145,11 @@ class TraversalTests extends AnyWordSpec with ExampleGraphSetup { val thingTraversalHelpVerbose = thingTraversal.helpVerbose thingTraversalHelpVerbose should include("name of the Thing") - thingTraversalHelpVerbose should include("testdomains.simple.SimpleDomainTraversal") + thingTraversalHelpVerbose should include("simple.SimpleDomainTravers") thingTraversalHelpVerbose should include("node label") thingTraversalHelpVerbose should include("flatgraph.traversal.NodeSteps") thingTraversalHelpVerbose should include("result to a list") - thingTraversalHelpVerbose should include("flatgraph.traversal.GenericSteps") + thingTraversalHelpVerbose should include("flatgraph.traversal.GenericSt") } } diff --git a/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala b/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala index 20a72bdd..5649b27f 100644 --- a/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala +++ b/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala @@ -1,5 +1,6 @@ package flatgraph.traversal.testdomains.simple +import flatgraph.help.Table.AvailableWidthProvider import flatgraph.{DiffGraphApplier, DiffGraphBuilder, GNode, GenericDNode, Graph, TestSchema} import flatgraph.help.{Doc, DocSearchPackages, Traversal, TraversalHelp, TraversalSource} import flatgraph.traversal.testdomains.simple.SimpleDomain.Thing @@ -58,8 +59,10 @@ object SimpleDomain { } val defaultDocSearchPackage: DocSearchPackages = DocSearchPackages(getClass.getPackage.getName) - lazy val help = TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = false) - lazy val helpVerbose = TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = true) + def help(using AvailableWidthProvider) = + TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = false) + def helpVerbose(using AvailableWidthProvider) = + TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = true) def newGraph: Graph = { val schema = TestSchema.make(1, 1)