All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.backuity.matchete.Formatter.scala Maven / Gradle / Ivy

/*
 * Copyright 2013 Bruno Bieth
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.backuity.matchete

import org.backuity.matchete.Diffable.{BasicDiff, NestedDiff, SomeDiff}

import scala.collection.{SortedSet, SortedMap}

trait Formatter[-T] {
  def format(t : T) : String
}

object Formatter extends UnorderedMapFormatter {
  def apply[T](f : T => String) : Formatter[T] = new Formatter[T] {
    def format(t: T ) : String = f(t)
  }

  implicit val stringFormatter : Formatter[String] = Formatter[String]{ s => s"'$s'" }

  implicit def arrayFormatter[T] : Formatter[Array[T]] = Formatter[Array[T]]{ _.toSeq.toString }

  implicit def orderedMapFormatter[K : Formatter : Ordering,V : Formatter] : Formatter[Map[K,V]] = new Formatter[Map[K, V]] {
    override def format(t: Map[K, V]): String = {
      val tupleFormatter : Formatter[(K,V)] = Formatter[(K,V)]( kv => formatI(kv._1) + " -> " + formatI(kv._2))
      traversableFormatter[(K,V)](tupleFormatter).format(SortedMap(t.toSeq : _*))
    }
  }

  // This cannot be implemented like that due to https://issues.scala-lang.org/browse/SI-2509
  //
  // To sum it up: scala ranks implicits by specificity which takes variance into account, a subtype wrt variance
  //               being more specific than its supertype. So for contravariant types (such as Formatter) the specificity
  //               decreases as you go deeper in the type hierarchy.
  //               Unfortunately the 'LowPriorityImplicit' trick does not get us out of this situation as it only decreases
  //               the specificity by one (so to speak), which makes your implicits ambiguous (same specificity).
  //
  // A fix would be to make Formatter invariant and implement them with implicit macros
  //
//  implicit def orderedSetFormatter[T : Formatter : Ordering] : Formatter[Set[T]] = new Formatter[Set[T]] {
//    override def format(t: Set[T]): String = {
//      traversableFormatter[T].format(SortedSet(t.toSeq : _*))
//    }
//  }

  implicit def diffFormatter[T](implicit formatter: Formatter[T]) : Formatter[SomeDiff[T]] = new Formatter[SomeDiff[T]] {
    override def format(diff: SomeDiff[T]): String = {

      val formattedArgA = formatter.format(diff.sourceA)
      val formattedArgB = formatter.format(diff.sourceB)

      val widthA = Formatter.width(formattedArgA)
      val widthB = Formatter.width(formattedArgB)

      val formattedArguments = if( (widthA + widthB) > 80 ) {
        s"\n${indent(formattedArgA, 2)}\n\nis not equal to\n\n${indent(formattedArgB,2)}\n"
      } else {
        s"$formattedArgA is not equal to $formattedArgB"
      }

      val reasonsString = if( diff.reasons.isEmpty ) "" else {
        "\nReasons:\n * " + diff.reasons.map( r => indent(r, 3, firstLine = false)).mkString("\n * ")
      }

      diff match {
        case _ : BasicDiff[T] =>
          s"$formattedArguments$reasonsString"

        case nestedDiff : NestedDiff[T] =>
          val indentLevel = 10 + nestedDiff.path.length + 3

          s"""$formattedArguments
             |Got     : ${indent(nestedDiff.pathValueA,indentLevel,firstLine = false)}
             |Expected: ${indent(nestedDiff.pathValueB,indentLevel,firstLine = false)}$reasonsString""".stripMargin
      }
    }
  }

  implicit def tuple2Formatter[A : Formatter,B : Formatter] : Formatter[(A,B)] = new Formatter[(A, B)] {
    override def format(t: (A, B)): String = s"(${formatI(t._1)},${formatI(t._2)})"
    override def toString = "tuple2"
  }
  implicit def tuple3Formatter[A : Formatter,B : Formatter,C : Formatter] : Formatter[(A,B,C)] = new Formatter[(A,B,C)] {
    override def format(t: (A,B,C)): String = s"(${formatI(t._1)},${formatI(t._2)},${formatI(t._3)})"
  }
  implicit def tuple4Formatter[A : Formatter,B : Formatter,C : Formatter,D : Formatter] : Formatter[(A,B,C,D)] = new Formatter[(A,B,C,D)] {
    override def format(t: (A,B,C,D)): String = s"(${formatI(t._1)},${formatI(t._2)},${formatI(t._3)},${formatI(t._4)})"
  }

  /** format implicitly */
  @inline def formatI[T : Formatter](t : T) : String = implicitly[Formatter[T]].format(t)

  def width(string: String) : Int = {
    string.split('\n').map(_.length).max
  }

  def indent(str: String, space: Int, firstLine: Boolean = true) : String = {
    val spaces = " " * space
    (if( firstLine ) spaces else "") + str.split('\n').mkString("\n" + spaces)
  }
}

trait UnorderedMapFormatter extends FormatterLowPriorityImplicits {

  import Formatter.formatI

  // ambiguous with orderedMapFormatter
  implicit def mapFormatter[K : Formatter,V : Formatter] : Formatter[Map[K,V]] = new Formatter[Map[K, V]] {
    override def format(t: Map[K, V]): String = {
      val tupleFormatter : Formatter[(K,V)] = Formatter[(K,V)]( kv => formatI(kv._1) + " -> " + formatI(kv._2))
      traversableFormatter[(K,V)](tupleFormatter).format(t)
    }
  }
}

trait FormatterLowPriorityImplicits {

  import Formatter.indent

  /** print 'null' if the value is null */
  implicit def anyFormatter[T] : Formatter[T] = Formatter[T]{ _.toString }

  def traversableContentFormatter[T : Formatter] : Formatter[Traversable[T]] = new TraversableFormatter[T](printCollectionName = false)

  // ambiguous with mapFormatter
  implicit def traversableFormatter[T : Formatter] : Formatter[Traversable[T]] = new TraversableFormatter[T](printCollectionName = true)

  class TraversableFormatter[T](printCollectionName: Boolean)(implicit formatter: Formatter[T]) extends Formatter[Traversable[T]] {
    override def format(t: Traversable[T]): String = {
      val formattedElements = t.map(elem => formatter.format(elem))
      val formattedElementsString = formattedElements.mkString(", ")
      val prefix = t match {
        case _: List[_] => "List"
        case _: Seq[_] => "Seq"
        case _: Set[_] => "Set"
        case _: Map[_,_] => "Map"
        case _     => "Traversable"
      }
      val draft = if( printCollectionName ) {
        s"$prefix($formattedElementsString)"
      } else {
        formattedElementsString
      }

      if( Formatter.width(draft) < 100 ) {
        draft
      } else {
        // multiline display
        val formattedElementsString = formattedElements.mkString(",\n")
        val oneElementPerLine = if( printCollectionName ) {
          prefix + "(\n" + indent(formattedElementsString, 2) + ")"
        } else {
          formattedElementsString
        }
        oneElementPerLine
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy