collections/app/model/Node.scala (41 lines of code) (raw):
package model
import play.api.libs.functional.syntax._
import play.api.libs.json._
// TODO: Convert fullPath to NonEmptylist
case class Node[T](basename: String, children: List[Node[T]], fullPath: List[String], correctPath: List[String], data: Option[T]) {
// This is a hackmap that should map from T => V
def hackmap[V](f: Node[T] => Node[V]): Node[V] = {
val newNode = f(this)
val newChilds = children.map(child => child.hackmap(f))
newNode.copy(children = newChilds)
}
def toList(acc: List[T] = Nil): List[T] = {
val dataList = data map (List(_)) getOrElse Nil
val dataChilds = children.flatMap(_.toList(dataList))
dataList ++ dataChilds
}
}
object Node {
def fromList[T](list: List[T], getPath: T => List[String], getCorrectPathBit: T => String): Node[T] = {
// returns children for a given path
def loop(ts: List[T], fullPath: List[String], correctPath: List[String]): List[Node[T]] = {
ts
// group by slug at current level
.groupBy(getPath(_).drop(fullPath.size).head)
.toList
.map { case (currentSlug, tsWithThisSlug) =>
// separate `T` at this level from its children
val (thisLevel, children) = tsWithThisSlug.partition { t =>
getPath(t).size == fullPath.size + 1
}
val thisFullPath = fullPath :+ currentSlug
val thisCorrectPath = thisLevel.headOption.map(getCorrectPathBit).map(c => correctPath :+ c).getOrElse(Nil)
// use the T at this level or an empty node to hold children
Node(currentSlug, loop(children, thisFullPath, thisCorrectPath), thisFullPath, thisCorrectPath, thisLevel.headOption)
}
.sortBy(node => (node.children.isEmpty, node.basename))
}
Node[T]("root", loop(list, Nil, Nil), Nil, Nil, None)
}
implicit def nodeFormat[T: Format]: Format[Node[T]] = (
(__ \ "basename").format[String] ~
(__ \ "children").lazyFormat(Reads.list(nodeFormat[T]), Writes.list(nodeFormat[T])) ~
(__ \ "fullPath").format[List[String]] ~
(__ \ "correctPath").format[List[String]] ~
(__ \ "data").formatNullable[T]
)(Node.apply, unlift(Node.unapply))
}