package xsbt
package datatype
import java.io.File
import sbt.IO.readLines
import Function.tupled
import java.util.regex.Pattern
class DatatypeParser extends NotNull
{
val WhitespacePattern = Pattern compile """\s*"""
val EnumPattern = Pattern compile """enum\s+(\S+)\s*:\s*(.+)"""
val ClassPattern = Pattern compile """(\t*)(\S+)\s*"""
val MemberPattern = Pattern compile """(\t*)(\S+)\s*:\s*([^\s*]+)([*]?)"""
def processWhitespaceLine(l: Array[String], line: Int) = new WhitespaceLine(l.mkString, line)
def processEnumLine(l: Array[String], line: Int) = new EnumLine(l(0), l(1).split(",").map(_.trim), line)
def processClassLine(l: Array[String], line: Int) = new ClassLine(l(1), l(0).length, line)
def processMemberLine(l: Array[String], line: Int) = new MemberLine(l(1), l(2), l(3).isEmpty, l(0).length, line)
def error(l: Line, msg: String): Nothing = error(l.line, msg)
def error(line: Int, msg: String): Nothing = throw new RuntimeException("{line " + line + "} " + msg)
def parseFile(file: File): Seq[Definition] =
{
val (open, closed) = ( (Array[ClassDef](), List[Definition]()) /: parseLines(file) ) {
case ((open, defs), line) => processLine(open, defs, line)
}
open ++ closed
}
def parseLines(file: File): Seq[Line] = readLines(file).zipWithIndex.map(tupled(parseLine))
def parseLine(line: String, lineNumber: Int): Line =
matchPattern(WhitespacePattern -> processWhitespaceLine _, EnumPattern -> processEnumLine _,
ClassPattern -> processClassLine _, MemberPattern -> processMemberLine _)(line, lineNumber)
type Handler = (Array[String], Int) => Line
def matchPattern(patterns: (Pattern, Handler)*)(line: String, lineNumber: Int): Line =
patterns.flatMap { case (pattern, f) => matchPattern(pattern, f)(line, lineNumber) }.headOption.getOrElse {
error(lineNumber, "Invalid line, expected enum, class, or member definition")
}
def matchPattern(pattern: Pattern, f: Handler)(line: String, lineNumber: Int): Option[Line] =
{
val matcher = pattern.matcher(line)
if(matcher.matches)
{
val count = matcher.groupCount
val groups = (for(i <- 1 to count) yield matcher.group(i)).toArray[String]
Some( f(groups, lineNumber) )
}
else
None
}
def processLine(open: Array[ClassDef], definitions: List[Definition], line: Line): (Array[ClassDef], List[Definition]) =
{
line match
{
case w: WhitespaceLine => (open, definitions)
case e: EnumLine => (Array(), new EnumDef(e.name, e.members) :: open.toList ::: definitions)
case m: MemberLine =>
if(m.level == 0 || m.level > open.length) error(m, "Member must be declared in a class definition")
else withCurrent(open, definitions, m.level) { c => List( c + m) }
case c: ClassLine =>
if(c.level == 0) (Array( new ClassDef(c.name, None, Nil) ), open.toList ::: definitions)
else if(c.level > open.length) error(c, "Class must be declared as top level or as a subclass")
else withCurrent(open, definitions, c.level) { p => p :: new ClassDef(c.name, Some(p), Nil) :: Nil}
}
}
private def withCurrent(open: Array[ClassDef], definitions: List[Definition], level: Int)(onCurrent: ClassDef => Seq[ClassDef]): (Array[ClassDef], List[Definition]) =
{
require(0 < level && level <= open.length)
val closed = open.drop(level).toList
val newOpen = open.take(level - 1) ++ onCurrent(open(level - 1))
( newOpen.toArray, closed ::: definitions )
}
}