diff --git a/README.md b/README.md index bece5b0..1362aa2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ together with something like, for instance, a Json writer. # User Guide -Current version: 1.0.4. +Current version: 1.0.5. See release notes below for history. @@ -120,6 +120,7 @@ The type _Row_ defines the specific row type (for example, _Movie_, in the examp _hasHeader_ is used to define if there is a header row in the first line of the file (or sequence of strings) to be parsed. _forgiving_, which defaults to _false_, can be set to _true_ if you expect that some rows will not parse, but where this will not invalidate your dataset as a whole. + In forgiving mode, any exceptions thrown in the parsing of a row are collected and then printed to _System.err_ at the conclusion of the parsing of the table. _rowParser_ is the specific parser for the _Row_ type (see below). _builder_ is used by the _parse_ method. @@ -321,7 +322,38 @@ Also, note that the instance of _ColumnHelper_ defined here has the formatter de Rendering ========= -TableParser provides a mechanism for rendering a table to a hierarchical (i.e. tree-structured) output. +_TableParser_ provides two mechanisms for rendering a table: +* one to a straight serialized output, for example, when rendering a table as a CSV file. +* the other to a hierarchical (i.e. tree-structured) output, such as an HTML file. + +## Non-hierarchical output + +For this type of output, the application programmer must provide an instance of _Writer[O]_ where is, for example a _StringBuilder_, +_BufferedOutput_, or perhaps an I/O Monad. + +The non-hierarchical output does not support the same customization of renderings as does the hierarchical output. +It's intended more as a straight, quick-and-dirty output mechanism to a CSV file. + +Here, for example, is an appropriate definition. + + implicit object StringBuilderWriteable extends Writable[StringBuilder] { + override def writeRaw(o: StringBuilder)(x: CharSequence): StringBuilder = o.append(x.toString) + override def unit: StringBuilder = new StringBuilder + override def delimiter: CharSequence = "|" + } + +The default _delimiter_ is ", ". +You can override the _newline_ and _quote_ methods too if you don't want the defaults. + +And then, folllowing this, you will write something like the following code: + + print(table.render.toString) + +The _Writable_ object will take care of inserting the delimiter and quotes as appropriate. +Columns will appear in the same order as the parameters of _Row_ type (which must be either a _Product_, such as a case class, or an _Array_ or a _Seq_). +If you need to change the order of the rows, you will need to override the _writeRow_ method of _Writable_. + +## Hierarchical output A type class called _TreeWriter_ is the main type for rendering. One of the instance methods of _Table[Row]_ is a method as follows: @@ -378,6 +410,9 @@ If you need to set HTML attributes for a specific type, for example a row in the Release Notes ============= +V1.0.4 -> V1.0.5 +* Added a convenient way of rendering a table as a non-hierarchical structure. In other words, serialization to a CSV file. + V1.0.3 -> V1.0.4 * Added the ability to add header row and header column for tables (NOTE: not finalized yet, but functional). @@ -396,7 +431,7 @@ V1.0.0 -> V.1.0.1 * Fixed Issue #1; * Added parsing of Seq[Seq[String]]; * Added cellParserRepetition; -* Implemented closing of Source in Table.parse methods; -* Added encoding parameters to Table.parse methods. +* Implemented closing of Source in _Table.parse_ methods; +* Added encoding parameters to _Table.parse_ methods. diff --git a/build.sbt b/build.sbt index 614f9aa..192d8ff 100755 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ organization := "com.phasmidsoftware" name := "TableParser" -version := "1.0.4" +version := "1.0.5" scalaVersion := "2.12.6" diff --git a/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala b/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala index 14b497e..eb228b2 100644 --- a/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala +++ b/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala @@ -103,10 +103,7 @@ trait CellParsers { construct(p1V) } - // TODO We need to allow for a single-parameter conversion of String => T via P1 - // This fixes issue #1 override def convertString(w: String): T = construct(implicitly[CellParser[P1]].convertString(w)) - } } diff --git a/src/main/scala/com/phasmidsoftware/parse/RowParser.scala b/src/main/scala/com/phasmidsoftware/parse/RowParser.scala index ac47e24..b239b42 100644 --- a/src/main/scala/com/phasmidsoftware/parse/RowParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/RowParser.scala @@ -56,10 +56,6 @@ case class StandardRowParser[Row: CellParser](parser: LineParser) extends String object StandardRowParser { def apply[Row: CellParser](implicit rowConfig: RowConfig): StandardRowParser[Row] = StandardRowParser(LineParser.apply) - - // CONSIDER eliminating as this is never used. - def apply[Row: CellParser](delimiter: Regex, string: Regex, enclosures: String, listSeparator: Char, quote: Char): StandardRowParser[Row] = - StandardRowParser(new LineParser(delimiter, string, enclosures, listSeparator, quote)) } /** diff --git a/src/main/scala/com/phasmidsoftware/parse/TableParser.scala b/src/main/scala/com/phasmidsoftware/parse/TableParser.scala index de12c1f..5580bc8 100644 --- a/src/main/scala/com/phasmidsoftware/parse/TableParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/TableParser.scala @@ -10,20 +10,53 @@ import com.phasmidsoftware.util.FP import scala.annotation.implicitNotFound import scala.util.{Failure, Try} +/** + * Type class to parse a set of rows as a Table. + * + * @tparam Table the Table type. + */ @implicitNotFound(msg = "Cannot find an implicit instance of TableParser[${Table}]. Typically, you should define an instance of StringTableParser or StringsTableParser.") trait TableParser[Table] { + /** + * The row type. + */ type Row + /** + * The input type. + */ type Input + /** + * NOTE: this method must be consistent with the builder method. + * + * @return true if this table parser should provide a header. + */ def hasHeader: Boolean + /** + * NOTE: this method must be consistent with the hasHeader method. + * + * @param rows the rows which will make up the table. + * @param maybeHeader an optional Header. + * @return an instance of Table. + */ + def builder(rows: Seq[Row], maybeHeader: Option[Header]): Table + + /** + * Method to determine how errors are handled. + * + * @return true if individual errors are logged but do not cause parsing to fail. + */ def forgiving: Boolean = false + /** + * Method to define a row parser. + * + * @return a RowParser[Row, Input]. + */ def rowParser: RowParser[Row, Input] - - def builder(rows: Seq[Row]): Table } abstract class AbstractTableParser[Table] extends TableParser[Table] { @@ -89,7 +122,7 @@ abstract class StringTableParser[Table] extends AbstractTableParser[Table] { def parseRows(xs: Strings, header: Header): Try[Table] = { val rys = for (w <- xs) yield rowParser.parse(w)(header) // System.err.println(s"StringTableParser: parsed ${rys.size} of ${xs.size} rows") - for (rs <- FP.sequence(if (forgiving) logFailures(rys) else rys)) yield builder(rs) + for (rs <- FP.sequence(if (forgiving) logFailures(rys) else rys)) yield builder(rs, Some(header)) } } @@ -105,6 +138,7 @@ abstract class StringsTableParser[Table] extends AbstractTableParser[Table] { def parseRows(xs: Seq[Strings], header: Header): Try[Table] = { val rys = for (w <- xs) yield rowParser.parse(w)(header) // System.err.println(s"StringsTableParser: parsed ${rys.size} of ${xs.size} rows") - for (rs <- FP.sequence(if (forgiving) logFailures(rys) else rys)) yield builder(rs) + for (rs <- FP.sequence(if (forgiving) logFailures(rys) else rys)) yield builder(rs, Some(header)) } + } \ No newline at end of file diff --git a/src/main/scala/com/phasmidsoftware/render/Renderer.scala b/src/main/scala/com/phasmidsoftware/render/Renderer.scala index 9b71afc..c9e5011 100644 --- a/src/main/scala/com/phasmidsoftware/render/Renderer.scala +++ b/src/main/scala/com/phasmidsoftware/render/Renderer.scala @@ -4,7 +4,6 @@ package com.phasmidsoftware.render -import com.phasmidsoftware.table.Indexed import org.joda.time.LocalDate import scala.annotation.implicitNotFound @@ -25,20 +24,18 @@ trait Renderer[T] { * * @param t the input parameter, i.e. the object to be rendered. * @param attrs a map of attributes for this value of U. - * @tparam U the type of the result. * @return a new instance of U. */ - def render[U: TreeWriter](t: T, attrs: Map[String, String]): U = implicitly[TreeWriter[U]].node(style, Some(asString(t)), baseAttrs ++ attrs) + def render(t: T, attrs: Map[String, String]): Node = Node(style, Some(asString(t)), baseAttrs ++ attrs) /** * Render an instance of T as a U. * This signature does not support the specification of attributes. * * @param t the input parameter, i.e. the object to be rendered. - * @tparam U the type of the result. * @return a new instance of U. */ - def render[U: TreeWriter](t: T): U = render(t, Map()) + def render(t: T): Node = render(t, Map()) /** * Method to render content as a String. @@ -75,34 +72,13 @@ trait UntaggedRenderer[T] extends Renderer[T] { abstract class TaggedRenderer[T](val style: String, override val baseAttrs: Map[String, String] = Map()) extends Renderer[T] abstract class ProductRenderer[T <: Product : ClassTag](val style: String, override val baseAttrs: Map[String, String] = Map()) extends Renderer[T] { - override def render[U: TreeWriter](t: T, attrs: Map[String, String]): U = implicitly[TreeWriter[U]].node(style, attrs, us(t)) + override def render(t: T, attrs: Map[String, String]): Node = Node(style, attrs, nodes(t)) - protected def us[U: TreeWriter](t: T): Seq[U] -} - -abstract class IndexedRenderer[T: Renderer](val style: String, indexStyle: String, override val baseAttrs: Map[String, String] = Map()) extends Renderer[Indexed[T]] { - /** - * Render an instance of Indexed[T] as a U. - * - * @param ti the input parameter, i.e. the object to be rendered. - * @param attrs a map of attributes for this value of U. - * @tparam U the type of the result. - * @return a new instance of U. - */ - override def render[U: TreeWriter](ti: Indexed[T], attrs: Map[String, String]): U = { - val indexRenderer: Renderer[Int] = new TaggedRenderer[Int](indexStyle) {} - implicitly[TreeWriter[U]].node(style, Seq(indexRenderer.render(ti.i), implicitly[Renderer[T]].render(ti.t))) - } + protected def nodes(t: T): Seq[Node] } object Renderer { - // NOTE: this is only used in unit tests - def render[T: Renderer, U: TreeWriter](t: T): U = implicitly[Renderer[T]].render(t) - - // NOTE: this is only used in unit tests - def render[T: Renderer, U: TreeWriter](t: T, a: String): U = implicitly[Renderer[T]].render(t, Map("name" ->a)) - trait StringRenderer extends UntaggedRenderer[String] implicit object StringRenderer extends StringRenderer diff --git a/src/main/scala/com/phasmidsoftware/render/Renderers.scala b/src/main/scala/com/phasmidsoftware/render/Renderers.scala index eb26417..97bef8c 100644 --- a/src/main/scala/com/phasmidsoftware/render/Renderers.scala +++ b/src/main/scala/com/phasmidsoftware/render/Renderers.scala @@ -17,97 +17,116 @@ import scala.reflect.ClassTag trait Renderers { /** - * Renderer for a header. + * Method to return a Renderer[T]. + * You do not need to define such a renderer if you can use the default style. * - * CONSIDER using sequenceRenderer - * - * @param style the style for the header (e.g. "th" for an HTML table). - * @param attrs the attributes. - * @return a Renderer[Seq[String] ] + * @param style the style of the resulting renderer. + * @param attrs a set of base attributes which are explicitly set for this Renderer; + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ - def headerRenderer(style: String, attrs: Map[String, String] = Map())(stringRenderer: Renderer[String]): Renderer[Header] = new TaggedRenderer[Header](style, attrs) { - override def render[U: TreeWriter](h: Header, attrs: Map[String, String]): U = implicitly[TreeWriter[U]].node(style, attrs, h.xs.map((t: String) => stringRenderer.render(t))) - } + def renderer[T: Renderer](style: String, attrs: Map[String, String] = Map()): Renderer[T] = new TaggedRenderer[T](style, attrs) {} /** - * Render an sequence of P as a U. - * NOTE: there are no identifiers generated with this Renderer. + * Method to return a Renderer[T] where you wish to explicitly define a conversion o a T into a String. * - * @tparam P the type of the result. - * @return a new instance of U. + * @param style the style of the resulting renderer. + * @param attrs a set of base attributes which are explicitly set for this Renderer; + * @param f the rendering function to transform a T into a String (overrides the default asString method). + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ - def sequenceRenderer[P: Renderer](style: String, attrs: Map[String, String] = Map()): Renderer[Seq[P]] = new TaggedRenderer[Seq[P]](style, attrs) { - override def render[U: TreeWriter](ps: Seq[P], attrs: Map[String, String]): U = implicitly[TreeWriter[U]].node(style, attrs, ps map (implicitly[Renderer[P]].render(_))) + def rendererExplicit[T: Renderer](style: String, attrs: Map[String, String] = Map())(f: T => String): Renderer[T] = new TaggedRenderer[T](style, attrs) { + override def asString(t: T): String = f(t) } /** - * Render an sequence of P as a U. - * NOTE: there are no identifiers generated with this Renderer. + * Method to return a Renderer[Header]. + * + * CONSIDER using sequenceRenderer * - * @tparam P the type of the result. - * @return a new instance of U. + * @param style the style for the header (e.g. "th" for an HTML table). + * @param attrs the attributes. + * @return a Renderer[ Seq[String] ] */ - def indexedRenderer[P: Renderer](style: String, indexStyle: String, attrs: Map[String, String] = Map()): Renderer[Indexed[P]] = new IndexedRenderer[P](style, indexStyle, attrs) {} + def headerRenderer(style: String, attrs: Map[String, String] = Map(), sequenced: Boolean)(stringRenderer: Renderer[String]): Renderer[Header] = new TaggedRenderer[Header](style, attrs) { + override def render(h: Header, attrs: Map[String, String]): Node = Node(style, attrs, headerElements(h).map((t: String) => stringRenderer.render(t))) + + private def headerElements(h: Header): Seq[String] = { + if (sequenced) "" +: h.xs + else h.xs + } + } + /** - * Render an option of P as a U. + * Method to return a Renderer[ Seq[T] ]. * NOTE: there are no identifiers generated with this Renderer. * - * CONSIDER improving this method. - * - * @tparam P the type of the result. - * @return a new instance of U. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[ Seq[T] ] */ - def optionRenderer[P: Renderer]: Renderer[Option[P]] = new UntaggedRenderer[Option[P]] { - override def render[U: TreeWriter](po: Option[P], attrs: Map[String, String]): U = implicitly[TreeWriter[U]].node(style, attrs, po.toSeq map (implicitly[Renderer[P]].render(_))) + def sequenceRenderer[T: Renderer](style: String, attrs: Map[String, String] = Map()): Renderer[Seq[T]] = new TaggedRenderer[Seq[T]](style, attrs) { + override def render(ts: Seq[T], attrs: Map[String, String]): Node = Node(style, attrs, ts map (implicitly[Renderer[T]].render(_))) } /** - * Method to return a Renderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. - * - * NOTE: be careful using this method it only applies where T is a 1-tuple (e.g. a case class with one field). - * It probably shouldn't ever be used in practice. It can cause strange initialization errors! - * This note may be irrelevant now that we have overridden convertString to fix issue #1. * - * @param style the style of the resulting renderer. - * @param attrs a set of base attributes which are explicitly set for this Renderer; - * @tparam T the type of the element to be rendered. - * @return a Renderer which converts an instance of T into an instance of U. + * @param overallStyle the style of the Node which will be created by this Renderer. + * @param indexStyle the style of the Index node which will form part of the Node created by this Renderer. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[ Indexed[T] ]. */ - def renderer[T: Renderer](style: String, attrs: Map[String, String] = Map()): Renderer[T] = new TaggedRenderer[T](style, attrs) {} + def indexedRenderer[T: Renderer](overallStyle: String, indexStyle: String): Renderer[Indexed[T]] = new Renderer[Indexed[T]] { + /** + * Render an instance of Indexed[T] as a U. + * + * @param ti the input parameter, i.e. the object to be rendered. + * @param attrs a map of attributes for this value of U. + * @return a new instance of U. + */ + override def render(ti: Indexed[T], attrs: Map[String, String]): Node = { + val sequence = new TaggedRenderer[Int](indexStyle) {}.render(ti.i) + val value = implicitly[Renderer[T]].render(ti.t) + Node(style, attrs, Seq(sequence, value)) + } + + /** + * Defines the default style for type T. + */ + val style: String = overallStyle + } /** - * Method to return a Renderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. - * - * NOTE: be careful using this method it only applies where T is a 1-tuple (e.g. a case class with one field). - * It probably shouldn't ever be used in practice. It can cause strange initialization errors! - * This note may be irrelevant now that we have overridden convertString to fix issue #1. + * Method to return a Renderer[ Option[T] ]. + * NOTE: there are no identifiers generated with this Renderer. * - * @param style the String which will be used for the "tag" parameter of U's node method. - * @param attrs a set of base attributes which are explicitly set for this Renderer; - * @param f the rendering function to transform a T into a String (overrides the default render method). - * @tparam T the type of the element to be rendered. - * @return a Renderer which converts an instance of T into an instance of U. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[ Option[T] ]. */ - def rendererExplicit[T: Renderer](style: String, attrs: Map[String, String] = Map())(f: T => String): Renderer[T] = new TaggedRenderer[T](style, attrs) { - override def asString(t: T): String = f(t) + def optionRenderer[T: Renderer](style: String, attrs: Map[String, String] = Map()): Renderer[Option[T]] = new TaggedRenderer[Option[T]](style, attrs) { + override def render(to: Option[T], attrs: Map[String, String]): Node = to match { + case Some(t) => Node(style, attrs, Seq(implicitly[Renderer[T]].render(t))) + case None => Node("", None, Map(), Nil) + } } /** * Method to return a Renderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. * - * NOTE: be careful using this method it only applies where T is a 1-tuple (e.g. a case class with one field). + * NOTE: be careful using this particular method it only applies where T is a 1-tuple (e.g. a case class with one field -- not common). * It probably shouldn't ever be used in practice. It can cause strange initialization errors! * This note may be irrelevant now that we have overridden convertString to fix issue #1. * * @param construct a function P => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the (single) field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts a String from a Row into the field type P and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer1[P1: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: P1 => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -119,14 +138,15 @@ trait Renderers { * Method to return a Renderer[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. * * @param construct a function (P1,P2) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1 and P2 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer2[P1: Renderer, P2: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -139,15 +159,16 @@ trait Renderers { * Method to return a Renderer[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. * * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the third field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2 and P3 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer3[P1: Renderer, P2: Renderer, P3: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -161,16 +182,17 @@ trait Renderers { * Method to return a Renderer[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. * * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. * @tparam P4 the type of the fourth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3 and P4 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer4[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -185,17 +207,18 @@ trait Renderers { * Method to return a Renderer[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. * * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. * @tparam P4 the type of the fourth field of the Product type T. * @tparam P5 the type of the fifth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4 and P5 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer5[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -211,17 +234,18 @@ trait Renderers { * Method to return a Renderer[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. * @tparam P4 the type of the fourth field of the Product type T. * @tparam P5 the type of the fifth field of the Product type T. * @tparam P6 the type of the sixth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5 and P6 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer6[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -238,6 +262,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -245,12 +270,12 @@ trait Renderers { * @tparam P5 the type of the fifth field of the Product type T. * @tparam P6 the type of the sixth field of the Product type T. * @tparam P7 the type of the seventh field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6 and P7 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer7[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -268,6 +293,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -276,12 +302,12 @@ trait Renderers { * @tparam P6 the type of the sixth field of the Product type T. * @tparam P7 the type of the seventh field of the Product type T. * @tparam P8 the type of the eighth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7 and P8 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer8[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, P8: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7, p8) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -300,6 +326,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -309,12 +336,12 @@ trait Renderers { * @tparam P7 the type of the seventh field of the Product type T. * @tparam P8 the type of the eighth field of the Product type T. * @tparam P9 the type of the ninth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8 and P9 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer9[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, P8: Renderer, P9: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -334,6 +361,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -344,12 +372,12 @@ trait Renderers { * @tparam P8 the type of the eighth field of the Product type T. * @tparam P9 the type of the ninth field of the Product type T. * @tparam P10 the type of the tenth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9 and P10 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer10[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, P8: Renderer, P9: Renderer, P10: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -370,6 +398,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -381,12 +410,12 @@ trait Renderers { * @tparam P9 the type of the ninth field of the Product type T. * @tparam P10 the type of the tenth field of the Product type T. * @tparam P11 the type of the eleventh field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10 and P11 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer11[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, P8: Renderer, P9: Renderer, P10: Renderer, P11: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T): Renderer[T] = new ProductRenderer[T](style, attrs) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) @@ -408,6 +437,7 @@ trait Renderers { * Method to return a Renderer[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. * * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. * @tparam P1 the type of the first field of the Product type T. * @tparam P2 the type of the second field of the Product type T. * @tparam P3 the type of the second field of the Product type T. @@ -420,12 +450,12 @@ trait Renderers { * @tparam P10 the type of the tenth field of the Product type T. * @tparam P11 the type of the eleventh field of the Product type T. * @tparam P12 the type of the twelfth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a Renderer which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11 and P12 and thence into a T + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a Renderer[T]. */ def renderer12[P1: Renderer, P2: Renderer, P3: Renderer, P4: Renderer, P5: Renderer, P6: Renderer, P7: Renderer, P8: Renderer, P9: Renderer, P10: Renderer, P11: Renderer, P12: Renderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T): Renderer[T] = new ProductRenderer[T](style) { - protected def us[U: TreeWriter](t: T): Seq[U] = { + protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) Seq( implicitly[Renderer[P1]].render(t.productElement(0).asInstanceOf[P1], nameAttr(p1)) diff --git a/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala b/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala index 49b873a..fbab38c 100644 --- a/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala +++ b/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala @@ -6,6 +6,7 @@ package com.phasmidsoftware.render import scala.annotation.implicitNotFound + /** * This trait defines the behavior of a hierarchical writer of objects. * For example, U might be defined as an HTML or XML document. @@ -18,18 +19,124 @@ import scala.annotation.implicitNotFound @implicitNotFound(msg = "Cannot find an implicit instance of TreeWriter[${U}].") trait TreeWriter[U] { - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[U]): U + /** + * This method is required to convert a Node into a U. + * + * @param node the Node to be converted. + * @return an instance of U. + */ + def evaluate(node: Node): U + +} + +/** + * This case class defines a Node in the hierarchical output produced by rendering. + * This class is used in conjunction with TreeWriter. + * The reason for this temporary structure is that we need the ability to merge (or otherwise process) nodes of the tree. + * Since U (see TreeWriter) is an opaque type as far as this code is concerned, we need our own representation of the tree. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param content the content of this Node, if any. + * @param attributes the attributes of this Node (may be empty). + * @param children the children of this Node (may be empty). + */ +case class Node(style: String, content: Option[String], attributes: Map[String, String], children: Seq[Node]) { + + /** + * Method to eliminate nodes of the form Node("", None, Map.empty, ...). + * + * @return a subtree rooted at this, but with nodes trimmed. + */ + def trim: Node = this match { + case Node(s, wo, kVm, ns) => Node(s, wo, kVm, for (n <- ns; x <- doTrim(n)) yield x) + } + + private def doTrim(n: Node): Seq[Node] = n match { + case Node("", None, kVm, ns) if kVm.isEmpty => ns map (_.trim) + case _ => Seq(n.trim) + } +} + +/** + * Companion object to Node. + */ +object Node { + + /** + * Create a Node with no content. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param attributes the attributes of this Node (may be empty). + * @param children the children of this Node (may be empty). + * @return a new Node. + */ + def apply(style: String, attributes: Map[String, String], children: Seq[Node]): Node = apply(style, None, attributes, children) + + /** + * Create a Node with only style and children. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param children the children of this Node (may be empty). + * @return a new Node. + */ + def apply(style: String, children: Seq[Node]): Node = apply(style, Map[String, String](), children) - def node(tag: String, attributes: Map[String, String], children: Seq[U]): U = node(tag, None, attributes, children) + /** + * Create a leaf Node (with no children). + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param content the content of this Node, if any. + * @param attributes the attributes of this Node (may be empty). + * @return a new Node. + */ + def apply(style: String, content: Option[String], attributes: Map[String, String]): Node = apply(style, content, attributes, Nil) - def node(tag: String, children: Seq[U]): U = node(tag, Map[String, String](), children) + /** + * Create a content-less leaf Node (with no children). + * NOTE: I'm not sure if this makes sense and is only used by unit tests. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param attributes the attributes of this Node (may be empty). + * @return a new Node. + */ + def apply(style: String, attributes: Map[String, String]): Node = apply(style, None, attributes) - def node(tag: String, content: Option[String], attributes: Map[String, String]): U = node(tag, content, attributes, Nil) + /** + * Create a content-less, no-attribute, leaf Node (with no children). + * NOTE: I'm not sure if this makes sense and is only used by unit tests. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @return a new Node. + */ + def apply(style: String): Node = apply(style, Nil) - def node(tag: String, attributes: Map[String, String]): U = node(tag, None, attributes) + /** + * TODO can eliminate + * + * Merge two Nodes together, taking the style, content (if any) and attributes from the left node. + * + * @param left the left Node (its children will precede the children of the right Node). + * @param right the right Node (its children will succeed the children of the left Node). + * @return a new Node containing all the children of the left and right. + */ + def mergeLeft(left: Node, right: Node): Node = Node(left.style, left.content, left.attributes, left.children ++ right.children) - def node(tag: String): U = node(tag, Nil) + /** + * TODO can eliminate + * + * Merge two Nodes together, taking the style, content (if any) and attributes from the right node. + * + * @param left the left Node (its children will precede the children of the right Node). + * @param right the right Node (its children will succeed the children of the left Node). + * @return a new Node containing all the children of the left and right. + */ + def mergeRight(left: Node, right: Node): Node = Node(right.style, right.content, right.attributes, left.children ++ right.children) - def addChild(parent: U, child: U): U } diff --git a/src/main/scala/com/phasmidsoftware/render/Writable.scala b/src/main/scala/com/phasmidsoftware/render/Writable.scala new file mode 100644 index 0000000..4d3922d --- /dev/null +++ b/src/main/scala/com/phasmidsoftware/render/Writable.scala @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.render + +/** + * Trait to enable rendering of a table to a sequential (non-hierarchical) output format. + * + * @tparam O the underlying type, for example, a StringBuilder. + */ +trait Writable[O] { + + /** + * Method to return an empty (i.e. new) instance of O + * + * @return + */ + def unit: O + + /** + * Method to write a character sequence to the given instance o. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeRaw(o: O)(x: CharSequence): O + + /** + * Method to write a character sequence to the given instance o, but within quotes. + * Any quote characters in x will be doubled. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeQuoted(o: O)(x: CharSequence): O = writeRaw(o)(quote + x.toString.replaceAll(quote.toString, quote + quote.toString) + quote) + + /** + * Method to write a value of type Any to the given instance o, possibly quoted. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeValue(o: O)(x: Any): O = if (x.toString.contains(delimiter.toString) || x.toString.contains(quote.toString)) + writeQuoted(o)(x.toString) + else + writeRaw(o)(x.toString) + + /** + * Method to write a value of type Any to the given instance o, possibly quoted. + * Elements will be separated by the delimite, but no newline is appended. + * Element strings may be enclosed in quotes if appropriate. + * + * @param o the instance of O whither the xs values should be written. + * @param xs the sequence of elements (values) to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeRowElements(o: O)(xs: Seq[Any]): O = { + // CONSIDER using foldLeft so that we can use the updated value of o at each step. + for (x <- xs.headOption) writeValue(o)(x) + for (x <- xs.tail) writeValue(writeRaw(o)(delimiter))(x) + o + } + + /** + * Method to write a value of type Any to the given instance o, possibly quoted. + * + * @param o the instance of O whither the xs values should be written. + * @param x the row instance to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeRow[Row <: Product](o: O)(x: Row): O = writeRaw(writeRowElements(o)(x.productIterator.toSeq))(newline) + + /** + * The default delimiter is a comma followed by a space. + * + * @return ", " + */ + def delimiter: CharSequence = ", " + + /** + * The default newline character is the newline. + * + * @return \n + */ + def newline: CharSequence = "\n" + + /** + * The default quote is one double-quote symbol + * + * @return " + */ + def quote: CharSequence = + """"""" +} diff --git a/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala b/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala index 786f4ce..41d5e01 100644 --- a/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala +++ b/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala @@ -41,6 +41,6 @@ object HTML { def apply(name: String, content: Option[String]): HTML = apply(name, Map.empty, content) - def defaultName(name: String): String = if (name == "") "span" else name + private def defaultName(name: String): String = if (name == "") "span" else name } diff --git a/src/main/scala/com/phasmidsoftware/table/Table.scala b/src/main/scala/com/phasmidsoftware/table/Table.scala index 9e1b3e0..c668d31 100644 --- a/src/main/scala/com/phasmidsoftware/table/Table.scala +++ b/src/main/scala/com/phasmidsoftware/table/Table.scala @@ -8,7 +8,7 @@ import java.io.{File, InputStream} import java.net.{URI, URL} import com.phasmidsoftware.parse.{ParserException, StringTableParser, StringsTableParser, TableParser} -import com.phasmidsoftware.render.{Renderer, Renderers, TreeWriter} +import com.phasmidsoftware.render._ import scala.io.{Codec, Source} import scala.util.{Failure, Success, Try} @@ -79,6 +79,25 @@ trait Table[Row] extends Iterable[Row] { */ def iterator: Iterator[Row] = rows.iterator + /** + * Method to render a table in a sequential (serialized) fashion. + * + * @tparam O a type which supports Writable (via evidence of type Writable[O]) + * @return a new (or possibly old) instance of O. + */ + def render[O: Writable]: O = { + val ww = implicitly[Writable[O]] + val o1 = ww.unit + val o2 = (maybeHeader map (h => ww.writeRaw(ww.writeRowElements(o1)(h.xs))(ww.newline))).getOrElse(o1) + rows map { + case p: Product => ww.writeRow(o2)(p) + case xs: Seq[Any] => ww.writeRowElements(o2)(xs) + case xs: Array[Any] => ww.writeRowElements(o2)(xs) + case _ => throw TableException("cannot render table because row is neither a Product, nor an array nor a sequence") + } + o1 + } + /** * Method to render a table in a hierarchical fashion. * @@ -91,14 +110,40 @@ trait Table[Row] extends Iterable[Row] { * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). * @return a new instance of U which represents this Table as a tree of some sort. */ - def render[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit rr: Renderer[Indexed[Row]]): U = { + def render[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit rr: Renderer[Row]): U = { object TableRenderers extends Renderers { - val rowsRenderer: Renderer[Seq[Indexed[Row]]] = sequenceRenderer[Indexed[Row]]("span") - val headerRenderer: Renderer[Header] = headerRenderer("th")(renderer("td", Map())) + val rowsRenderer: Renderer[Seq[Row]] = sequenceRenderer[Row]("tbody") + implicit val headerRenderer: Renderer[Header] = headerRenderer("tr", sequenced = false)(renderer("th", Map())) + implicit val optionRenderer: Renderer[Option[Header]] = optionRenderer[Header]("thead", Map()) } import TableRenderers._ - val uo: Option[U] = maybeHeader map (headerRenderer.render(_)) - implicitly[TreeWriter[U]].node(style, attributes, uo.toSeq ++ Seq(rowsRenderer.render(Indexed.index(rows)))) + val node: Node = implicitly[Renderer[Option[Header]]].render(maybeHeader) + implicitly[TreeWriter[U]].evaluate(Node(style, attributes, node +: Seq(rowsRenderer.render(rows)))) + } + + /** + * Method to render a table in a hierarchical fashion. + * + * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, + * outputting each row as you go. + * + * @param style the "style" to be used for the node which will represent this table. + * @param attributes the attributes to be applied to the top level node for this table. + * @param rr an (implicit) Renderer[ Indexed [ Row ] ] + * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). + * @return a new instance of U which represents this Table as a tree of some sort. + */ + def renderSequenced[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit rr: Renderer[Indexed[Row]]): U = { + object TableRenderers extends Renderers { + val rowsRenderer: Renderer[Seq[Indexed[Row]]] = sequenceRenderer[Indexed[Row]]("tbody") + implicit val headerRenderer: Renderer[Header] = headerRenderer("tr", sequenced = true)(renderer("th", Map())) + implicit val optionRenderer: Renderer[Option[Header]] = optionRenderer[Header]("thead", Map()) + } + import TableRenderers._ + val headerNode: Node = implicitly[Renderer[Option[Header]]].render(maybeHeader) + val tableNode = Node(style, attributes, headerNode +: Seq(rowsRenderer.render(Indexed.index(rows)))) + val trimmed = tableNode.trim + implicitly[TreeWriter[U]].evaluate(trimmed) } } @@ -268,6 +313,7 @@ object Table { case _ => Failure(ParserException(s"parse method incompatible with tableParser: $tableParser")) } } + } case class Header(xs: Seq[String]) { @@ -291,6 +337,15 @@ abstract class BaseTable[Row](rows: Seq[Row], val maybeHeader: Option[Header]) e self => } +/** + * Concrete case class implementing BaseTable with a Header. + * + * CONSIDER merging the two cases. + * + * @param rows the rows of the table. + * @param header the header. + * @tparam Row the underlying type of each Row + */ case class TableWithHeader[Row](rows: Seq[Row], header: Header) extends BaseTable[Row](rows, Some(header)) { override def unit[S](rows: Seq[S], maybeHeader: Option[Header]): Table[S] = maybeHeader match { case Some(h) => TableWithHeader(rows, h); @@ -298,6 +353,14 @@ case class TableWithHeader[Row](rows: Seq[Row], header: Header) extends BaseTabl } } +/** + * Concrete case class implementing BaseTable without a Header. + * + * CONSIDER merging the two cases. + * + * @param rows the rows of the table. + * @tparam Row the underlying type of each Row + */ case class TableWithoutHeader[Row](rows: Seq[Row]) extends BaseTable[Row](rows, None) { override def unit[S](rows: Seq[S], maybeHeader: Option[Header]): Table[S] = maybeHeader match { case None => TableWithoutHeader(rows); diff --git a/src/main/scala/com/phasmidsoftware/util/Reflection.scala b/src/main/scala/com/phasmidsoftware/util/Reflection.scala index 5cf01c8..6273ebf 100644 --- a/src/main/scala/com/phasmidsoftware/util/Reflection.scala +++ b/src/main/scala/com/phasmidsoftware/util/Reflection.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019. Phasmid Software + * This module derives from Spray. */ package com.phasmidsoftware.util @@ -9,7 +9,7 @@ import scala.reflect.ClassTag object Reflection { /** - * This method is borrowed directly from Spray JsonReader + * This method is borrowed directly from Spray JsonReader. * * @param classTag rhw class tag. * @return an Array of String. @@ -21,8 +21,8 @@ object Reflection { val clazz = classTag.runtimeClass try { - // copy methods have the form copy$default$N(), we need to sort them in order, but must account for the fact - // that lexical sorting of ...8(), ...9(), ...10() is not correct, so we extract N and sort by N.toInt + // NOTE: copy methods have the form copy$default$N(), we need to sort them in order, but must account for the fact + // ... that lexical sorting of ...8(), ...9(), ...10() is not correct, so we extract N and sort by N.toInt val copyDefaultMethods = clazz.getMethods.filter(_.getName.startsWith("copy$default$")).sortBy( _.getName.drop("copy$default$".length).takeWhile(_ != '(').toInt) val fields = clazz.getDeclaredFields.filterNot { f => diff --git a/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala index 91735f4..2cb2925 100644 --- a/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala @@ -44,6 +44,7 @@ class CellParserSpec extends FlatSpec with Matchers { it should "parse String" in { stringCellParser.parse(CellValue("Hello")) shouldBe "Hello" stringCellParser.parse(CellValue(""""Hello"""")) shouldBe """"Hello"""" + // CONSIDER re-instate this test // stringCellParser.parse(CellValue(""""Hello with internal "" Goodbye"""")) shouldBe """"Hello with internal " Goodbye"""" } diff --git a/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala index 31c1827..d19c7a6 100644 --- a/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala @@ -6,7 +6,7 @@ package com.phasmidsoftware.parse import java.util.Date -import com.phasmidsoftware.table.{Header, Table, TableException, TableWithoutHeader} +import com.phasmidsoftware.table._ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.scalatest.{FlatSpec, Matchers} @@ -49,7 +49,7 @@ class TableParserSpec extends FlatSpec with Matchers { def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - def builder(rows: Seq[Row]): Table[IntPair] = TableWithoutHeader(rows) + def builder(rows: Seq[IntPair], maybeHeader: Option[Header]): Table[IntPair] = TableWithoutHeader(rows) } implicit object IntPairTableParser extends IntPairTableParser @@ -103,7 +103,7 @@ class TableParserSpec extends FlatSpec with Matchers { def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - def builder(rows: Seq[Row]): Table[DailyRaptorReport] = TableWithoutHeader(rows) + def builder(rows: Seq[DailyRaptorReport], maybeHeader: Option[Header]): Table[DailyRaptorReport] = TableWithoutHeader(rows) } implicit object DailyRaptorReportTableParser extends DailyRaptorReportTableParser @@ -191,7 +191,10 @@ class TableParserSpec extends FlatSpec with Matchers { def rowParser: RowParser[Row, Seq[String]] = implicitly[RowParser[Row, Seq[String]]] - def builder(rows: Seq[Row]): Table[DailyRaptorReport] = TableWithoutHeader(rows) + def builder(rows: Seq[DailyRaptorReport], maybeHeader: Option[Header]): Table[DailyRaptorReport] = maybeHeader match { + case Some(h) if hasHeader => TableWithHeader(rows, h) + case None => TableWithoutHeader(rows) + } } implicit object DailyRaptorReportStringsTableParser extends DailyRaptorReportStringsTableParser @@ -205,7 +208,7 @@ class TableParserSpec extends FlatSpec with Matchers { Seq("09/16/2018", "Partly Cloudy", "SE", "6-12", "0", "0", "0", "4", "19", "3", "30", "2", "0", "0", "2", "3308", "5", "0", "0", "0", "0", "27", "8", "1", "0", "1", "0", "3410"), Seq("09/19/2018", "Overcast/Mostly cloudy/Partly cloudy/Clear", "NW", "4-7", "0", "0", "0", "47", "12", "0", "84", "10", "0", "0", "1", "821", "4", "0", "1", "0", "0", "27", "4", "1", "0", "2", "0", "1014")) val x: Try[Table[DailyRaptorReport]] = for (r <- Table.parseSequence(raw)) yield r - x should matchPattern { case Success(TableWithoutHeader(_)) => } + x should matchPattern { case Success(TableWithHeader(_, _)) => } x.get.rows.size shouldBe 2 //noinspection ScalaDeprecation x.get.rows.head shouldBe DailyRaptorReport(LocalDate.fromDateFields(new Date(118, 8, 16)), "Partly Cloudy", 3308, 5) @@ -237,11 +240,11 @@ class TableParserSpec extends FlatSpec with Matchers { def hasHeader: Boolean = true + def builder(rows: Seq[Row], maybeHeader: Option[Header]): Table[Submission] = TableWithHeader(rows, maybeHeader.get) + override def forgiving: Boolean = false def rowParser: RowParser[Row, Seq[String]] = implicitly[RowParser[Row, Seq[String]]] - - def builder(rows: Seq[Row]): Table[Submission] = TableWithoutHeader(rows) } } @@ -284,6 +287,8 @@ class TableParserSpec extends FlatSpec with Matchers { implicit object SubmissionTableParser extends StringTableParser[Table[Submission]] { type Row = Submission + override def builder(rows: Seq[Row], maybeHeader: Option[Header]): Table[Submission] = TableWithHeader(rows, maybeHeader.get) + def hasHeader: Boolean = true override def forgiving: Boolean = true diff --git a/src/test/scala/com/phasmidsoftware/render/NodeSpec.scala b/src/test/scala/com/phasmidsoftware/render/NodeSpec.scala new file mode 100644 index 0000000..73df82d --- /dev/null +++ b/src/test/scala/com/phasmidsoftware/render/NodeSpec.scala @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.render + +import org.scalatest.{FlatSpec, Matchers} + +class NodeSpec extends FlatSpec with Matchers { + + behavior of "NodeSpec" + + it should "mergeLeft" in { + val node1 = Node("left leaf") + val left = Node("left", Some("left"), Map("which" -> "left"), Seq(node1)) + val node2 = Node("right leaf") + val right = Node("right", Some("right"), Map("which" -> "right"), Seq(node2)) + val result = Node.mergeLeft(left, right) + println(result) + } + + it should "mergeRight" in { + val node1 = Node("left leaf") + val left = Node("left", Some("left"), Map("which" -> "left"), Seq(node1)) + val node2 = Node("right leaf") + val right = Node("right", Some("right"), Map("which" -> "right"), Seq(node2)) + val result = Node.mergeRight(left, right) + println(result) + + } + +} diff --git a/src/test/scala/com/phasmidsoftware/render/RendererSpec.scala b/src/test/scala/com/phasmidsoftware/render/RendererSpec.scala index b1a2b98..4679cff 100644 --- a/src/test/scala/com/phasmidsoftware/render/RendererSpec.scala +++ b/src/test/scala/com/phasmidsoftware/render/RendererSpec.scala @@ -28,29 +28,19 @@ class RendererSpec extends FlatSpec with Matchers { implicit val complexRenderer: Renderer[Complex] = renderer2("complex")(Complex.apply) trait TreeWriterString$ extends TreeWriter[String] { - - override def addChild(parent: String, child: String): String = parent + ", " + child - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[String]): String = s"""<$tag>: "${content.getOrElse("")}" ${attributes.mkString("(", ",", ")")} ${children.mkString("[", ",", "]")} """ + def evaluate(node: Node): String = s"""<${node.style}>: "${node.content.getOrElse("")}" ${node.attributes.mkString("(", ",", ")")} ${node.children.map(evaluate).mkString("[", ",", "]")} """ } - implicit object TreeWriterString$ extends TreeWriterString$ } object Complex2 extends Renderers { implicit val valueRenderer: Renderer[Double] = renderer("td") - implicit val complexRenderer: Renderer[Complex] = renderer2("span")(Complex) - implicit val indexedRenderer: Renderer[Indexed[Complex]] = indexedRenderer[Complex]("tr", "th", Map()) - // val rowsRenderer: Renderer[Seq[Indexed[Complex]]] = sequenceRenderer[Indexed[Complex]]("span") - + implicit val complexRenderer: Renderer[Complex] = renderer2("")(Complex) + implicit val indexedRenderer: Renderer[Indexed[Complex]] = indexedRenderer[Complex]("tr", "th") import HTML._ trait TreeWriterHTML$ extends TreeWriter[HTML] { - def addChild(parent: HTML, child: HTML): HTML = parent match { - case HTML(t, co, as, hs) => HTML(t, co, as, hs :+ child) - } - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[HTML]): HTML = HTML(tag, attributes.toSeq.map(kv => Attribute(kv)), content, children) + def evaluate(node: Node): HTML = HTML(node.style, node.attributes.toSeq.map(kv => Attribute(kv)), node.content, node.children map evaluate) } implicit object TreeWriterHTML$ extends TreeWriterHTML$ @@ -59,16 +49,11 @@ class RendererSpec extends FlatSpec with Matchers { object Complex3 extends Renderers { implicit val valueRenderer: Renderer[Double] = renderer("td") implicit val complexRenderer: Renderer[Complex] = renderer2("tr")(Complex) - // val rowsRenderer: Renderer[Seq[Indexed[Complex]]] = sequenceRenderer[Indexed[Complex]]("span") import HTML._ trait TreeWriterHTML$ extends TreeWriter[HTML] { - def addChild(parent: HTML, child: HTML): HTML = parent match { - case HTML(t, co, as, hs) => HTML(t, co, as, hs :+ child) - } - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[HTML]): HTML = HTML(tag, attributes.toSeq.map(kv => Attribute(kv)), content, children) + def evaluate(node: Node): HTML = HTML(node.style, node.attributes.toSeq.map(kv => Attribute(kv)), node.content, node.children map evaluate) } implicit object TreeWriterHTML$ extends TreeWriterHTML$ @@ -76,16 +61,12 @@ class RendererSpec extends FlatSpec with Matchers { object Complicated extends Renderers { implicit val elementRenderer: Renderer[String] = renderer("element") - implicit val optionLongRenderer: Renderer[Option[Long]] = optionRenderer + implicit val optionLongRenderer: Renderer[Option[Long]] = optionRenderer("", Map()) implicit val sequenceStringRenderer: Renderer[Seq[String]] = sequenceRenderer("") implicit val complicatedRenderer: Renderer[Complicated] = renderer5("x")(Complicated.apply) trait TreeWriterHTML$ extends TreeWriter[SimpleHTML] { - def addChild(parent: SimpleHTML, child: SimpleHTML): SimpleHTML = parent match { - case SimpleHTML(t, co, as, hs) => SimpleHTML(t, co, as, hs :+ child) - } - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[SimpleHTML]): SimpleHTML = SimpleHTML(tag, content, attributes, children) + def evaluate(node: Node): SimpleHTML = SimpleHTML(node.style, node.content, node.attributes, node.children map evaluate) } implicit object TreeWriterHTML$ extends TreeWriterHTML$ @@ -96,37 +77,60 @@ class RendererSpec extends FlatSpec with Matchers { it should "render Complex as sequence of numbers" in { import Complex1._ val z = Complex(0, 1) - Renderer.render(z) shouldBe ": \"\" () [<>: \"0.0\" (name -> r) [] ,<>: \"1.0\" (name -> i) [] ] " + val node = implicitly[Renderer[Complex]].render(z) + implicitly[TreeWriter[String]].evaluate(node) shouldBe ": \"\" () [<>: \"0.0\" (name -> r) [] ,<>: \"1.0\" (name -> i) [] ] " } it should "render Complex as an HTML" in { import Complex3._ val z = Complex(0, 1) import HTML._ - Renderer.render(z) shouldBe HTML("tr", Nil, None, List(HTML("td", Seq(Attribute("name" -> "r")), Some("0.0"), Nil), HTML("td", Seq(Attribute("name" -> "i")), Some("1.0"), Nil))) + val node = implicitly[Renderer[Complex]].render(z) + implicitly[TreeWriter[HTML]].evaluate(node) shouldBe HTML("tr", Nil, None, List(HTML("td", Seq(Attribute("name" -> "r")), Some("0.0"), Nil), HTML("td", Seq(Attribute("name" -> "i")), Some("1.0"), Nil))) } it should "render Complicated as an SimpleHTML" in { import Complicated._ val z = Complicated("strange", 42, open = false, Some(6175551234L), Seq("Tom", "Dick", "Harry")) - Renderer.render(z) shouldBe SimpleHTML("x", None, Map.empty, List(SimpleHTML("element", Some("strange"), Map("name" -> "name"), List()), SimpleHTML("", Some("42"), Map("name" -> "count"), List()), SimpleHTML("", Some("false"), Map("name" -> "open"), List()), SimpleHTML("", None, Map("name" -> "maybePhone"), List(SimpleHTML("", Some("6175551234"), Map.empty, List()))), SimpleHTML("", None, Map("name" -> "aliases"), List(SimpleHTML("element", Some("Tom"), Map.empty, List()), SimpleHTML("element", Some("Dick"), Map.empty, List()), SimpleHTML("element", Some("Harry"), Map.empty, List()))))) - - // Renderer.render(z, "Complicated") shouldBe - // Renderer.render(z) shouldBe SimpleHTML("x", None, Map("name"->"Complicated"), List(SimpleHTML("element", Some("strange"), Map("name"->"name"), List()), SimpleHTML("", Some("42"), Map("name"->"count"), List()), SimpleHTML("", Some("false"), Map("name"->"open"), List()), SimpleHTML("", None, Map("name"->"maybePhone"), List(SimpleHTML("", Some("6175551234"), Map.empty, List()))), SimpleHTML("", None, Map("name"->"aliases"), List(SimpleHTML("element", Some("Tom"), Map.empty, List()), SimpleHTML("element", Some("Dick"), Map.empty, List()), SimpleHTML("element", Some("Harry"), Map.empty, List()))))) + val node = implicitly[Renderer[Complicated]].render(z) + implicitly[TreeWriter[SimpleHTML]].evaluate(node) shouldBe SimpleHTML("x", None, Map.empty, List(SimpleHTML("element", Some("strange"), Map("name" -> "name"), List()), SimpleHTML("", Some("42"), Map("name" -> "count"), List()), SimpleHTML("", Some("false"), Map("name" -> "open"), List()), SimpleHTML("", None, Map("name" -> "maybePhone"), List(SimpleHTML("", Some("6175551234"), Map.empty, List()))), SimpleHTML("", None, Map("name" -> "aliases"), List(SimpleHTML("element", Some("Tom"), Map.empty, List()), SimpleHTML("element", Some("Dick"), Map.empty, List()), SimpleHTML("element", Some("Harry"), Map.empty, List()))))) } it should "render a table of Complexes in HTML without a header" in { - import Complex2._ + import Complex1._ val table = TableWithoutHeader(Seq(Complex(0, 1), Complex(-1, 0))) val h = table.render("table", Map("border" -> "1")) - println(h) + h.toString shouldBe ": \"\" (border -> 1) [<>: \"\" () [] ,: \"\" () [: \"\" () [<>: \"0.0\" (name -> r) [] ,<>: \"1.0\" (name -> i) [] ] ,: \"\" () [<>: \"-1.0\" (name -> r) [] ,<>: \"0.0\" (name -> i) [] ] ] ] " } - it should "render a table of Complexes in HTML with a header" in { + it should "render a table of sequenced Complexes in HTML without a header" in { + import Complex2._ + val table = TableWithoutHeader(Seq(Complex(0, 1), Complex(-1, 0))) + val h = table.renderSequenced("table", Map("border" -> "1")) + h.toString shouldBe "\n
\n\n\n\n\n\n\n\n\n
00.01.0
1-1.00.0
" + } + + it should "render a table of sequenced Complexes in HTML with a header" in { import Complex2._ val table = TableWithHeader(Seq(Complex(0, 1), Complex(-1, 0)), Header(Seq("real", "imaginary"))) - val h = table.render("table", Map("border" -> "1")) - println(h) + val h = table.renderSequenced("table", Map("border" -> "1")) + h.toString shouldBe + """ + + + + + + + + + + + + + + +
realimaginary
00.01.0
1-1.00.0
""" } } diff --git a/src/test/scala/com/phasmidsoftware/render/TreeWriterSpec.scala b/src/test/scala/com/phasmidsoftware/render/TreeWriterSpec.scala index 64670fd..07e02e2 100644 --- a/src/test/scala/com/phasmidsoftware/render/TreeWriterSpec.scala +++ b/src/test/scala/com/phasmidsoftware/render/TreeWriterSpec.scala @@ -20,15 +20,8 @@ class TreeWriterSpec extends FlatSpec with Matchers { def apply(x: String, hs: Seq[HTML]): HTML = apply(x, None, Map.empty, hs) trait HTMLTreeWriter extends TreeWriter[HTML] { - - def addChild(parent: HTML, child: HTML): HTML = parent match { - case HTML(t, co, as, hs) => HTML(t, co, as, hs :+ child) - } - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[HTML]): HTML = - HTML(tag, content map (_.toString), attributes, children) + def evaluate(node: Node): HTML = HTML(node.style, node.content map (_.toString), node.attributes, node.children map evaluate) } - implicit object HTMLTreeWriter extends HTMLTreeWriter } @@ -38,8 +31,8 @@ class TreeWriterSpec extends FlatSpec with Matchers { import HTML._ it should "implement node correctly for 1" in { - implicitly[TreeWriter[HTML]].node("1") shouldBe HTML("1") - implicitly[TreeWriter[HTML]].node("1", Map("name" -> "x")) shouldBe HTML("1", None, Map("name" -> "x")) + implicitly[TreeWriter[HTML]].evaluate(Node("1")) shouldBe HTML("1") + implicitly[TreeWriter[HTML]].evaluate(Node("1", Map("name" -> "x"))) shouldBe HTML("1", None, Map("name" -> "x")) } diff --git a/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala b/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala new file mode 100644 index 0000000..6cdffb2 --- /dev/null +++ b/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.render + +import org.scalatest.{FlatSpec, Matchers} + +class WritableSpec extends FlatSpec with Matchers { + + behavior of "Writable" + + implicit object StringBuilderWriteable extends Writable[StringBuilder] { + override def writeRaw(o: StringBuilder)(x: CharSequence): StringBuilder = o.append(x.toString) + + override def unit: StringBuilder = new StringBuilder + + override def delimiter: CharSequence = "|" + } + + it should "write value" in { + val sw = implicitly[Writable[StringBuilder]] + val o = sw.unit + sw.writeValue(o)(1) + o.toString shouldBe "1" + } + + it should "write value containing delimiter" in { + val sw = implicitly[Writable[StringBuilder]] + val o = sw.unit + sw.writeValue(o)("a|b") + o.toString shouldBe "\"a|b\"" + } + + it should "write value containing quote" in { + val sw = implicitly[Writable[StringBuilder]] + val o = sw.unit + sw.writeValue(o)("""a"b""") + o.toString shouldBe "\"a\"\"b\"" + } + + it should "writeRowElements" in { + val sw = implicitly[Writable[StringBuilder]] + val o = sw.unit + sw.writeRowElements(o)(Seq(1, 2)) + o.toString shouldBe "1|2" + } + + case class Complex(r: Double, i: Double) + + it should "writeRow" in { + val sw = implicitly[Writable[StringBuilder]] + val o = sw.unit + sw.writeRow(o)(Complex(1, -1)) + o.toString shouldBe + """1.0|-1.0 + |""".stripMargin + } + +} diff --git a/src/test/scala/com/phasmidsoftware/table/Movie.scala b/src/test/scala/com/phasmidsoftware/table/Movie.scala index 96f0ab6..ef70bdb 100644 --- a/src/test/scala/com/phasmidsoftware/table/Movie.scala +++ b/src/test/scala/com/phasmidsoftware/table/Movie.scala @@ -158,7 +158,7 @@ object MovieParser extends CellParsers { def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - def builder(rows: Seq[Row]): Table[Movie] = TableWithoutHeader(rows) + def builder(rows: Seq[Movie], maybeHeader: Option[Header]): Table[Movie] = TableWithoutHeader(rows) } } diff --git a/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala b/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala index 437410f..4025d99 100644 --- a/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala @@ -57,7 +57,7 @@ class MovieSpec extends FlatSpec with Matchers { def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - def builder(rows: Seq[Row]): Table[Movie] = TableWithoutHeader(rows) + def builder(rows: Seq[Movie], maybeHeader: Option[Header]): Table[Movie] = TableWithoutHeader(rows) } val movies = Seq( @@ -77,11 +77,11 @@ class MovieSpec extends FlatSpec with Matchers { def hasHeader: Boolean = true + def builder(rows: Seq[Movie], maybeHeader: Option[Header]): Table[Movie] = TableWithHeader(rows, maybeHeader.get) + override def forgiving: Boolean = false def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - - def builder(rows: Seq[Row]): Table[Movie] = TableWithoutHeader(rows) } val movies = Seq( @@ -90,7 +90,7 @@ class MovieSpec extends FlatSpec with Matchers { ) val mty = Table.parse(movies) - mty should matchPattern { case Success(TableWithoutHeader(_)) => } + mty should matchPattern { case Success(TableWithHeader(_, _)) => } mty.get.size shouldBe 1 } diff --git a/src/test/scala/com/phasmidsoftware/table/TableSpec.scala b/src/test/scala/com/phasmidsoftware/table/TableSpec.scala index bc8a848..980afdd 100644 --- a/src/test/scala/com/phasmidsoftware/table/TableSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/TableSpec.scala @@ -5,7 +5,7 @@ package com.phasmidsoftware.table import com.phasmidsoftware.parse.{RowParser, StringParser, StringTableParser} -import com.phasmidsoftware.render.{Renderer, Renderers, TreeWriter} +import com.phasmidsoftware.render._ import org.scalatest.{FlatSpec, Matchers} import scala.io.Source @@ -45,7 +45,7 @@ class TableSpec extends FlatSpec with Matchers { def rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] - def builder(rows: Seq[Row]): Table[IntPair] = TableWithoutHeader(rows) + def builder(rows: Seq[IntPair], maybeHeader: Option[Header]): Table[IntPair] = TableWithoutHeader(rows) } implicit object IntPairTableParser extends IntPairTableParser @@ -106,10 +106,6 @@ class TableSpec extends FlatSpec with Matchers { iIty.get.map(f).rows shouldBe Seq(IntPair(2, 4), IntPair(84, 198)) } - it should "$plus$plus" in { - // TODO implement this test - } - it should "flatMap" in { val f: IntPair => Table[IntPair] = p => TableWithoutHeader(Seq(p)) @@ -133,29 +129,39 @@ class TableSpec extends FlatSpec with Matchers { object IntPairHTML extends Renderers { trait HTMLTreeWriter extends TreeWriter[HTML] { - def addChild(parent: HTML, child: HTML): HTML = parent match { - case HTML(t, co, as, hs) => HTML(t, co, as, hs :+ child) - } - - def node(tag: String, content: Option[String], attributes: Map[String, String], children: Seq[HTML]): HTML = - HTML(tag, content, attributes, children) + def evaluate(node: Node): HTML = HTML(node.style, node.content map (_.toString), node.attributes, node.children map evaluate) } implicit object HTMLTreeWriter extends HTMLTreeWriter implicit val intPairRenderer: Renderer[IntPair] = renderer2("IntPair")(IntPair.apply) - implicit val r: Renderer[Indexed[IntPair]] = indexedRenderer("", "th", Map()) + implicit val r: Renderer[Indexed[IntPair]] = indexedRenderer("", "th") + + } + + it should "render the parsed table to CSV" in { + import IntPair._ + val iIty: Try[Table[IntPair]] = Table.parse(Seq("1 2", "42 99")) + implicit object StringBuilderWriteable extends Writable[StringBuilder] { + override def unit: StringBuilder = new StringBuilder + + override def delimiter: CharSequence = "|" + + override def writeRaw(o: StringBuilder)(x: CharSequence): StringBuilder = o.append(x.toString) + } + val sy = iIty map (_.render) + sy should matchPattern { case Success(_) => } + sy.get.toString shouldBe "1|2\n42|99\n" } - // FIXME - it should "render the parsed table" in { + it should "render the parsed table with TreeWriter" in { import IntPair._ val iIty = Table.parse(Seq("1 2", "42 99")) import IntPairHTML._ val hy = iIty map (_.render("table", Map())) hy should matchPattern { case Success(_) => } - // hy.get shouldBe HTML("table", None, Map(), List(HTML("span",None,Map(),List(HTML("IntPair", None, Map.empty, List(HTML("", Some("1"), Map("name" -> "a"), List()), HTML("", Some("2"), Map("name" -> "b"), List()))), HTML("IntPair", None, Map(), List(HTML("", Some("42"), Map("name" -> "a"), List()), HTML("", Some("99"), Map("name" -> "b"), List()))))))) + hy.get shouldBe HTML("table", None, Map(), List(HTML("",None,Map(),List()), HTML("tbody",None,Map(),List(HTML("IntPair",None,Map(),List(HTML("",Some("1"),Map("name" -> "a"),List()), HTML("",Some("2"),Map("name" -> "b"),List()))), HTML("IntPair",None,Map(),List(HTML("",Some("42"),Map("name" -> "a"),List()), HTML("",Some("99"),Map("name" -> "b"),List()))))))) }